Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / options / inline_editable_list.js
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.
4
5 cr.define('options', function() {
6   /** @const */ var DeletableItem = options.DeletableItem;
7   /** @const */ var DeletableItemList = options.DeletableItemList;
8
9   /**
10    * Creates a new list item with support for inline editing.
11    * @constructor
12    * @extends {options.DeletableItem}
13    */
14   function InlineEditableItem() {
15     var el = cr.doc.createElement('div');
16     InlineEditableItem.decorate(el);
17     return el;
18   }
19
20   /**
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.
24    */
25   InlineEditableItem.decorate = function(el) {
26     el.__proto__ = InlineEditableItem.prototype;
27     el.decorate();
28   };
29
30   InlineEditableItem.prototype = {
31     __proto__: DeletableItem.prototype,
32
33     /**
34      * Whether or not this item can be edited.
35      * @type {boolean}
36      * @private
37      */
38     editable_: true,
39
40     /**
41      * Whether or not this is a placeholder for adding a new item.
42      * @type {boolean}
43      * @private
44      */
45     isPlaceholder_: false,
46
47     /**
48      * Fields associated with edit mode.
49      * @type {Array}
50      * @private
51      */
52     editFields_: null,
53
54     /**
55      * Whether or not the current edit should be considered cancelled, rather
56      * than committed, when editing ends.
57      * @type {boolean}
58      * @private
59      */
60     editCancelled_: true,
61
62     /**
63      * The editable item corresponding to the last click, if any. Used to decide
64      * initial focus when entering edit mode.
65      * @type {HTMLElement}
66      * @private
67      */
68     editClickTarget_: null,
69
70     /** @override */
71     decorate: function() {
72       DeletableItem.prototype.decorate.call(this);
73
74       this.editFields_ = [];
75       this.addEventListener('mousedown', this.handleMouseDown_);
76       this.addEventListener('keydown', this.handleKeyDown_);
77       this.addEventListener('leadChange', this.handleLeadChange_);
78     },
79
80     /** @override */
81     selectionChanged: function() {
82       this.updateEditState();
83     },
84
85     /**
86      * Called when this element gains or loses 'lead' status. Updates editing
87      * mode accordingly.
88      * @private
89      */
90     handleLeadChange_: function() {
91       this.updateEditState();
92     },
93
94     /**
95      * Updates the edit state based on the current selected and lead states.
96      */
97     updateEditState: function() {
98       if (this.editable)
99         this.editing = this.selected && this.lead;
100     },
101
102     /**
103      * Whether the user is currently editing the list item.
104      * @type {boolean}
105      */
106     get editing() {
107       return this.hasAttribute('editing');
108     },
109     set editing(editing) {
110       if (this.editing == editing)
111         return;
112
113       if (editing)
114         this.setAttribute('editing', '');
115       else
116         this.removeAttribute('editing');
117
118       if (editing) {
119         this.editCancelled_ = false;
120
121         cr.dispatchSimpleEvent(this, 'edit', true);
122
123         var focusElement = this.editClickTarget_ || this.initialFocusElement;
124         this.editClickTarget_ = null;
125
126         if (focusElement) {
127           var self = this;
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() {
135               if (self.editing) {
136                 if (focusElement.disabled)
137                   self.parentNode.focus();
138                 self.focusAndMaybeSelect_(focusElement);
139               }
140               focusElement.staticVersion.removeAttribute('tabindex');
141             }, 0);
142           } else {
143             this.focusAndMaybeSelect_(focusElement);
144           }
145         }
146       } else {
147         if (!this.editCancelled_ && this.hasBeenEdited &&
148             this.currentInputIsValid) {
149           if (this.isPlaceholder)
150             this.parentNode.focusPlaceholder = true;
151
152           this.updateStaticValues_();
153           cr.dispatchSimpleEvent(this, 'commitedit', true);
154         } else {
155           this.resetEditableValues_();
156           cr.dispatchSimpleEvent(this, 'canceledit', true);
157         }
158       }
159     },
160
161     /**
162      * Focus on the specified element, and select the editable text in it
163      * if possible.
164      * @param {!Element} control An element to be focused.
165      * @private
166      */
167     focusAndMaybeSelect_: function(control) {
168       control.focus();
169       if (control.tagName == 'INPUT')
170         control.select();
171     },
172
173     /**
174      * Whether the item is editable.
175      * @type {boolean}
176      */
177     get editable() {
178       return this.editable_;
179     },
180     set editable(editable) {
181       this.editable_ = editable;
182       if (!editable)
183         this.editing = false;
184     },
185
186     /**
187      * Whether the item is a new item placeholder.
188      * @type {boolean}
189      */
190     get isPlaceholder() {
191       return this.isPlaceholder_;
192     },
193     set isPlaceholder(isPlaceholder) {
194       this.isPlaceholder_ = isPlaceholder;
195       if (isPlaceholder)
196         this.deletable = false;
197     },
198
199     /**
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}
205      */
206     get initialFocusElement() {
207       return this.contentElement.querySelector('input');
208     },
209
210     /**
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.
215      * @type {boolean}
216      */
217     get currentInputIsValid() {
218       return true;
219     },
220
221     /**
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.
225      * @type {boolean}
226      */
227     get hasBeenEdited() {
228       return true;
229     },
230
231     /**
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.
236      * @private
237      */
238     createEditableTextCell: function(text) {
239       var container = /** @type {HTMLElement} */(
240           this.ownerDocument.createElement('div'));
241       var textEl = null;
242       if (!this.isPlaceholder) {
243         textEl = this.ownerDocument.createElement('div');
244         textEl.className = 'static-text';
245         textEl.textContent = text;
246         textEl.setAttribute('displaymode', 'static');
247         container.appendChild(textEl);
248       }
249
250       var inputEl = this.ownerDocument.createElement('input');
251       inputEl.type = 'text';
252       inputEl.value = text;
253       if (!this.isPlaceholder) {
254         inputEl.setAttribute('displaymode', 'edit');
255       } else {
256         // At this point |this| is not attached to the parent list yet, so give
257         // a short timeout in order for the attachment to occur.
258         var self = this;
259         window.setTimeout(function() {
260           var list = self.parentNode;
261           if (list && list.focusPlaceholder) {
262             list.focusPlaceholder = false;
263             if (list.shouldFocusPlaceholder())
264               inputEl.focus();
265           }
266         }, 50);
267       }
268
269       // In some cases 'focus' event may arrive before 'input'.
270       // To make sure revalidation is triggered we postpone 'focus' handling.
271       var handler = this.handleFocus_.bind(this);
272       inputEl.addEventListener('focus', function() {
273         window.setTimeout(function() {
274           if (inputEl.ownerDocument.activeElement == inputEl)
275             handler();
276         }, 0);
277       });
278       container.appendChild(inputEl);
279       this.addEditField(inputEl, textEl);
280
281       return container;
282     },
283
284     /**
285      * Register an edit field.
286      * @param {!Element} control An editable element. It's a form control
287      *     element typically.
288      * @param {Element} staticElement An element representing non-editable
289      *     state.
290      */
291     addEditField: function(control, staticElement) {
292       control.staticVersion = staticElement;
293       this.editFields_.push(control);
294     },
295
296     /**
297      * Resets the editable version of any controls created by createEditable*
298      * to match the static text.
299      * @private
300      */
301     resetEditableValues_: function() {
302       var editFields = this.editFields_;
303       for (var i = 0; i < editFields.length; i++) {
304         var staticLabel = editFields[i].staticVersion;
305         if (!staticLabel && !this.isPlaceholder)
306           continue;
307
308         if (editFields[i].tagName == 'INPUT') {
309           editFields[i].value =
310             this.isPlaceholder ? '' : staticLabel.textContent;
311         }
312         // Add more tag types here as new createEditable* methods are added.
313
314         editFields[i].setCustomValidity('');
315       }
316     },
317
318     /**
319      * Sets the static version of any controls created by createEditable*
320      * to match the current value of the editable version. Called on commit so
321      * that there's no flicker of the old value before the model updates.
322      * @private
323      */
324     updateStaticValues_: function() {
325       var editFields = this.editFields_;
326       for (var i = 0; i < editFields.length; i++) {
327         var staticLabel = editFields[i].staticVersion;
328         if (!staticLabel)
329           continue;
330
331         if (editFields[i].tagName == 'INPUT')
332           staticLabel.textContent = editFields[i].value;
333         // Add more tag types here as new createEditable* methods are added.
334       }
335     },
336
337     /**
338      * Called when a key is pressed. Handles committing and canceling edits.
339      * @param {Event} e The key down event.
340      * @private
341      */
342     handleKeyDown_: function(e) {
343       if (!this.editing)
344         return;
345
346       var endEdit = false;
347       var handledKey = true;
348       switch (e.keyIdentifier) {
349         case 'U+001B':  // Esc
350           this.editCancelled_ = true;
351           endEdit = true;
352           break;
353         case 'Enter':
354           if (this.currentInputIsValid)
355             endEdit = true;
356           break;
357         default:
358           handledKey = false;
359       }
360       if (handledKey) {
361         // Make sure that handled keys aren't passed on and double-handled.
362         // (e.g., esc shouldn't both cancel an edit and close a subpage)
363         e.stopPropagation();
364       }
365       if (endEdit) {
366         // Blurring will trigger the edit to end; see InlineEditableItemList.
367         this.ownerDocument.activeElement.blur();
368       }
369     },
370
371     /**
372      * Called when the list item is clicked. If the click target corresponds to
373      * an editable item, stores that item to focus when edit mode is started.
374      * @param {Event} e The mouse down event.
375      * @private
376      */
377     handleMouseDown_: function(e) {
378       if (!this.editable || this.editing)
379         return;
380
381       var clickTarget = e.target;
382       var editFields = this.editFields_;
383       for (var i = 0; i < editFields.length; i++) {
384         if (editFields[i].staticVersion == clickTarget)
385           clickTarget.tabIndex = 0;
386         if (editFields[i] == clickTarget ||
387             editFields[i].staticVersion == clickTarget) {
388           this.editClickTarget_ = editFields[i];
389           return;
390         }
391       }
392     },
393   };
394
395   /**
396    * Takes care of committing changes to inline editable list items when the
397    * window loses focus.
398    */
399   function handleWindowBlurs() {
400     window.addEventListener('blur', function(e) {
401       var itemAncestor = findAncestor(document.activeElement, function(node) {
402         return node instanceof InlineEditableItem;
403       });
404       if (itemAncestor)
405         document.activeElement.blur();
406     });
407   }
408   handleWindowBlurs();
409
410   /**
411    * @constructor
412    * @extends {options.DeletableItemList}
413    */
414   var InlineEditableItemList = cr.ui.define('list');
415
416   InlineEditableItemList.prototype = {
417     __proto__: DeletableItemList.prototype,
418
419     /**
420      * Focuses the input element of the placeholder if true.
421      * @type {boolean}
422      */
423     focusPlaceholder: false,
424
425     /** @override */
426     decorate: function() {
427       DeletableItemList.prototype.decorate.call(this);
428       this.setAttribute('inlineeditable', '');
429       this.addEventListener('hasElementFocusChange',
430                             this.handleListFocusChange_);
431       // <list> isn't focusable by default, but cr.ui.List defaults tabindex to
432       // 0 if it's not set.
433       this.tabIndex = -1;
434     },
435
436     /**
437      * Called when the list hierarchy as a whole loses or gains focus; starts
438      * or ends editing for the lead item if necessary.
439      * @param {Event} e The change event.
440      * @private
441      */
442     handleListFocusChange_: function(e) {
443       var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex);
444       if (leadItem) {
445         if (e.newValue)
446           leadItem.updateEditState();
447         else
448           leadItem.editing = false;
449       }
450     },
451
452     /**
453      * May be overridden by subclasses to disable focusing the placeholder.
454      * @return {boolean} True if the placeholder element should be focused on
455      *     edit commit.
456      */
457     shouldFocusPlaceholder: function() {
458       return true;
459     },
460   };
461
462   // Export
463   return {
464     InlineEditableItem: InlineEditableItem,
465     InlineEditableItemList: InlineEditableItemList,
466   };
467 });