Home | History | Annotate | Download | only in options
      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 
      5 cr.define('options', function() {
      6   var EditableTextField = cr.ui.define('div');
      7 
      8   /**
      9    * Decorates an element as an editable text field.
     10    * @param {!HTMLElement} el The element to decorate.
     11    */
     12   EditableTextField.decorate = function(el) {
     13     el.__proto__ = EditableTextField.prototype;
     14     el.decorate();
     15   };
     16 
     17   EditableTextField.prototype = {
     18     __proto__: HTMLDivElement.prototype,
     19 
     20     /**
     21      * The actual input element in this field.
     22      * @type {?HTMLElement}
     23      * @private
     24      */
     25     editField_: null,
     26 
     27     /**
     28      * The static text displayed when this field isn't editable.
     29      * @type {?HTMLElement}
     30      * @private
     31      */
     32     staticText_: null,
     33 
     34     /**
     35      * The data model for this field.
     36      * @type {?Object}
     37      * @private
     38      */
     39     model_: null,
     40 
     41     /**
     42      * Whether or not the current edit should be considered canceled, rather
     43      * than committed, when editing ends.
     44      * @type {boolean}
     45      * @private
     46      */
     47     editCanceled_: true,
     48 
     49     /** @override */
     50     decorate: function() {
     51       this.classList.add('editable-text-field');
     52 
     53       this.createEditableTextCell();
     54 
     55       if (this.hasAttribute('i18n-placeholder-text')) {
     56         var identifier = this.getAttribute('i18n-placeholder-text');
     57         var localizedText = loadTimeData.getString(identifier);
     58         if (localizedText)
     59           this.setAttribute('placeholder-text', localizedText);
     60       }
     61 
     62       this.addEventListener('keydown', this.handleKeyDown_);
     63       this.editField_.addEventListener('focus', this.handleFocus_.bind(this));
     64       this.editField_.addEventListener('blur', this.handleBlur_.bind(this));
     65       this.checkForEmpty_();
     66     },
     67 
     68     /**
     69      * Indicates that this field has no value in the model, and the placeholder
     70      * text (if any) should be shown.
     71      * @type {boolean}
     72      */
     73     get empty() {
     74       return this.hasAttribute('empty');
     75     },
     76 
     77     /**
     78      * The placeholder text to be used when the model or its value is empty.
     79      * @type {string}
     80      */
     81     get placeholderText() {
     82       return this.getAttribute('placeholder-text');
     83     },
     84     set placeholderText(text) {
     85       if (text)
     86         this.setAttribute('placeholder-text', text);
     87       else
     88         this.removeAttribute('placeholder-text');
     89 
     90       this.checkForEmpty_();
     91     },
     92 
     93     /**
     94      * Returns the input element in this text field.
     95      * @type {HTMLElement} The element that is the actual input field.
     96      */
     97     get editField() {
     98       return this.editField_;
     99     },
    100 
    101     /**
    102      * Whether the user is currently editing the list item.
    103      * @type {boolean}
    104      */
    105     get editing() {
    106       return this.hasAttribute('editing');
    107     },
    108     set editing(editing) {
    109       if (this.editing == editing)
    110         return;
    111 
    112       if (editing)
    113         this.setAttribute('editing', '');
    114       else
    115         this.removeAttribute('editing');
    116 
    117       if (editing) {
    118         this.editCanceled_ = false;
    119 
    120         if (this.empty) {
    121           this.removeAttribute('empty');
    122           if (this.editField)
    123             this.editField.value = '';
    124         }
    125         if (this.editField) {
    126           this.editField.focus();
    127           this.editField.select();
    128         }
    129       } else {
    130         if (!this.editCanceled_ && this.hasBeenEdited &&
    131             this.currentInputIsValid) {
    132           this.updateStaticValues_();
    133           cr.dispatchSimpleEvent(this, 'commitedit', true);
    134         } else {
    135           this.resetEditableValues_();
    136           cr.dispatchSimpleEvent(this, 'canceledit', true);
    137         }
    138         this.checkForEmpty_();
    139       }
    140     },
    141 
    142     /**
    143      * Whether the item is editable.
    144      * @type {boolean}
    145      */
    146     get editable() {
    147       return this.hasAttribute('editable');
    148     },
    149     set editable(editable) {
    150       if (this.editable == editable)
    151         return;
    152 
    153       if (editable)
    154         this.setAttribute('editable', '');
    155       else
    156         this.removeAttribute('editable');
    157       this.editable_ = editable;
    158     },
    159 
    160     /**
    161      * The data model for this field.
    162      * @type {Object}
    163      */
    164     get model() {
    165       return this.model_;
    166     },
    167     set model(model) {
    168       this.model_ = model;
    169       this.checkForEmpty_();  // This also updates the editField value.
    170       this.updateStaticValues_();
    171     },
    172 
    173     /**
    174      * The HTML element that should have focus initially when editing starts,
    175      * if a specific element wasn't clicked. Defaults to the first <input>
    176      * element; can be overridden by subclasses if a different element should be
    177      * focused.
    178      * @type {?HTMLElement}
    179      */
    180     get initialFocusElement() {
    181       return this.querySelector('input');
    182     },
    183 
    184     /**
    185      * Whether the input in currently valid to submit. If this returns false
    186      * when editing would be submitted, either editing will not be ended,
    187      * or it will be cancelled, depending on the context. Can be overridden by
    188      * subclasses to perform input validation.
    189      * @type {boolean}
    190      */
    191     get currentInputIsValid() {
    192       return true;
    193     },
    194 
    195     /**
    196      * Returns true if the item has been changed by an edit. Can be overridden
    197      * by subclasses to return false when nothing has changed to avoid
    198      * unnecessary commits.
    199      * @type {boolean}
    200      */
    201     get hasBeenEdited() {
    202       return true;
    203     },
    204 
    205     /**
    206      * Mutates the input during a successful commit.  Can be overridden to
    207      * provide a way to "clean up" valid input so that it conforms to a
    208      * desired format.  Will only be called when commit succeeds for valid
    209      * input, or when the model is set.
    210      * @param {string} value Input text to be mutated.
    211      * @return {string} mutated text.
    212      */
    213     mutateInput: function(value) {
    214       return value;
    215     },
    216 
    217     /**
    218      * Creates a div containing an <input>, as well as static text, keeping
    219      * references to them so they can be manipulated.
    220      * @param {string} text The text of the cell.
    221      * @private
    222      */
    223     createEditableTextCell: function(text) {
    224       // This function should only be called once.
    225       if (this.editField_)
    226         return;
    227 
    228       var container = this.ownerDocument.createElement('div');
    229 
    230       var textEl = this.ownerDocument.createElement('div');
    231       textEl.className = 'static-text';
    232       textEl.textContent = text;
    233       textEl.setAttribute('displaymode', 'static');
    234       this.appendChild(textEl);
    235       this.staticText_ = textEl;
    236 
    237       var inputEl = this.ownerDocument.createElement('input');
    238       inputEl.className = 'editable-text';
    239       inputEl.type = 'text';
    240       inputEl.value = text;
    241       inputEl.setAttribute('displaymode', 'edit');
    242       inputEl.staticVersion = textEl;
    243       this.appendChild(inputEl);
    244       this.editField_ = inputEl;
    245     },
    246 
    247     /**
    248      * Resets the editable version of any controls created by
    249      * createEditableTextCell to match the static text.
    250      * @private
    251      */
    252     resetEditableValues_: function() {
    253       var editField = this.editField_;
    254       var staticLabel = editField.staticVersion;
    255       if (!staticLabel)
    256         return;
    257 
    258       if (editField instanceof HTMLInputElement)
    259         editField.value = staticLabel.textContent;
    260 
    261       editField.setCustomValidity('');
    262     },
    263 
    264     /**
    265      * Sets the static version of any controls created by createEditableTextCell
    266      * to match the current value of the editable version. Called on commit so
    267      * that there's no flicker of the old value before the model updates.  Also
    268      * updates the model's value with the mutated value of the edit field.
    269      * @private
    270      */
    271     updateStaticValues_: function() {
    272       var editField = this.editField_;
    273       var staticLabel = editField.staticVersion;
    274       if (!staticLabel)
    275         return;
    276 
    277       if (editField instanceof HTMLInputElement) {
    278         staticLabel.textContent = editField.value;
    279         this.model_.value = this.mutateInput(editField.value);
    280       }
    281     },
    282 
    283     /**
    284      * Checks to see if the model or its value are empty.  If they are, then set
    285      * the edit field to the placeholder text, if any, and if not, set it to the
    286      * model's value.
    287      * @private
    288      */
    289     checkForEmpty_: function() {
    290       var editField = this.editField_;
    291       if (!editField)
    292         return;
    293 
    294       if (!this.model_ || !this.model_.value) {
    295         this.setAttribute('empty', '');
    296         editField.value = this.placeholderText || '';
    297       } else {
    298         this.removeAttribute('empty');
    299         editField.value = this.model_.value;
    300       }
    301     },
    302 
    303     /**
    304      * Called when this widget receives focus.
    305      * @param {Event} e the focus event.
    306      * @private
    307      */
    308     handleFocus_: function(e) {
    309       if (this.editing)
    310         return;
    311 
    312       this.editing = true;
    313       if (this.editField_)
    314         this.editField_.focus();
    315     },
    316 
    317     /**
    318      * Called when this widget loses focus.
    319      * @param {Event} e the blur event.
    320      * @private
    321      */
    322     handleBlur_: function(e) {
    323       if (!this.editing)
    324         return;
    325 
    326       this.editing = false;
    327     },
    328 
    329     /**
    330      * Called when a key is pressed. Handles committing and canceling edits.
    331      * @param {Event} e The key down event.
    332      * @private
    333      */
    334     handleKeyDown_: function(e) {
    335       if (!this.editing)
    336         return;
    337 
    338       var endEdit;
    339       switch (e.keyIdentifier) {
    340         case 'U+001B':  // Esc
    341           this.editCanceled_ = true;
    342           endEdit = true;
    343           break;
    344         case 'Enter':
    345           if (this.currentInputIsValid)
    346             endEdit = true;
    347           break;
    348       }
    349 
    350       if (endEdit) {
    351         // Blurring will trigger the edit to end.
    352         this.ownerDocument.activeElement.blur();
    353         // Make sure that handled keys aren't passed on and double-handled.
    354         // (e.g., esc shouldn't both cancel an edit and close a subpage)
    355         e.stopPropagation();
    356       }
    357     },
    358   };
    359 
    360   /**
    361    * Takes care of committing changes to EditableTextField items when the
    362    * window loses focus.
    363    */
    364   window.addEventListener('blur', function(e) {
    365     var itemAncestor = findAncestor(document.activeElement, function(node) {
    366       return node instanceof EditableTextField;
    367     });
    368     if (itemAncestor)
    369       document.activeElement.blur();
    370   });
    371 
    372   return {
    373     EditableTextField: EditableTextField,
    374   };
    375 });
    376