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 * @param {WebInspector.SuggestBoxDelegate} suggestBoxDelegate
54 * @param {Element} anchorElement
55 * @param {string=} className
56 * @param {number=} maxItemsHeight
58 WebInspector.SuggestBox = function(suggestBoxDelegate, anchorElement, className, maxItemsHeight)
60 this._suggestBoxDelegate = suggestBoxDelegate;
61 this._anchorElement = anchorElement;
63 this._selectedIndex = -1;
64 this._selectedElement = null;
65 this._maxItemsHeight = maxItemsHeight;
66 this._boundOnScroll = this._onScrollOrResize.bind(this, true);
67 this._boundOnResize = this._onScrollOrResize.bind(this, false);
68 window.addEventListener("scroll", this._boundOnScroll, true);
69 window.addEventListener("resize", this._boundOnResize, true);
71 this._bodyElement = anchorElement.ownerDocument.body;
72 this._element = anchorElement.ownerDocument.createElement("div");
73 this._element.className = "suggest-box " + (className || "");
74 this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
75 this.containerElement = this._element.createChild("div", "container");
76 this.contentElement = this.containerElement.createChild("div", "content");
79 WebInspector.SuggestBox.prototype = {
85 return !!this._element.parentElement;
89 * @param {boolean} isScroll
90 * @param {Event} event
92 _onScrollOrResize: function(isScroll, event)
94 if (isScroll && this._element.isAncestor(event.target) || !this.visible())
96 this._updateBoxPosition(this._anchorBox);
100 * @param {AnchorBox} anchorBox
102 setPosition: function(anchorBox)
104 this._updateBoxPosition(anchorBox);
108 * @param {AnchorBox=} anchorBox
110 _updateBoxPosition: function(anchorBox)
112 this._anchorBox = anchorBox;
113 anchorBox = anchorBox || this._anchorElement.boxInWindow(window);
115 // Measure the content element box.
116 this.contentElement.style.display = "inline-block";
117 document.body.appendChild(this.contentElement);
118 this.contentElement.positionAt(0, 0);
119 var contentWidth = this.contentElement.offsetWidth;
120 var contentHeight = this.contentElement.offsetHeight;
121 this.contentElement.style.display = "block";
122 this.containerElement.appendChild(this.contentElement);
125 const suggestBoxPaddingX = 21;
126 const suggestBoxPaddingY = 2;
128 var maxWidth = document.body.offsetWidth - anchorBox.x - spacer;
129 var width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
130 var paddedWidth = contentWidth + suggestBoxPaddingX;
131 var boxX = anchorBox.x;
132 if (width < paddedWidth) {
133 // Shift the suggest box to the left to accommodate the content without trimming to the BODY edge.
134 maxWidth = document.body.offsetWidth - spacer;
135 width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
136 boxX = document.body.offsetWidth - width;
140 var aboveHeight = anchorBox.y;
141 var underHeight = document.body.offsetHeight - anchorBox.y - anchorBox.height;
143 var maxHeight = this._maxItemsHeight ? contentHeight * this._maxItemsHeight / this._length : Math.max(underHeight, aboveHeight) - spacer;
144 var height = Math.min(contentHeight, maxHeight - suggestBoxPaddingY) + suggestBoxPaddingY;
145 if (underHeight >= aboveHeight) {
146 // Locate the suggest box under the anchorBox.
147 boxY = anchorBox.y + anchorBox.height;
148 this._element.removeStyleClass("above-anchor");
149 this._element.addStyleClass("under-anchor");
151 // Locate the suggest box above the anchorBox.
152 boxY = anchorBox.y - height;
153 this._element.removeStyleClass("under-anchor");
154 this._element.addStyleClass("above-anchor");
157 this._element.positionAt(boxX, boxY);
158 this._element.style.width = width + "px";
159 this._element.style.height = height + "px";
163 * @param {Event} event
165 _onBoxMouseDown: function(event)
167 event.preventDefault();
175 this._element.remove();
176 delete this._selectedElement;
179 removeFromElement: function()
181 window.removeEventListener("scroll", this._boundOnScroll, true);
182 window.removeEventListener("resize", this._boundOnResize, true);
187 * @param {string=} text
188 * @param {boolean=} isIntermediateSuggestion
190 _applySuggestion: function(text, isIntermediateSuggestion)
192 if (!this.visible() || !(text || this._selectedElement))
195 var suggestion = text || this._selectedElement.textContent;
199 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
204 * @param {string=} text
206 acceptSuggestion: function(text)
208 var result = this._applySuggestion(text, false);
213 this._suggestBoxDelegate.acceptSuggestion();
219 * @param {number} shift
220 * @param {boolean=} isCircular
221 * @return {boolean} is changed
223 _selectClosest: function(shift, isCircular)
228 var index = this._selectedIndex + shift;
231 index = (this._length + index) % this._length;
233 index = Number.constrain(index, 0, this._length - 1);
235 this._selectItem(index);
236 this._applySuggestion(undefined, true);
241 * @param {string} text
242 * @param {Event} event
244 _onItemMouseDown: function(text, event)
246 this.acceptSuggestion(text);
251 * @param {string} prefix
252 * @param {string} 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
275 * @param {string} userEnteredText
277 _updateItems: function(items, selectedIndex, userEnteredText)
279 this._length = items.length;
280 this.contentElement.removeChildren();
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
311 * @param {string} userEnteredText
313 _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
315 if (!completions || !completions.length)
318 if (completions.length > 1)
321 // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
322 return canShowForSingleItem && completions[0] !== userEnteredText;
325 _rememberRowCountPerViewport: function()
327 if (!this.contentElement.firstChild)
330 this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight);
334 * @param {AnchorBox} anchorBox
335 * @param {!Array.<string>} completions
336 * @param {number} selectedIndex
337 * @param {boolean} canShowForSingleItem
338 * @param {string} userEnteredText
340 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
342 if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
343 this._updateItems(completions, selectedIndex, userEnteredText);
344 this._updateBoxPosition(anchorBox);
346 this._bodyElement.appendChild(this._element);
347 this._rememberRowCountPerViewport();
353 * @param {KeyboardEvent} event
356 keyPressed: function(event)
358 switch (event.keyIdentifier) {
360 return this.upKeyPressed();
362 return this.downKeyPressed();
364 return this.pageUpKeyPressed();
366 return this.pageDownKeyPressed();
368 return this.enterKeyPressed();
376 upKeyPressed: function()
378 return this._selectClosest(-1, true);
384 downKeyPressed: function()
386 return this._selectClosest(1, true);
392 pageUpKeyPressed: function()
394 return this._selectClosest(-this._rowCountPerViewport, false);
400 pageDownKeyPressed: function()
402 return this._selectClosest(this._rowCountPerViewport, false);
408 enterKeyPressed: function()
410 var hasSelectedItem = !!this._selectedElement;
411 this.acceptSuggestion();
413 // Report the event as non-handled if there is no selected item,
414 // to commit the input or handle it otherwise.
415 return hasSelectedItem;