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