Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 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} | |
|
Dan Beam
2012/08/14 03:59:56
nit: I think these are all technically supposed to
Greg Spencer (Chromium)
2012/08/14 16:54:53
OK, understood. Done.
| |
| 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 /** @inheritDoc */ | |
| 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. | |
|
Dan Beam
2012/08/14 03:59:56
nit: I think technically these should be {string}
Greg Spencer (Chromium)
2012/08/14 16:54:53
Done.
| |
| 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.tagName == 'INPUT') { | |
|
Dan Beam
2012/08/14 03:59:56
nit: same nit as before - editField instanceof HTM
Greg Spencer (Chromium)
2012/08/14 16:54:53
Done.
| |
| 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 ? this.placeholderText : ''; | |
|
Dan Beam
2012/08/14 03:59:56
nit: this.placeHolderText || ''
Greg Spencer (Chromium)
2012/08/14 16:54:53
Done.
| |
| 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; | |
|
Dan Beam
2012/08/14 03:59:56
nit: \n after if ()
Greg Spencer (Chromium)
2012/08/14 16:54:53
Done.
| |
| 311 this.editing = true; | |
| 312 if (this.editField_) | |
| 313 this.editField_.focus(); | |
| 314 }, | |
| 315 | |
| 316 /** | |
| 317 * Called when this widget loses focus. | |
| 318 * @param {Event} e the blur event. | |
| 319 * @private | |
| 320 */ | |
| 321 handleBlur_: function(e) { | |
| 322 if (!this.editing) | |
| 323 return; | |
|
Dan Beam
2012/08/14 03:59:56
nit: \n after if ()
Greg Spencer (Chromium)
2012/08/14 16:54:53
Done.
| |
| 324 this.editing = false; | |
| 325 }, | |
| 326 | |
| 327 /** | |
| 328 * Called when a key is pressed. Handles committing and canceling edits. | |
| 329 * @param {Event} e The key down event. | |
| 330 * @private | |
| 331 */ | |
| 332 handleKeyDown_: function(e) { | |
| 333 if (!this.editing) | |
| 334 return; | |
| 335 | |
| 336 var endEdit = false; | |
|
Dan Beam
2012/08/14 03:59:56
nit: you don't really need to initialize this to f
Greg Spencer (Chromium)
2012/08/14 16:54:53
Hmm. Good point. :) It seems wrong somehow to le
| |
| 337 switch (e.keyIdentifier) { | |
| 338 case 'U+001B': // Esc | |
| 339 this.editCanceled_ = true; | |
| 340 endEdit = true; | |
| 341 break; | |
| 342 case 'Enter': | |
| 343 if (this.currentInputIsValid) | |
| 344 endEdit = true; | |
| 345 break; | |
| 346 } | |
| 347 | |
| 348 if (endEdit) { | |
| 349 // Blurring will trigger the edit to end. | |
| 350 this.ownerDocument.activeElement.blur(); | |
| 351 // Make sure that handled keys aren't passed on and double-handled. | |
| 352 // (e.g., esc shouldn't both cancel an edit and close a subpage) | |
| 353 e.stopPropagation(); | |
| 354 } | |
| 355 }, | |
| 356 }; | |
| 357 | |
| 358 /** | |
| 359 * Takes care of committing changes to EditableTextField items when the | |
| 360 * window loses focus. | |
| 361 */ | |
| 362 window.addEventListener('blur', function(e) { | |
| 363 var itemAncestor = findAncestor(document.activeElement, function(node) { | |
| 364 return node instanceof EditableTextField; | |
| 365 }); | |
| 366 if (itemAncestor) | |
| 367 document.activeElement.blur(); | |
| 368 }); | |
| 369 | |
| 370 return { | |
| 371 EditableTextField: EditableTextField, | |
| 372 }; | |
| 373 }); | |
| OLD | NEW |