Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2012 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 (function() {
      6 /** @const */ var BookmarkList = bmm.BookmarkList;
      7 /** @const */ var BookmarkTree = bmm.BookmarkTree;
      8 /** @const */ var Command = cr.ui.Command;
      9 /** @const */ var CommandBinding = cr.ui.CommandBinding;
     10 /** @const */ var LinkKind = cr.LinkKind;
     11 /** @const */ var ListItem = cr.ui.ListItem;
     12 /** @const */ var Menu = cr.ui.Menu;
     13 /** @const */ var MenuButton = cr.ui.MenuButton;
     14 /** @const */ var Splitter = cr.ui.Splitter;
     15 /** @const */ var TreeItem = cr.ui.TreeItem;
     16 
     17 /**
     18  * An array containing the BookmarkTreeNodes that were deleted in the last
     19  * deletion action. This is used for implementing undo.
     20  * @type {Array.<BookmarkTreeNode>}
     21  */
     22 var lastDeletedNodes;
     23 
     24 /**
     25  *
     26  * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
     27  * view. Zero means pointer doesn't hover on folder.
     28  * @type {number}
     29  */
     30 var lastHoverOnFolderTimeStamp = 0;
     31 
     32 /**
     33  * Holds a function that will undo that last action, if global undo is enabled.
     34  * @type {Function}
     35  */
     36 var performGlobalUndo;
     37 
     38 /**
     39  * Holds a link controller singleton. Use getLinkController() rarther than
     40  * accessing this variabie.
     41  * @type {LinkController}
     42  */
     43 var linkController;
     44 
     45 /**
     46  * New Windows are not allowed in Windows 8 metro mode.
     47  */
     48 var canOpenNewWindows = true;
     49 
     50 /**
     51  * Incognito mode availability can take the following values: ,
     52  *   - 'enabled' for when both normal and incognito modes are available;
     53  *   - 'disabled' for when incognito mode is disabled;
     54  *   - 'forced' for when incognito mode is forced (normal mode is unavailable).
     55  */
     56 var incognitoModeAvailability = 'enabled';
     57 
     58 /**
     59  * Whether bookmarks can be modified.
     60  * @type {boolean}
     61  */
     62 var canEdit = true;
     63 
     64 /**
     65  * @type {TreeItem}
     66  * @const
     67  */
     68 var searchTreeItem = new TreeItem({
     69   bookmarkId: 'q='
     70 });
     71 
     72 /**
     73  * Command shortcut mapping.
     74  * @const
     75  */
     76 var commandShortcutMap = cr.isMac ? {
     77   'edit': 'Enter',
     78   // On Mac we also allow Meta+Backspace.
     79   'delete': 'U+007F  U+0008 Meta-U+0008',
     80   'open-in-background-tab': 'Meta-Enter',
     81   'open-in-new-tab': 'Shift-Meta-Enter',
     82   'open-in-same-window': 'Meta-Down',
     83   'open-in-new-window': 'Shift-Enter',
     84   'rename-folder': 'Enter',
     85   // Global undo is Command-Z. It is not in any menu.
     86   'undo': 'Meta-U+005A',
     87 } : {
     88   'edit': 'F2',
     89   'delete': 'U+007F',
     90   'open-in-background-tab': 'Ctrl-Enter',
     91   'open-in-new-tab': 'Shift-Ctrl-Enter',
     92   'open-in-same-window': 'Enter',
     93   'open-in-new-window': 'Shift-Enter',
     94   'rename-folder': 'F2',
     95   // Global undo is Ctrl-Z. It is not in any menu.
     96   'undo': 'Ctrl-U+005A',
     97 };
     98 
     99 /**
    100  * Mapping for folder id to suffix of UMA. These names will be appeared
    101  * after "BookmarkManager_NavigateTo_" in UMA dashboard.
    102  * @const
    103  */
    104 var folderMetricsNameMap = {
    105   '1': 'BookmarkBar',
    106   '2': 'Other',
    107   '3': 'Mobile',
    108   'q=': 'Search',
    109   'subfolder': 'SubFolder',
    110 };
    111 
    112 /**
    113  * Adds an event listener to a node that will remove itself after firing once.
    114  * @param {!Element} node The DOM node to add the listener to.
    115  * @param {string} name The name of the event listener to add to.
    116  * @param {function(Event)} handler Function called when the event fires.
    117  */
    118 function addOneShotEventListener(node, name, handler) {
    119   var f = function(e) {
    120     handler(e);
    121     node.removeEventListener(name, f);
    122   };
    123   node.addEventListener(name, f);
    124 }
    125 
    126 // Get the localized strings from the backend via bookmakrManagerPrivate API.
    127 function loadLocalizedStrings(data) {
    128   // The strings may contain & which we need to strip.
    129   for (var key in data) {
    130     data[key] = data[key].replace(/&/, '');
    131   }
    132 
    133   loadTimeData.data = data;
    134   i18nTemplate.process(document, loadTimeData);
    135 
    136   searchTreeItem.label = loadTimeData.getString('search');
    137   searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' :
    138                                   'images/bookmark_manager_search.png';
    139 }
    140 
    141 /**
    142  * Updates the location hash to reflect the current state of the application.
    143  */
    144 function updateHash() {
    145   window.location.hash = tree.selectedItem.bookmarkId;
    146   updateAllCommands();
    147 }
    148 
    149 /**
    150  * Navigates to a bookmark ID.
    151  * @param {string} id The ID to navigate to.
    152  * @param {function()=} opt_callback Function called when list view loaded or
    153  *     displayed specified folder.
    154  */
    155 function navigateTo(id, opt_callback) {
    156   window.location.hash = id;
    157   updateAllCommands();
    158 
    159   var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
    160                   folderMetricsNameMap['subfolder'];
    161   chrome.metricsPrivate.recordUserAction(
    162       'BookmarkManager_NavigateTo_' + metricsId);
    163 
    164   if (opt_callback) {
    165     if (list.parentId == id)
    166       opt_callback();
    167     else
    168       addOneShotEventListener(list, 'load', opt_callback);
    169   }
    170 }
    171 
    172 /**
    173  * Updates the parent ID of the bookmark list and selects the correct tree item.
    174  * @param {string} id The id.
    175  */
    176 function updateParentId(id) {
    177   // Setting list.parentId fires 'load' event.
    178   list.parentId = id;
    179 
    180   // When tree.selectedItem changed, tree view calls navigatTo() then it
    181   // calls updateHash() when list view displayed specified folder.
    182   tree.selectedItem = bmm.treeLookup[id] || tree.selectedItem;
    183 }
    184 
    185 // Process the location hash. This is called by onhashchange and when the page
    186 // is first loaded.
    187 function processHash() {
    188   var id = window.location.hash.slice(1);
    189   if (!id) {
    190     // If we do not have a hash, select first item in the tree.
    191     id = tree.items[0].bookmarkId;
    192   }
    193 
    194   var valid = false;
    195   if (/^e=/.test(id)) {
    196     id = id.slice(2);
    197 
    198     // If hash contains e=, edit the item specified.
    199     chrome.bookmarks.get(id, function(bookmarkNodes) {
    200       // Verify the node to edit is a valid node.
    201       if (!bookmarkNodes || bookmarkNodes.length != 1)
    202         return;
    203       var bookmarkNode = bookmarkNodes[0];
    204 
    205       // After the list reloads, edit the desired bookmark.
    206       var editBookmark = function(e) {
    207         var index = list.dataModel.findIndexById(bookmarkNode.id);
    208         if (index != -1) {
    209           var sm = list.selectionModel;
    210           sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
    211           scrollIntoViewAndMakeEditable(index);
    212         }
    213       };
    214 
    215       navigateTo(bookmarkNode.parentId, editBookmark);
    216     });
    217 
    218     // We handle the two cases of navigating to the bookmark to be edited
    219     // above. Don't run the standard navigation code below.
    220     return;
    221   } else if (/^q=/.test(id)) {
    222     // In case we got a search hash, update the text input and the
    223     // bmm.treeLookup to use the new id.
    224     setSearch(id.slice(2));
    225     valid = true;
    226   }
    227 
    228   // Navigate to bookmark 'id' (which may be a query of the form q=query).
    229   if (valid) {
    230     updateParentId(id);
    231   } else {
    232     // We need to verify that this is a correct ID.
    233     chrome.bookmarks.get(id, function(items) {
    234       if (items && items.length == 1)
    235         updateParentId(id);
    236     });
    237   }
    238 }
    239 
    240 // Activate is handled by the open-in-same-window-command.
    241 function handleDoubleClickForList(e) {
    242   if (e.button == 0)
    243     $('open-in-same-window-command').execute();
    244 }
    245 
    246 // The list dispatches an event when the user clicks on the URL or the Show in
    247 // folder part.
    248 function handleUrlClickedForList(e) {
    249   getLinkController().openUrlFromEvent(e.url, e.originalEvent);
    250   chrome.bookmarkManagerPrivate.recordLaunch();
    251 }
    252 
    253 function handleSearch(e) {
    254   setSearch(this.value);
    255 }
    256 
    257 /**
    258  * Navigates to the search results for the search text.
    259  * @param {string} searchText The text to search for.
    260  */
    261 function setSearch(searchText) {
    262   if (searchText) {
    263     // Only update search item if we have a search term. We never want the
    264     // search item to be for an empty search.
    265     delete bmm.treeLookup[searchTreeItem.bookmarkId];
    266     var id = searchTreeItem.bookmarkId = 'q=' + searchText;
    267     bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
    268   }
    269 
    270   var input = $('term');
    271   // Do not update the input if the user is actively using the text input.
    272   if (document.activeElement != input)
    273     input.value = searchText;
    274 
    275   if (searchText) {
    276     tree.add(searchTreeItem);
    277     tree.selectedItem = searchTreeItem;
    278   } else {
    279     // Go "home".
    280     tree.selectedItem = tree.items[0];
    281     id = tree.selectedItem.bookmarkId;
    282   }
    283 
    284   navigateTo(id);
    285 }
    286 
    287 /**
    288  * This returns the user visible path to the folder where the bookmark is
    289  * located.
    290  * @param {number} parentId The ID of the parent folder.
    291  * @return {string} The path to the the bookmark,
    292  */
    293 function getFolder(parentId) {
    294   var parentNode = tree.getBookmarkNodeById(parentId);
    295   if (parentNode) {
    296     var s = parentNode.title;
    297     if (parentNode.parentId != bmm.ROOT_ID) {
    298       return getFolder(parentNode.parentId) + '/' + s;
    299     }
    300     return s;
    301   }
    302 }
    303 
    304 function handleLoadForTree(e) {
    305   processHash();
    306 }
    307 
    308 /**
    309  * Returns a promise for all the URLs in the {@code nodes} and the direct
    310  * children of {@code nodes}.
    311  * @param {!Array.<BookmarkTreeNode>} nodes .
    312  * @return {!Promise.<Array.<string>>} .
    313  */
    314 function getAllUrls(nodes) {
    315   var urls = [];
    316 
    317   // Adds the node and all its direct children.
    318   function addNodes(node) {
    319     if (node.id == 'new')
    320       return;
    321 
    322     if (node.children) {
    323       node.children.forEach(function(child) {
    324         if (!bmm.isFolder(child))
    325           urls.push(child.url);
    326       });
    327     } else {
    328       urls.push(node.url);
    329     }
    330   }
    331 
    332   // Get a future promise for the nodes.
    333   var promises = nodes.map(function(node) {
    334     if (bmm.isFolder(node))
    335       return bmm.loadSubtree(node.id);
    336     // Not a folder so we already have all the data we need.
    337     return Promise.resolve(node);
    338   });
    339 
    340   return Promise.all(promises).then(function(nodes) {
    341     nodes.forEach(addNodes);
    342     return urls;
    343   });
    344 }
    345 
    346 /**
    347  * Returns the nodes (non recursive) to use for the open commands.
    348  * @param {HTMLElement} target .
    349  * @return {Array.<BookmarkTreeNode>} .
    350  */
    351 function getNodesForOpen(target) {
    352   if (target == tree) {
    353     if (tree.selectedItem != searchTreeItem)
    354       return tree.selectedFolders;
    355     // Fall through to use all nodes in the list.
    356   } else {
    357     var items = list.selectedItems;
    358     if (items.length)
    359       return items;
    360   }
    361 
    362   // The list starts off with a null dataModel. We can get here during startup.
    363   if (!list.dataModel)
    364     return [];
    365 
    366   // Return an array based on the dataModel.
    367   return list.dataModel.slice();
    368 }
    369 
    370 /**
    371  * Returns a promise that will contain all URLs of all the selected bookmarks
    372  * and the nested bookmarks for use with the open commands.
    373  * @param {HTMLElement} target The target list or tree.
    374  * @return {Promise.<Array.<string>>} .
    375  */
    376 function getUrlsForOpenCommands(target) {
    377   return getAllUrls(getNodesForOpen(target));
    378 }
    379 
    380 function notNewNode(node) {
    381   return node.id != 'new';
    382 }
    383 
    384 /**
    385  * Helper function that updates the canExecute and labels for the open-like
    386  * commands.
    387  * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
    388  * @param {!cr.ui.Command} command The command we are currently processing.
    389  * @param {string} singularId The string id of singular form of the menu label.
    390  * @param {string} pluralId The string id of menu label if the singular form is
    391        not used.
    392  * @param {boolean} commandDisabled Whether the menu item should be disabled
    393        no matter what bookmarks are selected.
    394  */
    395 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
    396   if (singularId) {
    397     // The command label reflects the selection which might not reflect
    398     // how many bookmarks will be opened. For example if you right click an
    399     // empty area in a folder with 1 bookmark the text should still say "all".
    400     var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
    401     var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
    402     command.label = loadTimeData.getString(singular ? singularId : pluralId);
    403   }
    404 
    405   if (commandDisabled) {
    406     command.disabled = true;
    407     e.canExecute = false;
    408     return;
    409   }
    410 
    411   getUrlsForOpenCommands(e.target).then(function(urls) {
    412     var disabled = !urls.length;
    413     command.disabled = disabled;
    414     e.canExecute = !disabled;
    415   });
    416 }
    417 
    418 /**
    419  * Calls the backend to figure out if we can paste the clipboard into the active
    420  * folder.
    421  * @param {Function=} opt_f Function to call after the state has been updated.
    422  */
    423 function updatePasteCommand(opt_f) {
    424   function update(canPaste) {
    425     var organizeMenuCommand = $('paste-from-organize-menu-command');
    426     var contextMenuCommand = $('paste-from-context-menu-command');
    427     organizeMenuCommand.disabled = !canPaste;
    428     contextMenuCommand.disabled = !canPaste;
    429     if (opt_f)
    430       opt_f();
    431   }
    432   // We cannot paste into search view.
    433   if (list.isSearch())
    434     update(false);
    435   else
    436     chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
    437 }
    438 
    439 function handleCanExecuteForDocument(e) {
    440   var command = e.command;
    441   switch (command.id) {
    442     case 'import-menu-command':
    443       e.canExecute = canEdit;
    444       break;
    445     case 'export-menu-command':
    446       // We can always execute the export-menu command.
    447       e.canExecute = true;
    448       break;
    449     case 'sort-command':
    450       e.canExecute = !list.isSearch() &&
    451           list.dataModel && list.dataModel.length > 1 &&
    452           !isUnmodifiable(tree.getBookmarkNodeById(list.parentId));
    453       break;
    454     case 'undo-command':
    455       // If the search box is active, pass the undo command through
    456       // (fixes http://crbug.com/278112). Otherwise, because
    457       // the global undo command has no visible UI, always enable it, and
    458       // just make it a no-op if undo is not possible.
    459       e.canExecute = e.currentTarget.activeElement !== $('term');
    460       break;
    461     default:
    462       canExecuteForList(e);
    463       break;
    464   }
    465 }
    466 
    467 /**
    468  * Helper function for handling canExecute for the list and the tree.
    469  * @param {!Event} e Can execute event object.
    470  * @param {boolean} isSearch Whether the user is trying to do a command on
    471  *     search.
    472  */
    473 function canExecuteShared(e, isSearch) {
    474   var command = e.command;
    475   var commandId = command.id;
    476   switch (commandId) {
    477     case 'paste-from-organize-menu-command':
    478     case 'paste-from-context-menu-command':
    479       updatePasteCommand();
    480       break;
    481 
    482     case 'add-new-bookmark-command':
    483     case 'new-folder-command':
    484       var parentId = computeParentFolderForNewItem();
    485       var unmodifiable = isUnmodifiable(tree.getBookmarkNodeById(parentId));
    486       e.canExecute = !isSearch && canEdit && !unmodifiable;
    487       break;
    488 
    489     case 'open-in-new-tab-command':
    490       updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
    491       break;
    492     case 'open-in-background-tab-command':
    493       updateOpenCommand(e, command, '', '', false);
    494       break;
    495     case 'open-in-new-window-command':
    496       updateOpenCommand(e, command,
    497           'open_in_new_window', 'open_all_new_window',
    498           // Disabled when incognito is forced.
    499           incognitoModeAvailability == 'forced' || !canOpenNewWindows);
    500       break;
    501     case 'open-incognito-window-command':
    502       updateOpenCommand(e, command,
    503           'open_incognito', 'open_all_incognito',
    504           // Not available when incognito is disabled.
    505           incognitoModeAvailability == 'disabled');
    506       break;
    507 
    508     case 'undo-delete-command':
    509       e.canExecute = !!lastDeletedNodes;
    510       break;
    511   }
    512 }
    513 
    514 /**
    515  * Helper function for handling canExecute for the list and document.
    516  * @param {!Event} e Can execute event object.
    517  */
    518 function canExecuteForList(e) {
    519   var command = e.command;
    520   var commandId = command.id;
    521 
    522   function hasSelected() {
    523     return !!list.selectedItem;
    524   }
    525 
    526   function hasSingleSelected() {
    527     return list.selectedItems.length == 1;
    528   }
    529 
    530   function canCopyItem(item) {
    531     return item.id != 'new';
    532   }
    533 
    534   function canCopyItems() {
    535     var selectedItems = list.selectedItems;
    536     return selectedItems && selectedItems.some(canCopyItem);
    537   }
    538 
    539   function isSearch() {
    540     return list.isSearch();
    541   }
    542 
    543   switch (commandId) {
    544     case 'rename-folder-command':
    545       // Show rename if a single folder is selected.
    546       var items = list.selectedItems;
    547       if (items.length != 1) {
    548         e.canExecute = false;
    549         command.hidden = true;
    550       } else {
    551         var isFolder = bmm.isFolder(items[0]);
    552         e.canExecute = isFolder && canEdit && !hasUnmodifiable(items);
    553         command.hidden = !isFolder;
    554       }
    555       break;
    556 
    557     case 'edit-command':
    558       // Show the edit command if not a folder.
    559       var items = list.selectedItems;
    560       if (items.length != 1) {
    561         e.canExecute = false;
    562         command.hidden = false;
    563       } else {
    564         var isFolder = bmm.isFolder(items[0]);
    565         e.canExecute = !isFolder && canEdit && !hasUnmodifiable(items);
    566         command.hidden = isFolder;
    567       }
    568       break;
    569 
    570     case 'show-in-folder-command':
    571       e.canExecute = isSearch() && hasSingleSelected();
    572       break;
    573 
    574     case 'delete-command':
    575     case 'cut-command':
    576       e.canExecute = canCopyItems() && canEdit &&
    577           !hasUnmodifiable(list.selectedItems);
    578       break;
    579 
    580     case 'copy-command':
    581       e.canExecute = canCopyItems();
    582       break;
    583 
    584     case 'open-in-same-window-command':
    585       e.canExecute = hasSelected();
    586       break;
    587 
    588     default:
    589       canExecuteShared(e, isSearch());
    590   }
    591 }
    592 
    593 // Update canExecute for the commands when the list is the active element.
    594 function handleCanExecuteForList(e) {
    595   if (e.target != list) return;
    596   canExecuteForList(e);
    597 }
    598 
    599 // Update canExecute for the commands when the tree is the active element.
    600 function handleCanExecuteForTree(e) {
    601   if (e.target != tree) return;
    602 
    603   var command = e.command;
    604   var commandId = command.id;
    605 
    606   function hasSelected() {
    607     return !!e.target.selectedItem;
    608   }
    609 
    610   function isSearch() {
    611     var item = e.target.selectedItem;
    612     return item == searchTreeItem;
    613   }
    614 
    615   function isTopLevelItem() {
    616     return e.target.selectedItem.parentNode == tree;
    617   }
    618 
    619   switch (commandId) {
    620     case 'rename-folder-command':
    621       command.hidden = false;
    622       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
    623           !hasUnmodifiable(tree.selectedFolders);
    624       break;
    625 
    626     case 'edit-command':
    627       command.hidden = true;
    628       e.canExecute = false;
    629       break;
    630 
    631     case 'delete-command':
    632     case 'cut-command':
    633       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
    634           !hasUnmodifiable(tree.selectedFolders);
    635       break;
    636 
    637     case 'copy-command':
    638       e.canExecute = hasSelected() && !isTopLevelItem();
    639       break;
    640 
    641     default:
    642       canExecuteShared(e, isSearch());
    643   }
    644 }
    645 
    646 /**
    647  * Update the canExecute state of all the commands.
    648  */
    649 function updateAllCommands() {
    650   var commands = document.querySelectorAll('command');
    651   for (var i = 0; i < commands.length; i++) {
    652     commands[i].canExecuteChange();
    653   }
    654 }
    655 
    656 function updateEditingCommands() {
    657   var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
    658       'add-new-bookmark', 'new-folder', 'sort',
    659       'paste-from-context-menu', 'paste-from-organize-menu'];
    660 
    661   chrome.bookmarkManagerPrivate.canEdit(function(result) {
    662     if (result != canEdit) {
    663       canEdit = result;
    664       editingCommands.forEach(function(baseId) {
    665         $(baseId + '-command').canExecuteChange();
    666       });
    667     }
    668   });
    669 }
    670 
    671 function handleChangeForTree(e) {
    672   navigateTo(tree.selectedItem.bookmarkId);
    673 }
    674 
    675 function handleOrganizeButtonClick(e) {
    676   updateEditingCommands();
    677   $('add-new-bookmark-command').canExecuteChange();
    678   $('new-folder-command').canExecuteChange();
    679   $('sort-command').canExecuteChange();
    680 }
    681 
    682 function handleRename(e) {
    683   var item = e.target;
    684   var bookmarkNode = item.bookmarkNode;
    685   chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
    686   performGlobalUndo = null;  // This can't be undone, so disable global undo.
    687 }
    688 
    689 function handleEdit(e) {
    690   var item = e.target;
    691   var bookmarkNode = item.bookmarkNode;
    692   var context = {
    693     title: bookmarkNode.title
    694   };
    695   if (!bmm.isFolder(bookmarkNode))
    696     context.url = bookmarkNode.url;
    697 
    698   if (bookmarkNode.id == 'new') {
    699     selectItemsAfterUserAction(list);
    700 
    701     // New page
    702     context.parentId = bookmarkNode.parentId;
    703     chrome.bookmarks.create(context, function(node) {
    704       // A new node was created and will get added to the list due to the
    705       // handler.
    706       var dataModel = list.dataModel;
    707       var index = dataModel.indexOf(bookmarkNode);
    708       dataModel.splice(index, 1);
    709 
    710       // Select new item.
    711       var newIndex = dataModel.findIndexById(node.id);
    712       if (newIndex != -1) {
    713         var sm = list.selectionModel;
    714         list.scrollIndexIntoView(newIndex);
    715         sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
    716       }
    717     });
    718   } else {
    719     // Edit
    720     chrome.bookmarks.update(bookmarkNode.id, context);
    721   }
    722   performGlobalUndo = null;  // This can't be undone, so disable global undo.
    723 }
    724 
    725 function handleCancelEdit(e) {
    726   var item = e.target;
    727   var bookmarkNode = item.bookmarkNode;
    728   if (bookmarkNode.id == 'new') {
    729     var dataModel = list.dataModel;
    730     var index = dataModel.findIndexById('new');
    731     dataModel.splice(index, 1);
    732   }
    733 }
    734 
    735 /**
    736  * Navigates to the folder that the selected item is in and selects it. This is
    737  * used for the show-in-folder command.
    738  */
    739 function showInFolder() {
    740   var bookmarkNode = list.selectedItem;
    741   if (!bookmarkNode)
    742     return;
    743   var parentId = bookmarkNode.parentId;
    744 
    745   // After the list is loaded we should select the revealed item.
    746   function selectItem() {
    747     var index = list.dataModel.findIndexById(bookmarkNode.id);
    748     if (index == -1)
    749       return;
    750     var sm = list.selectionModel;
    751     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
    752     list.scrollIndexIntoView(index);
    753   }
    754 
    755   var treeItem = bmm.treeLookup[parentId];
    756   treeItem.reveal();
    757 
    758   navigateTo(parentId, selectItem);
    759 }
    760 
    761 /**
    762  * @return {!cr.LinkController} The link controller used to open links based on
    763  *     user clicks and keyboard actions.
    764  */
    765 function getLinkController() {
    766   return linkController ||
    767       (linkController = new cr.LinkController(loadTimeData));
    768 }
    769 
    770 /**
    771  * Returns the selected bookmark nodes of the provided tree or list.
    772  * If |opt_target| is not provided or null the active element is used.
    773  * Only call this if the list or the tree is focused.
    774  * @param {BookmarkList|BookmarkTree} opt_target The target list or tree.
    775  * @return {!Array} Array of bookmark nodes.
    776  */
    777 function getSelectedBookmarkNodes(opt_target) {
    778   return (opt_target || document.activeElement) == tree ?
    779       tree.selectedFolders : list.selectedItems;
    780 }
    781 
    782 /**
    783  * @return {!Array.<string>} An array of the selected bookmark IDs.
    784  */
    785 function getSelectedBookmarkIds() {
    786   var selectedNodes = getSelectedBookmarkNodes();
    787   selectedNodes.sort(function(a, b) { return a.index - b.index });
    788   return selectedNodes.map(function(node) {
    789     return node.id;
    790   });
    791 }
    792 
    793 /**
    794  * @param {BookmarkTreeNode} node The node to test.
    795  * @return {boolean} Whether the given node is unmodifiable.
    796  */
    797 function isUnmodifiable(node) {
    798   return node && node.unmodifiable;
    799 }
    800 
    801 /**
    802  * @param {BookmarkList} A list of BookmarkNodes.
    803  * @return {boolean} Whether any of the nodes is managed.
    804  */
    805 function hasUnmodifiable(nodes) {
    806   return nodes.some(isUnmodifiable);
    807 }
    808 
    809 /**
    810  * Opens the selected bookmarks.
    811  * @param {LinkKind} kind The kind of link we want to open.
    812  * @param {HTMLElement} opt_eventTarget The target of the user initiated event.
    813  */
    814 function openBookmarks(kind, opt_eventTarget) {
    815   // If we have selected any folders, we need to find all the bookmarks one
    816   // level down. We use multiple async calls to getSubtree instead of getting
    817   // the whole tree since we would like to minimize the amount of data sent.
    818 
    819   var urlsP = getUrlsForOpenCommands(opt_eventTarget);
    820   urlsP.then(function(urls) {
    821     getLinkController().openUrls(urls, kind);
    822     chrome.bookmarkManagerPrivate.recordLaunch();
    823   });
    824 }
    825 
    826 /**
    827  * Opens an item in the list.
    828  */
    829 function openItem() {
    830   var bookmarkNodes = getSelectedBookmarkNodes();
    831   // If we double clicked or pressed enter on a single folder, navigate to it.
    832   if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0]))
    833     navigateTo(bookmarkNodes[0].id);
    834   else
    835     openBookmarks(LinkKind.FOREGROUND_TAB);
    836 }
    837 
    838 /**
    839  * Refreshes search results after delete or undo-delete.
    840  * This ensures children of deleted folders do not remain in results
    841  */
    842 function updateSearchResults() {
    843   if (list.isSearch()) {
    844     list.reload();
    845   }
    846 }
    847 
    848 /**
    849  * Deletes the selected bookmarks. The bookmarks are saved in memory in case
    850  * the user needs to undo the deletion.
    851  */
    852 function deleteBookmarks() {
    853   var selectedIds = getSelectedBookmarkIds();
    854   var filteredIds = getFilteredSelectedBookmarkIds();
    855   lastDeletedNodes = [];
    856 
    857   function performDelete() {
    858     // Only remove filtered ids.
    859     chrome.bookmarkManagerPrivate.removeTrees(filteredIds);
    860     $('undo-delete-command').canExecuteChange();
    861     performGlobalUndo = undoDelete;
    862   }
    863 
    864   // First, store information about the bookmarks being deleted.
    865   // Store all selected ids.
    866   selectedIds.forEach(function(id) {
    867     chrome.bookmarks.getSubTree(id, function(results) {
    868       lastDeletedNodes.push(results);
    869 
    870       // When all nodes have been saved, perform the deletion.
    871       if (lastDeletedNodes.length === selectedIds.length) {
    872         performDelete();
    873         updateSearchResults();
    874       }
    875     });
    876   });
    877 }
    878 
    879 /**
    880  * Restores a tree of bookmarks under a specified folder.
    881  * @param {BookmarkTreeNode} node The node to restore.
    882  * @param {=string} parentId The ID of the folder to restore under. If not
    883  *     specified, the original parentId of the node will be used.
    884  */
    885 function restoreTree(node, parentId) {
    886   var bookmarkInfo = {
    887     parentId: parentId || node.parentId,
    888     title: node.title,
    889     index: node.index,
    890     url: node.url
    891   };
    892 
    893   chrome.bookmarks.create(bookmarkInfo, function(result) {
    894     if (!result) {
    895       console.error('Failed to restore bookmark.');
    896       return;
    897     }
    898 
    899     if (node.children) {
    900       // Restore the children using the new ID for this node.
    901       node.children.forEach(function(child) {
    902         restoreTree(child, result.id);
    903       });
    904     }
    905 
    906     updateSearchResults();
    907   });
    908 }
    909 
    910 /**
    911  * Restores the last set of bookmarks that was deleted.
    912  */
    913 function undoDelete() {
    914   lastDeletedNodes.forEach(function(arr) {
    915     arr.forEach(restoreTree);
    916   });
    917   lastDeletedNodes = null;
    918   $('undo-delete-command').canExecuteChange();
    919 
    920   // Only a single level of undo is supported, so disable global undo now.
    921   performGlobalUndo = null;
    922 }
    923 
    924 /**
    925  * Computes folder for "Add Page" and "Add Folder".
    926  * @return {string} The id of folder node where we'll create new page/folder.
    927  */
    928 function computeParentFolderForNewItem() {
    929   if (document.activeElement == tree)
    930     return list.parentId;
    931   var selectedItem = list.selectedItem;
    932   return selectedItem && bmm.isFolder(selectedItem) ?
    933       selectedItem.id : list.parentId;
    934 }
    935 
    936 /**
    937  * Callback for rename folder and edit command. This starts editing for
    938  * selected item.
    939  */
    940 function editSelectedItem() {
    941   if (document.activeElement == tree) {
    942     tree.selectedItem.editing = true;
    943   } else {
    944     var li = list.getListItem(list.selectedItem);
    945     if (li)
    946       li.editing = true;
    947   }
    948 }
    949 
    950 /**
    951  * Callback for the new folder command. This creates a new folder and starts
    952  * a rename of it.
    953  */
    954 function newFolder() {
    955   performGlobalUndo = null;  // This can't be undone, so disable global undo.
    956 
    957   var parentId = computeParentFolderForNewItem();
    958 
    959   // Callback is called after tree and list data model updated.
    960   function createFolder(callback) {
    961     chrome.bookmarks.create({
    962       title: loadTimeData.getString('new_folder_name'),
    963       parentId: parentId
    964     }, callback);
    965   }
    966 
    967   if (document.activeElement == tree) {
    968     createFolder(function(newNode) {
    969       navigateTo(newNode.id, function() {
    970         bmm.treeLookup[newNode.id].editing = true;
    971       });
    972     });
    973     return;
    974   }
    975 
    976   function editNewFolderInList() {
    977     createFolder(function() {
    978       var index = list.dataModel.length - 1;
    979       var sm = list.selectionModel;
    980       sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
    981       scrollIntoViewAndMakeEditable(index);
    982     });
    983   }
    984 
    985   navigateTo(parentId, editNewFolderInList);
    986 }
    987 
    988 /**
    989  * Scrolls the list item into view and makes it editable.
    990  * @param {number} index The index of the item to make editable.
    991  */
    992 function scrollIntoViewAndMakeEditable(index) {
    993   list.scrollIndexIntoView(index);
    994   // onscroll is now dispatched asynchronously so we have to postpone
    995   // the rest.
    996   setTimeout(function() {
    997     var item = list.getListItemByIndex(index);
    998     if (item)
    999       item.editing = true;
   1000   });
   1001 }
   1002 
   1003 /**
   1004  * Adds a page to the current folder. This is called by the
   1005  * add-new-bookmark-command handler.
   1006  */
   1007 function addPage() {
   1008   var parentId = computeParentFolderForNewItem();
   1009 
   1010   function editNewBookmark() {
   1011     var fakeNode = {
   1012       title: '',
   1013       url: '',
   1014       parentId: parentId,
   1015       id: 'new'
   1016     };
   1017     var dataModel = list.dataModel;
   1018     var length = dataModel.length;
   1019     dataModel.splice(length, 0, fakeNode);
   1020     var sm = list.selectionModel;
   1021     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
   1022     scrollIntoViewAndMakeEditable(length);
   1023   };
   1024 
   1025   navigateTo(parentId, editNewBookmark);
   1026 }
   1027 
   1028 /**
   1029  * This function is used to select items after a user action such as paste, drop
   1030  * add page etc.
   1031  * @param {BookmarkList|BookmarkTree} target The target of the user action.
   1032  * @param {=string} opt_selectedTreeId If provided, then select that tree id.
   1033  */
   1034 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
   1035   // We get one onCreated event per item so we delay the handling until we get
   1036   // no more events coming.
   1037 
   1038   var ids = [];
   1039   var timer;
   1040 
   1041   function handle(id, bookmarkNode) {
   1042     clearTimeout(timer);
   1043     if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
   1044       ids.push(id);
   1045     timer = setTimeout(handleTimeout, 50);
   1046   }
   1047 
   1048   function handleTimeout() {
   1049     chrome.bookmarks.onCreated.removeListener(handle);
   1050     chrome.bookmarks.onMoved.removeListener(handle);
   1051 
   1052     if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
   1053       var index = ids.indexOf(opt_selectedTreeId);
   1054       if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
   1055         tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
   1056       }
   1057     } else if (target == list) {
   1058       var dataModel = list.dataModel;
   1059       var firstIndex = dataModel.findIndexById(ids[0]);
   1060       var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
   1061       if (firstIndex != -1 && lastIndex != -1) {
   1062         var selectionModel = list.selectionModel;
   1063         selectionModel.selectedIndex = -1;
   1064         selectionModel.selectRange(firstIndex, lastIndex);
   1065         selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
   1066         list.focus();
   1067       }
   1068     }
   1069 
   1070     list.endBatchUpdates();
   1071   }
   1072 
   1073   list.startBatchUpdates();
   1074 
   1075   chrome.bookmarks.onCreated.addListener(handle);
   1076   chrome.bookmarks.onMoved.addListener(handle);
   1077   timer = setTimeout(handleTimeout, 300);
   1078 }
   1079 
   1080 /**
   1081  * Record user action.
   1082  * @param {string} name An user action name.
   1083  */
   1084 function recordUserAction(name) {
   1085   chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
   1086 }
   1087 
   1088 /**
   1089  * The currently selected bookmark, based on where the user is clicking.
   1090  * @return {string} The ID of the currently selected bookmark (could be from
   1091  *     tree view or list view).
   1092  */
   1093 function getSelectedId() {
   1094   if (document.activeElement == tree)
   1095     return tree.selectedItem.bookmarkId;
   1096   var selectedItem = list.selectedItem;
   1097   return selectedItem && bmm.isFolder(selectedItem) ?
   1098       selectedItem.id : tree.selectedItem.bookmarkId;
   1099 }
   1100 
   1101 /**
   1102  * Pastes the copied/cutted bookmark into the right location depending whether
   1103  * if it was called from Organize Menu or from Context Menu.
   1104  * @param {string} id The id of the element being pasted from.
   1105  */
   1106 function pasteBookmark(id) {
   1107   recordUserAction('Paste');
   1108   selectItemsAfterUserAction(list);
   1109   chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
   1110 }
   1111 
   1112 /**
   1113  * Returns true if child is contained in another selected folder.
   1114  * Traces parent nodes up the tree until a selected ancestor or root is found.
   1115  */
   1116 function hasSelectedAncestor(parentNode) {
   1117   function contains(arr, item) {
   1118     for (var i = 0; i < arr.length; i++)
   1119         if (arr[i] === item)
   1120           return true;
   1121     return false;
   1122   }
   1123 
   1124   // Don't search top level, cannot select permanent nodes in search.
   1125   if (parentNode == null || parentNode.id <= 2)
   1126     return false;
   1127 
   1128   // Found selected ancestor.
   1129   if (contains(getSelectedBookmarkNodes(), parentNode))
   1130     return true;
   1131 
   1132   // Keep digging.
   1133   return hasSelectedAncestor(tree.getBookmarkNodeById(parentNode.parentId));
   1134 }
   1135 
   1136 function getFilteredSelectedBookmarkIds() {
   1137   // Remove duplicates from filteredIds and return.
   1138   var filteredIds = new Array();
   1139   // Selected nodes to iterate through for matches.
   1140   var nodes = getSelectedBookmarkNodes();
   1141 
   1142   for (var i = 0; i < nodes.length; i++)
   1143     if (!hasSelectedAncestor(tree.getBookmarkNodeById(nodes[i].parentId)))
   1144       filteredIds.splice(0, 0, nodes[i].id);
   1145 
   1146   return filteredIds;
   1147 }
   1148 
   1149 /**
   1150  * Handler for the command event. This is used for context menu of list/tree
   1151  * and organized menu.
   1152  * @param {!Event} e The event object.
   1153  */
   1154 function handleCommand(e) {
   1155   var command = e.command;
   1156   var commandId = command.id;
   1157   switch (commandId) {
   1158     case 'import-menu-command':
   1159       recordUserAction('Import');
   1160       chrome.bookmarks.import();
   1161       break;
   1162     case 'export-menu-command':
   1163       recordUserAction('Export');
   1164       chrome.bookmarks.export();
   1165       break;
   1166     case 'undo-command':
   1167       if (performGlobalUndo) {
   1168         recordUserAction('UndoGlobal');
   1169         performGlobalUndo();
   1170       } else {
   1171         recordUserAction('UndoNone');
   1172       }
   1173       break;
   1174     case 'show-in-folder-command':
   1175       recordUserAction('ShowInFolder');
   1176       showInFolder();
   1177       break;
   1178     case 'open-in-new-tab-command':
   1179     case 'open-in-background-tab-command':
   1180       recordUserAction('OpenInNewTab');
   1181       openBookmarks(LinkKind.BACKGROUND_TAB, e.target);
   1182       break;
   1183     case 'open-in-new-window-command':
   1184       recordUserAction('OpenInNewWindow');
   1185       openBookmarks(LinkKind.WINDOW, e.target);
   1186       break;
   1187     case 'open-incognito-window-command':
   1188       recordUserAction('OpenIncognito');
   1189       openBookmarks(LinkKind.INCOGNITO, e.target);
   1190       break;
   1191     case 'delete-command':
   1192       recordUserAction('Delete');
   1193       deleteBookmarks();
   1194       break;
   1195     case 'copy-command':
   1196       recordUserAction('Copy');
   1197       chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
   1198                                          updatePasteCommand);
   1199       break;
   1200     case 'cut-command':
   1201       recordUserAction('Cut');
   1202       chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
   1203                                         function() {
   1204                                           updatePasteCommand();
   1205                                           updateSearchResults();
   1206                                         });
   1207       break;
   1208     case 'paste-from-organize-menu-command':
   1209       pasteBookmark(list.parentId);
   1210       break;
   1211     case 'paste-from-context-menu-command':
   1212       pasteBookmark(getSelectedId());
   1213       break;
   1214     case 'sort-command':
   1215       recordUserAction('Sort');
   1216       chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
   1217       break;
   1218     case 'rename-folder-command':
   1219       editSelectedItem();
   1220       break;
   1221     case 'edit-command':
   1222       recordUserAction('Edit');
   1223       editSelectedItem();
   1224       break;
   1225     case 'new-folder-command':
   1226       recordUserAction('NewFolder');
   1227       newFolder();
   1228       break;
   1229     case 'add-new-bookmark-command':
   1230       recordUserAction('AddPage');
   1231       addPage();
   1232       break;
   1233     case 'open-in-same-window-command':
   1234       recordUserAction('OpenInSame');
   1235       openItem();
   1236       break;
   1237     case 'undo-delete-command':
   1238       recordUserAction('UndoDelete');
   1239       undoDelete();
   1240       break;
   1241   }
   1242 }
   1243 
   1244 // Execute the copy, cut and paste commands when those events are dispatched by
   1245 // the browser. This allows us to rely on the browser to handle the keyboard
   1246 // shortcuts for these commands.
   1247 function installEventHandlerForCommand(eventName, commandId) {
   1248   function handle(e) {
   1249     if (document.activeElement != list && document.activeElement != tree)
   1250       return;
   1251     var command = $(commandId);
   1252     if (!command.disabled) {
   1253       command.execute();
   1254       if (e)
   1255         e.preventDefault();  // Prevent the system beep.
   1256     }
   1257   }
   1258   if (eventName == 'paste') {
   1259     // Paste is a bit special since we need to do an async call to see if we
   1260     // can paste because the paste command might not be up to date.
   1261     document.addEventListener(eventName, function(e) {
   1262       updatePasteCommand(handle);
   1263     });
   1264   } else {
   1265     document.addEventListener(eventName, handle);
   1266   }
   1267 }
   1268 
   1269 function initializeSplitter() {
   1270   var splitter = document.querySelector('.main > .splitter');
   1271   Splitter.decorate(splitter);
   1272 
   1273   // The splitter persists the size of the left component in the local store.
   1274   if ('treeWidth' in localStorage)
   1275     splitter.previousElementSibling.style.width = localStorage['treeWidth'];
   1276 
   1277   splitter.addEventListener('resize', function(e) {
   1278     localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
   1279   });
   1280 }
   1281 
   1282 function initializeBookmarkManager() {
   1283   // Sometimes the extension API is not initialized.
   1284   if (!chrome.bookmarks)
   1285     console.error('Bookmarks extension API is not available');
   1286 
   1287   chrome.bookmarkManagerPrivate.getStrings(continueInitializeBookmarkManager);
   1288 }
   1289 
   1290 function continueInitializeBookmarkManager(localizedStrings) {
   1291   loadLocalizedStrings(localizedStrings);
   1292 
   1293   bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
   1294 
   1295   cr.ui.decorate('menu', Menu);
   1296   cr.ui.decorate('button[menu]', MenuButton);
   1297   cr.ui.decorate('command', Command);
   1298   BookmarkList.decorate(list);
   1299   BookmarkTree.decorate(tree);
   1300 
   1301   list.addEventListener('canceledit', handleCancelEdit);
   1302   list.addEventListener('canExecute', handleCanExecuteForList);
   1303   list.addEventListener('change', updateAllCommands);
   1304   list.addEventListener('contextmenu', updateEditingCommands);
   1305   list.addEventListener('dblclick', handleDoubleClickForList);
   1306   list.addEventListener('edit', handleEdit);
   1307   list.addEventListener('rename', handleRename);
   1308   list.addEventListener('urlClicked', handleUrlClickedForList);
   1309 
   1310   tree.addEventListener('canExecute', handleCanExecuteForTree);
   1311   tree.addEventListener('change', handleChangeForTree);
   1312   tree.addEventListener('contextmenu', updateEditingCommands);
   1313   tree.addEventListener('rename', handleRename);
   1314   tree.addEventListener('load', handleLoadForTree);
   1315 
   1316   cr.ui.contextMenuHandler.addContextMenuProperty(tree);
   1317   list.contextMenu = $('context-menu');
   1318   tree.contextMenu = $('context-menu');
   1319 
   1320   // We listen to hashchange so that we can update the currently shown folder
   1321   // when // the user goes back and forward in the history.
   1322   window.addEventListener('hashchange', processHash);
   1323 
   1324   document.querySelector('header form').onsubmit = function(e) {
   1325     setSearch($('term').value);
   1326     e.preventDefault();
   1327   };
   1328 
   1329   $('term').addEventListener('search', handleSearch);
   1330 
   1331   document.querySelector('.summary button').addEventListener(
   1332       'click', handleOrganizeButtonClick);
   1333 
   1334   document.addEventListener('canExecute', handleCanExecuteForDocument);
   1335   document.addEventListener('command', handleCommand);
   1336 
   1337   // Listen to copy, cut and paste events and execute the associated commands.
   1338   installEventHandlerForCommand('copy', 'copy-command');
   1339   installEventHandlerForCommand('cut', 'cut-command');
   1340   installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
   1341 
   1342   // Install shortcuts
   1343   for (var name in commandShortcutMap) {
   1344     $(name + '-command').shortcut = commandShortcutMap[name];
   1345   }
   1346 
   1347   // Disable almost all commands at startup.
   1348   var commands = document.querySelectorAll('command');
   1349   for (var i = 0, command; command = commands[i]; ++i) {
   1350     if (command.id != 'import-menu-command' &&
   1351         command.id != 'export-menu-command') {
   1352       command.disabled = true;
   1353     }
   1354   }
   1355 
   1356   chrome.bookmarkManagerPrivate.canEdit(function(result) {
   1357     canEdit = result;
   1358   });
   1359 
   1360   chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
   1361     // TODO(rustema): propagate policy value to the bookmark manager when it
   1362     // changes.
   1363     incognitoModeAvailability = result;
   1364   });
   1365 
   1366   chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
   1367     canOpenNewWindows = result;
   1368   });
   1369 
   1370   cr.ui.FocusOutlineManager.forDocument(document);
   1371   initializeSplitter();
   1372   bmm.addBookmarkModelListeners();
   1373   dnd.init(selectItemsAfterUserAction);
   1374   tree.reload();
   1375 }
   1376 
   1377 initializeBookmarkManager();
   1378 })();
   1379