// Copyright (c) 2011 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. // require: cr.js // require: cr/ui.js // require: cr/ui/tree.js (function() { /** * A helper function to determine if a node is the root of its type. * * @param {!Object} node The node to check. */ var isTypeRootNode = function(node) { return node.PARENT_ID == 'r' && node.UNIQUE_SERVER_TAG != ''; } /** * A helper function to determine if a node is a child of the given parent. * * @param {string} parentId The ID of the parent. * @param {!Object} node The node to check. */ var isChildOf = function(parentId, node) { return node.PARENT_ID == parentId; } /** * A helper function to sort sync nodes. * * Sorts by position index if possible, falls back to sorting by name, and * finally sorting by METAHANDLE. * * If this proves to be slow and expensive, we should experiment with moving * this functionality to C++ instead. */ var nodeComparator = function(nodeA, nodeB) { if (nodeA.hasOwnProperty('positionIndex') && nodeB.hasOwnProperty('positionIndex')) { return nodeA.positionIndex - nodeB.positionIndex; } else if (nodeA.NON_UNIQUE_NAME != nodeB.NON_UNIQUE_NAME) { return nodeA.NON_UNIQUE_NAME.localeCompare(nodeB.NON_UNIQUE_NAME); } else { return nodeA.METAHANDLE - nodeB.METAHANDLE; } } /** * Updates the node detail view with the details for the given node. * @param {!Object} node The struct representing the node we want to display. */ function updateNodeDetailView(node) { var nodeDetailsView = $('node-details'); nodeDetailsView.hidden = false; jstProcess(new JsEvalContext(node.entry_), nodeDetailsView); } /** * Updates the 'Last refresh time' display. * @param {string} The text to display. */ function setLastRefreshTime(str) { $('node-browser-refresh-time').textContent = str; } /** * Creates a new sync node tree item. * * @constructor * @param {!Object} node The nodeDetails object for the node as returned by * chrome.sync.getAllNodes(). * @extends {cr.ui.TreeItem} */ var SyncNodeTreeItem = function(node) { var treeItem = new cr.ui.TreeItem(); treeItem.__proto__ = SyncNodeTreeItem.prototype; treeItem.entry_ = node; treeItem.label = node.NON_UNIQUE_NAME; if (node.IS_DIR) { treeItem.mayHaveChildren_ = true; // Load children on expand. treeItem.expanded_ = false; treeItem.addEventListener('expand', treeItem.handleExpand_.bind(treeItem)); } else { treeItem.classList.add('leaf'); } return treeItem; }; SyncNodeTreeItem.prototype = { __proto__: cr.ui.TreeItem.prototype, /** * Finds the children of this node and appends them to the tree. */ handleExpand_: function(event) { var treeItem = this; if (treeItem.expanded_) { return; } treeItem.expanded_ = true; var children = treeItem.tree.allNodes.filter( isChildOf.bind(undefined, treeItem.entry_.ID)); children.sort(nodeComparator); children.forEach(function(node) { treeItem.add(new SyncNodeTreeItem(node)); }); }, }; /** * Creates a new sync node tree. Technically, it's a forest since it each * type has its own root node for its own tree, but it still looks and acts * mostly like a tree. * * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {cr.ui.Tree} */ var SyncNodeTree = cr.ui.define('tree'); SyncNodeTree.prototype = { __proto__: cr.ui.Tree.prototype, decorate: function() { cr.ui.Tree.prototype.decorate.call(this); this.addEventListener('change', this.handleChange_.bind(this)); this.allNodes = []; }, populate: function(nodes) { var tree = this; // We store the full set of nodes in the SyncNodeTree object. tree.allNodes = nodes; var roots = tree.allNodes.filter(isTypeRootNode); roots.sort(nodeComparator); roots.forEach(function(typeRoot) { tree.add(new SyncNodeTreeItem(typeRoot)); }); }, handleChange_: function(event) { if (this.selectedItem) { updateNodeDetailView(this.selectedItem); } } }; /** * Clears any existing UI state. Useful prior to a refresh. */ function clear() { var treeContainer = $('sync-node-tree-container'); while (treeContainer.firstChild) { treeContainer.removeChild(treeContainer.firstChild); } var nodeDetailsView = $('node-details'); nodeDetailsView.hidden = true; } /** * Fetch the latest set of nodes and refresh the UI. */ function refresh() { $('node-browser-refresh-button').disabled = true; clear(); setLastRefreshTime('In progress since ' + (new Date()).toLocaleString()); chrome.sync.getAllNodes(function(nodes) { var treeContainer = $('sync-node-tree-container'); var tree = document.createElement('tree'); tree.setAttribute('id', 'sync-node-tree'); tree.setAttribute('icon-visibility', 'parent'); treeContainer.appendChild(tree); cr.ui.decorate(tree, SyncNodeTree); tree.populate(nodes); setLastRefreshTime((new Date()).toLocaleString()); $('node-browser-refresh-button').disabled = false; }); } document.addEventListener('DOMContentLoaded', function(e) { $('node-browser-refresh-button').addEventListener('click', refresh); cr.ui.decorate('#sync-node-splitter', cr.ui.Splitter); // Automatically trigger a refresh the first time this tab is selected. $('sync-browser-tab').addEventListener('selectedChange', function f(e) { if (this.selected) { $('sync-browser-tab').removeEventListener('selectedChange', f); refresh(); } }); }); })();