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