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', function() { 6 /** @const */ var OptionsPage = options.OptionsPage; 7 /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; 8 9 /** 10 * AutofillEditAddressOverlay class 11 * Encapsulated handling of the 'Add Page' overlay page. 12 * @class 13 */ 14 function AutofillEditAddressOverlay() { 15 OptionsPage.call(this, 'autofillEditAddress', 16 loadTimeData.getString('autofillEditAddressTitle'), 17 'autofill-edit-address-overlay'); 18 } 19 20 cr.addSingletonGetter(AutofillEditAddressOverlay); 21 22 AutofillEditAddressOverlay.prototype = { 23 __proto__: OptionsPage.prototype, 24 25 /** 26 * The GUID of the loaded address. 27 * @type {string} 28 */ 29 guid_: '', 30 31 /** 32 * The BCP 47 language code for the layout of input fields. 33 * @type {string} 34 */ 35 languageCode_: '', 36 37 /** 38 * The saved field values for the address. For example, if the user changes 39 * from United States to Switzerland, then the State field will be hidden 40 * and its value will be stored here. If the user changes back to United 41 * States, then the State field will be restored to its previous value, as 42 * stored in this object. 43 * @type {Object} 44 */ 45 savedFieldValues_: {}, 46 47 /** 48 * Initializes the page. 49 */ 50 initializePage: function() { 51 OptionsPage.prototype.initializePage.call(this); 52 53 this.createMultiValueLists_(); 54 55 var self = this; 56 $('autofill-edit-address-cancel-button').onclick = function(event) { 57 self.dismissOverlay_(); 58 }; 59 60 // TODO(jhawkins): Investigate other possible solutions. 61 $('autofill-edit-address-apply-button').onclick = function(event) { 62 // Blur active element to ensure that pending changes are committed. 63 if (document.activeElement) 64 document.activeElement.blur(); 65 // Blurring is delayed for list elements. Queue save and close to 66 // ensure that pending changes have been applied. 67 setTimeout(function() { 68 self.pageDiv.querySelector('[field=phone]').doneValidating().then( 69 function() { 70 self.saveAddress_(); 71 self.dismissOverlay_(); 72 }); 73 }, 0); 74 }; 75 76 // Prevent 'blur' events on the OK and cancel buttons, which can trigger 77 // insertion of new placeholder elements. The addition of placeholders 78 // affects layout, which interferes with being able to click on the 79 // buttons. 80 $('autofill-edit-address-apply-button').onmousedown = function(event) { 81 event.preventDefault(); 82 }; 83 $('autofill-edit-address-cancel-button').onmousedown = function(event) { 84 event.preventDefault(); 85 }; 86 87 this.guid_ = ''; 88 this.populateCountryList_(); 89 this.rebuildInputFields_( 90 loadTimeData.getValue('autofillDefaultCountryComponents')); 91 this.languageCode_ = 92 loadTimeData.getString('autofillDefaultCountryLanguageCode'); 93 this.connectInputEvents_(); 94 this.setInputFields_({}); 95 this.getCountrySwitcher_().onchange = function(event) { 96 self.countryChanged_(); 97 }; 98 }, 99 100 /** 101 * Specifically catch the situations in which the overlay is cancelled 102 * externally (e.g. by pressing <Esc>), so that the input fields and 103 * GUID can be properly cleared. 104 * @override 105 */ 106 handleCancel: function() { 107 this.dismissOverlay_(); 108 }, 109 110 /** 111 * Creates, decorates and initializes the multi-value lists for phone and 112 * email. 113 * @private 114 */ 115 createMultiValueLists_: function() { 116 var list = this.pageDiv.querySelector('[field=phone]'); 117 options.autofillOptions.AutofillPhoneValuesList.decorate(list); 118 list.autoExpands = true; 119 120 list = this.pageDiv.querySelector('[field=email]'); 121 options.autofillOptions.AutofillValuesList.decorate(list); 122 list.autoExpands = true; 123 }, 124 125 /** 126 * Updates the data model for the |list| with the values from |entries|. 127 * @param {cr.ui.List} list The list to update. 128 * @param {Array} entries The list of items to be added to the list. 129 * @private 130 */ 131 setMultiValueList_: function(list, entries) { 132 // Add special entry for adding new values. 133 var augmentedList = entries.slice(); 134 augmentedList.push(null); 135 list.dataModel = new ArrayDataModel(augmentedList); 136 137 // Update the status of the 'OK' button. 138 this.inputFieldChanged_(); 139 140 list.dataModel.addEventListener('splice', 141 this.inputFieldChanged_.bind(this)); 142 list.dataModel.addEventListener('change', 143 this.inputFieldChanged_.bind(this)); 144 }, 145 146 /** 147 * Clears any uncommitted input, resets the stored GUID and dismisses the 148 * overlay. 149 * @private 150 */ 151 dismissOverlay_: function() { 152 this.setInputFields_({}); 153 this.inputFieldChanged_(); 154 this.guid_ = ''; 155 this.languageCode_ = ''; 156 this.savedInputFields_ = {}; 157 OptionsPage.closeOverlay(); 158 }, 159 160 /** 161 * @return {Element} The element used to switch countries. 162 * @private 163 */ 164 getCountrySwitcher_: function() { 165 return this.pageDiv.querySelector('[field=country]'); 166 }, 167 168 /** 169 * Returns all list elements. 170 * @return {!NodeList} The list elements. 171 * @private 172 */ 173 getLists_: function() { 174 return this.pageDiv.querySelectorAll('list[field]'); 175 }, 176 177 /** 178 * Returns all text input elements. 179 * @return {!NodeList} The text input elements. 180 * @private 181 */ 182 getTextFields_: function() { 183 return this.pageDiv.querySelectorAll('textarea[field], input[field]'); 184 }, 185 186 /** 187 * Creates a map from type => value for all text fields. 188 * @return {Object} The mapping from field names to values. 189 * @private 190 */ 191 getInputFields_: function() { 192 var address = {country: this.getCountrySwitcher_().value}; 193 194 var lists = this.getLists_(); 195 for (var i = 0; i < lists.length; i++) { 196 address[lists[i].getAttribute('field')] = 197 lists[i].dataModel.slice(0, lists[i].dataModel.length - 1); 198 } 199 200 var fields = this.getTextFields_(); 201 for (var i = 0; i < fields.length; i++) { 202 address[fields[i].getAttribute('field')] = fields[i].value; 203 } 204 205 return address; 206 }, 207 208 /** 209 * Sets the value of each input field according to |address|. 210 * @param {object} address The object with values to use. 211 * @private 212 */ 213 setInputFields_: function(address) { 214 this.getCountrySwitcher_().value = address.country || ''; 215 216 var lists = this.getLists_(); 217 for (var i = 0; i < lists.length; i++) { 218 this.setMultiValueList_( 219 lists[i], address[lists[i].getAttribute('field')] || []); 220 } 221 222 var fields = this.getTextFields_(); 223 for (var i = 0; i < fields.length; i++) { 224 fields[i].value = address[fields[i].getAttribute('field')] || ''; 225 } 226 }, 227 228 /** 229 * Aggregates the values in the input fields into an array and sends the 230 * array to the Autofill handler. 231 * @private 232 */ 233 saveAddress_: function() { 234 var inputFields = this.getInputFields_(); 235 var address = [ 236 this.guid_, 237 inputFields.fullName || [], 238 inputFields.companyName || '', 239 inputFields.addrLines || '', 240 inputFields.dependentLocality || '', 241 inputFields.city || '', 242 inputFields.state || '', 243 inputFields.postalCode || '', 244 inputFields.sortingCode || '', 245 inputFields.country || '', 246 inputFields.phone || [], 247 inputFields.email || [], 248 this.languageCode_, 249 ]; 250 chrome.send('setAddress', address); 251 }, 252 253 /** 254 * Connects each input field to the inputFieldChanged_() method that enables 255 * or disables the 'Ok' button based on whether all the fields are empty or 256 * not. 257 * @private 258 */ 259 connectInputEvents_: function() { 260 var fields = this.getTextFields_(); 261 for (var i = 0; i < fields.length; i++) { 262 fields[i].oninput = this.inputFieldChanged_.bind(this); 263 } 264 }, 265 266 /** 267 * Disables the 'Ok' button if all of the fields are empty. 268 * @private 269 */ 270 inputFieldChanged_: function() { 271 var disabled = !this.getCountrySwitcher_().value; 272 if (disabled) { 273 // Length of lists are tested for > 1 due to the "add" placeholder item 274 // in the list. 275 var lists = this.getLists_(); 276 for (var i = 0; i < lists.length; i++) { 277 if (lists[i].items.length > 1) { 278 disabled = false; 279 break; 280 } 281 } 282 } 283 284 if (disabled) { 285 var fields = this.getTextFields_(); 286 for (var i = 0; i < fields.length; i++) { 287 if (fields[i].value) { 288 disabled = false; 289 break; 290 } 291 } 292 } 293 294 $('autofill-edit-address-apply-button').disabled = disabled; 295 }, 296 297 /** 298 * Updates the address fields appropriately for the selected country. 299 * @private 300 */ 301 countryChanged_: function() { 302 var countryCode = this.getCountrySwitcher_().value; 303 if (countryCode) 304 chrome.send('loadAddressEditorComponents', [countryCode]); 305 else 306 this.inputFieldChanged_(); 307 }, 308 309 /** 310 * Populates the country <select> list. 311 * @private 312 */ 313 populateCountryList_: function() { 314 var countryList = loadTimeData.getValue('autofillCountrySelectList'); 315 316 // Add the countries to the country <select> list. 317 var countrySelect = this.getCountrySwitcher_(); 318 // Add an empty option. 319 countrySelect.appendChild(new Option('', '')); 320 for (var i = 0; i < countryList.length; i++) { 321 var option = new Option(countryList[i].name, 322 countryList[i].value); 323 option.disabled = countryList[i].value == 'separator'; 324 countrySelect.appendChild(option); 325 } 326 }, 327 328 /** 329 * Loads the address data from |address|, sets the input fields based on 330 * this data, and stores the GUID and language code of the address. 331 * @param {!Object} address Lots of info about an address from the browser. 332 * @private 333 */ 334 loadAddress_: function(address) { 335 this.rebuildInputFields_(address.components); 336 this.setInputFields_(address); 337 this.inputFieldChanged_(); 338 this.connectInputEvents_(); 339 this.guid_ = address.guid; 340 this.languageCode_ = address.languageCode; 341 }, 342 343 /** 344 * Takes a snapshot of the input values, clears the input values, loads the 345 * address input layout from |input.components|, restores the input values 346 * from snapshot, and stores the |input.languageCode| for the address. 347 * @param {{languageCode: string, components: Array.<Array.<Object>>}} input 348 * Info about how to layout inputs fields in this dialog. 349 * @private 350 */ 351 loadAddressComponents_: function(input) { 352 var inputFields = this.getInputFields_(); 353 for (var fieldName in inputFields) { 354 if (inputFields.hasOwnProperty(fieldName)) 355 this.savedFieldValues_[fieldName] = inputFields[fieldName]; 356 } 357 this.rebuildInputFields_(input.components); 358 this.setInputFields_(this.savedFieldValues_); 359 this.inputFieldChanged_(); 360 this.connectInputEvents_(); 361 this.languageCode_ = input.languageCode; 362 }, 363 364 /** 365 * Clears address inputs and rebuilds the input fields according to 366 * |components|. 367 * @param {Array.<Array.<Object>>} components A list of information about 368 * each input field. 369 * @private 370 */ 371 rebuildInputFields_: function(components) { 372 var content = $('autofill-edit-address-fields'); 373 content.innerHTML = ''; 374 375 var customContainerElements = {fullName: 'div'}; 376 var customInputElements = {fullName: 'list', addrLines: 'textarea'}; 377 378 for (var i in components) { 379 var row = document.createElement('div'); 380 row.classList.add('input-group', 'settings-row'); 381 content.appendChild(row); 382 383 for (var j in components[i]) { 384 if (components[i][j].field == 'country') 385 continue; 386 387 var fieldContainer = document.createElement( 388 customContainerElements[components[i][j].field] || 'label'); 389 row.appendChild(fieldContainer); 390 391 var fieldName = document.createElement('div'); 392 fieldName.textContent = components[i][j].name; 393 fieldContainer.appendChild(fieldName); 394 395 var input = document.createElement( 396 customInputElements[components[i][j].field] || 'input'); 397 input.setAttribute('field', components[i][j].field); 398 input.classList.add(components[i][j].length); 399 input.setAttribute('placeholder', components[i][j].placeholder || ''); 400 fieldContainer.appendChild(input); 401 402 if (input.tagName == 'LIST') { 403 options.autofillOptions.AutofillValuesList.decorate(input); 404 input.autoExpands = true; 405 } 406 } 407 } 408 }, 409 }; 410 411 AutofillEditAddressOverlay.loadAddress = function(address) { 412 AutofillEditAddressOverlay.getInstance().loadAddress_(address); 413 }; 414 415 AutofillEditAddressOverlay.loadAddressComponents = function(input) { 416 AutofillEditAddressOverlay.getInstance().loadAddressComponents_(input); 417 }; 418 419 AutofillEditAddressOverlay.setTitle = function(title) { 420 $('autofill-address-title').textContent = title; 421 }; 422 423 AutofillEditAddressOverlay.setValidatedPhoneNumbers = function(numbers) { 424 var instance = AutofillEditAddressOverlay.getInstance(); 425 var phoneList = instance.pageDiv.querySelector('[field=phone]'); 426 instance.setMultiValueList_(phoneList, numbers); 427 phoneList.didReceiveValidationResult(); 428 }; 429 430 // Export 431 return { 432 AutofillEditAddressOverlay: AutofillEditAddressOverlay 433 }; 434}); 435