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
3// found in the LICENSE file.
4
5// require: cr.js
6// require: cr/ui.js
7// require: cr/ui/tree.js
8
9(function() {
10  /**
11   * A helper function to determine if a node is the root of its type.
12   *
13   * @param {!Object} node The node to check.
14   */
15  var isTypeRootNode = function(node) {
16    return node.PARENT_ID == 'r' && node.UNIQUE_SERVER_TAG != '';
17  }
18
19  /**
20   * A helper function to determine if a node is a child of the given parent.
21   *
22   * @param {string} parentId The ID of the parent.
23   * @param {!Object} node The node to check.
24   */
25  var isChildOf = function(parentId, node) {
26    return node.PARENT_ID == parentId;
27  }
28
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;
65  }
66
67  /**
68   * Creates a new sync node tree item.
69   *
70   * @constructor
71   * @param {!Object} node The nodeDetails object for the node as returned by
72   *     chrome.sync.getAllNodes().
73   * @extends {cr.ui.TreeItem}
74   */
75  var SyncNodeTreeItem = function(node) {
76    var treeItem = new cr.ui.TreeItem();
77    treeItem.__proto__ = SyncNodeTreeItem.prototype;
78
79    treeItem.entry_ = node;
80    treeItem.label = node.NON_UNIQUE_NAME;
81    if (node.IS_DIR) {
82      treeItem.mayHaveChildren_ = true;
83
84      // Load children on expand.
85      treeItem.expanded_ = false;
86      treeItem.addEventListener('expand',
87                                treeItem.handleExpand_.bind(treeItem));
88    } else {
89      treeItem.classList.add('leaf');
90    }
91    return treeItem;
92  };
93
94  SyncNodeTreeItem.prototype = {
95    __proto__: cr.ui.TreeItem.prototype,
96
97    /**
98     * Finds the children of this node and appends them to the tree.
99     */
100    handleExpand_: function(event) {
101      var treeItem = this;
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));
114      });
115    },
116  };
117
118  /**
119   * Creates a new sync node tree.  Technically, it's a forest since it each
120   * type has its own root node for its own tree, but it still looks and acts
121   * mostly like a tree.
122   *
123   * @param {Object=} opt_propertyBag Optional properties.
124   * @constructor
125   * @extends {cr.ui.Tree}
126   */
127  var SyncNodeTree = cr.ui.define('tree');
128
129  SyncNodeTree.prototype = {
130    __proto__: cr.ui.Tree.prototype,
131
132    decorate: function() {
133      cr.ui.Tree.prototype.decorate.call(this);
134      this.addEventListener('change', this.handleChange_.bind(this));
135      this.allNodes = [];
136    },
137
138    populate: function(nodes) {
139      var tree = this;
140
141      // We store the full set of nodes in the SyncNodeTree object.
142      tree.allNodes = nodes;
143
144      var roots = tree.allNodes.filter(isTypeRootNode);
145      roots.sort(nodeComparator);
146
147      roots.forEach(function(typeRoot) {
148        tree.add(new SyncNodeTreeItem(typeRoot));
149      });
150    },
151
152    handleChange_: function(event) {
153      if (this.selectedItem) {
154        updateNodeDetailView(this.selectedItem);
155      }
156    }
157  };
158
159  /**
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;
170  }
171
172  /**
173   * Fetch the latest set of nodes and refresh the UI.
174   */
175  function refresh() {
176    $('node-browser-refresh-button').disabled = true;
177
178    clear();
179    setLastRefreshTime('In progress since ' + (new Date()).toLocaleString());
180
181    chrome.sync.getAllNodes(function(nodeMap) {
182      // Put all nodes into one big list that ignores the type.
183      var nodes = nodeMap.
184          map(function(x) { return x.nodes; }).
185          reduce(function(a, b) { return a.concat(b); });
186
187      var treeContainer = $('sync-node-tree-container');
188      var tree = document.createElement('tree');
189      tree.setAttribute('id', 'sync-node-tree');
190      tree.setAttribute('icon-visibility', 'parent');
191      treeContainer.appendChild(tree);
192
193      cr.ui.decorate(tree, SyncNodeTree);
194      tree.populate(nodes);
195
196      setLastRefreshTime((new Date()).toLocaleString());
197      $('node-browser-refresh-button').disabled = false;
198    });
199  }
200
201  document.addEventListener('DOMContentLoaded', function(e) {
202    $('node-browser-refresh-button').addEventListener('click', refresh);
203    cr.ui.decorate('#sync-node-splitter', cr.ui.Splitter);
204
205    // Automatically trigger a refresh the first time this tab is selected.
206    $('sync-browser-tab').addEventListener('selectedChange', function f(e) {
207      if (this.selected) {
208        $('sync-browser-tab').removeEventListener('selectedChange', f);
209        refresh();
210      }
211    });
212  });
213
214})();
215