Home | History | Annotate | Download | only in js
      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 const BookmarkList = bmm.BookmarkList;
      6 const BookmarkTree = bmm.BookmarkTree;
      7 const ListItem = cr.ui.ListItem;
      8 const TreeItem = cr.ui.TreeItem;
      9 const LinkKind = cr.LinkKind;
     10 const Command = cr.ui.Command;
     11 const CommandBinding = cr.ui.CommandBinding;
     12 const Menu = cr.ui.Menu;
     13 const MenuButton  = cr.ui.MenuButton;
     14 const Promise = cr.Promise;
     15 
     16 // Sometimes the extension API is not initialized.
     17 if (!chrome.bookmarks)
     18   console.error('Bookmarks extension API is not available');
     19 
     20 // Allow platform specific CSS rules.
     21 if (cr.isMac)
     22   document.documentElement.setAttribute('os', 'mac');
     23 
     24 /**
     25  * The local strings object which is used to do the translation.
     26  * @type {!LocalStrings}
     27  */
     28 var localStrings = new LocalStrings;
     29 
     30 // Get the localized strings from the backend.
     31 chrome.experimental.bookmarkManager.getStrings(function(data) {
     32   // The strings may contain & which we need to strip.
     33   for (var key in data) {
     34     data[key] = data[key].replace(/&/, '');
     35   }
     36 
     37   localStrings.templateData = data;
     38   i18nTemplate.process(document, data);
     39 
     40   recentTreeItem.label = localStrings.getString('recent');
     41   searchTreeItem.label = localStrings.getString('search');
     42 });
     43 
     44 /**
     45  * The id of the bookmark root.
     46  * @type {number}
     47  */
     48 const ROOT_ID = '0';
     49 
     50 var bookmarkCache = {
     51   /**
     52    * Removes the cached item from both the list and tree lookups.
     53    */
     54   remove: function(id) {
     55     var treeItem = bmm.treeLookup[id];
     56     if (treeItem) {
     57       var items = treeItem.items; // is an HTMLCollection
     58       for (var i = 0, item; item = items[i]; i++) {
     59         var bookmarkNode = item.bookmarkNode;
     60         delete bmm.treeLookup[bookmarkNode.id];
     61       }
     62       delete bmm.treeLookup[id];
     63     }
     64   },
     65 
     66   /**
     67    * Updates the underlying bookmark node for the tree items and list items by
     68    * querying the bookmark backend.
     69    * @param {string} id The id of the node to update the children for.
     70    * @param {Function=} opt_f A funciton to call when done.
     71    */
     72   updateChildren: function(id, opt_f) {
     73     function updateItem(bookmarkNode) {
     74       var treeItem = bmm.treeLookup[bookmarkNode.id];
     75       if (treeItem) {
     76         treeItem.bookmarkNode = bookmarkNode;
     77       }
     78     }
     79 
     80     chrome.bookmarks.getChildren(id, function(children) {
     81       if (children)
     82         children.forEach(updateItem);
     83 
     84       if (opt_f)
     85         opt_f(children);
     86     });
     87   }
     88 };
     89 
     90 var splitter = document.querySelector('.main > .splitter');
     91 cr.ui.Splitter.decorate(splitter);
     92 
     93 // The splitter persists the size of the left component in the local store.
     94 if ('treeWidth' in localStorage)
     95   splitter.previousElementSibling.style.width = localStorage['treeWidth'];
     96 splitter.addEventListener('resize', function(e) {
     97   localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
     98 });
     99 
    100 BookmarkList.decorate(list);
    101 
    102 var searchTreeItem = new TreeItem({
    103   icon: 'images/bookmark_manager_search.png',
    104   bookmarkId: 'q='
    105 });
    106 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
    107 
    108 var recentTreeItem = new TreeItem({
    109   icon: 'images/bookmark_manager_recent.png',
    110   bookmarkId: 'recent'
    111 });
    112 bmm.treeLookup[recentTreeItem.bookmarkId] = recentTreeItem;
    113 
    114 BookmarkTree.decorate(tree);
    115 
    116 tree.addEventListener('change', function() {
    117   navigateTo(tree.selectedItem.bookmarkId);
    118 });
    119 
    120 /**
    121  * Navigates to a bookmark ID.
    122  * @param {string} id The ID to navigate to.
    123  * @param {boolean=} opt_updateHashNow Whether to immediately update the
    124  *     location.hash. If false then it is updated in a timeout.
    125  */
    126 function navigateTo(id, opt_updateHashNow) {
    127   console.info('navigateTo', 'from', window.location.hash, 'to', id);
    128   // Update the location hash using a timer to prevent reentrancy. This is how
    129   // often we add history entries and the time here is a bit arbitrary but was
    130   // picked as the smallest time a human perceives as instant.
    131 
    132   function f() {
    133     window.location.hash = tree.selectedItem.bookmarkId;
    134   }
    135 
    136   clearTimeout(navigateTo.timer_);
    137   if (opt_updateHashNow)
    138     f();
    139   else
    140     navigateTo.timer_ = setTimeout(f, 250);
    141 
    142   updateParentId(id);
    143 }
    144 
    145 /**
    146  * Updates the parent ID of the bookmark list and selects the correct tree item.
    147  * @param {string} id The id.
    148  */
    149 function updateParentId(id) {
    150   list.parentId = id;
    151   if (id in bmm.treeLookup)
    152     tree.selectedItem = bmm.treeLookup[id];
    153 }
    154 
    155 // We listen to hashchange so that we can update the currently shown folder when
    156 // the user goes back and forward in the history.
    157 window.onhashchange = function(e) {
    158   var id = window.location.hash.slice(1);
    159 
    160   var valid = false;
    161 
    162   // In case we got a search hash update the text input and the bmm.treeLookup
    163   // to use the new id.
    164   if (/^q=/.test(id)) {
    165     setSearch(id.slice(2));
    166     valid = true;
    167   } else if (id == 'recent') {
    168     valid = true;
    169   }
    170 
    171   if (valid) {
    172     updateParentId(id);
    173   } else {
    174     // We need to verify that this is a correct ID.
    175     chrome.bookmarks.get(id, function(items) {
    176       if (items && items.length == 1)
    177         updateParentId(id);
    178     });
    179   }
    180 };
    181 
    182 // Activate is handled by the open-in-same-window-command.
    183 list.addEventListener('dblclick', function(e) {
    184   if (e.button == 0)
    185     $('open-in-same-window-command').execute();
    186 });
    187 
    188 // The list dispatches an event when the user clicks on the URL or the Show in
    189 // folder part.
    190 list.addEventListener('urlClicked', function(e) {
    191   getLinkController().openUrlFromEvent(e.url, e.originalEvent);
    192 });
    193 
    194 $('term').onsearch = function(e) {
    195   setSearch(this.value);
    196 };
    197 
    198 /**
    199  * Navigates to the search results for the search text.
    200  * @para {string} searchText The text to search for.
    201  */
    202 function setSearch(searchText) {
    203   if (searchText) {
    204     // Only update search item if we have a search term. We never want the
    205     // search item to be for an empty search.
    206     delete bmm.treeLookup[searchTreeItem.bookmarkId];
    207     var id = searchTreeItem.bookmarkId = 'q=' + searchText;
    208     bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
    209   }
    210 
    211   var input = $('term');
    212   // Do not update the input if the user is actively using the text input.
    213   if (document.activeElement != input)
    214     input.value = searchText;
    215 
    216   if (searchText) {
    217     tree.add(searchTreeItem);
    218     tree.selectedItem = searchTreeItem;
    219   } else {
    220     // Go "home".
    221     tree.selectedItem = tree.items[0];
    222     id = tree.selectedItem.bookmarkId;
    223   }
    224 
    225   // Navigate now and update hash immediately.
    226   navigateTo(id, true);
    227 }
    228 
    229 // Handle the logo button UI.
    230 // When the user clicks the button we should navigate "home" and focus the list
    231 document.querySelector('button.logo').onclick = function(e) {
    232   setSearch('');
    233   $('list').focus();
    234 };
    235 
    236 /**
    237  * Called when the title of a bookmark changes.
    238  * @param {string} id
    239  * @param {!Object} changeInfo
    240  */
    241 function handleBookmarkChanged(id, changeInfo) {
    242   // console.info('handleBookmarkChanged', id, changeInfo);
    243   list.handleBookmarkChanged(id, changeInfo);
    244   tree.handleBookmarkChanged(id, changeInfo);
    245 }
    246 
    247 /**
    248  * Callback for when the user reorders by title.
    249  * @param {string} id The id of the bookmark folder that was reordered.
    250  * @param {!Object} reorderInfo The information about how the items where
    251  *     reordered.
    252  */
    253 function handleChildrenReordered(id, reorderInfo) {
    254   // console.info('handleChildrenReordered', id, reorderInfo);
    255   list.handleChildrenReordered(id, reorderInfo);
    256   tree.handleChildrenReordered(id, reorderInfo);
    257   bookmarkCache.updateChildren(id);
    258 }
    259 
    260 /**
    261  * Callback for when a bookmark node is created.
    262  * @param {string} id The id of the newly created bookmark node.
    263  * @param {!Object} bookmarkNode The new bookmark node.
    264  */
    265 function handleCreated(id, bookmarkNode) {
    266   // console.info('handleCreated', id, bookmarkNode);
    267   list.handleCreated(id, bookmarkNode);
    268   tree.handleCreated(id, bookmarkNode);
    269   bookmarkCache.updateChildren(bookmarkNode.parentId);
    270 }
    271 
    272 function handleMoved(id, moveInfo) {
    273   // console.info('handleMoved', id, moveInfo);
    274   list.handleMoved(id, moveInfo);
    275   tree.handleMoved(id, moveInfo);
    276 
    277   bookmarkCache.updateChildren(moveInfo.parentId);
    278   if (moveInfo.parentId != moveInfo.oldParentId)
    279     bookmarkCache.updateChildren(moveInfo.oldParentId);
    280 }
    281 
    282 function handleRemoved(id, removeInfo) {
    283   // console.info('handleRemoved', id, removeInfo);
    284   list.handleRemoved(id, removeInfo);
    285   tree.handleRemoved(id, removeInfo);
    286 
    287   bookmarkCache.updateChildren(removeInfo.parentId);
    288   bookmarkCache.remove(id);
    289 }
    290 
    291 function handleImportBegan() {
    292   chrome.bookmarks.onCreated.removeListener(handleCreated);
    293   chrome.bookmarks.onChanged.removeListener(handleBookmarkChanged);
    294 }
    295 
    296 function handleImportEnded() {
    297   // When importing is done we reload the tree and the list.
    298 
    299   function f() {
    300     tree.removeEventListener('load', f);
    301 
    302     chrome.bookmarks.onCreated.addListener(handleCreated);
    303     chrome.bookmarks.onChanged.addListener(handleBookmarkChanged);
    304 
    305     if (list.selectImportedFolder) {
    306       var otherBookmarks = tree.items[1].items;
    307       var importedFolder = otherBookmarks[otherBookmarks.length - 1];
    308       navigateTo(importedFolder.bookmarkId)
    309       list.selectImportedFolder = false
    310     } else {
    311       list.reload();
    312     }
    313   }
    314 
    315   tree.addEventListener('load', f);
    316   tree.reload();
    317 }
    318 
    319 /**
    320  * Adds the listeners for the bookmark model change events.
    321  */
    322 function addBookmarkModelListeners() {
    323   chrome.bookmarks.onChanged.addListener(handleBookmarkChanged);
    324   chrome.bookmarks.onChildrenReordered.addListener(handleChildrenReordered);
    325   chrome.bookmarks.onCreated.addListener(handleCreated);
    326   chrome.bookmarks.onMoved.addListener(handleMoved);
    327   chrome.bookmarks.onRemoved.addListener(handleRemoved);
    328   chrome.bookmarks.onImportBegan.addListener(handleImportBegan);
    329   chrome.bookmarks.onImportEnded.addListener(handleImportEnded);
    330 }
    331 
    332 /**
    333  * This returns the user visible path to the folder where the bookmark is
    334  * located.
    335  * @param {number} parentId The ID of the parent folder.
    336  * @return {string} The path to the the bookmark,
    337  */
    338 function getFolder(parentId) {
    339   var parentNode = tree.getBookmarkNodeById(parentId);
    340   if (parentNode) {
    341     var s = parentNode.title;
    342     if (parentNode.parentId != ROOT_ID) {
    343       return getFolder(parentNode.parentId) + '/' + s;
    344     }
    345     return s;
    346   }
    347 }
    348 
    349 tree.addEventListener('load', function(e) {
    350   // Add hard coded tree items
    351   tree.add(recentTreeItem);
    352 
    353   // Now we can select a tree item.
    354   var hash = window.location.hash.slice(1);
    355   if (!hash) {
    356     // If we do not have a hash select first item in the tree.
    357     hash = tree.items[0].bookmarkId;
    358   }
    359 
    360   if (/^q=/.test(hash)) {
    361     var searchTerm = hash.slice(2);
    362     $('term').value = searchTerm;
    363     setSearch(searchTerm);
    364   } else {
    365     navigateTo(hash);
    366   }
    367 });
    368 
    369 tree.reload();
    370 addBookmarkModelListeners();
    371 
    372 var dnd = {
    373   dragData: null,
    374 
    375   getBookmarkElement: function(el) {
    376     while (el && !el.bookmarkNode) {
    377       el = el.parentNode;
    378     }
    379     return el;
    380   },
    381 
    382   // If we are over the list and the list is showing recent or search result
    383   // we cannot drop.
    384   isOverRecentOrSearch: function(overElement) {
    385     return (list.isRecent() || list.isSearch()) && list.contains(overElement);
    386   },
    387 
    388   checkEvery_: function(f, overBookmarkNode, overElement) {
    389     return this.dragData.elements.every(function(element) {
    390       return f.call(this, element, overBookmarkNode, overElement);
    391     }, this);
    392   },
    393 
    394   /**
    395    * @return {boolean} Whether we are currently dragging any folders.
    396    */
    397   isDraggingFolders: function() {
    398     return !!this.dragData && this.dragData.elements.some(function(node) {
    399       return !node.url;
    400     });
    401   },
    402 
    403   /**
    404    * This is a first pass wether we can drop the dragged items.
    405    *
    406    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
    407    *     currently dragging over.
    408    * @param {!HTMLElement} overElement The element that we are currently
    409    *     dragging over.
    410    * @return {boolean} If this returns false then we know we should not drop
    411    *     the items. If it returns true we still have to call canDropOn,
    412    *     canDropAbove and canDropBelow.
    413    */
    414   canDrop: function(overBookmarkNode, overElement) {
    415     var dragData = this.dragData;
    416     if (!dragData)
    417       return false;
    418 
    419     if (this.isOverRecentOrSearch(overElement))
    420       return false;
    421 
    422     if (!dragData.sameProfile)
    423       return true;
    424 
    425     return this.checkEvery_(this.canDrop_, overBookmarkNode, overElement);
    426   },
    427 
    428   /**
    429    * Helper for canDrop that only checks one bookmark node.
    430    * @private
    431    */
    432   canDrop_: function(dragNode, overBookmarkNode, overElement) {
    433     var dragId = dragNode.id;
    434 
    435     if (overBookmarkNode.id == dragId)
    436       return false;
    437 
    438     // If we are dragging a folder we cannot drop it on any of its descendants
    439     var dragBookmarkItem = bmm.treeLookup[dragId];
    440     var dragBookmarkNode = dragBookmarkItem && dragBookmarkItem.bookmarkNode;
    441     if (dragBookmarkNode && bmm.contains(dragBookmarkNode, overBookmarkNode)) {
    442       return false;
    443     }
    444 
    445     return true;
    446   },
    447 
    448   /**
    449    * Whether we can drop the dragged items above the drop target.
    450    *
    451    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
    452    *     currently dragging over.
    453    * @param {!HTMLElement} overElement The element that we are currently
    454    *     dragging over.
    455    * @return {boolean} Whether we can drop the dragged items above the drop
    456    *     target.
    457    */
    458   canDropAbove: function(overBookmarkNode, overElement) {
    459     if (overElement instanceof BookmarkList)
    460       return false;
    461 
    462     // We cannot drop between Bookmarks bar and Other bookmarks
    463     if (overBookmarkNode.parentId == ROOT_ID)
    464       return false;
    465 
    466     var isOverTreeItem = overElement instanceof TreeItem;
    467 
    468     // We can only drop between items in the tree if we have any folders.
    469     if (isOverTreeItem && !this.isDraggingFolders())
    470       return false;
    471 
    472     if (!this.dragData.sameProfile)
    473       return this.isDraggingFolders() || !isOverTreeItem;
    474 
    475     return this.checkEvery_(this.canDropAbove_, overBookmarkNode, overElement);
    476   },
    477 
    478   /**
    479    * Helper for canDropAbove that only checks one bookmark node.
    480    * @private
    481    */
    482   canDropAbove_: function(dragNode, overBookmarkNode, overElement) {
    483     var dragId = dragNode.id;
    484 
    485     // We cannot drop above if the item below is already in the drag source
    486     var previousElement = overElement.previousElementSibling;
    487     if (previousElement &&
    488         previousElement.bookmarkId == dragId)
    489       return false;
    490 
    491     return true;
    492   },
    493 
    494   /**
    495    * Whether we can drop the dragged items below the drop target.
    496    *
    497    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
    498    *     currently dragging over.
    499    * @param {!HTMLElement} overElement The element that we are currently
    500    *     dragging over.
    501    * @return {boolean} Whether we can drop the dragged items below the drop
    502    *     target.
    503    */
    504   canDropBelow: function(overBookmarkNode, overElement) {
    505     if (overElement instanceof BookmarkList)
    506       return false;
    507 
    508     // We cannot drop between Bookmarks bar and Other bookmarks
    509     if (overBookmarkNode.parentId == ROOT_ID)
    510       return false;
    511 
    512     // We can only drop between items in the tree if we have any folders.
    513     if (!this.isDraggingFolders() && overElement instanceof TreeItem)
    514       return false;
    515 
    516     var isOverTreeItem = overElement instanceof TreeItem;
    517 
    518     // Don't allow dropping below an expanded tree item since it is confusing
    519     // to the user anyway.
    520     if (isOverTreeItem && overElement.expanded)
    521       return false;
    522 
    523     if (!this.dragData.sameProfile)
    524       return this.isDraggingFolders() || !isOverTreeItem;
    525 
    526     return this.checkEvery_(this.canDropBelow_, overBookmarkNode, overElement);
    527   },
    528 
    529   /**
    530    * Helper for canDropBelow that only checks one bookmark node.
    531    * @private
    532    */
    533   canDropBelow_: function(dragNode, overBookmarkNode, overElement) {
    534     var dragId = dragNode.id;
    535 
    536     // We cannot drop below if the item below is already in the drag source
    537     var nextElement = overElement.nextElementSibling;
    538     if (nextElement &&
    539         nextElement.bookmarkId == dragId)
    540       return false;
    541 
    542     return true;
    543   },
    544 
    545   /**
    546    * Whether we can drop the dragged items on the drop target.
    547    *
    548    * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
    549    *     currently dragging over.
    550    * @param {!HTMLElement} overElement The element that we are currently
    551    *     dragging over.
    552    * @return {boolean} Whether we can drop the dragged items on the drop
    553    *     target.
    554    */
    555   canDropOn: function(overBookmarkNode, overElement) {
    556     // We can only drop on a folder.
    557     if (!bmm.isFolder(overBookmarkNode))
    558       return false;
    559 
    560     if (!this.dragData.sameProfile)
    561       return true;
    562 
    563     return this.checkEvery_(this.canDropOn_, overBookmarkNode, overElement);
    564   },
    565 
    566   /**
    567    * Helper for canDropOn that only checks one bookmark node.
    568    * @private
    569    */
    570   canDropOn_: function(dragNode, overBookmarkNode, overElement) {
    571     var dragId = dragNode.id;
    572 
    573     if (overElement instanceof BookmarkList) {
    574       // We are trying to drop an item after the last item in the list. This
    575       // is allowed if the item is different from the last item in the list
    576       var listItems = list.items;
    577       var len = listItems.length;
    578       if (len == 0 ||
    579           listItems[len - 1].bookmarkId != dragId) {
    580         return true;
    581       }
    582     }
    583 
    584     // Cannot drop on current parent.
    585     if (overBookmarkNode.id == dragNode.parentId)
    586       return false;
    587 
    588     return true;
    589   },
    590 
    591   /**
    592    * Callback for the dragstart event.
    593    * @param {Event} e The dragstart event.
    594    */
    595   handleDragStart: function(e) {
    596     // Determine the selected bookmarks.
    597     var target = e.target;
    598     var draggedNodes = [];
    599     if (target instanceof ListItem) {
    600       // Use selected items.
    601       draggedNodes = target.parentNode.selectedItems;
    602     } else if (target instanceof TreeItem) {
    603       draggedNodes.push(target.bookmarkNode);
    604     }
    605 
    606     // We manage starting the drag by using the extension API.
    607     e.preventDefault();
    608 
    609     if (draggedNodes.length) {
    610       // If we are dragging a single link we can do the *Link* effect, otherwise
    611       // we only allow copy and move.
    612       var effectAllowed;
    613       if (draggedNodes.length == 1 &&
    614           !bmm.isFolder(draggedNodes[0])) {
    615         effectAllowed = 'copyMoveLink';
    616       } else {
    617         effectAllowed = 'copyMove';
    618       }
    619       e.dataTransfer.effectAllowed = effectAllowed;
    620 
    621       var ids = draggedNodes.map(function(node) {
    622         return node.id;
    623       });
    624 
    625       chrome.experimental.bookmarkManager.startDrag(ids);
    626     }
    627   },
    628 
    629   handleDragEnter: function(e) {
    630     e.preventDefault();
    631   },
    632 
    633   /**
    634    * Calback for the dragover event.
    635    * @param {Event} e The dragover event.
    636    */
    637   handleDragOver: function(e) {
    638     // TODO(arv): This function is way too long. Please refactor it.
    639 
    640     // Allow DND on text inputs.
    641     if (e.target.tagName != 'INPUT') {
    642       // The default operation is to allow dropping links etc to do navigation.
    643       // We never want to do that for the bookmark manager.
    644       e.preventDefault();
    645 
    646       // Set to none. This will get set to something if we can do the drop.
    647       e.dataTransfer.dropEffect = 'none';
    648     }
    649 
    650     if (!this.dragData)
    651       return;
    652 
    653     var overElement = this.getBookmarkElement(e.target);
    654     if (!overElement && e.target == list)
    655       overElement = list;
    656 
    657     if (!overElement)
    658       return;
    659 
    660     var overBookmarkNode = overElement.bookmarkNode;
    661 
    662     if (!this.canDrop(overBookmarkNode, overElement))
    663       return;
    664 
    665     var bookmarkNode = overElement.bookmarkNode;
    666 
    667     var canDropAbove = this.canDropAbove(overBookmarkNode, overElement);
    668     var canDropOn = this.canDropOn(overBookmarkNode, overElement);
    669     var canDropBelow = this.canDropBelow(overBookmarkNode, overElement);
    670 
    671     if (!canDropAbove && !canDropOn && !canDropBelow)
    672       return;
    673 
    674     // Now we know that we can drop. Determine if we will drop above, on or
    675     // below based on mouse position etc.
    676 
    677     var dropPos;
    678 
    679     e.dataTransfer.dropEffect = this.dragData.sameProfile ? 'move' : 'copy';
    680 
    681     var rect;
    682     if (overElement instanceof TreeItem) {
    683       // We only want the rect of the row representing the item and not
    684       // its children
    685       rect = overElement.rowElement.getBoundingClientRect();
    686     } else {
    687       rect = overElement.getBoundingClientRect();
    688     }
    689 
    690     var dy = e.clientY - rect.top;
    691     var yRatio = dy / rect.height;
    692 
    693     //  above
    694     if (canDropAbove &&
    695         (yRatio <= .25 || yRatio <= .5 && !(canDropBelow && canDropOn))) {
    696       dropPos = 'above';
    697 
    698     // below
    699     } else if (canDropBelow &&
    700                (yRatio > .75 || yRatio > .5 && !(canDropAbove && canDropOn))) {
    701       dropPos = 'below';
    702 
    703     // on
    704     } else if (canDropOn) {
    705       dropPos = 'on';
    706 
    707     // none
    708     } else {
    709       // No drop can happen. Exit now.
    710       e.dataTransfer.dropEffect = 'none';
    711       return;
    712     }
    713 
    714     function cloneClientRect(rect) {
    715       var newRect = {};
    716       for (var key in rect) {
    717         newRect[key] = rect[key];
    718       }
    719       return newRect;
    720     }
    721 
    722     // If we are dropping above or below a tree item adjust the width so
    723     // that it is clearer where the item will be dropped.
    724     if ((dropPos == 'above' || dropPos == 'below') &&
    725         overElement instanceof TreeItem) {
    726       // ClientRect is read only so clone in into a read-write object.
    727       rect = cloneClientRect(rect);
    728       var rtl = getComputedStyle(overElement).direction == 'rtl';
    729       var labelElement = overElement.labelElement;
    730       var labelRect = labelElement.getBoundingClientRect();
    731       if (rtl) {
    732         rect.width = labelRect.left + labelRect.width - rect.left;
    733       } else {
    734         rect.left = labelRect.left;
    735         rect.width -= rect.left
    736       }
    737     }
    738 
    739     var overlayType = dropPos;
    740 
    741     // If we are dropping on a list we want to show a overlay drop line after
    742     // the last element
    743     if (overElement instanceof BookmarkList) {
    744       overlayType = 'below';
    745 
    746       // Get the rect of the last list item.
    747       var length = overElement.dataModel.length;
    748       if (length) {
    749         dropPos = 'below';
    750         overElement = overElement.getListItemByIndex(length - 1);
    751         rect = overElement.getBoundingClientRect();
    752       } else {
    753         // If there are no items, collapse the height of the rect
    754         rect = cloneClientRect(rect);
    755         rect.height = 0;
    756         // We do not use bottom so we don't care to adjust it.
    757       }
    758     }
    759 
    760     this.showDropOverlay_(rect, overlayType);
    761 
    762     this.dropDestination = {
    763       dropPos: dropPos,
    764       relatedNode: overElement.bookmarkNode
    765     };
    766   },
    767 
    768   /**
    769    * Shows and positions the drop marker overlay.
    770    * @param {ClientRect} targetRect The drop target rect
    771    * @param {string} overlayType The position relative to the target rect.
    772    * @private
    773    */
    774   showDropOverlay_: function(targetRect, overlayType) {
    775     window.clearTimeout(this.hideDropOverlayTimer_);
    776     var overlay = $('drop-overlay');
    777     if (overlayType == 'on') {
    778       overlay.className = '';
    779       overlay.style.top = targetRect.top + 'px';
    780       overlay.style.height = targetRect.height + 'px';
    781     } else {
    782       overlay.className = 'line';
    783       overlay.style.height = '';
    784     }
    785     overlay.style.width = targetRect.width + 'px';
    786     overlay.style.left = targetRect.left + 'px';
    787     overlay.style.display = 'block';
    788 
    789     if (overlayType != 'on') {
    790       var overlayRect = overlay.getBoundingClientRect();
    791       if (overlayType == 'above') {
    792         overlay.style.top = targetRect.top - overlayRect.height / 2 + 'px';
    793       } else {
    794         overlay.style.top = targetRect.top + targetRect.height -
    795             overlayRect.height / 2 + 'px';
    796       }
    797     }
    798   },
    799 
    800   /**
    801    * Hides the drop overlay element.
    802    * @private
    803    */
    804   hideDropOverlay_: function() {
    805     // Hide the overlay in a timeout to reduce flickering as we move between
    806     // valid drop targets.
    807     window.clearTimeout(this.hideDropOverlayTimer_);
    808     this.hideDropOverlayTimer_ = window.setTimeout(function() {
    809       $('drop-overlay').style.display = '';
    810     }, 100);
    811   },
    812 
    813   handleDragLeave: function(e) {
    814     this.hideDropOverlay_();
    815   },
    816 
    817   handleDrop: function(e) {
    818     if (this.dropDestination && this.dragData) {
    819       var dropPos = this.dropDestination.dropPos;
    820       var relatedNode = this.dropDestination.relatedNode;
    821       var parentId = dropPos == 'on' ? relatedNode.id : relatedNode.parentId;
    822 
    823       var selectTarget;
    824       var selectedTreeId;
    825       var index;
    826       var relatedIndex;
    827       // Try to find the index in the dataModel so we don't have to always keep
    828       // the index for the list items up to date.
    829       var overElement = this.getBookmarkElement(e.target);
    830       if (overElement instanceof ListItem) {
    831         relatedIndex = overElement.parentNode.dataModel.indexOf(relatedNode);
    832         selectTarget = list;
    833       } else if (overElement instanceof BookmarkList) {
    834         relatedIndex = overElement.dataModel.length - 1;
    835         selectTarget = list;
    836       } else {
    837         // Tree
    838         relatedIndex = relatedNode.index;
    839         selectTarget = tree;
    840         selectedTreeId =
    841             tree.selectedItem ? tree.selectedItem.bookmarkId : null;
    842       }
    843 
    844       if (dropPos == 'above')
    845         index = relatedIndex;
    846       else if (dropPos == 'below')
    847         index = relatedIndex + 1;
    848 
    849       selectItemsAfterUserAction(selectTarget, selectedTreeId);
    850 
    851       if (index != undefined && index != -1)
    852         chrome.experimental.bookmarkManager.drop(parentId, index);
    853       else
    854         chrome.experimental.bookmarkManager.drop(parentId);
    855 
    856       // TODO(arv): Select the newly dropped items.
    857     }
    858     this.dropDestination = null;
    859     this.hideDropOverlay_();
    860   },
    861 
    862   clearDragData: function() {
    863     this.dragData = null;
    864   },
    865 
    866   handleChromeDragEnter: function(dragData) {
    867     this.dragData = dragData;
    868   },
    869 
    870   init: function() {
    871     var boundClearData = this.clearDragData.bind(this);
    872     function deferredClearData() {
    873       setTimeout(boundClearData);
    874     }
    875 
    876     document.addEventListener('dragstart', this.handleDragStart.bind(this));
    877     document.addEventListener('dragenter', this.handleDragEnter.bind(this));
    878     document.addEventListener('dragover', this.handleDragOver.bind(this));
    879     document.addEventListener('dragleave', this.handleDragLeave.bind(this));
    880     document.addEventListener('drop', this.handleDrop.bind(this));
    881     document.addEventListener('dragend', deferredClearData);
    882     document.addEventListener('mouseup', deferredClearData);
    883 
    884     chrome.experimental.bookmarkManager.onDragEnter.addListener(
    885         this.handleChromeDragEnter.bind(this));
    886     chrome.experimental.bookmarkManager.onDragLeave.addListener(
    887         deferredClearData);
    888     chrome.experimental.bookmarkManager.onDrop.addListener(deferredClearData);
    889   }
    890 };
    891 
    892 dnd.init();
    893 
    894 // Commands
    895 
    896 cr.ui.decorate('menu', Menu);
    897 cr.ui.decorate('button[menu]', MenuButton);
    898 cr.ui.decorate('command', Command);
    899 
    900 cr.ui.contextMenuHandler.addContextMenuProperty(tree);
    901 list.contextMenu = $('context-menu');
    902 tree.contextMenu = $('context-menu');
    903 
    904 // Disable almost all commands at startup.
    905 var commands = document.querySelectorAll('command');
    906 for (var i = 0, command; command = commands[i]; i++) {
    907   if (command.id != 'import-menu-command' &&
    908       command.id != 'export-menu-command') {
    909     command.disabled = true;
    910   }
    911 }
    912 
    913 /**
    914  * Helper function that updates the canExecute and labels for the open like
    915  * commands.
    916  * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
    917  * @param {!cr.ui.Command} command The command we are currently precessing.
    918  */
    919 function updateOpenCommands(e, command) {
    920   var selectedItem = e.target.selectedItem;
    921   var selectionCount;
    922   if (e.target == tree) {
    923     selectionCount = selectedItem ? 1 : 0;
    924     selectedItem = selectedItem.bookmarkNode;
    925   } else {
    926     selectionCount = e.target.selectedItems.length;
    927   }
    928 
    929   var isFolder = selectionCount == 1 &&
    930                  selectedItem &&
    931                  bmm.isFolder(selectedItem);
    932   var multiple = selectionCount != 1 || isFolder;
    933 
    934   function hasBookmarks(node) {
    935     for (var i = 0; i < node.children.length; i++) {
    936       if (!bmm.isFolder(node.children[i]))
    937         return true;
    938     }
    939     return false;
    940   }
    941 
    942   switch (command.id) {
    943     case 'open-in-new-tab-command':
    944       command.label = localStrings.getString(multiple ?
    945           'open_all' : 'open_in_new_tab');
    946       break;
    947 
    948     case 'open-in-new-window-command':
    949       command.label = localStrings.getString(multiple ?
    950           'open_all_new_window' : 'open_in_new_window');
    951       break;
    952     case 'open-incognito-window-command':
    953       command.label = localStrings.getString(multiple ?
    954           'open_all_incognito' : 'open_incognito');
    955       break;
    956   }
    957   e.canExecute = selectionCount > 0 && !!selectedItem;
    958   if (isFolder && e.canExecute) {
    959     // We need to get all the bookmark items in this tree. If the tree does not
    960     // contain any non-folders we need to disable the command.
    961     var p = bmm.loadSubtree(selectedItem.id);
    962     p.addListener(function(node) {
    963       command.disabled = !node || !hasBookmarks(node);
    964     });
    965   }
    966 }
    967 
    968 /**
    969  * Calls the backend to figure out if we can paste the clipboard into the active
    970  * folder.
    971  * @param {Function=} opt_f Function to call after the state has been
    972  *     updated.
    973  */
    974 function updatePasteCommand(opt_f) {
    975   function update(canPaste) {
    976     var command = $('paste-command');
    977     command.disabled = !canPaste;
    978     if (opt_f)
    979       opt_f();
    980   }
    981   // We cannot paste into search and recent view.
    982   if (list.isSearch() || list.isRecent()) {
    983     update(false);
    984   } else {
    985     chrome.experimental.bookmarkManager.canPaste(list.parentId, update);
    986   }
    987 }
    988 
    989 // We can always execute the import-menu and export-menu commands.
    990 document.addEventListener('canExecute', function(e) {
    991   var command = e.command;
    992   var commandId = command.id;
    993   if (commandId == 'import-menu-command' ||
    994       commandId == 'export-menu-command') {
    995     e.canExecute = true;
    996   }
    997 });
    998 
    999 /**
   1000  * Helper function for handling canExecute for the list and the tree.
   1001  * @param {!Event} e Can exectue event object.
   1002  * @param {boolean} isRecentOrSearch Whether the user is trying to do a command
   1003  *     on recent or search.
   1004  */
   1005 function canExcuteShared(e, isRecentOrSearch) {
   1006   var command = e.command;
   1007   var commandId = command.id;
   1008   switch (commandId) {
   1009     case 'paste-command':
   1010       updatePasteCommand();
   1011       break;
   1012 
   1013     case 'sort-command':
   1014       if (isRecentOrSearch) {
   1015         e.canExecute = false;
   1016       } else {
   1017         e.canExecute = list.dataModel.length > 0;
   1018 
   1019         // The list might be loading so listen to the load event.
   1020         var f = function() {
   1021           list.removeEventListener('load', f);
   1022           command.disabled = list.dataModel.length == 0;
   1023         };
   1024         list.addEventListener('load', f);
   1025       }
   1026       break;
   1027 
   1028     case 'add-new-bookmark-command':
   1029     case 'new-folder-command':
   1030       e.canExecute = !isRecentOrSearch;
   1031       break;
   1032 
   1033     case 'open-in-new-tab-command':
   1034     case 'open-in-background-tab-command':
   1035     case 'open-in-new-window-command':
   1036     case 'open-incognito-window-command':
   1037       updateOpenCommands(e, command);
   1038       break;
   1039   }
   1040 }
   1041 
   1042 // Update canExecute for the commands when the list is the active element.
   1043 list.addEventListener('canExecute', function(e) {
   1044   if (e.target != list) return;
   1045 
   1046   var command = e.command;
   1047   var commandId = command.id;
   1048 
   1049   function hasSelected() {
   1050     return !!e.target.selectedItem;
   1051   }
   1052 
   1053   function hasSingleSelected() {
   1054     return e.target.selectedItems.length == 1;
   1055   }
   1056 
   1057   function isRecentOrSearch() {
   1058     return list.isRecent() || list.isSearch();
   1059   }
   1060 
   1061   switch (commandId) {
   1062     case 'rename-folder-command':
   1063       // Show rename if a single folder is selected
   1064       var items = e.target.selectedItems;
   1065       if (items.length != 1) {
   1066         e.canExecute = false;
   1067         command.hidden = true;
   1068       } else {
   1069         var isFolder = bmm.isFolder(items[0]);
   1070         e.canExecute = isFolder;
   1071         command.hidden = !isFolder;
   1072       }
   1073       break;
   1074 
   1075     case 'edit-command':
   1076       // Show the edit command if not a folder
   1077       var items = e.target.selectedItems;
   1078       if (items.length != 1) {
   1079         e.canExecute = false;
   1080         command.hidden = false;
   1081       } else {
   1082         var isFolder = bmm.isFolder(items[0]);
   1083         e.canExecute = !isFolder;
   1084         command.hidden = isFolder;
   1085       }
   1086       break;
   1087 
   1088     case 'show-in-folder-command':
   1089       e.canExecute = isRecentOrSearch() && hasSingleSelected();
   1090       break;
   1091 
   1092     case 'delete-command':
   1093     case 'cut-command':
   1094     case 'copy-command':
   1095       e.canExecute = hasSelected();
   1096       break;
   1097 
   1098     case 'open-in-same-window-command':
   1099       e.canExecute = hasSelected();
   1100       break;
   1101 
   1102     default:
   1103       canExcuteShared(e, isRecentOrSearch());
   1104   }
   1105 });
   1106 
   1107 // Update canExecute for the commands when the tree is the active element.
   1108 tree.addEventListener('canExecute', function(e) {
   1109   if (e.target != tree) return;
   1110 
   1111   var command = e.command;
   1112   var commandId = command.id;
   1113 
   1114   function hasSelected() {
   1115     return !!e.target.selectedItem;
   1116   }
   1117 
   1118   function isRecentOrSearch() {
   1119     var item = e.target.selectedItem;
   1120     return item == recentTreeItem || item == searchTreeItem;
   1121   }
   1122 
   1123   function isTopLevelItem() {
   1124     return e.target.selectedItem.parentNode == tree;
   1125   }
   1126 
   1127   switch (commandId) {
   1128     case 'rename-folder-command':
   1129       command.hidden = false;
   1130       e.canExecute = hasSelected() && !isTopLevelItem();
   1131       break;
   1132 
   1133     case 'edit-command':
   1134       command.hidden = true;
   1135       e.canExecute = false;
   1136       break;
   1137 
   1138     case 'delete-command':
   1139     case 'cut-command':
   1140     case 'copy-command':
   1141       e.canExecute = hasSelected() && !isTopLevelItem();
   1142       break;
   1143 
   1144     default:
   1145       canExcuteShared(e, isRecentOrSearch());
   1146   }
   1147 });
   1148 
   1149 /**
   1150  * Update the canExecute state of the commands when the selection changes.
   1151  * @param {Event} e The change event object.
   1152  */
   1153 function updateCommandsBasedOnSelection(e) {
   1154   if (e.target == document.activeElement) {
   1155     // Paste only needs to updated when the tree selection changes.
   1156     var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
   1157         'add-new-bookmark', 'new-folder', 'open-in-new-tab',
   1158         'open-in-new-window', 'open-incognito-window', 'open-in-same-window'];
   1159 
   1160     if (e.target == tree) {
   1161       commandNames.push('paste', 'show-in-folder', 'sort');
   1162     }
   1163 
   1164     commandNames.forEach(function(baseId) {
   1165       $(baseId + '-command').canExecuteChange();
   1166     });
   1167   }
   1168 }
   1169 
   1170 list.addEventListener('change', updateCommandsBasedOnSelection);
   1171 tree.addEventListener('change', updateCommandsBasedOnSelection);
   1172 
   1173 document.addEventListener('command', function(e) {
   1174   var command = e.command;
   1175   var commandId = command.id;
   1176   console.log(command.id, 'executed', 'on', e.target);
   1177   if (commandId == 'import-menu-command') {
   1178     // Set a flag on the list so we can select the newly imported folder.
   1179     list.selectImportedFolder = true;
   1180     chrome.bookmarks.import();
   1181   } else if (command.id == 'export-menu-command') {
   1182     chrome.bookmarks.export();
   1183   }
   1184 });
   1185 
   1186 function handleRename(e) {
   1187   var item = e.target;
   1188   var bookmarkNode = item.bookmarkNode;
   1189   chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
   1190 }
   1191 
   1192 tree.addEventListener('rename', handleRename);
   1193 list.addEventListener('rename', handleRename);
   1194 
   1195 list.addEventListener('edit', function(e) {
   1196   var item = e.target;
   1197   var bookmarkNode = item.bookmarkNode;
   1198   var context = {
   1199     title: bookmarkNode.title
   1200   };
   1201   if (!bmm.isFolder(bookmarkNode))
   1202     context.url = bookmarkNode.url;
   1203 
   1204   if (bookmarkNode.id == 'new') {
   1205     selectItemsAfterUserAction(list);
   1206 
   1207     // New page
   1208     context.parentId = bookmarkNode.parentId;
   1209     chrome.bookmarks.create(context, function(node) {
   1210       // A new node was created and will get added to the list due to the
   1211       // handler.
   1212       var dataModel = list.dataModel;
   1213       var index = dataModel.indexOf(bookmarkNode);
   1214       dataModel.splice(index, 1);
   1215 
   1216       // Select new item.
   1217       var newIndex = dataModel.findIndexById(node.id);
   1218       if (newIndex != -1) {
   1219         var sm = list.selectionModel;
   1220         list.scrollIndexIntoView(newIndex);
   1221         sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
   1222       }
   1223     });
   1224   } else {
   1225     // Edit
   1226     chrome.bookmarks.update(bookmarkNode.id, context);
   1227   }
   1228 });
   1229 
   1230 list.addEventListener('canceledit', function(e) {
   1231   var item = e.target;
   1232   var bookmarkNode = item.bookmarkNode;
   1233   if (bookmarkNode.id == 'new') {
   1234     var dataModel = list.dataModel;
   1235     var index = dataModel.findIndexById('new');
   1236     dataModel.splice(index, 1);
   1237   }
   1238 });
   1239 
   1240 /**
   1241  * Navigates to the folder that the selected item is in and selects it. This is
   1242  * used for the show-in-folder command.
   1243  */
   1244 function showInFolder() {
   1245   var bookmarkNode = list.selectedItem;
   1246   var parentId = bookmarkNode.parentId;
   1247 
   1248   // After the list is loaded we should select the revealed item.
   1249   function f(e) {
   1250     var index;
   1251     if (bookmarkNode &&
   1252         (index = list.dataModel.findIndexById(bookmarkNode.id)) != -1) {
   1253       var sm = list.selectionModel;
   1254       sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
   1255       list.scrollIndexIntoView(index);
   1256     }
   1257     list.removeEventListener('load', f);
   1258   }
   1259   list.addEventListener('load', f);
   1260   var treeItem = bmm.treeLookup[parentId];
   1261   treeItem.reveal();
   1262 
   1263   navigateTo(parentId);
   1264 }
   1265 
   1266 var linkController;
   1267 
   1268 /**
   1269  * @return {!cr.LinkController} The link controller used to open links based on
   1270  *     user clicks and keyboard actions.
   1271  */
   1272 function getLinkController() {
   1273   return linkController ||
   1274       (linkController = new cr.LinkController(localStrings));
   1275 }
   1276 
   1277 /**
   1278  * Returns the selected bookmark nodes of the active element. Only call this
   1279  * if the list or the tree is focused.
   1280  * @return {!Array} Array of bookmark nodes.
   1281  */
   1282 function getSelectedBookmarkNodes() {
   1283   if (document.activeElement == list) {
   1284     return list.selectedItems;
   1285   } else if (document.activeElement == tree) {
   1286     return [tree.selectedItem.bookmarkNode];
   1287   } else {
   1288     throw Error('getSelectedBookmarkNodes called when wrong element focused.');
   1289   }
   1290 }
   1291 
   1292 /**
   1293  * @return {!Array.<string>} An array of the selected bookmark IDs.
   1294  */
   1295 function getSelectedBookmarkIds() {
   1296   return getSelectedBookmarkNodes().map(function(node) {
   1297     return node.id;
   1298   });
   1299 }
   1300 
   1301 /**
   1302  * Opens the selected bookmarks.
   1303  * @param {LinkKind} kind The kind of link we want to open.
   1304  */
   1305 function openBookmarks(kind) {
   1306   // If we have selected any folders we need to find all items recursively.
   1307   // We use multiple async calls to getSubtree instead of getting the whole
   1308   // tree since we would like to minimize the amount of data sent.
   1309 
   1310   var urls = [];
   1311 
   1312   // Adds the node and all its children.
   1313   function addNodes(node) {
   1314     if (node.children) {
   1315       node.children.forEach(function(child) {
   1316         if (!bmm.isFolder(child))
   1317           urls.push(child.url);
   1318       });
   1319     } else {
   1320       urls.push(node.url);
   1321     }
   1322   }
   1323 
   1324   var nodes = getSelectedBookmarkNodes();
   1325 
   1326   // Get a future promise for every selected item.
   1327   var promises = nodes.map(function(node) {
   1328     if (bmm.isFolder(node))
   1329       return bmm.loadSubtree(node.id);
   1330     // Not a folder so we already have all the data we need.
   1331     return new Promise(node.url);
   1332   });
   1333 
   1334   var p = Promise.all.apply(null, promises);
   1335   p.addListener(function(values) {
   1336     values.forEach(function(v) {
   1337       if (typeof v == 'string')
   1338         urls.push(v);
   1339       else
   1340         addNodes(v);
   1341     });
   1342     getLinkController().openUrls(urls, kind);
   1343   });
   1344 }
   1345 
   1346 /**
   1347  * Opens an item in the list.
   1348  */
   1349 function openItem() {
   1350   var bookmarkNodes = getSelectedBookmarkNodes();
   1351   // If we double clicked or pressed enter on a single folder navigate to it.
   1352   if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) {
   1353     navigateTo(bookmarkNodes[0].id);
   1354   } else {
   1355     openBookmarks(LinkKind.FOREGROUND_TAB);
   1356   }
   1357 }
   1358 
   1359 /**
   1360  * Deletes the selected bookmarks.
   1361  */
   1362 function deleteBookmarks() {
   1363   getSelectedBookmarkIds().forEach(function(id) {
   1364     chrome.bookmarks.removeTree(id);
   1365   });
   1366 }
   1367 
   1368 /**
   1369  * Callback for the new folder command. This creates a new folder and starts
   1370  * a rename of it.
   1371  */
   1372 function newFolder() {
   1373   var parentId = list.parentId;
   1374   var isTree = document.activeElement == tree;
   1375   chrome.bookmarks.create({
   1376     title: localStrings.getString('new_folder_name'),
   1377     parentId: parentId
   1378   }, function(newNode) {
   1379     // This callback happens before the event that triggers the tree/list to
   1380     // get updated so delay the work so that the tree/list gets updated first.
   1381     setTimeout(function() {
   1382       var newItem;
   1383       if (isTree) {
   1384         newItem = bmm.treeLookup[newNode.id];
   1385         tree.selectedItem = newItem;
   1386         newItem.editing = true;
   1387       } else {
   1388         var index = list.dataModel.findIndexById(newNode.id);
   1389         var sm = list.selectionModel;
   1390         sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
   1391         scrollIntoViewAndMakeEditable(index);
   1392       }
   1393     }, 50);
   1394   });
   1395 }
   1396 
   1397 /**
   1398  * Scrolls the list item into view and makes it editable.
   1399  * @param {number} index The index of the item to make editable.
   1400  */
   1401 function scrollIntoViewAndMakeEditable(index) {
   1402   list.scrollIndexIntoView(index);
   1403   // onscroll is now dispatched asynchronously so we have to postpone
   1404   // the rest.
   1405   setTimeout(function() {
   1406     var item = list.getListItemByIndex(index);
   1407     if (item)
   1408       item.editing = true;
   1409   });
   1410 }
   1411 
   1412 /**
   1413  * Adds a page to the current folder. This is called by the
   1414  * add-new-bookmark-command handler.
   1415  */
   1416 function addPage() {
   1417   var parentId = list.parentId;
   1418   var fakeNode = {
   1419     title: '',
   1420     url: '',
   1421     parentId: parentId,
   1422     id: 'new'
   1423   };
   1424 
   1425   var dataModel = list.dataModel;
   1426   var length = dataModel.length;
   1427   dataModel.splice(length, 0, fakeNode);
   1428   var sm = list.selectionModel;
   1429   sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
   1430   scrollIntoViewAndMakeEditable(length);
   1431 }
   1432 
   1433 /**
   1434  * This function is used to select items after a user action such as paste, drop
   1435  * add page etc.
   1436  * @param {BookmarkList|BookmarkTree} target The target of the user action.
   1437  * @param {=string} opt_selectedTreeId If provided, then select that tree id.
   1438  */
   1439 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
   1440   // We get one onCreated event per item so we delay the handling until we got
   1441   // no more events coming.
   1442 
   1443   var ids = [];
   1444   var timer;
   1445 
   1446   function handle(id, bookmarkNode) {
   1447     clearTimeout(timer);
   1448     if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
   1449       ids.push(id);
   1450     timer = setTimeout(handleTimeout, 50);
   1451   }
   1452 
   1453   function handleTimeout() {
   1454     chrome.bookmarks.onCreated.removeListener(handle);
   1455     chrome.bookmarks.onMoved.removeListener(handle);
   1456 
   1457     if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
   1458       var index = ids.indexOf(opt_selectedTreeId);
   1459       if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
   1460         tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
   1461       }
   1462     } else if (target == list) {
   1463       var dataModel = list.dataModel;
   1464       var firstIndex = dataModel.findIndexById(ids[0]);
   1465       var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
   1466       if (firstIndex != -1 && lastIndex != -1) {
   1467         var selectionModel = list.selectionModel;
   1468         selectionModel.selectedIndex = -1;
   1469         selectionModel.selectRange(firstIndex, lastIndex);
   1470         selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
   1471         list.focus();
   1472       }
   1473     }
   1474 
   1475     list.endBatchUpdates();
   1476   }
   1477 
   1478   list.startBatchUpdates();
   1479 
   1480   chrome.bookmarks.onCreated.addListener(handle);
   1481   chrome.bookmarks.onMoved.addListener(handle);
   1482   timer = setTimeout(handleTimeout, 300);
   1483 }
   1484 
   1485 /**
   1486  * Handler for the command event. This is used both for the tree and the list.
   1487  * @param {!Event} e The event object.
   1488  */
   1489 function handleCommand(e) {
   1490   var command = e.command;
   1491   var commandId = command.id;
   1492   switch (commandId) {
   1493     case 'show-in-folder-command':
   1494       showInFolder();
   1495       break;
   1496     case 'open-in-new-tab-command':
   1497       openBookmarks(LinkKind.FOREGROUND_TAB);
   1498       break;
   1499     case 'open-in-background-tab-command':
   1500       openBookmarks(LinkKind.BACKGROUND_TAB);
   1501       break;
   1502     case 'open-in-new-window-command':
   1503       openBookmarks(LinkKind.WINDOW);
   1504       break;
   1505     case 'open-incognito-window-command':
   1506       openBookmarks(LinkKind.INCOGNITO);
   1507       break;
   1508     case 'delete-command':
   1509       deleteBookmarks();
   1510       break;
   1511     case 'copy-command':
   1512       chrome.experimental.bookmarkManager.copy(getSelectedBookmarkIds(),
   1513                                                updatePasteCommand);
   1514       break;
   1515     case 'cut-command':
   1516       chrome.experimental.bookmarkManager.cut(getSelectedBookmarkIds(),
   1517                                               updatePasteCommand);
   1518       break;
   1519     case 'paste-command':
   1520       selectItemsAfterUserAction(list);
   1521       chrome.experimental.bookmarkManager.paste(list.parentId,
   1522                                                 getSelectedBookmarkIds());
   1523       break;
   1524     case 'sort-command':
   1525       chrome.experimental.bookmarkManager.sortChildren(list.parentId);
   1526       break;
   1527     case 'rename-folder-command':
   1528     case 'edit-command':
   1529       if (document.activeElement == list) {
   1530         var li = list.getListItem(list.selectedItem);
   1531         if (li)
   1532           li.editing = true;
   1533       } else {
   1534         document.activeElement.selectedItem.editing = true;
   1535       }
   1536       break;
   1537     case 'new-folder-command':
   1538       newFolder();
   1539       break;
   1540     case 'add-new-bookmark-command':
   1541       addPage();
   1542       break;
   1543     case 'open-in-same-window-command':
   1544       openItem();
   1545       break;
   1546   }
   1547 }
   1548 
   1549 // Delete on all platforms. On Mac we also allow Meta+Backspace.
   1550 $('delete-command').shortcut = 'U+007F' +
   1551                                (cr.isMac ? ' U+0008 Meta-U+0008' : '');
   1552 
   1553 $('open-in-same-window-command').shortcut = cr.isMac ? 'Meta-Down' :
   1554                                                        'Enter';
   1555 
   1556 $('open-in-new-window-command').shortcut = 'Shift-Enter';
   1557 $('open-in-background-tab-command').shortcut = cr.isMac ? 'Meta-Enter' :
   1558                                                           'Ctrl-Enter';
   1559 $('open-in-new-tab-command').shortcut = cr.isMac ? 'Shift-Meta-Enter' :
   1560                                                    'Shift-Ctrl-Enter';
   1561 
   1562 $('rename-folder-command').shortcut = $('edit-command').shortcut =
   1563     cr.isMac ? 'Enter' : 'F2';
   1564 
   1565 list.addEventListener('command', handleCommand);
   1566 tree.addEventListener('command', handleCommand);
   1567 
   1568 // Execute the copy, cut and paste commands when those events are dispatched by
   1569 // the browser. This allows us to rely on the browser to handle the keyboard
   1570 // shortcuts for these commands.
   1571 (function() {
   1572   function handle(id) {
   1573     return function(e) {
   1574       var command = $(id);
   1575       if (!command.disabled) {
   1576         command.execute();
   1577         if (e) e.preventDefault(); // Prevent the system beep
   1578       }
   1579     };
   1580   }
   1581 
   1582   // Listen to copy, cut and paste events and execute the associated commands.
   1583   document.addEventListener('copy', handle('copy-command'));
   1584   document.addEventListener('cut', handle('cut-command'));
   1585 
   1586   var pasteHandler = handle('paste-command');
   1587   document.addEventListener('paste', function(e) {
   1588     // Paste is a bit special since we need to do an async call to see if we can
   1589     // paste because the paste command might not be up to date.
   1590     updatePasteCommand(pasteHandler);
   1591   });
   1592 })();
   1593