OLD | NEW |
(Empty) | |
| 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 * Makes sure that the suggestion list matches the width of the input it is. |
| 184 * attached to. Should be called any time the input is resized. |
| 185 */ |
| 186 syncWidthToInput: function() { |
| 187 var input = this.targetInput_ |
| 188 if (input) |
| 189 this.style.width = input.getBoundingClientRect().width + 'px'; |
| 190 }, |
| 191 |
| 192 /** |
| 193 * The text field the autocomplete popup is currently attached to, if any. |
| 194 * @return {HTMLElement} |
| 195 */ |
| 196 get targetInput() { |
| 197 return this.targetInput_; |
| 198 }, |
| 199 |
| 200 /** |
| 201 * Handles input field key events that should be interpreted as autocomplete |
| 202 * commands. |
| 203 * @param {Event} event The keydown event. |
| 204 * @private |
| 205 */ |
| 206 handleAutocompleteKeydown_: function(event) { |
| 207 if (this.hidden) |
| 208 return; |
| 209 var handled = false; |
| 210 switch (event.keyIdentifier) { |
| 211 case 'U+001B': // Esc |
| 212 this.suggestions = []; |
| 213 handled = true; |
| 214 break; |
| 215 case 'Enter': |
| 216 var hadSelection = this.selectedItem != null; |
| 217 this.suggestions = []; |
| 218 // Only count the event as handled if a selection is being commited. |
| 219 handled = hadSelection; |
| 220 break; |
| 221 case 'Up': |
| 222 case 'Down': |
| 223 this.dispatchEvent(event); |
| 224 handled = true; |
| 225 break; |
| 226 } |
| 227 // Don't let arrow keys affect the text field, or bubble up to, e.g., |
| 228 // an enclosing list item. |
| 229 if (handled) { |
| 230 event.preventDefault(); |
| 231 event.stopPropagation(); |
| 232 } |
| 233 }, |
| 234 }; |
| 235 |
| 236 return { |
| 237 AutocompleteList: AutocompleteList |
| 238 }; |
| 239 }); |
OLD | NEW |