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.createElement("div");
     66     this._element.className = "suggest-box";
     67     this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
     68 }
     69 
     70 WebInspector.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  */
    419 WebInspector.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 
    427 WebInspector.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