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