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