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 if (!this.editField_) | |
54 this.createEditableTextCell(); | |
55 | |
56 if (this.hasAttribute('i18n-placeholder-text')) { | |
57 var identifier = this.getAttribute('i18n-placeholder-text'); | |
58 var localizedText = loadTimeData.getString(identifier); | |
Dan Beam
2012/08/11 01:38:02
do you also need to check if loadTimeData.valueExi
Greg Spencer (Chromium)
2012/08/13 19:20:04
No, you're right, I should probably check that too
Dan Beam
2012/08/13 19:48:55
If you assume it's there, just use the value. loa
| |
59 if (localizedText) | |
60 this.setAttribute('placeholder-text', localizedText); | |
61 } | |
62 | |
63 var self = this; | |
Dan Beam
2012/08/11 01:38:02
I think .bind() is preferred, i.e.
this.editFie
Greg Spencer (Chromium)
2012/08/13 19:20:04
OK, thanks. Wasn't sure about bind here. Indent i
| |
64 this.addEventListener('keydown', this.handleKeyDown_); | |
65 this.editField_.addEventListener('focus', | |
66 function(e) { self.handleFocus_(e); }); | |
67 this.editField_.addEventListener('blur', | |
68 function(e) { self.handleBlur_(e); }); | |
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) { | |
Dan Beam
2012/08/11 01:38:02
nit: no curlies since all the bodys and conditiona
Greg Spencer (Chromium)
2012/08/13 19:20:04
Done.
| |
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_(); // Also updates the editField value | |
Dan Beam
2012/08/11 01:38:02
nit: . at end (even if it's a sentence fragment, I
Greg Spencer (Chromium)
2012/08/13 19:20:04
Sorry, that was sloppy: I usually like that too.
| |
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. | |
Dan Beam
2012/08/11 01:38:02
nit: you can leave this \n here between sentences
Greg Spencer (Chromium)
2012/08/13 19:20:04
Done, here and below.
| |
180 * Defaults to the first <input> element; can be overridden by subclasses if | |
181 * a different element should be focused. | |
182 * @type {HTMLElement} | |
Dan Beam
2012/08/11 01:38:02
@type {?HTMLElement} is probably more appropriate
Greg Spencer (Chromium)
2012/08/13 19:20:04
Done.
| |
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. | |
192 * Can be overridden by 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. | |
201 * Can be overridden by subclasses to return false when nothing has changed | |
202 * to avoid 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 | |
Dan Beam
2012/08/11 01:38:02
nit: 1 \s between sentences
Greg Spencer (Chromium)
2012/08/13 19:20:04
Done.
| |
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 {Object} value Input to be mutated. | |
215 * @return {Object} mutated value. | |
216 */ | |
217 mutateInput: function(value) { | |
218 return value; | |
219 }, | |
220 | |
221 /** | |
222 * Returns a div containing an <input>, as well as static text. | |
223 * @param {string} text The text of the cell. | |
224 * @return {HTMLElement} The HTML element for the cell. | |
225 * @private | |
226 */ | |
227 createEditableTextCell: function(text) { | |
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); | |
Dan Beam
2012/08/11 01:38:02
do you ever only call this method once? might be
Greg Spencer (Chromium)
2012/08/13 19:20:04
Yes, it's only called once (well, it's called only
| |
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 return container; | |
247 }, | |
248 | |
249 /** | |
250 * Resets the editable version of any controls created by | |
251 * createEditableTextCell to match the static text. | |
252 * @private | |
253 */ | |
254 resetEditableValues_: function() { | |
255 var editField = this.editField_; | |
256 var staticLabel = editField.staticVersion; | |
257 if (!staticLabel) | |
258 return; | |
259 | |
260 if (editField.tagName == 'INPUT') | |
Dan Beam
2012/08/11 01:38:02
nit: if (editField instanceof HTMLInputElement) IM
Greg Spencer (Chromium)
2012/08/13 19:20:04
Done.
| |
261 editField.value = staticLabel.textContent; | |
262 | |
263 editField.setCustomValidity(''); | |
264 }, | |
265 | |
266 /** | |
267 * Sets the static version of any controls created by createEditableTextCell | |
268 * to match the current value of the editable version. Called on commit so | |
269 * that there's no flicker of the old value before the model updates. Also | |
270 * updates the model's value with the mutated value of the edit field. | |
271 * @private | |
272 */ | |
273 updateStaticValues_: function() { | |
274 var editField = this.editField_; | |
275 var staticLabel = editField.staticVersion; | |
276 if (!staticLabel) | |
277 return; | |
278 | |
279 if (editField.tagName == 'INPUT') { | |
280 staticLabel.textContent = editField.value; | |
281 this.model_.value = this.mutateInput(editField.value); | |
282 } | |
283 }, | |
284 | |
285 /** | |
286 * Checks to see if the model or its value are empty. If they are, then set | |
287 * the edit field to the placeholder text, if any, and if not, set it to the | |
288 * model's value. | |
289 * @private | |
290 */ | |
291 checkForEmpty_: function() { | |
292 var editField = this.editField_; | |
293 if (!editField) | |
294 return; | |
295 | |
296 if (!this.model_ || !this.model_.value) { | |
297 this.setAttribute('empty', ''); | |
298 editField.value = this.placeholderText ? this.placeholderText : ''; | |
299 } else { | |
300 this.removeAttribute('empty'); | |
301 editField.value = this.model_.value; | |
302 } | |
303 }, | |
304 | |
305 /** | |
306 * Called when this widget receives focus. | |
307 * @param {Event} e the focus event. | |
308 * @private | |
309 */ | |
310 handleFocus_: function(e) { | |
311 if (this.editing) | |
312 return; | |
313 this.editing = true; | |
314 if (this.editField_) | |
315 this.editField_.focus(); | |
316 }, | |
317 | |
318 /** | |
319 * Called when this widget loses focus. | |
320 * @param {Event} e the blur event. | |
321 * @private | |
322 */ | |
323 handleBlur_: function(e) { | |
324 if (!this.editing) | |
325 return; | |
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 = false; | |
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 function handleWindowBlurs() { | |
365 window.addEventListener('blur', function(e) { | |
366 var itemAncestor = findAncestor(document.activeElement, function(node) { | |
367 return node instanceof EditableTextField; | |
368 }); | |
369 if (itemAncestor) | |
370 document.activeElement.blur(); | |
371 }); | |
372 } | |
373 handleWindowBlurs(); | |
Dan Beam
2012/08/11 01:38:02
why are you making this a function then calling it
Greg Spencer (Chromium)
2012/08/13 19:20:04
Yeah, good point. It was a pattern I saw in some
| |
374 | |
375 return { | |
376 EditableTextField: EditableTextField, | |
377 }; | |
378 }); | |
OLD | NEW |