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._bodyElement = anchorElement.ownerDocument.body;
67 this._element = anchorElement.ownerDocument.createElement("div");
68 this._element.className = "suggest-box " + (className || "");
69 this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
70 this.containerElement = this._element.createChild("div", "container");
71 this.contentElement = this.containerElement.createChild("div", "content");
74 WebInspector.SuggestBox.prototype = {
80 return !!this._element.parentElement;
84 * @param {!AnchorBox} anchorBox
86 setPosition: function(anchorBox)
88 this._updateBoxPosition(anchorBox);
92 * @param {?AnchorBox=} anchorBox
94 _updateBoxPosition: function(anchorBox)
96 this._anchorBox = anchorBox;
97 anchorBox = anchorBox || this._anchorElement.boxInWindow(window);
99 // Position relative to main DevTools element.
100 var container = WebInspector.Dialog.modalHostView().element;
101 anchorBox = anchorBox.relativeToElement(container);
102 var totalWidth = container.offsetWidth;
103 var totalHeight = container.offsetHeight;
105 // Measure the content element box.
106 this.contentElement.style.display = "inline-block";
107 document.body.appendChild(this.contentElement);
108 this.contentElement.positionAt(0, 0);
109 var contentWidth = this.contentElement.offsetWidth;
110 var contentHeight = this.contentElement.offsetHeight;
111 this.contentElement.style.display = "block";
112 this.containerElement.appendChild(this.contentElement);
115 const suggestBoxPaddingX = 21;
116 const suggestBoxPaddingY = 2;
118 var maxWidth = totalWidth - anchorBox.x - spacer;
119 var width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
120 var paddedWidth = contentWidth + suggestBoxPaddingX;
121 var boxX = anchorBox.x;
122 if (width < paddedWidth) {
123 // Shift the suggest box to the left to accommodate the content without trimming to the container edge.
124 maxWidth = totalWidth - spacer;
125 width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
126 boxX = totalWidth - width;
130 var aboveHeight = anchorBox.y;
131 var underHeight = totalHeight - anchorBox.y - anchorBox.height;
133 var maxHeight = this._maxItemsHeight ? contentHeight * this._maxItemsHeight / this._length : Math.max(underHeight, aboveHeight) - spacer;
134 var height = Math.min(contentHeight, maxHeight - suggestBoxPaddingY) + suggestBoxPaddingY;
135 if (underHeight >= aboveHeight) {
136 // Locate the suggest box under the anchorBox.
137 boxY = anchorBox.y + anchorBox.height;
138 this._element.classList.remove("above-anchor");
139 this._element.classList.add("under-anchor");
141 // Locate the suggest box above the anchorBox.
142 boxY = anchorBox.y - height;
143 this._element.classList.remove("under-anchor");
144 this._element.classList.add("above-anchor");
147 this._element.positionAt(boxX, boxY, container);
148 this._element.style.width = width + "px";
149 this._element.style.height = height + "px";
153 * @param {?Event} event
155 _onBoxMouseDown: function(event)
157 event.preventDefault();
165 this._element.remove();
166 delete this._selectedElement;
167 this._selectedIndex = -1;
170 removeFromElement: function()
176 * @param {boolean=} isIntermediateSuggestion
178 _applySuggestion: function(isIntermediateSuggestion)
180 if (!this.visible() || !this._selectedElement)
183 var suggestion = this._selectedElement.textContent;
187 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
194 acceptSuggestion: function()
196 var result = this._applySuggestion();
201 this._suggestBoxDelegate.acceptSuggestion();
207 * @param {number} shift
208 * @param {boolean=} isCircular
209 * @return {boolean} is changed
211 _selectClosest: function(shift, isCircular)
216 if (this._selectedIndex === -1 && shift < 0)
219 var index = this._selectedIndex + shift;
222 index = (this._length + index) % this._length;
224 index = Number.constrain(index, 0, this._length - 1);
226 this._selectItem(index);
227 this._applySuggestion(true);
232 * @param {?Event} event
234 _onItemMouseDown: function(event)
236 this._selectedElement = event.currentTarget;
237 this.acceptSuggestion();
242 * @param {string} prefix
243 * @param {string} text
245 _createItemElement: function(prefix, text)
247 var element = document.createElement("div");
248 element.className = "suggest-box-content-item source-code";
249 element.tabIndex = -1;
250 if (prefix && prefix.length && !text.indexOf(prefix)) {
251 var prefixElement = element.createChild("span", "prefix");
252 prefixElement.textContent = prefix;
253 var suffixElement = element.createChild("span", "suffix");
254 suffixElement.textContent = text.substring(prefix.length);
256 var suffixElement = element.createChild("span", "suffix");
257 suffixElement.textContent = text;
259 element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false);
264 * @param {!Array.<string>} items
265 * @param {number} selectedIndex
266 * @param {string} userEnteredText
268 _updateItems: function(items, selectedIndex, userEnteredText)
270 this._length = items.length;
271 this.contentElement.removeChildren();
273 for (var i = 0; i < items.length; ++i) {
275 var currentItemElement = this._createItemElement(userEnteredText, item);
276 this.contentElement.appendChild(currentItemElement);
279 this._selectedElement = null;
280 if (typeof selectedIndex === "number")
281 this._selectItem(selectedIndex);
285 * @param {number} index
287 _selectItem: function(index)
289 if (this._selectedElement)
290 this._selectedElement.classList.remove("selected");
292 this._selectedIndex = index;
296 this._selectedElement = this.contentElement.children[index];
297 this._selectedElement.classList.add("selected");
299 this._selectedElement.scrollIntoViewIfNeeded(false);
303 * @param {!Array.<string>} completions
304 * @param {boolean} canShowForSingleItem
305 * @param {string} userEnteredText
307 _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
309 if (!completions || !completions.length)
312 if (completions.length > 1)
315 // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
316 return canShowForSingleItem && completions[0] !== userEnteredText;
319 _rememberRowCountPerViewport: function()
321 if (!this.contentElement.firstChild)
324 this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight);
328 * @param {?AnchorBox} anchorBox
329 * @param {!Array.<string>} completions
330 * @param {number} selectedIndex
331 * @param {boolean} canShowForSingleItem
332 * @param {string} userEnteredText
334 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
336 if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
337 this._updateItems(completions, selectedIndex, userEnteredText);
338 this._updateBoxPosition(anchorBox);
340 this._bodyElement.appendChild(this._element);
341 this._rememberRowCountPerViewport();
347 * @param {!KeyboardEvent} event
350 keyPressed: function(event)
352 switch (event.keyIdentifier) {
354 return this.upKeyPressed();
356 return this.downKeyPressed();
358 return this.pageUpKeyPressed();
360 return this.pageDownKeyPressed();
362 return this.enterKeyPressed();
370 upKeyPressed: function()
372 return this._selectClosest(-1, true);
378 downKeyPressed: function()
380 return this._selectClosest(1, true);
386 pageUpKeyPressed: function()
388 return this._selectClosest(-this._rowCountPerViewport, false);
394 pageDownKeyPressed: function()
396 return this._selectClosest(this._rowCountPerViewport, false);
402 enterKeyPressed: function()
404 var hasSelectedItem = !!this._selectedElement;
405 this.acceptSuggestion();
407 // Report the event as non-handled if there is no selected item,
408 // to commit the input or handle it otherwise.
409 return hasSelectedItem;