1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 /** @const */ var BookmarkList = bmm.BookmarkList;
9 /** @const */ var BookmarkTree = bmm.BookmarkTree;
10 /** @const */ var Command = cr.ui.Command;
11 /** @const */ var LinkKind = cr.LinkKind;
12 /** @const */ var ListItem = cr.ui.ListItem;
13 /** @const */ var Menu = cr.ui.Menu;
14 /** @const */ var MenuButton = cr.ui.MenuButton;
15 /** @const */ var Splitter = cr.ui.Splitter;
16 /** @const */ var TreeItem = cr.ui.TreeItem;
19 * An array containing the BookmarkTreeNodes that were deleted in the last
20 * deletion action. This is used for implementing undo.
21 * @type {?{nodes: Array<Array<BookmarkTreeNode>>, target: EventTarget}}
27 * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
28 * view. Zero means pointer doesn't hover on folder.
31 var lastHoverOnFolderTimeStamp = 0;
34 * Holds a function that will undo that last action, if global undo is enabled.
37 var performGlobalUndo;
40 * Holds a link controller singleton. Use getLinkController() rarther than
41 * accessing this variabie.
42 * @type {cr.LinkController}
47 * New Windows are not allowed in Windows 8 metro mode.
49 var canOpenNewWindows = true;
52 * Incognito mode availability can take the following values: ,
53 * - 'enabled' for when both normal and incognito modes are available;
54 * - 'disabled' for when incognito mode is disabled;
55 * - 'forced' for when incognito mode is forced (normal mode is unavailable).
57 var incognitoModeAvailability = 'enabled';
60 * Whether bookmarks can be modified.
69 var searchTreeItem = new TreeItem({
74 * Command shortcut mapping.
77 var commandShortcutMap = cr.isMac ? {
79 // On Mac we also allow Meta+Backspace.
80 'delete': 'U+007F U+0008 Meta-U+0008',
81 'open-in-background-tab': 'Meta-Enter',
82 'open-in-new-tab': 'Shift-Meta-Enter',
83 'open-in-same-window': 'Meta-Down',
84 'open-in-new-window': 'Shift-Enter',
85 'rename-folder': 'Enter',
86 // Global undo is Command-Z. It is not in any menu.
87 'undo': 'Meta-U+005A',
91 'open-in-background-tab': 'Ctrl-Enter',
92 'open-in-new-tab': 'Shift-Ctrl-Enter',
93 'open-in-same-window': 'Enter',
94 'open-in-new-window': 'Shift-Enter',
95 'rename-folder': 'F2',
96 // Global undo is Ctrl-Z. It is not in any menu.
97 'undo': 'Ctrl-U+005A',
101 * Mapping for folder id to suffix of UMA. These names will be appeared
102 * after "BookmarkManager_NavigateTo_" in UMA dashboard.
105 var folderMetricsNameMap = {
110 'subfolder': 'SubFolder',
114 * Adds an event listener to a node that will remove itself after firing once.
115 * @param {!Element} node The DOM node to add the listener to.
116 * @param {string} name The name of the event listener to add to.
117 * @param {function(Event)} handler Function called when the event fires.
119 function addOneShotEventListener(node, name, handler) {
120 var f = function(e) {
122 node.removeEventListener(name, f);
124 node.addEventListener(name, f);
127 // Get the localized strings from the backend via bookmakrManagerPrivate API.
128 function loadLocalizedStrings(data) {
129 // The strings may contain & which we need to strip.
130 for (var key in data) {
131 data[key] = data[key].replace(/&/, '');
134 loadTimeData.data = data;
135 i18nTemplate.process(document, loadTimeData);
137 searchTreeItem.label = loadTimeData.getString('search');
138 searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' :
139 'images/bookmark_manager_search.png';
143 * Updates the location hash to reflect the current state of the application.
145 function updateHash() {
146 window.location.hash = bmm.tree.selectedItem.bookmarkId;
151 * Navigates to a bookmark ID.
152 * @param {string} id The ID to navigate to.
153 * @param {function()=} opt_callback Function called when list view loaded or
154 * displayed specified folder.
156 function navigateTo(id, opt_callback) {
157 window.location.hash = id;
159 var sameParent = bmm.list.parentId == id;
165 var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
166 folderMetricsNameMap['subfolder'];
167 chrome.metricsPrivate.recordUserAction(
168 'BookmarkManager_NavigateTo_' + metricsId);
174 addOneShotEventListener(bmm.list, 'load', opt_callback);
179 * Updates the parent ID of the bookmark list and selects the correct tree item.
180 * @param {string} id The id.
182 function updateParentId(id) {
183 // Setting list.parentId fires 'load' event.
184 bmm.list.parentId = id;
186 // When tree.selectedItem changed, tree view calls navigatTo() then it
187 // calls updateHash() when list view displayed specified folder.
188 bmm.tree.selectedItem = bmm.treeLookup[id] || bmm.tree.selectedItem;
191 // Process the location hash. This is called by onhashchange and when the page
193 function processHash() {
194 var id = window.location.hash.slice(1);
196 // If we do not have a hash, select first item in the tree.
197 id = bmm.tree.items[0].bookmarkId;
201 if (/^e=/.test(id)) {
204 // If hash contains e=, edit the item specified.
205 chrome.bookmarks.get(id, function(bookmarkNodes) {
206 // Verify the node to edit is a valid node.
207 if (!bookmarkNodes || bookmarkNodes.length != 1)
209 var bookmarkNode = bookmarkNodes[0];
211 // After the list reloads, edit the desired bookmark.
212 var editBookmark = function() {
213 var index = bmm.list.dataModel.findIndexById(bookmarkNode.id);
215 var sm = bmm.list.selectionModel;
216 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
217 scrollIntoViewAndMakeEditable(index);
221 var parentId = assert(bookmarkNode.parentId);
222 navigateTo(parentId, editBookmark);
225 // We handle the two cases of navigating to the bookmark to be edited
226 // above. Don't run the standard navigation code below.
228 } else if (/^q=/.test(id)) {
229 // In case we got a search hash, update the text input and the
230 // bmm.treeLookup to use the new id.
231 setSearch(id.slice(2));
235 // Navigate to bookmark 'id' (which may be a query of the form q=query).
239 // We need to verify that this is a correct ID.
240 chrome.bookmarks.get(id, function(items) {
241 if (items && items.length == 1)
247 // Activate is handled by the open-in-same-window-command.
248 function handleDoubleClickForList(e) {
250 $('open-in-same-window-command').execute();
253 // The list dispatches an event when the user clicks on the URL or the Show in
255 function handleUrlClickedForList(e) {
256 getLinkController().openUrlFromEvent(e.url, e.originalEvent);
257 chrome.bookmarkManagerPrivate.recordLaunch();
260 function handleSearch(e) {
261 setSearch(this.value);
265 * Navigates to the search results for the search text.
266 * @param {string} searchText The text to search for.
268 function setSearch(searchText) {
270 // Only update search item if we have a search term. We never want the
271 // search item to be for an empty search.
272 delete bmm.treeLookup[searchTreeItem.bookmarkId];
273 var id = searchTreeItem.bookmarkId = 'q=' + searchText;
274 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
277 var input = $('term');
278 // Do not update the input if the user is actively using the text input.
279 if (document.activeElement != input)
280 input.value = searchText;
283 bmm.tree.add(searchTreeItem);
284 bmm.tree.selectedItem = searchTreeItem;
287 bmm.tree.selectedItem = bmm.tree.items[0];
288 id = bmm.tree.selectedItem.bookmarkId;
295 * This returns the user visible path to the folder where the bookmark is
297 * @param {number} parentId The ID of the parent folder.
298 * @return {string} The path to the the bookmark,
300 function getFolder(parentId) {
301 var parentNode = bmm.tree.getBookmarkNodeById(parentId);
303 var s = parentNode.title;
304 if (parentNode.parentId != bmm.ROOT_ID) {
305 return getFolder(parentNode.parentId) + '/' + s;
311 function handleLoadForTree(e) {
316 * Returns a promise for all the URLs in the {@code nodes} and the direct
317 * children of {@code nodes}.
318 * @param {!Array<BookmarkTreeNode>} nodes .
319 * @return {!Promise<Array<string>>} .
321 function getAllUrls(nodes) {
324 // Adds the node and all its direct children.
325 // TODO(deepak.m1): Here node should exist. When we delete the nodes then
326 // datamodel gets updated but still it shows deleted items as selected items
327 // and accessing those nodes throws chrome.runtime.lastError. This cause
328 // undefined value for node. Please refer https://crbug.com/480935.
329 function addNodes(node) {
330 if (!node || node.id == 'new')
334 node.children.forEach(function(child) {
335 if (!bmm.isFolder(child))
336 urls.push(child.url);
343 // Get a future promise for the nodes.
344 var promises = nodes.map(function(node) {
345 if (bmm.isFolder(assert(node)))
346 return bmm.loadSubtree(node.id);
347 // Not a folder so we already have all the data we need.
348 return Promise.resolve(node);
351 return Promise.all(promises).then(function(nodes) {
352 nodes.forEach(addNodes);
358 * Returns the nodes (non recursive) to use for the open commands.
359 * @param {HTMLElement} target
360 * @return {!Array<BookmarkTreeNode>}
362 function getNodesForOpen(target) {
363 if (target == bmm.tree) {
364 if (bmm.tree.selectedItem != searchTreeItem)
365 return bmm.tree.selectedFolders;
366 // Fall through to use all nodes in the list.
368 var items = bmm.list.selectedItems;
373 // The list starts off with a null dataModel. We can get here during startup.
374 if (!bmm.list.dataModel)
377 // Return an array based on the dataModel.
378 return bmm.list.dataModel.slice();
382 * Returns a promise that will contain all URLs of all the selected bookmarks
383 * and the nested bookmarks for use with the open commands.
384 * @param {HTMLElement} target The target list or tree.
385 * @return {Promise<Array<string>>} .
387 function getUrlsForOpenCommands(target) {
388 return getAllUrls(getNodesForOpen(target));
391 function notNewNode(node) {
392 return node.id != 'new';
396 * Helper function that updates the canExecute and labels for the open-like
398 * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
399 * @param {!cr.ui.Command} command The command we are currently processing.
400 * @param {string} singularId The string id of singular form of the menu label.
401 * @param {string} pluralId The string id of menu label if the singular form is
403 * @param {boolean} commandDisabled Whether the menu item should be disabled
404 no matter what bookmarks are selected.
406 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
408 // The command label reflects the selection which might not reflect
409 // how many bookmarks will be opened. For example if you right click an
410 // empty area in a folder with 1 bookmark the text should still say "all".
411 var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
412 var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
413 command.label = loadTimeData.getString(singular ? singularId : pluralId);
416 if (commandDisabled) {
417 command.disabled = true;
418 e.canExecute = false;
422 getUrlsForOpenCommands(assertInstanceof(e.target, HTMLElement)).then(
424 var disabled = !urls.length;
425 command.disabled = disabled;
426 e.canExecute = !disabled;
431 * Calls the backend to figure out if we can paste the clipboard into the active
433 * @param {Function=} opt_f Function to call after the state has been updated.
435 function updatePasteCommand(opt_f) {
436 function update(commandId, canPaste) {
437 $(commandId).disabled = !canPaste;
443 // We can not paste into search item in tree.
444 if (bmm.tree.selectedItem && bmm.tree.selectedItem != searchTreeItem) {
445 promises.push(new Promise(function(resolve) {
446 var id = bmm.tree.selectedItem.bookmarkId;
447 chrome.bookmarkManagerPrivate.canPaste(id, function(canPaste) {
448 update('paste-from-folders-menu-command', canPaste);
453 // Tree's not loaded yet.
454 update('paste-from-folders-menu-command', false);
457 // The organize menu.
458 var listId = bmm.list.parentId;
459 if (bmm.list.isSearch() || !listId) {
460 // We cannot paste into search view or the list isn't ready.
461 update('paste-from-organize-menu-command', false);
463 promises.push(new Promise(function(resolve) {
464 chrome.bookmarkManagerPrivate.canPaste(listId, function(canPaste) {
465 update('paste-from-organize-menu-command', canPaste);
471 Promise.all(promises).then(function() {
473 if (document.activeElement == bmm.list)
474 cmd = 'paste-from-organize-menu-command';
475 else if (document.activeElement == bmm.tree)
476 cmd = 'paste-from-folders-menu-command';
479 update('paste-from-context-menu-command', !$(cmd).disabled);
485 function handleCanExecuteForSearchBox(e) {
486 var command = e.command;
487 switch (command.id) {
488 case 'delete-command':
490 // Pass the delete and undo commands through
491 // (fixes http://crbug.com/278112).
492 e.canExecute = false;
497 function handleCanExecuteForDocument(e) {
498 var command = e.command;
499 switch (command.id) {
500 case 'import-menu-command':
501 e.canExecute = canEdit;
504 case 'export-menu-command':
505 // We can always execute the export-menu command.
510 e.canExecute = !bmm.list.isSearch() &&
511 bmm.list.dataModel && bmm.list.dataModel.length > 1 &&
512 !isUnmodifiable(bmm.tree.getBookmarkNodeById(bmm.list.parentId));
516 // Because the global undo command has no visible UI, always enable it,
517 // and just make it a no-op if undo is not possible.
522 canExecuteForList(e);
523 if (!e.defaultPrevented)
524 canExecuteForTree(e);
530 * Helper function for handling canExecute for the list and the tree.
531 * @param {!cr.ui.CanExecuteEvent} e Can execute event object.
532 * @param {boolean} isSearch Whether the user is trying to do a command on
535 function canExecuteShared(e, isSearch) {
536 var command = e.command;
537 switch (command.id) {
538 case 'paste-from-folders-menu-command':
539 case 'paste-from-organize-menu-command':
540 case 'paste-from-context-menu-command':
541 updatePasteCommand();
544 case 'add-new-bookmark-command':
545 case 'new-folder-command':
546 case 'new-folder-from-folders-menu-command':
547 var parentId = computeParentFolderForNewItem();
548 var unmodifiable = isUnmodifiable(
549 bmm.tree.getBookmarkNodeById(parentId));
550 e.canExecute = !isSearch && canEdit && !unmodifiable;
553 case 'open-in-new-tab-command':
554 updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
557 case 'open-in-background-tab-command':
558 updateOpenCommand(e, command, '', '', false);
561 case 'open-in-new-window-command':
562 updateOpenCommand(e, command,
563 'open_in_new_window', 'open_all_new_window',
564 // Disabled when incognito is forced.
565 incognitoModeAvailability == 'forced' || !canOpenNewWindows);
568 case 'open-incognito-window-command':
569 updateOpenCommand(e, command,
570 'open_incognito', 'open_all_incognito',
571 // Not available when incognito is disabled.
572 incognitoModeAvailability == 'disabled');
575 case 'undo-delete-command':
576 e.canExecute = !!lastDeleted;
582 * Helper function for handling canExecute for the list and document.
583 * @param {!cr.ui.CanExecuteEvent} e Can execute event object.
585 function canExecuteForList(e) {
586 function hasSelected() {
587 return !!bmm.list.selectedItem;
590 function hasSingleSelected() {
591 return bmm.list.selectedItems.length == 1;
594 function canCopyItem(item) {
595 return item.id != 'new';
598 function canCopyItems() {
599 var selectedItems = bmm.list.selectedItems;
600 return selectedItems && selectedItems.some(canCopyItem);
603 function isSearch() {
604 return bmm.list.isSearch();
607 var command = e.command;
608 switch (command.id) {
609 case 'rename-folder-command':
610 // Show rename if a single folder is selected.
611 var items = bmm.list.selectedItems;
612 if (items.length != 1) {
613 e.canExecute = false;
614 command.hidden = true;
616 var isFolder = bmm.isFolder(items[0]);
617 e.canExecute = isFolder && canEdit && !hasUnmodifiable(items);
618 command.hidden = !isFolder;
623 // Show the edit command if not a folder.
624 var items = bmm.list.selectedItems;
625 if (items.length != 1) {
626 e.canExecute = false;
627 command.hidden = false;
629 var isFolder = bmm.isFolder(items[0]);
630 e.canExecute = !isFolder && canEdit && !hasUnmodifiable(items);
631 command.hidden = isFolder;
635 case 'show-in-folder-command':
636 e.canExecute = isSearch() && hasSingleSelected();
639 case 'delete-command':
641 e.canExecute = canCopyItems() && canEdit &&
642 !hasUnmodifiable(bmm.list.selectedItems);
646 e.canExecute = canCopyItems();
649 case 'open-in-same-window-command':
650 e.canExecute = (e.target == bmm.list) && hasSelected();
654 canExecuteShared(e, isSearch());
658 // Update canExecute for the commands when the list is the active element.
659 function handleCanExecuteForList(e) {
660 if (e.target != bmm.list) return;
661 canExecuteForList(e);
664 // Update canExecute for the commands when the tree is the active element.
665 function handleCanExecuteForTree(e) {
666 if (e.target != bmm.tree) return;
667 canExecuteForTree(e);
670 function canExecuteForTree(e) {
671 function hasSelected() {
672 return !!bmm.tree.selectedItem;
675 function isSearch() {
676 return bmm.tree.selectedItem == searchTreeItem;
679 function isTopLevelItem() {
680 return bmm.tree.selectedItem &&
681 bmm.tree.selectedItem.parentNode == bmm.tree;
684 var command = e.command;
685 switch (command.id) {
686 case 'rename-folder-command':
687 case 'rename-folder-from-folders-menu-command':
688 command.hidden = false;
689 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
690 !hasUnmodifiable(bmm.tree.selectedFolders);
694 command.hidden = true;
695 e.canExecute = false;
698 case 'delete-command':
699 case 'delete-from-folders-menu-command':
701 case 'cut-from-folders-menu-command':
702 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
703 !hasUnmodifiable(bmm.tree.selectedFolders);
707 case 'copy-from-folders-menu-command':
708 e.canExecute = hasSelected() && !isTopLevelItem();
711 case 'undo-delete-from-folders-menu-command':
712 e.canExecute = lastDeleted && lastDeleted.target == bmm.tree;
716 canExecuteShared(e, isSearch());
721 * Update the canExecute state of all the commands.
723 function updateAllCommands() {
724 var commands = document.querySelectorAll('command');
725 for (var i = 0; i < commands.length; i++) {
726 commands[i].canExecuteChange();
730 function updateEditingCommands() {
731 var editingCommands = [
734 'cut-from-folders-menu',
738 'paste-from-context-menu',
739 'paste-from-folders-menu',
740 'paste-from-organize-menu',
745 chrome.bookmarkManagerPrivate.canEdit(function(result) {
746 if (result != canEdit) {
748 editingCommands.forEach(function(baseId) {
749 $(baseId + '-command').canExecuteChange();
755 function handleChangeForTree(e) {
756 navigateTo(bmm.tree.selectedItem.bookmarkId);
759 function handleMenuButtonClicked(e) {
760 updateEditingCommands();
762 if (e.currentTarget.id == 'folders-menu') {
763 $('copy-from-folders-menu-command').canExecuteChange();
764 $('undo-delete-from-folders-menu-command').canExecuteChange();
766 $('copy-command').canExecuteChange();
770 function handleRename(e) {
772 var bookmarkNode = item.bookmarkNode;
773 chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
774 performGlobalUndo = null; // This can't be undone, so disable global undo.
777 function handleEdit(e) {
779 var bookmarkNode = item.bookmarkNode;
781 title: bookmarkNode.title
783 if (!bmm.isFolder(bookmarkNode))
784 context.url = bookmarkNode.url;
786 if (bookmarkNode.id == 'new') {
787 selectItemsAfterUserAction(/** @type {BookmarkList} */(bmm.list));
790 context.parentId = bookmarkNode.parentId;
791 chrome.bookmarks.create(context, function(node) {
792 // A new node was created and will get added to the list due to the
794 var dataModel = bmm.list.dataModel;
795 var index = dataModel.indexOf(bookmarkNode);
796 dataModel.splice(index, 1);
799 var newIndex = dataModel.findIndexById(node.id);
800 if (newIndex != -1) {
801 var sm = bmm.list.selectionModel;
802 bmm.list.scrollIndexIntoView(newIndex);
803 sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
808 chrome.bookmarks.update(bookmarkNode.id, context);
810 performGlobalUndo = null; // This can't be undone, so disable global undo.
813 function handleCancelEdit(e) {
815 var bookmarkNode = item.bookmarkNode;
816 if (bookmarkNode.id == 'new') {
817 var dataModel = bmm.list.dataModel;
818 var index = dataModel.findIndexById('new');
819 dataModel.splice(index, 1);
824 * Navigates to the folder that the selected item is in and selects it. This is
825 * used for the show-in-folder command.
827 function showInFolder() {
828 var bookmarkNode = bmm.list.selectedItem;
831 var parentId = bookmarkNode.parentId;
833 // After the list is loaded we should select the revealed item.
834 function selectItem() {
835 var index = bmm.list.dataModel.findIndexById(bookmarkNode.id);
838 var sm = bmm.list.selectionModel;
839 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
840 bmm.list.scrollIndexIntoView(index);
843 var treeItem = bmm.treeLookup[parentId];
846 navigateTo(parentId, selectItem);
850 * @return {!cr.LinkController} The link controller used to open links based on
851 * user clicks and keyboard actions.
853 function getLinkController() {
854 return linkController ||
855 (linkController = new cr.LinkController(loadTimeData));
859 * Returns the selected bookmark nodes of the provided tree or list.
860 * If |opt_target| is not provided or null the active element is used.
861 * Only call this if the list or the tree is focused.
862 * @param {EventTarget=} opt_target The target list or tree.
863 * @return {!Array} Array of bookmark nodes.
865 function getSelectedBookmarkNodes(opt_target) {
866 return (opt_target || document.activeElement) == bmm.tree ?
867 bmm.tree.selectedFolders : bmm.list.selectedItems;
871 * @param {EventTarget=} opt_target The target list or tree.
872 * @return {!Array<string>} An array of the selected bookmark IDs.
874 function getSelectedBookmarkIds(opt_target) {
875 var selectedNodes = getSelectedBookmarkNodes(opt_target);
876 selectedNodes.sort(function(a, b) { return a.index - b.index });
877 return selectedNodes.map(function(node) {
883 * @param {BookmarkTreeNode} node The node to test.
884 * @return {boolean} Whether the given node is unmodifiable.
886 function isUnmodifiable(node) {
887 return !!(node && node.unmodifiable);
891 * @param {Array<BookmarkTreeNode>} nodes A list of BookmarkTreeNodes.
892 * @return {boolean} Whether any of the nodes is managed.
894 function hasUnmodifiable(nodes) {
895 return nodes.some(isUnmodifiable);
899 * Opens the selected bookmarks.
900 * @param {cr.LinkKind} kind The kind of link we want to open.
901 * @param {HTMLElement=} opt_eventTarget The target of the user initiated event.
903 function openBookmarks(kind, opt_eventTarget) {
904 // If we have selected any folders, we need to find all the bookmarks one
905 // level down. We use multiple async calls to getSubtree instead of getting
906 // the whole tree since we would like to minimize the amount of data sent.
908 var urlsP = getUrlsForOpenCommands(opt_eventTarget ? opt_eventTarget : null);
909 urlsP.then(function(urls) {
910 getLinkController().openUrls(assert(urls), kind);
911 chrome.bookmarkManagerPrivate.recordLaunch();
916 * Opens an item in the list.
918 function openItem() {
919 var bookmarkNodes = getSelectedBookmarkNodes();
920 // If we double clicked or pressed enter on a single folder, navigate to it.
921 if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0]))
922 navigateTo(bookmarkNodes[0].id);
924 openBookmarks(LinkKind.FOREGROUND_TAB);
928 * Refreshes search results after delete or undo-delete.
929 * This ensures children of deleted folders do not remain in results
931 function updateSearchResults() {
932 if (bmm.list.isSearch())
937 * Deletes the selected bookmarks. The bookmarks are saved in memory in case
938 * the user needs to undo the deletion.
939 * @param {EventTarget=} opt_target The deleter of bookmarks.
941 function deleteBookmarks(opt_target) {
942 var selectedIds = getSelectedBookmarkIds(opt_target);
943 if (!selectedIds.length)
946 var filteredIds = getFilteredSelectedBookmarkIds(opt_target);
947 lastDeleted = {nodes: [], target: opt_target || document.activeElement};
949 function performDelete() {
950 // Only remove filtered ids.
951 chrome.bookmarkManagerPrivate.removeTrees(filteredIds);
952 $('undo-delete-command').canExecuteChange();
953 $('undo-delete-from-folders-menu-command').canExecuteChange();
954 performGlobalUndo = undoDelete;
957 // First, store information about the bookmarks being deleted.
958 // Store all selected ids.
959 selectedIds.forEach(function(id) {
960 chrome.bookmarks.getSubTree(id, function(results) {
961 lastDeleted.nodes.push(results);
963 // When all nodes have been saved, perform the deletion.
964 if (lastDeleted.nodes.length === selectedIds.length) {
966 updateSearchResults();
973 * Restores a tree of bookmarks under a specified folder.
974 * @param {BookmarkTreeNode} node The node to restore.
975 * @param {(string|number)=} opt_parentId If a string is passed, it's the ID of
976 * the folder to restore under. If not specified or a number is passed, the
977 * original parentId of the node will be used.
979 function restoreTree(node, opt_parentId) {
981 parentId: typeof opt_parentId == 'string' ? opt_parentId : node.parentId,
987 chrome.bookmarks.create(bookmarkInfo, function(result) {
989 console.error('Failed to restore bookmark.');
994 // Restore the children using the new ID for this node.
995 node.children.forEach(function(child) {
996 restoreTree(child, result.id);
1000 updateSearchResults();
1005 * Restores the last set of bookmarks that was deleted.
1007 function undoDelete() {
1008 lastDeleted.nodes.forEach(function(arr) {
1009 arr.forEach(restoreTree);
1012 $('undo-delete-command').canExecuteChange();
1013 $('undo-delete-from-folders-menu-command').canExecuteChange();
1015 // Only a single level of undo is supported, so disable global undo now.
1016 performGlobalUndo = null;
1020 * Computes folder for "Add Page" and "Add Folder".
1021 * @return {string} The id of folder node where we'll create new page/folder.
1023 function computeParentFolderForNewItem() {
1024 if (document.activeElement == bmm.tree)
1025 return bmm.list.parentId;
1026 var selectedItem = bmm.list.selectedItem;
1027 return selectedItem && bmm.isFolder(selectedItem) ?
1028 selectedItem.id : bmm.list.parentId;
1032 * Callback for rename folder and edit command. This starts editing for
1033 * the passed in target, or the selected item.
1034 * @param {EventTarget=} opt_target The target to start editing. If absent or
1035 * null, the selected item will be edited instead.
1037 function editItem(opt_target) {
1038 if ((opt_target || document.activeElement) == bmm.tree) {
1039 bmm.tree.selectedItem.editing = true;
1041 var li = bmm.list.getListItem(bmm.list.selectedItem);
1048 * Callback for the new folder command. This creates a new folder and starts
1050 * @param {EventTarget=} opt_target The target to create a new folder in.
1052 function newFolder(opt_target) {
1053 performGlobalUndo = null; // This can't be undone, so disable global undo.
1055 var parentId = computeParentFolderForNewItem();
1056 var selectedItems = bmm.list.selectedItems;
1058 // Callback is called after tree and list data model updated.
1059 function createFolder(callback) {
1060 if (selectedItems.length == 1 && document.activeElement != bmm.tree &&
1061 !bmm.isFolder(selectedItems[0]) && selectedItems[0].id != 'new') {
1062 newIndex = bmm.list.dataModel.indexOf(selectedItems[0]) + 1;
1064 chrome.bookmarks.create({
1065 title: loadTimeData.getString('new_folder_name'),
1071 if ((opt_target || document.activeElement) == bmm.tree) {
1072 createFolder(function(newNode) {
1073 navigateTo(newNode.id, function() {
1074 bmm.treeLookup[newNode.id].editing = true;
1080 function editNewFolderInList() {
1081 createFolder(function(newNode) {
1082 var index = newNode.index;
1083 var sm = bmm.list.selectionModel;
1084 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
1085 scrollIntoViewAndMakeEditable(index);
1089 navigateTo(parentId, editNewFolderInList);
1093 * Scrolls the list item into view and makes it editable.
1094 * @param {number} index The index of the item to make editable.
1096 function scrollIntoViewAndMakeEditable(index) {
1097 bmm.list.scrollIndexIntoView(index);
1098 // onscroll is now dispatched asynchronously so we have to postpone
1100 setTimeout(function() {
1101 var item = bmm.list.getListItemByIndex(index);
1103 item.editing = true;
1108 * Adds a page to the current folder. This is called by the
1109 * add-new-bookmark-command handler.
1111 function addPage() {
1112 var parentId = computeParentFolderForNewItem();
1113 var selectedItems = bmm.list.selectedItems;
1115 function editNewBookmark() {
1116 if (selectedItems.length == 1 && document.activeElement != bmm.tree &&
1117 !bmm.isFolder(selectedItems[0])) {
1118 newIndex = bmm.list.dataModel.indexOf(selectedItems[0]) + 1;
1128 var dataModel = bmm.list.dataModel;
1129 var index = dataModel.length;
1130 if (newIndex != undefined)
1132 dataModel.splice(index, 0, fakeNode);
1133 var sm = bmm.list.selectionModel;
1134 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
1135 scrollIntoViewAndMakeEditable(index);
1138 navigateTo(parentId, editNewBookmark);
1142 * This function is used to select items after a user action such as paste, drop
1144 * @param {BookmarkList|BookmarkTree} target The target of the user action.
1145 * @param {string=} opt_selectedTreeId If provided, then select that tree id.
1147 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1148 // We get one onCreated event per item so we delay the handling until we get
1149 // no more events coming.
1154 function handle(id, bookmarkNode) {
1155 clearTimeout(timer);
1156 if (opt_selectedTreeId || bmm.list.parentId == bookmarkNode.parentId)
1158 timer = setTimeout(handleTimeout, 50);
1161 function handleTimeout() {
1162 chrome.bookmarks.onCreated.removeListener(handle);
1163 chrome.bookmarks.onMoved.removeListener(handle);
1165 if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1166 var index = ids.indexOf(opt_selectedTreeId);
1167 if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1168 bmm.tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1170 } else if (target == bmm.list) {
1171 var dataModel = bmm.list.dataModel;
1172 var firstIndex = dataModel.findIndexById(ids[0]);
1173 var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1174 if (firstIndex != -1 && lastIndex != -1) {
1175 var selectionModel = bmm.list.selectionModel;
1176 selectionModel.selectedIndex = -1;
1177 selectionModel.selectRange(firstIndex, lastIndex);
1178 selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1183 bmm.list.endBatchUpdates();
1186 bmm.list.startBatchUpdates();
1188 chrome.bookmarks.onCreated.addListener(handle);
1189 chrome.bookmarks.onMoved.addListener(handle);
1190 timer = setTimeout(handleTimeout, 300);
1194 * Record user action.
1195 * @param {string} name An user action name.
1197 function recordUserAction(name) {
1198 chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1202 * The currently selected bookmark, based on where the user is clicking.
1203 * @return {string} The ID of the currently selected bookmark (could be from
1204 * tree view or list view).
1206 function getSelectedId() {
1207 if (document.activeElement == bmm.tree)
1208 return bmm.tree.selectedItem.bookmarkId;
1209 var selectedItem = bmm.list.selectedItem;
1210 return selectedItem && bmm.isFolder(selectedItem) ?
1211 selectedItem.id : bmm.tree.selectedItem.bookmarkId;
1215 * Pastes the copied/cutted bookmark into the right location depending whether
1216 * if it was called from Organize Menu or from Context Menu.
1217 * @param {string} id The id of the element being pasted from.
1219 function pasteBookmark(id) {
1220 recordUserAction('Paste');
1221 selectItemsAfterUserAction(/** @type {BookmarkList} */(bmm.list));
1222 chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1226 * Returns true if child is contained in another selected folder.
1227 * Traces parent nodes up the tree until a selected ancestor or root is found.
1229 function hasSelectedAncestor(parentNode) {
1230 function contains(arr, item) {
1231 for (var i = 0; i < arr.length; i++)
1232 if (arr[i] === item)
1237 // Don't search top level, cannot select permanent nodes in search.
1238 if (parentNode == null || parentNode.id <= 2)
1241 // Found selected ancestor.
1242 if (contains(getSelectedBookmarkNodes(), parentNode))
1246 return hasSelectedAncestor(
1247 bmm.tree.getBookmarkNodeById(parentNode.parentId));
1251 * @param {EventTarget=} opt_target A target to get bookmark IDs from.
1252 * @return {Array<string>} An array of bookmarks IDs.
1254 function getFilteredSelectedBookmarkIds(opt_target) {
1255 // Remove duplicates from filteredIds and return.
1256 var filteredIds = [];
1257 // Selected nodes to iterate through for matches.
1258 var nodes = getSelectedBookmarkNodes(opt_target);
1260 for (var i = 0; i < nodes.length; i++)
1261 if (!hasSelectedAncestor(bmm.tree.getBookmarkNodeById(nodes[i].parentId)))
1262 filteredIds.splice(0, 0, nodes[i].id);
1268 * Handler for the command event. This is used for context menu of list/tree
1269 * and organized menu.
1270 * @param {!Event} e The event object.
1272 function handleCommand(e) {
1273 var command = e.command;
1274 var target = assertInstanceof(e.target, HTMLElement);
1275 switch (command.id) {
1276 case 'import-menu-command':
1277 recordUserAction('Import');
1278 chrome.bookmarks.import();
1281 case 'export-menu-command':
1282 recordUserAction('Export');
1283 chrome.bookmarks.export();
1286 case 'undo-command':
1287 if (performGlobalUndo) {
1288 recordUserAction('UndoGlobal');
1289 performGlobalUndo();
1291 recordUserAction('UndoNone');
1295 case 'show-in-folder-command':
1296 recordUserAction('ShowInFolder');
1300 case 'open-in-new-tab-command':
1301 case 'open-in-background-tab-command':
1302 recordUserAction('OpenInNewTab');
1303 openBookmarks(LinkKind.BACKGROUND_TAB, target);
1306 case 'open-in-new-window-command':
1307 recordUserAction('OpenInNewWindow');
1308 openBookmarks(LinkKind.WINDOW, target);
1311 case 'open-incognito-window-command':
1312 recordUserAction('OpenIncognito');
1313 openBookmarks(LinkKind.INCOGNITO, target);
1316 case 'delete-from-folders-menu-command':
1318 case 'delete-command':
1319 recordUserAction('Delete');
1320 deleteBookmarks(target);
1323 case 'copy-from-folders-menu-command':
1325 case 'copy-command':
1326 recordUserAction('Copy');
1327 chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(target),
1328 updatePasteCommand);
1331 case 'cut-from-folders-menu-command':
1334 recordUserAction('Cut');
1335 chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(target),
1337 updatePasteCommand();
1338 updateSearchResults();
1342 case 'paste-from-organize-menu-command':
1343 pasteBookmark(bmm.list.parentId);
1346 case 'paste-from-folders-menu-command':
1347 pasteBookmark(bmm.tree.selectedItem.bookmarkId);
1350 case 'paste-from-context-menu-command':
1351 pasteBookmark(getSelectedId());
1354 case 'sort-command':
1355 recordUserAction('Sort');
1356 chrome.bookmarkManagerPrivate.sortChildren(bmm.list.parentId);
1360 case 'rename-folder-from-folders-menu-command':
1362 case 'rename-folder-command':
1366 case 'edit-command':
1367 recordUserAction('Edit');
1371 case 'new-folder-from-folders-menu-command':
1373 case 'new-folder-command':
1374 recordUserAction('NewFolder');
1378 case 'add-new-bookmark-command':
1379 recordUserAction('AddPage');
1383 case 'open-in-same-window-command':
1384 recordUserAction('OpenInSame');
1388 case 'undo-delete-command':
1389 case 'undo-delete-from-folders-menu-command':
1390 recordUserAction('UndoDelete');
1396 // Execute the copy, cut and paste commands when those events are dispatched by
1397 // the browser. This allows us to rely on the browser to handle the keyboard
1398 // shortcuts for these commands.
1399 function installEventHandlerForCommand(eventName, commandId) {
1400 function handle(e) {
1401 if (document.activeElement != bmm.list &&
1402 document.activeElement != bmm.tree)
1404 var command = $(commandId);
1405 if (!command.disabled) {
1408 e.preventDefault(); // Prevent the system beep.
1411 if (eventName == 'paste') {
1412 // Paste is a bit special since we need to do an async call to see if we
1413 // can paste because the paste command might not be up to date.
1414 document.addEventListener(eventName, function(e) {
1415 updatePasteCommand(handle);
1418 document.addEventListener(eventName, handle);
1422 function initializeSplitter() {
1423 var splitter = document.querySelector('.main > .splitter');
1424 Splitter.decorate(splitter);
1426 var splitterStyle = splitter.previousElementSibling.style;
1428 // The splitter persists the size of the left component in the local store.
1429 if ('treeWidth' in window.localStorage)
1430 splitterStyle.width = window.localStorage['treeWidth'];
1432 splitter.addEventListener('resize', function(e) {
1433 window.localStorage['treeWidth'] = splitterStyle.width;
1437 function initializeBookmarkManager() {
1438 // Sometimes the extension API is not initialized.
1439 if (!chrome.bookmarks)
1440 console.error('Bookmarks extension API is not available');
1442 chrome.bookmarkManagerPrivate.getStrings(continueInitializeBookmarkManager);
1445 function continueInitializeBookmarkManager(localizedStrings) {
1446 loadLocalizedStrings(localizedStrings);
1448 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1450 cr.ui.decorate('cr-menu', Menu);
1451 cr.ui.decorate('button[menu]', MenuButton);
1452 cr.ui.decorate('command', Command);
1453 BookmarkList.decorate($('list'));
1454 BookmarkTree.decorate($('tree'));
1456 bmm.list.addEventListener('canceledit', handleCancelEdit);
1457 bmm.list.addEventListener('canExecute', handleCanExecuteForList);
1458 bmm.list.addEventListener('change', updateAllCommands);
1459 bmm.list.addEventListener('contextmenu', updateEditingCommands);
1460 bmm.list.addEventListener('dblclick', handleDoubleClickForList);
1461 bmm.list.addEventListener('edit', handleEdit);
1462 bmm.list.addEventListener('rename', handleRename);
1463 bmm.list.addEventListener('urlClicked', handleUrlClickedForList);
1465 bmm.tree.addEventListener('canExecute', handleCanExecuteForTree);
1466 bmm.tree.addEventListener('change', handleChangeForTree);
1467 bmm.tree.addEventListener('contextmenu', updateEditingCommands);
1468 bmm.tree.addEventListener('rename', handleRename);
1469 bmm.tree.addEventListener('load', handleLoadForTree);
1471 cr.ui.contextMenuHandler.addContextMenuProperty(
1472 /** @type {!Element} */(bmm.tree));
1473 bmm.list.contextMenu = $('context-menu');
1474 bmm.tree.contextMenu = $('context-menu');
1476 // We listen to hashchange so that we can update the currently shown folder
1477 // when // the user goes back and forward in the history.
1478 window.addEventListener('hashchange', processHash);
1480 document.querySelector('header form').onsubmit =
1481 /** @type {function(Event=)} */(function(e) {
1482 setSearch($('term').value);
1486 $('term').addEventListener('search', handleSearch);
1487 $('term').addEventListener('canExecute', handleCanExecuteForSearchBox);
1489 $('folders-button').addEventListener('click', handleMenuButtonClicked);
1490 $('organize-button').addEventListener('click', handleMenuButtonClicked);
1492 document.addEventListener('canExecute', handleCanExecuteForDocument);
1493 document.addEventListener('command', handleCommand);
1495 // Listen to copy, cut and paste events and execute the associated commands.
1496 installEventHandlerForCommand('copy', 'copy-command');
1497 installEventHandlerForCommand('cut', 'cut-command');
1498 installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1500 // Install shortcuts
1501 for (var name in commandShortcutMap) {
1502 $(name + '-command').shortcut = commandShortcutMap[name];
1505 // Disable almost all commands at startup.
1506 var commands = document.querySelectorAll('command');
1507 for (var i = 0, command; command = commands[i]; ++i) {
1508 if (command.id != 'import-menu-command' &&
1509 command.id != 'export-menu-command') {
1510 command.disabled = true;
1514 chrome.bookmarkManagerPrivate.canEdit(function(result) {
1518 chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1519 // TODO(rustema): propagate policy value to the bookmark manager when it
1521 incognitoModeAvailability = result;
1524 chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1525 canOpenNewWindows = result;
1528 cr.ui.FocusOutlineManager.forDocument(document);
1529 initializeSplitter();
1530 bmm.addBookmarkModelListeners();
1531 dnd.init(selectItemsAfterUserAction);
1535 initializeBookmarkManager();