Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / autocomplete_list.js
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
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;
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 });