Index: chrome/browser/resources/options2/inline_editable_list.js |
diff --git a/chrome/browser/resources/options2/inline_editable_list.js b/chrome/browser/resources/options2/inline_editable_list.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8aed93beedf423fe5d24ca8c6f317fa50979672e |
--- /dev/null |
+++ b/chrome/browser/resources/options2/inline_editable_list.js |
@@ -0,0 +1,414 @@ |
+// Copyright (c) 2011 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 this is a placeholder for adding a new item. |
+ * @type {boolean} |
+ * @private |
+ */ |
+ isPlaceholder_: false, |
+ |
+ /** |
+ * Fields associated with edit mode. |
+ * @type {array} |
+ * @private |
+ */ |
+ editFields_: null, |
+ |
+ /** |
+ * Whether or not the current edit should be considered cancelled, rather |
+ * than committed, when editing ends. |
+ * @type {boolean} |
+ * @private |
+ */ |
+ editCancelled_: true, |
+ |
+ /** |
+ * The editable item corresponding to the last click, if any. Used to decide |
+ * initial focus when entering edit mode. |
+ * @type {HTMLElement} |
+ * @private |
+ */ |
+ editClickTarget_: null, |
+ |
+ /** @inheritDoc */ |
+ decorate: function() { |
+ DeletableItem.prototype.decorate.call(this); |
+ |
+ this.editFields_ = []; |
+ this.addEventListener('mousedown', this.handleMouseDown_); |
+ this.addEventListener('keydown', this.handleKeyDown_); |
+ this.addEventListener('leadChange', this.handleLeadChange_); |
+ }, |
+ |
+ /** @inheritDoc */ |
+ selectionChanged: function() { |
+ this.updateEditState(); |
+ }, |
+ |
+ /** |
+ * Called when this element gains or loses 'lead' status. Updates editing |
+ * mode accordingly. |
+ * @private |
+ */ |
+ handleLeadChange_: function() { |
+ this.updateEditState(); |
+ }, |
+ |
+ /** |
+ * Updates the edit state based on the current selected and lead states. |
+ */ |
+ updateEditState: function() { |
+ if (this.editable) |
+ this.editing = this.selected && this.lead; |
+ }, |
+ |
+ /** |
+ * 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.editClickTarget_ || this.initialFocusElement; |
+ this.editClickTarget_ = null; |
+ |
+ // When this is called in response to the selectedChange event, |
+ // the list grabs focus immediately afterwards. Thus we must delay |
+ // our focus grab. |
+ var self = this; |
+ if (focusElement) { |
+ window.setTimeout(function() { |
+ // Make sure we are still in edit mode by the time we execute. |
+ if (self.editing) { |
+ focusElement.focus(); |
+ focusElement.select(); |
+ } |
+ }, 50); |
+ } |
+ } else { |
+ if (!this.editCancelled_ && this.hasBeenEdited && |
+ this.currentInputIsValid) { |
+ if (this.isPlaceholder) |
+ this.parentNode.focusPlaceholder = true; |
+ |
+ 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; |
+ }, |
+ |
+ /** |
+ * Whether the item is a new item placeholder. |
+ * @type {boolean} |
+ */ |
+ get isPlaceholder() { |
+ return this.isPlaceholder_; |
+ }, |
+ set isPlaceholder(isPlaceholder) { |
+ this.isPlaceholder_ = isPlaceholder; |
+ if (isPlaceholder) |
+ this.deletable = false; |
+ }, |
+ |
+ /** |
+ * 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 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 <input>, as well as static text if |
+ * isPlaceholder is not true. |
+ * @param {string} text The text of the cell. |
+ * @return {HTMLElement} The HTML element for the cell. |
+ * @private |
+ */ |
+ createEditableTextCell: function(text) { |
+ var container = this.ownerDocument.createElement('div'); |
+ |
+ if (!this.isPlaceholder) { |
+ var textEl = this.ownerDocument.createElement('div'); |
+ textEl.className = 'static-text'; |
+ textEl.textContent = text; |
+ textEl.setAttribute('displaymode', 'static'); |
+ container.appendChild(textEl); |
+ } |
+ |
+ var inputEl = this.ownerDocument.createElement('input'); |
+ inputEl.type = 'text'; |
+ inputEl.value = text; |
+ if (!this.isPlaceholder) { |
+ inputEl.setAttribute('displaymode', 'edit'); |
+ inputEl.staticVersion = textEl; |
+ } else { |
+ // At this point |this| is not attached to the parent list yet, so give |
+ // a short timeout in order for the attachment to occur. |
+ var self = this; |
+ window.setTimeout(function() { |
+ var list = self.parentNode; |
+ if (list && list.focusPlaceholder) { |
+ list.focusPlaceholder = false; |
+ if (list.shouldFocusPlaceholder()) |
+ inputEl.focus(); |
+ } |
+ }, 50); |
+ } |
+ |
+ inputEl.addEventListener('focus', this.handleFocus_.bind(this)); |
+ container.appendChild(inputEl); |
+ this.editFields_.push(inputEl); |
+ |
+ return container; |
+ }, |
+ |
+ /** |
+ * Resets the editable version of any controls created by createEditable* |
+ * to match the static text. |
+ * @private |
+ */ |
+ resetEditableValues_: function() { |
+ var editFields = this.editFields_; |
+ for (var i = 0; i < editFields.length; i++) { |
+ var staticLabel = editFields[i].staticVersion; |
+ if (!staticLabel && !this.isPlaceholder) |
+ continue; |
+ |
+ if (editFields[i].tagName == 'INPUT') { |
+ editFields[i].value = |
+ this.isPlaceholder ? '' : 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.editFields_; |
+ 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(); |
+ } |
+ }, |
+ |
+ /** |
+ * Called when the list item is clicked. If the click target corresponds to |
+ * an editable item, stores that item to focus when edit mode is started. |
+ * @param {Event} e The mouse down event. |
+ * @private |
+ */ |
+ handleMouseDown_: function(e) { |
+ if (!this.editable || this.editing) |
+ return; |
+ |
+ var clickTarget = e.target; |
+ var editFields = this.editFields_; |
+ for (var i = 0; i < editFields.length; i++) { |
+ if (editFields[i] == clickTarget || |
+ editFields[i].staticVersion == clickTarget) { |
+ this.editClickTarget_ = editFields[i]; |
+ return; |
+ } |
+ } |
+ }, |
+ }; |
+ |
+ /** |
+ * Takes care of committing changes to inline editable list items when the |
+ * window loses focus. |
+ */ |
+ function handleWindowBlurs() { |
+ window.addEventListener('blur', function(e) { |
+ var itemAncestor = findAncestor(document.activeElement, function(node) { |
+ return node instanceof InlineEditableItem; |
+ }); |
+ if (itemAncestor); |
+ document.activeElement.blur(); |
+ }); |
+ } |
+ handleWindowBlurs(); |
+ |
+ var InlineEditableItemList = cr.ui.define('list'); |
+ |
+ InlineEditableItemList.prototype = { |
+ __proto__: DeletableItemList.prototype, |
+ |
+ /** |
+ * Focuses the input element of the placeholder if true. |
+ * @type {boolean} |
+ */ |
+ focusPlaceholder: false, |
+ |
+ /** @inheritDoc */ |
+ decorate: function() { |
+ DeletableItemList.prototype.decorate.call(this); |
+ this.setAttribute('inlineeditable', ''); |
+ this.addEventListener('hasElementFocusChange', |
+ this.handleListFocusChange_); |
+ }, |
+ |
+ /** |
+ * Called when the list hierarchy as a whole loses or gains focus; starts |
+ * or ends editing for the lead item if necessary. |
+ * @param {Event} e The change event. |
+ * @private |
+ */ |
+ handleListFocusChange_: function(e) { |
+ var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex); |
+ if (leadItem) { |
+ if (e.newValue) |
+ leadItem.updateEditState(); |
+ else |
+ leadItem.editing = false; |
+ } |
+ }, |
+ |
+ /** |
+ * May be overridden by subclasses to disable focusing the placeholder. |
+ * @return true if the placeholder element should be focused on edit commit. |
+ */ |
+ shouldFocusPlaceholder: function() { |
+ return true; |
+ }, |
+ }; |
+ |
+ // Export |
+ return { |
+ InlineEditableItem: InlineEditableItem, |
+ InlineEditableItemList: InlineEditableItemList, |
+ }; |
+}); |