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.
5 cr.define('options', function() {
6 /** @const */ var DeletableItem = options.DeletableItem;
7 /** @const */ var DeletableItemList = options.DeletableItemList;
10 * Creates a new list item with support for inline editing.
12 * @extends {options.DeletableListItem}
14 function InlineEditableItem() {
15 var el = cr.doc.createElement('div');
16 InlineEditableItem.decorate(el);
21 * Decorates an element as a inline-editable list item. Note that this is
22 * a subclass of DeletableItem.
23 * @param {!HTMLElement} el The element to decorate.
25 InlineEditableItem.decorate = function(el) {
26 el.__proto__ = InlineEditableItem.prototype;
30 InlineEditableItem.prototype = {
31 __proto__: DeletableItem.prototype,
34 * Whether or not this item can be edited.
41 * Whether or not this is a placeholder for adding a new item.
45 isPlaceholder_: false,
48 * Fields associated with edit mode.
55 * Whether or not the current edit should be considered cancelled, rather
56 * than committed, when editing ends.
63 * The editable item corresponding to the last click, if any. Used to decide
64 * initial focus when entering edit mode.
68 editClickTarget_: null,
71 decorate: function() {
72 DeletableItem.prototype.decorate.call(this);
74 this.editFields_ = [];
75 this.addEventListener('mousedown', this.handleMouseDown_);
76 this.addEventListener('keydown', this.handleKeyDown_);
77 this.addEventListener('leadChange', this.handleLeadChange_);
81 selectionChanged: function() {
82 this.updateEditState();
86 * Called when this element gains or loses 'lead' status. Updates editing
90 handleLeadChange_: function() {
91 this.updateEditState();
95 * Updates the edit state based on the current selected and lead states.
97 updateEditState: function() {
99 this.editing = this.selected && this.lead;
103 * Whether the user is currently editing the list item.
107 return this.hasAttribute('editing');
109 set editing(editing) {
110 if (this.editing == editing)
114 this.setAttribute('editing', '');
116 this.removeAttribute('editing');
119 this.editCancelled_ = false;
121 cr.dispatchSimpleEvent(this, 'edit', true);
123 var focusElement = this.editClickTarget_ || this.initialFocusElement;
124 this.editClickTarget_ = null;
128 // We should delay to give focus on |focusElement| if this is called
129 // in mousedown event handler. If we did give focus immediately, Blink
130 // would try to focus on an ancestor of the mousedown target element,
131 // and remove focus from |focusElement|.
132 if (focusElement.staticVersion &&
133 focusElement.staticVersion.hasAttribute('tabindex')) {
134 setTimeout(function() {
136 if (focusElement.disabled)
137 self.parentNode.focus();
138 self.focusAndMaybeSelect_(focusElement);
140 focusElement.staticVersion.removeAttribute('tabindex');
143 this.focusAndMaybeSelect_(focusElement);
147 if (!this.editCancelled_ && this.hasBeenEdited &&
148 this.currentInputIsValid) {
149 if (this.isPlaceholder)
150 this.parentNode.focusPlaceholder = true;
152 this.updateStaticValues_();
153 cr.dispatchSimpleEvent(this, 'commitedit', true);
155 this.resetEditableValues_();
156 cr.dispatchSimpleEvent(this, 'canceledit', true);
162 * Focus on the specified element, and select the editable text in it
164 * @param {!Element} control An element to be focused.
167 focusAndMaybeSelect_: function(control) {
169 if (control.tagName == 'INPUT')
174 * Whether the item is editable.
178 return this.editable_;
180 set editable(editable) {
181 this.editable_ = editable;
183 this.editing = false;
187 * Whether the item is a new item placeholder.
190 get isPlaceholder() {
191 return this.isPlaceholder_;
193 set isPlaceholder(isPlaceholder) {
194 this.isPlaceholder_ = isPlaceholder;
196 this.deletable = false;
200 * The HTML element that should have focus initially when editing starts,
201 * if a specific element wasn't clicked.
202 * Defaults to the first <input> element; can be overridden by subclasses if
203 * a different element should be focused.
204 * @type {HTMLElement}
206 get initialFocusElement() {
207 return this.contentElement.querySelector('input');
211 * Whether the input in currently valid to submit. If this returns false
212 * when editing would be submitted, either editing will not be ended,
213 * or it will be cancelled, depending on the context.
214 * Can be overridden by subclasses to perform input validation.
217 get currentInputIsValid() {
222 * Returns true if the item has been changed by an edit.
223 * Can be overridden by subclasses to return false when nothing has changed
224 * to avoid unnecessary commits.
227 get hasBeenEdited() {
232 * Returns a div containing an <input>, as well as static text if
233 * isPlaceholder is not true.
234 * @param {string} text The text of the cell.
235 * @return {HTMLElement} The HTML element for the cell.
238 createEditableTextCell: function(text) {
239 var container = this.ownerDocument.createElement('div');
241 if (!this.isPlaceholder) {
242 textEl = this.ownerDocument.createElement('div');
243 textEl.className = 'static-text';
244 textEl.textContent = text;
245 textEl.setAttribute('displaymode', 'static');
246 container.appendChild(textEl);
249 var inputEl = this.ownerDocument.createElement('input');
250 inputEl.type = 'text';
251 inputEl.value = text;
252 if (!this.isPlaceholder) {
253 inputEl.setAttribute('displaymode', 'edit');
255 // At this point |this| is not attached to the parent list yet, so give
256 // a short timeout in order for the attachment to occur.
258 window.setTimeout(function() {
259 var list = self.parentNode;
260 if (list && list.focusPlaceholder) {
261 list.focusPlaceholder = false;
262 if (list.shouldFocusPlaceholder())
268 // In some cases 'focus' event may arrive before 'input'.
269 // To make sure revalidation is triggered we postpone 'focus' handling.
270 var handler = this.handleFocus_.bind(this);
271 inputEl.addEventListener('focus', function() {
272 window.setTimeout(function() {
273 if (inputEl.ownerDocument.activeElement == inputEl)
277 container.appendChild(inputEl);
278 this.addEditField(inputEl, textEl);
284 * Register an edit field.
285 * @param {!Element} control An editable element. It's a form control
287 * @param {Element} staticElement An element representing non-editable
290 addEditField: function(control, staticElement) {
291 control.staticVersion = staticElement;
292 this.editFields_.push(control);
296 * Resets the editable version of any controls created by createEditable*
297 * to match the static text.
300 resetEditableValues_: function() {
301 var editFields = this.editFields_;
302 for (var i = 0; i < editFields.length; i++) {
303 var staticLabel = editFields[i].staticVersion;
304 if (!staticLabel && !this.isPlaceholder)
307 if (editFields[i].tagName == 'INPUT') {
308 editFields[i].value =
309 this.isPlaceholder ? '' : staticLabel.textContent;
311 // Add more tag types here as new createEditable* methods are added.
313 editFields[i].setCustomValidity('');
318 * Sets the static version of any controls created by createEditable*
319 * to match the current value of the editable version. Called on commit so
320 * that there's no flicker of the old value before the model updates.
323 updateStaticValues_: function() {
324 var editFields = this.editFields_;
325 for (var i = 0; i < editFields.length; i++) {
326 var staticLabel = editFields[i].staticVersion;
330 if (editFields[i].tagName == 'INPUT')
331 staticLabel.textContent = editFields[i].value;
332 // Add more tag types here as new createEditable* methods are added.
337 * Called when a key is pressed. Handles committing and canceling edits.
338 * @param {Event} e The key down event.
341 handleKeyDown_: function(e) {
346 var handledKey = true;
347 switch (e.keyIdentifier) {
348 case 'U+001B': // Esc
349 this.editCancelled_ = true;
353 if (this.currentInputIsValid)
360 // Make sure that handled keys aren't passed on and double-handled.
361 // (e.g., esc shouldn't both cancel an edit and close a subpage)
365 // Blurring will trigger the edit to end; see InlineEditableItemList.
366 this.ownerDocument.activeElement.blur();
371 * Called when the list item is clicked. If the click target corresponds to
372 * an editable item, stores that item to focus when edit mode is started.
373 * @param {Event} e The mouse down event.
376 handleMouseDown_: function(e) {
377 if (!this.editable || this.editing)
380 var clickTarget = e.target;
381 var editFields = this.editFields_;
382 for (var i = 0; i < editFields.length; i++) {
383 if (editFields[i].staticVersion == clickTarget)
384 clickTarget.tabIndex = 0;
385 if (editFields[i] == clickTarget ||
386 editFields[i].staticVersion == clickTarget) {
387 this.editClickTarget_ = editFields[i];
395 * Takes care of committing changes to inline editable list items when the
396 * window loses focus.
398 function handleWindowBlurs() {
399 window.addEventListener('blur', function(e) {
400 var itemAncestor = findAncestor(document.activeElement, function(node) {
401 return node instanceof InlineEditableItem;
404 document.activeElement.blur();
409 var InlineEditableItemList = cr.ui.define('list');
411 InlineEditableItemList.prototype = {
412 __proto__: DeletableItemList.prototype,
415 * Focuses the input element of the placeholder if true.
418 focusPlaceholder: false,
421 decorate: function() {
422 DeletableItemList.prototype.decorate.call(this);
423 this.setAttribute('inlineeditable', '');
424 this.addEventListener('hasElementFocusChange',
425 this.handleListFocusChange_);
429 * Called when the list hierarchy as a whole loses or gains focus; starts
430 * or ends editing for the lead item if necessary.
431 * @param {Event} e The change event.
434 handleListFocusChange_: function(e) {
435 var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex);
438 leadItem.updateEditState();
440 leadItem.editing = false;
445 * May be overridden by subclasses to disable focusing the placeholder.
446 * @return {boolean} True if the placeholder element should be focused on
449 shouldFocusPlaceholder: function() {
456 InlineEditableItem: InlineEditableItem,
457 InlineEditableItemList: InlineEditableItemList,