OLD | NEW |
| (Empty) |
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 /** @inheritDoc */ | |
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 }); | |
OLD | NEW |