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._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 = {
74 return !!this._element
.parentElement
;
78 * @param {!AnchorBox} anchorBox
80 setPosition: function(anchorBox
)
82 this._updateBoxPosition(anchorBox
);
86 * @param {!AnchorBox} anchorBox
88 _updateBoxPosition: function(anchorBox
)
90 console
.assert(this._overlay
);
91 if (this._lastAnchorBox
&& this._lastAnchorBox
.equals(anchorBox
))
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
;
106 this._overlay
.setVerticalOffset(anchorBox
.y
+ anchorBox
.height
, true);
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
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
);
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()
169 * @param {boolean=} isIntermediateSuggestion
171 _applySuggestion: function(isIntermediateSuggestion
)
173 if (!this.visible() || !this._selectedElement
)
176 var suggestion
= this._selectedElement
.textContent
;
180 this._suggestBoxDelegate
.applySuggestion(suggestion
, isIntermediateSuggestion
);
187 acceptSuggestion: function()
189 var result
= this._applySuggestion();
194 this._suggestBoxDelegate
.acceptSuggestion();
200 * @param {number} shift
201 * @param {boolean=} isCircular
202 * @return {boolean} is changed
204 _selectClosest: function(shift
, isCircular
)
209 if (this._selectedIndex
=== -1 && shift
< 0)
212 var index
= this._selectedIndex
+ shift
;
215 index
= (this._length
+ index
) % this._length
;
217 index
= Number
.constrain(index
, 0, this._length
- 1);
219 this._selectItem(index
, true);
220 this._applySuggestion(true);
225 * @param {!Event} event
227 _onItemMouseDown: function(event
)
229 this._selectedElement
= event
.currentTarget
;
230 this.acceptSuggestion();
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
);
246 element
.createChild("span", "suffix").textContent
= text
;
248 element
.createChild("span", "spacer");
249 element
.addEventListener("mousedown", this._onItemMouseDown
.bind(this), false);
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
) {
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
;
283 this._selectedElement
= this._element
.children
[index
];
284 this._selectedElement
.classList
.add("selected");
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
)
300 if (completions
.length
> 1)
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
)
311 if (!this._element
.firstChild
)
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
);
329 this._updateBoxPosition(anchorBox
);
330 this._selectItem(selectedIndex
, selectedIndex
> 0);
331 delete this._rowCountPerViewport
;
337 * @param {!KeyboardEvent} event
340 keyPressed: function(event
)
342 switch (event
.keyIdentifier
) {
344 return this.upKeyPressed();
346 return this.downKeyPressed();
348 return this.pageUpKeyPressed();
350 return this.pageDownKeyPressed();
352 return this.enterKeyPressed();
360 upKeyPressed: function()
362 return this._selectClosest(-1, true);
368 downKeyPressed: function()
370 return this._selectClosest(1, true);
376 pageUpKeyPressed: function()
378 this._ensureRowCountPerViewport();
379 return this._selectClosest(-this._rowCountPerViewport
, false);
385 pageDownKeyPressed: function()
387 this._ensureRowCountPerViewport();
388 return this._selectClosest(this._rowCountPerViewport
, false);
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
;
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");
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
);
441 this._bottomSpacerElement
.style
.flexBasis
= "auto";
442 this._topSpacerElement
.style
.flexBasis
= offset
+ "px";
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
);
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";
470 this.element
.remove();