Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / ui / SuggestBox.js
blob32b3c52282616f84ecb7df1250dd9182739a72a5
1 /*
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
6 * met:
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
13 * distribution.
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.
31 /**
32 * @interface
34 WebInspector.SuggestBoxDelegate = function()
38 WebInspector.SuggestBoxDelegate.prototype = {
39 /**
40 * @param {string} suggestion
41 * @param {boolean=} isIntermediateSuggestion
43 applySuggestion: function(suggestion, isIntermediateSuggestion) { },
45 /**
46 * acceptSuggestion will be always called after call to applySuggestion with isIntermediateSuggestion being equal to false.
48 acceptSuggestion: function() { },
51 /**
52 * @constructor
53 * @param {!WebInspector.SuggestBoxDelegate} suggestBoxDelegate
54 * @param {number=} maxItemsHeight
56 WebInspector.SuggestBox = function(suggestBoxDelegate, maxItemsHeight)
58 this._suggestBoxDelegate = suggestBoxDelegate;
59 this._length = 0;
60 this._selectedIndex = -1;
61 this._selectedElement = null;
62 this._maxItemsHeight = maxItemsHeight;
63 this._maybeHideBound = this._maybeHide.bind(this);
64 this._element = createElementWithClass("div", "suggest-box");
65 this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
68 WebInspector.SuggestBox.prototype = {
69 /**
70 * @return {boolean}
72 visible: function()
74 return !!this._element.parentElement;
77 /**
78 * @param {!AnchorBox} anchorBox
80 setPosition: function(anchorBox)
82 this._updateBoxPosition(anchorBox);
85 /**
86 * @param {!AnchorBox} anchorBox
88 _updateBoxPosition: function(anchorBox)
90 console.assert(this._overlay);
91 if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox))
92 return;
93 this._lastAnchorBox = anchorBox;
95 // Position relative to main DevTools element.
96 var container = WebInspector.Dialog.modalHostView().element;
97 anchorBox = anchorBox.relativeToElement(container);
98 var totalHeight = container.offsetHeight;
99 var aboveHeight = anchorBox.y;
100 var underHeight = totalHeight - anchorBox.y - anchorBox.height;
102 this._overlay.setLeftOffset(anchorBox.x);
104 var under = underHeight >= aboveHeight;
105 if (under)
106 this._overlay.setVerticalOffset(anchorBox.y + anchorBox.height, true);
107 else
108 this._overlay.setVerticalOffset(totalHeight - anchorBox.y, false);
110 /** const */ var rowHeight = 17;
111 /** const */ var spacer = 6;
112 var maxHeight = this._maxItemsHeight ? this._maxItemsHeight * rowHeight : Math.max(underHeight, aboveHeight) - spacer;
113 this._element.style.maxHeight = maxHeight + "px";
117 * @param {!Event} event
119 _onBoxMouseDown: function(event)
121 if (this._hideTimeoutId) {
122 window.clearTimeout(this._hideTimeoutId);
123 delete this._hideTimeoutId;
125 event.preventDefault();
128 _maybeHide: function()
130 if (!this._hideTimeoutId)
131 this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0);
135 * // FIXME: make SuggestBox work for multiple documents.
136 * @suppressGlobalPropertiesCheck
138 _show: function()
140 if (this.visible())
141 return;
142 this._bodyElement = document.body;
143 this._bodyElement.addEventListener("mousedown", this._maybeHideBound, true);
144 this._overlay = new WebInspector.SuggestBox.Overlay();
145 this._overlay.setContentElement(this._element);
148 hide: function()
150 if (!this.visible())
151 return;
153 this._bodyElement.removeEventListener("mousedown", this._maybeHideBound, true);
154 delete this._bodyElement;
155 this._element.remove();
156 this._overlay.dispose();
157 delete this._overlay;
158 delete this._selectedElement;
159 this._selectedIndex = -1;
160 delete this._lastAnchorBox;
163 removeFromElement: function()
165 this.hide();
169 * @param {boolean=} isIntermediateSuggestion
171 _applySuggestion: function(isIntermediateSuggestion)
173 if (!this.visible() || !this._selectedElement)
174 return false;
176 var suggestion = this._selectedElement.textContent;
177 if (!suggestion)
178 return false;
180 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
181 return true;
185 * @return {boolean}
187 acceptSuggestion: function()
189 var result = this._applySuggestion();
190 this.hide();
191 if (!result)
192 return false;
194 this._suggestBoxDelegate.acceptSuggestion();
196 return true;
200 * @param {number} shift
201 * @param {boolean=} isCircular
202 * @return {boolean} is changed
204 _selectClosest: function(shift, isCircular)
206 if (!this._length)
207 return false;
209 if (this._selectedIndex === -1 && shift < 0)
210 shift += 1;
212 var index = this._selectedIndex + shift;
214 if (isCircular)
215 index = (this._length + index) % this._length;
216 else
217 index = Number.constrain(index, 0, this._length - 1);
219 this._selectItem(index, true);
220 this._applySuggestion(true);
221 return true;
225 * @param {!Event} event
227 _onItemMouseDown: function(event)
229 this._selectedElement = event.currentTarget;
230 this.acceptSuggestion();
231 event.consume(true);
235 * @param {string} prefix
236 * @param {string} text
238 _createItemElement: function(prefix, text)
240 var element = createElementWithClass("div", "suggest-box-content-item source-code");
241 element.tabIndex = -1;
242 if (prefix && prefix.length && !text.indexOf(prefix)) {
243 element.createChild("span", "prefix").textContent = prefix;
244 element.createChild("span", "suffix").textContent = text.substring(prefix.length);
245 } else {
246 element.createChild("span", "suffix").textContent = text;
248 element.createChild("span", "spacer");
249 element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false);
250 return element;
254 * @param {!Array.<string>} items
255 * @param {string} userEnteredText
257 _updateItems: function(items, userEnteredText)
259 this._length = items.length;
260 this._element.removeChildren();
261 delete this._selectedElement;
263 for (var i = 0; i < items.length; ++i) {
264 var item = items[i];
265 var currentItemElement = this._createItemElement(userEnteredText, item);
266 this._element.appendChild(currentItemElement);
271 * @param {number} index
272 * @param {boolean} scrollIntoView
274 _selectItem: function(index, scrollIntoView)
276 if (this._selectedElement)
277 this._selectedElement.classList.remove("selected");
279 this._selectedIndex = index;
280 if (index < 0)
281 return;
283 this._selectedElement = this._element.children[index];
284 this._selectedElement.classList.add("selected");
286 if (scrollIntoView)
287 this._selectedElement.scrollIntoViewIfNeeded(false);
291 * @param {!Array.<string>} completions
292 * @param {boolean} canShowForSingleItem
293 * @param {string} userEnteredText
295 _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
297 if (!completions || !completions.length)
298 return false;
300 if (completions.length > 1)
301 return true;
303 // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
304 return canShowForSingleItem && completions[0] !== userEnteredText;
307 _ensureRowCountPerViewport: function()
309 if (this._rowCountPerViewport)
310 return;
311 if (!this._element.firstChild)
312 return;
314 this._rowCountPerViewport = Math.floor(this._element.offsetHeight / this._element.firstChild.offsetHeight);
318 * @param {!AnchorBox} anchorBox
319 * @param {!Array.<string>} completions
320 * @param {number} selectedIndex
321 * @param {boolean} canShowForSingleItem
322 * @param {string} userEnteredText
324 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
326 if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
327 this._updateItems(completions, userEnteredText);
328 this._show();
329 this._updateBoxPosition(anchorBox);
330 this._selectItem(selectedIndex, selectedIndex > 0);
331 delete this._rowCountPerViewport;
332 } else
333 this.hide();
337 * @param {!KeyboardEvent} event
338 * @return {boolean}
340 keyPressed: function(event)
342 switch (event.keyIdentifier) {
343 case "Up":
344 return this.upKeyPressed();
345 case "Down":
346 return this.downKeyPressed();
347 case "PageUp":
348 return this.pageUpKeyPressed();
349 case "PageDown":
350 return this.pageDownKeyPressed();
351 case "Enter":
352 return this.enterKeyPressed();
354 return false;
358 * @return {boolean}
360 upKeyPressed: function()
362 return this._selectClosest(-1, true);
366 * @return {boolean}
368 downKeyPressed: function()
370 return this._selectClosest(1, true);
374 * @return {boolean}
376 pageUpKeyPressed: function()
378 this._ensureRowCountPerViewport();
379 return this._selectClosest(-this._rowCountPerViewport, false);
383 * @return {boolean}
385 pageDownKeyPressed: function()
387 this._ensureRowCountPerViewport();
388 return this._selectClosest(this._rowCountPerViewport, false);
392 * @return {boolean}
394 enterKeyPressed: function()
396 var hasSelectedItem = !!this._selectedElement;
397 this.acceptSuggestion();
399 // Report the event as non-handled if there is no selected item,
400 // to commit the input or handle it otherwise.
401 return hasSelectedItem;
406 * @constructor
407 * // FIXME: make SuggestBox work for multiple documents.
408 * @suppressGlobalPropertiesCheck
410 WebInspector.SuggestBox.Overlay = function()
412 this.element = createElementWithClass("div", "suggest-box-overlay");
413 var root = WebInspector.createShadowRootWithCoreStyles(this.element);
414 root.appendChild(WebInspector.Widget.createStyleElement("ui/suggestBox.css"));
415 this._leftSpacerElement = root.createChild("div", "suggest-box-left-spacer");
416 this._horizontalElement = root.createChild("div", "suggest-box-horizontal");
417 this._topSpacerElement = this._horizontalElement.createChild("div", "suggest-box-top-spacer");
418 this._bottomSpacerElement = this._horizontalElement.createChild("div", "suggest-box-bottom-spacer");
419 this._resize();
420 document.body.appendChild(this.element);
423 WebInspector.SuggestBox.Overlay.prototype = {
425 * @param {number} offset
427 setLeftOffset: function(offset)
429 this._leftSpacerElement.style.flexBasis = offset + "px";
433 * @param {number} offset
434 * @param {boolean} isTopOffset
436 setVerticalOffset: function(offset, isTopOffset)
438 this.element.classList.toggle("under-anchor", isTopOffset);
440 if (isTopOffset) {
441 this._bottomSpacerElement.style.flexBasis = "auto";
442 this._topSpacerElement.style.flexBasis = offset + "px";
443 } else {
444 this._bottomSpacerElement.style.flexBasis = offset + "px";
445 this._topSpacerElement.style.flexBasis = "auto";
450 * @param {!Element} element
452 setContentElement: function(element)
454 this._horizontalElement.insertBefore(element, this._bottomSpacerElement);
457 _resize: function()
459 var container = WebInspector.Dialog.modalHostView().element;
460 var containerBox = container.boxInWindow(container.ownerDocument.defaultView);
462 this.element.style.left = containerBox.x + "px";
463 this.element.style.top = containerBox.y + "px";
464 this.element.style.height = containerBox.height + "px";
465 this.element.style.width = containerBox.width + "px";
468 dispose: function()
470 this.element.remove();