- add sources.
[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.DeletableListItem}
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           !this.isExtraFocusableControl(document.activeElement);
101       }
102     },
103
104     /**
105      * Whether the user is currently editing the list item.
106      * @type {boolean}
107      */
108     get editing() {
109       return this.hasAttribute('editing');
110     },
111     set editing(editing) {
112       if (this.editing == editing)
113         return;
114
115       if (editing)
116         this.setAttribute('editing', '');
117       else
118         this.removeAttribute('editing');
119
120       if (editing) {
121         this.editCancelled_ = false;
122
123         cr.dispatchSimpleEvent(this, 'edit', true);
124
125         var focusElement = this.editClickTarget_ || this.initialFocusElement;
126         this.editClickTarget_ = null;
127
128         if (focusElement) {
129           focusElement.focus();
130           // select() doesn't work well in mousedown event handler.
131           setTimeout(function() {
132               if (focusElement.ownerDocument.activeElement == focusElement)
133                 focusElement.select();
134           }, 0);
135         }
136       } else {
137         if (!this.editCancelled_ && this.hasBeenEdited &&
138             this.currentInputIsValid) {
139           if (this.isPlaceholder)
140             this.parentNode.focusPlaceholder = true;
141
142           this.updateStaticValues_();
143           cr.dispatchSimpleEvent(this, 'commitedit', true);
144         } else {
145           this.resetEditableValues_();
146           cr.dispatchSimpleEvent(this, 'canceledit', true);
147         }
148       }
149     },
150
151     /**
152      * Whether the item is editable.
153      * @type {boolean}
154      */
155     get editable() {
156       return this.editable_;
157     },
158     set editable(editable) {
159       this.editable_ = editable;
160       if (!editable)
161         this.editing = false;
162     },
163
164     /**
165      * Whether the item is a new item placeholder.
166      * @type {boolean}
167      */
168     get isPlaceholder() {
169       return this.isPlaceholder_;
170     },
171     set isPlaceholder(isPlaceholder) {
172       this.isPlaceholder_ = isPlaceholder;
173       if (isPlaceholder)
174         this.deletable = false;
175     },
176
177     /**
178      * The HTML element that should have focus initially when editing starts,
179      * if a specific element wasn't clicked.
180      * Defaults to the first <input> element; can be overridden by subclasses if
181      * a different element should be focused.
182      * @type {HTMLElement}
183      */
184     get initialFocusElement() {
185       return this.contentElement.querySelector('input');
186     },
187
188     /**
189      * Whether the input in currently valid to submit. If this returns false
190      * when editing would be submitted, either editing will not be ended,
191      * or it will be cancelled, depending on the context.
192      * Can be overridden by subclasses to perform input validation.
193      * @type {boolean}
194      */
195     get currentInputIsValid() {
196       return true;
197     },
198
199     /**
200      * Returns true if the item has been changed by an edit.
201      * Can be overridden by subclasses to return false when nothing has changed
202      * to avoid unnecessary commits.
203      * @type {boolean}
204      */
205     get hasBeenEdited() {
206       return true;
207     },
208
209     /**
210      * Returns a div containing an <input>, as well as static text if
211      * isPlaceholder is not true.
212      * @param {string} text The text of the cell.
213      * @return {HTMLElement} The HTML element for the cell.
214      * @private
215      */
216     createEditableTextCell: function(text) {
217       var container = this.ownerDocument.createElement('div');
218
219       if (!this.isPlaceholder) {
220         var textEl = this.ownerDocument.createElement('div');
221         textEl.className = 'static-text';
222         textEl.textContent = text;
223         textEl.setAttribute('displaymode', 'static');
224         container.appendChild(textEl);
225       }
226
227       var inputEl = this.ownerDocument.createElement('input');
228       inputEl.type = 'text';
229       inputEl.value = text;
230       if (!this.isPlaceholder) {
231         inputEl.setAttribute('displaymode', 'edit');
232         inputEl.staticVersion = textEl;
233       } else {
234         // At this point |this| is not attached to the parent list yet, so give
235         // a short timeout in order for the attachment to occur.
236         var self = this;
237         window.setTimeout(function() {
238           var list = self.parentNode;
239           if (list && list.focusPlaceholder) {
240             list.focusPlaceholder = false;
241             if (list.shouldFocusPlaceholder())
242               inputEl.focus();
243           }
244         }, 50);
245       }
246
247       inputEl.addEventListener('focus', this.handleFocus_.bind(this));
248       container.appendChild(inputEl);
249       this.editFields_.push(inputEl);
250
251       return container;
252     },
253
254     /**
255      * Resets the editable version of any controls created by createEditable*
256      * to match the static text.
257      * @private
258      */
259     resetEditableValues_: function() {
260       var editFields = this.editFields_;
261       for (var i = 0; i < editFields.length; i++) {
262         var staticLabel = editFields[i].staticVersion;
263         if (!staticLabel && !this.isPlaceholder)
264           continue;
265
266         if (editFields[i].tagName == 'INPUT') {
267           editFields[i].value =
268             this.isPlaceholder ? '' : staticLabel.textContent;
269         }
270         // Add more tag types here as new createEditable* methods are added.
271
272         editFields[i].setCustomValidity('');
273       }
274     },
275
276     /**
277      * Sets the static version of any controls created by createEditable*
278      * to match the current value of the editable version. Called on commit so
279      * that there's no flicker of the old value before the model updates.
280      * @private
281      */
282     updateStaticValues_: function() {
283       var editFields = this.editFields_;
284       for (var i = 0; i < editFields.length; i++) {
285         var staticLabel = editFields[i].staticVersion;
286         if (!staticLabel)
287           continue;
288
289         if (editFields[i].tagName == 'INPUT')
290           staticLabel.textContent = editFields[i].value;
291         // Add more tag types here as new createEditable* methods are added.
292       }
293     },
294
295     /**
296      * Called when a key is pressed. Handles committing and canceling edits.
297      * @param {Event} e The key down event.
298      * @private
299      */
300     handleKeyDown_: function(e) {
301       if (!this.editing)
302         return;
303
304       var endEdit = false;
305       var handledKey = true;
306       switch (e.keyIdentifier) {
307         case 'U+001B':  // Esc
308           this.editCancelled_ = true;
309           endEdit = true;
310           break;
311         case 'Enter':
312           if (this.currentInputIsValid)
313             endEdit = true;
314           break;
315         default:
316           handledKey = false;
317       }
318       if (handledKey) {
319         // Make sure that handled keys aren't passed on and double-handled.
320         // (e.g., esc shouldn't both cancel an edit and close a subpage)
321         e.stopPropagation();
322       }
323       if (endEdit) {
324         // Blurring will trigger the edit to end; see InlineEditableItemList.
325         this.ownerDocument.activeElement.blur();
326       }
327     },
328
329     /**
330      * Called when the list item is clicked. If the click target corresponds to
331      * an editable item, stores that item to focus when edit mode is started.
332      * @param {Event} e The mouse down event.
333      * @private
334      */
335     handleMouseDown_: function(e) {
336       if (!this.editable || this.editing)
337         return;
338
339       var clickTarget = e.target;
340       if (this.isExtraFocusableControl(clickTarget)) {
341         clickTarget.focus();
342         return;
343       }
344
345       var editFields = this.editFields_;
346       for (var i = 0; i < editFields.length; i++) {
347         if (editFields[i] == clickTarget ||
348             editFields[i].staticVersion == clickTarget) {
349           this.editClickTarget_ = editFields[i];
350           return;
351         }
352       }
353     },
354
355     /**
356      * Check if the specified element is a focusable form control which is in
357      * the list item and not in |editFields_|.
358      * @param {!Element} element An element.
359      * @return {boolean} Returns true if the element is one of focusable
360      *     controls in this list item.
361      */
362     isExtraFocusableControl: function(element) {
363       return false;
364     },
365   };
366
367   /**
368    * Takes care of committing changes to inline editable list items when the
369    * window loses focus.
370    */
371   function handleWindowBlurs() {
372     window.addEventListener('blur', function(e) {
373       var itemAncestor = findAncestor(document.activeElement, function(node) {
374         return node instanceof InlineEditableItem;
375       });
376       if (itemAncestor)
377         document.activeElement.blur();
378     });
379   }
380   handleWindowBlurs();
381
382   var InlineEditableItemList = cr.ui.define('list');
383
384   InlineEditableItemList.prototype = {
385     __proto__: DeletableItemList.prototype,
386
387     /**
388      * Focuses the input element of the placeholder if true.
389      * @type {boolean}
390      */
391     focusPlaceholder: false,
392
393     /** @override */
394     decorate: function() {
395       DeletableItemList.prototype.decorate.call(this);
396       this.setAttribute('inlineeditable', '');
397       this.addEventListener('hasElementFocusChange',
398                             this.handleListFocusChange_);
399     },
400
401     /**
402      * Called when the list hierarchy as a whole loses or gains focus; starts
403      * or ends editing for the lead item if necessary.
404      * @param {Event} e The change event.
405      * @private
406      */
407     handleListFocusChange_: function(e) {
408       var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex);
409       if (leadItem) {
410         if (e.newValue)
411           leadItem.updateEditState();
412         else
413           leadItem.editing = false;
414       }
415     },
416
417     /**
418      * May be overridden by subclasses to disable focusing the placeholder.
419      * @return {boolean} True if the placeholder element should be focused on
420      *     edit commit.
421      */
422     shouldFocusPlaceholder: function() {
423       return true;
424     },
425   };
426
427   // Export
428   return {
429     InlineEditableItem: InlineEditableItem,
430     InlineEditableItemList: InlineEditableItemList,
431   };
432 });