123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- /*
- * 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;
- // 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.removeStyleClass("above-anchor");
- this._element.addStyleClass("under-anchor");
- } else {
- // Locate the suggest box above the anchorBox.
- boxY = anchorBox.y - height;
- this._element.removeStyleClass("under-anchor");
- this._element.addStyleClass("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;
- },
- 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
- */
- 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;
- 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;
- 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;
- }
- }
|