Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(41)

Side by Side Diff: chrome/browser/resources/options/intents_list.js

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

Powered by Google App Engine
This is Rietveld 408576698