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 |