| /* |
| * Copyright (C) 2013 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * @interface |
| */ |
| WebInspector.SuggestBoxDelegate = function() |
| { |
| } |
| |
| WebInspector.SuggestBoxDelegate.prototype = { |
| /** |
| * @param {string} suggestion |
| * @param {boolean=} isIntermediateSuggestion |
| */ |
| applySuggestion: function(suggestion, isIntermediateSuggestion) { }, |
| |
| /** |
| * acceptSuggestion will be always called after call to applySuggestion with isIntermediateSuggestion being equal to false. |
| */ |
| acceptSuggestion: function() { }, |
| } |
| |
| /** |
| * @constructor |
| * @param {!WebInspector.SuggestBoxDelegate} suggestBoxDelegate |
| * @param {!Element} anchorElement |
| * @param {string=} className |
| * @param {number=} maxItemsHeight |
| */ |
| WebInspector.SuggestBox = function(suggestBoxDelegate, anchorElement, className, maxItemsHeight) |
| { |
| this._suggestBoxDelegate = suggestBoxDelegate; |
| this._anchorElement = anchorElement; |
| this._length = 0; |
| this._selectedIndex = -1; |
| this._selectedElement = null; |
| this._maxItemsHeight = maxItemsHeight; |
| this._boundOnScroll = this._onScrollOrResize.bind(this, true); |
| this._boundOnResize = this._onScrollOrResize.bind(this, false); |
| window.addEventListener("scroll", this._boundOnScroll, true); |
| window.addEventListener("resize", this._boundOnResize, true); |
| |
| this._bodyElement = anchorElement.ownerDocument.body; |
| this._element = anchorElement.ownerDocument.createElement("div"); |
| this._element.className = "suggest-box " + (className || ""); |
| this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true); |
| this.containerElement = this._element.createChild("div", "container"); |
| this.contentElement = this.containerElement.createChild("div", "content"); |
| } |
| |
| WebInspector.SuggestBox.prototype = { |
| /** |
| * @return {boolean} |
| */ |
| visible: function() |
| { |
| return !!this._element.parentElement; |
| }, |
| |
| /** |
| * @param {boolean} isScroll |
| * @param {?Event} event |
| */ |
| _onScrollOrResize: function(isScroll, event) |
| { |
| if (isScroll && this._element.isAncestor(event.target) || !this.visible()) |
| return; |
| this._updateBoxPosition(this._anchorBox); |
| }, |
| |
| /** |
| * @param {!AnchorBox} anchorBox |
| */ |
| setPosition: function(anchorBox) |
| { |
| this._updateBoxPosition(anchorBox); |
| }, |
| |
| /** |
| * @param {!AnchorBox=} anchorBox |
| */ |
| _updateBoxPosition: function(anchorBox) |
| { |
| this._anchorBox = anchorBox; |
| anchorBox = anchorBox || this._anchorElement.boxInWindow(window); |
| |
| // Measure the content element box. |
| this.contentElement.style.display = "inline-block"; |
| document.body.appendChild(this.contentElement); |
| this.contentElement.positionAt(0, 0); |
| var contentWidth = this.contentElement.offsetWidth; |
| var contentHeight = this.contentElement.offsetHeight; |
| this.contentElement.style.display = "block"; |
| this.containerElement.appendChild(this.contentElement); |
| |
| const spacer = 6; |
| const suggestBoxPaddingX = 21; |
| const suggestBoxPaddingY = 2; |
| |
| var maxWidth = document.body.offsetWidth - anchorBox.x - spacer; |
| var width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX; |
| var paddedWidth = contentWidth + suggestBoxPaddingX; |
| var boxX = anchorBox.x; |
| if (width < paddedWidth) { |
| // Shift the suggest box to the left to accommodate the content without trimming to the BODY edge. |
| maxWidth = document.body.offsetWidth - spacer; |
| width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX; |
| boxX = document.body.offsetWidth - width; |
| } |
| |
| var boxY; |
| var aboveHeight = anchorBox.y; |
| var underHeight = document.body.offsetHeight - anchorBox.y - anchorBox.height; |
| |
| var maxHeight = this._maxItemsHeight ? contentHeight * this._maxItemsHeight / this._length : Math.max(underHeight, aboveHeight) - spacer; |
| var height = Math.min(contentHeight, maxHeight - suggestBoxPaddingY) + suggestBoxPaddingY; |
| if (underHeight >= aboveHeight) { |
| // Locate the suggest box under the anchorBox. |
| boxY = anchorBox.y + anchorBox.height; |
| this._element.classList.remove("above-anchor"); |
| this._element.classList.add("under-anchor"); |
| } else { |
| // Locate the suggest box above the anchorBox. |
| boxY = anchorBox.y - height; |
| this._element.classList.remove("under-anchor"); |
| this._element.classList.add("above-anchor"); |
| } |
| |
| this._element.positionAt(boxX, boxY); |
| this._element.style.width = width + "px"; |
| this._element.style.height = height + "px"; |
| }, |
| |
| /** |
| * @param {?Event} event |
| */ |
| _onBoxMouseDown: function(event) |
| { |
| event.preventDefault(); |
| }, |
| |
| hide: function() |
| { |
| if (!this.visible()) |
| return; |
| |
| this._element.remove(); |
| delete this._selectedElement; |
| this._selectedIndex = -1; |
| }, |
| |
| removeFromElement: function() |
| { |
| window.removeEventListener("scroll", this._boundOnScroll, true); |
| window.removeEventListener("resize", this._boundOnResize, true); |
| this.hide(); |
| }, |
| |
| /** |
| * @param {string=} text |
| * @param {boolean=} isIntermediateSuggestion |
| */ |
| _applySuggestion: function(text, isIntermediateSuggestion) |
| { |
| if (!this.visible() || !(text || this._selectedElement)) |
| return false; |
| |
| var suggestion = text || this._selectedElement.textContent; |
| if (!suggestion) |
| return false; |
| |
| this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion); |
| return true; |
| }, |
| |
| /** |
| * @param {string=} text |
| * @return {boolean} |
| */ |
| acceptSuggestion: function(text) |
| { |
| var result = this._applySuggestion(text, false); |
| this.hide(); |
| if (!result) |
| return false; |
| |
| this._suggestBoxDelegate.acceptSuggestion(); |
| |
| return true; |
| }, |
| |
| /** |
| * @param {number} shift |
| * @param {boolean=} isCircular |
| * @return {boolean} is changed |
| */ |
| _selectClosest: function(shift, isCircular) |
| { |
| if (!this._length) |
| return false; |
| |
| if (this._selectedIndex === -1 && shift < 0) |
| shift += 1; |
| |
| var index = this._selectedIndex + shift; |
| |
| if (isCircular) |
| index = (this._length + index) % this._length; |
| else |
| index = Number.constrain(index, 0, this._length - 1); |
| |
| this._selectItem(index); |
| this._applySuggestion(undefined, true); |
| return true; |
| }, |
| |
| /** |
| * @param {string} text |
| * @param {?Event} event |
| */ |
| _onItemMouseDown: function(text, event) |
| { |
| this.acceptSuggestion(text); |
| event.consume(true); |
| }, |
| |
| /** |
| * @param {string} prefix |
| * @param {string} text |
| */ |
| _createItemElement: function(prefix, text) |
| { |
| var element = document.createElement("div"); |
| element.className = "suggest-box-content-item source-code"; |
| element.tabIndex = -1; |
| if (prefix && prefix.length && !text.indexOf(prefix)) { |
| var prefixElement = element.createChild("span", "prefix"); |
| prefixElement.textContent = prefix; |
| var suffixElement = element.createChild("span", "suffix"); |
| suffixElement.textContent = text.substring(prefix.length); |
| } else { |
| var suffixElement = element.createChild("span", "suffix"); |
| suffixElement.textContent = text; |
| } |
| element.addEventListener("mousedown", this._onItemMouseDown.bind(this, text), false); |
| return element; |
| }, |
| |
| /** |
| * @param {!Array.<string>} items |
| * @param {number} selectedIndex |
| * @param {string} userEnteredText |
| */ |
| _updateItems: function(items, selectedIndex, userEnteredText) |
| { |
| this._length = items.length; |
| this.contentElement.removeChildren(); |
| |
| for (var i = 0; i < items.length; ++i) { |
| var item = items[i]; |
| var currentItemElement = this._createItemElement(userEnteredText, item); |
| this.contentElement.appendChild(currentItemElement); |
| } |
| |
| this._selectedElement = null; |
| if (typeof selectedIndex === "number") |
| this._selectItem(selectedIndex); |
| }, |
| |
| /** |
| * @param {number} index |
| */ |
| _selectItem: function(index) |
| { |
| if (this._selectedElement) |
| this._selectedElement.classList.remove("selected"); |
| |
| this._selectedIndex = index; |
| if (index < 0) |
| return; |
| |
| this._selectedElement = this.contentElement.children[index]; |
| this._selectedElement.classList.add("selected"); |
| |
| this._selectedElement.scrollIntoViewIfNeeded(false); |
| }, |
| |
| /** |
| * @param {!Array.<string>} completions |
| * @param {boolean} canShowForSingleItem |
| * @param {string} userEnteredText |
| */ |
| _canShowBox: function(completions, canShowForSingleItem, userEnteredText) |
| { |
| if (!completions || !completions.length) |
| return false; |
| |
| if (completions.length > 1) |
| return true; |
| |
| // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes. |
| return canShowForSingleItem && completions[0] !== userEnteredText; |
| }, |
| |
| _rememberRowCountPerViewport: function() |
| { |
| if (!this.contentElement.firstChild) |
| return; |
| |
| this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight); |
| }, |
| |
| /** |
| * @param {!AnchorBox} anchorBox |
| * @param {!Array.<string>} completions |
| * @param {number} selectedIndex |
| * @param {boolean} canShowForSingleItem |
| * @param {string} userEnteredText |
| */ |
| updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText) |
| { |
| if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) { |
| this._updateItems(completions, selectedIndex, userEnteredText); |
| this._updateBoxPosition(anchorBox); |
| if (!this.visible()) |
| this._bodyElement.appendChild(this._element); |
| this._rememberRowCountPerViewport(); |
| } else |
| this.hide(); |
| }, |
| |
| /** |
| * @param {!KeyboardEvent} event |
| * @return {boolean} |
| */ |
| keyPressed: function(event) |
| { |
| switch (event.keyIdentifier) { |
| case "Up": |
| return this.upKeyPressed(); |
| case "Down": |
| return this.downKeyPressed(); |
| case "PageUp": |
| return this.pageUpKeyPressed(); |
| case "PageDown": |
| return this.pageDownKeyPressed(); |
| case "Enter": |
| return this.enterKeyPressed(); |
| } |
| return false; |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| upKeyPressed: function() |
| { |
| return this._selectClosest(-1, true); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| downKeyPressed: function() |
| { |
| return this._selectClosest(1, true); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| pageUpKeyPressed: function() |
| { |
| return this._selectClosest(-this._rowCountPerViewport, false); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| pageDownKeyPressed: function() |
| { |
| return this._selectClosest(this._rowCountPerViewport, false); |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| enterKeyPressed: function() |
| { |
| var hasSelectedItem = !!this._selectedElement; |
| this.acceptSuggestion(); |
| |
| // Report the event as non-handled if there is no selected item, |
| // to commit the input or handle it otherwise. |
| return hasSelectedItem; |
| } |
| } |