• 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.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