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