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