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