• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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