• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2010 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
6cr.define('bmm', function() {
7  const Tree = cr.ui.Tree;
8  const TreeItem = cr.ui.TreeItem;
9
10  var treeLookup = {};
11
12  // Manager for persisting the expanded state.
13  var expandedManager = {
14    /**
15     * A map of the collapsed IDs.
16     * @type {Object}
17     */
18    map: 'bookmarkTreeState' in localStorage ?
19        JSON.parse(localStorage['bookmarkTreeState']) : {},
20
21    /**
22     * Set the collapsed state for an ID.
23     * @param {string} The bookmark ID of the tree item that was expanded or
24     *     collapsed.
25     * @param {boolean} expanded Whether the tree item was expanded.
26     */
27    set: function(id, expanded) {
28      if (expanded)
29        delete this.map[id];
30      else
31        this.map[id] = 1;
32
33      this.save();
34    },
35
36    /**
37     * @param {string} id The bookmark ID.
38     * @return {boolean} Whether the tree item should be expanded.
39     */
40    get: function(id) {
41      return !(id in this.map);
42    },
43
44    /**
45     * Callback for the expand and collapse events from the tree.
46     * @param {!Event} e The collapse or expand event.
47     */
48    handleEvent: function(e) {
49      this.set(e.target.bookmarkId, e.type == 'expand');
50    },
51
52    /**
53     * Cleans up old bookmark IDs.
54     */
55    cleanUp: function() {
56      for (var id in this.map) {
57        // If the id is no longer in the treeLookup the bookmark no longer
58        // exists.
59        if (!(id in treeLookup))
60          delete this.map[id];
61      }
62      this.save();
63    },
64
65    timer: null,
66
67    /**
68     * Saves the expanded state to the localStorage.
69     */
70    save: function() {
71      clearTimeout(this.timer);
72      var map = this.map;
73      // Save in a timeout so that we can coalesce multiple changes.
74      this.timer = setTimeout(function() {
75        localStorage['bookmarkTreeState'] = JSON.stringify(map);
76      }, 100);
77    }
78  };
79
80  // Clean up once per session but wait until things settle down a bit.
81  setTimeout(expandedManager.cleanUp.bind(expandedManager), 1e4);
82
83  /**
84   * Creates a new tree item for a bookmark node.
85   * @param {!Object} bookmarkNode The bookmark node.
86   * @constructor
87   * @extends {TreeItem}
88   */
89  function BookmarkTreeItem(bookmarkNode) {
90    var ti = new TreeItem({
91      label: bookmarkNode.title,
92      bookmarkNode: bookmarkNode,
93      // Bookmark toolbar and Other bookmarks are not draggable.
94      draggable: bookmarkNode.parentId != ROOT_ID
95    });
96    ti.__proto__ = BookmarkTreeItem.prototype;
97    return ti;
98  }
99
100  BookmarkTreeItem.prototype = {
101    __proto__: TreeItem.prototype,
102
103    /** @inheritDoc */
104    addAt: function(child, index) {
105      TreeItem.prototype.addAt.call(this, child, index);
106      if (child.bookmarkNode)
107        treeLookup[child.bookmarkNode.id] = child;
108    },
109
110    /** @inheritDoc */
111    remove: function(child) {
112      TreeItem.prototype.remove.call(this, child);
113      if (child.bookmarkNode)
114        delete treeLookup[child.bookmarkNode.id];
115    },
116
117    /**
118     * The ID of the bookmark this tree item represents.
119     * @type {string}
120     */
121    get bookmarkId() {
122      return this.bookmarkNode.id;
123    }
124  };
125
126  /**
127   * Asynchronousy adds a tree item at the correct index based on the bookmark
128   * backend.
129   *
130   * Since the bookmark tree only contains folders the index we get from certain
131   * callbacks is not very useful so we therefore have this async call which
132   * gets the children of the parent and adds the tree item at the desired
133   * index.
134   *
135   * This also exoands the parent so that newly added children are revealed.
136   *
137   * @param {!cr.ui.TreeItem} parent The parent tree item.
138   * @param {!cr.ui.TreeItem} treeItem The tree item to add.
139   * @param {Function=} f A function which gets called after the item has been
140   *     added at the right index.
141   */
142  function addTreeItem(parent, treeItem, opt_f) {
143    chrome.bookmarks.getChildren(parent.bookmarkNode.id, function(children) {
144      var index = children.filter(bmm.isFolder).map(function(item) {
145        return item.id;
146      }).indexOf(treeItem.bookmarkNode.id);
147      parent.addAt(treeItem, index);
148      parent.expanded = true;
149      if (opt_f)
150        opt_f();
151    });
152  }
153
154
155  /**
156   * Creates a new bookmark list.
157   * @param {Object=} opt_propertyBag Optional properties.
158   * @constructor
159   * @extends {HTMLButtonElement}
160   */
161  var BookmarkTree = cr.ui.define('tree');
162
163  BookmarkTree.prototype = {
164    __proto__: Tree.prototype,
165
166    decorate: function() {
167      Tree.prototype.decorate.call(this);
168      this.addEventListener('expand', expandedManager);
169      this.addEventListener('collapse', expandedManager);
170    },
171
172    handleBookmarkChanged: function(id, changeInfo) {
173      var treeItem = treeLookup[id];
174      if (treeItem)
175        treeItem.label = treeItem.bookmarkNode.title = changeInfo.title;
176    },
177
178    handleChildrenReordered: function(id, reorderInfo) {
179      var parentItem = treeLookup[id];
180      // The tree only contains folders.
181      var dirIds = reorderInfo.childIds.filter(function(id) {
182        return id in treeLookup;
183      }).forEach(function(id, i) {
184        parentItem.addAt(treeLookup[id], i);
185      });
186    },
187
188    handleCreated: function(id, bookmarkNode) {
189      if (bmm.isFolder(bookmarkNode)) {
190        var parentItem = treeLookup[bookmarkNode.parentId];
191        var newItem = new BookmarkTreeItem(bookmarkNode);
192        addTreeItem(parentItem, newItem);
193      }
194    },
195
196    handleMoved: function(id, moveInfo) {
197      var treeItem = treeLookup[id];
198      if (treeItem) {
199        var oldParentItem = treeLookup[moveInfo.oldParentId];
200        oldParentItem.remove(treeItem);
201        var newParentItem = treeLookup[moveInfo.parentId];
202        // The tree only shows folders so the index is not the index we want. We
203        // therefore get the children need to adjust the index.
204        addTreeItem(newParentItem, treeItem);
205      }
206    },
207
208    handleRemoved: function(id, removeInfo) {
209      var parentItem = treeLookup[removeInfo.parentId];
210      var itemToRemove = treeLookup[id];
211      if (parentItem && itemToRemove)
212        parentItem.remove(itemToRemove);
213    },
214
215    insertSubtree:function(folder) {
216      if (!bmm.isFolder(folder))
217        return;
218      var children = folder.children;
219      this.handleCreated(folder.id, folder);
220      for(var i = 0; i < children.length; i++) {
221        var child = children[i];
222        this.insertSubtree(child);
223      }
224    },
225
226    /**
227     * Returns the bookmark node with the given ID. The tree only maintains
228     * folder nodes.
229     * @param {string} id The ID of the node to find.
230     * @return {BookmarkTreeNode} The bookmark tree node or null if not found.
231     */
232    getBookmarkNodeById: function(id) {
233      var treeItem = treeLookup[id];
234      if (treeItem)
235        return treeItem.bookmarkNode;
236      return null;
237    },
238
239    /**
240     * Fetches the bookmark items and builds the tree control.
241     */
242    reload: function() {
243      /**
244       * Recursive helper function that adds all the directories to the
245       * parentTreeItem.
246       * @param {!cr.ui.Tree|!cr.ui.TreeItem} parentTreeItem The parent tree
247       *     element to append to.
248       * @param {!Array.<BookmarkTreeNode>} bookmarkNodes
249       * @return {boolean} Whether any directories where added.
250       */
251      function buildTreeItems(parentTreeItem, bookmarkNodes) {
252        var hasDirectories = false;
253        for (var i = 0, bookmarkNode; bookmarkNode = bookmarkNodes[i]; i++) {
254          if (bmm.isFolder(bookmarkNode)) {
255            hasDirectories = true;
256            var item = new BookmarkTreeItem(bookmarkNode);
257            parentTreeItem.add(item);
258            var anyChildren = buildTreeItems(item, bookmarkNode.children);
259            item.expanded = anyChildren && expandedManager.get(bookmarkNode.id);
260          }
261        }
262        return hasDirectories;
263      }
264
265      var self = this;
266      chrome.experimental.bookmarkManager.getSubtree('', true, function(root) {
267        self.clear();
268        buildTreeItems(self, root[0].children);
269        cr.dispatchSimpleEvent(self, 'load');
270      });
271    },
272
273    /**
274     * Clears the tree.
275     */
276    clear: function() {
277      // Remove all fields without recreating the object since other code
278      // references it.
279      for (var id in treeLookup){
280        delete treeLookup[id];
281      }
282      this.textContent = '';
283    },
284
285    /** @inheritDoc */
286    addAt: function(child, index) {
287      Tree.prototype.addAt.call(this, child, index);
288      if (child.bookmarkNode)
289        treeLookup[child.bookmarkNode.id] = child;
290    },
291
292    /** @inheritDoc */
293    remove: function(child) {
294      Tree.prototype.remove.call(this, child);
295      if (child.bookmarkNode)
296        delete treeLookup[child.bookmarkNode.id];
297    }
298  };
299
300  return {
301    BookmarkTree: BookmarkTree,
302    BookmarkTreeItem: BookmarkTreeItem,
303    treeLookup: treeLookup
304  };
305});
306