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