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