Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / 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 {!Element} anchorElement
55  * @param {string=} className
56  * @param {number=} maxItemsHeight
57  */
58 WebInspector.SuggestBox = function(suggestBoxDelegate, anchorElement, className, maxItemsHeight)
59 {
60     this._suggestBoxDelegate = suggestBoxDelegate;
61     this._anchorElement = anchorElement;
62     this._length = 0;
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");
72 }
73
74 WebInspector.SuggestBox.prototype = {
75     /**
76      * @return {boolean}
77      */
78     visible: function()
79     {
80         return !!this._element.parentElement;
81     },
82
83     /**
84      * @param {!AnchorBox} anchorBox
85      */
86     setPosition: function(anchorBox)
87     {
88         this._updateBoxPosition(anchorBox);
89     },
90
91     /**
92      * @param {?AnchorBox=} anchorBox
93      */
94     _updateBoxPosition: function(anchorBox)
95     {
96         this._anchorBox = anchorBox;
97         anchorBox = anchorBox || this._anchorElement.boxInWindow(window);
98
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;
104
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);
113
114         const spacer = 6;
115         const suggestBoxPaddingX = 21;
116         const suggestBoxPaddingY = 2;
117
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;
127         }
128
129         var boxY;
130         var aboveHeight = anchorBox.y;
131         var underHeight = totalHeight - anchorBox.y - anchorBox.height;
132
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");
140         } else {
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");
145         }
146
147         this._element.positionAt(boxX, boxY, container);
148         this._element.style.width = width + "px";
149         this._element.style.height = height + "px";
150     },
151
152     /**
153      * @param {?Event} event
154      */
155     _onBoxMouseDown: function(event)
156     {
157         event.preventDefault();
158     },
159
160     hide: function()
161     {
162         if (!this.visible())
163             return;
164
165         this._element.remove();
166         delete this._selectedElement;
167         this._selectedIndex = -1;
168     },
169
170     removeFromElement: function()
171     {
172         this.hide();
173     },
174
175     /**
176      * @param {boolean=} isIntermediateSuggestion
177      */
178     _applySuggestion: function(isIntermediateSuggestion)
179     {
180         if (!this.visible() || !this._selectedElement)
181             return false;
182
183         var suggestion = this._selectedElement.textContent;
184         if (!suggestion)
185             return false;
186
187         this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
188         return true;
189     },
190
191     /**
192      * @return {boolean}
193      */
194     acceptSuggestion: function()
195     {
196         var result = this._applySuggestion();
197         this.hide();
198         if (!result)
199             return false;
200
201         this._suggestBoxDelegate.acceptSuggestion();
202
203         return true;
204     },
205
206     /**
207      * @param {number} shift
208      * @param {boolean=} isCircular
209      * @return {boolean} is changed
210      */
211     _selectClosest: function(shift, isCircular)
212     {
213         if (!this._length)
214             return false;
215
216         if (this._selectedIndex === -1 && shift < 0)
217             shift += 1;
218
219         var index = this._selectedIndex + shift;
220
221         if (isCircular)
222             index = (this._length + index) % this._length;
223         else
224             index = Number.constrain(index, 0, this._length - 1);
225
226         this._selectItem(index);
227         this._applySuggestion(true);
228         return true;
229     },
230
231     /**
232      * @param {?Event} event
233      */
234     _onItemMouseDown: function(event)
235     {
236         this._selectedElement = event.currentTarget;
237         this.acceptSuggestion();
238         event.consume(true);
239     },
240
241     /**
242      * @param {string} prefix
243      * @param {string} text
244      */
245     _createItemElement: function(prefix, text)
246     {
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);
255         } else {
256             var suffixElement = element.createChild("span", "suffix");
257             suffixElement.textContent = text;
258         }
259         element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false);
260         return element;
261     },
262
263     /**
264      * @param {!Array.<string>} items
265      * @param {number} selectedIndex
266      * @param {string} userEnteredText
267      */
268     _updateItems: function(items, selectedIndex, userEnteredText)
269     {
270         this._length = items.length;
271         this.contentElement.removeChildren();
272
273         for (var i = 0; i < items.length; ++i) {
274             var item = items[i];
275             var currentItemElement = this._createItemElement(userEnteredText, item);
276             this.contentElement.appendChild(currentItemElement);
277         }
278
279         this._selectedElement = null;
280         if (typeof selectedIndex === "number")
281             this._selectItem(selectedIndex);
282     },
283
284     /**
285      * @param {number} index
286      */
287     _selectItem: function(index)
288     {
289         if (this._selectedElement)
290             this._selectedElement.classList.remove("selected");
291
292         this._selectedIndex = index;
293         if (index < 0)
294             return;
295
296         this._selectedElement = this.contentElement.children[index];
297         this._selectedElement.classList.add("selected");
298
299         this._selectedElement.scrollIntoViewIfNeeded(false);
300     },
301
302     /**
303      * @param {!Array.<string>} completions
304      * @param {boolean} canShowForSingleItem
305      * @param {string} userEnteredText
306      */
307     _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
308     {
309         if (!completions || !completions.length)
310             return false;
311
312         if (completions.length > 1)
313             return true;
314
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;
317     },
318
319     _rememberRowCountPerViewport: function()
320     {
321         if (!this.contentElement.firstChild)
322             return;
323
324         this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight);
325     },
326
327     /**
328      * @param {?AnchorBox} anchorBox
329      * @param {!Array.<string>} completions
330      * @param {number} selectedIndex
331      * @param {boolean} canShowForSingleItem
332      * @param {string} userEnteredText
333      */
334     updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
335     {
336         if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
337             this._updateItems(completions, selectedIndex, userEnteredText);
338             this._updateBoxPosition(anchorBox);
339             if (!this.visible())
340                 this._bodyElement.appendChild(this._element);
341             this._rememberRowCountPerViewport();
342         } else
343             this.hide();
344     },
345
346     /**
347      * @param {!KeyboardEvent} event
348      * @return {boolean}
349      */
350     keyPressed: function(event)
351     {
352         switch (event.keyIdentifier) {
353         case "Up":
354             return this.upKeyPressed();
355         case "Down":
356             return this.downKeyPressed();
357         case "PageUp":
358             return this.pageUpKeyPressed();
359         case "PageDown":
360             return this.pageDownKeyPressed();
361         case "Enter":
362             return this.enterKeyPressed();
363         }
364         return false;
365     },
366
367     /**
368      * @return {boolean}
369      */
370     upKeyPressed: function()
371     {
372         return this._selectClosest(-1, true);
373     },
374
375     /**
376      * @return {boolean}
377      */
378     downKeyPressed: function()
379     {
380         return this._selectClosest(1, true);
381     },
382
383     /**
384      * @return {boolean}
385      */
386     pageUpKeyPressed: function()
387     {
388         return this._selectClosest(-this._rowCountPerViewport, false);
389     },
390
391     /**
392      * @return {boolean}
393      */
394     pageDownKeyPressed: function()
395     {
396         return this._selectClosest(this._rowCountPerViewport, false);
397     },
398
399     /**
400      * @return {boolean}
401      */
402     enterKeyPressed: function()
403     {
404         var hasSelectedItem = !!this._selectedElement;
405         this.acceptSuggestion();
406
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;
410     }
411 }