Home | History | Annotate | Download | only in bmm
      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 
      6 cr.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