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 /** @const */ var DeletableItem = options.DeletableItem; | |
7 /** @const */ var DeletableItemList = options.DeletableItemList; | |
8 | |
9 /** | |
10 * Creates a new list item with support for inline editing. | |
11 * @constructor | |
12 * @extends {options.DeletableListItem} | |
13 */ | |
14 function InlineEditableItem() { | |
15 var el = cr.doc.createElement('div'); | |
16 InlineEditableItem.decorate(el); | |
17 return el; | |
18 } | |
19 | |
20 /** | |
21 * Decorates an element as a inline-editable list item. Note that this is | |
22 * a subclass of DeletableItem. | |
23 * @param {!HTMLElement} el The element to decorate. | |
24 */ | |
25 InlineEditableItem.decorate = function(el) { | |
26 el.__proto__ = InlineEditableItem.prototype; | |
27 el.decorate(); | |
28 }; | |
29 | |
30 InlineEditableItem.prototype = { | |
31 __proto__: DeletableItem.prototype, | |
32 | |
33 /** | |
34 * Whether or not this item can be edited. | |
35 * @type {boolean} | |
36 * @private | |
37 */ | |
38 editable_: true, | |
39 | |
40 /** | |
41 * Whether or not this is a placeholder for adding a new item. | |
42 * @type {boolean} | |
43 * @private | |
44 */ | |
45 isPlaceholder_: false, | |
46 | |
47 /** | |
48 * Fields associated with edit mode. | |
49 * @type {array} | |
50 * @private | |
51 */ | |
52 editFields_: null, | |
53 | |
54 /** | |
55 * Whether or not the current edit should be considered cancelled, rather | |
56 * than committed, when editing ends. | |
57 * @type {boolean} | |
58 * @private | |
59 */ | |
60 editCancelled_: true, | |
61 | |
62 /** | |
63 * The editable item corresponding to the last click, if any. Used to decide | |
64 * initial focus when entering edit mode. | |
65 * @type {HTMLElement} | |
66 * @private | |
67 */ | |
68 editClickTarget_: null, | |
69 | |
70 /** @inheritDoc */ | |
71 decorate: function() { | |
72 DeletableItem.prototype.decorate.call(this); | |
73 | |
74 this.editFields_ = []; | |
75 this.addEventListener('mousedown', this.handleMouseDown_); | |
76 this.addEventListener('keydown', this.handleKeyDown_); | |
77 this.addEventListener('leadChange', this.handleLeadChange_); | |
78 }, | |
79 | |
80 /** @inheritDoc */ | |
81 selectionChanged: function() { | |
82 this.updateEditState(); | |
83 }, | |
84 | |
85 /** | |
86 * Called when this element gains or loses 'lead' status. Updates editing | |
87 * mode accordingly. | |
88 * @private | |
89 */ | |
90 handleLeadChange_: function() { | |
91 this.updateEditState(); | |
92 }, | |
93 | |
94 /** | |
95 * Updates the edit state based on the current selected and lead states. | |
96 */ | |
97 updateEditState: function() { | |
98 if (this.editable) | |
99 this.editing = this.selected && this.lead; | |
100 }, | |
101 | |
102 /** | |
103 * Whether the user is currently editing the list item. | |
104 * @type {boolean} | |
105 */ | |
106 get editing() { | |
107 return this.hasAttribute('editing'); | |
108 }, | |
109 set editing(editing) { | |
110 if (this.editing == editing) | |
111 return; | |
112 | |
113 if (editing) | |
114 this.setAttribute('editing', ''); | |
115 else | |
116 this.removeAttribute('editing'); | |
117 | |
118 if (editing) { | |
119 this.editCancelled_ = false; | |
120 | |
121 cr.dispatchSimpleEvent(this, 'edit', true); | |
122 | |
123 var focusElement = this.editClickTarget_ || this.initialFocusElement; | |
124 this.editClickTarget_ = null; | |
125 | |
126 // When this is called in response to the selectedChange event, | |
127 // the list grabs focus immediately afterwards. Thus we must delay | |
128 // our focus grab. | |
129 var self = this; | |
130 if (focusElement) { | |
131 window.setTimeout(function() { | |
132 // Make sure we are still in edit mode by the time we execute. | |
133 if (self.editing) { | |
134 focusElement.focus(); | |
135 focusElement.select(); | |
136 } | |
137 }, 50); | |
138 } | |
139 } else { | |
140 if (!this.editCancelled_ && this.hasBeenEdited && | |
141 this.currentInputIsValid) { | |
142 if (this.isPlaceholder) | |
143 this.parentNode.focusPlaceholder = true; | |
144 | |
145 this.updateStaticValues_(); | |
146 cr.dispatchSimpleEvent(this, 'commitedit', true); | |
147 } else { | |
148 this.resetEditableValues_(); | |
149 cr.dispatchSimpleEvent(this, 'canceledit', true); | |
150 } | |
151 } | |
152 }, | |
153 | |
154 /** | |
155 * Whether the item is editable. | |
156 * @type {boolean} | |
157 */ | |
158 get editable() { | |
159 return this.editable_; | |
160 }, | |
161 set editable(editable) { | |
162 this.editable_ = editable; | |
163 if (!editable) | |
164 this.editing = false; | |
165 }, | |
166 | |
167 /** | |
168 * Whether the item is a new item placeholder. | |
169 * @type {boolean} | |
170 */ | |
171 get isPlaceholder() { | |
172 return this.isPlaceholder_; | |
173 }, | |
174 set isPlaceholder(isPlaceholder) { | |
175 this.isPlaceholder_ = isPlaceholder; | |
176 if (isPlaceholder) | |
177 this.deletable = false; | |
178 }, | |
179 | |
180 /** | |
181 * The HTML element that should have focus initially when editing starts, | |
182 * if a specific element wasn't clicked. | |
183 * Defaults to the first <input> element; can be overridden by subclasses if | |
184 * a different element should be focused. | |
185 * @type {HTMLElement} | |
186 */ | |
187 get initialFocusElement() { | |
188 return this.contentElement.querySelector('input'); | |
189 }, | |
190 | |
191 /** | |
192 * Whether the input in currently valid to submit. If this returns false | |
193 * when editing would be submitted, either editing will not be ended, | |
194 * or it will be cancelled, depending on the context. | |
195 * Can be overridden by subclasses to perform input validation. | |
196 * @type {boolean} | |
197 */ | |
198 get currentInputIsValid() { | |
199 return true; | |
200 }, | |
201 | |
202 /** | |
203 * Returns true if the item has been changed by an edit. | |
204 * Can be overridden by subclasses to return false when nothing has changed | |
205 * to avoid unnecessary commits. | |
206 * @type {boolean} | |
207 */ | |
208 get hasBeenEdited() { | |
209 return true; | |
210 }, | |
211 | |
212 /** | |
213 * Returns a div containing an <input>, as well as static text if | |
214 * isPlaceholder is not true. | |
215 * @param {string} text The text of the cell. | |
216 * @return {HTMLElement} The HTML element for the cell. | |
217 * @private | |
218 */ | |
219 createEditableTextCell: function(text) { | |
220 var container = this.ownerDocument.createElement('div'); | |
221 | |
222 if (!this.isPlaceholder) { | |
223 var textEl = this.ownerDocument.createElement('div'); | |
224 textEl.className = 'static-text'; | |
225 textEl.textContent = text; | |
226 textEl.setAttribute('displaymode', 'static'); | |
227 container.appendChild(textEl); | |
228 } | |
229 | |
230 var inputEl = this.ownerDocument.createElement('input'); | |
231 inputEl.type = 'text'; | |
232 inputEl.value = text; | |
233 if (!this.isPlaceholder) { | |
234 inputEl.setAttribute('displaymode', 'edit'); | |
235 inputEl.staticVersion = textEl; | |
236 } else { | |
237 // At this point |this| is not attached to the parent list yet, so give | |
238 // a short timeout in order for the attachment to occur. | |
239 var self = this; | |
240 window.setTimeout(function() { | |
241 var list = self.parentNode; | |
242 if (list && list.focusPlaceholder) { | |
243 list.focusPlaceholder = false; | |
244 if (list.shouldFocusPlaceholder()) | |
245 inputEl.focus(); | |
246 } | |
247 }, 50); | |
248 } | |
249 | |
250 inputEl.addEventListener('focus', this.handleFocus_.bind(this)); | |
251 container.appendChild(inputEl); | |
252 this.editFields_.push(inputEl); | |
253 | |
254 return container; | |
255 }, | |
256 | |
257 /** | |
258 * Resets the editable version of any controls created by createEditable* | |
259 * to match the static text. | |
260 * @private | |
261 */ | |
262 resetEditableValues_: function() { | |
263 var editFields = this.editFields_; | |
264 for (var i = 0; i < editFields.length; i++) { | |
265 var staticLabel = editFields[i].staticVersion; | |
266 if (!staticLabel && !this.isPlaceholder) | |
267 continue; | |
268 | |
269 if (editFields[i].tagName == 'INPUT') { | |
270 editFields[i].value = | |
271 this.isPlaceholder ? '' : staticLabel.textContent; | |
272 } | |
273 // Add more tag types here as new createEditable* methods are added. | |
274 | |
275 editFields[i].setCustomValidity(''); | |
276 } | |
277 }, | |
278 | |
279 /** | |
280 * Sets the static version of any controls created by createEditable* | |
281 * to match the current value of the editable version. Called on commit so | |
282 * that there's no flicker of the old value before the model updates. | |
283 * @private | |
284 */ | |
285 updateStaticValues_: function() { | |
286 var editFields = this.editFields_; | |
287 for (var i = 0; i < editFields.length; i++) { | |
288 var staticLabel = editFields[i].staticVersion; | |
289 if (!staticLabel) | |
290 continue; | |
291 | |
292 if (editFields[i].tagName == 'INPUT') | |
293 staticLabel.textContent = editFields[i].value; | |
294 // Add more tag types here as new createEditable* methods are added. | |
295 } | |
296 }, | |
297 | |
298 /** | |
299 * Called when a key is pressed. Handles committing and canceling edits. | |
300 * @param {Event} e The key down event. | |
301 * @private | |
302 */ | |
303 handleKeyDown_: function(e) { | |
304 if (!this.editing) | |
305 return; | |
306 | |
307 var endEdit = false; | |
308 switch (e.keyIdentifier) { | |
309 case 'U+001B': // Esc | |
310 this.editCancelled_ = true; | |
311 endEdit = true; | |
312 break; | |
313 case 'Enter': | |
314 if (this.currentInputIsValid) | |
315 endEdit = true; | |
316 break; | |
317 } | |
318 | |
319 if (endEdit) { | |
320 // Blurring will trigger the edit to end; see InlineEditableItemList. | |
321 this.ownerDocument.activeElement.blur(); | |
322 // Make sure that handled keys aren't passed on and double-handled. | |
323 // (e.g., esc shouldn't both cancel an edit and close a subpage) | |
324 e.stopPropagation(); | |
325 } | |
326 }, | |
327 | |
328 /** | |
329 * Called when the list item is clicked. If the click target corresponds to | |
330 * an editable item, stores that item to focus when edit mode is started. | |
331 * @param {Event} e The mouse down event. | |
332 * @private | |
333 */ | |
334 handleMouseDown_: function(e) { | |
335 if (!this.editable || this.editing) | |
336 return; | |
337 | |
338 var clickTarget = e.target; | |
339 var editFields = this.editFields_; | |
340 for (var i = 0; i < editFields.length; i++) { | |
341 if (editFields[i] == clickTarget || | |
342 editFields[i].staticVersion == clickTarget) { | |
343 this.editClickTarget_ = editFields[i]; | |
344 return; | |
345 } | |
346 } | |
347 }, | |
348 }; | |
349 | |
350 /** | |
351 * Takes care of committing changes to inline editable list items when the | |
352 * window loses focus. | |
353 */ | |
354 function handleWindowBlurs() { | |
355 window.addEventListener('blur', function(e) { | |
356 var itemAncestor = findAncestor(document.activeElement, function(node) { | |
357 return node instanceof InlineEditableItem; | |
358 }); | |
359 if (itemAncestor) | |
360 document.activeElement.blur(); | |
361 }); | |
362 } | |
363 handleWindowBlurs(); | |
364 | |
365 var InlineEditableItemList = cr.ui.define('list'); | |
366 | |
367 InlineEditableItemList.prototype = { | |
368 __proto__: DeletableItemList.prototype, | |
369 | |
370 /** | |
371 * Focuses the input element of the placeholder if true. | |
372 * @type {boolean} | |
373 */ | |
374 focusPlaceholder: false, | |
375 | |
376 /** @inheritDoc */ | |
377 decorate: function() { | |
378 DeletableItemList.prototype.decorate.call(this); | |
379 this.setAttribute('inlineeditable', ''); | |
380 this.addEventListener('hasElementFocusChange', | |
381 this.handleListFocusChange_); | |
382 }, | |
383 | |
384 /** | |
385 * Called when the list hierarchy as a whole loses or gains focus; starts | |
386 * or ends editing for the lead item if necessary. | |
387 * @param {Event} e The change event. | |
388 * @private | |
389 */ | |
390 handleListFocusChange_: function(e) { | |
391 var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex); | |
392 if (leadItem) { | |
393 if (e.newValue) | |
394 leadItem.updateEditState(); | |
395 else | |
396 leadItem.editing = false; | |
397 } | |
398 }, | |
399 | |
400 /** | |
401 * May be overridden by subclasses to disable focusing the placeholder. | |
402 * @return {boolean} True if the placeholder element should be focused on | |
403 * edit commit. | |
404 */ | |
405 shouldFocusPlaceholder: function() { | |
406 return true; | |
407 }, | |
408 }; | |
409 | |
410 // Export | |
411 return { | |
412 InlineEditableItem: InlineEditableItem, | |
413 InlineEditableItemList: InlineEditableItemList, | |
414 }; | |
415 }); | |
OLD | NEW |