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