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