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