blob: c3b97c214b82d71dc1b16b5f4814b0f153d72331 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('options', function() {
/** @const */ var DeletableItemList = options.DeletableItemList;
/** @const */ var DeletableItem = options.DeletableItem;
/** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
/** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
// This structure maps the various cookie type names from C++ (hence the
// underscores) to arrays of the different types of data each has, along with
// the i18n name for the description of that data type.
/** @const */ var cookieInfo = {
'cookie': [['name', 'label_cookie_name'],
['content', 'label_cookie_content'],
['domain', 'label_cookie_domain'],
['path', 'label_cookie_path'],
['sendfor', 'label_cookie_send_for'],
['accessibleToScript', 'label_cookie_accessible_to_script'],
['created', 'label_cookie_created'],
['expires', 'label_cookie_expires']],
'app_cache': [['manifest', 'label_app_cache_manifest'],
['size', 'label_local_storage_size'],
['created', 'label_cookie_created'],
['accessed', 'label_cookie_last_accessed']],
'database': [['name', 'label_cookie_name'],
['desc', 'label_webdb_desc'],
['size', 'label_local_storage_size'],
['modified', 'label_local_storage_last_modified']],
'local_storage': [['origin', 'label_local_storage_origin'],
['size', 'label_local_storage_size'],
['modified', 'label_local_storage_last_modified']],
'indexed_db': [['origin', 'label_indexed_db_origin'],
['size', 'label_indexed_db_size'],
['modified', 'label_indexed_db_last_modified']],
'file_system': [['origin', 'label_file_system_origin'],
['persistent', 'label_file_system_persistent_usage'],
['temporary', 'label_file_system_temporary_usage']],
'channel_id': [['serverId', 'label_channel_id_server_id'],
['certType', 'label_channel_id_type'],
['created', 'label_channel_id_created']],
'service_worker': [['origin', 'label_service_worker_origin'],
['size', 'label_service_worker_size'],
['scopes', 'label_service_worker_scopes']],
'cache_storage': [['origin', 'label_cache_storage_origin'],
['size', 'label_cache_storage_size'],
['modified', 'label_cache_storage_last_modified']],
'flash_lso': [['domain', 'label_cookie_domain']],
};
/**
* Returns the item's height, like offsetHeight but such that it works better
* when the page is zoomed. See the similar calculation in @{code cr.ui.List}.
* This version also accounts for the animation done in this file.
* @param {Element} item The item to get the height of.
* @return {number} The height of the item, calculated with zooming in mind.
*/
function getItemHeight(item) {
var height = item.style.height;
// Use the fixed animation target height if set, in case the element is
// currently being animated and we'd get an intermediate height below.
if (height && height.substr(-2) == 'px')
return parseInt(height.substr(0, height.length - 2), 10);
return item.getBoundingClientRect().height;
}
/**
* Create tree nodes for the objects in the data array, and insert them all
* into the given list using its @{code splice} method at the given index.
* @param {Array<Object>} data The data objects for the nodes to add.
* @param {number} start The index at which to start inserting the nodes.
* @return {Array<options.CookieTreeNode>} An array of CookieTreeNodes added.
*/
function spliceTreeNodes(data, start, list) {
var nodes = data.map(function(x) { return new CookieTreeNode(x); });
// Insert [start, 0] at the beginning of the array of nodes, making it
// into the arguments we want to pass to @{code list.splice} below.
nodes.splice(0, 0, start, 0);
list.splice.apply(list, nodes);
// Remove the [start, 0] prefix and return the array of nodes.
nodes.splice(0, 2);
return nodes;
}
/**
* Adds information about an app that protects this data item to the
* |element|.
* @param {Element} element The DOM element the information should be
appended to.
* @param {{id: string, name: string}} appInfo Information about an app.
*/
function addAppInfo(element, appInfo) {
var img = element.ownerDocument.createElement('img');
img.src = 'chrome://extension-icon/' + appInfo.id + '/16/1';
element.title = loadTimeData.getString('label_protected_by_apps') +
' ' + appInfo.name;
img.className = 'protecting-app';
element.appendChild(img);
}
var parentLookup = {};
var lookupRequests = {};
/**
* Creates a new list item for sites data. Note that these are created and
* destroyed lazily as they scroll into and out of view, so they must be
* stateless. We cache the expanded item in @{code CookiesList} though, so it
* can keep state. (Mostly just which item is selected.)
* @param {Object} origin Data used to create a cookie list item.
* @param {options.CookiesList} list The list that will contain this item.
* @constructor
* @extends {options.DeletableItem}
*/
function CookieListItem(origin, list) {
var listItem = new DeletableItem();
listItem.__proto__ = CookieListItem.prototype;
listItem.origin = origin;
listItem.list = list;
listItem.decorate();
// This hooks up updateOrigin() to the list item, makes the top-level
// tree nodes (i.e., origins) register their IDs in parentLookup, and
// causes them to request their children if they have none. Note that we
// have special logic in the setter for the parent property to make sure
// that we can still garbage collect list items when they scroll out of
// view, even though it appears that we keep a direct reference.
if (origin) {
origin.parent = listItem;
origin.updateOrigin();
}
return listItem;
}
CookieListItem.prototype = {
__proto__: DeletableItem.prototype,
/** @override */
decorate: function() {
this.siteChild = this.ownerDocument.createElement('div');
this.siteChild.className = 'cookie-site';
this.dataChild = this.ownerDocument.createElement('div');
this.dataChild.className = 'cookie-data';
this.sizeChild = this.ownerDocument.createElement('div');
this.sizeChild.className = 'cookie-size';
this.itemsChild = this.ownerDocument.createElement('div');
this.itemsChild.className = 'cookie-items';
this.infoChild = this.ownerDocument.createElement('div');
this.infoChild.className = 'cookie-details';
this.infoChild.hidden = true;
var remove = this.ownerDocument.createElement('button');
remove.textContent = loadTimeData.getString('remove_cookie');
remove.onclick = this.removeCookie_.bind(this);
this.infoChild.appendChild(remove);
var content = this.contentElement;
content.appendChild(this.siteChild);
content.appendChild(this.dataChild);
content.appendChild(this.sizeChild);
content.appendChild(this.itemsChild);
this.itemsChild.appendChild(this.infoChild);
if (this.origin && this.origin.data) {
this.siteChild.textContent = this.origin.data.title;
this.siteChild.setAttribute('title', this.origin.data.title);
}
this.itemList_ = [];
},
/** @type {boolean} */
get expanded() {
return this.expanded_;
},
set expanded(expanded) {
if (this.expanded_ == expanded)
return;
this.expanded_ = expanded;
if (expanded) {
this.classList.add('show-items');
var oldExpanded = this.list.expandedItem;
this.list.expandedItem = this;
this.updateItems_();
if (oldExpanded)
oldExpanded.expanded = false;
} else {
if (this.list.expandedItem == this) {
this.list.expandedItem = null;
}
this.style.height = '';
this.itemsChild.style.height = '';
this.classList.remove('show-items');
}
},
/**
* The callback for the "remove" button shown when an item is selected.
* Requests that the currently selected cookie be removed.
* @private
*/
removeCookie_: function() {
if (this.selectedIndex_ >= 0) {
var item = this.itemList_[this.selectedIndex_];
if (item && item.node)
chrome.send('removeCookie', [item.node.pathId]);
}
},
/**
* Disable animation within this cookie list item, in preparation for making
* changes that will need to be animated. Makes it possible to measure the
* contents without displaying them, to set animation targets.
* @private
*/
disableAnimation_: function() {
this.itemsHeight_ = getItemHeight(this.itemsChild);
this.classList.add('measure-items');
},
/**
* Enable animation after changing the contents of this cookie list item.
* See @{code disableAnimation_}.
* @private
*/
enableAnimation_: function() {
if (!this.classList.contains('measure-items'))
this.disableAnimation_();
this.itemsChild.style.height = '';
// This will force relayout in order to calculate the new heights.
var itemsHeight = getItemHeight(this.itemsChild);
var fixedHeight = getItemHeight(this) + itemsHeight - this.itemsHeight_;
this.itemsChild.style.height = this.itemsHeight_ + 'px';
// Force relayout before enabling animation, so that if we have
// changed things since the last layout, they will not be animated
// during subsequent layouts.
/** @suppress {suspiciousCode} */
this.itemsChild.offsetHeight;
this.classList.remove('measure-items');
this.itemsChild.style.height = itemsHeight + 'px';
this.style.height = fixedHeight + 'px';
},
/**
* Updates the origin summary to reflect changes in its items.
* Both CookieListItem and CookieTreeNode implement this API.
* This implementation scans the descendants to update the text.
*/
updateOrigin: function() {
var info = {
cookies: 0,
database: false,
localStorage: false,
appCache: false,
indexedDb: false,
fileSystem: false,
channelIDs: 0,
serviceWorker: false,
cacheStorage: false,
};
if (this.origin)
this.origin.collectSummaryInfo(info);
var list = [];
if (info.cookies > 1)
list.push(loadTimeData.getStringF('cookie_plural', info.cookies));
else if (info.cookies > 0)
list.push(loadTimeData.getString('cookie_singular'));
if (info.database || info.indexedDb)
list.push(loadTimeData.getString('cookie_database_storage'));
if (info.localStorage)
list.push(loadTimeData.getString('cookie_local_storage'));
if (info.appCache)
list.push(loadTimeData.getString('cookie_app_cache'));
if (info.fileSystem)
list.push(loadTimeData.getString('cookie_file_system'));
if (info.channelIDs)
list.push(loadTimeData.getString('cookie_channel_id'));
if (info.serviceWorker)
list.push(loadTimeData.getString('cookie_service_worker'));
if (info.cacheStorage)
list.push(loadTimeData.getString('cookie_cache_storage'));
if (info.flashLSO)
list.push(loadTimeData.getString('cookie_flash_lso'));
var text = '';
for (var i = 0; i < list.length; ++i) {
if (text.length > 0)
text += ', ' + list[i];
else
text = list[i];
}
this.dataChild.textContent = text;
var apps = info.appsProtectingThis;
for (var key in apps) {
addAppInfo(this.dataChild, apps[key]);
}
if (info.quota && info.quota.totalUsage)
this.sizeChild.textContent = info.quota.totalUsage;
if (this.expanded)
this.updateItems_();
},
/**
* Updates the items section to reflect changes, animating to the new state.
* Removes existing contents and calls @{code CookieTreeNode.createItems}.
* @private
*/
updateItems_: function() {
this.disableAnimation_();
this.itemsChild.textContent = '';
this.infoChild.hidden = true;
this.selectedIndex_ = -1;
this.itemList_ = [];
if (this.origin)
this.origin.createItems(this);
this.itemsChild.appendChild(this.infoChild);
this.enableAnimation_();
},
/**
* Append a new cookie node "bubble" to this list item.
* @param {options.CookieTreeNode} node The cookie node to add a bubble for.
* @param {Element} div The DOM element for the bubble itself.
* @return {number} The index the bubble was added at.
*/
appendItem: function(node, div) {
this.itemList_.push({node: node, div: div});
this.itemsChild.appendChild(div);
return this.itemList_.length - 1;
},
/**
* The currently selected cookie node ("cookie bubble") index.
* @type {number}
* @private
*/
selectedIndex_: -1,
/**
* Get the currently selected cookie node ("cookie bubble") index.
* @type {number}
*/
get selectedIndex() {
return this.selectedIndex_;
},
/**
* Set the currently selected cookie node ("cookie bubble") index to
* |itemIndex|, unselecting any previously selected node first.
* @param {number} itemIndex The index to set as the selected index.
*/
set selectedIndex(itemIndex) {
// Get the list index up front before we change anything.
var index = this.list.getIndexOfListItem(this);
// Unselect any previously selected item.
if (this.selectedIndex_ >= 0) {
var item = this.itemList_[this.selectedIndex_];
if (item && item.div)
item.div.removeAttribute('selected');
}
// Special case: decrementing -1 wraps around to the end of the list.
if (itemIndex == -2)
itemIndex = this.itemList_.length - 1;
// Check if we're going out of bounds and hide the item details.
if (itemIndex < 0 || itemIndex >= this.itemList_.length) {
this.selectedIndex_ = -1;
this.disableAnimation_();
this.infoChild.hidden = true;
this.enableAnimation_();
return;
}
// Set the new selected item and show the item details for it.
this.selectedIndex_ = itemIndex;
this.itemList_[itemIndex].div.setAttribute('selected', '');
this.disableAnimation_();
this.itemList_[itemIndex].node.setDetailText(this.infoChild,
this.list.infoNodes);
this.infoChild.hidden = false;
this.enableAnimation_();
// If we're near the bottom of the list this may cause the list item to go
// beyond the end of the visible area. Fix it after the animation is done.
var list = this.list;
window.setTimeout(function() { list.scrollIndexIntoView(index); }, 150);
},
};
/**
* {@code CookieTreeNode}s mirror the structure of the cookie tree lazily, and
* contain all the actual data used to generate the {@code CookieListItem}s.
* @param {Object} data The data object for this node.
* @constructor
*/
function CookieTreeNode(data) {
this.data = data;
this.children = [];
}
CookieTreeNode.prototype = {
/**
* Insert the given list of cookie tree nodes at the given index.
* Both CookiesList and CookieTreeNode implement this API.
* @param {Array<Object>} data The data objects for the nodes to add.
* @param {number} start The index at which to start inserting the nodes.
*/
insertAt: function(data, start) {
var nodes = spliceTreeNodes(data, start, this.children);
for (var i = 0; i < nodes.length; i++)
nodes[i].parent = this;
this.updateOrigin();
},
/**
* Remove a cookie tree node from the given index.
* Both CookiesList and CookieTreeNode implement this API.
*
* TODO(dbeam): this method now conflicts with HTMLElement#remove(), which
* is why the param is optional. Rename.
*
* @param {number=} index The index of the tree node to remove.
*/
remove: function(index) {
if (index < this.children.length) {
this.children.splice(index, 1);
this.updateOrigin();
}
},
/**
* Clears all children.
* Both CookiesList and CookieTreeNode implement this API.
* It is used by CookiesList.loadChildren().
*/
clear: function() {
// We might leave some garbage in parentLookup for removed children.
// But that should be OK because parentLookup is cleared when we
// reload the tree.
this.children = [];
this.updateOrigin();
},
/**
* The counter used by startBatchUpdates() and endBatchUpdates().
* @type {number}
*/
batchCount_: 0,
/**
* See cr.ui.List.startBatchUpdates().
* Both CookiesList (via List) and CookieTreeNode implement this API.
*/
startBatchUpdates: function() {
this.batchCount_++;
},
/**
* See cr.ui.List.endBatchUpdates().
* Both CookiesList (via List) and CookieTreeNode implement this API.
*/
endBatchUpdates: function() {
if (!--this.batchCount_)
this.updateOrigin();
},
/**
* Requests updating the origin summary to reflect changes in this item.
* Both CookieListItem and CookieTreeNode implement this API.
*/
updateOrigin: function() {
if (!this.batchCount_ && this.parent)
this.parent.updateOrigin();
},
/**
* Summarize the information in this node and update @{code info}.
* This will recurse into child nodes to summarize all descendants.
* @param {Object} info The info object from @{code updateOrigin}.
*/
collectSummaryInfo: function(info) {
if (this.children.length > 0) {
for (var i = 0; i < this.children.length; ++i)
this.children[i].collectSummaryInfo(info);
} else if (this.data && !this.data.hasChildren) {
if (this.data.type == 'cookie') {
info.cookies++;
} else if (this.data.type == 'database') {
info.database = true;
} else if (this.data.type == 'local_storage') {
info.localStorage = true;
} else if (this.data.type == 'app_cache') {
info.appCache = true;
} else if (this.data.type == 'indexed_db') {
info.indexedDb = true;
} else if (this.data.type == 'file_system') {
info.fileSystem = true;
} else if (this.data.type == 'quota') {
info.quota = this.data;
} else if (this.data.type == 'channel_id') {
info.channelIDs++;
} else if (this.data.type == 'service_worker') {
info.serviceWorker = true;
} else if (this.data.type == 'cache_storage') {
info.cacheStorage = true;
} else if (this.data.type == 'flash_lso') {
info.flashLSO = true;
}
var apps = this.data.appsProtectingThis;
if (apps) {
if (!info.appsProtectingThis)
info.appsProtectingThis = {};
apps.forEach(function(appInfo) {
info.appsProtectingThis[appInfo.id] = appInfo;
});
}
}
},
/**
* Create the cookie "bubbles" for this node, recursing into children
* if there are any. Append the cookie bubbles to @{code item}.
* @param {options.CookieListItem} item The cookie list item to create items
* in.
*/
createItems: function(item) {
if (this.children.length > 0) {
for (var i = 0; i < this.children.length; ++i)
this.children[i].createItems(item);
return;
}
if (!this.data || this.data.hasChildren)
return;
var text = '';
switch (this.data.type) {
case 'cookie':
case 'database':
text = this.data.name;
break;
default:
text = loadTimeData.getString('cookie_' + this.data.type);
}
if (!text)
return;
var div = item.ownerDocument.createElement('div');
div.className = 'cookie-item';
// Help out screen readers and such: this is a clickable thing.
div.setAttribute('role', 'button');
div.textContent = text;
var apps = this.data.appsProtectingThis;
if (apps)
apps.forEach(addAppInfo.bind(null, div));
var index = item.appendItem(this, div);
div.onclick = function() {
item.selectedIndex = (item.selectedIndex == index) ? -1 : index;
};
},
/**
* Set the detail text to be displayed to that of this cookie tree node.
* Uses preallocated DOM elements for each cookie node type from @{code
* infoNodes}, and inserts the appropriate elements to @{code element}.
* @param {Element} element The DOM element to insert elements to.
* @param {Object<{table: Element, info: Object<Element>}>} infoNodes The
* map from cookie node types to maps from cookie attribute names to DOM
* elements to display cookie attribute values, created by
* @see {CookiesList.decorate}.
*/
setDetailText: function(element, infoNodes) {
var table;
if (this.data && !this.data.hasChildren && cookieInfo[this.data.type]) {
var info = cookieInfo[this.data.type];
var nodes = infoNodes[this.data.type].info;
for (var i = 0; i < info.length; ++i) {
var name = info[i][0];
if (name != 'id' && this.data[name])
nodes[name].textContent = this.data[name];
else
nodes[name].textContent = '';
}
table = infoNodes[this.data.type].table;
}
while (element.childNodes.length > 1)
element.removeChild(element.firstChild);
if (table)
element.insertBefore(table, element.firstChild);
},
/**
* The parent of this cookie tree node.
* @type {?(options.CookieTreeNode|options.CookieListItem)}
*/
get parent() {
// See below for an explanation of this special case.
if (typeof this.parent_ == 'number')
return this.list_.getListItemByIndex(this.parent_);
return this.parent_;
},
set parent(parent) {
if (parent == this.parent)
return;
if (parent instanceof CookieListItem) {
// If the parent is to be a CookieListItem, then we keep the reference
// to it by its containing list and list index, rather than directly.
// This allows the list items to be garbage collected when they scroll
// out of view (except the expanded item, which we cache). This is
// transparent except in the setter and getter, where we handle it.
if (this.parent_ == undefined || parent.listIndex != -1) {
// Setting the parent is somewhat tricky because the CookieListItem
// constructor has side-effects on the |origin| that it wraps. Every
// time a CookieListItem is created for an |origin|, it registers
// itself as the parent of the |origin|.
// The List implementation may create a temporary CookieListItem item
// that wraps the |origin| of the very first entry of the CokiesList,
// when the List is redrawn the first time. This temporary
// CookieListItem is fresh (has listIndex = -1) and is never inserted
// into the List. Therefore it gets never updated. This destroys the
// chain of parent pointers.
// This is the stack trace:
// CookieListItem
// CookiesList.createItem
// List.measureItem
// List.getDefaultItemSize_
// List.getDefaultItemHeight_
// List.getIndexForListOffset_
// List.getItemsInViewPort
// List.redraw
// List.endBatchUpdates
// CookiesList.loadChildren
this.parent_ = parent.listIndex;
}
this.list_ = parent.list;
parent.addEventListener('listIndexChange',
this.parentIndexChanged_.bind(this));
} else {
this.parent_ = parent;
}
if (this.data && this.data.id) {
if (parent)
parentLookup[this.data.id] = this;
else
delete parentLookup[this.data.id];
}
if (this.data && this.data.hasChildren &&
!this.children.length && !lookupRequests[this.data.id]) {
lookupRequests[this.data.id] = true;
chrome.send('loadCookie', [this.pathId]);
}
},
/**
* Called when the parent is a CookieListItem whose index has changed.
* See the code above that avoids keeping a direct reference to
* CookieListItem parents, to allow them to be garbage collected.
* @private
*/
parentIndexChanged_: function(event) {
if (typeof this.parent_ == 'number') {
this.parent_ = event.newValue;
// We set a timeout to update the origin, rather than doing it right
// away, because this callback may occur while the list items are
// being repopulated following a scroll event. Calling updateOrigin()
// immediately could trigger relayout that would reset the scroll
// position within the list, among other things.
window.setTimeout(this.updateOrigin.bind(this), 0);
}
},
/**
* The cookie tree path id.
* @type {string}
*/
get pathId() {
var parent = this.parent;
if (parent && parent instanceof CookieTreeNode)
return parent.pathId + ',' + this.data.id;
return this.data.id;
},
};
/**
* Creates a new cookies list.
* @param {Object=} opt_propertyBag Optional properties.
* @constructor
* @extends {options.DeletableItemList}
*/
var CookiesList = cr.ui.define('list');
CookiesList.prototype = {
__proto__: DeletableItemList.prototype,
/** @override */
decorate: function() {
DeletableItemList.prototype.decorate.call(this);
this.classList.add('cookie-list');
this.dataModel = new ArrayDataModel([]);
this.addEventListener('keydown', this.handleKeyLeftRight_.bind(this));
var sm = new ListSingleSelectionModel();
sm.addEventListener('change', this.cookieSelectionChange_.bind(this));
sm.addEventListener('leadIndexChange', this.cookieLeadChange_.bind(this));
this.selectionModel = sm;
this.infoNodes = {};
this.fixedHeight = false;
var doc = this.ownerDocument;
// Create a table for each type of site data (e.g. cookies, databases,
// etc.) and save it so that we can reuse it for all origins.
for (var type in cookieInfo) {
var table = doc.createElement('table');
table.className = 'cookie-details-table';
var tbody = doc.createElement('tbody');
table.appendChild(tbody);
var info = {};
for (var i = 0; i < cookieInfo[type].length; i++) {
var tr = doc.createElement('tr');
var name = doc.createElement('td');
var data = doc.createElement('td');
var pair = cookieInfo[type][i];
name.className = 'cookie-details-label';
name.textContent = loadTimeData.getString(pair[1]);
data.className = 'cookie-details-value';
data.textContent = '';
tr.appendChild(name);
tr.appendChild(data);
tbody.appendChild(tr);
info[pair[0]] = data;
}
this.infoNodes[type] = {table: table, info: info};
}
},
/**
* Handles key down events and looks for left and right arrows, then
* dispatches to the currently expanded item, if any.
* @param {Event} e The keydown event.
* @private
*/
handleKeyLeftRight_: function(e) {
var id = e.key;
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)
return;
if ((id == 'ArrowLeft' || id == 'ArrowRight') && this.expandedItem) {
var cs = this.ownerDocument.defaultView.getComputedStyle(this);
var rtl = cs.direction == 'rtl';
if ((!rtl && id == 'ArrowLeft') || (rtl && id == 'ArrowRight'))
this.expandedItem.selectedIndex--;
else
this.expandedItem.selectedIndex++;
this.scrollIndexIntoView(this.expandedItem.listIndex);
// Prevent the page itself from scrolling.
e.preventDefault();
}
},
/**
* Called on selection model selection changes.
* @param {Event} ce The selection change event.
* @private
*/
cookieSelectionChange_: function(ce) {
ce.changes.forEach(function(change) {
var listItem = this.getListItemByIndex(change.index);
if (listItem) {
if (!change.selected) {
// We set a timeout here, rather than setting the item unexpanded
// immediately, so that if another item gets set expanded right
// away, it will be expanded before this item is unexpanded. It
// will notice that, and unexpand this item in sync with its own
// expansion. Later, this callback will end up having no effect.
window.setTimeout(function() {
if (!listItem.selected || !listItem.lead)
listItem.expanded = false;
}, 0);
} else if (listItem.lead) {
listItem.expanded = true;
}
}
}, this);
},
/**
* Called on selection model lead changes.
* @param {Event} pe The lead change event.
* @private
*/
cookieLeadChange_: function(pe) {
if (pe.oldValue != -1) {
var listItem = this.getListItemByIndex(pe.oldValue);
if (listItem) {
// See cookieSelectionChange_ above for why we use a timeout here.
window.setTimeout(function() {
if (!listItem.lead || !listItem.selected)
listItem.expanded = false;
}, 0);
}
}
if (pe.newValue != -1) {
var listItem = this.getListItemByIndex(pe.newValue);
if (listItem && listItem.selected)
listItem.expanded = true;
}
},
/**
* The currently expanded item. Used by CookieListItem above.
* @type {?options.CookieListItem}
*/
expandedItem: null,
// from cr.ui.List
/**
* @override
* @param {Object} data
*/
createItem: function(data) {
// We use the cached expanded item in order to allow it to maintain some
// state (like its fixed height, and which bubble is selected).
if (this.expandedItem && this.expandedItem.origin == data)
return this.expandedItem;
return new CookieListItem(data, this);
},
// from options.DeletableItemList
/** @override */
deleteItemAtIndex: function(index) {
var item = this.dataModel.item(index);
if (item) {
var pathId = item.pathId;
if (pathId)
chrome.send('removeCookie', [pathId]);
}
},
/**
* Insert the given list of cookie tree nodes at the given index.
* Both CookiesList and CookieTreeNode implement this API.
* @param {Array<Object>} data The data objects for the nodes to add.
* @param {number} start The index at which to start inserting the nodes.
*/
insertAt: function(data, start) {
spliceTreeNodes(data, start, this.dataModel);
},
/**
* Remove a cookie tree node from the given index.
* Both CookiesList and CookieTreeNode implement this API.
*
* TODO(dbeam): this method now conflicts with HTMLElement#remove(), which
* is why the param is optional. Rename.
*
* @param {number=} index The index of the tree node to remove.
*/
remove: function(index) {
if (index < this.dataModel.length)
this.dataModel.splice(index, 1);
},
/**
* Clears the list.
* Both CookiesList and CookieTreeNode implement this API.
* It is used by CookiesList.loadChildren().
*/
clear: function() {
parentLookup = {};
this.dataModel.splice(0, this.dataModel.length);
this.redraw();
},
/**
* Add tree nodes by given parent.
* @param {Object} parent The parent node.
* @param {number} start The index at which to start inserting the nodes.
* @param {Array} nodesData Nodes data array.
* @private
*/
addByParent_: function(parent, start, nodesData) {
if (!parent)
return;
parent.startBatchUpdates();
parent.insertAt(nodesData, start);
parent.endBatchUpdates();
cr.dispatchSimpleEvent(this, 'change');
},
/**
* Add tree nodes by parent id.
* This is used by cookies_view.js.
* @param {string} parentId Id of the parent node.
* @param {number} start The index at which to start inserting the nodes.
* @param {Array} nodesData Nodes data array.
*/
addByParentId: function(parentId, start, nodesData) {
var parent = parentId ? parentLookup[parentId] : this;
this.addByParent_(parent, start, nodesData);
},
/**
* Removes tree nodes by parent id.
* This is used by cookies_view.js.
* @param {string} parentId Id of the parent node.
* @param {number} start The index at which to start removing the nodes.
* @param {number} count Number of nodes to remove.
*/
removeByParentId: function(parentId, start, count) {
var parent = parentId ? parentLookup[parentId] : this;
if (!parent)
return;
parent.startBatchUpdates();
while (count-- > 0)
parent.remove(start);
parent.endBatchUpdates();
cr.dispatchSimpleEvent(this, 'change');
},
/**
* Loads the immediate children of given parent node.
* This is used by cookies_view.js.
* @param {string} parentId Id of the parent node.
* @param {Array} children The immediate children of parent node.
*/
loadChildren: function(parentId, children) {
if (parentId)
delete lookupRequests[parentId];
var parent = parentId ? parentLookup[parentId] : this;
if (!parent)
return;
parent.startBatchUpdates();
parent.clear();
this.addByParent_(parent, 0, children);
parent.endBatchUpdates();
},
};
return {
CookiesList: CookiesList,
CookieListItem: CookieListItem,
CookieTreeNode: CookieTreeNode,
};
});