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
5cr.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