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