2 * Copyright (C) 2013 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 WebInspector.SuggestBoxDelegate = function()
38 WebInspector.SuggestBoxDelegate.prototype = {
40 * @param {string} suggestion
41 * @param {boolean=} isIntermediateSuggestion
43 applySuggestion: function(suggestion, isIntermediateSuggestion) { },
46 * acceptSuggestion will be always called after call to applySuggestion with isIntermediateSuggestion being equal to false.
48 acceptSuggestion: function() { },
53 userEnteredText: function() { }
58 * @param {WebInspector.SuggestBoxDelegate} suggestBoxDelegate
59 * @param {Element} inputElement
60 * @param {string} className
62 WebInspector.SuggestBox = function(suggestBoxDelegate, inputElement, className)
64 this._suggestBoxDelegate = suggestBoxDelegate;
65 this._inputElement = inputElement;
67 this._selectedIndex = -1;
68 this._selectedElement = null;
69 this._boundOnScroll = this._onscrollresize.bind(this, true);
70 this._boundOnResize = this._onscrollresize.bind(this, false);
71 window.addEventListener("scroll", this._boundOnScroll, true);
72 window.addEventListener("resize", this._boundOnResize, true);
74 this._bodyElement = inputElement.ownerDocument.body;
75 this._element = inputElement.ownerDocument.createElement("div");
76 this._element.className = "suggest-box " + (className || "");
77 this._element.addEventListener("mousedown", this._onboxmousedown.bind(this), true);
78 this.containerElement = this._element.createChild("div", "container");
79 this.contentElement = this.containerElement.createChild("div", "content");
82 WebInspector.SuggestBox.prototype = {
85 return !!this._element.parentElement;
90 return !!this._selectedElement;
93 _onscrollresize: function(isScroll, event)
95 if (isScroll && this._element.isAncestor(event.target) || !this.visible)
97 this._updateBoxPositionWithExistingAnchor();
100 _updateBoxPositionWithExistingAnchor: function()
102 this._updateBoxPosition(this._anchorBox);
106 * @param {AnchorBox} anchorBox
108 _updateBoxPosition: function(anchorBox)
110 // Measure the content element box.
111 this.contentElement.style.display = "inline-block";
112 document.body.appendChild(this.contentElement);
113 this.contentElement.positionAt(0, 0);
114 var contentWidth = this.contentElement.offsetWidth;
115 var contentHeight = this.contentElement.offsetHeight;
116 this.contentElement.style.display = "block";
117 this.containerElement.appendChild(this.contentElement);
119 // Lay out the suggest-box relative to the anchorBox.
120 this._anchorBox = anchorBox;
123 const suggestBoxPaddingX = 21;
124 var maxWidth = document.body.offsetWidth - anchorBox.x - spacer;
125 var width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
126 var paddedWidth = contentWidth + suggestBoxPaddingX;
127 var boxX = anchorBox.x;
128 if (width < paddedWidth) {
129 // Shift the suggest box to the left to accommodate the content without trimming to the BODY edge.
130 maxWidth = document.body.offsetWidth - spacer;
131 width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
132 boxX = document.body.offsetWidth - width;
135 const suggestBoxPaddingY = 2;
137 var aboveHeight = anchorBox.y;
138 var underHeight = document.body.offsetHeight - anchorBox.y - anchorBox.height;
139 var maxHeight = Math.max(underHeight, aboveHeight) - spacer;
140 var height = Math.min(contentHeight, maxHeight - suggestBoxPaddingY) + suggestBoxPaddingY;
141 if (underHeight >= aboveHeight) {
142 // Locate the suggest box under the anchorBox.
143 boxY = anchorBox.y + anchorBox.height;
144 this._element.removeStyleClass("above-anchor");
145 this._element.addStyleClass("under-anchor");
147 // Locate the suggest box above the anchorBox.
148 boxY = anchorBox.y - height;
149 this._element.removeStyleClass("under-anchor");
150 this._element.addStyleClass("above-anchor");
153 this._element.positionAt(boxX, boxY);
154 this._element.style.width = width + "px";
155 this._element.style.height = height + "px";
158 _onboxmousedown: function(event)
160 event.preventDefault();
168 this._element.parentElement.removeChild(this._element);
169 delete this._selectedElement;
172 removeFromElement: function()
174 window.removeEventListener("scroll", this._boundOnScroll, true);
175 window.removeEventListener("resize", this._boundOnResize, true);
180 * @param {string=} text
181 * @param {boolean=} isIntermediateSuggestion
183 _applySuggestion: function(text, isIntermediateSuggestion)
185 if (!this.visible || !(text || this._selectedElement))
188 var suggestion = text || this._selectedElement.textContent;
192 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
197 * @param {string=} text
199 acceptSuggestion: function(text)
201 var result = this._applySuggestion(text, false);
206 this._suggestBoxDelegate.acceptSuggestion();
212 * @param {number} shift
213 * @param {boolean=} isCircular
214 * @return {boolean} is changed
216 _selectClosest: function(shift, isCircular)
221 var index = this._selectedIndex + shift;
224 index = (this._length + index) % this._length;
226 index = Number.constrain(index, 0, this._length - 1);
228 this._selectItem(index);
229 this._applySuggestion(undefined, true);
234 * @param {AnchorBox} anchorBox
235 * @param {Array.<string>=} completions
236 * @param {number=} selectedIndex
237 * @param {boolean=} canShowForSingleItem
239 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem)
241 if (this._suggestTimeout) {
242 clearTimeout(this._suggestTimeout);
243 delete this._suggestTimeout;
245 this._completionsReady(anchorBox, completions, selectedIndex, canShowForSingleItem);
248 _onItemMouseDown: function(text, event)
250 this.acceptSuggestion(text);
254 _createItemElement: function(prefix, text)
256 var element = document.createElement("div");
257 element.className = "suggest-box-content-item source-code";
258 element.tabIndex = -1;
259 if (prefix && prefix.length && !text.indexOf(prefix)) {
260 var prefixElement = element.createChild("span", "prefix");
261 prefixElement.textContent = prefix;
262 var suffixElement = element.createChild("span", "suffix");
263 suffixElement.textContent = text.substring(prefix.length);
265 var suffixElement = element.createChild("span", "suffix");
266 suffixElement.textContent = text;
268 element.addEventListener("mousedown", this._onItemMouseDown.bind(this, text), false);
273 * @param {Array.<string>=} items
274 * @param {number=} selectedIndex
276 _updateItems: function(items, selectedIndex)
278 this._length = items.length;
279 this.contentElement.removeChildren();
281 var userEnteredText = this._suggestBoxDelegate.userEnteredText();
282 for (var i = 0; i < items.length; ++i) {
284 var currentItemElement = this._createItemElement(userEnteredText, item);
285 this.contentElement.appendChild(currentItemElement);
288 this._selectedElement = null;
289 if (typeof selectedIndex === "number")
290 this._selectItem(selectedIndex);
294 * @param {number} index
296 _selectItem: function(index)
298 if (this._selectedElement)
299 this._selectedElement.classList.remove("selected");
301 this._selectedIndex = index;
302 this._selectedElement = this.contentElement.children[index];
303 this._selectedElement.classList.add("selected");
305 this._selectedElement.scrollIntoViewIfNeeded(false);
309 * @param {Array.<string>=} completions
310 * @param {boolean=} canShowForSingleItem
312 _canShowBox: function(completions, canShowForSingleItem)
314 if (!completions || !completions.length)
317 if (completions.length > 1)
320 // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
321 return canShowForSingleItem && completions[0] !== this._suggestBoxDelegate.userEnteredText();
324 _rememberRowCountPerViewport: function()
326 if (!this.contentElement.firstChild)
329 this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight);
333 * @param {AnchorBox} anchorBox
334 * @param {Array.<string>=} completions
335 * @param {number=} selectedIndex
336 * @param {boolean=} canShowForSingleItem
338 _completionsReady: function(anchorBox, completions, selectedIndex, canShowForSingleItem)
340 if (this._canShowBox(completions, canShowForSingleItem)) {
341 this._updateItems(completions, selectedIndex);
342 this._updateBoxPosition(anchorBox);
344 this._bodyElement.appendChild(this._element);
345 this._rememberRowCountPerViewport();
353 upKeyPressed: function()
355 return this._selectClosest(-1, true);
361 downKeyPressed: function()
363 return this._selectClosest(1, true);
369 pageUpKeyPressed: function()
371 return this._selectClosest(-this._rowCountPerViewport, false);
377 pageDownKeyPressed: function()
379 return this._selectClosest(this._rowCountPerViewport, false);
385 enterKeyPressed: function()
387 var hasSelectedItem = !!this._selectedElement;
388 this.acceptSuggestion();
390 // Report the event as non-handled if there is no selected item,
391 // to commit the input or handle it otherwise.
392 return hasSelectedItem;
398 tabKeyPressed: function()
400 return this.enterKeyPressed();