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.
5 cr.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;
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.
16 * @extends {cr.ui.ListItem}
18 function AutocompleteListItem(pageInfo) {
19 var el = cr.doc.createElement('div');
20 el.pageInfo_ = pageInfo;
21 AutocompleteListItem.decorate(el);
26 * Decorates an element as an autocomplete list item.
27 * @param {!HTMLElement} el The element to decorate.
29 AutocompleteListItem.decorate = function(el) {
30 el.__proto__ = AutocompleteListItem.prototype;
34 AutocompleteListItem.prototype = {
35 __proto__: ListItem.prototype,
38 decorate: function() {
39 ListItem.prototype.decorate.call(this);
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);
48 if (title && title.length > 0 && url != title) {
49 var separatorEl = this.ownerDocument.createTextNode(' - ');
50 this.appendChild(separatorEl);
52 var urlEl = this.ownerDocument.createElement('span');
53 urlEl.className = 'url';
54 urlEl.textContent = url;
55 this.appendChild(urlEl);
61 * Creates a new autocomplete list popup.
63 * @extends {cr.ui.List}
65 var AutocompleteList = cr.ui.define('list');
67 AutocompleteList.prototype = {
68 __proto__: List.prototype,
71 * The text field the autocomplete popup is currently attached to, if any.
78 * Keydown event listener to attach to a text field.
82 textFieldKeyHandler_: null,
85 * Input event listener to attach to a text field.
89 textFieldInputHandler_: null,
92 decorate: function() {
93 List.prototype.decorate.call(this);
94 this.classList.add('autocomplete-suggestions');
95 this.selectionModel = new cr.ui.ListSingleSelectionModel;
97 this.itemConstructor = AutocompleteListItem;
98 this.textFieldKeyHandler_ = this.handleAutocompleteKeydown_.bind(this);
100 this.textFieldInputHandler_ = function(e) {
101 self.requestSuggestions(self.targetInput_.value);
103 this.addEventListener('change', function(e) {
104 if (self.selectedItem)
105 self.handleSelectedSuggestion(self.selectedItem);
107 // Start hidden; adding suggestions will unhide.
112 createItem: function(pageInfo) {
113 return new this.itemConstructor(pageInfo);
117 * The suggestions to show.
120 set suggestions(suggestions) {
121 this.dataModel = new ArrayDataModel(suggestions);
122 this.hidden = !this.targetInput_ || suggestions.length == 0;
126 * Requests new suggestions. Called when new suggestions are needed.
127 * @param {string} query the text to autocomplete from.
129 requestSuggestions: function(query) {
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.
137 handleEnterKeydown: function() {
138 this.suggestions = [];
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
147 handleSelectedSuggestion: function(selectedSuggestion) {
148 var input = this.targetInput_;
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);
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.
162 attachToInput: function(input) {
163 if (this.targetInput_ == input)
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.
174 input.addEventListener('keydown', this.textFieldKeyHandler_, true);
175 input.addEventListener('input', this.textFieldInputHandler_);
177 if (!this.boundSyncWidthAndPositionToInput_) {
178 this.boundSyncWidthAndPositionToInput_ =
179 this.syncWidthAndPositionToInput.bind(this);
181 // We need to call syncWidthAndPositionToInput whenever page zoom level or
182 // page size is changed.
183 window.addEventListener('resize', this.boundSyncWidthAndPositionToInput_);
187 * Detaches the autocomplete popup from its current input element, if any.
190 var input = this.targetInput_;
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_);
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
209 syncWidthAndPositionToInput: function() {
210 var input = this.targetInput_;
212 this.style.width = input.getBoundingClientRect().width + 'px';
213 cr.ui.positionPopupAroundElement(input, this, cr.ui.AnchorType.BELOW);
218 * syncWidthAndPositionToInput function bound to |this|.
219 * @type {!Function|undefined}
222 boundSyncWidthAndPositionToInput_: undefined,
225 * @return {HTMLElement} The text field the autocomplete popup is currently
226 * attached to, if any.
229 return this.targetInput_;
233 * Handles input field key events that should be interpreted as autocomplete
235 * @param {Event} event The keydown event.
238 handleAutocompleteKeydown_: function(event) {
242 switch (event.keyIdentifier) {
243 case 'U+001B': // Esc
244 this.suggestions = [];
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();
255 var newEvent = new Event(event.type);
256 newEvent.keyIdentifier = event.keyIdentifier;
257 this.dispatchEvent(newEvent);
261 // Don't let arrow keys affect the text field, or bubble up to, e.g.,
262 // an enclosing list item.
264 event.preventDefault();
265 event.stopPropagation();
271 AutocompleteList: AutocompleteList