Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / bookmark_manager / js / bmm / bookmark_tree.js
blob0a94621a4649c57fd12bba04c0ecee2cdffe37d8
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.
6 cr.define('bmm', function() {
7   /**
8    * The id of the bookmark root.
9    * @type {string}
10    * @const
11    */
12   var ROOT_ID = '0';
14   /** @const */ var Tree = cr.ui.Tree;
15   /** @const */ var TreeItem = cr.ui.TreeItem;
17   var treeLookup = {};
18   var tree;
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']) : {},
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;
41       this.save();
42     },
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     },
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     },
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     },
73     timer: null,
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   };
88   // Clean up once per session but wait until things settle down a bit.
89   setTimeout(expandedManager.cleanUp.bind(expandedManager), 1e4);
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   }
109   BookmarkTreeItem.prototype = {
110     __proto__: TreeItem.prototype,
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   };
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   }
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');
158   BookmarkTree.prototype = {
159     __proto__: Tree.prototype,
161     decorate: function() {
162       Tree.prototype.decorate.call(this);
163       this.addEventListener('expand', expandedManager);
164       this.addEventListener('collapse', expandedManager);
166       bmm.tree = this;
167     },
169     handleBookmarkChanged: function(id, changeInfo) {
170       var treeItem = treeLookup[id];
171       if (treeItem)
172         treeItem.label = treeItem.bookmarkNode.title = changeInfo.title;
173     },
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     },
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     },
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     },
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     },
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     },
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     },
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      },
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       }
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     },
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     },
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   };
300   return {
301     BookmarkTree: BookmarkTree,
302     BookmarkTreeItem: BookmarkTreeItem,
303     treeLookup: treeLookup,
304     tree: tree,
305     ROOT_ID: ROOT_ID
306   };