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 | |
Nicolas Zea
2014/01/13 20:10:14
Can the sorting not be done in the C++ code by hav
rlarocque
2014/01/13 21:56:17
That could work. I can see two reasons in favor o
Nicolas Zea
2014/01/14 22:14:33
Maybe just add a note that we can consider sorting
rlarocque
2014/01/14 22:20:43
Done.
| |
34 */ | |
35 var nodeComparator = function(nodeA, nodeB) { | |
36 if (nodeA.hasOwnProperty('positionIndex') && | |
37 nodeB.hasOwnProperty('positionIndex')) { | |
38 return nodeA.positionIndex - nodeB.positionIndex; | |
39 } else if (nodeA.NON_UNIQUE_NAME != nodeB.NON_UNIQUE_NAME) { | |
40 return nodeA.NON_UNIQUE_NAME.localeCompare(nodeB.NON_UNIQUE_NAME); | |
41 } else { | |
42 return nodeA.METAHANDLE - nodeB.METAHANDLE; | |
43 } | |
44 } | |
45 | |
46 /** | |
47 * Updates the node detail view with the details for the given node. | |
48 * @param {!Object} node The struct representing the node we want to display. | |
49 */ | |
50 function updateNodeDetailView(node) { | |
51 var nodeDetailsView = $('node-details'); | |
52 nodeDetailsView.hidden = false; | |
53 jstProcess(new JsEvalContext(node.entry_), nodeDetailsView); | |
54 } | |
55 | |
56 /** | |
57 * Updates the 'Last refresh time' display. | |
58 * @param {string} The text to display. | |
59 */ | |
60 function setLastRefreshTime(str) { | |
61 $('node-browser-refresh-time').textContent = str; | |
32 } | 62 } |
33 | 63 |
34 /** | 64 /** |
35 * Creates a new sync node tree item. | 65 * Creates a new sync node tree item. |
36 * @param {{id: string, title: string, isFolder: boolean}} | 66 * |
37 * nodeSummary The nodeSummary object for the node (as returned | |
38 * by chrome.sync.getNodeSummariesById()). | |
39 * @constructor | 67 * @constructor |
68 * @param {!Object} node The nodeDetails object for the node as returned by | |
69 * chrome.sync.getAllNodes(). | |
40 * @extends {cr.ui.TreeItem} | 70 * @extends {cr.ui.TreeItem} |
41 */ | 71 */ |
42 var SyncNodeTreeItem = function(nodeSummary) { | 72 var SyncNodeTreeItem = function(node) { |
43 var treeItem = new cr.ui.TreeItem({ | 73 var treeItem = new cr.ui.TreeItem(); |
44 id_: nodeSummary.id | |
45 }); | |
46 treeItem.__proto__ = SyncNodeTreeItem.prototype; | 74 treeItem.__proto__ = SyncNodeTreeItem.prototype; |
47 | 75 |
48 treeItem.label = nodeSummary.title; | 76 treeItem.entry_ = node; |
49 if (nodeSummary.isFolder) { | 77 treeItem.label = node.NON_UNIQUE_NAME; |
78 if (node.IS_DIR) { | |
50 treeItem.mayHaveChildren_ = true; | 79 treeItem.mayHaveChildren_ = true; |
51 | 80 |
52 // Load children asynchronously on expand. | 81 // Load children on expand. |
53 // TODO(akalin): Add a throbber while loading? | 82 treeItem.expanded_ = false; |
54 treeItem.triggeredLoad_ = false; | |
55 treeItem.addEventListener('expand', | 83 treeItem.addEventListener('expand', |
56 treeItem.handleExpand_.bind(treeItem)); | 84 treeItem.handleExpand_.bind(treeItem)); |
57 } else { | 85 } else { |
58 treeItem.classList.add('leaf'); | 86 treeItem.classList.add('leaf'); |
59 } | 87 } |
60 return treeItem; | 88 return treeItem; |
61 }; | 89 }; |
62 | 90 |
63 SyncNodeTreeItem.prototype = { | 91 SyncNodeTreeItem.prototype = { |
64 __proto__: cr.ui.TreeItem.prototype, | 92 __proto__: cr.ui.TreeItem.prototype, |
65 | 93 |
66 /** | 94 /** |
67 * Retrieves the details for this node. | 95 * 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 */ | 96 */ |
72 getDetails: function(callback) { | 97 handleExpand_: function(event) { |
73 chrome.sync.getNodeDetailsById([this.id_], function(nodeDetails) { | 98 var treeItem = this; |
74 callback(nodeDetails[0] || null); | 99 |
100 if (treeItem.expanded_) { | |
101 return; | |
102 } | |
103 treeItem.expanded_ = true; | |
104 | |
105 var children = treeItem.tree.allNodes.filter( | |
106 isChildOf.bind(undefined, treeItem.entry_.ID)); | |
107 children.sort(nodeComparator); | |
108 | |
109 children.forEach(function(node) { | |
110 treeItem.add(new SyncNodeTreeItem(node)); | |
75 }); | 111 }); |
76 }, | 112 }, |
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 }; | 113 }; |
101 | 114 |
102 /** | 115 /** |
103 * Updates the node detail view with the details for the given node. | 116 * 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 | 117 * type has its own root node for its own tree, but it still looks and acts |
105 * to display. | 118 * mostly like a tree. |
106 */ | 119 * |
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. | 120 * @param {Object=} opt_propertyBag Optional properties. |
117 * @constructor | 121 * @constructor |
118 * @extends {cr.ui.Tree} | 122 * @extends {cr.ui.Tree} |
119 */ | 123 */ |
120 var SyncNodeTree = cr.ui.define('tree'); | 124 var SyncNodeTree = cr.ui.define('tree'); |
121 | 125 |
122 SyncNodeTree.prototype = { | 126 SyncNodeTree.prototype = { |
123 __proto__: cr.ui.Tree.prototype, | 127 __proto__: cr.ui.Tree.prototype, |
124 | 128 |
125 decorate: function() { | 129 decorate: function() { |
126 cr.ui.Tree.prototype.decorate.call(this); | 130 cr.ui.Tree.prototype.decorate.call(this); |
127 this.addEventListener('change', this.handleChange_.bind(this)); | 131 this.addEventListener('change', this.handleChange_.bind(this)); |
128 chrome.sync.getRootNodeDetails(this.makeRoot_.bind(this)); | 132 this.allNodes = []; |
129 }, | 133 }, |
130 | 134 |
131 /** | 135 populate: function(nodes) { |
132 * Creates the root of the tree. | 136 var tree = this; |
133 * @param {{id: string, title: string, isFolder: boolean}} | 137 |
134 * rootNodeSummary The summary info for the root node. | 138 // We store the full set of nodes in the SyncNodeTree object. |
135 */ | 139 tree.allNodes = nodes; |
136 makeRoot_: function(rootNodeSummary) { | 140 |
137 // The root node usually doesn't have a title. | 141 var roots = tree.allNodes.filter(isTypeRootNode); |
138 rootNodeSummary.title = rootNodeSummary.title || 'Root'; | 142 roots.sort(nodeComparator); |
139 var rootTreeItem = new SyncNodeTreeItem(rootNodeSummary); | 143 |
140 this.add(rootTreeItem); | 144 roots.forEach(function(typeRoot) { |
145 tree.add(new SyncNodeTreeItem(typeRoot)); | |
146 }); | |
141 }, | 147 }, |
142 | 148 |
143 handleChange_: function(event) { | 149 handleChange_: function(event) { |
144 if (this.selectedItem) { | 150 if (this.selectedItem) { |
145 this.selectedItem.getDetails(updateNodeDetailView); | 151 updateNodeDetailView(this.selectedItem); |
146 } | 152 } |
147 } | 153 } |
148 }; | 154 }; |
149 | 155 |
150 function decorateSyncNodeBrowser(syncNodeBrowser) { | 156 /** |
151 cr.ui.decorate(syncNodeBrowser, SyncNodeTree); | 157 * Clears any existing UI state. Useful prior to a refresh. |
158 */ | |
159 function clear() { | |
160 var treeContainer = $('sync-node-tree-container'); | |
161 while (treeContainer.firstChild) { | |
162 treeContainer.removeChild(treeContainer.firstChild); | |
163 } | |
164 | |
165 var nodeDetailsView = $('node-details'); | |
166 nodeDetailsView.hidden = true; | |
152 } | 167 } |
153 | 168 |
154 // This is needed because JsTemplate (which is needed by | 169 /** |
155 // updateNodeDetailView) is loaded at the end of the file after | 170 * Fetch the latest set of nodes and refresh the UI. |
156 // everything else. | 171 */ |
157 // | 172 function refresh() { |
158 // TODO(akalin): Remove dependency on JsTemplate and get rid of | 173 $('node-browser-refresh-button').disabled = true; |
159 // this. | 174 |
160 var domLoaded = false; | 175 clear(); |
161 var pendingSyncNodeBrowsers = []; | 176 setLastRefreshTime('In progress since ' + (new Date()).toLocaleString()); |
162 function decorateSyncNodeBrowserAfterDOMLoad(id) { | 177 |
163 var e = document.getElementById(id); | 178 chrome.sync.getAllNodes(function(nodes) { |
164 if (domLoaded) { | 179 var treeContainer = $('sync-node-tree-container'); |
165 decorateSyncNodeBrowser(e); | 180 var tree = document.createElement('tree'); |
166 } else { | 181 tree.setAttribute('id', 'sync-node-tree'); |
167 pendingSyncNodeBrowsers.push(e); | 182 tree.setAttribute('icon-visibility', 'parent'); |
168 } | 183 treeContainer.appendChild(tree); |
184 | |
185 cr.ui.decorate(tree, SyncNodeTree); | |
186 tree.populate(nodes); | |
187 | |
188 setLastRefreshTime((new Date()).toLocaleString()); | |
189 $('node-browser-refresh-button').disabled = false; | |
190 }); | |
169 } | 191 } |
170 | 192 |
171 document.addEventListener('DOMContentLoaded', function() { | 193 document.addEventListener('DOMContentLoaded', function(e) { |
172 for (var i = 0; i < pendingSyncNodeBrowsers.length; ++i) { | 194 $('node-browser-refresh-button').addEventListener('click', refresh); |
173 decorateSyncNodeBrowser(pendingSyncNodeBrowsers[i]); | 195 cr.ui.decorate('#sync-node-splitter', cr.ui.Splitter); |
174 } | 196 |
175 domLoaded = true; | 197 // Automatically trigger a refresh the first time this tab is selected. |
198 $('sync-browser-tab').addEventListener('selectedChange', function f(e) { | |
199 if (this.selected) { | |
200 $('sync-browser-tab').removeEventListener('selectedChange', f); | |
201 refresh(); | |
202 } | |
203 }); | |
176 }); | 204 }); |
177 | 205 |
178 return { | 206 })(); |
179 decorateSyncNodeBrowser: decorateSyncNodeBrowserAfterDOMLoad | |
180 }; | |
181 }); | |
OLD | NEW |