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