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