Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2010 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   const DeletableItem = options.DeletableItem;
      7   const DeletableItemList = options.DeletableItemList;
      8 
      9   /**
     10    * Creates a new list item with support for inline editing.
     11    * @constructor
     12    * @extends {options.DeletableListItem}
     13    */
     14   function InlineEditableItem() {
     15     var el = cr.doc.createElement('div');
     16     InlineEditableItem.decorate(el);
     17     return el;
     18   }
     19 
     20   /**
     21    * Decorates an element as a inline-editable list item. Note that this is
     22    * a subclass of DeletableItem.
     23    * @param {!HTMLElement} el The element to decorate.
     24    */
     25   InlineEditableItem.decorate = function(el) {
     26     el.__proto__ = InlineEditableItem.prototype;
     27     el.decorate();
     28   };
     29 
     30   InlineEditableItem.prototype = {
     31     __proto__: DeletableItem.prototype,
     32 
     33     /**
     34      * Whether or not this item can be edited.
     35      * @type {boolean}
     36      * @private
     37      */
     38     editable_: true,
     39 
     40     /**
     41      * Whether or not this is a placeholder for adding a new item.
     42      * @type {boolean}
     43      * @private
     44      */
     45     isPlaceholder_: false,
     46 
     47     /**
     48      * Fields associated with edit mode.
     49      * @type {array}
     50      * @private
     51      */
     52     editFields_: null,
     53 
     54     /**
     55      * Whether or not the current edit should be considered cancelled, rather
     56      * than committed, when editing ends.
     57      * @type {boolean}
     58      * @private
     59      */
     60     editCancelled_: true,
     61 
     62     /**
     63      * The editable item corresponding to the last click, if any. Used to decide
     64      * initial focus when entering edit mode.
     65      * @type {HTMLElement}
     66      * @private
     67      */
     68     editClickTarget_: null,
     69 
     70     /** @inheritDoc */
     71     decorate: function() {
     72       DeletableItem.prototype.decorate.call(this);
     73 
     74       this.editFields_ = [];
     75       this.addEventListener('mousedown', this.handleMouseDown_.bind(this));
     76       this.addEventListener('keydown', this.handleKeyDown_.bind(this));
     77       this.addEventListener('leadChange', this.handleLeadChange_);
     78     },
     79 
     80     /** @inheritDoc */
     81     selectionChanged: function() {
     82       this.updateEditState();
     83     },
     84 
     85     /**
     86      * Called when this element gains or loses 'lead' status. Updates editing
     87      * mode accordingly.
     88      * @private
     89      */
     90     handleLeadChange_: function() {
     91       this.updateEditState();
     92     },
     93 
     94     /**
     95      * Updates the edit state based on the current selected and lead states.
     96      */
     97     updateEditState: function() {
     98       if (this.editable)
     99         this.editing = this.selected && this.lead;
    100     },
    101 
    102     /**
    103      * Whether the user is currently editing the list item.
    104      * @type {boolean}
    105      */
    106     get editing() {
    107       return this.hasAttribute('editing');
    108     },
    109     set editing(editing) {
    110       if (this.editing == editing)
    111         return;
    112 
    113       if (editing)
    114         this.setAttribute('editing', '');
    115       else
    116         this.removeAttribute('editing');
    117 
    118       if (editing) {
    119         this.editCancelled_ = false;
    120 
    121         cr.dispatchSimpleEvent(this, 'edit', true);
    122 
    123         var focusElement = this.editClickTarget_ || this.initialFocusElement;
    124         this.editClickTarget_ = null;
    125 
    126         // When this is called in response to the selectedChange event,
    127         // the list grabs focus immediately afterwards. Thus we must delay
    128         // our focus grab.
    129         var self = this;
    130         if (focusElement) {
    131           window.setTimeout(function() {
    132             // Make sure we are still in edit mode by the time we execute.
    133             if (self.editing) {
    134               focusElement.focus();
    135               focusElement.select();
    136             }
    137           }, 50);
    138         }
    139       } else {
    140         if (!this.editCancelled_ && this.hasBeenEdited &&
    141             this.currentInputIsValid) {
    142           this.updateStaticValues_();
    143           cr.dispatchSimpleEvent(this, 'commitedit', true);
    144         } else {
    145           this.resetEditableValues_();
    146           cr.dispatchSimpleEvent(this, 'canceledit', true);
    147         }
    148       }
    149     },
    150 
    151     /**
    152      * Whether the item is editable.
    153      * @type {boolean}
    154      */
    155     get editable() {
    156       return this.editable_;
    157     },
    158     set editable(editable) {
    159       this.editable_ = editable;
    160       if (!editable)
    161         this.editing = false;
    162     },
    163 
    164     /**
    165      * Whether the item is a new item placeholder.
    166      * @type {boolean}
    167      */
    168     get isPlaceholder() {
    169       return this.isPlaceholder_;
    170     },
    171     set isPlaceholder(isPlaceholder) {
    172       this.isPlaceholder_ = isPlaceholder;
    173       if (isPlaceholder)
    174         this.deletable = false;
    175     },
    176 
    177     /**
    178      * The HTML element that should have focus initially when editing starts,
    179      * if a specific element wasn't clicked.
    180      * Defaults to the first <input> element; can be overriden by subclasses if
    181      * a different element should be focused.
    182      * @type {HTMLElement}
    183      */
    184     get initialFocusElement() {
    185       return this.contentElement.querySelector('input');
    186     },
    187 
    188     /**
    189      * Whether the input in currently valid to submit. If this returns false
    190      * when editing would be submitted, either editing will not be ended,
    191      * or it will be cancelled, depending on the context.
    192      * Can be overrided by subclasses to perform input validation.
    193      * @type {boolean}
    194      */
    195     get currentInputIsValid() {
    196       return true;
    197     },
    198 
    199     /**
    200      * Returns true if the item has been changed by an edit.
    201      * Can be overrided by subclasses to return false when nothing has changed
    202      * to avoid unnecessary commits.
    203      * @type {boolean}
    204      */
    205     get hasBeenEdited() {
    206       return true;
    207     },
    208 
    209     /**
    210      * Returns a div containing an <input>, as well as static text if
    211      * isPlaceholder is not true.
    212      * @param {string} text The text of the cell.
    213      * @return {HTMLElement} The HTML element for the cell.
    214      * @private
    215      */
    216     createEditableTextCell: function(text) {
    217       var container = this.ownerDocument.createElement('div');
    218 
    219       if (!this.isPlaceholder) {
    220         var textEl = this.ownerDocument.createElement('div');
    221         textEl.className = 'static-text';
    222         textEl.textContent = text;
    223         textEl.setAttribute('displaymode', 'static');
    224         container.appendChild(textEl);
    225       }
    226 
    227       var inputEl = this.ownerDocument.createElement('input');
    228       inputEl.type = 'text';
    229       inputEl.value = text;
    230       if (!this.isPlaceholder) {
    231         inputEl.setAttribute('displaymode', 'edit');
    232         inputEl.staticVersion = textEl;
    233       }
    234       container.appendChild(inputEl);
    235       this.editFields_.push(inputEl);
    236 
    237       return container;
    238     },
    239 
    240     /**
    241      * Resets the editable version of any controls created by createEditable*
    242      * to match the static text.
    243      * @private
    244      */
    245     resetEditableValues_: function() {
    246       var editFields = this.editFields_;
    247       for (var i = 0; i < editFields.length; i++) {
    248         var staticLabel = editFields[i].staticVersion;
    249         if (!staticLabel && !this.isPlaceholder)
    250           continue;
    251         if (editFields[i].tagName == 'INPUT') {
    252           editFields[i].value =
    253             this.isPlaceholder ? '' : staticLabel.textContent;
    254         }
    255         // Add more tag types here as new createEditable* methods are added.
    256 
    257         editFields[i].setCustomValidity('');
    258       }
    259     },
    260 
    261     /**
    262      * Sets the static version of any controls created by createEditable*
    263      * to match the current value of the editable version. Called on commit so
    264      * that there's no flicker of the old value before the model updates.
    265      * @private
    266      */
    267     updateStaticValues_: function() {
    268       var editFields = this.editFields_;
    269       for (var i = 0; i < editFields.length; i++) {
    270         var staticLabel = editFields[i].staticVersion;
    271         if (!staticLabel)
    272           continue;
    273         if (editFields[i].tagName == 'INPUT')
    274           staticLabel.textContent = editFields[i].value;
    275         // Add more tag types here as new createEditable* methods are added.
    276       }
    277     },
    278 
    279     /**
    280      * Called a key is pressed. Handles committing and cancelling edits.
    281      * @param {Event} e The key down event.
    282      * @private
    283      */
    284     handleKeyDown_: function(e) {
    285       if (!this.editing)
    286         return;
    287 
    288       var endEdit = false;
    289       switch (e.keyIdentifier) {
    290         case 'U+001B':  // Esc
    291           this.editCancelled_ = true;
    292           endEdit = true;
    293           break;
    294         case 'Enter':
    295           if (this.currentInputIsValid)
    296             endEdit = true;
    297           break;
    298       }
    299 
    300       if (endEdit) {
    301         // Blurring will trigger the edit to end; see InlineEditableItemList.
    302         this.ownerDocument.activeElement.blur();
    303         // Make sure that handled keys aren't passed on and double-handled.
    304         // (e.g., esc shouldn't both cancel an edit and close a subpage)
    305         e.stopPropagation();
    306       }
    307     },
    308 
    309     /**
    310      * Called when the list item is clicked. If the click target corresponds to
    311      * an editable item, stores that item to focus when edit mode is started.
    312      * @param {Event} e The mouse down event.
    313      * @private
    314      */
    315     handleMouseDown_: function(e) {
    316       if (!this.editable || this.editing)
    317         return;
    318 
    319       var clickTarget = e.target;
    320       var editFields = this.editFields_;
    321       for (var i = 0; i < editFields.length; i++) {
    322         if (editFields[i] == clickTarget ||
    323             editFields[i].staticVersion == clickTarget) {
    324           this.editClickTarget_ = editFields[i];
    325           return;
    326         }
    327       }
    328     },
    329   };
    330 
    331   var InlineEditableItemList = cr.ui.define('list');
    332 
    333   InlineEditableItemList.prototype = {
    334     __proto__: DeletableItemList.prototype,
    335 
    336     /** @inheritDoc */
    337     decorate: function() {
    338       DeletableItemList.prototype.decorate.call(this);
    339       this.setAttribute('inlineeditable', '');
    340       this.addEventListener('hasElementFocusChange',
    341                             this.handleListFocusChange_);
    342     },
    343 
    344     /**
    345      * Called when the list hierarchy as a whole loses or gains focus; starts
    346      * or ends editing for the lead item if necessary.
    347      * @param {Event} e The change event.
    348      * @private
    349      */
    350     handleListFocusChange_: function(e) {
    351       var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex);
    352       if (leadItem) {
    353         if (e.newValue)
    354           leadItem.updateEditState();
    355         else
    356           leadItem.editing = false;
    357       }
    358     },
    359   };
    360 
    361   // Export
    362   return {
    363     InlineEditableItem: InlineEditableItem,
    364     InlineEditableItemList: InlineEditableItemList,
    365   };
    366 });
    367