blob: b36b6c0376fac3ee6f998fba9eb1b7d19eb2c013 [file] [log] [blame]
<!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)) {
delete bmm.treeLookup[searchTreeItem.bookmarkId];
$('term').value = id.slice(2);
searchTreeItem.bookmarkId = id;
bmm.treeLookup[id] = searchTreeItem;
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);
});
/**
* Timer id used for delaying find-as-you-type
*/
var inputDelayTimer;
// Capture input changes to the search term input element and delay searching
// for 250ms to reduce flicker.
$('term').oninput = function(e) {
clearTimeout(inputDelayTimer);
inputDelayTimer = setTimeout(function() {
setSearch($('term').value);
}, 250);
};
/**
* Navigates to the search results for the search text.
* @para {string} searchText The text to search for.
*/
function setSearch(searchText) {
navigateTo('q=' + searchText);
}
/**
* 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))
$('term').value = hash.slice(2);
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:delete" id="delete-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: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="#delete-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="#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="#delete-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="#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;
});
// Incognito mode is not yet supported by the extensions APIs.
// http://code.google.com/p/chromium/issues/detail?id=12658
if (kind == 'window') {
chrome.windows.create({url: urls[0]}, 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-in-new-incognito-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>