- add third_party src.
[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._boundOnScroll = this._onScrollOrResize.bind(this, true);
67     this._boundOnResize = this._onScrollOrResize.bind(this, false);
68     window.addEventListener("scroll", this._boundOnScroll, true);
69     window.addEventListener("resize", this._boundOnResize, true);
70
71     this._bodyElement = anchorElement.ownerDocument.body;
72     this._element = anchorElement.ownerDocument.createElement("div");
73     this._element.className = "suggest-box " + (className || "");
74     this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
75     this.containerElement = this._element.createChild("div", "container");
76     this.contentElement = this.containerElement.createChild("div", "content");
77 }
78
79 WebInspector.SuggestBox.prototype = {
80     /**
81      * @return {boolean}
82      */
83     visible: function()
84     {
85         return !!this._element.parentElement;
86     },
87
88     /**
89      * @param {boolean} isScroll
90      * @param {Event} event
91      */
92     _onScrollOrResize: function(isScroll, event)
93     {
94         if (isScroll && this._element.isAncestor(event.target) || !this.visible())
95             return;
96         this._updateBoxPosition(this._anchorBox);
97     },
98
99     /**
100      * @param {AnchorBox} anchorBox
101      */
102     setPosition: function(anchorBox)
103     {
104         this._updateBoxPosition(anchorBox);
105     },
106
107     /**
108      * @param {AnchorBox=} anchorBox
109      */
110     _updateBoxPosition: function(anchorBox)
111     {
112         this._anchorBox = anchorBox;
113         anchorBox = anchorBox || this._anchorElement.boxInWindow(window);
114
115         // Measure the content element box.
116         this.contentElement.style.display = "inline-block";
117         document.body.appendChild(this.contentElement);
118         this.contentElement.positionAt(0, 0);
119         var contentWidth = this.contentElement.offsetWidth;
120         var contentHeight = this.contentElement.offsetHeight;
121         this.contentElement.style.display = "block";
122         this.containerElement.appendChild(this.contentElement);
123
124         const spacer = 6;
125         const suggestBoxPaddingX = 21;
126         const suggestBoxPaddingY = 2;
127
128         var maxWidth = document.body.offsetWidth - anchorBox.x - spacer;
129         var width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
130         var paddedWidth = contentWidth + suggestBoxPaddingX;
131         var boxX = anchorBox.x;
132         if (width < paddedWidth) {
133             // Shift the suggest box to the left to accommodate the content without trimming to the BODY edge.
134             maxWidth = document.body.offsetWidth - spacer;
135             width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
136             boxX = document.body.offsetWidth - width;
137         }
138
139         var boxY;
140         var aboveHeight = anchorBox.y;
141         var underHeight = document.body.offsetHeight - anchorBox.y - anchorBox.height;
142
143         var maxHeight = this._maxItemsHeight ? contentHeight * this._maxItemsHeight / this._length : Math.max(underHeight, aboveHeight) - spacer;
144         var height = Math.min(contentHeight, maxHeight - suggestBoxPaddingY) + suggestBoxPaddingY;
145         if (underHeight >= aboveHeight) {
146             // Locate the suggest box under the anchorBox.
147             boxY = anchorBox.y + anchorBox.height;
148             this._element.removeStyleClass("above-anchor");
149             this._element.addStyleClass("under-anchor");
150         } else {
151             // Locate the suggest box above the anchorBox.
152             boxY = anchorBox.y - height;
153             this._element.removeStyleClass("under-anchor");
154             this._element.addStyleClass("above-anchor");
155         }
156
157         this._element.positionAt(boxX, boxY);
158         this._element.style.width = width + "px";
159         this._element.style.height = height + "px";
160     },
161
162     /**
163      * @param {Event} event
164      */
165     _onBoxMouseDown: function(event)
166     {
167         event.preventDefault();
168     },
169
170     hide: function()
171     {
172         if (!this.visible())
173             return;
174
175         this._element.remove();
176         delete this._selectedElement;
177     },
178
179     removeFromElement: function()
180     {
181         window.removeEventListener("scroll", this._boundOnScroll, true);
182         window.removeEventListener("resize", this._boundOnResize, true);
183         this.hide();
184     },
185
186     /**
187      * @param {string=} text
188      * @param {boolean=} isIntermediateSuggestion
189      */
190     _applySuggestion: function(text, isIntermediateSuggestion)
191     {
192         if (!this.visible() || !(text || this._selectedElement))
193             return false;
194
195         var suggestion = text || this._selectedElement.textContent;
196         if (!suggestion)
197             return false;
198
199         this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
200         return true;
201     },
202
203     /**
204      * @param {string=} text
205      */
206     acceptSuggestion: function(text)
207     {
208         var result = this._applySuggestion(text, false);
209         this.hide();
210         if (!result)
211             return false;
212
213         this._suggestBoxDelegate.acceptSuggestion();
214
215         return true;
216     },
217
218     /**
219      * @param {number} shift
220      * @param {boolean=} isCircular
221      * @return {boolean} is changed
222      */
223     _selectClosest: function(shift, isCircular)
224     {
225         if (!this._length)
226             return false;
227
228         var index = this._selectedIndex + shift;
229
230         if (isCircular)
231             index = (this._length + index) % this._length;
232         else
233             index = Number.constrain(index, 0, this._length - 1);
234
235         this._selectItem(index);
236         this._applySuggestion(undefined, true);
237         return true;
238     },
239
240     /**
241      * @param {string} text
242      * @param {Event} event
243      */
244     _onItemMouseDown: function(text, event)
245     {
246         this.acceptSuggestion(text);
247         event.consume(true);
248     },
249
250     /**
251      * @param {string} prefix
252      * @param {string} text
253      */
254     _createItemElement: function(prefix, text)
255     {
256         var element = document.createElement("div");
257         element.className = "suggest-box-content-item source-code";
258         element.tabIndex = -1;
259         if (prefix && prefix.length && !text.indexOf(prefix)) {
260             var prefixElement = element.createChild("span", "prefix");
261             prefixElement.textContent = prefix;
262             var suffixElement = element.createChild("span", "suffix");
263             suffixElement.textContent = text.substring(prefix.length);
264         } else {
265             var suffixElement = element.createChild("span", "suffix");
266             suffixElement.textContent = text;
267         }
268         element.addEventListener("mousedown", this._onItemMouseDown.bind(this, text), false);
269         return element;
270     },
271
272     /**
273      * @param {!Array.<string>} items
274      * @param {number} selectedIndex
275      * @param {string} userEnteredText
276      */
277     _updateItems: function(items, selectedIndex, userEnteredText)
278     {
279         this._length = items.length;
280         this.contentElement.removeChildren();
281
282         for (var i = 0; i < items.length; ++i) {
283             var item = items[i];
284             var currentItemElement = this._createItemElement(userEnteredText, item);
285             this.contentElement.appendChild(currentItemElement);
286         }
287
288         this._selectedElement = null;
289         if (typeof selectedIndex === "number")
290             this._selectItem(selectedIndex);
291     },
292
293     /**
294      * @param {number} index
295      */
296     _selectItem: function(index)
297     {
298         if (this._selectedElement)
299             this._selectedElement.classList.remove("selected");
300
301         this._selectedIndex = index;
302         this._selectedElement = this.contentElement.children[index];
303         this._selectedElement.classList.add("selected");
304
305         this._selectedElement.scrollIntoViewIfNeeded(false);
306     },
307
308     /**
309      * @param {!Array.<string>} completions
310      * @param {boolean} canShowForSingleItem
311      * @param {string} userEnteredText
312      */
313     _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
314     {
315         if (!completions || !completions.length)
316             return false;
317
318         if (completions.length > 1)
319             return true;
320
321         // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
322         return canShowForSingleItem && completions[0] !== userEnteredText;
323     },
324
325     _rememberRowCountPerViewport: function()
326     {
327         if (!this.contentElement.firstChild)
328             return;
329
330         this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight);
331     },
332
333     /**
334      * @param {AnchorBox} anchorBox
335      * @param {!Array.<string>} completions
336      * @param {number} selectedIndex
337      * @param {boolean} canShowForSingleItem
338      * @param {string} userEnteredText
339      */
340     updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
341     {
342         if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
343             this._updateItems(completions, selectedIndex, userEnteredText);
344             this._updateBoxPosition(anchorBox);
345             if (!this.visible())
346                 this._bodyElement.appendChild(this._element);
347             this._rememberRowCountPerViewport();
348         } else
349             this.hide();
350     },
351
352     /**
353      * @param {KeyboardEvent} event
354      * @return {boolean}
355      */
356     keyPressed: function(event)
357     {
358         switch (event.keyIdentifier) {
359         case "Up":
360             return this.upKeyPressed();
361         case "Down":
362             return this.downKeyPressed();
363         case "PageUp":
364             return this.pageUpKeyPressed();
365         case "PageDown":
366             return this.pageDownKeyPressed();
367         case "Enter":
368             return this.enterKeyPressed();
369         }
370         return false;
371     },
372
373     /**
374      * @return {boolean}
375      */
376     upKeyPressed: function()
377     {
378         return this._selectClosest(-1, true);
379     },
380
381     /**
382      * @return {boolean}
383      */
384     downKeyPressed: function()
385     {
386         return this._selectClosest(1, true);
387     },
388
389     /**
390      * @return {boolean}
391      */
392     pageUpKeyPressed: function()
393     {
394         return this._selectClosest(-this._rowCountPerViewport, false);
395     },
396
397     /**
398      * @return {boolean}
399      */
400     pageDownKeyPressed: function()
401     {
402         return this._selectClosest(this._rowCountPerViewport, false);
403     },
404
405     /**
406      * @return {boolean}
407      */
408     enterKeyPressed: function()
409     {
410         var hasSelectedItem = !!this._selectedElement;
411         this.acceptSuggestion();
412
413         // Report the event as non-handled if there is no selected item,
414         // to commit the input or handle it otherwise.
415         return hasSelectedItem;
416     }
417 }