Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 cr.define('options', function() {
      6   const ArrayDataModel = cr.ui.ArrayDataModel;
      7   const List = cr.ui.List;
      8   const ListItem = cr.ui.ListItem;
      9 
     10   /**
     11    * Creates a new autocomplete list item.
     12    * @param {Object} pageInfo The page this item represents.
     13    * @constructor
     14    * @extends {cr.ui.ListItem}
     15    */
     16   function AutocompleteListItem(pageInfo) {
     17     var el = cr.doc.createElement('div');
     18     el.pageInfo_ = pageInfo;
     19     AutocompleteListItem.decorate(el);
     20     return el;
     21   }
     22 
     23   /**
     24    * Decorates an element as an autocomplete list item.
     25    * @param {!HTMLElement} el The element to decorate.
     26    */
     27   AutocompleteListItem.decorate = function(el) {
     28     el.__proto__ = AutocompleteListItem.prototype;
     29     el.decorate();
     30   };
     31 
     32   AutocompleteListItem.prototype = {
     33     __proto__: ListItem.prototype,
     34 
     35     /** @inheritDoc */
     36     decorate: function() {
     37       ListItem.prototype.decorate.call(this);
     38 
     39       var title = this.pageInfo_['title'];
     40       var url = this.pageInfo_['displayURL'];
     41       var titleEl = this.ownerDocument.createElement('span');
     42       titleEl.className = 'title';
     43       titleEl.textContent = title || url;
     44       this.appendChild(titleEl);
     45 
     46       if (title && title.length > 0 && url != title) {
     47         var separatorEl = this.ownerDocument.createTextNode(' - ');
     48         this.appendChild(separatorEl);
     49 
     50         var urlEl = this.ownerDocument.createElement('span');
     51         urlEl.className = 'url';
     52         urlEl.textContent = url;
     53         this.appendChild(urlEl);
     54       }
     55     },
     56   };
     57 
     58   /**
     59    * Creates a new autocomplete list popup.
     60    * @constructor
     61    * @extends {cr.ui.List}
     62    */
     63   var AutocompleteList = cr.ui.define('list');
     64 
     65   AutocompleteList.prototype = {
     66     __proto__: List.prototype,
     67 
     68     /**
     69      * The text field the autocomplete popup is currently attached to, if any.
     70      * @type {HTMLElement}
     71      * @private
     72      */
     73     targetInput_: null,
     74 
     75     /**
     76      * Keydown event listener to attach to a text field.
     77      * @type {Function}
     78      * @private
     79      */
     80     textFieldKeyHandler_: null,
     81 
     82     /**
     83      * Input event listener to attach to a text field.
     84      * @type {Function}
     85      * @private
     86      */
     87     textFieldInputHandler_: null,
     88 
     89     /**
     90      * A function to call when new suggestions are needed.
     91      * @type {Function}
     92      * @private
     93      */
     94     suggestionUpdateRequestCallback_: null,
     95 
     96     /** @inheritDoc */
     97     decorate: function() {
     98       List.prototype.decorate.call(this);
     99       this.classList.add('autocomplete-suggestions');
    100       this.selectionModel = new cr.ui.ListSingleSelectionModel;
    101 
    102       this.textFieldKeyHandler_ = this.handleAutocompleteKeydown_.bind(this);
    103       var self = this;
    104       this.textFieldInputHandler_ = function(e) {
    105         if (self.suggestionUpdateRequestCallback_)
    106           self.suggestionUpdateRequestCallback_(self.targetInput_.value);
    107       };
    108       this.addEventListener('change', function(e) {
    109         var input = self.targetInput;
    110         if (!input || !self.selectedItem)
    111           return;
    112         input.value = self.selectedItem['url'];
    113         // Programatically change the value won't trigger a change event, but
    114         // clients are likely to want to know when changes happen, so fire one.
    115         var changeEvent = document.createEvent('Event');
    116         changeEvent.initEvent('change', true, true);
    117         input.dispatchEvent(changeEvent);
    118       });
    119       // Start hidden; adding suggestions will unhide.
    120       this.hidden = true;
    121     },
    122 
    123     /** @inheritDoc */
    124     createItem: function(pageInfo) {
    125       return new AutocompleteListItem(pageInfo);
    126     },
    127 
    128     /**
    129      * The suggestions to show.
    130      * @type {Array}
    131      */
    132     set suggestions(suggestions) {
    133       this.dataModel = new ArrayDataModel(suggestions);
    134       this.hidden = !this.targetInput_ || suggestions.length == 0;
    135     },
    136 
    137     /**
    138      * A function to call when the attached input field's contents change.
    139      * The function should take one string argument, which will be the text
    140      * to autocomplete from.
    141      * @type {Function}
    142      */
    143     set suggestionUpdateRequestCallback(callback) {
    144       this.suggestionUpdateRequestCallback_ = callback;
    145     },
    146 
    147     /**
    148      * Attaches the popup to the given input element. Requires
    149      * that the input be wrapped in a block-level container of the same width.
    150      * @param {HTMLElement} input The input element to attach to.
    151      */
    152     attachToInput: function(input) {
    153       if (this.targetInput_ == input)
    154         return;
    155 
    156       this.detach();
    157       this.targetInput_ = input;
    158       this.style.width = input.getBoundingClientRect().width + 'px';
    159       this.hidden = false;  // Necessary for positionPopupAroundElement to work.
    160       cr.ui.positionPopupAroundElement(input, this, cr.ui.AnchorType.BELOW)
    161       // Start hidden; when the data model gets results the list will show.
    162       this.hidden = true;
    163 
    164       input.addEventListener('keydown', this.textFieldKeyHandler_, true);
    165       input.addEventListener('input', this.textFieldInputHandler_);
    166     },
    167 
    168     /**
    169      * Detaches the autocomplete popup from its current input element, if any.
    170      */
    171     detach: function() {
    172       var input = this.targetInput_
    173       if (!input)
    174         return;
    175 
    176       input.removeEventListener('keydown', this.textFieldKeyHandler_);
    177       input.removeEventListener('input', this.textFieldInputHandler_);
    178       this.targetInput_ = null;
    179       this.suggestions = [];
    180     },
    181 
    182     /**
    183      * The text field the autocomplete popup is currently attached to, if any.
    184      * @return {HTMLElement}
    185      */
    186     get targetInput() {
    187       return this.targetInput_;
    188     },
    189 
    190     /**
    191      * Handles input field key events that should be interpreted as autocomplete
    192      * commands.
    193      * @param {Event} event The keydown event.
    194      * @private
    195      */
    196     handleAutocompleteKeydown_: function(event) {
    197       if (this.hidden)
    198         return;
    199       var handled = false;
    200       switch (event.keyIdentifier) {
    201         case 'U+001B':  // Esc
    202           this.suggestions = [];
    203           handled = true;
    204           break;
    205         case 'Enter':
    206           var hadSelection = this.selectedItem != null;
    207           this.suggestions = [];
    208           // Only count the event as handled if a selection is being commited.
    209           handled = hadSelection;
    210           break;
    211         case 'Up':
    212         case 'Down':
    213           this.dispatchEvent(event);
    214           handled = true;
    215           break;
    216       }
    217       // Don't let arrow keys affect the text field, or bubble up to, e.g.,
    218       // an enclosing list item.
    219       if (handled) {
    220         event.preventDefault();
    221         event.stopPropagation();
    222       }
    223     },
    224   };
    225 
    226   return {
    227     AutocompleteList: AutocompleteList
    228   };
    229 });
    230