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