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