Home | History | Annotate | Download | only in options
      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     '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   };
     45 
     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   }
     61 
     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   }
     79 
     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   }
     95 
     96   var parentLookup = {};
     97   var lookupRequests = {};
     98 
     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;
    112 
    113     listItem.origin = origin;
    114     listItem.list = list;
    115     listItem.decorate();
    116 
    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     }
    127 
    128     return listItem;
    129   }
    130 
    131   CookieListItem.prototype = {
    132     __proto__: DeletableItem.prototype,
    133 
    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;
    147 
    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     },
    164 
    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     },
    189 
    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     },
    202 
    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     },
    213 
    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     },
    235 
    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);
    253 
    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'));
    271 
    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;
    280 
    281       var apps = info.appsProtectingThis;
    282       for (var key in apps) {
    283         addAppInfo(this.dataChild, apps[key]);
    284       }
    285 
    286       if (info.quota && info.quota.totalUsage)
    287         this.sizeChild.textContent = info.quota.totalUsage;
    288 
    289       if (this.expanded)
    290         this.updateItems_();
    291     },
    292 
    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     },
    309 
    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     },
    321 
    322     /**
    323      * The currently selected cookie node ("cookie bubble") index.
    324      * @type {number}
    325      * @private
    326      */
    327     selectedIndex_: -1,
    328 
    329     /**
    330      * Get the currently selected cookie node ("cookie bubble") index.
    331      * @type {number}
    332      */
    333     get selectedIndex() {
    334       return this.selectedIndex_;
    335     },
    336 
    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   };
    376 
    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   }
    387 
    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     },
    401 
    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     },
    413 
    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     },
    426 
    427     /**
    428      * The counter used by startBatchUpdates() and endBatchUpdates().
    429      * @type {number}
    430      */
    431     batchCount_: 0,
    432 
    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     },
    440 
    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     },
    449 
    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     },
    458 
    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         }
    488 
    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     },
    499 
    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       }
    511 
    512       if (!this.data || this.data.hasChildren)
    513         return;
    514 
    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;
    526 
    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));
    536 
    537       var index = item.appendItem(this, div);
    538       div.onclick = function() {
    539         item.selectedIndex = (item.selectedIndex == index) ? -1 : index;
    540       };
    541     },
    542 
    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       }
    567 
    568       while (element.childNodes.length > 1)
    569         element.removeChild(element.firstChild);
    570 
    571       if (table)
    572         element.insertBefore(table, element.firstChild);
    573     },
    574 
    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;
    588 
    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       }
    625 
    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       }
    632 
    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     },
    639 
    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     },
    657 
    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   };
    669 
    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');
    677 
    678   CookiesList.prototype = {
    679     __proto__: DeletableItemList.prototype,
    680 
    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     },
    719 
    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     },
    740 
    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     },
    766 
    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     },
    789 
    790     /**
    791      * The currently expanded item. Used by CookieListItem above.
    792      * @type {?CookieListItem}
    793      */
    794     expandedItem: null,
    795 
    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     },
    805 
    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     },
    816 
    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     },
    826 
    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     },
    836 
    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     },
    847 
    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;
    858 
    859       parent.startBatchUpdates();
    860       parent.insertAt(nodesData, start);
    861       parent.endBatchUpdates();
    862 
    863       cr.dispatchSimpleEvent(this, 'change');
    864     },
    865 
    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     },
    877 
    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;
    889 
    890       parent.startBatchUpdates();
    891       while (count-- > 0)
    892         parent.remove(start);
    893       parent.endBatchUpdates();
    894 
    895       cr.dispatchSimpleEvent(this, 'change');
    896     },
    897 
    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;
    910 
    911       parent.startBatchUpdates();
    912       parent.clear();
    913       this.addByParent_(parent, 0, children);
    914       parent.endBatchUpdates();
    915     },
    916   };
    917 
    918   return {
    919     CookiesList: CookiesList
    920   };
    921 });
    922