// Copyright (c) 2010 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() { const DeletableItem = options.DeletableItem; const DeletableItemList = options.DeletableItemList; /** * Creates a new list item with support for inline editing. * @constructor * @extends {options.DeletableListItem} */ function InlineEditableItem() { var el = cr.doc.createElement('div'); InlineEditableItem.decorate(el); return el; } /** * Decorates an element as a inline-editable list item. Note that this is * a subclass of DeletableItem. * @param {!HTMLElement} el The element to decorate. */ InlineEditableItem.decorate = function(el) { el.__proto__ = InlineEditableItem.prototype; el.decorate(); }; InlineEditableItem.prototype = { __proto__: DeletableItem.prototype, /** * Whether or not this item can be edited. * @type {boolean} * @private */ editable_: true, /** * Whether or not the current edit should be considered cancelled, rather * than committed, when editing ends. * @type {boolean} * @private */ editCancelled_: true, /** @inheritDoc */ decorate: function() { DeletableItem.prototype.decorate.call(this); this.addEventListener('keydown', this.handleKeyDown_.bind(this)); }, /** @inheritDoc */ selectionChanged: function() { if (this.editable) this.editing = this.selected; }, /** * 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.editCancelled_ = false; cr.dispatchSimpleEvent(this, 'edit', true); var focusElement = this.initialFocusElement; // When this is called in response to the selectedChange event, // the list grabs focus immediately afterwards. Thus we must delay // our focus grab. if (focusElement) { window.setTimeout(function() { focusElement.focus(); focusElement.select(); }, 50); } } else { if (!this.editCancelled_ && this.hasBeenEdited && this.currentInputIsValid) { this.updateStaticValues_(); cr.dispatchSimpleEvent(this, 'commitedit', true); } else { this.resetEditableValues_(); cr.dispatchSimpleEvent(this, 'canceledit', true); } } }, /** * Whether the item is editable. * @type {boolean} */ get editable() { return this.editable_; }, set editable(editable) { this.editable_ = editable; if (!editable) this.editing = false; }, /** * The HTML element that should have focus initially when editing starts. * Defaults to the first element; can be overriden by subclasses if * a different element should be focused. * @type {HTMLElement} */ get initialFocusElement() { return this.contentElement.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 overrided 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 overrided by subclasses to return false when nothing has changed * to avoid unnecessary commits. * @type {boolean} */ get hasBeenEdited() { return true; }, /** * Returns a div containing an , as well as static text if * opt_alwaysEditable is not true. * @param {string} text The text of the cell. * @param {bool} opt_alwaysEditable True if the cell always shows the input. * @return {HTMLElement} The HTML element for the cell. * @private */ createEditableTextCell: function(text, opt_alwaysEditable) { var container = this.ownerDocument.createElement('div'); if (!opt_alwaysEditable) { var textEl = this.ownerDocument.createElement('div'); textEl.className = 'static-text'; textEl.textContent = text; textEl.setAttribute('editmode', false); container.appendChild(textEl); } var inputEl = this.ownerDocument.createElement('input'); inputEl.type = 'text'; inputEl.value = text; if (!opt_alwaysEditable) { inputEl.setAttribute('editmode', true); inputEl.staticVersion = textEl; } container.appendChild(inputEl); return container; }, /** * Resets the editable version of any controls created by createEditable* * to match the static text. * @private */ resetEditableValues_: function() { var editFields = this.querySelectorAll('[editmode=true]'); for (var i = 0; i < editFields.length; i++) { var staticLabel = editFields[i].staticVersion; if (!staticLabel) continue; if (editFields[i].tagName == 'INPUT') editFields[i].value = staticLabel.textContent; // Add more tag types here as new createEditable* methods are added. editFields[i].setCustomValidity(''); } }, /** * Sets the static version of any controls created by createEditable* * 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. * @private */ updateStaticValues_: function() { var editFields = this.querySelectorAll('[editmode=true]'); for (var i = 0; i < editFields.length; i++) { var staticLabel = editFields[i].staticVersion; if (!staticLabel) continue; if (editFields[i].tagName == 'INPUT') staticLabel.textContent = editFields[i].value; // Add more tag types here as new createEditable* methods are added. } }, /** * Called a key is pressed. Handles committing and cancelling edits. * @param {Event} e The key down event. * @private */ handleKeyDown_: function(e) { if (!this.editing) return; var endEdit = false; switch (e.keyIdentifier) { case 'U+001B': // Esc this.editCancelled_ = true; endEdit = true; break; case 'Enter': if (this.currentInputIsValid) endEdit = true; break; } if (endEdit) { // Blurring will trigger the edit to end; see InlineEditableItemList. 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(); } }, }; var InlineEditableItemList = cr.ui.define('list'); InlineEditableItemList.prototype = { __proto__: DeletableItemList.prototype, /** @inheritDoc */ decorate: function() { DeletableItemList.prototype.decorate.call(this); this.addEventListener('blur', this.handleBlur_.bind(this), true); }, /** * Called when an element in the list is blurred. Removes selection (thus * ending edit) if focus moves outside the list. * @param {Event} e The blur event. * @private */ handleBlur_: function(e) { // When the blur event happens we do not know who is getting focus so we // delay this a bit until we know if the new focus node is outside the // list. var list = this; var doc = e.target.ownerDocument; window.setTimeout(function() { var activeElement = doc.activeElement; if (!list.contains(activeElement)) list.selectionModel.unselectAll(); }, 50); }, }; // Export return { InlineEditableItem: InlineEditableItem, InlineEditableItemList: InlineEditableItemList, }; });