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 5cr.define('options.search_engines', function() { 6 /** @const */ var ControlledSettingIndicator = 7 options.ControlledSettingIndicator; 8 /** @const */ var InlineEditableItemList = options.InlineEditableItemList; 9 /** @const */ var InlineEditableItem = options.InlineEditableItem; 10 /** @const */ var ListSelectionController = cr.ui.ListSelectionController; 11 12 /** 13 * Creates a new search engine list item. 14 * @param {Object} searchEnigne The search engine this represents. 15 * @constructor 16 * @extends {cr.ui.ListItem} 17 */ 18 function SearchEngineListItem(searchEngine) { 19 var el = cr.doc.createElement('div'); 20 el.searchEngine_ = searchEngine; 21 SearchEngineListItem.decorate(el); 22 return el; 23 } 24 25 /** 26 * Decorates an element as a search engine list item. 27 * @param {!HTMLElement} el The element to decorate. 28 */ 29 SearchEngineListItem.decorate = function(el) { 30 el.__proto__ = SearchEngineListItem.prototype; 31 el.decorate(); 32 }; 33 34 SearchEngineListItem.prototype = { 35 __proto__: InlineEditableItem.prototype, 36 37 /** 38 * Input field for editing the engine name. 39 * @type {HTMLElement} 40 * @private 41 */ 42 nameField_: null, 43 44 /** 45 * Input field for editing the engine keyword. 46 * @type {HTMLElement} 47 * @private 48 */ 49 keywordField_: null, 50 51 /** 52 * Input field for editing the engine url. 53 * @type {HTMLElement} 54 * @private 55 */ 56 urlField_: null, 57 58 /** 59 * Whether or not an input validation request is currently outstanding. 60 * @type {boolean} 61 * @private 62 */ 63 waitingForValidation_: false, 64 65 /** 66 * Whether or not the current set of input is known to be valid. 67 * @type {boolean} 68 * @private 69 */ 70 currentlyValid_: false, 71 72 /** @override */ 73 decorate: function() { 74 InlineEditableItem.prototype.decorate.call(this); 75 76 var engine = this.searchEngine_; 77 78 if (engine.modelIndex == '-1') { 79 this.isPlaceholder = true; 80 engine.name = ''; 81 engine.keyword = ''; 82 engine.url = ''; 83 } 84 85 this.currentlyValid_ = !this.isPlaceholder; 86 87 if (engine.default) 88 this.classList.add('default'); 89 90 this.deletable = engine.canBeRemoved; 91 92 // Construct the name column. 93 var nameColEl = this.ownerDocument.createElement('div'); 94 nameColEl.className = 'name-column'; 95 nameColEl.classList.add('weakrtl'); 96 this.contentElement.appendChild(nameColEl); 97 98 // Add the favicon. 99 var faviconDivEl = this.ownerDocument.createElement('div'); 100 faviconDivEl.className = 'favicon'; 101 if (!this.isPlaceholder) { 102 faviconDivEl.style.backgroundImage = imageset( 103 'chrome://favicon/size/16@scalefactorx/iconurl/' + engine.iconURL); 104 } 105 nameColEl.appendChild(faviconDivEl); 106 107 var nameEl = this.createEditableTextCell(engine.displayName); 108 nameEl.classList.add('weakrtl'); 109 nameColEl.appendChild(nameEl); 110 111 // Then the keyword column. 112 var keywordEl = this.createEditableTextCell(engine.keyword); 113 keywordEl.className = 'keyword-column'; 114 keywordEl.classList.add('weakrtl'); 115 this.contentElement.appendChild(keywordEl); 116 117 // And the URL column. 118 var urlEl = this.createEditableTextCell(engine.url); 119 // Extensions should not display a URL column. 120 if (!engine.isExtension) { 121 var urlWithButtonEl = this.ownerDocument.createElement('div'); 122 urlWithButtonEl.appendChild(urlEl); 123 urlWithButtonEl.className = 'url-column'; 124 urlWithButtonEl.classList.add('weakrtl'); 125 this.contentElement.appendChild(urlWithButtonEl); 126 // Add the Make Default button. Temporary until drag-and-drop 127 // re-ordering is implemented. When this is removed, remove the extra 128 // div above. 129 if (engine.canBeDefault) { 130 var makeDefaultButtonEl = this.ownerDocument.createElement('button'); 131 makeDefaultButtonEl.className = 132 'custom-appearance list-inline-button'; 133 makeDefaultButtonEl.textContent = 134 loadTimeData.getString('makeDefaultSearchEngineButton'); 135 makeDefaultButtonEl.onclick = function(e) { 136 chrome.send('managerSetDefaultSearchEngine', [engine.modelIndex]); 137 }; 138 makeDefaultButtonEl.onmousedown = function(e) { 139 // Don't select the row when clicking the button. 140 e.stopPropagation(); 141 // Don't focus on the button. 142 e.preventDefault(); 143 }; 144 urlWithButtonEl.appendChild(makeDefaultButtonEl); 145 } 146 } 147 148 // Do final adjustment to the input fields. 149 this.nameField_ = nameEl.querySelector('input'); 150 // The editable field uses the raw name, not the display name. 151 this.nameField_.value = engine.name; 152 this.keywordField_ = keywordEl.querySelector('input'); 153 this.urlField_ = urlEl.querySelector('input'); 154 155 if (engine.urlLocked) 156 this.urlField_.disabled = true; 157 158 if (engine.isExtension) 159 this.nameField_.disabled = true; 160 161 if (this.isPlaceholder) { 162 this.nameField_.placeholder = 163 loadTimeData.getString('searchEngineTableNamePlaceholder'); 164 this.keywordField_.placeholder = 165 loadTimeData.getString('searchEngineTableKeywordPlaceholder'); 166 this.urlField_.placeholder = 167 loadTimeData.getString('searchEngineTableURLPlaceholder'); 168 } 169 170 var fields = [this.nameField_, this.keywordField_, this.urlField_]; 171 for (var i = 0; i < fields.length; i++) { 172 fields[i].oninput = this.startFieldValidation_.bind(this); 173 } 174 175 // Listen for edit events. 176 if (engine.canBeEdited) { 177 this.addEventListener('edit', this.onEditStarted_.bind(this)); 178 this.addEventListener('canceledit', this.onEditCancelled_.bind(this)); 179 this.addEventListener('commitedit', this.onEditCommitted_.bind(this)); 180 } else { 181 this.editable = false; 182 this.querySelector('.row-delete-button').hidden = true; 183 var indicator = ControlledSettingIndicator(); 184 indicator.setAttribute('setting', 'search-engine'); 185 // Create a synthetic pref change event decorated as 186 // CoreOptionsHandler::CreateValueForPref() does. 187 var event = new Event(this.contentType); 188 if (engine.extension) { 189 event.value = { controlledBy: 'extension' }; 190 // TODO(mad): add id, name, and icon once we solved the issue with the 191 // search engine manager in http://crbug.com/314507. 192 } else { 193 event.value = { controlledBy: 'policy' }; 194 } 195 indicator.handlePrefChange(event); 196 this.appendChild(indicator); 197 } 198 }, 199 200 /** @override */ 201 get currentInputIsValid() { 202 return !this.waitingForValidation_ && this.currentlyValid_; 203 }, 204 205 /** @override */ 206 get hasBeenEdited() { 207 var engine = this.searchEngine_; 208 return this.nameField_.value != engine.name || 209 this.keywordField_.value != engine.keyword || 210 this.urlField_.value != engine.url; 211 }, 212 213 /** 214 * Called when entering edit mode; starts an edit session in the model. 215 * @param {Event} e The edit event. 216 * @private 217 */ 218 onEditStarted_: function(e) { 219 var editIndex = this.searchEngine_.modelIndex; 220 chrome.send('editSearchEngine', [String(editIndex)]); 221 this.startFieldValidation_(); 222 }, 223 224 /** 225 * Called when committing an edit; updates the model. 226 * @param {Event} e The end event. 227 * @private 228 */ 229 onEditCommitted_: function(e) { 230 chrome.send('searchEngineEditCompleted', this.getInputFieldValues_()); 231 }, 232 233 /** 234 * Called when cancelling an edit; informs the model and resets the control 235 * states. 236 * @param {Event} e The cancel event. 237 * @private 238 */ 239 onEditCancelled_: function() { 240 chrome.send('searchEngineEditCancelled'); 241 242 // The name field has been automatically set to match the display name, 243 // but it should use the raw name instead. 244 this.nameField_.value = this.searchEngine_.name; 245 this.currentlyValid_ = !this.isPlaceholder; 246 }, 247 248 /** 249 * Returns the input field values as an array suitable for passing to 250 * chrome.send. The order of the array is important. 251 * @private 252 * @return {array} The current input field values. 253 */ 254 getInputFieldValues_: function() { 255 return [this.nameField_.value, 256 this.keywordField_.value, 257 this.urlField_.value]; 258 }, 259 260 /** 261 * Begins the process of asynchronously validing the input fields. 262 * @private 263 */ 264 startFieldValidation_: function() { 265 this.waitingForValidation_ = true; 266 var args = this.getInputFieldValues_(); 267 args.push(this.searchEngine_.modelIndex); 268 chrome.send('checkSearchEngineInfoValidity', args); 269 }, 270 271 /** 272 * Callback for the completion of an input validition check. 273 * @param {Object} validity A dictionary of validitation results. 274 */ 275 validationComplete: function(validity) { 276 this.waitingForValidation_ = false; 277 // TODO(stuartmorgan): Implement the full validation UI with 278 // checkmark/exclamation mark icons and tooltips showing the errors. 279 if (validity.name) { 280 this.nameField_.setCustomValidity(''); 281 } else { 282 this.nameField_.setCustomValidity( 283 loadTimeData.getString('editSearchEngineInvalidTitleToolTip')); 284 } 285 286 if (validity.keyword) { 287 this.keywordField_.setCustomValidity(''); 288 } else { 289 this.keywordField_.setCustomValidity( 290 loadTimeData.getString('editSearchEngineInvalidKeywordToolTip')); 291 } 292 293 if (validity.url) { 294 this.urlField_.setCustomValidity(''); 295 } else { 296 this.urlField_.setCustomValidity( 297 loadTimeData.getString('editSearchEngineInvalidURLToolTip')); 298 } 299 300 this.currentlyValid_ = validity.name && validity.keyword && validity.url; 301 }, 302 }; 303 304 var SearchEngineList = cr.ui.define('list'); 305 306 SearchEngineList.prototype = { 307 __proto__: InlineEditableItemList.prototype, 308 309 /** @override */ 310 createItem: function(searchEngine) { 311 return new SearchEngineListItem(searchEngine); 312 }, 313 314 /** @override */ 315 deleteItemAtIndex: function(index) { 316 var modelIndex = this.dataModel.item(index).modelIndex; 317 chrome.send('removeSearchEngine', [String(modelIndex)]); 318 }, 319 320 /** 321 * Passes the results of an input validation check to the requesting row 322 * if it's still being edited. 323 * @param {number} modelIndex The model index of the item that was checked. 324 * @param {Object} validity A dictionary of validitation results. 325 */ 326 validationComplete: function(validity, modelIndex) { 327 // If it's not still being edited, it no longer matters. 328 var currentSelection = this.selectedItem; 329 if (!currentSelection) 330 return; 331 var listItem = this.getListItem(currentSelection); 332 if (listItem.editing && currentSelection.modelIndex == modelIndex) 333 listItem.validationComplete(validity); 334 }, 335 }; 336 337 // Export 338 return { 339 SearchEngineList: SearchEngineList 340 }; 341 342}); 343 344