• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 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
5cr.define('cr.ui', function() {
6  /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
7  /** @const */ var List = cr.ui.List;
8  /** @const */ var ListItem = cr.ui.ListItem;
9
10  /**
11   * Creates a new autocomplete list item.
12   * This is suitable for selecting a web site, and used by default.
13   * A different behavior can be set by AutocompleteListItem.itemConstructor.
14   * @param {Object} pageInfo The page this item represents.
15   * @constructor
16   * @extends {cr.ui.ListItem}
17   */
18  function AutocompleteListItem(pageInfo) {
19    var el = cr.doc.createElement('div');
20    el.pageInfo_ = pageInfo;
21    AutocompleteListItem.decorate(el);
22    return el;
23  }
24
25  /**
26   * Decorates an element as an autocomplete list item.
27   * @param {!HTMLElement} el The element to decorate.
28   */
29  AutocompleteListItem.decorate = function(el) {
30    el.__proto__ = AutocompleteListItem.prototype;
31    el.decorate();
32  };
33
34  AutocompleteListItem.prototype = {
35    __proto__: ListItem.prototype,
36
37    /** @override */
38    decorate: function() {
39      ListItem.prototype.decorate.call(this);
40
41      var title = this.pageInfo_['title'];
42      var url = this.pageInfo_['displayURL'];
43      var titleEl = this.ownerDocument.createElement('span');
44      titleEl.className = 'title';
45      titleEl.textContent = title || url;
46      this.appendChild(titleEl);
47
48      if (title && title.length > 0 && url != title) {
49        var separatorEl = this.ownerDocument.createTextNode(' - ');
50        this.appendChild(separatorEl);
51
52        var urlEl = this.ownerDocument.createElement('span');
53        urlEl.className = 'url';
54        urlEl.textContent = url;
55        this.appendChild(urlEl);
56      }
57    },
58  };
59
60  /**
61   * Creates a new autocomplete list popup.
62   * @constructor
63   * @extends {cr.ui.List}
64   */
65  var AutocompleteList = cr.ui.define('list');
66
67  AutocompleteList.prototype = {
68    __proto__: List.prototype,
69
70    /**
71     * The text field the autocomplete popup is currently attached to, if any.
72     * @type {HTMLElement}
73     * @private
74     */
75    targetInput_: null,
76
77    /**
78     * Keydown event listener to attach to a text field.
79     * @type {Function}
80     * @private
81     */
82    textFieldKeyHandler_: null,
83
84    /**
85     * Input event listener to attach to a text field.
86     * @type {Function}
87     * @private
88     */
89    textFieldInputHandler_: null,
90
91    /** @override */
92    decorate: function() {
93      List.prototype.decorate.call(this);
94      this.classList.add('autocomplete-suggestions');
95      this.selectionModel = new cr.ui.ListSingleSelectionModel;
96
97      this.itemConstructor = AutocompleteListItem;
98      this.textFieldKeyHandler_ = this.handleAutocompleteKeydown_.bind(this);
99      var self = this;
100      this.textFieldInputHandler_ = function(e) {
101        self.requestSuggestions(self.targetInput_.value);
102      };
103      this.addEventListener('change', function(e) {
104        if (self.selectedItem)
105          self.handleSelectedSuggestion(self.selectedItem);
106      });
107      // Start hidden; adding suggestions will unhide.
108      this.hidden = true;
109    },
110
111    /** @override */
112    createItem: function(pageInfo) {
113      return new this.itemConstructor(pageInfo);
114    },
115
116    /**
117     * The suggestions to show.
118     * @type {Array}
119     */
120    set suggestions(suggestions) {
121      this.dataModel = new ArrayDataModel(suggestions);
122      this.hidden = !this.targetInput_ || suggestions.length == 0;
123    },
124
125    /**
126     * Requests new suggestions. Called when new suggestions are needed.
127     * @param {string} query the text to autocomplete from.
128     */
129    requestSuggestions: function(query) {
130    },
131
132    /**
133     * Handles the Enter keydown event.
134     * By default, clears and hides the autocomplete popup. Note that the
135     * keydown event bubbles up, so the input field can handle the event.
136     */
137    handleEnterKeydown: function() {
138      this.suggestions = [];
139    },
140
141    /**
142     * Handles the selected suggestion. Called when a suggestion is selected.
143     * By default, sets the target input element's value to the 'url' field
144     * of the selected suggestion.
145     * @param {Object} selectedSuggestion
146     */
147    handleSelectedSuggestion: function(selectedSuggestion) {
148      var input = this.targetInput_;
149      if (!input)
150        return;
151      input.value = selectedSuggestion['url'];
152      // Programatically change the value won't trigger a change event, but
153      // clients are likely to want to know when changes happen, so fire one.
154      cr.dispatchSimpleEvent(input, 'change', true);
155    },
156
157    /**
158     * Attaches the popup to the given input element. Requires
159     * that the input be wrapped in a block-level container of the same width.
160     * @param {HTMLElement} input The input element to attach to.
161     */
162    attachToInput: function(input) {
163      if (this.targetInput_ == input)
164        return;
165
166      this.detach();
167      this.targetInput_ = input;
168      this.style.width = input.getBoundingClientRect().width + 'px';
169      this.hidden = false;  // Necessary for positionPopupAroundElement to work.
170      cr.ui.positionPopupAroundElement(input, this, cr.ui.AnchorType.BELOW);
171      // Start hidden; when the data model gets results the list will show.
172      this.hidden = true;
173
174      input.addEventListener('keydown', this.textFieldKeyHandler_, true);
175      input.addEventListener('input', this.textFieldInputHandler_);
176
177      if (!this.boundSyncWidthAndPositionToInput_) {
178        this.boundSyncWidthAndPositionToInput_ =
179            this.syncWidthAndPositionToInput.bind(this);
180      }
181      // We need to call syncWidthAndPositionToInput whenever page zoom level or
182      // page size is changed.
183      window.addEventListener('resize', this.boundSyncWidthAndPositionToInput_);
184    },
185
186    /**
187     * Detaches the autocomplete popup from its current input element, if any.
188     */
189    detach: function() {
190      var input = this.targetInput_;
191      if (!input)
192        return;
193
194      input.removeEventListener('keydown', this.textFieldKeyHandler_, true);
195      input.removeEventListener('input', this.textFieldInputHandler_);
196      this.targetInput_ = null;
197      this.suggestions = [];
198      if (this.boundSyncWidthAndPositionToInput_) {
199        window.removeEventListener(
200            'resize', this.boundSyncWidthAndPositionToInput_);
201      }
202    },
203
204    /**
205     * Makes sure that the suggestion list matches the width and the position
206     * of the input it is attached to. Should be called any time the input is
207     * resized.
208     */
209    syncWidthAndPositionToInput: function() {
210      var input = this.targetInput_;
211      if (input) {
212        this.style.width = input.getBoundingClientRect().width + 'px';
213        cr.ui.positionPopupAroundElement(input, this, cr.ui.AnchorType.BELOW);
214      }
215    },
216
217    /**
218     * syncWidthAndPositionToInput function bound to |this|.
219     * @type {!Function|undefined}
220     * @private
221     */
222    boundSyncWidthAndPositionToInput_: undefined,
223
224    /**
225     * @return {HTMLElement} The text field the autocomplete popup is currently
226     *     attached to, if any.
227     */
228    get targetInput() {
229      return this.targetInput_;
230    },
231
232    /**
233     * Handles input field key events that should be interpreted as autocomplete
234     * commands.
235     * @param {Event} event The keydown event.
236     * @private
237     */
238    handleAutocompleteKeydown_: function(event) {
239      if (this.hidden)
240        return;
241      var handled = false;
242      switch (event.keyIdentifier) {
243        case 'U+001B':  // Esc
244          this.suggestions = [];
245          handled = true;
246          break;
247        case 'Enter':
248          // If the user has already selected an item using the arrow keys then
249          // presses Enter, keep |handled| = false, so the input field can
250          // handle the event as well.
251          this.handleEnterKeydown();
252          break;
253        case 'Up':
254        case 'Down':
255          var newEvent = new Event(event.type);
256          newEvent.keyIdentifier = event.keyIdentifier;
257          this.dispatchEvent(newEvent);
258          handled = true;
259          break;
260      }
261      // Don't let arrow keys affect the text field, or bubble up to, e.g.,
262      // an enclosing list item.
263      if (handled) {
264        event.preventDefault();
265        event.stopPropagation();
266      }
267    },
268  };
269
270  return {
271    AutocompleteList: AutocompleteList
272  };
273});
274