// 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} * @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, /** @override */ 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 * 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. * @return {string} mutated text. */ mutateInput: function(value) { return value; }, /** * Creates a div containing an , 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 instanceof HTMLInputElement) { 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 || ''; } 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; 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; 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; 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, }; });