<!DOCTYPE html> <html i18n-values="dir:textdirection"> <!-- Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. This is work in progress: Favicons: chrome-extension: is not allowed to access chrome://favicon. We need to whitelist it or expose a way to get the data URI for the favicon (slow and sucky). Favicon of bmm does not work. No icon is showed. --> <head> <title i18n-content="title"></title> <link rel="stylesheet" href="css/list.css"> <link rel="stylesheet" href="css/tree.css"> <link rel="stylesheet" href="css/menu.css"> <link rel="stylesheet" href="css/bmm.css"> <script src="css/tree.css.js"></script> <script src="css/bmm.css.js"></script> <script src="js/cr.js"></script> <script src="js/cr/event.js"></script> <script src="js/cr/eventtarget.js"></script> <script src="js/cr/promise.js"></script> <script src="js/cr/ui.js"></script> <script src="js/cr/ui/listselectionmodel.js"></script> <script src="js/cr/ui/listitem.js"></script> <script src="js/cr/ui/list.js"></script> <script src="js/cr/ui/tree.js"></script> <script src="js/cr/ui/command.js"></script> <script src="js/cr/ui/menuitem.js"></script> <script src="js/cr/ui/menu.js"></script> <script src="js/cr/ui/menubutton.js"></script> <script src="js/cr/ui/contextmenuhandler.js"></script> <script src="js/util.js"></script> <script src="js/localstrings.js"></script> <script src="js/i18ntemplate.js"></script> <script src="js/bmm/treeiterator.js"></script> <script src="js/bmm.js"></script> <script src="js/bmm/bookmarklist.js"></script> <script src="js/bmm/bookmarktree.js"></script> <script> // Sometimes the extension API is not initialized. if (!chrome.bookmarks) window.location.reload(); // Allow platform specific CSS rules. if (cr.isMac) document.documentElement.setAttribute('os', 'mac'); </script> </head> <body i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> <div class="header"> <button onclick="resetSearch()" class="logo" tabindex=3></button> <div> <form onsubmit="setSearch(this.term.value); return false;" class="form"> <input type="text" id="term" tabindex=1 autofocus> <input type="submit" i18n-values=".value:search_button" tabindex=1> </form> <div class=toolbar> <button menu="#organize-menu" tabindex="-1" i18n-content="organize_menu"></button> <button menu="#tools-menu" tabindex="-1" i18n-content="tools_menu"></button> </div> </div> </div> <div class=main> <div id=tree-container> <tree id=tree tabindex=2></tree> </div> <list id=list tabindex=2></list> </div> <script> const BookmarkList = bmm.BookmarkList; const BookmarkTree = bmm.BookmarkTree; const ListItem = cr.ui.ListItem; const TreeItem = cr.ui.TreeItem; /** * The id of the bookmark root. * @type {number} */ const ROOT_ID = '0'; var bookmarkCache = { /** * This returns a reference to the bookmark node that is cached by the tree * or list. Use this funciton when we need to update the local cachea after * changes. It only returns bookmarks that are used by the tree and/or the * list. * @param {string} The ID of the bookmark that we want to get. * @return {BookmarkTreeNode} */ getById: function(id) { var el = bmm.treeLookup[id] || bmm.listLookup[id]; return el && el.bookmarkNode; }, /** * Removes the cached item from both the list and tree lookups. */ remove: function(id) { delete bmm.listLookup[id]; var treeItem = bmm.treeLookup[id]; if (treeItem) { var items = treeItem.items; // is an HTMLCollection for (var i = 0, item; item = items[i]; i++) { var bookmarkNode = item.bookmarkNode; delete bmm.treeLookup[bookmarkNode.id]; } delete bmm.treeLookup[id]; } }, /** * Updates the underlying bookmark node for the tree items and list items by * querying the bookmark backend. * @param {string} id The id of the node to update the children for. * @param {Function=} opt_f A funciton to call when done. */ updateChildren: function(id, opt_f) { function updateItem(bookmarkNode) { var treeItem = bmm.treeLookup[bookmarkNode.id]; if (treeItem) { treeItem.bookmarkNode = bookmarkNode; } var listItem = bmm.listLookup[bookmarkNode.id]; if (listItem) { listItem.bookmarkNode = bookmarkNode; } } chrome.bookmarks.getChildren(id, function(children) { children.forEach(updateItem); if (opt_f) opt_f(children); }); } }; </script> <script> BookmarkList.decorate(list); var searchTreeItem = new TreeItem({ label: 'Search', icon: 'images/bookmark_manager_search.png', bookmarkId: 'q=' }); bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem; var recentTreeItem = new TreeItem({ label: 'Recent', icon: 'images/bookmark_manager_recent.png', bookmarkId: 'recent' }); bmm.treeLookup[recentTreeItem.bookmarkId] = recentTreeItem; BookmarkTree.decorate(tree); tree.addEventListener('change', function() { navigateTo(tree.selectedItem.bookmarkId); }); /** * Navigates to a bookmark ID. * @param {string} id The ID to navigate to. */ function navigateTo(id) { console.info('navigateTo', window.location.hash, id); // Update the location hash using a timer to prevent reentrancy. This is how // often we add history entries and the time here is a bit arbitrary but was // picked as the smallest time a human perceives as instant. clearTimeout(navigateTo.timer_); navigateTo.timer_ = setTimeout(function() { window.location.hash = tree.selectedItem.bookmarkId; }, 300); updateParentId(id); } /** * Updates the parent ID of the bookmark list and selects the correct tree item. * @param {string} id The id. */ function updateParentId(id) { list.parentId = id; if (id in bmm.treeLookup) tree.selectedItem = bmm.treeLookup[id]; } // We listen to hashchange so that we can update the currently shown folder when // the user goes back and forward in the history. window.onhashchange = function(e) { var id = window.location.hash.slice(1); var valid = false; // In case we got a search hash update the text input and the bmm.treeLookup // to use the new id. if (/^q=/.test(id)) { setSearch(id.slice(2)); valid = true; } else if (id == 'recent') { valid = true; } if (valid) { updateParentId(id); } else { // We need to verify that this is a correct ID. chrome.bookmarks.get(id, function(items) { if (items && items.length == 1) updateParentId(id); }); } }; list.addEventListener('activate', function(e) { var bookmarkNodes = getSelectedBookmarkNodes(); // If we double clicked or pressed enter on a single folder navigate to it. if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) { navigateTo(bookmarkNodes[0].id); } else { var command = $('open-in-new-tab-command'); command.execute(); } }); // The list dispatches an event when the user clicks on the URL or the Show in // folder part. list.addEventListener('urlClicked', function(e) { openUrls([e.url], e.kind); }); /** * Navigates to the search results for the search text. * @para {string} searchText The text to search for. */ function setSearch(searchText) { delete bmm.treeLookup[searchTreeItem.bookmarkId]; searchTreeItem.bookmarkId = 'q=' + searchText; bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem; tree.selectedItem = searchTreeItem; } /** * Clears the search. */ function resetSearch() { $('term').value = ''; setSearch(''); $('term').focus(); } /** * Called when the title of a bookmark changes. * @param {string} id * @param {!Object} changeInfo */ function handleBookmarkChanged(id, changeInfo) { // console.log('handleBookmarkChanged', id, changeInfo); list.handleBookmarkChanged(id, changeInfo); tree.handleBookmarkChanged(id, changeInfo); } /** * Callback for when the user reorders by title. * @param {string} id The id of the bookmark folder that was reordered. * @param {!Object} reorderInfo The information about how the items where * reordered. */ function handleChildrenReordered(id, reorderInfo) { // console.info('handleChildrenReordered', id, reorderInfo); list.handleChildrenReordered(id, reorderInfo); tree.handleChildrenReordered(id, reorderInfo); bookmarkCache.updateChildren(id); } /** * Callback for when a bookmark node is created. * @param {string} id The id of the newly created bookmark node. * @param {!Object} bookmarkNode The new bookmark node. */ function handleCreated(id, bookmarkNode) { // console.info('handleCreated', id, bookmarkNode); list.handleCreated(id, bookmarkNode); tree.handleCreated(id, bookmarkNode); bookmarkCache.updateChildren(bookmarkNode.parentId); } function handleMoved(id, moveInfo) { // console.info('handleMoved', id, moveInfo); list.handleMoved(id, moveInfo); tree.handleMoved(id, moveInfo); bookmarkCache.updateChildren(moveInfo.parentId); if (moveInfo.parentId != moveInfo.oldParentId) bookmarkCache.updateChildren(moveInfo.oldParentId); } function handleRemoved(id, removeInfo) { // console.info('handleRemoved', id, removeInfo); list.handleRemoved(id, removeInfo); tree.handleRemoved(id, removeInfo); bookmarkCache.updateChildren(removeInfo.parentId); bookmarkCache.remove(id); } function handleImportBegan() { chrome.bookmarks.onCreated.removeListener(handleCreated); } function handleImportEnded() { chrome.bookmarks.onCreated.addListener(handleCreated); var p = bmm.loadTree(); p.addListener(function(node) { var otherBookmarks = node.children[1].children; var importedFolder = otherBookmarks[otherBookmarks.length - 1]; var importId = importedFolder.id; tree.insertSubtree(importedFolder); navigateTo(importId) }); } /** * Adds the listeners for the bookmark model change events. */ function addBookmarkModelListeners() { chrome.bookmarks.onChanged.addListener(handleBookmarkChanged); chrome.bookmarks.onChildrenReordered.addListener(handleChildrenReordered); chrome.bookmarks.onCreated.addListener(handleCreated); chrome.bookmarks.onMoved.addListener(handleMoved); chrome.bookmarks.onRemoved.addListener(handleRemoved); chrome.experimental.bookmarkManager.onImportBegan.addListener( handleImportBegan); chrome.experimental.bookmarkManager.onImportEnded.addListener( handleImportEnded); } /** * This returns the user visible path to the folder where the bookmark is * located. * @param {number} parentId The ID of the parent folder. * @return {string} The path to the the bookmark, */ function getFolder(parentId) { var parentNode = tree.getBookmarkNodeById(parentId); if (parentNode) { var s = parentNode.title; if (parentNode.parentId != ROOT_ID) { return getFolder(parentNode.parentId) + '/' + s; } return s; } } tree.addEventListener('load', function(e) { // Add hard coded tree items tree.add(recentTreeItem); tree.add(searchTreeItem); // Now we can select a tree item. var hash = window.location.hash.slice(1); if (!hash) { // If we do not have a hash select first item in the tree. hash = tree.items[0].bookmarkId; } if (/^q=/.test(hash)) { var searchTerm = hash.slice(2); $('term').value = searchTerm; setSearch(searchTerm); } else { navigateTo(hash); } }); tree.buildTree(); addBookmarkModelListeners(); var dnd = { DND_EFFECT_COPY: 'copy', DND_EFFECT_MOVE: cr.isMac ? 'move' : 'copy', // http://crbug.com/14654 dragData: null, getBookmarkElement: function(el) { while (el && !el.bookmarkNode) { el = el.parentNode; } return el; }, // If we are over the list and the list is showing recent or search result // we cannot drop. isOverRecentOrSearch: function(overElement) { return (list.isRecent() || list.isSearch()) && list.contains(overElement); }, checkEvery_: function(f, overBookmarkNode, overElement) { return this.dragData.elements.every(function(element) { return f.call(this, element, overBookmarkNode, overElement); }, this); }, /** * @return {boolean} Whether we are currently dragging any folders. */ isDraggingFolders: function() { return !!this.dragData && this.dragData.elements.some(function(node) { return !node.url; }); }, /** * This is a first pass wether we can drop the dragged items. * * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are * currently dragging over. * @param {!HTMLElement} overElement The element that we are currently * dragging over. * @return {boolean} If this returns false then we know we should not drop * the items. If it returns true we still have to call canDropOn, * canDropAbove and canDropBelow. */ canDrop: function(overBookmarkNode, overElement) { var dragData = this.dragData; if (!dragData) return false; if (this.isOverRecentOrSearch(overElement)) return false; if (!dragData.sameProfile) return true; return this.checkEvery_(this.canDrop_, overBookmarkNode, overElement); }, /** * Helper for canDrop that only checks one bookmark node. * @private */ canDrop_: function(dragNode, overBookmarkNode, overElement) { var dragId = dragNode.id; if (overBookmarkNode.id == dragId) return false; // If we are dragging a folder we cannot drop it on any of its descendants var dragBookmarkItem = bmm.treeLookup[dragId]; var dragBookmarkNode = dragBookmarkItem && dragBookmarkItem.bookmarkNode; if (dragBookmarkNode && bmm.contains(dragBookmarkNode, overBookmarkNode)) { return false; } return true; }, /** * Whether we can drop the dragged items above the drop target. * * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are * currently dragging over. * @param {!HTMLElement} overElement The element that we are currently * dragging over. * @return {boolean} Whether we can drop the dragged items above the drop * target. */ canDropAbove: function(overBookmarkNode, overElement) { if (overElement instanceof BookmarkList) return false; // We cannot drop between Bookmarks bar and Other bookmarks if (overBookmarkNode.parentId == ROOT_ID) return false; var isOverTreeItem = overElement instanceof TreeItem; // We can only drop between items in the tree if we have any folders. if (isOverTreeItem && !this.isDraggingFolders()) return false; if (!this.dragData.sameProfile) return this.isDraggingFolders() || !isOverTreeItem; return this.checkEvery_(this.canDropAbove_, overBookmarkNode, overElement); }, /** * Helper for canDropAbove that only checks one bookmark node. * @private */ canDropAbove_: function(dragNode, overBookmarkNode, overElement) { var dragId = dragNode.id; // We cannot drop above if the item below is already in the drag source var previousElement = overElement.previousElementSibling; if (previousElement && previousElement.bookmarkId == dragId) return false; return true; }, /** * Whether we can drop the dragged items below the drop target. * * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are * currently dragging over. * @param {!HTMLElement} overElement The element that we are currently * dragging over. * @return {boolean} Whether we can drop the dragged items below the drop * target. */ canDropBelow: function(overBookmarkNode, overElement) { if (overElement instanceof BookmarkList) return false; // We cannot drop between Bookmarks bar and Other bookmarks if (overBookmarkNode.parentId == ROOT_ID) return false; // We can only drop between items in the tree if we have any folders. if (!this.isDraggingFolders() && overElement instanceof TreeItem) return false; var isOverTreeItem = overElement instanceof TreeItem; // Don't allow dropping below an expanded tree item since it is confusing // to the user anyway. if (isOverTreeItem && overElement.expanded) return false; if (!this.dragData.sameProfile) return this.isDraggingFolders() || !isOverTreeItem; return this.checkEvery_(this.canDropBelow_, overBookmarkNode, overElement); }, /** * Helper for canDropBelow that only checks one bookmark node. * @private */ canDropBelow_: function(dragNode, overBookmarkNode, overElement) { var dragId = dragNode.id; // We cannot drop below if the item below is already in the drag source var nextElement = overElement.nextElementSibling; if (nextElement && nextElement.bookmarkId == dragId) return false; return true; }, /** * Whether we can drop the dragged items on the drop target. * * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are * currently dragging over. * @param {!HTMLElement} overElement The element that we are currently * dragging over. * @return {boolean} Whether we can drop the dragged items on the drop * target. */ canDropOn: function(overBookmarkNode, overElement) { // We can only drop on a folder. if (!bmm.isFolder(overBookmarkNode)) return false; if (!this.dragData.sameProfile) return true; return this.checkEvery_(this.canDropOn_, overBookmarkNode, overElement); }, /** * Helper for canDropOn that only checks one bookmark node. * @private */ canDropOn_: function(dragNode, overBookmarkNode, overElement) { var dragId = dragNode.id; if (overElement instanceof BookmarkList) { // We are trying to drop an item after the last item in the list. This // is allowed if the item is different from the last item in the list var listItems = list.items; var len = listItems.length; if (len == 0 || listItems[len - 1].bookmarkId != dragId) { return true; } } // Cannot drop on current parent. if (overBookmarkNode.id == dragNode.parentId) return false; return true; }, /** * Callback for the dragstart event. * @param {Event} e The dragstart event. */ handleDragStart: function(e) { // console.log(e.type); // Determine the selected bookmarks. var target = e.target; var draggedItems = []; if (target instanceof ListItem) { // Use selected items. draggedItems = target.parentNode.selectedItems; } else if (target instanceof TreeItem) { draggedItems.push(target); } // We manage starting the drag by using the extension API. e.preventDefault(); if (draggedItems.length) { // If we are dragging a single link we can do the *Link* effect, otherwise // we only allow copy and move. var effectAllowed; if (draggedItems.length == 1 && !bmm.isFolder(draggedItems[0].bookmarkNode)) { effectAllowed = 'copyMoveLink'; } else { effectAllowed = 'copyMove'; } e.dataTransfer.effectAllowed = effectAllowed; var ids = draggedItems.map(function(el) { return el.bookmarkId; }); chrome.experimental.bookmarkManager.startDrag(ids); } }, handleDragEnter: function(e) { // console.log(e.type); e.preventDefault(); }, /** * Calback for the dragover event. * @param {Event} e The dragover event. */ handleDragOver: function(e) { // console.log(e.type); // The default operation is to allow dropping links etc to do navigation. // We never want to do that for the bookmark manager. e.preventDefault(); if (!this.dragData) return; var overElement = this.getBookmarkElement(e.target); if (!overElement && e.target == list) overElement = list; if (!overElement) return; var overBookmarkNode = overElement.bookmarkNode; if (!this.canDrop(overBookmarkNode, overElement)) return; var bookmarkNode = overElement.bookmarkNode; var canDropAbove = this.canDropAbove(overBookmarkNode, overElement); var canDropOn = this.canDropOn(overBookmarkNode, overElement); var canDropBelow = this.canDropBelow(overBookmarkNode, overElement); if (!canDropAbove && !canDropOn && !canDropBelow) return; // Now we know that we can drop. Determine if we will drop above, on or // below based on mouse position etc. var dropPos; e.dataTransfer.dropEffect = this.dragData.sameProfile ? this.DND_EFFECT_MOVE : this.DND_EFFECT_COPY; var rect; if (overElement instanceof TreeItem) { // We only want the rect of the row representing the item and not // its children rect = overElement.rowElement.getBoundingClientRect(); } else { rect = overElement.getBoundingClientRect(); } var dy = e.clientY - rect.top; var yRatio = dy / rect.height; // above if (canDropAbove && (yRatio <= .25 || yRatio <= .5 && !(canDropBelow && canDropOn))) { dropPos = 'above'; // below } else if (canDropBelow && (yRatio > .75 || yRatio > .5 && !(canDropAbove && canDropOn))) { dropPos = 'below'; // on } else if (canDropOn) { dropPos = 'on'; // none } else { // No drop can happen. Exit now. e.dataTransfer.dropEffect = 'none'; return; } function cloneClientRect(rect) { var newRect = {}; for (var key in rect) { newRect[key] = rect[key]; } return newRect; } // If we are dropping above or below a tree item adjust the width so // that it is clearer where the item will be dropped. if ((dropPos == 'above' || dropPos == 'below') && overElement instanceof TreeItem) { // ClientRect is read only so clone in into a read-write object. rect = cloneClientRect(rect); var rtl = getComputedStyle(overElement).direction == 'rtl'; var labelElement = overElement.labelElement; var labelRect = labelElement.getBoundingClientRect(); if (rtl) { rect.width = labelRect.left + labelRect.width - rect.left; } else { rect.left = labelRect.left; rect.width -= rect.left } } var overlayType = dropPos; // If we are dropping on a list we want to show a overlay drop line after // the last element if (overElement instanceof BookmarkList) { overlayType = 'below'; // Get the rect of the last list item. var items = overElement.items; var length = items.length; if (length) { dropPos = 'below'; overElement = items[length - 1]; rect = overElement.getBoundingClientRect(); } else { // If there are no items, collapse the height of the rect rect = cloneClientRect(rect); rect.height = 0; // We do not use bottom so we don't care to adjust it. } } this.showDropOverlay_(rect, overlayType); this.dropDestination = { dropPos: dropPos, relatedNode: overElement.bookmarkNode }; }, /** * Shows and positions the drop marker overlay. * @param {ClientRect} targetRect The drop target rect * @param {string} overlayType The position relative to the target rect. * @private */ showDropOverlay_: function(targetRect, overlayType) { window.clearTimeout(this.hideDropOverlayTimer_); var overlay = $('drop-overlay'); if (overlayType == 'on') { overlay.className = ''; overlay.style.top = targetRect.top + 'px'; overlay.style.height = targetRect.height + 'px'; } else { overlay.className = 'line'; overlay.style.height = ''; } overlay.style.width = targetRect.width + 'px'; overlay.style.left = targetRect.left + 'px'; overlay.style.display = 'block'; if (overlayType != 'on') { var overlayRect = overlay.getBoundingClientRect(); if (overlayType == 'above') { overlay.style.top = targetRect.top - overlayRect.height / 2 + 'px'; } else { overlay.style.top = targetRect.top + targetRect.height - overlayRect.height / 2 + 'px'; } } }, /** * Hides the drop overlay element. * @private */ hideDropOverlay_: function() { // Hide the overlay in a timeout to reduce flickering as we move between // valid drop targets. window.clearTimeout(this.hideDropOverlayTimer_); this.hideDropOverlayTimer_ = window.setTimeout(function() { $('drop-overlay').style.display = ''; }, 100); }, handleDragLeave: function(e) { // console.log(e.type); this.hideDropOverlay_(); }, handleDrop: function(e) { // console.log(e.type); if (this.dropDestination && this.dragData) { var dropPos = this.dropDestination.dropPos; var relatedNode = this.dropDestination.relatedNode; var parentId = dropPos == 'on' ? relatedNode.id : relatedNode.parentId; var index; if (dropPos == 'above') index = relatedNode.index; else if (dropPos == 'below') index = relatedNode.index + 1; if (index != undefined) chrome.experimental.bookmarkManager.drop(parentId, index); else chrome.experimental.bookmarkManager.drop(parentId); // TODO(arv): Select the newly dropped items. } this.dropDestination = null; this.hideDropOverlay_(); }, handleDrag: function(e) { // console.log(e.type); }, handleDragEnd: function(e) { // console.log(e.type); var self = this; // Chromium Win incorrectly fires the dragend event before the drop event. // http://code.google.com/p/chromium/issues/detail?id=31292 window.setTimeout(function() { self.dragData = null; }, 1) }, handleChromeDragEnter: function(dragData) { this.dragData = dragData; }, init: function() { document.addEventListener('dragstart', cr.bind(this.handleDragStart, this)); document.addEventListener('dragenter', cr.bind(this.handleDragEnter, this)); document.addEventListener('dragover', cr.bind(this.handleDragOver, this)); document.addEventListener('dragleave', cr.bind(this.handleDragLeave, this)); document.addEventListener('drop', cr.bind(this.handleDrop, this)); document.addEventListener('dragend', cr.bind(this.handleDragEnd, this)); document.addEventListener('drag', cr.bind(this.handleDrag, this)); chrome.experimental.bookmarkManager.onDragEnter.addListener(cr.bind( this.handleChromeDragEnter, this)); } }; dnd.init(); </script> <!-- Organize menu --> <command i18n-values=".label:rename_folder" id="rename-folder-command"></command> <command i18n-values=".label:edit" id="edit-command"></command> <command i18n-values=".label:show_in_folder" id="show-in-folder-command"></command> <command i18n-values=".label:cut" id="cut-command"></command> <command i18n-values=".label:copy" id="copy-command"></command> <command i18n-values=".label:paste" id="paste-command"></command> <command i18n-values=".label:delete" id="delete-command"></command> <command i18n-values=".label:sort" id="sort-command"></command> <command i18n-values=".label:add_new_bookmark" id="add-new-bookmark-command"></command> <command i18n-values=".label:new_folder" id="new-folder-command"></command> <!-- Tools menu --> <command i18n-values=".label:import_menu" id="import-menu-command"></command> <command i18n-values=".label:export_menu" id="export-menu-command"></command> <!-- open * are handled in canExecute handler --> <command id="open-in-new-tab-command"></command> <command id="open-in-new-window-command"></command> <command id="open-incognito-window-command"></command> <!-- TODO(arv): I think the commands might be better created in code? --> <menu id="organize-menu"> <button command="#rename-folder-command"></button> <button command="#edit-command"></button> <button command="#show-in-folder-command"></button> <hr> <button command="#cut-command"></button> <button command="#copy-command"></button> <button command="#paste-command"></button> <hr> <button command="#delete-command"></button> <hr> <button command="#sort-command"></button> <hr> <button command="#add-new-bookmark-command"></button> <button command="#new-folder-command"></button> </menu> <menu id="tools-menu"> <button command="#import-menu-command"></button> <button command="#export-menu-command"></button> </menu> <menu id="context-menu"> <button command="#open-in-new-tab-command"></button> <button command="#open-in-new-window-command"></button> <button command="#open-incognito-window-command"></button> <hr> <button command="#rename-folder-command"></button> <button command="#edit-command"></button> <button command="#show-in-folder-command"></button> <hr> <button command="#cut-command"></button> <button command="#copy-command"></button> <button command="#paste-command"></button> <hr> <button command="#delete-command"></button> <hr> <button command="#add-new-bookmark-command"></button> <button command="#new-folder-command"></button> </menu> <script> // Commands const Command = cr.ui.Command; const CommandBinding = cr.ui.CommandBinding; const Menu = cr.ui.Menu; const MenuButton = cr.ui.MenuButton; cr.ui.decorate('menu', Menu); cr.ui.decorate('button[menu]', MenuButton); cr.ui.decorate('command', Command); cr.ui.contextMenuHandler.addContextMenuProperty(tree); list.contextMenu = $('context-menu'); tree.contextMenu = $('context-menu'); /** * Helper function that updates the canExecute and labels for the open like * commands. * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system. * @param {!cr.ui.Command} command The command we are currently precessing. */ function updateOpenCommands(e, command) { var selectedItem = e.target.selectedItem; var selectionCount; if (e.target == tree) selectionCount = selectedItem ? 1 : 0; else selectionCount = e.target.selectedItems.length; var isFolder = selectionCount == 1 && selectedItem.bookmarkNode && bmm.isFolder(selectedItem.bookmarkNode); var multiple = selectionCount != 1 || isFolder; function hasBookmarks(node) { var it = new bmm.TreeIterator(node); while (it.moveNext()) { if (!bmm.isFolder(it.current)) return true; } return false; } switch (command.id) { case 'open-in-new-tab-command': command.label = localStrings.getString(multiple ? 'open_all' : 'open_in_new_tab'); break; case 'open-in-new-window-command': command.label = localStrings.getString(multiple ? 'open_all_new_window' : 'open_in_new_window'); break; case 'open-incognito-window-command': command.label = localStrings.getString(multiple ? 'open_all_incognito' : 'open_incognito'); break; } e.canExecute = selectionCount > 0 && !!selectedItem.bookmarkNode; if (isFolder && e.canExecute) { // We need to get all the bookmark items in this tree. If the tree does not // contain any non-folders we need to disable the command. var p = bmm.loadSubtree(selectedItem.bookmarkId); p.addListener(function(node) { command.disabled = !node || !hasBookmarks(node); }); } } /** * Calls the backend to figure out if we can paste the clipboard into the active * folder. * @param {Function=} opt_f Function to call after the state has been * updated. */ function updatePasteCommand(opt_f) { function update(canPaste) { var command = $('paste-command'); command.disabled = !canPaste; if (opt_f) opt_f(); } // We cannot paste into search and recent view. if (list.isSearch() || list.isRecent()) { update(false); } else { chrome.experimental.bookmarkManager.canPaste(list.parentId, update); } } // We can always execute the import-menu and export-menu commands. document.addEventListener('canExecute', function(e) { var command = e.command; var commandId = command.id; if (commandId == 'import-menu-command' || commandId == 'export-menu-command') { e.canExecute = true; } }); // Update canExecute for the commands when the list is the active element. list.addEventListener('canExecute', function(e) { if (e.target != list) return; var command = e.command; var commandId = command.id; function hasSelected() { return !!e.target.selectedItem; } function hasSingleSelected() { return e.target.selectedItems.length == 1; } function isRecentOrSearch() { return list.isRecent() || list.isSearch(); } switch (commandId) { case 'rename-folder-command': // Show rename if a single folder is selected var items = e.target.selectedItems; if (items.length != 1) { e.canExecute = false; command.hidden = true; } else { var isFolder = bmm.isFolder(items[0].bookmarkNode); e.canExecute = isFolder; command.hidden = !isFolder; } break; case 'edit-command': // Show the edit command if not a folder var items = e.target.selectedItems; if (items.length != 1) { e.canExecute = false; command.hidden = false; } else { var isFolder = bmm.isFolder(items[0].bookmarkNode); e.canExecute = !isFolder; command.hidden = isFolder; } break; case 'show-in-folder-command': e.canExecute = isRecentOrSearch() && hasSingleSelected(); break; case 'delete-command': case 'cut-command': case 'copy-command': e.canExecute = hasSelected(); break; case 'paste-command': updatePasteCommand(); break; case 'sort-command': case 'add-new-bookmark-command': case 'new-folder-command': e.canExecute = !isRecentOrSearch(); break; case 'open-in-new-tab-command': case 'open-in-new-window-command': case 'open-incognito-window-command': updateOpenCommands(e, command); break; } }); // Update canExecute for the commands when the tree is the active element. tree.addEventListener('canExecute', function(e) { if (e.target != tree) return; var command = e.command; var commandId = command.id; function hasSelected() { return !!e.target.selectedItem; } function isRecentOrSearch() { var item = e.target.selectedItem; return item == recentTreeItem || item == searchTreeItem; } function isTopLevelItem() { return e.target.selectedItem.parentNode == tree; } switch (commandId) { case 'rename-folder-command': command.hidden = false; e.canExecute = hasSelected() && !isTopLevelItem(); break; case 'edit-command': command.hidden = true; e.canExecute = false; break; case 'delete-command': case 'cut-command': case 'copy-command': e.canExecute = hasSelected() && !isTopLevelItem(); break; case 'paste-command': updatePasteCommand(); break; case 'sort-command': case 'add-new-bookmark-command': case 'new-folder-command': e.canExecute = !isRecentOrSearch(); break; case 'open-in-new-tab-command': case 'open-in-new-window-command': case 'open-incognito-window-command': updateOpenCommands(e, command); break; } }); /** * Update the canExecute state of the commands when the selection changes. * @param {Event} e The change event object. */ function updateCommandsBasedOnSelection(e) { if (e.target == document.activeElement) { // Paste only needs to updated when the tree selection changes. var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit', 'add-new-bookmark', 'new-folder', 'open-in-new-tab', 'open-in-new-window', 'open-incognito-window']; if (e.target == tree) { commandNames.push('paste', 'show-in-folder', 'sort'); } commandNames.forEach(function(baseId) { $(baseId + '-command').canExecuteChange(); }); } } list.addEventListener('change', updateCommandsBasedOnSelection); tree.addEventListener('change', updateCommandsBasedOnSelection); document.addEventListener('command', function(e) { var command = e.command; var commandId = command.id; console.log(command.id, 'executed', 'on', e.target); if (commandId == 'import-menu-command') { chrome.experimental.bookmarkManager.import(); } else if (command.id == 'export-menu-command') { chrome.experimental.bookmarkManager.export(); } }); function handleRename(e) { var item = e.target; var bookmarkNode = item.bookmarkNode; chrome.bookmarks.update(bookmarkNode.id, {title: item.label}); } tree.addEventListener('rename', handleRename); list.addEventListener('rename', handleRename); list.addEventListener('edit', function(e) { var item = e.target; var bookmarkNode = item.bookmarkNode; var context = { title: bookmarkNode.title }; if (!bmm.isFolder(bookmarkNode)) context.url = bookmarkNode.url; if (bookmarkNode.id == 'new') { // New page context.parentId = bookmarkNode.parentId; chrome.bookmarks.create(context, function(node) { list.remove(item); list.selectedItem = bmm.listLookup[node.id]; }); } else { // Edit chrome.bookmarks.update(bookmarkNode.id, context); } }); list.addEventListener('canceledit', function(e) { var item = e.target; var bookmarkNode = item.bookmarkNode; if (bookmarkNode.id == 'new') { list.remove(item); list.selectionModel.leadItem = list.lastChild; list.selectionModel.anchorItem = list.lastChild; list.focus(); } }); /** * Navigates to the folder that the selected item is in and selects it. This is * used for the show-in-folder command. */ function showInFolder() { var bookmarkId = list.selectedItem.bookmarkNode.id; var parentId = list.selectedItem.bookmarkNode.parentId; // After the list is loaded we should select the revealed item. var f = function(e) { var item = bmm.listLookup[bookmarkId]; if (item) { list.selectionModel.leadItem = item; item.selected = true; } list.removeEventListener('load', f); } list.addEventListener('load', f); var treeItem = bmm.treeLookup[parentId]; treeItem.reveal(); navigateTo(parentId); } /** * Opens URLs in new tab, window or incognito mode. * @param {!Array.<string>} urls The URLs to open. * @param {string} kind The kind is either 'tab', 'window', or 'incognito'. */ function openUrls(urls, kind) { if (urls.length < 1) return; if (urls.length > 15) { if (!confirm(localStrings.getStringF('should_open_all', urls.length))) return; } // Fix '#124' URLs since open those in a new window does not work. We prepend // the base URL when we encounter those. var base = window.location.href.split('#')[0]; urls = urls.map(function(url) { return url[0] == '#' ? base + url : url; }); var incognito = kind == 'incognito'; if (kind == 'window' || incognito) { chrome.windows.create({ url: urls[0], incognito: incognito }, function(window) { urls.forEach(function(url, i) { if (i > 0) chrome.tabs.create({ url: url, windowId: window.id, selected: false }); }); }); } else if (kind == 'tab') { urls.forEach(function(url, i) { chrome.tabs.create({url: url, selected: !i}); }); } else { window.location.href = urls[0]; } } /** * Returns the selected bookmark nodes of the active element. Only call this * if the list or the tree is focused. * @return {!Array} Array of bookmark nodes. */ function getSelectedBookmarkNodes() { if (document.activeElement == list) { return list.selectedItems.map(function(item) { return item.bookmarkNode; }); } else if (document.activeElement == tree) { return [tree.selectedItem.bookmarkNode]; } else { throw Error('getSelectedBookmarkNodes called when wrong element focused.'); } } /** * @return {!Array.<string>} An array of the selected bookmark IDs. */ function getSelectedBookmarkIds() { return getSelectedBookmarkNodes().map(function(node) { return node.id; }); } /** * Opens the selected bookmarks. */ function openBookmarks(kind) { // If we have selected any folders we need to find all items recursively. // We can do several async calls to getChildren but instead we do a single // call to getTree and only add the subtrees of the selected items. var urls = []; var idMap = {}; // Traverses the tree until it finds a node tree that should be added. Then // we switch over to use addNodes. We could merge these two functions into // one but that would make the code less readable. function traverseNodes(node) { // This is not using the iterator since it uses breadth first search. if (node.id in idMap) { addNodes(node); } else if (node.children) { for (var i = 0; i < node.children.length; i++) { traverseNodes(node.children[i]); } } } // Adds the node and all the descendants function addNodes(node) { var it = new bmm.TreeIterator(node); while (it.moveNext()) { var n = it.current; if (!bmm.isFolder(n)) urls.push(n.url); } } var nodes = getSelectedBookmarkNodes(); // Create a map for simpler lookup later. nodes.forEach(function(node) { idMap[node.id] = true; }); var p = bmm.loadTree(); p.addListener(function(node) { traverseNodes(node); openUrls(urls, kind); }); } /** * Deletes the selected bookmarks. */ function deleteBookmarks() { getSelectedBookmarkIds().forEach(function(id) { chrome.bookmarks.removeTree(id); }); } /** * Callback for the new folder command. This creates a new folder and starts * a rename of it. */ function newFolder() { var parentId = list.parentId; var isTree = document.activeElement == tree; chrome.bookmarks.create({ title: localStrings.getString('new_folder_name'), parentId: parentId }, function(newNode) { // We need to do this in a timeout to be able to focus the newly created // item. setTimeout(function() { var newItem = isTree ? bmm.treeLookup[newNode.id] : bmm.listLookup[newNode.id]; document.activeElement.selectedItem = newItem; newItem.editing = true; }); }); } /** * Adds a page to the current folder. This is called by the * add-new-bookmark-command handler. */ function addPage() { var parentId = list.parentId; var fakeNode = { title: '', url: '', parentId: parentId, id: 'new' }; var newListItem = bmm.createListItem(fakeNode, false); list.add(newListItem); list.selectedItem = newListItem; newListItem.editing = true; } /** * Handler for the command event. This is used both for the tree and the list. * @param {!Event} e The event object. */ function handleCommand(e) { var command = e.command; var commandId = command.id; switch (commandId) { case 'show-in-folder-command': showInFolder(); break; case 'open-in-new-tab-command': openBookmarks('tab'); break; case 'open-in-new-window-command': openBookmarks('window'); break; case 'open-incognito-window-command': openBookmarks('incognito'); break; case 'delete-command': deleteBookmarks(); break; case 'copy-command': chrome.experimental.bookmarkManager.copy(getSelectedBookmarkIds()); break; case 'cut-command': chrome.experimental.bookmarkManager.cut(getSelectedBookmarkIds()); break; case 'paste-command': chrome.experimental.bookmarkManager.paste(list.parentId); break; case 'sort-command': chrome.experimental.bookmarkManager.sortChildren(list.parentId); break; case 'rename-folder-command': case 'edit-command': document.activeElement.selectedItem.editing = true; break; case 'new-folder-command': newFolder(); break; case 'add-new-bookmark-command': addPage(); break; } } // TODO(arv): Move shortcut to HTML? // Meta+Backspace on Mac, Del on other platforms. $('delete-command').shortcut = cr.isMac ? 'U+0008-meta' : 'U+007F'; list.addEventListener('command', handleCommand); tree.addEventListener('command', handleCommand); // Execute the copy, cut and paste commands when those events are dispatched by // the browser. This allows us to rely on the browser to handle the keyboard // shortcuts for these commands. (function() { function handle(id) { return function(e) { var command = $(id); if (!command.disabled) { command.execute(); e.preventDefault(); // Prevent the system beep } }; } // Listen to copy, cut and paste events and execute the associated commands. document.addEventListener('copy', handle('copy-command')); document.addEventListener('cut', handle('cut-command')); var pasteHandler = handle('paste-command'); document.addEventListener('paste', function(e) { // Paste is a bit special since we need to do an async call to see if we can // paste because the paste command might not be up to date. updatePasteCommand(pasteHandler); }); })(); /** * The local strings object which is used to do the translation. * @type {!LocalStrings} */ var localStrings = new LocalStrings; // Get the localized strings from the backend. chrome.experimental.bookmarkManager.getStrings(function setTemplateData(data) { // The strings may contain & which we need to strip. for (var key in data) { data[key] = data[key].replace(/&/, ''); } localStrings.templateData = data; i18nTemplate.process(document, data); }); </script> <div id="drop-overlay"></div> </body> </html>