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