Chromium Code Reviews| Index: chrome/browser/resources/options2/editable_text_field.js |
| diff --git a/chrome/browser/resources/options2/editable_text_field.js b/chrome/browser/resources/options2/editable_text_field.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..bffcdbca8114a24e38e455a82be99c863451acc6 |
| --- /dev/null |
| +++ b/chrome/browser/resources/options2/editable_text_field.js |
| @@ -0,0 +1,373 @@ |
| +// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +cr.define('options', function() { |
| + var EditableTextField = cr.ui.define('div'); |
| + |
| + /** |
| + * Decorates an element as an editable text field. |
| + * @param {!HTMLElement} el The element to decorate. |
| + */ |
| + EditableTextField.decorate = function(el) { |
| + el.__proto__ = EditableTextField.prototype; |
| + el.decorate(); |
| + }; |
| + |
| + EditableTextField.prototype = { |
| + __proto__: HTMLDivElement.prototype, |
| + |
| + /** |
| + * The actual input element in this field. |
| + * @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.
|
| + * @private |
| + */ |
| + editField_: null, |
| + |
| + /** |
| + * The static text displayed when this field isn't editable. |
| + * @type {HTMLElement} |
| + * @private |
| + */ |
| + staticText_: null, |
| + |
| + /** |
| + * The data model for this field. |
| + * @type {Object} |
| + * @private |
| + */ |
| + model_: null, |
| + |
| + /** |
| + * Whether or not the current edit should be considered canceled, rather |
| + * than committed, when editing ends. |
| + * @type {boolean} |
| + * @private |
| + */ |
| + editCanceled_: true, |
| + |
| + /** @inheritDoc */ |
| + decorate: function() { |
| + this.classList.add('editable-text-field'); |
| + |
| + this.createEditableTextCell(); |
| + |
| + if (this.hasAttribute('i18n-placeholder-text')) { |
| + var identifier = this.getAttribute('i18n-placeholder-text'); |
| + var localizedText = loadTimeData.getString(identifier); |
| + if (localizedText) |
| + this.setAttribute('placeholder-text', localizedText); |
| + } |
| + |
| + this.addEventListener('keydown', this.handleKeyDown_); |
| + this.editField_.addEventListener('focus', this.handleFocus_.bind(this)); |
| + this.editField_.addEventListener('blur', this.handleBlur_.bind(this)); |
| + this.checkForEmpty_(); |
| + }, |
| + |
| + /** |
| + * Indicates that this field has no value in the model, and the placeholder |
| + * text (if any) should be shown. |
| + * @type {boolean} |
| + */ |
| + get empty() { |
| + return this.hasAttribute('empty'); |
| + }, |
| + |
| + /** |
| + * The placeholder text to be used when the model or its value is empty. |
| + * @type {string} |
| + */ |
| + get placeholderText() { |
| + return this.getAttribute('placeholder-text'); |
| + }, |
| + set placeholderText(text) { |
| + if (text) |
| + this.setAttribute('placeholder-text', text); |
| + else |
| + this.removeAttribute('placeholder-text'); |
| + |
| + this.checkForEmpty_(); |
| + }, |
| + |
| + /** |
| + * Returns the input element in this text field. |
| + * @type {HTMLElement} The element that is the actual input field. |
| + */ |
| + get editField() { |
| + return this.editField_; |
| + }, |
| + |
| + /** |
| + * Whether the user is currently editing the list item. |
| + * @type {boolean} |
| + */ |
| + get editing() { |
| + return this.hasAttribute('editing'); |
| + }, |
| + set editing(editing) { |
| + if (this.editing == editing) |
| + return; |
| + |
| + if (editing) |
| + this.setAttribute('editing', ''); |
| + else |
| + this.removeAttribute('editing'); |
| + |
| + if (editing) { |
| + this.editCanceled_ = false; |
| + |
| + if (this.empty) { |
| + this.removeAttribute('empty'); |
| + if (this.editField) |
| + this.editField.value = ''; |
| + } |
| + if (this.editField) { |
| + this.editField.focus(); |
| + this.editField.select(); |
| + } |
| + } else { |
| + if (!this.editCanceled_ && this.hasBeenEdited && |
| + this.currentInputIsValid) { |
| + this.updateStaticValues_(); |
| + cr.dispatchSimpleEvent(this, 'commitedit', true); |
| + } else { |
| + this.resetEditableValues_(); |
| + cr.dispatchSimpleEvent(this, 'canceledit', true); |
| + } |
| + this.checkForEmpty_(); |
| + } |
| + }, |
| + |
| + /** |
| + * Whether the item is editable. |
| + * @type {boolean} |
| + */ |
| + get editable() { |
| + return this.hasAttribute('editable'); |
| + }, |
| + set editable(editable) { |
| + if (this.editable == editable) |
| + return; |
| + |
| + if (editable) |
| + this.setAttribute('editable', ''); |
| + else |
| + this.removeAttribute('editable'); |
| + this.editable_ = editable; |
| + }, |
| + |
| + /** |
| + * The data model for this field. |
| + * @type {Object} |
| + */ |
| + get model() { |
| + return this.model_; |
| + }, |
| + set model(model) { |
| + this.model_ = model; |
| + this.checkForEmpty_(); // This also updates the editField value. |
| + this.updateStaticValues_(); |
| + }, |
| + |
| + /** |
| + * The HTML element that should have focus initially when editing starts, |
| + * if a specific element wasn't clicked. Defaults to the first <input> |
| + * element; can be overridden by subclasses if a different element should be |
| + * focused. |
| + * @type {?HTMLElement} |
| + */ |
| + get initialFocusElement() { |
| + return this.querySelector('input'); |
| + }, |
| + |
| + /** |
| + * Whether the input in currently valid to submit. If this returns false |
| + * when editing would be submitted, either editing will not be ended, |
| + * or it will be cancelled, depending on the context. Can be overridden by |
| + * subclasses to perform input validation. |
| + * @type {boolean} |
| + */ |
| + get currentInputIsValid() { |
| + return true; |
| + }, |
| + |
| + /** |
| + * Returns true if the item has been changed by an edit. Can be overridden |
| + * by subclasses to return false when nothing has changed to avoid |
| + * unnecessary commits. |
| + * @type {boolean} |
| + */ |
| + get hasBeenEdited() { |
| + return true; |
| + }, |
| + |
| + /** |
| + * Mutates the input during a successful commit. Can be overridden to |
| + * provide a way to "clean up" valid input so that it conforms to a |
| + * desired format. Will only be called when commit succeeds for valid |
| + * input, or when the model is set. |
| + * @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.
|
| + * @return {String} mutated text. |
| + */ |
| + mutateInput: function(value) { |
| + return value; |
| + }, |
| + |
| + /** |
| + * Creates a div containing an <input>, as well as static text, keeping |
| + * references to them so they can be manipulated. |
| + * @param {string} text The text of the cell. |
| + * @private |
| + */ |
| + createEditableTextCell: function(text) { |
| + // This function should only be called once. |
| + if (this.editField_) |
| + return; |
| + |
| + var container = this.ownerDocument.createElement('div'); |
| + |
| + var textEl = this.ownerDocument.createElement('div'); |
| + textEl.className = 'static-text'; |
| + textEl.textContent = text; |
| + textEl.setAttribute('displaymode', 'static'); |
| + this.appendChild(textEl); |
| + this.staticText_ = textEl; |
| + |
| + var inputEl = this.ownerDocument.createElement('input'); |
| + inputEl.className = 'editable-text'; |
| + inputEl.type = 'text'; |
| + inputEl.value = text; |
| + inputEl.setAttribute('displaymode', 'edit'); |
| + inputEl.staticVersion = textEl; |
| + this.appendChild(inputEl); |
| + this.editField_ = inputEl; |
| + }, |
| + |
| + /** |
| + * Resets the editable version of any controls created by |
| + * createEditableTextCell to match the static text. |
| + * @private |
| + */ |
| + resetEditableValues_: function() { |
| + var editField = this.editField_; |
| + var staticLabel = editField.staticVersion; |
| + if (!staticLabel) |
| + return; |
| + |
| + if (editField instanceof HTMLInputElement) |
| + editField.value = staticLabel.textContent; |
| + |
| + editField.setCustomValidity(''); |
| + }, |
| + |
| + /** |
| + * Sets the static version of any controls created by createEditableTextCell |
| + * to match the current value of the editable version. Called on commit so |
| + * that there's no flicker of the old value before the model updates. Also |
| + * updates the model's value with the mutated value of the edit field. |
| + * @private |
| + */ |
| + updateStaticValues_: function() { |
| + var editField = this.editField_; |
| + var staticLabel = editField.staticVersion; |
| + if (!staticLabel) |
| + return; |
| + |
| + 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.
|
| + staticLabel.textContent = editField.value; |
| + this.model_.value = this.mutateInput(editField.value); |
| + } |
| + }, |
| + |
| + /** |
| + * Checks to see if the model or its value are empty. If they are, then set |
| + * the edit field to the placeholder text, if any, and if not, set it to the |
| + * model's value. |
| + * @private |
| + */ |
| + checkForEmpty_: function() { |
| + var editField = this.editField_; |
| + if (!editField) |
| + return; |
| + |
| + if (!this.model_ || !this.model_.value) { |
| + this.setAttribute('empty', ''); |
| + 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.
|
| + } else { |
| + this.removeAttribute('empty'); |
| + editField.value = this.model_.value; |
| + } |
| + }, |
| + |
| + /** |
| + * Called when this widget receives focus. |
| + * @param {Event} e the focus event. |
| + * @private |
| + */ |
| + handleFocus_: function(e) { |
| + if (this.editing) |
| + return; |
|
Dan Beam
2012/08/14 03:59:56
nit: \n after if ()
Greg Spencer (Chromium)
2012/08/14 16:54:53
Done.
|
| + this.editing = true; |
| + if (this.editField_) |
| + this.editField_.focus(); |
| + }, |
| + |
| + /** |
| + * Called when this widget loses focus. |
| + * @param {Event} e the blur event. |
| + * @private |
| + */ |
| + handleBlur_: function(e) { |
| + if (!this.editing) |
| + return; |
|
Dan Beam
2012/08/14 03:59:56
nit: \n after if ()
Greg Spencer (Chromium)
2012/08/14 16:54:53
Done.
|
| + this.editing = false; |
| + }, |
| + |
| + /** |
| + * Called when a key is pressed. Handles committing and canceling edits. |
| + * @param {Event} e The key down event. |
| + * @private |
| + */ |
| + handleKeyDown_: function(e) { |
| + if (!this.editing) |
| + return; |
| + |
| + 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
|
| + switch (e.keyIdentifier) { |
| + case 'U+001B': // Esc |
| + this.editCanceled_ = true; |
| + endEdit = true; |
| + break; |
| + case 'Enter': |
| + if (this.currentInputIsValid) |
| + endEdit = true; |
| + break; |
| + } |
| + |
| + if (endEdit) { |
| + // Blurring will trigger the edit to end. |
| + this.ownerDocument.activeElement.blur(); |
| + // Make sure that handled keys aren't passed on and double-handled. |
| + // (e.g., esc shouldn't both cancel an edit and close a subpage) |
| + e.stopPropagation(); |
| + } |
| + }, |
| + }; |
| + |
| + /** |
| + * Takes care of committing changes to EditableTextField items when the |
| + * window loses focus. |
| + */ |
| + window.addEventListener('blur', function(e) { |
| + var itemAncestor = findAncestor(document.activeElement, function(node) { |
| + return node instanceof EditableTextField; |
| + }); |
| + if (itemAncestor) |
| + document.activeElement.blur(); |
| + }); |
| + |
| + return { |
| + EditableTextField: EditableTextField, |
| + }; |
| +}); |