• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5cr.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});
382