• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
34WebInspector.SuggestBoxDelegate = function()
35{
36}
37
38WebInspector.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 */
56WebInspector.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.createElement("div");
66    this._element.className = "suggest-box";
67    this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
68}
69
70WebInspector.SuggestBox.prototype = {
71    /**
72     * @return {boolean}
73     */
74    visible: function()
75    {
76        return !!this._element.parentElement;
77    },
78
79    /**
80     * @param {!AnchorBox} anchorBox
81     */
82    setPosition: function(anchorBox)
83    {
84        this._updateBoxPosition(anchorBox);
85    },
86
87    /**
88     * @param {!AnchorBox} anchorBox
89     */
90    _updateBoxPosition: function(anchorBox)
91    {
92        console.assert(this._overlay);
93        if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox))
94            return;
95        this._lastAnchorBox = anchorBox;
96
97        // Position relative to main DevTools element.
98        var container = WebInspector.Dialog.modalHostView().element;
99        anchorBox = anchorBox.relativeToElement(container);
100        var totalWidth = container.offsetWidth;
101        var totalHeight = container.offsetHeight;
102        var aboveHeight = anchorBox.y;
103        var underHeight = totalHeight - anchorBox.y - anchorBox.height;
104
105        var rowHeight = 17;
106        const spacer = 6;
107
108        var maxHeight = this._maxItemsHeight ? this._maxItemsHeight * rowHeight : Math.max(underHeight, aboveHeight) - spacer;
109        var under = underHeight >= aboveHeight;
110        this._leftSpacerElement.style.flexBasis = anchorBox.x + "px";
111
112        this._overlay.element.classList.toggle("under-anchor", under);
113
114        if (under) {
115            this._bottomSpacerElement.style.flexBasis = "auto";
116            this._topSpacerElement.style.flexBasis = (anchorBox.y + anchorBox.height) + "px";
117        } else {
118            this._bottomSpacerElement.style.flexBasis = (totalHeight - anchorBox.y) + "px";
119            this._topSpacerElement.style.flexBasis = "auto";
120        }
121        this._element.style.maxHeight = maxHeight + "px";
122    },
123
124    /**
125     * @param {?Event} event
126     */
127    _onBoxMouseDown: function(event)
128    {
129        if (this._hideTimeoutId) {
130            window.clearTimeout(this._hideTimeoutId);
131            delete this._hideTimeoutId;
132        }
133        event.preventDefault();
134    },
135
136    _maybeHide: function()
137    {
138        if (!this._hideTimeoutId)
139            this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0);
140    },
141
142    _show: function()
143    {
144        if (this.visible())
145            return;
146        this._overlay = new WebInspector.SuggestBox.Overlay();
147        this._bodyElement.addEventListener("mousedown", this._maybeHideBound, true);
148
149        this._leftSpacerElement = this._overlay.element.createChild("div", "suggest-box-left-spacer");
150        this._horizontalElement = this._overlay.element.createChild("div", "suggest-box-horizontal");
151        this._topSpacerElement = this._horizontalElement.createChild("div", "suggest-box-top-spacer");
152        this._horizontalElement.appendChild(this._element);
153        this._bottomSpacerElement = this._horizontalElement.createChild("div", "suggest-box-bottom-spacer");
154    },
155
156    hide: function()
157    {
158        if (!this.visible())
159            return;
160
161        this._bodyElement.removeEventListener("mousedown", this._maybeHideBound, true);
162        this._element.remove();
163        this._overlay.dispose();
164        delete this._overlay;
165        delete this._selectedElement;
166        this._selectedIndex = -1;
167        delete this._lastAnchorBox;
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, true);
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.createChild("span", "spacer");
260        element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false);
261        return element;
262    },
263
264    /**
265     * @param {!Array.<string>} items
266     * @param {string} userEnteredText
267     */
268    _updateItems: function(items, userEnteredText)
269    {
270        this._length = items.length;
271        this._element.removeChildren();
272        delete this._selectedElement;
273
274        for (var i = 0; i < items.length; ++i) {
275            var item = items[i];
276            var currentItemElement = this._createItemElement(userEnteredText, item);
277            this._element.appendChild(currentItemElement);
278        }
279    },
280
281    /**
282     * @param {number} index
283     * @param {boolean} scrollIntoView
284     */
285    _selectItem: function(index, scrollIntoView)
286    {
287        if (this._selectedElement)
288            this._selectedElement.classList.remove("selected");
289
290        this._selectedIndex = index;
291        if (index < 0)
292            return;
293
294        this._selectedElement = this._element.children[index];
295        this._selectedElement.classList.add("selected");
296
297        if (scrollIntoView)
298            this._selectedElement.scrollIntoViewIfNeeded(false);
299    },
300
301    /**
302     * @param {!Array.<string>} completions
303     * @param {boolean} canShowForSingleItem
304     * @param {string} userEnteredText
305     */
306    _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
307    {
308        if (!completions || !completions.length)
309            return false;
310
311        if (completions.length > 1)
312            return true;
313
314        // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
315        return canShowForSingleItem && completions[0] !== userEnteredText;
316    },
317
318    _ensureRowCountPerViewport: function()
319    {
320        if (this._rowCountPerViewport)
321            return;
322        if (!this._element.firstChild)
323            return;
324
325        this._rowCountPerViewport = Math.floor(this._element.offsetHeight / this._element.firstChild.offsetHeight);
326    },
327
328    /**
329     * @param {!AnchorBox} anchorBox
330     * @param {!Array.<string>} completions
331     * @param {number} selectedIndex
332     * @param {boolean} canShowForSingleItem
333     * @param {string} userEnteredText
334     */
335    updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText)
336    {
337        if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
338            this._updateItems(completions, userEnteredText);
339            this._show();
340            this._updateBoxPosition(anchorBox);
341            this._selectItem(selectedIndex, selectedIndex > 0);
342            delete this._rowCountPerViewport;
343        } else
344            this.hide();
345    },
346
347    /**
348     * @param {!KeyboardEvent} event
349     * @return {boolean}
350     */
351    keyPressed: function(event)
352    {
353        switch (event.keyIdentifier) {
354        case "Up":
355            return this.upKeyPressed();
356        case "Down":
357            return this.downKeyPressed();
358        case "PageUp":
359            return this.pageUpKeyPressed();
360        case "PageDown":
361            return this.pageDownKeyPressed();
362        case "Enter":
363            return this.enterKeyPressed();
364        }
365        return false;
366    },
367
368    /**
369     * @return {boolean}
370     */
371    upKeyPressed: function()
372    {
373        return this._selectClosest(-1, true);
374    },
375
376    /**
377     * @return {boolean}
378     */
379    downKeyPressed: function()
380    {
381        return this._selectClosest(1, true);
382    },
383
384    /**
385     * @return {boolean}
386     */
387    pageUpKeyPressed: function()
388    {
389        this._ensureRowCountPerViewport();
390        return this._selectClosest(-this._rowCountPerViewport, false);
391    },
392
393    /**
394     * @return {boolean}
395     */
396    pageDownKeyPressed: function()
397    {
398        this._ensureRowCountPerViewport();
399        return this._selectClosest(this._rowCountPerViewport, false);
400    },
401
402    /**
403     * @return {boolean}
404     */
405    enterKeyPressed: function()
406    {
407        var hasSelectedItem = !!this._selectedElement;
408        this.acceptSuggestion();
409
410        // Report the event as non-handled if there is no selected item,
411        // to commit the input or handle it otherwise.
412        return hasSelectedItem;
413    }
414}
415
416/**
417 * @constructor
418 */
419WebInspector.SuggestBox.Overlay = function()
420{
421    this.element = document.createElement("div");
422    this.element.classList.add("suggest-box-overlay");
423    this._resize();
424    document.body.appendChild(this.element);
425}
426
427WebInspector.SuggestBox.Overlay.prototype = {
428    _resize: function()
429    {
430        var container = WebInspector.Dialog.modalHostView().element;
431        var containerBox = container.boxInWindow(container.ownerDocument.defaultView);
432
433        this.element.style.left = containerBox.x + "px";
434        this.element.style.top = containerBox.y + "px";
435        this.element.style.height = containerBox.height + "px";
436        this.element.style.width = containerBox.width + "px";
437    },
438
439    dispose: function()
440    {
441        this.element.remove();
442    }
443}
444