OLD | NEW |
| (Empty) |
1 // Copyright (c) 2010 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 DeletableItem = options.DeletableItem; | |
7 const 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_.bind(this)); | |
76 this.addEventListener('keydown', this.handleKeyDown_.bind(this)); | |
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 this.updateStaticValues_(); | |
143 cr.dispatchSimpleEvent(this, 'commitedit', true); | |
144 } else { | |
145 this.resetEditableValues_(); | |
146 cr.dispatchSimpleEvent(this, 'canceledit', true); | |
147 } | |
148 } | |
149 }, | |
150 | |
151 /** | |
152 * Whether the item is editable. | |
153 * @type {boolean} | |
154 */ | |
155 get editable() { | |
156 return this.editable_; | |
157 }, | |
158 set editable(editable) { | |
159 this.editable_ = editable; | |
160 if (!editable) | |
161 this.editing = false; | |
162 }, | |
163 | |
164 /** | |
165 * Whether the item is a new item placeholder. | |
166 * @type {boolean} | |
167 */ | |
168 get isPlaceholder() { | |
169 return this.isPlaceholder_; | |
170 }, | |
171 set isPlaceholder(isPlaceholder) { | |
172 this.isPlaceholder_ = isPlaceholder; | |
173 if (isPlaceholder) | |
174 this.deletable = false; | |
175 }, | |
176 | |
177 /** | |
178 * The HTML element that should have focus initially when editing starts, | |
179 * if a specific element wasn't clicked. | |
180 * Defaults to the first <input> element; can be overriden by subclasses if | |
181 * a different element should be focused. | |
182 * @type {HTMLElement} | |
183 */ | |
184 get initialFocusElement() { | |
185 return this.contentElement.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 overrided 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 overrided 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 * Returns a div containing an <input>, as well as static text if | |
211 * isPlaceholder is not true. | |
212 * @param {string} text The text of the cell. | |
213 * @return {HTMLElement} The HTML element for the cell. | |
214 * @private | |
215 */ | |
216 createEditableTextCell: function(text) { | |
217 var container = this.ownerDocument.createElement('div'); | |
218 | |
219 if (!this.isPlaceholder) { | |
220 var textEl = this.ownerDocument.createElement('div'); | |
221 textEl.className = 'static-text'; | |
222 textEl.textContent = text; | |
223 textEl.setAttribute('displaymode', 'static'); | |
224 container.appendChild(textEl); | |
225 } | |
226 | |
227 var inputEl = this.ownerDocument.createElement('input'); | |
228 inputEl.type = 'text'; | |
229 inputEl.value = text; | |
230 if (!this.isPlaceholder) { | |
231 inputEl.setAttribute('displaymode', 'edit'); | |
232 inputEl.staticVersion = textEl; | |
233 } | |
234 container.appendChild(inputEl); | |
235 this.editFields_.push(inputEl); | |
236 | |
237 return container; | |
238 }, | |
239 | |
240 /** | |
241 * Resets the editable version of any controls created by createEditable* | |
242 * to match the static text. | |
243 * @private | |
244 */ | |
245 resetEditableValues_: function() { | |
246 var editFields = this.editFields_; | |
247 for (var i = 0; i < editFields.length; i++) { | |
248 var staticLabel = editFields[i].staticVersion; | |
249 if (!staticLabel && !this.isPlaceholder) | |
250 continue; | |
251 if (editFields[i].tagName == 'INPUT') { | |
252 editFields[i].value = | |
253 this.isPlaceholder ? '' : staticLabel.textContent; | |
254 } | |
255 // Add more tag types here as new createEditable* methods are added. | |
256 | |
257 editFields[i].setCustomValidity(''); | |
258 } | |
259 }, | |
260 | |
261 /** | |
262 * Sets the static version of any controls created by createEditable* | |
263 * to match the current value of the editable version. Called on commit so | |
264 * that there's no flicker of the old value before the model updates. | |
265 * @private | |
266 */ | |
267 updateStaticValues_: function() { | |
268 var editFields = this.editFields_; | |
269 for (var i = 0; i < editFields.length; i++) { | |
270 var staticLabel = editFields[i].staticVersion; | |
271 if (!staticLabel) | |
272 continue; | |
273 if (editFields[i].tagName == 'INPUT') | |
274 staticLabel.textContent = editFields[i].value; | |
275 // Add more tag types here as new createEditable* methods are added. | |
276 } | |
277 }, | |
278 | |
279 /** | |
280 * Called a key is pressed. Handles committing and cancelling edits. | |
281 * @param {Event} e The key down event. | |
282 * @private | |
283 */ | |
284 handleKeyDown_: function(e) { | |
285 if (!this.editing) | |
286 return; | |
287 | |
288 var endEdit = false; | |
289 switch (e.keyIdentifier) { | |
290 case 'U+001B': // Esc | |
291 this.editCancelled_ = true; | |
292 endEdit = true; | |
293 break; | |
294 case 'Enter': | |
295 if (this.currentInputIsValid) | |
296 endEdit = true; | |
297 break; | |
298 } | |
299 | |
300 if (endEdit) { | |
301 // Blurring will trigger the edit to end; see InlineEditableItemList. | |
302 this.ownerDocument.activeElement.blur(); | |
303 // Make sure that handled keys aren't passed on and double-handled. | |
304 // (e.g., esc shouldn't both cancel an edit and close a subpage) | |
305 e.stopPropagation(); | |
306 } | |
307 }, | |
308 | |
309 /** | |
310 * Called when the list item is clicked. If the click target corresponds to | |
311 * an editable item, stores that item to focus when edit mode is started. | |
312 * @param {Event} e The mouse down event. | |
313 * @private | |
314 */ | |
315 handleMouseDown_: function(e) { | |
316 if (!this.editable || this.editing) | |
317 return; | |
318 | |
319 var clickTarget = e.target; | |
320 var editFields = this.editFields_; | |
321 for (var i = 0; i < editFields.length; i++) { | |
322 if (editFields[i] == clickTarget || | |
323 editFields[i].staticVersion == clickTarget) { | |
324 this.editClickTarget_ = editFields[i]; | |
325 return; | |
326 } | |
327 } | |
328 }, | |
329 }; | |
330 | |
331 var InlineEditableItemList = cr.ui.define('list'); | |
332 | |
333 InlineEditableItemList.prototype = { | |
334 __proto__: DeletableItemList.prototype, | |
335 | |
336 /** @inheritDoc */ | |
337 decorate: function() { | |
338 DeletableItemList.prototype.decorate.call(this); | |
339 this.setAttribute('inlineeditable', ''); | |
340 this.addEventListener('hasElementFocusChange', | |
341 this.handleListFocusChange_); | |
342 }, | |
343 | |
344 /** | |
345 * Called when the list hierarchy as a whole loses or gains focus; starts | |
346 * or ends editing for the lead item if necessary. | |
347 * @param {Event} e The change event. | |
348 * @private | |
349 */ | |
350 handleListFocusChange_: function(e) { | |
351 var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex); | |
352 if (leadItem) { | |
353 if (e.newValue) | |
354 leadItem.updateEditState(); | |
355 else | |
356 leadItem.editing = false; | |
357 } | |
358 }, | |
359 }; | |
360 | |
361 // Export | |
362 return { | |
363 InlineEditableItem: InlineEditableItem, | |
364 InlineEditableItemList: InlineEditableItemList, | |
365 }; | |
366 }); | |
OLD | NEW |