| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 // require: cr.js | 5 // require: cr.js |
| 6 // require: cr/ui.js | 6 // require: cr/ui.js |
| 7 // require: cr/ui/tree.js | 7 // require: cr/ui/tree.js |
| 8 | 8 |
| 9 cr.define('chrome.sync', function() { | 9 (function() { |
| 10 /** | 10 /** |
| 11 * Gets all children of the given node and passes it to the given | 11 * A helper function to determine if a node is the root of its type. |
| 12 * callback. | 12 * |
| 13 * @param {string} id The id whose children we want. | 13 * @param {!Object} node The node to check. |
| 14 * @param {function(Array.<!Object>)} callback The callback to call | |
| 15 * with the list of children summaries. | |
| 16 */ | 14 */ |
| 17 function getSyncNodeChildrenSummaries(id, callback) { | 15 var isTypeRootNode = function(node) { |
| 18 var timer = chrome.sync.makeTimer(); | 16 return node.PARENT_ID == 'r' && node.UNIQUE_SERVER_TAG != ''; |
| 19 chrome.sync.getChildNodeIds(id, function(childNodeIds) { | 17 } |
| 20 console.debug('getChildNodeIds took ' + | 18 |
| 21 timer.elapsedSeconds + 's to retrieve ' + | 19 /** |
| 22 childNodeIds.length + ' ids'); | 20 * A helper function to determine if a node is a child of the given parent. |
| 23 timer = chrome.sync.makeTimer(); | 21 * |
| 24 chrome.sync.getNodeSummariesById( | 22 * @param {string} parentId The ID of the parent. |
| 25 childNodeIds, function(childrenSummaries) { | 23 * @param {!Object} node The node to check. |
| 26 console.debug('getNodeSummariesById took ' + | 24 */ |
| 27 timer.elapsedSeconds + 's to retrieve summaries for ' + | 25 var isChildOf = function(parentId, node) { |
| 28 childrenSummaries.length + ' nodes'); | 26 return node.PARENT_ID == parentId; |
| 29 callback(childrenSummaries); | 27 } |
| 30 }); | 28 |
| 31 }); | 29 /** |
| 30 * A helper function to sort sync nodes. |
| 31 * |
| 32 * Sorts by position index if possible, falls back to sorting by name, and |
| 33 * finally sorting by METAHANDLE. |
| 34 * |
| 35 * If this proves to be slow and expensive, we should experiment with moving |
| 36 * this functionality to C++ instead. |
| 37 */ |
| 38 var nodeComparator = function(nodeA, nodeB) { |
| 39 if (nodeA.hasOwnProperty('positionIndex') && |
| 40 nodeB.hasOwnProperty('positionIndex')) { |
| 41 return nodeA.positionIndex - nodeB.positionIndex; |
| 42 } else if (nodeA.NON_UNIQUE_NAME != nodeB.NON_UNIQUE_NAME) { |
| 43 return nodeA.NON_UNIQUE_NAME.localeCompare(nodeB.NON_UNIQUE_NAME); |
| 44 } else { |
| 45 return nodeA.METAHANDLE - nodeB.METAHANDLE; |
| 46 } |
| 47 } |
| 48 |
| 49 /** |
| 50 * Updates the node detail view with the details for the given node. |
| 51 * @param {!Object} node The struct representing the node we want to display. |
| 52 */ |
| 53 function updateNodeDetailView(node) { |
| 54 var nodeDetailsView = $('node-details'); |
| 55 nodeDetailsView.hidden = false; |
| 56 jstProcess(new JsEvalContext(node.entry_), nodeDetailsView); |
| 57 } |
| 58 |
| 59 /** |
| 60 * Updates the 'Last refresh time' display. |
| 61 * @param {string} The text to display. |
| 62 */ |
| 63 function setLastRefreshTime(str) { |
| 64 $('node-browser-refresh-time').textContent = str; |
| 32 } | 65 } |
| 33 | 66 |
| 34 /** | 67 /** |
| 35 * Creates a new sync node tree item. | 68 * Creates a new sync node tree item. |
| 36 * @param {{id: string, title: string, isFolder: boolean}} | 69 * |
| 37 * nodeSummary The nodeSummary object for the node (as returned | |
| 38 * by chrome.sync.getNodeSummariesById()). | |
| 39 * @constructor | 70 * @constructor |
| 71 * @param {!Object} node The nodeDetails object for the node as returned by |
| 72 * chrome.sync.getAllNodes(). |
| 40 * @extends {cr.ui.TreeItem} | 73 * @extends {cr.ui.TreeItem} |
| 41 */ | 74 */ |
| 42 var SyncNodeTreeItem = function(nodeSummary) { | 75 var SyncNodeTreeItem = function(node) { |
| 43 var treeItem = new cr.ui.TreeItem({ | 76 var treeItem = new cr.ui.TreeItem(); |
| 44 id_: nodeSummary.id | |
| 45 }); | |
| 46 treeItem.__proto__ = SyncNodeTreeItem.prototype; | 77 treeItem.__proto__ = SyncNodeTreeItem.prototype; |
| 47 | 78 |
| 48 treeItem.label = nodeSummary.title; | 79 treeItem.entry_ = node; |
| 49 if (nodeSummary.isFolder) { | 80 treeItem.label = node.NON_UNIQUE_NAME; |
| 81 if (node.IS_DIR) { |
| 50 treeItem.mayHaveChildren_ = true; | 82 treeItem.mayHaveChildren_ = true; |
| 51 | 83 |
| 52 // Load children asynchronously on expand. | 84 // Load children on expand. |
| 53 // TODO(akalin): Add a throbber while loading? | 85 treeItem.expanded_ = false; |
| 54 treeItem.triggeredLoad_ = false; | |
| 55 treeItem.addEventListener('expand', | 86 treeItem.addEventListener('expand', |
| 56 treeItem.handleExpand_.bind(treeItem)); | 87 treeItem.handleExpand_.bind(treeItem)); |
| 57 } else { | 88 } else { |
| 58 treeItem.classList.add('leaf'); | 89 treeItem.classList.add('leaf'); |
| 59 } | 90 } |
| 60 return treeItem; | 91 return treeItem; |
| 61 }; | 92 }; |
| 62 | 93 |
| 63 SyncNodeTreeItem.prototype = { | 94 SyncNodeTreeItem.prototype = { |
| 64 __proto__: cr.ui.TreeItem.prototype, | 95 __proto__: cr.ui.TreeItem.prototype, |
| 65 | 96 |
| 66 /** | 97 /** |
| 67 * Retrieves the details for this node. | 98 * Finds the children of this node and appends them to the tree. |
| 68 * @param {function(Object)} callback The callback that will be | |
| 69 * called with the node details, or null if it could not be | |
| 70 * retrieved. | |
| 71 */ | 99 */ |
| 72 getDetails: function(callback) { | 100 handleExpand_: function(event) { |
| 73 chrome.sync.getNodeDetailsById([this.id_], function(nodeDetails) { | 101 var treeItem = this; |
| 74 callback(nodeDetails[0] || null); | 102 |
| 103 if (treeItem.expanded_) { |
| 104 return; |
| 105 } |
| 106 treeItem.expanded_ = true; |
| 107 |
| 108 var children = treeItem.tree.allNodes.filter( |
| 109 isChildOf.bind(undefined, treeItem.entry_.ID)); |
| 110 children.sort(nodeComparator); |
| 111 |
| 112 children.forEach(function(node) { |
| 113 treeItem.add(new SyncNodeTreeItem(node)); |
| 75 }); | 114 }); |
| 76 }, | 115 }, |
| 77 | |
| 78 handleExpand_: function(event) { | |
| 79 if (!this.triggeredLoad_) { | |
| 80 getSyncNodeChildrenSummaries(this.id_, this.addChildNodes_.bind(this)); | |
| 81 this.triggeredLoad_ = true; | |
| 82 } | |
| 83 }, | |
| 84 | |
| 85 /** | |
| 86 * Adds children from the list of children summaries. | |
| 87 * @param {Array.<{id: string, title: string, isFolder: boolean}>} | |
| 88 * childrenSummaries The list of children summaries with which | |
| 89 * to create the child nodes. | |
| 90 */ | |
| 91 addChildNodes_: function(childrenSummaries) { | |
| 92 var timer = chrome.sync.makeTimer(); | |
| 93 for (var i = 0; i < childrenSummaries.length; ++i) { | |
| 94 var childTreeItem = new SyncNodeTreeItem(childrenSummaries[i]); | |
| 95 this.add(childTreeItem); | |
| 96 } | |
| 97 console.debug('adding ' + childrenSummaries.length + | |
| 98 ' children took ' + timer.elapsedSeconds + 's'); | |
| 99 } | |
| 100 }; | 116 }; |
| 101 | 117 |
| 102 /** | 118 /** |
| 103 * Updates the node detail view with the details for the given node. | 119 * Creates a new sync node tree. Technically, it's a forest since it each |
| 104 * @param {!Object} nodeDetails The details for the node we want | 120 * type has its own root node for its own tree, but it still looks and acts |
| 105 * to display. | 121 * mostly like a tree. |
| 106 */ | 122 * |
| 107 function updateNodeDetailView(nodeDetails) { | |
| 108 var nodeBrowser = document.getElementById('node-browser'); | |
| 109 // TODO(akalin): Write a nicer detail viewer. | |
| 110 nodeDetails.entry = JSON.stringify(nodeDetails.entry, null, 2); | |
| 111 jstProcess(new JsEvalContext(nodeDetails), nodeBrowser); | |
| 112 } | |
| 113 | |
| 114 /** | |
| 115 * Creates a new sync node tree. | |
| 116 * @param {Object=} opt_propertyBag Optional properties. | 123 * @param {Object=} opt_propertyBag Optional properties. |
| 117 * @constructor | 124 * @constructor |
| 118 * @extends {cr.ui.Tree} | 125 * @extends {cr.ui.Tree} |
| 119 */ | 126 */ |
| 120 var SyncNodeTree = cr.ui.define('tree'); | 127 var SyncNodeTree = cr.ui.define('tree'); |
| 121 | 128 |
| 122 SyncNodeTree.prototype = { | 129 SyncNodeTree.prototype = { |
| 123 __proto__: cr.ui.Tree.prototype, | 130 __proto__: cr.ui.Tree.prototype, |
| 124 | 131 |
| 125 decorate: function() { | 132 decorate: function() { |
| 126 cr.ui.Tree.prototype.decorate.call(this); | 133 cr.ui.Tree.prototype.decorate.call(this); |
| 127 this.addEventListener('change', this.handleChange_.bind(this)); | 134 this.addEventListener('change', this.handleChange_.bind(this)); |
| 128 chrome.sync.getRootNodeDetails(this.makeRoot_.bind(this)); | 135 this.allNodes = []; |
| 129 }, | 136 }, |
| 130 | 137 |
| 131 /** | 138 populate: function(nodes) { |
| 132 * Creates the root of the tree. | 139 var tree = this; |
| 133 * @param {{id: string, title: string, isFolder: boolean}} | 140 |
| 134 * rootNodeSummary The summary info for the root node. | 141 // We store the full set of nodes in the SyncNodeTree object. |
| 135 */ | 142 tree.allNodes = nodes; |
| 136 makeRoot_: function(rootNodeSummary) { | 143 |
| 137 // The root node usually doesn't have a title. | 144 var roots = tree.allNodes.filter(isTypeRootNode); |
| 138 rootNodeSummary.title = rootNodeSummary.title || 'Root'; | 145 roots.sort(nodeComparator); |
| 139 var rootTreeItem = new SyncNodeTreeItem(rootNodeSummary); | 146 |
| 140 this.add(rootTreeItem); | 147 roots.forEach(function(typeRoot) { |
| 148 tree.add(new SyncNodeTreeItem(typeRoot)); |
| 149 }); |
| 141 }, | 150 }, |
| 142 | 151 |
| 143 handleChange_: function(event) { | 152 handleChange_: function(event) { |
| 144 if (this.selectedItem) { | 153 if (this.selectedItem) { |
| 145 this.selectedItem.getDetails(updateNodeDetailView); | 154 updateNodeDetailView(this.selectedItem); |
| 146 } | 155 } |
| 147 } | 156 } |
| 148 }; | 157 }; |
| 149 | 158 |
| 150 function decorateSyncNodeBrowser(syncNodeBrowser) { | 159 /** |
| 151 cr.ui.decorate(syncNodeBrowser, SyncNodeTree); | 160 * Clears any existing UI state. Useful prior to a refresh. |
| 161 */ |
| 162 function clear() { |
| 163 var treeContainer = $('sync-node-tree-container'); |
| 164 while (treeContainer.firstChild) { |
| 165 treeContainer.removeChild(treeContainer.firstChild); |
| 166 } |
| 167 |
| 168 var nodeDetailsView = $('node-details'); |
| 169 nodeDetailsView.hidden = true; |
| 152 } | 170 } |
| 153 | 171 |
| 154 // This is needed because JsTemplate (which is needed by | 172 /** |
| 155 // updateNodeDetailView) is loaded at the end of the file after | 173 * Fetch the latest set of nodes and refresh the UI. |
| 156 // everything else. | 174 */ |
| 157 // | 175 function refresh() { |
| 158 // TODO(akalin): Remove dependency on JsTemplate and get rid of | 176 $('node-browser-refresh-button').disabled = true; |
| 159 // this. | 177 |
| 160 var domLoaded = false; | 178 clear(); |
| 161 var pendingSyncNodeBrowsers = []; | 179 setLastRefreshTime('In progress since ' + (new Date()).toLocaleString()); |
| 162 function decorateSyncNodeBrowserAfterDOMLoad(id) { | 180 |
| 163 var e = document.getElementById(id); | 181 chrome.sync.getAllNodes(function(nodes) { |
| 164 if (domLoaded) { | 182 var treeContainer = $('sync-node-tree-container'); |
| 165 decorateSyncNodeBrowser(e); | 183 var tree = document.createElement('tree'); |
| 166 } else { | 184 tree.setAttribute('id', 'sync-node-tree'); |
| 167 pendingSyncNodeBrowsers.push(e); | 185 tree.setAttribute('icon-visibility', 'parent'); |
| 168 } | 186 treeContainer.appendChild(tree); |
| 187 |
| 188 cr.ui.decorate(tree, SyncNodeTree); |
| 189 tree.populate(nodes); |
| 190 |
| 191 setLastRefreshTime((new Date()).toLocaleString()); |
| 192 $('node-browser-refresh-button').disabled = false; |
| 193 }); |
| 169 } | 194 } |
| 170 | 195 |
| 171 document.addEventListener('DOMContentLoaded', function() { | 196 document.addEventListener('DOMContentLoaded', function(e) { |
| 172 for (var i = 0; i < pendingSyncNodeBrowsers.length; ++i) { | 197 $('node-browser-refresh-button').addEventListener('click', refresh); |
| 173 decorateSyncNodeBrowser(pendingSyncNodeBrowsers[i]); | 198 cr.ui.decorate('#sync-node-splitter', cr.ui.Splitter); |
| 174 } | 199 |
| 175 domLoaded = true; | 200 // Automatically trigger a refresh the first time this tab is selected. |
| 201 $('sync-browser-tab').addEventListener('selectedChange', function f(e) { |
| 202 if (this.selected) { |
| 203 $('sync-browser-tab').removeEventListener('selectedChange', f); |
| 204 refresh(); |
| 205 } |
| 206 }); |
| 176 }); | 207 }); |
| 177 | 208 |
| 178 return { | 209 })(); |
| 179 decorateSyncNodeBrowser: decorateSyncNodeBrowserAfterDOMLoad | |
| 180 }; | |
| 181 }); | |
| OLD | NEW |