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