3c79dc6427164287431fe9217d38291948275b31
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ui / SuggestBox.js
1 /*
2  * Copyright (C) 2013 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
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.
17  *
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.
29  */
30
31 /**
32  * @interface
33  */
34 WebInspector.SuggestBoxDelegate = function()
35 {
36 }
37
38 WebInspector.SuggestBoxDelegate.prototype = {
39     /**
40      * @param {string} suggestion
41      * @param {boolean=} isIntermediateSuggestion
42      */
43     applySuggestion: function(suggestion, isIntermediateSuggestion) { },
44
45     /**
46      * acceptSuggestion will be always called after call to applySuggestion with isIntermediateSuggestion being equal to false.
47      */
48     acceptSuggestion: function() { },
49 }
50
51 /**
52  * @constructor
53  * @param {!WebInspector.SuggestBoxDelegate} suggestBoxDelegate
54  * @param {number=} maxItemsHeight
55  */
56 WebInspector.SuggestBox = function(suggestBoxDelegate, maxItemsHeight)
57 {
58     this._suggestBoxDelegate = suggestBoxDelegate;
59     this._length = 0;
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);
67 }
68
69 WebInspector.SuggestBox.prototype = {
70     /**
71      * @return {boolean}
72      */
73     visible: function()
74     {
75         return !!this._element.parentElement;
76     },
77
78     /**
79      * @param {!AnchorBox} anchorBox
80      */
81     setPosition: function(anchorBox)
82     {
83         this._updateBoxPosition(anchorBox);
84     },
85
86     /**
87      * @param {!AnchorBox} anchorBox
88      */
89     _updateBoxPosition: function(anchorBox)
90     {
91         console.assert(this._overlay);
92         if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox))
93             return;
94         this._lastAnchorBox = anchorBox;
95
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;
103
104         var rowHeight = 17;
105         const spacer = 6;
106
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";
110
111         this._overlay.element.classList.toggle("under-anchor", under);
112
113         if (under) {
114             this._bottomSpacerElement.style.flexBasis = "auto";
115             this._topSpacerElement.style.flexBasis = (anchorBox.y + anchorBox.height) + "px";
116         } else {
117             this._bottomSpacerElement.style.flexBasis = (totalHeight - anchorBox.y) + "px";
118             this._topSpacerElement.style.flexBasis = "auto";
119         }
120         this._element.style.maxHeight = maxHeight + "px";
121     },
122
123     /**
124      * @param {!Event} event
125      */
126     _onBoxMouseDown: function(event)
127     {
128         if (this._hideTimeoutId) {
129             window.clearTimeout(this._hideTimeoutId);
130             delete this._hideTimeoutId;
131         }
132         event.preventDefault();
133     },
134
135     _maybeHide: function()
136     {
137         if (!this._hideTimeoutId)
138             this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0);
139     },
140
141     _show: function()
142     {
143         if (this.visible())
144             return;
145         this._overlay = new WebInspector.SuggestBox.Overlay();
146         this._bodyElement.addEventListener("mousedown", this._maybeHideBound, true);
147
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");
153     },
154
155     hide: function()
156     {
157         if (!this.visible())
158             return;
159
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;
167     },
168
169     removeFromElement: function()
170     {
171         this.hide();
172     },
173
174     /**
175      * @param {boolean=} isIntermediateSuggestion
176      */
177     _applySuggestion: function(isIntermediateSuggestion)
178     {
179         if (!this.visible() || !this._selectedElement)
180             return false;
181
182         var suggestion = this._selectedElement.textContent;
183         if (!suggestion)
184             return false;
185
186         this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
187         return true;
188     },
189
190     /**
191      * @return {boolean}
192      */
193     acceptSuggestion: function()
194     {
195         var result = this._applySuggestion();
196         this.hide();
197         if (!result)
198             return false;
199
200         this._suggestBoxDelegate.acceptSuggestion();
201
202         return true;
203     },
204
205     /**
206      * @param {number} shift
207      * @param {boolean=} isCircular
208      * @return {boolean} is changed
209      */
210     _selectClosest: function(shift, isCircular)
211     {
212         if (!this._length)
213             return false;
214
215         if (this._selectedIndex === -1 && shift < 0)
216             shift += 1;
217
218         var index = this._selectedIndex + shift;
219
220         if (isCircular)
221             index = (this._length + index) % this._length;
222         else
223             index = Number.constrain(index, 0, this._length - 1);
224
225         this._selectItem(index, true);
226         this._applySuggestion(true);
227         return true;
228     },
229
230     /**
231      * @param {!Event} event
232      */
233     _onItemMouseDown: function(event)
234     {
235         this._selectedElement = event.currentTarget;
236         this.acceptSuggestion();
237         event.consume(true);
238     },
239
240     /**
241      * @param {string} prefix
242      * @param {string} text
243      */
244     _createItemElement: function(prefix, text)
245     {
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);
251         } else {
252             element.createChild("span", "suffix").textContent = text;
253         }
254         element.createChild("span", "spacer");
255         element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false);
256         return element;
257     },
258
259     /**
260      * @param {!Array.<string>} items
261      * @param {string} userEnteredText
262      */
263     _updateItems: function(items, userEnteredText)
264     {
265         this._length = items.length;
266         this._element.removeChildren();
267         delete this._selectedElement;
268
269         for (var i = 0; i < items.length; ++i) {
270             var item = items[i];
271             var currentItemElement = this._createItemElement(userEnteredText, item);
272             this._element.appendChild(currentItemElement);
273         }
274     },
275
276     /**
277      * @param {number} index
278      * @param {boolean} scrollIntoView
279      */
280     _selectItem: function(index, scrollIntoView)
281     {
282         if (this._selectedElement)
283             this._selectedElement.classList.remove("selected");
284
285         this._selectedIndex = index;
286         if (index < 0)
287             return;
288
289         this._selectedElement = this._element.children[index];
290         this._selectedElement.classList.add("selected");
291
292         if (scrollIntoView)
293             this._selectedElement.scrollIntoViewIfNeeded(false);
294     },
295
296     /**
297      * @param {!Array.<string>} completions
298      * @param {boolean} canShowForSingleItem
299      * @param {string} userEnteredText
300      */
301     _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
302     {
303         if (!completions || !completions.length)
304             return false;
305
306         if (completions.length > 1)
307             return true;
308
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;
311     },
312
313     _ensureRowCountPerViewport: function()
314     {
315         if (this._rowCountPerViewport)
316             return;
317         if (!this._element.firstChild)
318             return;
319
320         this._rowCountPerViewport = Math.floor(this._element.offsetHeight / this._element.firstChild.offsetHeight);
321     },
322
323     /**
324      * @param {!AnchorBox} anchorBox
325      * @param {!Array.<string>} completions
326      * @param {number} selectedIndex
327      * @param {boolean} canShowForSingleItem
328      * @param {string} userEnteredText
329      */
330     updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
331     {
332         if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
333             this._updateItems(completions, userEnteredText);
334             this._show();
335             this._updateBoxPosition(anchorBox);
336             this._selectItem(selectedIndex, selectedIndex > 0);
337             delete this._rowCountPerViewport;
338         } else
339             this.hide();
340     },
341
342     /**
343      * @param {!KeyboardEvent} event
344      * @return {boolean}
345      */
346     keyPressed: function(event)
347     {
348         switch (event.keyIdentifier) {
349         case "Up":
350             return this.upKeyPressed();
351         case "Down":
352             return this.downKeyPressed();
353         case "PageUp":
354             return this.pageUpKeyPressed();
355         case "PageDown":
356             return this.pageDownKeyPressed();
357         case "Enter":
358             return this.enterKeyPressed();
359         }
360         return false;
361     },
362
363     /**
364      * @return {boolean}
365      */
366     upKeyPressed: function()
367     {
368         return this._selectClosest(-1, true);
369     },
370
371     /**
372      * @return {boolean}
373      */
374     downKeyPressed: function()
375     {
376         return this._selectClosest(1, true);
377     },
378
379     /**
380      * @return {boolean}
381      */
382     pageUpKeyPressed: function()
383     {
384         this._ensureRowCountPerViewport();
385         return this._selectClosest(-this._rowCountPerViewport, false);
386     },
387
388     /**
389      * @return {boolean}
390      */
391     pageDownKeyPressed: function()
392     {
393         this._ensureRowCountPerViewport();
394         return this._selectClosest(this._rowCountPerViewport, false);
395     },
396
397     /**
398      * @return {boolean}
399      */
400     enterKeyPressed: function()
401     {
402         var hasSelectedItem = !!this._selectedElement;
403         this.acceptSuggestion();
404
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;
408     }
409 }
410
411 /**
412  * @constructor
413  */
414 WebInspector.SuggestBox.Overlay = function()
415 {
416     this.element = document.createElementWithClass("div", "suggest-box-overlay");
417     this._resize();
418     document.body.appendChild(this.element);
419 }
420
421 WebInspector.SuggestBox.Overlay.prototype = {
422     _resize: function()
423     {
424         var container = WebInspector.Dialog.modalHostView().element;
425         var containerBox = container.boxInWindow(container.ownerDocument.defaultView);
426
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";
431     },
432
433     dispose: function()
434     {
435         this.element.remove();
436     }
437 }