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 {number=} maxItemsHeight
56 WebInspector.SuggestBox = function(suggestBoxDelegate, maxItemsHeight)
58 this._suggestBoxDelegate = suggestBoxDelegate;
60 this._selectedIndex = -1;
61 this._selectedElement = null;
62 this._maxItemsHeight = maxItemsHeight;
63 this._bodyElement = document.body;
64 this._maybeHideBound = this._maybeHide.bind(this);
65 this._element = document.createElementWithClass("div", "suggest-box");
66 this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
69 WebInspector.SuggestBox.prototype = {
75 return !!this._element.parentElement;
79 * @param {!AnchorBox} anchorBox
81 setPosition: function(anchorBox)
83 this._updateBoxPosition(anchorBox);
87 * @param {!AnchorBox} anchorBox
89 _updateBoxPosition: function(anchorBox)
91 console.assert(this._overlay);
92 if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox))
94 this._lastAnchorBox = anchorBox;
96 // Position relative to main DevTools element.
97 var container = WebInspector.Dialog.modalHostView().element;
98 anchorBox = anchorBox.relativeToElement(container);
99 var totalWidth = container.offsetWidth;
100 var totalHeight = container.offsetHeight;
101 var aboveHeight = anchorBox.y;
102 var underHeight = totalHeight - anchorBox.y - anchorBox.height;
107 var maxHeight = this._maxItemsHeight ? this._maxItemsHeight * rowHeight : Math.max(underHeight, aboveHeight) - spacer;
108 var under = underHeight >= aboveHeight;
109 this._leftSpacerElement.style.flexBasis = anchorBox.x + "px";
111 this._overlay.element.classList.toggle("under-anchor", under);
114 this._bottomSpacerElement.style.flexBasis = "auto";
115 this._topSpacerElement.style.flexBasis = (anchorBox.y + anchorBox.height) + "px";
117 this._bottomSpacerElement.style.flexBasis = (totalHeight - anchorBox.y) + "px";
118 this._topSpacerElement.style.flexBasis = "auto";
120 this._element.style.maxHeight = maxHeight + "px";
124 * @param {!Event} event
126 _onBoxMouseDown: function(event)
128 if (this._hideTimeoutId) {
129 window.clearTimeout(this._hideTimeoutId);
130 delete this._hideTimeoutId;
132 event.preventDefault();
135 _maybeHide: function()
137 if (!this._hideTimeoutId)
138 this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0);
145 this._overlay = new WebInspector.SuggestBox.Overlay();
146 this._bodyElement.addEventListener("mousedown", this._maybeHideBound, true);
148 this._leftSpacerElement = this._overlay.element.createChild("div", "suggest-box-left-spacer");
149 this._horizontalElement = this._overlay.element.createChild("div", "suggest-box-horizontal");
150 this._topSpacerElement = this._horizontalElement.createChild("div", "suggest-box-top-spacer");
151 this._horizontalElement.appendChild(this._element);
152 this._bottomSpacerElement = this._horizontalElement.createChild("div", "suggest-box-bottom-spacer");
160 this._bodyElement.removeEventListener("mousedown", this._maybeHideBound, true);
161 this._element.remove();
162 this._overlay.dispose();
163 delete this._overlay;
164 delete this._selectedElement;
165 this._selectedIndex = -1;
166 delete this._lastAnchorBox;
169 removeFromElement: function()
175 * @param {boolean=} isIntermediateSuggestion
177 _applySuggestion: function(isIntermediateSuggestion)
179 if (!this.visible() || !this._selectedElement)
182 var suggestion = this._selectedElement.textContent;
186 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
193 acceptSuggestion: function()
195 var result = this._applySuggestion();
200 this._suggestBoxDelegate.acceptSuggestion();
206 * @param {number} shift
207 * @param {boolean=} isCircular
208 * @return {boolean} is changed
210 _selectClosest: function(shift, isCircular)
215 if (this._selectedIndex === -1 && shift < 0)
218 var index = this._selectedIndex + shift;
221 index = (this._length + index) % this._length;
223 index = Number.constrain(index, 0, this._length - 1);
225 this._selectItem(index, true);
226 this._applySuggestion(true);
231 * @param {!Event} event
233 _onItemMouseDown: function(event)
235 this._selectedElement = event.currentTarget;
236 this.acceptSuggestion();
241 * @param {string} prefix
242 * @param {string} text
244 _createItemElement: function(prefix, text)
246 var element = document.createElementWithClass("div", "suggest-box-content-item source-code");
247 element.tabIndex = -1;
248 if (prefix && prefix.length && !text.indexOf(prefix)) {
249 element.createChild("span", "prefix").textContent = prefix;
250 element.createChild("span", "suffix").textContent = text.substring(prefix.length);
252 element.createChild("span", "suffix").textContent = text;
254 element.createChild("span", "spacer");
255 element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false);
260 * @param {!Array.<string>} items
261 * @param {string} userEnteredText
263 _updateItems: function(items, userEnteredText)
265 this._length = items.length;
266 this._element.removeChildren();
267 delete this._selectedElement;
269 for (var i = 0; i < items.length; ++i) {
271 var currentItemElement = this._createItemElement(userEnteredText, item);
272 this._element.appendChild(currentItemElement);
277 * @param {number} index
278 * @param {boolean} scrollIntoView
280 _selectItem: function(index, scrollIntoView)
282 if (this._selectedElement)
283 this._selectedElement.classList.remove("selected");
285 this._selectedIndex = index;
289 this._selectedElement = this._element.children[index];
290 this._selectedElement.classList.add("selected");
293 this._selectedElement.scrollIntoViewIfNeeded(false);
297 * @param {!Array.<string>} completions
298 * @param {boolean} canShowForSingleItem
299 * @param {string} userEnteredText
301 _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
303 if (!completions || !completions.length)
306 if (completions.length > 1)
309 // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
310 return canShowForSingleItem && completions[0] !== userEnteredText;
313 _ensureRowCountPerViewport: function()
315 if (this._rowCountPerViewport)
317 if (!this._element.firstChild)
320 this._rowCountPerViewport = Math.floor(this._element.offsetHeight / this._element.firstChild.offsetHeight);
324 * @param {!AnchorBox} anchorBox
325 * @param {!Array.<string>} completions
326 * @param {number} selectedIndex
327 * @param {boolean} canShowForSingleItem
328 * @param {string} userEnteredText
330 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
332 if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
333 this._updateItems(completions, userEnteredText);
335 this._updateBoxPosition(anchorBox);
336 this._selectItem(selectedIndex, selectedIndex > 0);
337 delete this._rowCountPerViewport;
343 * @param {!KeyboardEvent} event
346 keyPressed: function(event)
348 switch (event.keyIdentifier) {
350 return this.upKeyPressed();
352 return this.downKeyPressed();
354 return this.pageUpKeyPressed();
356 return this.pageDownKeyPressed();
358 return this.enterKeyPressed();
366 upKeyPressed: function()
368 return this._selectClosest(-1, true);
374 downKeyPressed: function()
376 return this._selectClosest(1, true);
382 pageUpKeyPressed: function()
384 this._ensureRowCountPerViewport();
385 return this._selectClosest(-this._rowCountPerViewport, false);
391 pageDownKeyPressed: function()
393 this._ensureRowCountPerViewport();
394 return this._selectClosest(this._rowCountPerViewport, false);
400 enterKeyPressed: function()
402 var hasSelectedItem = !!this._selectedElement;
403 this.acceptSuggestion();
405 // Report the event as non-handled if there is no selected item,
406 // to commit the input or handle it otherwise.
407 return hasSelectedItem;
414 WebInspector.SuggestBox.Overlay = function()
416 this.element = document.createElementWithClass("div", "suggest-box-overlay");
418 document.body.appendChild(this.element);
421 WebInspector.SuggestBox.Overlay.prototype = {
424 var container = WebInspector.Dialog.modalHostView().element;
425 var containerBox = container.boxInWindow(container.ownerDocument.defaultView);
427 this.element.style.left = containerBox.x + "px";
428 this.element.style.top = containerBox.y + "px";
429 this.element.style.height = containerBox.height + "px";
430 this.element.style.width = containerBox.width + "px";
435 this.element.remove();