OLD | NEW |
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 cr.define('options.search_engines', function() { | 5 cr.define('options.search_engines', function() { |
6 const DeletableItem = options.DeletableItem; | 6 const InlineEditableItemList = options.InlineEditableItemList; |
7 const DeletableItemList = options.DeletableItemList; | 7 const InlineEditableItem = options.InlineEditableItem; |
8 const ListInlineHeaderSelectionController = | 8 const ListInlineHeaderSelectionController = |
9 options.ListInlineHeaderSelectionController; | 9 options.ListInlineHeaderSelectionController; |
10 | 10 |
11 /** | 11 /** |
12 * Creates a new search engine list item. | 12 * Creates a new search engine list item. |
13 * @param {Object} searchEnigne The search engine this represents. | 13 * @param {Object} searchEnigne The search engine this represents. |
14 * @constructor | 14 * @constructor |
15 * @extends {cr.ui.ListItem} | 15 * @extends {cr.ui.ListItem} |
16 */ | 16 */ |
17 function SearchEngineListItem(searchEngine) { | 17 function SearchEngineListItem(searchEngine) { |
18 var el = cr.doc.createElement('div'); | 18 var el = cr.doc.createElement('div'); |
19 el.searchEngine_ = searchEngine; | 19 el.searchEngine_ = searchEngine; |
20 SearchEngineListItem.decorate(el); | 20 SearchEngineListItem.decorate(el); |
21 return el; | 21 return el; |
22 } | 22 } |
23 | 23 |
24 /** | 24 /** |
25 * Decorates an element as a search engine list item. | 25 * Decorates an element as a search engine list item. |
26 * @param {!HTMLElement} el The element to decorate. | 26 * @param {!HTMLElement} el The element to decorate. |
27 */ | 27 */ |
28 SearchEngineListItem.decorate = function(el) { | 28 SearchEngineListItem.decorate = function(el) { |
29 el.__proto__ = SearchEngineListItem.prototype; | 29 el.__proto__ = SearchEngineListItem.prototype; |
30 el.decorate(); | 30 el.decorate(); |
31 }; | 31 }; |
32 | 32 |
33 SearchEngineListItem.prototype = { | 33 SearchEngineListItem.prototype = { |
34 __proto__: DeletableItem.prototype, | 34 __proto__: InlineEditableItem.prototype, |
| 35 |
| 36 /** |
| 37 * Input field for editing the engine name. |
| 38 * @type {HTMLElement} |
| 39 * @private |
| 40 */ |
| 41 nameField_: null, |
| 42 |
| 43 /** |
| 44 * Input field for editing the engine keyword. |
| 45 * @type {HTMLElement} |
| 46 * @private |
| 47 */ |
| 48 keywordField_: null, |
| 49 |
| 50 /** |
| 51 * Input field for editing the engine url. |
| 52 * @type {HTMLElement} |
| 53 * @private |
| 54 */ |
| 55 urlField_: null, |
| 56 |
| 57 /** |
| 58 * Whether or not this is a placeholder for adding an engine. |
| 59 * @type {boolean} |
| 60 * @private |
| 61 */ |
| 62 isPlaceholder_: false, |
| 63 |
| 64 /** |
| 65 * Whether or not an input validation request is currently outstanding. |
| 66 * @type {boolean} |
| 67 * @private |
| 68 */ |
| 69 waitingForValidation_: false, |
| 70 |
| 71 /** |
| 72 * Whether or not the current set of input is known to be valid. |
| 73 * @type {boolean} |
| 74 * @private |
| 75 */ |
| 76 currentlyValid_: false, |
35 | 77 |
36 /** @inheritDoc */ | 78 /** @inheritDoc */ |
37 decorate: function() { | 79 decorate: function() { |
38 DeletableItem.prototype.decorate.call(this); | 80 InlineEditableItem.prototype.decorate.call(this); |
39 | 81 |
40 var engine = this.searchEngine_; | 82 var engine = this.searchEngine_; |
41 | 83 |
42 if (engine['heading']) | 84 if (engine['modelIndex'] == '-1') { |
| 85 this.isPlaceholder_ = true; |
| 86 engine['name'] = ''; |
| 87 engine['keyword'] = ''; |
| 88 engine['url'] = ''; |
| 89 } |
| 90 |
| 91 this.currentlyValid_ = !this.isPlaceholder_; |
| 92 |
| 93 if (engine['heading']) { |
43 this.classList.add('heading'); | 94 this.classList.add('heading'); |
44 else if (engine['default']) | 95 this.editable = false; |
| 96 } else if (engine['default']) { |
45 this.classList.add('default'); | 97 this.classList.add('default'); |
| 98 } |
46 | 99 |
47 this.deletable = engine['canBeRemoved']; | 100 this.deletable = engine['canBeRemoved']; |
48 | 101 |
49 var nameEl = this.ownerDocument.createElement('div'); | 102 var nameText = engine['name']; |
50 nameEl.className = 'name'; | 103 var keywordText = engine['keyword']; |
| 104 var urlText = engine['url']; |
51 if (engine['heading']) { | 105 if (engine['heading']) { |
52 nameEl.textContent = engine['heading']; | 106 nameText = engine['heading']; |
53 } else { | 107 keywordText = localStrings.getString('searchEngineTableKeywordHeader'); |
54 nameEl.textContent = engine['name']; | 108 urlText = localStrings.getString('searchEngineTableURLHeader'); |
55 nameEl.classList.add('favicon-cell'); | 109 } |
56 nameEl.style.backgroundImage = url('chrome://favicon/iconurl/' + | 110 |
57 engine['iconURL']); | 111 // Construct the name column. |
58 } | 112 var nameColEl = this.ownerDocument.createElement('div'); |
59 this.contentElement.appendChild(nameEl); | 113 nameColEl.className = 'name-column'; |
60 | 114 this.contentElement.appendChild(nameColEl); |
61 var keywordEl = this.ownerDocument.createElement('div'); | 115 |
62 keywordEl.className = 'keyword'; | 116 // For non-heading rows, start with a favicon. |
63 keywordEl.textContent = engine['heading'] ? | 117 if (!engine['heading']) { |
64 localStrings.getString('searchEngineTableKeywordHeader') : | 118 var faviconDivEl = this.ownerDocument.createElement('div'); |
65 engine['keyword']; | 119 faviconDivEl.className = 'favicon'; |
| 120 var imgEl = this.ownerDocument.createElement('img'); |
| 121 imgEl.src = 'chrome://favicon/iconurl/' + engine['iconURL']; |
| 122 faviconDivEl.appendChild(imgEl); |
| 123 nameColEl.appendChild(faviconDivEl); |
| 124 } |
| 125 |
| 126 var nameEl = this.createEditableTextCell_(nameText); |
| 127 nameColEl.appendChild(nameEl); |
| 128 |
| 129 // Then the keyword column. |
| 130 var keywordEl = this.createEditableTextCell_(keywordText); |
| 131 keywordEl.className = 'keyword-column'; |
66 this.contentElement.appendChild(keywordEl); | 132 this.contentElement.appendChild(keywordEl); |
| 133 |
| 134 // And the URL column. |
| 135 var urlEl = this.createEditableTextCell_(urlText); |
| 136 urlEl.className = 'url-column'; |
| 137 this.contentElement.appendChild(urlEl); |
| 138 |
| 139 // Do final adjustment to the input fields. |
| 140 if (!engine['heading']) { |
| 141 this.nameField_ = nameEl.querySelector('input'); |
| 142 this.keywordField_ = keywordEl.querySelector('input'); |
| 143 this.urlField_ = urlEl.querySelector('input'); |
| 144 |
| 145 if (engine['urlLocked']) |
| 146 this.urlField_.disabled = true; |
| 147 |
| 148 if (this.isPlaceholder_) { |
| 149 this.nameField_.placeholder = |
| 150 localStrings.getString('searchEngineTableNamePlaceholder'); |
| 151 this.keywordField_.placeholder = |
| 152 localStrings.getString('searchEngineTableKeywordPlaceholder'); |
| 153 this.urlField_.placeholder = |
| 154 localStrings.getString('searchEngineTableURLPlaceholder'); |
| 155 } |
| 156 |
| 157 var fields = [ this.nameField_, this.keywordField_, this.urlField_ ]; |
| 158 for (var i = 0; i < fields.length; i++) { |
| 159 fields[i].oninput = this.startFieldValidation_.bind(this); |
| 160 } |
| 161 } |
| 162 |
| 163 // Listen for edit events. |
| 164 this.addEventListener('edit', this.onEditStarted_.bind(this)); |
| 165 this.addEventListener('canceledit', this.onEditCancelled_.bind(this)); |
| 166 this.addEventListener('commitedit', this.onEditCommitted_.bind(this)); |
| 167 }, |
| 168 |
| 169 /** |
| 170 * Returns a div containing an <input>, as well as static text if needed. |
| 171 * @param {string} text The text of the cell. |
| 172 * @return {HTMLElement} The HTML element for the cell. |
| 173 * @private |
| 174 */ |
| 175 createEditableTextCell_: function(text) { |
| 176 var container = this.ownerDocument.createElement('div'); |
| 177 |
| 178 if (!this.isPlaceholder_) { |
| 179 var textEl = this.ownerDocument.createElement('div'); |
| 180 textEl.className = 'static-text'; |
| 181 textEl.textContent = text; |
| 182 textEl.setAttribute('editmode', false); |
| 183 container.appendChild(textEl); |
| 184 } |
| 185 |
| 186 var inputEl = this.ownerDocument.createElement('input'); |
| 187 inputEl.type = 'text'; |
| 188 inputEl.value = text; |
| 189 if (!this.isPlaceholder_) { |
| 190 inputEl.setAttribute('editmode', true); |
| 191 inputEl.staticVersion = textEl; |
| 192 } |
| 193 container.appendChild(inputEl); |
| 194 |
| 195 return container; |
| 196 }, |
| 197 |
| 198 /** @inheritDoc */ |
| 199 get initialFocusElement() { |
| 200 return this.nameField_; |
| 201 }, |
| 202 |
| 203 /** @inheritDoc */ |
| 204 get currentInputIsValid() { |
| 205 return !this.waitingForValidation_ && this.currentlyValid_; |
| 206 }, |
| 207 |
| 208 /** @inheritDoc */ |
| 209 hasBeenEdited: function(e) { |
| 210 var engine = this.searchEngine_; |
| 211 return this.nameField_.value != engine['name'] || |
| 212 this.keywordField_.value != engine['keyword'] || |
| 213 this.urlField_.value != engine['url']; |
| 214 }, |
| 215 |
| 216 /** |
| 217 * Called when entering edit mode; starts an edit session in the model. |
| 218 * @param {Event} e The edit event. |
| 219 * @private |
| 220 */ |
| 221 onEditStarted_: function(e) { |
| 222 var editIndex = this.searchEngine_['modelIndex']; |
| 223 chrome.send('editSearchEngine', [String(editIndex)]); |
| 224 }, |
| 225 |
| 226 /** |
| 227 * Called when committing an edit; updates the model. |
| 228 * @param {Event} e The end event. |
| 229 * @private |
| 230 */ |
| 231 onEditCommitted_: function(e) { |
| 232 chrome.send('searchEngineEditCompleted', this.getInputFieldValues_()); |
| 233 // Update the static version immediately to prevent flickering before |
| 234 // the model update callback updates the UI. |
| 235 var editFields = [ this.nameField_, this.keywordField_, this.urlField_ ]; |
| 236 for (var i = 0; i < editFields.length; i++) { |
| 237 var staticLabel = editFields[i].staticVersion; |
| 238 if (staticLabel) |
| 239 staticLabel.textContent = editFields[i].value; |
| 240 } |
| 241 }, |
| 242 |
| 243 /** |
| 244 * Called when cancelling an edit; informs the model and resets the control |
| 245 * states. |
| 246 * @param {Event} e The cancel event. |
| 247 * @private |
| 248 */ |
| 249 onEditCancelled_: function() { |
| 250 chrome.send('searchEngineEditCancelled'); |
| 251 var engine = this.searchEngine_; |
| 252 this.nameField_.value = engine['name']; |
| 253 this.keywordField_.value = engine['keyword']; |
| 254 this.urlField_.value = engine['url']; |
| 255 |
| 256 var editFields = [ this.nameField_, this.keywordField_, this.urlField_ ]; |
| 257 for (var i = 0; i < editFields.length; i++) { |
| 258 editFields[i].classList.remove('invalid'); |
| 259 } |
| 260 this.currentlyValid_ = !this.isPlaceholder_; |
| 261 }, |
| 262 |
| 263 /** |
| 264 * Returns the input field values as an array suitable for passing to |
| 265 * chrome.send. The order of the array is important. |
| 266 * @private |
| 267 * @return {array} The current input field values. |
| 268 */ |
| 269 getInputFieldValues_: function() { |
| 270 return [ this.nameField_.value, |
| 271 this.keywordField_.value, |
| 272 this.urlField_.value ]; |
| 273 }, |
| 274 |
| 275 /** |
| 276 * Begins the process of asynchronously validing the input fields. |
| 277 * @private |
| 278 */ |
| 279 startFieldValidation_: function() { |
| 280 this.waitingForValidation_ = true; |
| 281 var args = this.getInputFieldValues_(); |
| 282 args.push(this.searchEngine_['modelIndex']); |
| 283 chrome.send('checkSearchEngineInfoValidity', args); |
| 284 }, |
| 285 |
| 286 /** |
| 287 * Callback for the completion of an input validition check. |
| 288 * @param {Object} validity A dictionary of validitation results. |
| 289 */ |
| 290 validationComplete: function(validity) { |
| 291 this.waitingForValidation_ = false; |
| 292 // TODO(stuartmorgan): Implement the full validation UI with |
| 293 // checkmark/exclamation mark icons and tooltips. |
| 294 if (validity['name']) |
| 295 this.nameField_.classList.remove('invalid'); |
| 296 else |
| 297 this.nameField_.classList.add('invalid'); |
| 298 |
| 299 if (validity['keyword']) |
| 300 this.keywordField_.classList.remove('invalid'); |
| 301 else |
| 302 this.keywordField_.classList.add('invalid'); |
| 303 |
| 304 if (validity['url']) |
| 305 this.urlField_.classList.remove('invalid'); |
| 306 else |
| 307 this.urlField_.classList.add('invalid'); |
| 308 |
| 309 this.currentlyValid_ = validity['name'] && validity['keyword'] && |
| 310 validity['url']; |
67 }, | 311 }, |
68 }; | 312 }; |
69 | 313 |
70 var SearchEngineList = cr.ui.define('list'); | 314 var SearchEngineList = cr.ui.define('list'); |
71 | 315 |
72 SearchEngineList.prototype = { | 316 SearchEngineList.prototype = { |
73 __proto__: DeletableItemList.prototype, | 317 __proto__: InlineEditableItemList.prototype, |
74 | 318 |
75 /** @inheritDoc */ | 319 /** @inheritDoc */ |
76 createItem: function(searchEngine) { | 320 createItem: function(searchEngine) { |
77 return new SearchEngineListItem(searchEngine); | 321 return new SearchEngineListItem(searchEngine); |
78 }, | 322 }, |
79 | 323 |
80 /** @inheritDoc */ | 324 /** @inheritDoc */ |
81 createSelectionController: function(sm) { | 325 createSelectionController: function(sm) { |
82 return new ListInlineHeaderSelectionController(sm, this); | 326 return new ListInlineHeaderSelectionController(sm, this); |
83 }, | 327 }, |
84 | 328 |
85 /** @inheritDoc */ | 329 /** @inheritDoc */ |
86 deleteItemAtIndex: function(index) { | 330 deleteItemAtIndex: function(index) { |
87 var modelIndex = this.dataModel.item(index)['modelIndex'] | 331 var modelIndex = this.dataModel.item(index)['modelIndex'] |
88 chrome.send('removeSearchEngine', [String(modelIndex)]); | 332 chrome.send('removeSearchEngine', [String(modelIndex)]); |
89 }, | 333 }, |
90 | 334 |
91 /** | 335 /** |
92 * Returns true if the given item is selectable. | 336 * Returns true if the given item is selectable. |
93 * @param {number} index The index to check. | 337 * @param {number} index The index to check. |
94 */ | 338 */ |
95 canSelectIndex: function(index) { | 339 canSelectIndex: function(index) { |
96 return !this.dataModel.item(index).hasOwnProperty('heading'); | 340 return !this.dataModel.item(index).hasOwnProperty('heading'); |
97 }, | 341 }, |
| 342 |
| 343 /** |
| 344 * Passes the results of an input validation check to the requesting row |
| 345 * if it's still being edited. |
| 346 * @param {number} modelIndex The model index of the item that was checked. |
| 347 * @param {Object} validity A dictionary of validitation results. |
| 348 */ |
| 349 validationComplete: function(validity, modelIndex) { |
| 350 // If it's not still being edited, it no longer matters. |
| 351 var currentSelection = this.selectedItem; |
| 352 var listItem = this.getListItem(currentSelection); |
| 353 if (listItem.editing && currentSelection['modelIndex'] == modelIndex) |
| 354 listItem.validationComplete(validity); |
| 355 }, |
98 }; | 356 }; |
99 | 357 |
100 // Export | 358 // Export |
101 return { | 359 return { |
102 SearchEngineList: SearchEngineList | 360 SearchEngineList: SearchEngineList |
103 }; | 361 }; |
104 | 362 |
105 }); | 363 }); |
106 | 364 |
OLD | NEW |