- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / options / editable_text_field.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   var EditableTextField = cr.ui.define('div');
7
8   /**
9    * Decorates an element as an editable text field.
10    * @param {!HTMLElement} el The element to decorate.
11    */
12   EditableTextField.decorate = function(el) {
13     el.__proto__ = EditableTextField.prototype;
14     el.decorate();
15   };
16
17   EditableTextField.prototype = {
18     __proto__: HTMLDivElement.prototype,
19
20     /**
21      * The actual input element in this field.
22      * @type {?HTMLElement}
23      * @private
24      */
25     editField_: null,
26
27     /**
28      * The static text displayed when this field isn't editable.
29      * @type {?HTMLElement}
30      * @private
31      */
32     staticText_: null,
33
34     /**
35      * The data model for this field.
36      * @type {?Object}
37      * @private
38      */
39     model_: null,
40
41     /**
42      * Whether or not the current edit should be considered canceled, rather
43      * than committed, when editing ends.
44      * @type {boolean}
45      * @private
46      */
47     editCanceled_: true,
48
49     /** @override */
50     decorate: function() {
51       this.classList.add('editable-text-field');
52
53       this.createEditableTextCell();
54
55       if (this.hasAttribute('i18n-placeholder-text')) {
56         var identifier = this.getAttribute('i18n-placeholder-text');
57         var localizedText = loadTimeData.getString(identifier);
58         if (localizedText)
59           this.setAttribute('placeholder-text', localizedText);
60       }
61
62       this.addEventListener('keydown', this.handleKeyDown_);
63       this.editField_.addEventListener('focus', this.handleFocus_.bind(this));
64       this.editField_.addEventListener('blur', this.handleBlur_.bind(this));
65       this.checkForEmpty_();
66     },
67
68     /**
69      * Indicates that this field has no value in the model, and the placeholder
70      * text (if any) should be shown.
71      * @type {boolean}
72      */
73     get empty() {
74       return this.hasAttribute('empty');
75     },
76
77     /**
78      * The placeholder text to be used when the model or its value is empty.
79      * @type {string}
80      */
81     get placeholderText() {
82       return this.getAttribute('placeholder-text');
83     },
84     set placeholderText(text) {
85       if (text)
86         this.setAttribute('placeholder-text', text);
87       else
88         this.removeAttribute('placeholder-text');
89
90       this.checkForEmpty_();
91     },
92
93     /**
94      * Returns the input element in this text field.
95      * @type {HTMLElement} The element that is the actual input field.
96      */
97     get editField() {
98       return this.editField_;
99     },
100
101     /**
102      * Whether the user is currently editing the list item.
103      * @type {boolean}
104      */
105     get editing() {
106       return this.hasAttribute('editing');
107     },
108     set editing(editing) {
109       if (this.editing == editing)
110         return;
111
112       if (editing)
113         this.setAttribute('editing', '');
114       else
115         this.removeAttribute('editing');
116
117       if (editing) {
118         this.editCanceled_ = false;
119
120         if (this.empty) {
121           this.removeAttribute('empty');
122           if (this.editField)
123             this.editField.value = '';
124         }
125         if (this.editField) {
126           this.editField.focus();
127           this.editField.select();
128         }
129       } else {
130         if (!this.editCanceled_ && this.hasBeenEdited &&
131             this.currentInputIsValid) {
132           this.updateStaticValues_();
133           cr.dispatchSimpleEvent(this, 'commitedit', true);
134         } else {
135           this.resetEditableValues_();
136           cr.dispatchSimpleEvent(this, 'canceledit', true);
137         }
138         this.checkForEmpty_();
139       }
140     },
141
142     /**
143      * Whether the item is editable.
144      * @type {boolean}
145      */
146     get editable() {
147       return this.hasAttribute('editable');
148     },
149     set editable(editable) {
150       if (this.editable == editable)
151         return;
152
153       if (editable)
154         this.setAttribute('editable', '');
155       else
156         this.removeAttribute('editable');
157       this.editable_ = editable;
158     },
159
160     /**
161      * The data model for this field.
162      * @type {Object}
163      */
164     get model() {
165       return this.model_;
166     },
167     set model(model) {
168       this.model_ = model;
169       this.checkForEmpty_();  // This also updates the editField value.
170       this.updateStaticValues_();
171     },
172
173     /**
174      * The HTML element that should have focus initially when editing starts,
175      * if a specific element wasn't clicked. Defaults to the first <input>
176      * element; can be overridden by subclasses if a different element should be
177      * focused.
178      * @type {?HTMLElement}
179      */
180     get initialFocusElement() {
181       return this.querySelector('input');
182     },
183
184     /**
185      * Whether the input in currently valid to submit. If this returns false
186      * when editing would be submitted, either editing will not be ended,
187      * or it will be cancelled, depending on the context. Can be overridden by
188      * subclasses to perform input validation.
189      * @type {boolean}
190      */
191     get currentInputIsValid() {
192       return true;
193     },
194
195     /**
196      * Returns true if the item has been changed by an edit. Can be overridden
197      * by subclasses to return false when nothing has changed to avoid
198      * unnecessary commits.
199      * @type {boolean}
200      */
201     get hasBeenEdited() {
202       return true;
203     },
204
205     /**
206      * Mutates the input during a successful commit.  Can be overridden to
207      * provide a way to "clean up" valid input so that it conforms to a
208      * desired format.  Will only be called when commit succeeds for valid
209      * input, or when the model is set.
210      * @param {string} value Input text to be mutated.
211      * @return {string} mutated text.
212      */
213     mutateInput: function(value) {
214       return value;
215     },
216
217     /**
218      * Creates a div containing an <input>, as well as static text, keeping
219      * references to them so they can be manipulated.
220      * @param {string} text The text of the cell.
221      * @private
222      */
223     createEditableTextCell: function(text) {
224       // This function should only be called once.
225       if (this.editField_)
226         return;
227
228       var container = this.ownerDocument.createElement('div');
229
230       var textEl = this.ownerDocument.createElement('div');
231       textEl.className = 'static-text';
232       textEl.textContent = text;
233       textEl.setAttribute('displaymode', 'static');
234       this.appendChild(textEl);
235       this.staticText_ = textEl;
236
237       var inputEl = this.ownerDocument.createElement('input');
238       inputEl.className = 'editable-text';
239       inputEl.type = 'text';
240       inputEl.value = text;
241       inputEl.setAttribute('displaymode', 'edit');
242       inputEl.staticVersion = textEl;
243       this.appendChild(inputEl);
244       this.editField_ = inputEl;
245     },
246
247     /**
248      * Resets the editable version of any controls created by
249      * createEditableTextCell to match the static text.
250      * @private
251      */
252     resetEditableValues_: function() {
253       var editField = this.editField_;
254       var staticLabel = editField.staticVersion;
255       if (!staticLabel)
256         return;
257
258       if (editField instanceof HTMLInputElement)
259         editField.value = staticLabel.textContent;
260
261       editField.setCustomValidity('');
262     },
263
264     /**
265      * Sets the static version of any controls created by createEditableTextCell
266      * to match the current value of the editable version. Called on commit so
267      * that there's no flicker of the old value before the model updates.  Also
268      * updates the model's value with the mutated value of the edit field.
269      * @private
270      */
271     updateStaticValues_: function() {
272       var editField = this.editField_;
273       var staticLabel = editField.staticVersion;
274       if (!staticLabel)
275         return;
276
277       if (editField instanceof HTMLInputElement) {
278         staticLabel.textContent = editField.value;
279         this.model_.value = this.mutateInput(editField.value);
280       }
281     },
282
283     /**
284      * Checks to see if the model or its value are empty.  If they are, then set
285      * the edit field to the placeholder text, if any, and if not, set it to the
286      * model's value.
287      * @private
288      */
289     checkForEmpty_: function() {
290       var editField = this.editField_;
291       if (!editField)
292         return;
293
294       if (!this.model_ || !this.model_.value) {
295         this.setAttribute('empty', '');
296         editField.value = this.placeholderText || '';
297       } else {
298         this.removeAttribute('empty');
299         editField.value = this.model_.value;
300       }
301     },
302
303     /**
304      * Called when this widget receives focus.
305      * @param {Event} e the focus event.
306      * @private
307      */
308     handleFocus_: function(e) {
309       if (this.editing)
310         return;
311
312       this.editing = true;
313       if (this.editField_)
314         this.editField_.focus();
315     },
316
317     /**
318      * Called when this widget loses focus.
319      * @param {Event} e the blur event.
320      * @private
321      */
322     handleBlur_: function(e) {
323       if (!this.editing)
324         return;
325
326       this.editing = false;
327     },
328
329     /**
330      * Called when a key is pressed. Handles committing and canceling edits.
331      * @param {Event} e The key down event.
332      * @private
333      */
334     handleKeyDown_: function(e) {
335       if (!this.editing)
336         return;
337
338       var endEdit;
339       switch (e.keyIdentifier) {
340         case 'U+001B':  // Esc
341           this.editCanceled_ = true;
342           endEdit = true;
343           break;
344         case 'Enter':
345           if (this.currentInputIsValid)
346             endEdit = true;
347           break;
348       }
349
350       if (endEdit) {
351         // Blurring will trigger the edit to end.
352         this.ownerDocument.activeElement.blur();
353         // Make sure that handled keys aren't passed on and double-handled.
354         // (e.g., esc shouldn't both cancel an edit and close a subpage)
355         e.stopPropagation();
356       }
357     },
358   };
359
360   /**
361    * Takes care of committing changes to EditableTextField items when the
362    * window loses focus.
363    */
364   window.addEventListener('blur', function(e) {
365     var itemAncestor = findAncestor(document.activeElement, function(node) {
366       return node instanceof EditableTextField;
367     });
368     if (itemAncestor)
369       document.activeElement.blur();
370   });
371
372   return {
373     EditableTextField: EditableTextField,
374   };
375 });