1 // Copyright (c) 2011 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 const BookmarkList = bmm.BookmarkList; 6 const BookmarkTree = bmm.BookmarkTree; 7 const ListItem = cr.ui.ListItem; 8 const TreeItem = cr.ui.TreeItem; 9 const LinkKind = cr.LinkKind; 10 const Command = cr.ui.Command; 11 const CommandBinding = cr.ui.CommandBinding; 12 const Menu = cr.ui.Menu; 13 const MenuButton = cr.ui.MenuButton; 14 const Promise = cr.Promise; 15 16 // Sometimes the extension API is not initialized. 17 if (!chrome.bookmarks) 18 console.error('Bookmarks extension API is not available'); 19 20 // Allow platform specific CSS rules. 21 if (cr.isMac) 22 document.documentElement.setAttribute('os', 'mac'); 23 24 /** 25 * The local strings object which is used to do the translation. 26 * @type {!LocalStrings} 27 */ 28 var localStrings = new LocalStrings; 29 30 // Get the localized strings from the backend. 31 chrome.experimental.bookmarkManager.getStrings(function(data) { 32 // The strings may contain & which we need to strip. 33 for (var key in data) { 34 data[key] = data[key].replace(/&/, ''); 35 } 36 37 localStrings.templateData = data; 38 i18nTemplate.process(document, data); 39 40 recentTreeItem.label = localStrings.getString('recent'); 41 searchTreeItem.label = localStrings.getString('search'); 42 }); 43 44 /** 45 * The id of the bookmark root. 46 * @type {number} 47 */ 48 const ROOT_ID = '0'; 49 50 var bookmarkCache = { 51 /** 52 * Removes the cached item from both the list and tree lookups. 53 */ 54 remove: function(id) { 55 var treeItem = bmm.treeLookup[id]; 56 if (treeItem) { 57 var items = treeItem.items; // is an HTMLCollection 58 for (var i = 0, item; item = items[i]; i++) { 59 var bookmarkNode = item.bookmarkNode; 60 delete bmm.treeLookup[bookmarkNode.id]; 61 } 62 delete bmm.treeLookup[id]; 63 } 64 }, 65 66 /** 67 * Updates the underlying bookmark node for the tree items and list items by 68 * querying the bookmark backend. 69 * @param {string} id The id of the node to update the children for. 70 * @param {Function=} opt_f A funciton to call when done. 71 */ 72 updateChildren: function(id, opt_f) { 73 function updateItem(bookmarkNode) { 74 var treeItem = bmm.treeLookup[bookmarkNode.id]; 75 if (treeItem) { 76 treeItem.bookmarkNode = bookmarkNode; 77 } 78 } 79 80 chrome.bookmarks.getChildren(id, function(children) { 81 if (children) 82 children.forEach(updateItem); 83 84 if (opt_f) 85 opt_f(children); 86 }); 87 } 88 }; 89 90 var splitter = document.querySelector('.main > .splitter'); 91 cr.ui.Splitter.decorate(splitter); 92 93 // The splitter persists the size of the left component in the local store. 94 if ('treeWidth' in localStorage) 95 splitter.previousElementSibling.style.width = localStorage['treeWidth']; 96 splitter.addEventListener('resize', function(e) { 97 localStorage['treeWidth'] = splitter.previousElementSibling.style.width; 98 }); 99 100 BookmarkList.decorate(list); 101 102 var searchTreeItem = new TreeItem({ 103 icon: 'images/bookmark_manager_search.png', 104 bookmarkId: 'q=' 105 }); 106 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem; 107 108 var recentTreeItem = new TreeItem({ 109 icon: 'images/bookmark_manager_recent.png', 110 bookmarkId: 'recent' 111 }); 112 bmm.treeLookup[recentTreeItem.bookmarkId] = recentTreeItem; 113 114 BookmarkTree.decorate(tree); 115 116 tree.addEventListener('change', function() { 117 navigateTo(tree.selectedItem.bookmarkId); 118 }); 119 120 /** 121 * Navigates to a bookmark ID. 122 * @param {string} id The ID to navigate to. 123 * @param {boolean=} opt_updateHashNow Whether to immediately update the 124 * location.hash. If false then it is updated in a timeout. 125 */ 126 function navigateTo(id, opt_updateHashNow) { 127 console.info('navigateTo', 'from', window.location.hash, 'to', id); 128 // Update the location hash using a timer to prevent reentrancy. This is how 129 // often we add history entries and the time here is a bit arbitrary but was 130 // picked as the smallest time a human perceives as instant. 131 132 function f() { 133 window.location.hash = tree.selectedItem.bookmarkId; 134 } 135 136 clearTimeout(navigateTo.timer_); 137 if (opt_updateHashNow) 138 f(); 139 else 140 navigateTo.timer_ = setTimeout(f, 250); 141 142 updateParentId(id); 143 } 144 145 /** 146 * Updates the parent ID of the bookmark list and selects the correct tree item. 147 * @param {string} id The id. 148 */ 149 function updateParentId(id) { 150 list.parentId = id; 151 if (id in bmm.treeLookup) 152 tree.selectedItem = bmm.treeLookup[id]; 153 } 154 155 // We listen to hashchange so that we can update the currently shown folder when 156 // the user goes back and forward in the history. 157 window.onhashchange = function(e) { 158 var id = window.location.hash.slice(1); 159 160 var valid = false; 161 162 // In case we got a search hash update the text input and the bmm.treeLookup 163 // to use the new id. 164 if (/^q=/.test(id)) { 165 setSearch(id.slice(2)); 166 valid = true; 167 } else if (id == 'recent') { 168 valid = true; 169 } 170 171 if (valid) { 172 updateParentId(id); 173 } else { 174 // We need to verify that this is a correct ID. 175 chrome.bookmarks.get(id, function(items) { 176 if (items && items.length == 1) 177 updateParentId(id); 178 }); 179 } 180 }; 181 182 // Activate is handled by the open-in-same-window-command. 183 list.addEventListener('dblclick', function(e) { 184 if (e.button == 0) 185 $('open-in-same-window-command').execute(); 186 }); 187 188 // The list dispatches an event when the user clicks on the URL or the Show in 189 // folder part. 190 list.addEventListener('urlClicked', function(e) { 191 getLinkController().openUrlFromEvent(e.url, e.originalEvent); 192 }); 193 194 $('term').onsearch = function(e) { 195 setSearch(this.value); 196 }; 197 198 /** 199 * Navigates to the search results for the search text. 200 * @para {string} searchText The text to search for. 201 */ 202 function setSearch(searchText) { 203 if (searchText) { 204 // Only update search item if we have a search term. We never want the 205 // search item to be for an empty search. 206 delete bmm.treeLookup[searchTreeItem.bookmarkId]; 207 var id = searchTreeItem.bookmarkId = 'q=' + searchText; 208 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem; 209 } 210 211 var input = $('term'); 212 // Do not update the input if the user is actively using the text input. 213 if (document.activeElement != input) 214 input.value = searchText; 215 216 if (searchText) { 217 tree.add(searchTreeItem); 218 tree.selectedItem = searchTreeItem; 219 } else { 220 // Go "home". 221 tree.selectedItem = tree.items[0]; 222 id = tree.selectedItem.bookmarkId; 223 } 224 225 // Navigate now and update hash immediately. 226 navigateTo(id, true); 227 } 228 229 // Handle the logo button UI. 230 // When the user clicks the button we should navigate "home" and focus the list 231 document.querySelector('button.logo').onclick = function(e) { 232 setSearch(''); 233 $('list').focus(); 234 }; 235 236 /** 237 * Called when the title of a bookmark changes. 238 * @param {string} id 239 * @param {!Object} changeInfo 240 */ 241 function handleBookmarkChanged(id, changeInfo) { 242 // console.info('handleBookmarkChanged', id, changeInfo); 243 list.handleBookmarkChanged(id, changeInfo); 244 tree.handleBookmarkChanged(id, changeInfo); 245 } 246 247 /** 248 * Callback for when the user reorders by title. 249 * @param {string} id The id of the bookmark folder that was reordered. 250 * @param {!Object} reorderInfo The information about how the items where 251 * reordered. 252 */ 253 function handleChildrenReordered(id, reorderInfo) { 254 // console.info('handleChildrenReordered', id, reorderInfo); 255 list.handleChildrenReordered(id, reorderInfo); 256 tree.handleChildrenReordered(id, reorderInfo); 257 bookmarkCache.updateChildren(id); 258 } 259 260 /** 261 * Callback for when a bookmark node is created. 262 * @param {string} id The id of the newly created bookmark node. 263 * @param {!Object} bookmarkNode The new bookmark node. 264 */ 265 function handleCreated(id, bookmarkNode) { 266 // console.info('handleCreated', id, bookmarkNode); 267 list.handleCreated(id, bookmarkNode); 268 tree.handleCreated(id, bookmarkNode); 269 bookmarkCache.updateChildren(bookmarkNode.parentId); 270 } 271 272 function handleMoved(id, moveInfo) { 273 // console.info('handleMoved', id, moveInfo); 274 list.handleMoved(id, moveInfo); 275 tree.handleMoved(id, moveInfo); 276 277 bookmarkCache.updateChildren(moveInfo.parentId); 278 if (moveInfo.parentId != moveInfo.oldParentId) 279 bookmarkCache.updateChildren(moveInfo.oldParentId); 280 } 281 282 function handleRemoved(id, removeInfo) { 283 // console.info('handleRemoved', id, removeInfo); 284 list.handleRemoved(id, removeInfo); 285 tree.handleRemoved(id, removeInfo); 286 287 bookmarkCache.updateChildren(removeInfo.parentId); 288 bookmarkCache.remove(id); 289 } 290 291 function handleImportBegan() { 292 chrome.bookmarks.onCreated.removeListener(handleCreated); 293 chrome.bookmarks.onChanged.removeListener(handleBookmarkChanged); 294 } 295 296 function handleImportEnded() { 297 // When importing is done we reload the tree and the list. 298 299 function f() { 300 tree.removeEventListener('load', f); 301 302 chrome.bookmarks.onCreated.addListener(handleCreated); 303 chrome.bookmarks.onChanged.addListener(handleBookmarkChanged); 304 305 if (list.selectImportedFolder) { 306 var otherBookmarks = tree.items[1].items; 307 var importedFolder = otherBookmarks[otherBookmarks.length - 1]; 308 navigateTo(importedFolder.bookmarkId) 309 list.selectImportedFolder = false 310 } else { 311 list.reload(); 312 } 313 } 314 315 tree.addEventListener('load', f); 316 tree.reload(); 317 } 318 319 /** 320 * Adds the listeners for the bookmark model change events. 321 */ 322 function addBookmarkModelListeners() { 323 chrome.bookmarks.onChanged.addListener(handleBookmarkChanged); 324 chrome.bookmarks.onChildrenReordered.addListener(handleChildrenReordered); 325 chrome.bookmarks.onCreated.addListener(handleCreated); 326 chrome.bookmarks.onMoved.addListener(handleMoved); 327 chrome.bookmarks.onRemoved.addListener(handleRemoved); 328 chrome.bookmarks.onImportBegan.addListener(handleImportBegan); 329 chrome.bookmarks.onImportEnded.addListener(handleImportEnded); 330 } 331 332 /** 333 * This returns the user visible path to the folder where the bookmark is 334 * located. 335 * @param {number} parentId The ID of the parent folder. 336 * @return {string} The path to the the bookmark, 337 */ 338 function getFolder(parentId) { 339 var parentNode = tree.getBookmarkNodeById(parentId); 340 if (parentNode) { 341 var s = parentNode.title; 342 if (parentNode.parentId != ROOT_ID) { 343 return getFolder(parentNode.parentId) + '/' + s; 344 } 345 return s; 346 } 347 } 348 349 tree.addEventListener('load', function(e) { 350 // Add hard coded tree items 351 tree.add(recentTreeItem); 352 353 // Now we can select a tree item. 354 var hash = window.location.hash.slice(1); 355 if (!hash) { 356 // If we do not have a hash select first item in the tree. 357 hash = tree.items[0].bookmarkId; 358 } 359 360 if (/^q=/.test(hash)) { 361 var searchTerm = hash.slice(2); 362 $('term').value = searchTerm; 363 setSearch(searchTerm); 364 } else { 365 navigateTo(hash); 366 } 367 }); 368 369 tree.reload(); 370 addBookmarkModelListeners(); 371 372 var dnd = { 373 dragData: null, 374 375 getBookmarkElement: function(el) { 376 while (el && !el.bookmarkNode) { 377 el = el.parentNode; 378 } 379 return el; 380 }, 381 382 // If we are over the list and the list is showing recent or search result 383 // we cannot drop. 384 isOverRecentOrSearch: function(overElement) { 385 return (list.isRecent() || list.isSearch()) && list.contains(overElement); 386 }, 387 388 checkEvery_: function(f, overBookmarkNode, overElement) { 389 return this.dragData.elements.every(function(element) { 390 return f.call(this, element, overBookmarkNode, overElement); 391 }, this); 392 }, 393 394 /** 395 * @return {boolean} Whether we are currently dragging any folders. 396 */ 397 isDraggingFolders: function() { 398 return !!this.dragData && this.dragData.elements.some(function(node) { 399 return !node.url; 400 }); 401 }, 402 403 /** 404 * This is a first pass wether we can drop the dragged items. 405 * 406 * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are 407 * currently dragging over. 408 * @param {!HTMLElement} overElement The element that we are currently 409 * dragging over. 410 * @return {boolean} If this returns false then we know we should not drop 411 * the items. If it returns true we still have to call canDropOn, 412 * canDropAbove and canDropBelow. 413 */ 414 canDrop: function(overBookmarkNode, overElement) { 415 var dragData = this.dragData; 416 if (!dragData) 417 return false; 418 419 if (this.isOverRecentOrSearch(overElement)) 420 return false; 421 422 if (!dragData.sameProfile) 423 return true; 424 425 return this.checkEvery_(this.canDrop_, overBookmarkNode, overElement); 426 }, 427 428 /** 429 * Helper for canDrop that only checks one bookmark node. 430 * @private 431 */ 432 canDrop_: function(dragNode, overBookmarkNode, overElement) { 433 var dragId = dragNode.id; 434 435 if (overBookmarkNode.id == dragId) 436 return false; 437 438 // If we are dragging a folder we cannot drop it on any of its descendants 439 var dragBookmarkItem = bmm.treeLookup[dragId]; 440 var dragBookmarkNode = dragBookmarkItem && dragBookmarkItem.bookmarkNode; 441 if (dragBookmarkNode && bmm.contains(dragBookmarkNode, overBookmarkNode)) { 442 return false; 443 } 444 445 return true; 446 }, 447 448 /** 449 * Whether we can drop the dragged items above the drop target. 450 * 451 * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are 452 * currently dragging over. 453 * @param {!HTMLElement} overElement The element that we are currently 454 * dragging over. 455 * @return {boolean} Whether we can drop the dragged items above the drop 456 * target. 457 */ 458 canDropAbove: function(overBookmarkNode, overElement) { 459 if (overElement instanceof BookmarkList) 460 return false; 461 462 // We cannot drop between Bookmarks bar and Other bookmarks 463 if (overBookmarkNode.parentId == ROOT_ID) 464 return false; 465 466 var isOverTreeItem = overElement instanceof TreeItem; 467 468 // We can only drop between items in the tree if we have any folders. 469 if (isOverTreeItem && !this.isDraggingFolders()) 470 return false; 471 472 if (!this.dragData.sameProfile) 473 return this.isDraggingFolders() || !isOverTreeItem; 474 475 return this.checkEvery_(this.canDropAbove_, overBookmarkNode, overElement); 476 }, 477 478 /** 479 * Helper for canDropAbove that only checks one bookmark node. 480 * @private 481 */ 482 canDropAbove_: function(dragNode, overBookmarkNode, overElement) { 483 var dragId = dragNode.id; 484 485 // We cannot drop above if the item below is already in the drag source 486 var previousElement = overElement.previousElementSibling; 487 if (previousElement && 488 previousElement.bookmarkId == dragId) 489 return false; 490 491 return true; 492 }, 493 494 /** 495 * Whether we can drop the dragged items below the drop target. 496 * 497 * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are 498 * currently dragging over. 499 * @param {!HTMLElement} overElement The element that we are currently 500 * dragging over. 501 * @return {boolean} Whether we can drop the dragged items below the drop 502 * target. 503 */ 504 canDropBelow: function(overBookmarkNode, overElement) { 505 if (overElement instanceof BookmarkList) 506 return false; 507 508 // We cannot drop between Bookmarks bar and Other bookmarks 509 if (overBookmarkNode.parentId == ROOT_ID) 510 return false; 511 512 // We can only drop between items in the tree if we have any folders. 513 if (!this.isDraggingFolders() && overElement instanceof TreeItem) 514 return false; 515 516 var isOverTreeItem = overElement instanceof TreeItem; 517 518 // Don't allow dropping below an expanded tree item since it is confusing 519 // to the user anyway. 520 if (isOverTreeItem && overElement.expanded) 521 return false; 522 523 if (!this.dragData.sameProfile) 524 return this.isDraggingFolders() || !isOverTreeItem; 525 526 return this.checkEvery_(this.canDropBelow_, overBookmarkNode, overElement); 527 }, 528 529 /** 530 * Helper for canDropBelow that only checks one bookmark node. 531 * @private 532 */ 533 canDropBelow_: function(dragNode, overBookmarkNode, overElement) { 534 var dragId = dragNode.id; 535 536 // We cannot drop below if the item below is already in the drag source 537 var nextElement = overElement.nextElementSibling; 538 if (nextElement && 539 nextElement.bookmarkId == dragId) 540 return false; 541 542 return true; 543 }, 544 545 /** 546 * Whether we can drop the dragged items on the drop target. 547 * 548 * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are 549 * currently dragging over. 550 * @param {!HTMLElement} overElement The element that we are currently 551 * dragging over. 552 * @return {boolean} Whether we can drop the dragged items on the drop 553 * target. 554 */ 555 canDropOn: function(overBookmarkNode, overElement) { 556 // We can only drop on a folder. 557 if (!bmm.isFolder(overBookmarkNode)) 558 return false; 559 560 if (!this.dragData.sameProfile) 561 return true; 562 563 return this.checkEvery_(this.canDropOn_, overBookmarkNode, overElement); 564 }, 565 566 /** 567 * Helper for canDropOn that only checks one bookmark node. 568 * @private 569 */ 570 canDropOn_: function(dragNode, overBookmarkNode, overElement) { 571 var dragId = dragNode.id; 572 573 if (overElement instanceof BookmarkList) { 574 // We are trying to drop an item after the last item in the list. This 575 // is allowed if the item is different from the last item in the list 576 var listItems = list.items; 577 var len = listItems.length; 578 if (len == 0 || 579 listItems[len - 1].bookmarkId != dragId) { 580 return true; 581 } 582 } 583 584 // Cannot drop on current parent. 585 if (overBookmarkNode.id == dragNode.parentId) 586 return false; 587 588 return true; 589 }, 590 591 /** 592 * Callback for the dragstart event. 593 * @param {Event} e The dragstart event. 594 */ 595 handleDragStart: function(e) { 596 // Determine the selected bookmarks. 597 var target = e.target; 598 var draggedNodes = []; 599 if (target instanceof ListItem) { 600 // Use selected items. 601 draggedNodes = target.parentNode.selectedItems; 602 } else if (target instanceof TreeItem) { 603 draggedNodes.push(target.bookmarkNode); 604 } 605 606 // We manage starting the drag by using the extension API. 607 e.preventDefault(); 608 609 if (draggedNodes.length) { 610 // If we are dragging a single link we can do the *Link* effect, otherwise 611 // we only allow copy and move. 612 var effectAllowed; 613 if (draggedNodes.length == 1 && 614 !bmm.isFolder(draggedNodes[0])) { 615 effectAllowed = 'copyMoveLink'; 616 } else { 617 effectAllowed = 'copyMove'; 618 } 619 e.dataTransfer.effectAllowed = effectAllowed; 620 621 var ids = draggedNodes.map(function(node) { 622 return node.id; 623 }); 624 625 chrome.experimental.bookmarkManager.startDrag(ids); 626 } 627 }, 628 629 handleDragEnter: function(e) { 630 e.preventDefault(); 631 }, 632 633 /** 634 * Calback for the dragover event. 635 * @param {Event} e The dragover event. 636 */ 637 handleDragOver: function(e) { 638 // TODO(arv): This function is way too long. Please refactor it. 639 640 // Allow DND on text inputs. 641 if (e.target.tagName != 'INPUT') { 642 // The default operation is to allow dropping links etc to do navigation. 643 // We never want to do that for the bookmark manager. 644 e.preventDefault(); 645 646 // Set to none. This will get set to something if we can do the drop. 647 e.dataTransfer.dropEffect = 'none'; 648 } 649 650 if (!this.dragData) 651 return; 652 653 var overElement = this.getBookmarkElement(e.target); 654 if (!overElement && e.target == list) 655 overElement = list; 656 657 if (!overElement) 658 return; 659 660 var overBookmarkNode = overElement.bookmarkNode; 661 662 if (!this.canDrop(overBookmarkNode, overElement)) 663 return; 664 665 var bookmarkNode = overElement.bookmarkNode; 666 667 var canDropAbove = this.canDropAbove(overBookmarkNode, overElement); 668 var canDropOn = this.canDropOn(overBookmarkNode, overElement); 669 var canDropBelow = this.canDropBelow(overBookmarkNode, overElement); 670 671 if (!canDropAbove && !canDropOn && !canDropBelow) 672 return; 673 674 // Now we know that we can drop. Determine if we will drop above, on or 675 // below based on mouse position etc. 676 677 var dropPos; 678 679 e.dataTransfer.dropEffect = this.dragData.sameProfile ? 'move' : 'copy'; 680 681 var rect; 682 if (overElement instanceof TreeItem) { 683 // We only want the rect of the row representing the item and not 684 // its children 685 rect = overElement.rowElement.getBoundingClientRect(); 686 } else { 687 rect = overElement.getBoundingClientRect(); 688 } 689 690 var dy = e.clientY - rect.top; 691 var yRatio = dy / rect.height; 692 693 // above 694 if (canDropAbove && 695 (yRatio <= .25 || yRatio <= .5 && !(canDropBelow && canDropOn))) { 696 dropPos = 'above'; 697 698 // below 699 } else if (canDropBelow && 700 (yRatio > .75 || yRatio > .5 && !(canDropAbove && canDropOn))) { 701 dropPos = 'below'; 702 703 // on 704 } else if (canDropOn) { 705 dropPos = 'on'; 706 707 // none 708 } else { 709 // No drop can happen. Exit now. 710 e.dataTransfer.dropEffect = 'none'; 711 return; 712 } 713 714 function cloneClientRect(rect) { 715 var newRect = {}; 716 for (var key in rect) { 717 newRect[key] = rect[key]; 718 } 719 return newRect; 720 } 721 722 // If we are dropping above or below a tree item adjust the width so 723 // that it is clearer where the item will be dropped. 724 if ((dropPos == 'above' || dropPos == 'below') && 725 overElement instanceof TreeItem) { 726 // ClientRect is read only so clone in into a read-write object. 727 rect = cloneClientRect(rect); 728 var rtl = getComputedStyle(overElement).direction == 'rtl'; 729 var labelElement = overElement.labelElement; 730 var labelRect = labelElement.getBoundingClientRect(); 731 if (rtl) { 732 rect.width = labelRect.left + labelRect.width - rect.left; 733 } else { 734 rect.left = labelRect.left; 735 rect.width -= rect.left 736 } 737 } 738 739 var overlayType = dropPos; 740 741 // If we are dropping on a list we want to show a overlay drop line after 742 // the last element 743 if (overElement instanceof BookmarkList) { 744 overlayType = 'below'; 745 746 // Get the rect of the last list item. 747 var length = overElement.dataModel.length; 748 if (length) { 749 dropPos = 'below'; 750 overElement = overElement.getListItemByIndex(length - 1); 751 rect = overElement.getBoundingClientRect(); 752 } else { 753 // If there are no items, collapse the height of the rect 754 rect = cloneClientRect(rect); 755 rect.height = 0; 756 // We do not use bottom so we don't care to adjust it. 757 } 758 } 759 760 this.showDropOverlay_(rect, overlayType); 761 762 this.dropDestination = { 763 dropPos: dropPos, 764 relatedNode: overElement.bookmarkNode 765 }; 766 }, 767 768 /** 769 * Shows and positions the drop marker overlay. 770 * @param {ClientRect} targetRect The drop target rect 771 * @param {string} overlayType The position relative to the target rect. 772 * @private 773 */ 774 showDropOverlay_: function(targetRect, overlayType) { 775 window.clearTimeout(this.hideDropOverlayTimer_); 776 var overlay = $('drop-overlay'); 777 if (overlayType == 'on') { 778 overlay.className = ''; 779 overlay.style.top = targetRect.top + 'px'; 780 overlay.style.height = targetRect.height + 'px'; 781 } else { 782 overlay.className = 'line'; 783 overlay.style.height = ''; 784 } 785 overlay.style.width = targetRect.width + 'px'; 786 overlay.style.left = targetRect.left + 'px'; 787 overlay.style.display = 'block'; 788 789 if (overlayType != 'on') { 790 var overlayRect = overlay.getBoundingClientRect(); 791 if (overlayType == 'above') { 792 overlay.style.top = targetRect.top - overlayRect.height / 2 + 'px'; 793 } else { 794 overlay.style.top = targetRect.top + targetRect.height - 795 overlayRect.height / 2 + 'px'; 796 } 797 } 798 }, 799 800 /** 801 * Hides the drop overlay element. 802 * @private 803 */ 804 hideDropOverlay_: function() { 805 // Hide the overlay in a timeout to reduce flickering as we move between 806 // valid drop targets. 807 window.clearTimeout(this.hideDropOverlayTimer_); 808 this.hideDropOverlayTimer_ = window.setTimeout(function() { 809 $('drop-overlay').style.display = ''; 810 }, 100); 811 }, 812 813 handleDragLeave: function(e) { 814 this.hideDropOverlay_(); 815 }, 816 817 handleDrop: function(e) { 818 if (this.dropDestination && this.dragData) { 819 var dropPos = this.dropDestination.dropPos; 820 var relatedNode = this.dropDestination.relatedNode; 821 var parentId = dropPos == 'on' ? relatedNode.id : relatedNode.parentId; 822 823 var selectTarget; 824 var selectedTreeId; 825 var index; 826 var relatedIndex; 827 // Try to find the index in the dataModel so we don't have to always keep 828 // the index for the list items up to date. 829 var overElement = this.getBookmarkElement(e.target); 830 if (overElement instanceof ListItem) { 831 relatedIndex = overElement.parentNode.dataModel.indexOf(relatedNode); 832 selectTarget = list; 833 } else if (overElement instanceof BookmarkList) { 834 relatedIndex = overElement.dataModel.length - 1; 835 selectTarget = list; 836 } else { 837 // Tree 838 relatedIndex = relatedNode.index; 839 selectTarget = tree; 840 selectedTreeId = 841 tree.selectedItem ? tree.selectedItem.bookmarkId : null; 842 } 843 844 if (dropPos == 'above') 845 index = relatedIndex; 846 else if (dropPos == 'below') 847 index = relatedIndex + 1; 848 849 selectItemsAfterUserAction(selectTarget, selectedTreeId); 850 851 if (index != undefined && index != -1) 852 chrome.experimental.bookmarkManager.drop(parentId, index); 853 else 854 chrome.experimental.bookmarkManager.drop(parentId); 855 856 // TODO(arv): Select the newly dropped items. 857 } 858 this.dropDestination = null; 859 this.hideDropOverlay_(); 860 }, 861 862 clearDragData: function() { 863 this.dragData = null; 864 }, 865 866 handleChromeDragEnter: function(dragData) { 867 this.dragData = dragData; 868 }, 869 870 init: function() { 871 var boundClearData = this.clearDragData.bind(this); 872 function deferredClearData() { 873 setTimeout(boundClearData); 874 } 875 876 document.addEventListener('dragstart', this.handleDragStart.bind(this)); 877 document.addEventListener('dragenter', this.handleDragEnter.bind(this)); 878 document.addEventListener('dragover', this.handleDragOver.bind(this)); 879 document.addEventListener('dragleave', this.handleDragLeave.bind(this)); 880 document.addEventListener('drop', this.handleDrop.bind(this)); 881 document.addEventListener('dragend', deferredClearData); 882 document.addEventListener('mouseup', deferredClearData); 883 884 chrome.experimental.bookmarkManager.onDragEnter.addListener( 885 this.handleChromeDragEnter.bind(this)); 886 chrome.experimental.bookmarkManager.onDragLeave.addListener( 887 deferredClearData); 888 chrome.experimental.bookmarkManager.onDrop.addListener(deferredClearData); 889 } 890 }; 891 892 dnd.init(); 893 894 // Commands 895 896 cr.ui.decorate('menu', Menu); 897 cr.ui.decorate('button[menu]', MenuButton); 898 cr.ui.decorate('command', Command); 899 900 cr.ui.contextMenuHandler.addContextMenuProperty(tree); 901 list.contextMenu = $('context-menu'); 902 tree.contextMenu = $('context-menu'); 903 904 // Disable almost all commands at startup. 905 var commands = document.querySelectorAll('command'); 906 for (var i = 0, command; command = commands[i]; i++) { 907 if (command.id != 'import-menu-command' && 908 command.id != 'export-menu-command') { 909 command.disabled = true; 910 } 911 } 912 913 /** 914 * Helper function that updates the canExecute and labels for the open like 915 * commands. 916 * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system. 917 * @param {!cr.ui.Command} command The command we are currently precessing. 918 */ 919 function updateOpenCommands(e, command) { 920 var selectedItem = e.target.selectedItem; 921 var selectionCount; 922 if (e.target == tree) { 923 selectionCount = selectedItem ? 1 : 0; 924 selectedItem = selectedItem.bookmarkNode; 925 } else { 926 selectionCount = e.target.selectedItems.length; 927 } 928 929 var isFolder = selectionCount == 1 && 930 selectedItem && 931 bmm.isFolder(selectedItem); 932 var multiple = selectionCount != 1 || isFolder; 933 934 function hasBookmarks(node) { 935 for (var i = 0; i < node.children.length; i++) { 936 if (!bmm.isFolder(node.children[i])) 937 return true; 938 } 939 return false; 940 } 941 942 switch (command.id) { 943 case 'open-in-new-tab-command': 944 command.label = localStrings.getString(multiple ? 945 'open_all' : 'open_in_new_tab'); 946 break; 947 948 case 'open-in-new-window-command': 949 command.label = localStrings.getString(multiple ? 950 'open_all_new_window' : 'open_in_new_window'); 951 break; 952 case 'open-incognito-window-command': 953 command.label = localStrings.getString(multiple ? 954 'open_all_incognito' : 'open_incognito'); 955 break; 956 } 957 e.canExecute = selectionCount > 0 && !!selectedItem; 958 if (isFolder && e.canExecute) { 959 // We need to get all the bookmark items in this tree. If the tree does not 960 // contain any non-folders we need to disable the command. 961 var p = bmm.loadSubtree(selectedItem.id); 962 p.addListener(function(node) { 963 command.disabled = !node || !hasBookmarks(node); 964 }); 965 } 966 } 967 968 /** 969 * Calls the backend to figure out if we can paste the clipboard into the active 970 * folder. 971 * @param {Function=} opt_f Function to call after the state has been 972 * updated. 973 */ 974 function updatePasteCommand(opt_f) { 975 function update(canPaste) { 976 var command = $('paste-command'); 977 command.disabled = !canPaste; 978 if (opt_f) 979 opt_f(); 980 } 981 // We cannot paste into search and recent view. 982 if (list.isSearch() || list.isRecent()) { 983 update(false); 984 } else { 985 chrome.experimental.bookmarkManager.canPaste(list.parentId, update); 986 } 987 } 988 989 // We can always execute the import-menu and export-menu commands. 990 document.addEventListener('canExecute', function(e) { 991 var command = e.command; 992 var commandId = command.id; 993 if (commandId == 'import-menu-command' || 994 commandId == 'export-menu-command') { 995 e.canExecute = true; 996 } 997 }); 998 999 /** 1000 * Helper function for handling canExecute for the list and the tree. 1001 * @param {!Event} e Can exectue event object. 1002 * @param {boolean} isRecentOrSearch Whether the user is trying to do a command 1003 * on recent or search. 1004 */ 1005 function canExcuteShared(e, isRecentOrSearch) { 1006 var command = e.command; 1007 var commandId = command.id; 1008 switch (commandId) { 1009 case 'paste-command': 1010 updatePasteCommand(); 1011 break; 1012 1013 case 'sort-command': 1014 if (isRecentOrSearch) { 1015 e.canExecute = false; 1016 } else { 1017 e.canExecute = list.dataModel.length > 0; 1018 1019 // The list might be loading so listen to the load event. 1020 var f = function() { 1021 list.removeEventListener('load', f); 1022 command.disabled = list.dataModel.length == 0; 1023 }; 1024 list.addEventListener('load', f); 1025 } 1026 break; 1027 1028 case 'add-new-bookmark-command': 1029 case 'new-folder-command': 1030 e.canExecute = !isRecentOrSearch; 1031 break; 1032 1033 case 'open-in-new-tab-command': 1034 case 'open-in-background-tab-command': 1035 case 'open-in-new-window-command': 1036 case 'open-incognito-window-command': 1037 updateOpenCommands(e, command); 1038 break; 1039 } 1040 } 1041 1042 // Update canExecute for the commands when the list is the active element. 1043 list.addEventListener('canExecute', function(e) { 1044 if (e.target != list) return; 1045 1046 var command = e.command; 1047 var commandId = command.id; 1048 1049 function hasSelected() { 1050 return !!e.target.selectedItem; 1051 } 1052 1053 function hasSingleSelected() { 1054 return e.target.selectedItems.length == 1; 1055 } 1056 1057 function isRecentOrSearch() { 1058 return list.isRecent() || list.isSearch(); 1059 } 1060 1061 switch (commandId) { 1062 case 'rename-folder-command': 1063 // Show rename if a single folder is selected 1064 var items = e.target.selectedItems; 1065 if (items.length != 1) { 1066 e.canExecute = false; 1067 command.hidden = true; 1068 } else { 1069 var isFolder = bmm.isFolder(items[0]); 1070 e.canExecute = isFolder; 1071 command.hidden = !isFolder; 1072 } 1073 break; 1074 1075 case 'edit-command': 1076 // Show the edit command if not a folder 1077 var items = e.target.selectedItems; 1078 if (items.length != 1) { 1079 e.canExecute = false; 1080 command.hidden = false; 1081 } else { 1082 var isFolder = bmm.isFolder(items[0]); 1083 e.canExecute = !isFolder; 1084 command.hidden = isFolder; 1085 } 1086 break; 1087 1088 case 'show-in-folder-command': 1089 e.canExecute = isRecentOrSearch() && hasSingleSelected(); 1090 break; 1091 1092 case 'delete-command': 1093 case 'cut-command': 1094 case 'copy-command': 1095 e.canExecute = hasSelected(); 1096 break; 1097 1098 case 'open-in-same-window-command': 1099 e.canExecute = hasSelected(); 1100 break; 1101 1102 default: 1103 canExcuteShared(e, isRecentOrSearch()); 1104 } 1105 }); 1106 1107 // Update canExecute for the commands when the tree is the active element. 1108 tree.addEventListener('canExecute', function(e) { 1109 if (e.target != tree) return; 1110 1111 var command = e.command; 1112 var commandId = command.id; 1113 1114 function hasSelected() { 1115 return !!e.target.selectedItem; 1116 } 1117 1118 function isRecentOrSearch() { 1119 var item = e.target.selectedItem; 1120 return item == recentTreeItem || item == searchTreeItem; 1121 } 1122 1123 function isTopLevelItem() { 1124 return e.target.selectedItem.parentNode == tree; 1125 } 1126 1127 switch (commandId) { 1128 case 'rename-folder-command': 1129 command.hidden = false; 1130 e.canExecute = hasSelected() && !isTopLevelItem(); 1131 break; 1132 1133 case 'edit-command': 1134 command.hidden = true; 1135 e.canExecute = false; 1136 break; 1137 1138 case 'delete-command': 1139 case 'cut-command': 1140 case 'copy-command': 1141 e.canExecute = hasSelected() && !isTopLevelItem(); 1142 break; 1143 1144 default: 1145 canExcuteShared(e, isRecentOrSearch()); 1146 } 1147 }); 1148 1149 /** 1150 * Update the canExecute state of the commands when the selection changes. 1151 * @param {Event} e The change event object. 1152 */ 1153 function updateCommandsBasedOnSelection(e) { 1154 if (e.target == document.activeElement) { 1155 // Paste only needs to updated when the tree selection changes. 1156 var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit', 1157 'add-new-bookmark', 'new-folder', 'open-in-new-tab', 1158 'open-in-new-window', 'open-incognito-window', 'open-in-same-window']; 1159 1160 if (e.target == tree) { 1161 commandNames.push('paste', 'show-in-folder', 'sort'); 1162 } 1163 1164 commandNames.forEach(function(baseId) { 1165 $(baseId + '-command').canExecuteChange(); 1166 }); 1167 } 1168 } 1169 1170 list.addEventListener('change', updateCommandsBasedOnSelection); 1171 tree.addEventListener('change', updateCommandsBasedOnSelection); 1172 1173 document.addEventListener('command', function(e) { 1174 var command = e.command; 1175 var commandId = command.id; 1176 console.log(command.id, 'executed', 'on', e.target); 1177 if (commandId == 'import-menu-command') { 1178 // Set a flag on the list so we can select the newly imported folder. 1179 list.selectImportedFolder = true; 1180 chrome.bookmarks.import(); 1181 } else if (command.id == 'export-menu-command') { 1182 chrome.bookmarks.export(); 1183 } 1184 }); 1185 1186 function handleRename(e) { 1187 var item = e.target; 1188 var bookmarkNode = item.bookmarkNode; 1189 chrome.bookmarks.update(bookmarkNode.id, {title: item.label}); 1190 } 1191 1192 tree.addEventListener('rename', handleRename); 1193 list.addEventListener('rename', handleRename); 1194 1195 list.addEventListener('edit', function(e) { 1196 var item = e.target; 1197 var bookmarkNode = item.bookmarkNode; 1198 var context = { 1199 title: bookmarkNode.title 1200 }; 1201 if (!bmm.isFolder(bookmarkNode)) 1202 context.url = bookmarkNode.url; 1203 1204 if (bookmarkNode.id == 'new') { 1205 selectItemsAfterUserAction(list); 1206 1207 // New page 1208 context.parentId = bookmarkNode.parentId; 1209 chrome.bookmarks.create(context, function(node) { 1210 // A new node was created and will get added to the list due to the 1211 // handler. 1212 var dataModel = list.dataModel; 1213 var index = dataModel.indexOf(bookmarkNode); 1214 dataModel.splice(index, 1); 1215 1216 // Select new item. 1217 var newIndex = dataModel.findIndexById(node.id); 1218 if (newIndex != -1) { 1219 var sm = list.selectionModel; 1220 list.scrollIndexIntoView(newIndex); 1221 sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex; 1222 } 1223 }); 1224 } else { 1225 // Edit 1226 chrome.bookmarks.update(bookmarkNode.id, context); 1227 } 1228 }); 1229 1230 list.addEventListener('canceledit', function(e) { 1231 var item = e.target; 1232 var bookmarkNode = item.bookmarkNode; 1233 if (bookmarkNode.id == 'new') { 1234 var dataModel = list.dataModel; 1235 var index = dataModel.findIndexById('new'); 1236 dataModel.splice(index, 1); 1237 } 1238 }); 1239 1240 /** 1241 * Navigates to the folder that the selected item is in and selects it. This is 1242 * used for the show-in-folder command. 1243 */ 1244 function showInFolder() { 1245 var bookmarkNode = list.selectedItem; 1246 var parentId = bookmarkNode.parentId; 1247 1248 // After the list is loaded we should select the revealed item. 1249 function f(e) { 1250 var index; 1251 if (bookmarkNode && 1252 (index = list.dataModel.findIndexById(bookmarkNode.id)) != -1) { 1253 var sm = list.selectionModel; 1254 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index; 1255 list.scrollIndexIntoView(index); 1256 } 1257 list.removeEventListener('load', f); 1258 } 1259 list.addEventListener('load', f); 1260 var treeItem = bmm.treeLookup[parentId]; 1261 treeItem.reveal(); 1262 1263 navigateTo(parentId); 1264 } 1265 1266 var linkController; 1267 1268 /** 1269 * @return {!cr.LinkController} The link controller used to open links based on 1270 * user clicks and keyboard actions. 1271 */ 1272 function getLinkController() { 1273 return linkController || 1274 (linkController = new cr.LinkController(localStrings)); 1275 } 1276 1277 /** 1278 * Returns the selected bookmark nodes of the active element. Only call this 1279 * if the list or the tree is focused. 1280 * @return {!Array} Array of bookmark nodes. 1281 */ 1282 function getSelectedBookmarkNodes() { 1283 if (document.activeElement == list) { 1284 return list.selectedItems; 1285 } else if (document.activeElement == tree) { 1286 return [tree.selectedItem.bookmarkNode]; 1287 } else { 1288 throw Error('getSelectedBookmarkNodes called when wrong element focused.'); 1289 } 1290 } 1291 1292 /** 1293 * @return {!Array.<string>} An array of the selected bookmark IDs. 1294 */ 1295 function getSelectedBookmarkIds() { 1296 return getSelectedBookmarkNodes().map(function(node) { 1297 return node.id; 1298 }); 1299 } 1300 1301 /** 1302 * Opens the selected bookmarks. 1303 * @param {LinkKind} kind The kind of link we want to open. 1304 */ 1305 function openBookmarks(kind) { 1306 // If we have selected any folders we need to find all items recursively. 1307 // We use multiple async calls to getSubtree instead of getting the whole 1308 // tree since we would like to minimize the amount of data sent. 1309 1310 var urls = []; 1311 1312 // Adds the node and all its children. 1313 function addNodes(node) { 1314 if (node.children) { 1315 node.children.forEach(function(child) { 1316 if (!bmm.isFolder(child)) 1317 urls.push(child.url); 1318 }); 1319 } else { 1320 urls.push(node.url); 1321 } 1322 } 1323 1324 var nodes = getSelectedBookmarkNodes(); 1325 1326 // Get a future promise for every selected item. 1327 var promises = nodes.map(function(node) { 1328 if (bmm.isFolder(node)) 1329 return bmm.loadSubtree(node.id); 1330 // Not a folder so we already have all the data we need. 1331 return new Promise(node.url); 1332 }); 1333 1334 var p = Promise.all.apply(null, promises); 1335 p.addListener(function(values) { 1336 values.forEach(function(v) { 1337 if (typeof v == 'string') 1338 urls.push(v); 1339 else 1340 addNodes(v); 1341 }); 1342 getLinkController().openUrls(urls, kind); 1343 }); 1344 } 1345 1346 /** 1347 * Opens an item in the list. 1348 */ 1349 function openItem() { 1350 var bookmarkNodes = getSelectedBookmarkNodes(); 1351 // If we double clicked or pressed enter on a single folder navigate to it. 1352 if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) { 1353 navigateTo(bookmarkNodes[0].id); 1354 } else { 1355 openBookmarks(LinkKind.FOREGROUND_TAB); 1356 } 1357 } 1358 1359 /** 1360 * Deletes the selected bookmarks. 1361 */ 1362 function deleteBookmarks() { 1363 getSelectedBookmarkIds().forEach(function(id) { 1364 chrome.bookmarks.removeTree(id); 1365 }); 1366 } 1367 1368 /** 1369 * Callback for the new folder command. This creates a new folder and starts 1370 * a rename of it. 1371 */ 1372 function newFolder() { 1373 var parentId = list.parentId; 1374 var isTree = document.activeElement == tree; 1375 chrome.bookmarks.create({ 1376 title: localStrings.getString('new_folder_name'), 1377 parentId: parentId 1378 }, function(newNode) { 1379 // This callback happens before the event that triggers the tree/list to 1380 // get updated so delay the work so that the tree/list gets updated first. 1381 setTimeout(function() { 1382 var newItem; 1383 if (isTree) { 1384 newItem = bmm.treeLookup[newNode.id]; 1385 tree.selectedItem = newItem; 1386 newItem.editing = true; 1387 } else { 1388 var index = list.dataModel.findIndexById(newNode.id); 1389 var sm = list.selectionModel; 1390 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index; 1391 scrollIntoViewAndMakeEditable(index); 1392 } 1393 }, 50); 1394 }); 1395 } 1396 1397 /** 1398 * Scrolls the list item into view and makes it editable. 1399 * @param {number} index The index of the item to make editable. 1400 */ 1401 function scrollIntoViewAndMakeEditable(index) { 1402 list.scrollIndexIntoView(index); 1403 // onscroll is now dispatched asynchronously so we have to postpone 1404 // the rest. 1405 setTimeout(function() { 1406 var item = list.getListItemByIndex(index); 1407 if (item) 1408 item.editing = true; 1409 }); 1410 } 1411 1412 /** 1413 * Adds a page to the current folder. This is called by the 1414 * add-new-bookmark-command handler. 1415 */ 1416 function addPage() { 1417 var parentId = list.parentId; 1418 var fakeNode = { 1419 title: '', 1420 url: '', 1421 parentId: parentId, 1422 id: 'new' 1423 }; 1424 1425 var dataModel = list.dataModel; 1426 var length = dataModel.length; 1427 dataModel.splice(length, 0, fakeNode); 1428 var sm = list.selectionModel; 1429 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length; 1430 scrollIntoViewAndMakeEditable(length); 1431 } 1432 1433 /** 1434 * This function is used to select items after a user action such as paste, drop 1435 * add page etc. 1436 * @param {BookmarkList|BookmarkTree} target The target of the user action. 1437 * @param {=string} opt_selectedTreeId If provided, then select that tree id. 1438 */ 1439 function selectItemsAfterUserAction(target, opt_selectedTreeId) { 1440 // We get one onCreated event per item so we delay the handling until we got 1441 // no more events coming. 1442 1443 var ids = []; 1444 var timer; 1445 1446 function handle(id, bookmarkNode) { 1447 clearTimeout(timer); 1448 if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId) 1449 ids.push(id); 1450 timer = setTimeout(handleTimeout, 50); 1451 } 1452 1453 function handleTimeout() { 1454 chrome.bookmarks.onCreated.removeListener(handle); 1455 chrome.bookmarks.onMoved.removeListener(handle); 1456 1457 if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) { 1458 var index = ids.indexOf(opt_selectedTreeId); 1459 if (index != -1 && opt_selectedTreeId in bmm.treeLookup) { 1460 tree.selectedItem = bmm.treeLookup[opt_selectedTreeId]; 1461 } 1462 } else if (target == list) { 1463 var dataModel = list.dataModel; 1464 var firstIndex = dataModel.findIndexById(ids[0]); 1465 var lastIndex = dataModel.findIndexById(ids[ids.length - 1]); 1466 if (firstIndex != -1 && lastIndex != -1) { 1467 var selectionModel = list.selectionModel; 1468 selectionModel.selectedIndex = -1; 1469 selectionModel.selectRange(firstIndex, lastIndex); 1470 selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex; 1471 list.focus(); 1472 } 1473 } 1474 1475 list.endBatchUpdates(); 1476 } 1477 1478 list.startBatchUpdates(); 1479 1480 chrome.bookmarks.onCreated.addListener(handle); 1481 chrome.bookmarks.onMoved.addListener(handle); 1482 timer = setTimeout(handleTimeout, 300); 1483 } 1484 1485 /** 1486 * Handler for the command event. This is used both for the tree and the list. 1487 * @param {!Event} e The event object. 1488 */ 1489 function handleCommand(e) { 1490 var command = e.command; 1491 var commandId = command.id; 1492 switch (commandId) { 1493 case 'show-in-folder-command': 1494 showInFolder(); 1495 break; 1496 case 'open-in-new-tab-command': 1497 openBookmarks(LinkKind.FOREGROUND_TAB); 1498 break; 1499 case 'open-in-background-tab-command': 1500 openBookmarks(LinkKind.BACKGROUND_TAB); 1501 break; 1502 case 'open-in-new-window-command': 1503 openBookmarks(LinkKind.WINDOW); 1504 break; 1505 case 'open-incognito-window-command': 1506 openBookmarks(LinkKind.INCOGNITO); 1507 break; 1508 case 'delete-command': 1509 deleteBookmarks(); 1510 break; 1511 case 'copy-command': 1512 chrome.experimental.bookmarkManager.copy(getSelectedBookmarkIds(), 1513 updatePasteCommand); 1514 break; 1515 case 'cut-command': 1516 chrome.experimental.bookmarkManager.cut(getSelectedBookmarkIds(), 1517 updatePasteCommand); 1518 break; 1519 case 'paste-command': 1520 selectItemsAfterUserAction(list); 1521 chrome.experimental.bookmarkManager.paste(list.parentId, 1522 getSelectedBookmarkIds()); 1523 break; 1524 case 'sort-command': 1525 chrome.experimental.bookmarkManager.sortChildren(list.parentId); 1526 break; 1527 case 'rename-folder-command': 1528 case 'edit-command': 1529 if (document.activeElement == list) { 1530 var li = list.getListItem(list.selectedItem); 1531 if (li) 1532 li.editing = true; 1533 } else { 1534 document.activeElement.selectedItem.editing = true; 1535 } 1536 break; 1537 case 'new-folder-command': 1538 newFolder(); 1539 break; 1540 case 'add-new-bookmark-command': 1541 addPage(); 1542 break; 1543 case 'open-in-same-window-command': 1544 openItem(); 1545 break; 1546 } 1547 } 1548 1549 // Delete on all platforms. On Mac we also allow Meta+Backspace. 1550 $('delete-command').shortcut = 'U+007F' + 1551 (cr.isMac ? ' U+0008 Meta-U+0008' : ''); 1552 1553 $('open-in-same-window-command').shortcut = cr.isMac ? 'Meta-Down' : 1554 'Enter'; 1555 1556 $('open-in-new-window-command').shortcut = 'Shift-Enter'; 1557 $('open-in-background-tab-command').shortcut = cr.isMac ? 'Meta-Enter' : 1558 'Ctrl-Enter'; 1559 $('open-in-new-tab-command').shortcut = cr.isMac ? 'Shift-Meta-Enter' : 1560 'Shift-Ctrl-Enter'; 1561 1562 $('rename-folder-command').shortcut = $('edit-command').shortcut = 1563 cr.isMac ? 'Enter' : 'F2'; 1564 1565 list.addEventListener('command', handleCommand); 1566 tree.addEventListener('command', handleCommand); 1567 1568 // Execute the copy, cut and paste commands when those events are dispatched by 1569 // the browser. This allows us to rely on the browser to handle the keyboard 1570 // shortcuts for these commands. 1571 (function() { 1572 function handle(id) { 1573 return function(e) { 1574 var command = $(id); 1575 if (!command.disabled) { 1576 command.execute(); 1577 if (e) e.preventDefault(); // Prevent the system beep 1578 } 1579 }; 1580 } 1581 1582 // Listen to copy, cut and paste events and execute the associated commands. 1583 document.addEventListener('copy', handle('copy-command')); 1584 document.addEventListener('cut', handle('cut-command')); 1585 1586 var pasteHandler = handle('paste-command'); 1587 document.addEventListener('paste', function(e) { 1588 // Paste is a bit special since we need to do an async call to see if we can 1589 // paste because the paste command might not be up to date. 1590 updatePasteCommand(pasteHandler); 1591 }); 1592 })(); 1593