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