OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 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 // TODO(gbillock): refactor this together with CookiesList once we have | |
6 // a better sense from UX design what it'll look like and so what'll be shared. | |
7 cr.define('options', function() { | |
8 const DeletableItemList = options.DeletableItemList; | |
9 const DeletableItem = options.DeletableItem; | |
10 const ArrayDataModel = cr.ui.ArrayDataModel; | |
11 const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; | |
12 const localStrings = new LocalStrings(); | |
13 | |
14 /** | |
15 * Returns the item's height, like offsetHeight but such that it works better | |
16 * when the page is zoomed. See the similar calculation in @{code cr.ui.List}. | |
17 * This version also accounts for the animation done in this file. | |
18 * @param {Element} item The item to get the height of. | |
19 * @return {number} The height of the item, calculated with zooming in mind. | |
20 */ | |
21 function getItemHeight(item) { | |
22 var height = item.style.height; | |
23 // Use the fixed animation target height if set, in case the element is | |
24 // currently being animated and we'd get an intermediate height below. | |
25 if (height && height.substr(-2) == 'px') | |
26 return parseInt(height.substr(0, height.length - 2)); | |
27 return item.getBoundingClientRect().height; | |
28 } | |
29 | |
30 // Map of parent pathIDs to node objects. | |
31 var parentLookup = {}; | |
32 | |
33 // Pending requests for child information. | |
34 var lookupRequests = {}; | |
35 | |
36 /** | |
37 * Creates a new list item for intent service data. Note that these are | |
38 * created and destroyed lazily as they scroll into and out of view, | |
39 * so they must be stateless. We cache the expanded item in | |
40 * @{code IntentsList} though, so it can keep state. | |
41 * (Mostly just which item is selected.) | |
42 * | |
43 * @param {Object} origin Data used to create an intents list item. | |
44 * @param {IntentsList} list The list that will contain this item. | |
45 * @constructor | |
46 * @extends {DeletableItem} | |
47 */ | |
48 function IntentsListItem(origin, list) { | |
49 var listItem = new DeletableItem(null); | |
50 listItem.__proto__ = IntentsListItem.prototype; | |
51 | |
52 listItem.origin = origin; | |
53 listItem.list = list; | |
54 listItem.decorate(); | |
55 | |
56 // This hooks up updateOrigin() to the list item, makes the top-level | |
57 // tree nodes (i.e., origins) register their IDs in parentLookup, and | |
58 // causes them to request their children if they have none. Note that we | |
59 // have special logic in the setter for the parent property to make sure | |
60 // that we can still garbage collect list items when they scroll out of | |
61 // view, even though it appears that we keep a direct reference. | |
62 if (origin) { | |
63 origin.parent = listItem; | |
64 origin.updateOrigin(); | |
65 } | |
66 | |
67 return listItem; | |
68 } | |
69 | |
70 IntentsListItem.prototype = { | |
71 __proto__: DeletableItem.prototype, | |
72 | |
73 /** @inheritDoc */ | |
74 decorate: function() { | |
75 this.siteChild = this.ownerDocument.createElement('div'); | |
76 this.siteChild.className = 'intents-site'; | |
77 this.dataChild = this.ownerDocument.createElement('div'); | |
78 this.dataChild.className = 'intents-data'; | |
79 this.itemsChild = this.ownerDocument.createElement('div'); | |
80 this.itemsChild.className = 'intents-items'; | |
81 this.infoChild = this.ownerDocument.createElement('div'); | |
82 this.infoChild.className = 'intents-details'; | |
83 this.infoChild.hidden = true; | |
84 var remove = this.ownerDocument.createElement('button'); | |
85 remove.textContent = localStrings.getString('removeIntent'); | |
86 remove.onclick = this.removeIntent_.bind(this); | |
87 this.infoChild.appendChild(remove); | |
88 var content = this.contentElement; | |
89 content.appendChild(this.siteChild); | |
90 content.appendChild(this.dataChild); | |
91 content.appendChild(this.itemsChild); | |
92 this.itemsChild.appendChild(this.infoChild); | |
93 if (this.origin && this.origin.data) { | |
94 this.siteChild.textContent = this.origin.data.site; | |
95 this.siteChild.setAttribute('title', this.origin.data.site); | |
96 } | |
97 this.itemList_ = []; | |
98 }, | |
99 | |
100 /** @type {boolean} */ | |
101 get expanded() { | |
102 return this.expanded_; | |
103 }, | |
104 set expanded(expanded) { | |
105 if (this.expanded_ == expanded) | |
106 return; | |
107 this.expanded_ = expanded; | |
108 if (expanded) { | |
109 var oldExpanded = this.list.expandedItem; | |
110 this.list.expandedItem = this; | |
111 this.updateItems_(); | |
112 if (oldExpanded) | |
113 oldExpanded.expanded = false; | |
114 this.classList.add('show-items'); | |
115 this.dataChild.hidden = true; | |
116 } else { | |
117 if (this.list.expandedItem == this) { | |
118 this.list.expandedItem = null; | |
119 } | |
120 this.style.height = ''; | |
121 this.itemsChild.style.height = ''; | |
122 this.classList.remove('show-items'); | |
123 this.dataChild.hidden = false; | |
124 } | |
125 }, | |
126 | |
127 /** | |
128 * The callback for the "remove" button shown when an item is selected. | |
129 * Requests that the currently selected intent service be removed. | |
130 * @private | |
131 */ | |
132 removeIntent_: function() { | |
133 if (this.selectedIndex_ >= 0) { | |
134 var item = this.itemList_[this.selectedIndex_]; | |
135 if (item && item.node) | |
136 chrome.send('removeIntent', [item.node.pathId]); | |
137 } | |
138 }, | |
139 | |
140 /** | |
141 * Disable animation within this intents list item, in preparation for | |
142 * making changes that will need to be animated. Makes it possible to | |
143 * measure the contents without displaying them, to set animation targets. | |
144 * @private | |
145 */ | |
146 disableAnimation_: function() { | |
147 this.itemsHeight_ = getItemHeight(this.itemsChild); | |
148 this.classList.add('measure-items'); | |
149 }, | |
150 | |
151 /** | |
152 * Enable animation after changing the contents of this intents list item. | |
153 * See @{code disableAnimation_}. | |
154 * @private | |
155 */ | |
156 enableAnimation_: function() { | |
157 if (!this.classList.contains('measure-items')) | |
158 this.disableAnimation_(); | |
159 this.itemsChild.style.height = ''; | |
160 // This will force relayout in order to calculate the new heights. | |
161 var itemsHeight = getItemHeight(this.itemsChild); | |
162 var fixedHeight = getItemHeight(this) + itemsHeight - this.itemsHeight_; | |
163 this.itemsChild.style.height = this.itemsHeight_ + 'px'; | |
164 // Force relayout before enabling animation, so that if we have | |
165 // changed things since the last layout, they will not be animated | |
166 // during subsequent layouts. | |
167 this.itemsChild.offsetHeight; | |
168 this.classList.remove('measure-items'); | |
169 this.itemsChild.style.height = itemsHeight + 'px'; | |
170 this.style.height = fixedHeight + 'px'; | |
171 }, | |
172 | |
173 /** | |
174 * Updates the origin summary to reflect changes in its items. | |
175 * Both IntentsListItem and IntentsTreeNode implement this API. | |
176 * This implementation scans the descendants to update the text. | |
177 */ | |
178 updateOrigin: function() { | |
179 var text = ''; | |
180 for (var i = 0; i < this.origin.children.length; ++i) { | |
181 if (text.length > 0) | |
182 text += ', ' + this.origin.children[i].data.action; | |
183 else | |
184 text = this.origin.children[i].data.action; | |
185 } | |
186 this.dataChild.textContent = text; | |
187 | |
188 if (this.expanded) | |
189 this.updateItems_(); | |
190 }, | |
191 | |
192 /** | |
193 * Updates the items section to reflect changes, animating to the new state. | |
194 * Removes existing contents and calls @{code IntentsTreeNode.createItems}. | |
195 * @private | |
196 */ | |
197 updateItems_: function() { | |
198 this.disableAnimation_(); | |
199 this.itemsChild.textContent = ''; | |
200 this.infoChild.hidden = true; | |
201 this.selectedIndex_ = -1; | |
202 this.itemList_ = []; | |
203 if (this.origin) | |
204 this.origin.createItems(this); | |
205 this.itemsChild.appendChild(this.infoChild); | |
206 this.enableAnimation_(); | |
207 }, | |
208 | |
209 /** | |
210 * Append a new intents node "bubble" to this list item. | |
211 * @param {IntentsTreeNode} node The intents node to add a bubble for. | |
212 * @param {Element} div The DOM element for the bubble itself. | |
213 * @return {number} The index the bubble was added at. | |
214 */ | |
215 appendItem: function(node, div) { | |
216 this.itemList_.push({node: node, div: div}); | |
217 this.itemsChild.appendChild(div); | |
218 return this.itemList_.length - 1; | |
219 }, | |
220 | |
221 /** | |
222 * The currently selected intents node ("intents bubble") index. | |
223 * @type {number} | |
224 * @private | |
225 */ | |
226 selectedIndex_: -1, | |
227 | |
228 /** | |
229 * Get the currently selected intents node ("intents bubble") index. | |
230 * @type {number} | |
231 */ | |
232 get selectedIndex() { | |
233 return this.selectedIndex_; | |
234 }, | |
235 | |
236 /** | |
237 * Set the currently selected intents node ("intents bubble") index to | |
238 * @{code itemIndex}, unselecting any previously selected node first. | |
239 * @param {number} itemIndex The index to set as the selected index. | |
240 * TODO: KILL THIS | |
241 */ | |
242 set selectedIndex(itemIndex) { | |
243 // Get the list index up front before we change anything. | |
244 var index = this.list.getIndexOfListItem(this); | |
245 // Unselect any previously selected item. | |
246 if (this.selectedIndex_ >= 0) { | |
247 var item = this.itemList_[this.selectedIndex_]; | |
248 if (item && item.div) | |
249 item.div.removeAttribute('selected'); | |
250 } | |
251 // Special case: decrementing -1 wraps around to the end of the list. | |
252 if (itemIndex == -2) | |
253 itemIndex = this.itemList_.length - 1; | |
254 // Check if we're going out of bounds and hide the item details. | |
255 if (itemIndex < 0 || itemIndex >= this.itemList_.length) { | |
256 this.selectedIndex_ = -1; | |
257 this.disableAnimation_(); | |
258 this.infoChild.hidden = true; | |
259 this.enableAnimation_(); | |
260 return; | |
261 } | |
262 // Set the new selected item and show the item details for it. | |
263 this.selectedIndex_ = itemIndex; | |
264 this.itemList_[itemIndex].div.setAttribute('selected', ''); | |
265 this.disableAnimation_(); | |
266 this.infoChild.hidden = false; | |
267 this.enableAnimation_(); | |
268 // If we're near the bottom of the list this may cause the list item to go | |
269 // beyond the end of the visible area. Fix it after the animation is done. | |
270 var list = this.list; | |
271 window.setTimeout(function() { list.scrollIndexIntoView(index); }, 150); | |
272 }, | |
273 }; | |
274 | |
275 /** | |
276 * {@code IntentsTreeNode}s mirror the structure of the intents tree lazily, | |
277 * and contain all the actual data used to generate the | |
278 * {@code IntentsListItem}s. | |
279 * @param {Object} data The data object for this node. | |
280 * @constructor | |
281 */ | |
282 function IntentsTreeNode(data) { | |
283 this.data = data; | |
284 this.children = []; | |
285 } | |
286 | |
287 IntentsTreeNode.prototype = { | |
288 /** | |
289 * Insert an intents tree node at the given index. | |
290 * Both IntentsList and IntentsTreeNode implement this API. | |
291 * @param {Object} data The data object for the node to add. | |
292 * @param {number} index The index at which to insert the node. | |
293 */ | |
294 insertAt: function(data, index) { | |
295 var child = new IntentsTreeNode(data); | |
296 this.children.splice(index, 0, child); | |
297 child.parent = this; | |
298 this.updateOrigin(); | |
299 }, | |
300 | |
301 /** | |
302 * Remove an intents tree node from the given index. | |
303 * Both IntentsList and IntentsTreeNode implement this API. | |
304 * @param {number} index The index of the tree node to remove. | |
305 */ | |
306 remove: function(index) { | |
307 if (index < this.children.length) { | |
308 this.children.splice(index, 1); | |
309 this.updateOrigin(); | |
310 } | |
311 }, | |
312 | |
313 /** | |
314 * Clears all children. | |
315 * Both IntentsList and IntentsTreeNode implement this API. | |
316 * It is used by IntentsList.loadChildren(). | |
317 */ | |
318 clear: function() { | |
319 // We might leave some garbage in parentLookup for removed children. | |
320 // But that should be OK because parentLookup is cleared when we | |
321 // reload the tree. | |
322 this.children = []; | |
323 this.updateOrigin(); | |
324 }, | |
325 | |
326 /** | |
327 * The counter used by startBatchUpdates() and endBatchUpdates(). | |
328 * @type {number} | |
329 */ | |
330 batchCount_: 0, | |
331 | |
332 /** | |
333 * See cr.ui.List.startBatchUpdates(). | |
334 * Both IntentsList (via List) and IntentsTreeNode implement this API. | |
335 */ | |
336 startBatchUpdates: function() { | |
337 this.batchCount_++; | |
338 }, | |
339 | |
340 /** | |
341 * See cr.ui.List.endBatchUpdates(). | |
342 * Both IntentsList (via List) and IntentsTreeNode implement this API. | |
343 */ | |
344 endBatchUpdates: function() { | |
345 if (!--this.batchCount_) | |
346 this.updateOrigin(); | |
347 }, | |
348 | |
349 /** | |
350 * Requests updating the origin summary to reflect changes in this item. | |
351 * Both IntentsListItem and IntentsTreeNode implement this API. | |
352 */ | |
353 updateOrigin: function() { | |
354 if (!this.batchCount_ && this.parent) | |
355 this.parent.updateOrigin(); | |
356 }, | |
357 | |
358 /** | |
359 * Create the intents services rows for this node. | |
360 * Append the rows to @{code item}. | |
361 * @param {IntentsListItem} item The intents list item to create items in. | |
362 */ | |
363 createItems: function(item) { | |
364 if (this.children.length > 0) { | |
365 for (var i = 0; i < this.children.length; ++i) | |
366 this.children[i].createItems(item); | |
367 } else if (this.data && !this.data.hasChildren) { | |
368 var div = item.ownerDocument.createElement('div'); | |
369 div.className = 'intents-item'; | |
370 // Help out screen readers and such: this is a clickable thing. | |
371 div.setAttribute('role', 'button'); | |
372 | |
373 var divAction = item.ownerDocument.createElement('div'); | |
374 divAction.className = 'intents-item-action'; | |
375 divAction.textContent = this.data.action; | |
376 div.appendChild(divAction); | |
377 | |
378 var divTypes = item.ownerDocument.createElement('div'); | |
379 divTypes.className = 'intents-item-types'; | |
380 var text = ""; | |
381 for (var i = 0; i < this.data.types.length; ++i) { | |
382 if (text != "") | |
383 text += ", "; | |
384 text += this.data.types[i]; | |
385 } | |
386 divTypes.textContent = text; | |
387 div.appendChild(divTypes); | |
388 | |
389 var divUrl = item.ownerDocument.createElement('div'); | |
390 divUrl.className = 'intents-item-url'; | |
391 divUrl.textContent = this.data.url; | |
392 div.appendChild(divUrl); | |
393 | |
394 var index = item.appendItem(this, div); | |
395 div.onclick = function() { | |
396 if (item.selectedIndex == index) | |
397 item.selectedIndex = -1; | |
398 else | |
399 item.selectedIndex = index; | |
400 }; | |
401 } | |
402 }, | |
403 | |
404 /** | |
405 * The parent of this intents tree node. | |
406 * @type {?IntentsTreeNode|IntentsListItem} | |
407 */ | |
408 get parent(parent) { | |
409 // See below for an explanation of this special case. | |
410 if (typeof this.parent_ == 'number') | |
411 return this.list_.getListItemByIndex(this.parent_); | |
412 return this.parent_; | |
413 }, | |
414 set parent(parent) { | |
415 if (parent == this.parent) | |
416 return; | |
417 | |
418 if (parent instanceof IntentsListItem) { | |
419 // If the parent is to be a IntentsListItem, then we keep the reference | |
420 // to it by its containing list and list index, rather than directly. | |
421 // This allows the list items to be garbage collected when they scroll | |
422 // out of view (except the expanded item, which we cache). This is | |
423 // transparent except in the setter and getter, where we handle it. | |
424 this.parent_ = parent.listIndex; | |
425 this.list_ = parent.list; | |
426 parent.addEventListener('listIndexChange', | |
427 this.parentIndexChanged_.bind(this)); | |
428 } else { | |
429 this.parent_ = parent; | |
430 } | |
431 | |
432 | |
433 if (parent) | |
434 parentLookup[this.pathId] = this; | |
435 else | |
436 delete parentLookup[this.pathId]; | |
437 | |
438 if (this.data && this.data.hasChildren && | |
439 !this.children.length && !lookupRequests[this.pathId]) { | |
440 lookupRequests[this.pathId] = true; | |
441 chrome.send('loadIntents', [this.pathId]); | |
442 } | |
443 }, | |
444 | |
445 /** | |
446 * Called when the parent is a IntentsListItem whose index has changed. | |
447 * See the code above that avoids keeping a direct reference to | |
448 * IntentsListItem parents, to allow them to be garbage collected. | |
449 * @private | |
450 */ | |
451 parentIndexChanged_: function(event) { | |
452 if (typeof this.parent_ == 'number') { | |
453 this.parent_ = event.newValue; | |
454 // We set a timeout to update the origin, rather than doing it right | |
455 // away, because this callback may occur while the list items are | |
456 // being repopulated following a scroll event. Calling updateOrigin() | |
457 // immediately could trigger relayout that would reset the scroll | |
458 // position within the list, among other things. | |
459 window.setTimeout(this.updateOrigin.bind(this), 0); | |
460 } | |
461 }, | |
462 | |
463 /** | |
464 * The intents tree path id. | |
465 * @type {string} | |
466 */ | |
467 get pathId() { | |
468 var parent = this.parent; | |
469 if (parent && parent instanceof IntentsTreeNode) | |
470 return parent.pathId + ',' + this.data.action; | |
471 return this.data.site; | |
472 }, | |
473 }; | |
474 | |
475 /** | |
476 * Creates a new intents list. | |
477 * @param {Object=} opt_propertyBag Optional properties. | |
478 * @constructor | |
479 * @extends {DeletableItemList} | |
480 */ | |
481 var IntentsList = cr.ui.define('list'); | |
482 | |
483 IntentsList.prototype = { | |
484 __proto__: DeletableItemList.prototype, | |
485 | |
486 /** @inheritDoc */ | |
487 decorate: function() { | |
488 DeletableItemList.prototype.decorate.call(this); | |
489 this.classList.add('intents-list'); | |
490 this.data_ = []; | |
491 this.dataModel = new ArrayDataModel(this.data_); | |
492 this.addEventListener('keydown', this.handleKeyLeftRight_.bind(this)); | |
493 var sm = new ListSingleSelectionModel(); | |
494 sm.addEventListener('change', this.cookieSelectionChange_.bind(this)); | |
495 sm.addEventListener('leadIndexChange', this.cookieLeadChange_.bind(this)); | |
496 this.selectionModel = sm; | |
497 this.fixedHeight = false; | |
498 }, | |
499 | |
500 /** | |
501 * Handles key down events and looks for left and right arrows, then | |
502 * dispatches to the currently expanded item, if any. | |
503 * @param {Event} e The keydown event. | |
504 * @private | |
505 */ | |
506 handleKeyLeftRight_: function(e) { | |
507 var id = e.keyIdentifier; | |
508 if ((id == 'Left' || id == 'Right') && this.expandedItem) { | |
509 var cs = this.ownerDocument.defaultView.getComputedStyle(this); | |
510 var rtl = cs.direction == 'rtl'; | |
511 if ((!rtl && id == 'Left') || (rtl && id == 'Right')) | |
512 this.expandedItem.selectedIndex--; | |
513 else | |
514 this.expandedItem.selectedIndex++; | |
515 this.scrollIndexIntoView(this.expandedItem.listIndex); | |
516 // Prevent the page itself from scrolling. | |
517 e.preventDefault(); | |
518 } | |
519 }, | |
520 | |
521 /** | |
522 * Called on selection model selection changes. | |
523 * @param {Event} ce The selection change event. | |
524 * @private | |
525 */ | |
526 cookieSelectionChange_: function(ce) { | |
527 ce.changes.forEach(function(change) { | |
528 var listItem = this.getListItemByIndex(change.index); | |
529 if (listItem) { | |
530 if (!change.selected) { | |
531 // We set a timeout here, rather than setting the item unexpanded | |
532 // immediately, so that if another item gets set expanded right | |
533 // away, it will be expanded before this item is unexpanded. It | |
534 // will notice that, and unexpand this item in sync with its own | |
535 // expansion. Later, this callback will end up having no effect. | |
536 window.setTimeout(function() { | |
537 if (!listItem.selected || !listItem.lead) | |
538 listItem.expanded = false; | |
539 }, 0); | |
540 } else if (listItem.lead) { | |
541 listItem.expanded = true; | |
542 } | |
543 } | |
544 }, this); | |
545 }, | |
546 | |
547 /** | |
548 * Called on selection model lead changes. | |
549 * @param {Event} pe The lead change event. | |
550 * @private | |
551 */ | |
552 cookieLeadChange_: function(pe) { | |
553 if (pe.oldValue != -1) { | |
554 var listItem = this.getListItemByIndex(pe.oldValue); | |
555 if (listItem) { | |
556 // See cookieSelectionChange_ above for why we use a timeout here. | |
557 window.setTimeout(function() { | |
558 if (!listItem.lead || !listItem.selected) | |
559 listItem.expanded = false; | |
560 }, 0); | |
561 } | |
562 } | |
563 if (pe.newValue != -1) { | |
564 var listItem = this.getListItemByIndex(pe.newValue); | |
565 if (listItem && listItem.selected) | |
566 listItem.expanded = true; | |
567 } | |
568 }, | |
569 | |
570 /** | |
571 * The currently expanded item. Used by IntentsListItem above. | |
572 * @type {?IntentsListItem} | |
573 */ | |
574 expandedItem: null, | |
575 | |
576 // from cr.ui.List | |
577 /** @inheritDoc */ | |
578 createItem: function(data) { | |
579 // We use the cached expanded item in order to allow it to maintain some | |
580 // state (like its fixed height, and which bubble is selected). | |
581 if (this.expandedItem && this.expandedItem.origin == data) | |
582 return this.expandedItem; | |
583 return new IntentsListItem(data, this); | |
584 }, | |
585 | |
586 // from options.DeletableItemList | |
587 /** @inheritDoc */ | |
588 deleteItemAtIndex: function(index) { | |
589 var item = this.data_[index]; | |
590 if (item) { | |
591 var pathId = item.pathId; | |
592 if (pathId) | |
593 chrome.send('removeIntent', [pathId]); | |
594 } | |
595 }, | |
596 | |
597 /** | |
598 * Insert an intents tree node at the given index. | |
599 * Both IntentsList and IntentsTreeNode implement this API. | |
600 * @param {Object} data The data object for the node to add. | |
601 * @param {number} index The index at which to insert the node. | |
602 */ | |
603 insertAt: function(data, index) { | |
604 this.dataModel.splice(index, 0, new IntentsTreeNode(data)); | |
605 }, | |
606 | |
607 /** | |
608 * Remove an intents tree node from the given index. | |
609 * Both IntentsList and IntentsTreeNode implement this API. | |
610 * @param {number} index The index of the tree node to remove. | |
611 */ | |
612 remove: function(index) { | |
613 if (index < this.data_.length) | |
614 this.dataModel.splice(index, 1); | |
615 }, | |
616 | |
617 /** | |
618 * Clears the list. | |
619 * Both IntentsList and IntentsTreeNode implement this API. | |
620 * It is used by IntentsList.loadChildren(). | |
621 */ | |
622 clear: function() { | |
623 parentLookup = {}; | |
624 this.data_ = []; | |
625 this.dataModel = new ArrayDataModel(this.data_); | |
626 this.redraw(); | |
627 }, | |
628 | |
629 /** | |
630 * Add tree nodes by given parent. | |
631 * Note: this method will be O(n^2) in the general case. Use it only to | |
632 * populate an empty parent or to insert single nodes to avoid this. | |
633 * @param {Object} parent The parent node. | |
634 * @param {number} start Start index of where to insert nodes. | |
635 * @param {Array} nodesData Nodes data array. | |
636 * @private | |
637 */ | |
638 addByParent_: function(parent, start, nodesData) { | |
639 if (!parent) | |
640 return; | |
641 | |
642 parent.startBatchUpdates(); | |
643 for (var i = 0; i < nodesData.length; ++i) | |
644 parent.insertAt(nodesData[i], start + i); | |
645 parent.endBatchUpdates(); | |
646 | |
647 cr.dispatchSimpleEvent(this, 'change'); | |
648 }, | |
649 | |
650 /** | |
651 * Add tree nodes by parent id. | |
652 * This is used by intents_view.js. | |
653 * Note: this method will be O(n^2) in the general case. Use it only to | |
654 * populate an empty parent or to insert single nodes to avoid this. | |
655 * @param {string} parentId Id of the parent node. | |
656 * @param {number} start Start index of where to insert nodes. | |
657 * @param {Array} nodesData Nodes data array. | |
658 */ | |
659 addByParentId: function(parentId, start, nodesData) { | |
660 var parent = parentId ? parentLookup[parentId] : this; | |
661 this.addByParent_(parent, start, nodesData); | |
662 }, | |
663 | |
664 /** | |
665 * Removes tree nodes by parent id. | |
666 * This is used by intents_view.js. | |
667 * @param {string} parentId Id of the parent node. | |
668 * @param {number} start Start index of nodes to remove. | |
669 * @param {number} count Number of nodes to remove. | |
670 */ | |
671 removeByParentId: function(parentId, start, count) { | |
672 var parent = parentId ? parentLookup[parentId] : this; | |
673 if (!parent) | |
674 return; | |
675 | |
676 parent.startBatchUpdates(); | |
677 while (count-- > 0) | |
678 parent.remove(start); | |
679 parent.endBatchUpdates(); | |
680 | |
681 cr.dispatchSimpleEvent(this, 'change'); | |
682 }, | |
683 | |
684 /** | |
685 * Loads the immediate children of given parent node. | |
686 * This is used by intents_view.js. | |
687 * @param {string} parentId Id of the parent node. | |
688 * @param {Array} children The immediate children of parent node. | |
689 */ | |
690 loadChildren: function(parentId, children) { | |
691 if (parentId) | |
692 delete lookupRequests[parentId]; | |
693 var parent = parentId ? parentLookup[parentId] : this; | |
694 if (!parent) | |
695 return; | |
696 | |
697 parent.startBatchUpdates(); | |
698 parent.clear(); | |
699 this.addByParent_(parent, 0, children); | |
700 parent.endBatchUpdates(); | |
701 }, | |
702 }; | |
703 | |
704 return { | |
705 IntentsList: IntentsList | |
706 }; | |
707 }); | |
OLD | NEW |