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