Home | History | Annotate | Download | only in ui
      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 }
    438