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.autofillOptions', function() { 6 /** @const */ var DeletableItem = options.DeletableItem; 7 /** @const */ var DeletableItemList = options.DeletableItemList; 8 /** @const */ var InlineEditableItem = options.InlineEditableItem; 9 /** @const */ var InlineEditableItemList = options.InlineEditableItemList; 10 11 function AutofillEditProfileButton(guid, edit) { 12 var editButtonEl = document.createElement('button'); 13 editButtonEl.className = 'list-inline-button custom-appearance'; 14 editButtonEl.textContent = 15 loadTimeData.getString('autofillEditProfileButton'); 16 editButtonEl.onclick = function(e) { edit(guid); }; 17 18 editButtonEl.onmousedown = function(e) { 19 // Don't select the row when clicking the button. 20 e.stopPropagation(); 21 // Don't focus on the button when clicking it. 22 e.preventDefault(); 23 }; 24 25 return editButtonEl; 26 } 27 28 /** 29 * Creates a new address list item. 30 * @param {Array} entry An array of the form [guid, label]. 31 * @constructor 32 * @extends {options.DeletableItem} 33 */ 34 function AddressListItem(entry) { 35 var el = cr.doc.createElement('div'); 36 el.guid = entry[0]; 37 el.label = entry[1]; 38 el.__proto__ = AddressListItem.prototype; 39 el.decorate(); 40 41 return el; 42 } 43 44 AddressListItem.prototype = { 45 __proto__: DeletableItem.prototype, 46 47 /** @override */ 48 decorate: function() { 49 DeletableItem.prototype.decorate.call(this); 50 51 // The stored label. 52 var label = this.ownerDocument.createElement('div'); 53 label.className = 'autofill-list-item'; 54 label.textContent = this.label; 55 this.contentElement.appendChild(label); 56 57 // The 'Edit' button. 58 var editButtonEl = new AutofillEditProfileButton( 59 this.guid, 60 AutofillOptions.loadAddressEditor); 61 this.contentElement.appendChild(editButtonEl); 62 }, 63 }; 64 65 /** 66 * Creates a new credit card list item. 67 * @param {Array} entry An array of the form [guid, label, icon]. 68 * @constructor 69 * @extends {options.DeletableItem} 70 */ 71 function CreditCardListItem(entry) { 72 var el = cr.doc.createElement('div'); 73 el.guid = entry[0]; 74 el.label = entry[1]; 75 el.icon = entry[2]; 76 el.description = entry[3]; 77 el.__proto__ = CreditCardListItem.prototype; 78 el.decorate(); 79 80 return el; 81 } 82 83 CreditCardListItem.prototype = { 84 __proto__: DeletableItem.prototype, 85 86 /** @override */ 87 decorate: function() { 88 DeletableItem.prototype.decorate.call(this); 89 90 // The stored label. 91 var label = this.ownerDocument.createElement('div'); 92 label.className = 'autofill-list-item'; 93 label.textContent = this.label; 94 this.contentElement.appendChild(label); 95 96 // The credit card icon. 97 var icon = this.ownerDocument.createElement('img'); 98 icon.src = this.icon; 99 icon.alt = this.description; 100 this.contentElement.appendChild(icon); 101 102 // The 'Edit' button. 103 var editButtonEl = new AutofillEditProfileButton( 104 this.guid, 105 AutofillOptions.loadCreditCardEditor); 106 this.contentElement.appendChild(editButtonEl); 107 }, 108 }; 109 110 /** 111 * Creates a new value list item. 112 * @param {AutofillValuesList} list The parent list of this item. 113 * @param {string} entry A string value. 114 * @constructor 115 * @extends {options.InlineEditableItem} 116 */ 117 function ValuesListItem(list, entry) { 118 var el = cr.doc.createElement('div'); 119 el.list = list; 120 el.value = entry ? entry : ''; 121 el.__proto__ = ValuesListItem.prototype; 122 el.decorate(); 123 124 return el; 125 } 126 127 ValuesListItem.prototype = { 128 __proto__: InlineEditableItem.prototype, 129 130 /** @override */ 131 decorate: function() { 132 InlineEditableItem.prototype.decorate.call(this); 133 134 // Note: This must be set prior to calling |createEditableTextCell|. 135 this.isPlaceholder = !this.value; 136 137 // The stored value. 138 var cell = this.createEditableTextCell(this.value); 139 this.contentElement.appendChild(cell); 140 this.input = cell.querySelector('input'); 141 142 if (this.isPlaceholder) { 143 this.input.placeholder = this.list.getAttribute('placeholder'); 144 this.deletable = false; 145 } 146 147 this.addEventListener('commitedit', this.onEditCommitted_); 148 }, 149 150 /** 151 * @return {string} This item's value. 152 * @protected 153 */ 154 value_: function() { 155 return this.input.value; 156 }, 157 158 /** 159 * @param {Object} value The value to test. 160 * @return {boolean} True if the given value is non-empty. 161 * @protected 162 */ 163 valueIsNonEmpty_: function(value) { 164 return !!value; 165 }, 166 167 /** 168 * @return {boolean} True if value1 is logically equal to value2. 169 */ 170 valuesAreEqual_: function(value1, value2) { 171 return value1 === value2; 172 }, 173 174 /** 175 * Clears the item's value. 176 * @protected 177 */ 178 clearValue_: function() { 179 this.input.value = ''; 180 }, 181 182 /** 183 * Called when committing an edit. 184 * If this is an "Add ..." item, committing a non-empty value adds that 185 * value to the end of the values list, but also leaves this "Add ..." item 186 * in place. 187 * @param {Event} e The end event. 188 * @private 189 */ 190 onEditCommitted_: function(e) { 191 var value = this.value_(); 192 var i = this.list.items.indexOf(this); 193 if (i < this.list.dataModel.length && 194 this.valuesAreEqual_(value, this.list.dataModel.item(i))) { 195 return; 196 } 197 198 var entries = this.list.dataModel.slice(); 199 if (this.valueIsNonEmpty_(value) && 200 !entries.some(this.valuesAreEqual_.bind(this, value))) { 201 // Update with new value. 202 if (this.isPlaceholder) { 203 // It is important that updateIndex is done before validateAndSave. 204 // Otherwise we can not be sure about AddRow index. 205 this.list.dataModel.updateIndex(i); 206 this.list.validateAndSave(i, 0, value); 207 } else { 208 this.list.validateAndSave(i, 1, value); 209 } 210 } else { 211 // Reject empty values and duplicates. 212 if (!this.isPlaceholder) 213 this.list.dataModel.splice(i, 1); 214 else 215 this.clearValue_(); 216 } 217 }, 218 }; 219 220 /** 221 * Creates a new name value list item. 222 * @param {AutofillNameValuesList} list The parent list of this item. 223 * @param {array} entry An array of [first, middle, last] names. 224 * @constructor 225 * @extends {options.ValuesListItem} 226 */ 227 function NameListItem(list, entry) { 228 var el = cr.doc.createElement('div'); 229 el.list = list; 230 el.first = entry ? entry[0] : ''; 231 el.middle = entry ? entry[1] : ''; 232 el.last = entry ? entry[2] : ''; 233 el.__proto__ = NameListItem.prototype; 234 el.decorate(); 235 236 return el; 237 } 238 239 NameListItem.prototype = { 240 __proto__: ValuesListItem.prototype, 241 242 /** @override */ 243 decorate: function() { 244 InlineEditableItem.prototype.decorate.call(this); 245 246 // Note: This must be set prior to calling |createEditableTextCell|. 247 this.isPlaceholder = !this.first && !this.middle && !this.last; 248 249 // The stored value. 250 // For the simulated static "input element" to display correctly, the 251 // value must not be empty. We use a space to force the UI to render 252 // correctly when the value is logically empty. 253 var cell = this.createEditableTextCell(this.first); 254 this.contentElement.appendChild(cell); 255 this.firstNameInput = cell.querySelector('input'); 256 257 cell = this.createEditableTextCell(this.middle); 258 this.contentElement.appendChild(cell); 259 this.middleNameInput = cell.querySelector('input'); 260 261 cell = this.createEditableTextCell(this.last); 262 this.contentElement.appendChild(cell); 263 this.lastNameInput = cell.querySelector('input'); 264 265 if (this.isPlaceholder) { 266 this.firstNameInput.placeholder = 267 loadTimeData.getString('autofillAddFirstNamePlaceholder'); 268 this.middleNameInput.placeholder = 269 loadTimeData.getString('autofillAddMiddleNamePlaceholder'); 270 this.lastNameInput.placeholder = 271 loadTimeData.getString('autofillAddLastNamePlaceholder'); 272 this.deletable = false; 273 } 274 275 this.addEventListener('commitedit', this.onEditCommitted_); 276 }, 277 278 /** @override */ 279 value_: function() { 280 return [this.firstNameInput.value, 281 this.middleNameInput.value, 282 this.lastNameInput.value]; 283 }, 284 285 /** @override */ 286 valueIsNonEmpty_: function(value) { 287 return value[0] || value[1] || value[2]; 288 }, 289 290 /** @override */ 291 valuesAreEqual_: function(value1, value2) { 292 // First, check for null values. 293 if (!value1 || !value2) 294 return value1 == value2; 295 296 return value1[0] === value2[0] && 297 value1[1] === value2[1] && 298 value1[2] === value2[2]; 299 }, 300 301 /** @override */ 302 clearValue_: function() { 303 this.firstNameInput.value = ''; 304 this.middleNameInput.value = ''; 305 this.lastNameInput.value = ''; 306 }, 307 }; 308 309 /** 310 * Base class for shared implementation between address and credit card lists. 311 * @constructor 312 * @extends {options.DeletableItemList} 313 */ 314 var AutofillProfileList = cr.ui.define('list'); 315 316 AutofillProfileList.prototype = { 317 __proto__: DeletableItemList.prototype, 318 319 decorate: function() { 320 DeletableItemList.prototype.decorate.call(this); 321 322 this.addEventListener('blur', this.onBlur_); 323 }, 324 325 /** 326 * When the list loses focus, unselect all items in the list. 327 * @private 328 */ 329 onBlur_: function() { 330 this.selectionModel.unselectAll(); 331 }, 332 }; 333 334 /** 335 * Create a new address list. 336 * @constructor 337 * @extends {options.AutofillProfileList} 338 */ 339 var AutofillAddressList = cr.ui.define('list'); 340 341 AutofillAddressList.prototype = { 342 __proto__: AutofillProfileList.prototype, 343 344 decorate: function() { 345 AutofillProfileList.prototype.decorate.call(this); 346 }, 347 348 /** @override */ 349 activateItemAtIndex: function(index) { 350 AutofillOptions.loadAddressEditor(this.dataModel.item(index)[0]); 351 }, 352 353 /** @override */ 354 createItem: function(entry) { 355 return new AddressListItem(entry); 356 }, 357 358 /** @override */ 359 deleteItemAtIndex: function(index) { 360 AutofillOptions.removeData(this.dataModel.item(index)[0]); 361 }, 362 }; 363 364 /** 365 * Create a new credit card list. 366 * @constructor 367 * @extends {options.DeletableItemList} 368 */ 369 var AutofillCreditCardList = cr.ui.define('list'); 370 371 AutofillCreditCardList.prototype = { 372 __proto__: AutofillProfileList.prototype, 373 374 decorate: function() { 375 AutofillProfileList.prototype.decorate.call(this); 376 }, 377 378 /** @override */ 379 activateItemAtIndex: function(index) { 380 AutofillOptions.loadCreditCardEditor(this.dataModel.item(index)[0]); 381 }, 382 383 /** @override */ 384 createItem: function(entry) { 385 return new CreditCardListItem(entry); 386 }, 387 388 /** @override */ 389 deleteItemAtIndex: function(index) { 390 AutofillOptions.removeData(this.dataModel.item(index)[0]); 391 }, 392 }; 393 394 /** 395 * Create a new value list. 396 * @constructor 397 * @extends {options.InlineEditableItemList} 398 */ 399 var AutofillValuesList = cr.ui.define('list'); 400 401 AutofillValuesList.prototype = { 402 __proto__: InlineEditableItemList.prototype, 403 404 /** @override */ 405 createItem: function(entry) { 406 return new ValuesListItem(this, entry); 407 }, 408 409 /** @override */ 410 deleteItemAtIndex: function(index) { 411 this.dataModel.splice(index, 1); 412 }, 413 414 /** @override */ 415 shouldFocusPlaceholder: function() { 416 return false; 417 }, 418 419 /** 420 * Called when the list hierarchy as a whole loses or gains focus. 421 * If the list was focused in response to a mouse click, call into the 422 * superclass's implementation. If the list was focused in response to a 423 * keyboard navigation, focus the first item. 424 * If the list loses focus, unselect all the elements. 425 * @param {Event} e The change event. 426 * @private 427 */ 428 handleListFocusChange_: function(e) { 429 // We check to see whether there is a selected item as a proxy for 430 // distinguishing between mouse- and keyboard-originated focus events. 431 var selectedItem = this.selectedItem; 432 if (selectedItem) 433 InlineEditableItemList.prototype.handleListFocusChange_.call(this, e); 434 435 if (!e.newValue) { 436 // When the list loses focus, unselect all the elements. 437 this.selectionModel.unselectAll(); 438 } else { 439 // When the list gains focus, select the first item if nothing else is 440 // selected. 441 var firstItem = this.getListItemByIndex(0); 442 if (!selectedItem && firstItem && e.newValue) 443 firstItem.handleFocus_(); 444 } 445 }, 446 447 /** 448 * Called when a new list item should be validated; subclasses are 449 * responsible for implementing if validation is required. 450 * @param {number} index The index of the item that was inserted or changed. 451 * @param {number} remove The number items to remove. 452 * @param {string} value The value of the item to insert. 453 */ 454 validateAndSave: function(index, remove, value) { 455 this.dataModel.splice(index, remove, value); 456 }, 457 }; 458 459 /** 460 * Create a new value list for phone number validation. 461 * @constructor 462 * @extends {options.AutofillValuesList} 463 */ 464 var AutofillNameValuesList = cr.ui.define('list'); 465 466 AutofillNameValuesList.prototype = { 467 __proto__: AutofillValuesList.prototype, 468 469 /** @override */ 470 createItem: function(entry) { 471 return new NameListItem(this, entry); 472 }, 473 }; 474 475 /** 476 * Create a new value list for phone number validation. 477 * @constructor 478 * @extends {options.AutofillValuesList} 479 */ 480 var AutofillPhoneValuesList = cr.ui.define('list'); 481 482 AutofillPhoneValuesList.prototype = { 483 __proto__: AutofillValuesList.prototype, 484 485 /** @override */ 486 validateAndSave: function(index, remove, value) { 487 var numbers = this.dataModel.slice(0, this.dataModel.length - 1); 488 numbers.splice(index, remove, value); 489 var info = new Array(); 490 info[0] = index; 491 info[1] = numbers; 492 info[2] = document.querySelector( 493 '#autofill-edit-address-overlay [field=country]').value; 494 this.validationRequests_++; 495 chrome.send('validatePhoneNumbers', info); 496 }, 497 498 /** 499 * The number of ongoing validation requests. 500 * @type {number} 501 * @private 502 */ 503 validationRequests_: 0, 504 505 /** 506 * Pending Promise resolver functions. 507 * @type {Array.<!Function>} 508 * @private 509 */ 510 validationPromiseResolvers_: [], 511 512 /** 513 * This should be called when a reply of chrome.send('validatePhoneNumbers') 514 * is received. 515 */ 516 didReceiveValidationResult: function() { 517 this.validationRequests_--; 518 assert(this.validationRequests_ >= 0); 519 if (this.validationRequests_ <= 0) { 520 while (this.validationPromiseResolvers_.length) { 521 this.validationPromiseResolvers_.pop()(); 522 } 523 } 524 }, 525 526 /** 527 * Returns a Promise which is fulfilled when all of validation requests are 528 * completed. 529 * @return {!Promise} A promise. 530 */ 531 doneValidating: function() { 532 if (this.validationRequests_ <= 0) 533 return Promise.resolve(); 534 return new Promise(function(resolve) { 535 this.validationPromiseResolvers_.push(resolve); 536 }.bind(this)); 537 } 538 }; 539 540 return { 541 AddressListItem: AddressListItem, 542 CreditCardListItem: CreditCardListItem, 543 ValuesListItem: ValuesListItem, 544 NameListItem: NameListItem, 545 AutofillAddressList: AutofillAddressList, 546 AutofillCreditCardList: AutofillCreditCardList, 547 AutofillValuesList: AutofillValuesList, 548 AutofillNameValuesList: AutofillNameValuesList, 549 AutofillPhoneValuesList: AutofillPhoneValuesList, 550 }; 551}); 552