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