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