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 }
    148 
    149 /**
    150  * Navigates to a bookmark ID.
    151  * @param {string} id The ID to navigate to.
    152  * @param {function()} callback Function called when list view loaded or
    153  *     displayed specified folder.
    154  */
    155 function navigateTo(id, callback) {
    156   if (list.parentId == id) {
    157     callback();
    158     return;
    159   }
    160 
    161   var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
    162                   folderMetricsNameMap['subfolder'];
    163   chrome.metricsPrivate.recordUserAction(
    164       'BookmarkManager_NavigateTo_' + metricsId);
    165 
    166   addOneShotEventListener(list, 'load', callback);
    167   updateParentId(id);
    168 }
    169 
    170 /**
    171  * Updates the parent ID of the bookmark list and selects the correct tree item.
    172  * @param {string} id The id.
    173  */
    174 function updateParentId(id) {
    175   // Setting list.parentId fires 'load' event.
    176   list.parentId = id;
    177 
    178   // When tree.selectedItem changed, tree view calls navigatTo() then it
    179   // calls updateHash() when list view displayed specified folder.
    180   tree.selectedItem = bmm.treeLookup[id] || tree.selectedItem;
    181 }
    182 
    183 // Process the location hash. This is called by onhashchange and when the page
    184 // is first loaded.
    185 function processHash() {
    186   var id = window.location.hash.slice(1);
    187   if (!id) {
    188     // If we do not have a hash, select first item in the tree.
    189     id = tree.items[0].bookmarkId;
    190   }
    191 
    192   var valid = false;
    193   if (/^e=/.test(id)) {
    194     id = id.slice(2);
    195 
    196     // If hash contains e=, edit the item specified.
    197     chrome.bookmarks.get(id, function(bookmarkNodes) {
    198       // Verify the node to edit is a valid node.
    199       if (!bookmarkNodes || bookmarkNodes.length != 1)
    200         return;
    201       var bookmarkNode = bookmarkNodes[0];
    202 
    203       // After the list reloads, edit the desired bookmark.
    204       var editBookmark = function(e) {
    205         var index = list.dataModel.findIndexById(bookmarkNode.id);
    206         if (index != -1) {
    207           var sm = list.selectionModel;
    208           sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
    209           scrollIntoViewAndMakeEditable(index);
    210         }
    211       };
    212 
    213       navigateTo(bookmarkNode.parentId, editBookmark);
    214     });
    215 
    216     // We handle the two cases of navigating to the bookmark to be edited
    217     // above. Don't run the standard navigation code below.
    218     return;
    219   } else if (/^q=/.test(id)) {
    220     // In case we got a search hash, update the text input and the
    221     // bmm.treeLookup to use the new id.
    222     setSearch(id.slice(2));
    223     valid = true;
    224   }
    225 
    226   // Navigate to bookmark 'id' (which may be a query of the form q=query).
    227   if (valid) {
    228     updateParentId(id);
    229   } else {
    230     // We need to verify that this is a correct ID.
    231     chrome.bookmarks.get(id, function(items) {
    232       if (items && items.length == 1)
    233         updateParentId(id);
    234     });
    235   }
    236 }
    237 
    238 // Activate is handled by the open-in-same-window-command.
    239 function handleDoubleClickForList(e) {
    240   if (e.button == 0)
    241     $('open-in-same-window-command').execute();
    242 }
    243 
    244 // The list dispatches an event when the user clicks on the URL or the Show in
    245 // folder part.
    246 function handleUrlClickedForList(e) {
    247   getLinkController().openUrlFromEvent(e.url, e.originalEvent);
    248   chrome.bookmarkManagerPrivate.recordLaunch();
    249 }
    250 
    251 function handleSearch(e) {
    252   setSearch(this.value);
    253 }
    254 
    255 /**
    256  * Navigates to the search results for the search text.
    257  * @param {string} searchText The text to search for.
    258  */
    259 function setSearch(searchText) {
    260   if (searchText) {
    261     // Only update search item if we have a search term. We never want the
    262     // search item to be for an empty search.
    263     delete bmm.treeLookup[searchTreeItem.bookmarkId];
    264     var id = searchTreeItem.bookmarkId = 'q=' + searchText;
    265     bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
    266   }
    267 
    268   var input = $('term');
    269   // Do not update the input if the user is actively using the text input.
    270   if (document.activeElement != input)
    271     input.value = searchText;
    272 
    273   if (searchText) {
    274     tree.add(searchTreeItem);
    275     tree.selectedItem = searchTreeItem;
    276   } else {
    277     // Go "home".
    278     tree.selectedItem = tree.items[0];
    279     id = tree.selectedItem.bookmarkId;
    280   }
    281 
    282   // Navigate now and update hash immediately.
    283   navigateTo(id, updateHash);
    284 }
    285 
    286 // Handle the logo button UI.
    287 // When the user clicks the button we should navigate "home" and focus the list.
    288 function handleClickOnLogoButton(e) {
    289   setSearch('');
    290   $('list').focus();
    291 }
    292 
    293 /**
    294  * This returns the user visible path to the folder where the bookmark is
    295  * located.
    296  * @param {number} parentId The ID of the parent folder.
    297  * @return {string} The path to the the bookmark,
    298  */
    299 function getFolder(parentId) {
    300   var parentNode = tree.getBookmarkNodeById(parentId);
    301   if (parentNode) {
    302     var s = parentNode.title;
    303     if (parentNode.parentId != bmm.ROOT_ID) {
    304       return getFolder(parentNode.parentId) + '/' + s;
    305     }
    306     return s;
    307   }
    308 }
    309 
    310 function handleLoadForTree(e) {
    311   processHash();
    312 }
    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 new Promise(node);
    338   });
    339 
    340   var urlsPromise = new Promise();
    341 
    342   var p = Promise.all.apply(null, promises);
    343   p.addListener(function(nodes) {
    344     nodes.forEach(function(node) {
    345       addNodes(node);
    346     });
    347     urlsPromise.value = urls;
    348   });
    349 
    350   return urlsPromise;
    351 }
    352 
    353 /**
    354  * Returns the nodes (non recursive) to use for the open commands.
    355  * @param {HTMLElement} target .
    356  * @return {Array.<BookmarkTreeNode>} .
    357  */
    358 function getNodesForOpen(target) {
    359   if (target == tree) {
    360     var folderItem = tree.selectedItem;
    361     return folderItem == searchTreeItem ?
    362         list.dataModel.slice() : tree.selectedFolders;
    363   }
    364   var items = list.selectedItems;
    365   return items.length ? items : list.dataModel.slice();
    366 }
    367 
    368 /**
    369  * Returns a promise that will contain all URLs of all the selected bookmarks
    370  * and the nested bookmarks for use with the open commands.
    371  * @param {HTMLElement} target The target list or tree.
    372  * @return {Promise} .
    373  */
    374 function getUrlsForOpenCommands(target) {
    375   return getAllUrls(getNodesForOpen(target));
    376 }
    377 
    378 function notNewNode(node) {
    379   return node.id != 'new';
    380 }
    381 
    382 /**
    383  * Helper function that updates the canExecute and labels for the open-like
    384  * commands.
    385  * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
    386  * @param {!cr.ui.Command} command The command we are currently processing.
    387  * @param {string} singularId The string id of singular form of the menu label.
    388  * @param {string} pluralId The string id of menu label if the singular form is
    389        not used.
    390  * @param {boolean} commandDisabled Whether the menu item should be disabled
    391        no matter what bookmarks are selected.
    392  */
    393 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
    394   if (singularId) {
    395     // The command label reflects the selection which might not reflect
    396     // how many bookmarks will be opened. For example if you right click an
    397     // empty area in a folder with 1 bookmark the text should still say "all".
    398     var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
    399     var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
    400     command.label = loadTimeData.getString(singular ? singularId : pluralId);
    401   }
    402 
    403   if (commandDisabled) {
    404     command.disabled = true;
    405     e.canExecute = false;
    406     return;
    407   }
    408 
    409   getUrlsForOpenCommands(e.target).addListener(function(urls) {
    410     var disabled = !urls.length;
    411     command.disabled = disabled;
    412     e.canExecute = !disabled;
    413   });
    414 }
    415 
    416 /**
    417  * Calls the backend to figure out if we can paste the clipboard into the active
    418  * folder.
    419  * @param {Function=} opt_f Function to call after the state has been updated.
    420  */
    421 function updatePasteCommand(opt_f) {
    422   function update(canPaste) {
    423     var organizeMenuCommand = $('paste-from-organize-menu-command');
    424     var contextMenuCommand = $('paste-from-context-menu-command');
    425     organizeMenuCommand.disabled = !canPaste;
    426     contextMenuCommand.disabled = !canPaste;
    427     if (opt_f)
    428       opt_f();
    429   }
    430   // We cannot paste into search view.
    431   if (list.isSearch())
    432     update(false);
    433   else
    434     chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
    435 }
    436 
    437 function handleCanExecuteForDocument(e) {
    438   var command = e.command;
    439   switch (command.id) {
    440     case 'import-menu-command':
    441       e.canExecute = canEdit;
    442       break;
    443     case 'export-menu-command':
    444       // We can always execute the export-menu command.
    445       e.canExecute = true;
    446       break;
    447     case 'sort-command':
    448       e.canExecute = !list.isSearch() && list.dataModel.length > 1;
    449       break;
    450     case 'undo-command':
    451       // The global undo command has no visible UI, so always enable it, and
    452       // just make it a no-op if undo is not possible.
    453       e.canExecute = true;
    454       break;
    455     default:
    456       canExecuteForList(e);
    457       break;
    458   }
    459 }
    460 
    461 /**
    462  * Helper function for handling canExecute for the list and the tree.
    463  * @param {!Event} e Can execute event object.
    464  * @param {boolean} isSearch Whether the user is trying to do a command on
    465  *     search.
    466  */
    467 function canExecuteShared(e, isSearch) {
    468   var command = e.command;
    469   var commandId = command.id;
    470   switch (commandId) {
    471     case 'paste-from-organize-menu-command':
    472     case 'paste-from-context-menu-command':
    473       updatePasteCommand();
    474       break;
    475 
    476     case 'add-new-bookmark-command':
    477     case 'new-folder-command':
    478       e.canExecute = !isSearch && canEdit;
    479       break;
    480 
    481     case 'open-in-new-tab-command':
    482       updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
    483       break;
    484     case 'open-in-background-tab-command':
    485       updateOpenCommand(e, command, '', '', false);
    486       break;
    487     case 'open-in-new-window-command':
    488       updateOpenCommand(e, command,
    489           'open_in_new_window', 'open_all_new_window',
    490           // Disabled when incognito is forced.
    491           incognitoModeAvailability == 'forced' || !canOpenNewWindows);
    492       break;
    493     case 'open-incognito-window-command':
    494       updateOpenCommand(e, command,
    495           'open_incognito', 'open_all_incognito',
    496           // Not available when incognito is disabled.
    497           incognitoModeAvailability == 'disabled');
    498       break;
    499 
    500     case 'undo-delete-command':
    501       e.canExecute = !!lastDeletedNodes;
    502       break;
    503   }
    504 }
    505 
    506 /**
    507  * Helper function for handling canExecute for the list and document.
    508  * @param {!Event} e Can execute event object.
    509  */
    510 function canExecuteForList(e) {
    511   var command = e.command;
    512   var commandId = command.id;
    513 
    514   function hasSelected() {
    515     return !!list.selectedItem;
    516   }
    517 
    518   function hasSingleSelected() {
    519     return list.selectedItems.length == 1;
    520   }
    521 
    522   function canCopyItem(item) {
    523     return item.id != 'new';
    524   }
    525 
    526   function canCopyItems() {
    527     var selectedItems = list.selectedItems;
    528     return selectedItems && selectedItems.some(canCopyItem);
    529   }
    530 
    531   function isSearch() {
    532     return list.isSearch();
    533   }
    534 
    535   switch (commandId) {
    536     case 'rename-folder-command':
    537       // Show rename if a single folder is selected.
    538       var items = list.selectedItems;
    539       if (items.length != 1) {
    540         e.canExecute = false;
    541         command.hidden = true;
    542       } else {
    543         var isFolder = bmm.isFolder(items[0]);
    544         e.canExecute = isFolder && canEdit;
    545         command.hidden = !isFolder;
    546       }
    547       break;
    548 
    549     case 'edit-command':
    550       // Show the edit command if not a folder.
    551       var items = list.selectedItems;
    552       if (items.length != 1) {
    553         e.canExecute = false;
    554         command.hidden = false;
    555       } else {
    556         var isFolder = bmm.isFolder(items[0]);
    557         e.canExecute = !isFolder && canEdit;
    558         command.hidden = isFolder;
    559       }
    560       break;
    561 
    562     case 'show-in-folder-command':
    563       e.canExecute = isSearch() && hasSingleSelected();
    564       break;
    565 
    566     case 'delete-command':
    567     case 'cut-command':
    568       e.canExecute = canCopyItems() && canEdit;
    569       break;
    570 
    571     case 'copy-command':
    572       e.canExecute = canCopyItems();
    573       break;
    574 
    575     case 'open-in-same-window-command':
    576       e.canExecute = hasSelected();
    577       break;
    578 
    579     default:
    580       canExecuteShared(e, isSearch());
    581   }
    582 }
    583 
    584 // Update canExecute for the commands when the list is the active element.
    585 function handleCanExecuteForList(e) {
    586   if (e.target != list) return;
    587   canExecuteForList(e);
    588 }
    589 
    590 // Update canExecute for the commands when the tree is the active element.
    591 function handleCanExecuteForTree(e) {
    592   if (e.target != tree) return;
    593 
    594   var command = e.command;
    595   var commandId = command.id;
    596 
    597   function hasSelected() {
    598     return !!e.target.selectedItem;
    599   }
    600 
    601   function isSearch() {
    602     var item = e.target.selectedItem;
    603     return item == searchTreeItem;
    604   }
    605 
    606   function isTopLevelItem() {
    607     return e.target.selectedItem.parentNode == tree;
    608   }
    609 
    610   switch (commandId) {
    611     case 'rename-folder-command':
    612       command.hidden = false;
    613       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
    614       break;
    615 
    616     case 'edit-command':
    617       command.hidden = true;
    618       e.canExecute = false;
    619       break;
    620 
    621     case 'delete-command':
    622     case 'cut-command':
    623       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
    624       break;
    625 
    626     case 'copy-command':
    627       e.canExecute = hasSelected() && !isTopLevelItem();
    628       break;
    629 
    630     default:
    631       canExecuteShared(e, isSearch());
    632   }
    633 }
    634 
    635 /**
    636  * Update the canExecute state of the commands when the selection changes.
    637  * @param {Event} e The change event object.
    638  */
    639 function updateCommandsBasedOnSelection(e) {
    640   if (e.target == document.activeElement) {
    641     // Paste only needs to be updated when the tree selection changes.
    642     var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
    643       'add-new-bookmark', 'new-folder', 'open-in-new-tab',
    644       'open-in-background-tab', 'open-in-new-window', 'open-incognito-window',
    645       'open-in-same-window', 'show-in-folder'];
    646 
    647     if (e.target == tree) {
    648       commandNames.push('paste-from-context-menu', 'paste-from-organize-menu',
    649                         'sort');
    650     }
    651 
    652     commandNames.forEach(function(baseId) {
    653       $(baseId + '-command').canExecuteChange();
    654     });
    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   updateCommandsBasedOnSelection(e);
    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(loadLocalizedStrings);
   1219 
   1220   bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
   1221 
   1222   cr.ui.decorate('menu', Menu);
   1223   cr.ui.decorate('button[menu]', MenuButton);
   1224   cr.ui.decorate('command', Command);
   1225   BookmarkList.decorate(list);
   1226   BookmarkTree.decorate(tree);
   1227 
   1228   list.addEventListener('canceledit', handleCancelEdit);
   1229   list.addEventListener('canExecute', handleCanExecuteForList);
   1230   list.addEventListener('change', updateCommandsBasedOnSelection);
   1231   list.addEventListener('contextmenu', updateEditingCommands);
   1232   list.addEventListener('dblclick', handleDoubleClickForList);
   1233   list.addEventListener('edit', handleEdit);
   1234   list.addEventListener('rename', handleRename);
   1235   list.addEventListener('urlClicked', handleUrlClickedForList);
   1236 
   1237   tree.addEventListener('canExecute', handleCanExecuteForTree);
   1238   tree.addEventListener('change', handleChangeForTree);
   1239   tree.addEventListener('contextmenu', updateEditingCommands);
   1240   tree.addEventListener('rename', handleRename);
   1241   tree.addEventListener('load', handleLoadForTree);
   1242 
   1243   cr.ui.contextMenuHandler.addContextMenuProperty(tree);
   1244   list.contextMenu = $('context-menu');
   1245   tree.contextMenu = $('context-menu');
   1246 
   1247   // We listen to hashchange so that we can update the currently shown folder
   1248   // when // the user goes back and forward in the history.
   1249   window.addEventListener('hashchange', processHash);
   1250 
   1251   document.querySelector('.header form').onsubmit = function(e) {
   1252     setSearch($('term').value);
   1253     e.preventDefault();
   1254   };
   1255 
   1256   $('term').addEventListener('search', handleSearch);
   1257 
   1258   document.querySelector('.summary > button').addEventListener(
   1259       'click', handleOrganizeButtonClick);
   1260 
   1261   document.querySelector('button.logo').addEventListener(
   1262       'click', handleClickOnLogoButton);
   1263 
   1264   document.addEventListener('canExecute', handleCanExecuteForDocument);
   1265   document.addEventListener('command', handleCommand);
   1266 
   1267   // Listen to copy, cut and paste events and execute the associated commands.
   1268   installEventHandlerForCommand('copy', 'copy-command');
   1269   installEventHandlerForCommand('cut', 'cut-command');
   1270   installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
   1271 
   1272   // Install shortcuts
   1273   for (var name in commandShortcutMap) {
   1274     $(name + '-command').shortcut = commandShortcutMap[name];
   1275   }
   1276 
   1277   // Disable almost all commands at startup.
   1278   var commands = document.querySelectorAll('command');
   1279   for (var i = 0, command; command = commands[i]; ++i) {
   1280     if (command.id != 'import-menu-command' &&
   1281         command.id != 'export-menu-command') {
   1282       command.disabled = true;
   1283     }
   1284   }
   1285 
   1286   chrome.bookmarkManagerPrivate.canEdit(function(result) {
   1287     canEdit = result;
   1288   });
   1289 
   1290   chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
   1291     // TODO(rustema): propagate policy value to the bookmark manager when it
   1292     // changes.
   1293     incognitoModeAvailability = result;
   1294   });
   1295 
   1296   chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
   1297     canOpenNewWindows = result;
   1298   });
   1299 
   1300   cr.ui.FocusOutlineManager.forDocument(document);
   1301   initializeSplitter();
   1302   bmm.addBookmarkModelListeners();
   1303   dnd.init(selectItemsAfterUserAction);
   1304   tree.reload();
   1305 }
   1306 
   1307 initializeBookmarkManager();
   1308 })();
   1309