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.
6 /** @const */ var BookmarkList = bmm.BookmarkList;
7 /** @const */ var BookmarkTree = bmm.BookmarkTree;
8 /** @const */ var Command = cr.ui.Command;
9 /** @const */ var LinkKind = cr.LinkKind;
10 /** @const */ var ListItem = cr.ui.ListItem;
11 /** @const */ var Menu = cr.ui.Menu;
12 /** @const */ var MenuButton = cr.ui.MenuButton;
13 /** @const */ var Splitter = cr.ui.Splitter;
14 /** @const */ var TreeItem = cr.ui.TreeItem;
17 * An array containing the BookmarkTreeNodes that were deleted in the last
18 * deletion action. This is used for implementing undo.
19 * @type {?{nodes: Array<Array<BookmarkTreeNode>>, target: EventTarget}}
25 * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
26 * view. Zero means pointer doesn't hover on folder.
29 var lastHoverOnFolderTimeStamp = 0;
32 * Holds a function that will undo that last action, if global undo is enabled.
35 var performGlobalUndo;
38 * Holds a link controller singleton. Use getLinkController() rarther than
39 * accessing this variabie.
40 * @type {cr.LinkController}
45 * New Windows are not allowed in Windows 8 metro mode.
47 var canOpenNewWindows = true;
50 * Incognito mode availability can take the following values: ,
51 * - 'enabled' for when both normal and incognito modes are available;
52 * - 'disabled' for when incognito mode is disabled;
53 * - 'forced' for when incognito mode is forced (normal mode is unavailable).
55 var incognitoModeAvailability = 'enabled';
58 * Whether bookmarks can be modified.
67 var searchTreeItem = new TreeItem({
72 * Command shortcut mapping.
75 var commandShortcutMap = cr.isMac ? {
77 // On Mac we also allow Meta+Backspace.
78 'delete': 'U+007F U+0008 Meta-U+0008',
79 'open-in-background-tab': 'Meta-Enter',
80 'open-in-new-tab': 'Shift-Meta-Enter',
81 'open-in-same-window': 'Meta-Down',
82 'open-in-new-window': 'Shift-Enter',
83 'rename-folder': 'Enter',
84 // Global undo is Command-Z. It is not in any menu.
85 'undo': 'Meta-U+005A',
89 'open-in-background-tab': 'Ctrl-Enter',
90 'open-in-new-tab': 'Shift-Ctrl-Enter',
91 'open-in-same-window': 'Enter',
92 'open-in-new-window': 'Shift-Enter',
93 'rename-folder': 'F2',
94 // Global undo is Ctrl-Z. It is not in any menu.
95 'undo': 'Ctrl-U+005A',
99 * Mapping for folder id to suffix of UMA. These names will be appeared
100 * after "BookmarkManager_NavigateTo_" in UMA dashboard.
103 var folderMetricsNameMap = {
108 'subfolder': 'SubFolder',
112 * Adds an event listener to a node that will remove itself after firing once.
113 * @param {!Element} node The DOM node to add the listener to.
114 * @param {string} name The name of the event listener to add to.
115 * @param {function(Event)} handler Function called when the event fires.
117 function addOneShotEventListener(node, name, handler) {
118 var f = function(e) {
120 node.removeEventListener(name, f);
122 node.addEventListener(name, f);
125 // Get the localized strings from the backend via bookmakrManagerPrivate API.
126 function loadLocalizedStrings(data) {
127 // The strings may contain & which we need to strip.
128 for (var key in data) {
129 data[key] = data[key].replace(/&/, '');
132 loadTimeData.data = data;
133 i18nTemplate.process(document, loadTimeData);
135 searchTreeItem.label = loadTimeData.getString('search');
136 searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' :
137 'images/bookmark_manager_search.png';
141 * Updates the location hash to reflect the current state of the application.
143 function updateHash() {
144 window.location.hash = bmm.tree.selectedItem.bookmarkId;
149 * Navigates to a bookmark ID.
150 * @param {string} id The ID to navigate to.
151 * @param {function()=} opt_callback Function called when list view loaded or
152 * displayed specified folder.
154 function navigateTo(id, opt_callback) {
155 window.location.hash = id;
158 var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
159 folderMetricsNameMap['subfolder'];
160 chrome.metricsPrivate.recordUserAction(
161 'BookmarkManager_NavigateTo_' + metricsId);
164 if (bmm.list.parentId == id)
167 addOneShotEventListener(bmm.list, 'load', opt_callback);
172 * Updates the parent ID of the bookmark list and selects the correct tree item.
173 * @param {string} id The id.
175 function updateParentId(id) {
176 // Setting list.parentId fires 'load' event.
177 bmm.list.parentId = id;
179 // When tree.selectedItem changed, tree view calls navigatTo() then it
180 // calls updateHash() when list view displayed specified folder.
181 bmm.tree.selectedItem = bmm.treeLookup[id] || bmm.tree.selectedItem;
184 // Process the location hash. This is called by onhashchange and when the page
186 function processHash() {
187 var id = window.location.hash.slice(1);
189 // If we do not have a hash, select first item in the tree.
190 id = bmm.tree.items[0].bookmarkId;
194 if (/^e=/.test(id)) {
197 // If hash contains e=, edit the item specified.
198 chrome.bookmarks.get(id, function(bookmarkNodes) {
199 // Verify the node to edit is a valid node.
200 if (!bookmarkNodes || bookmarkNodes.length != 1)
202 var bookmarkNode = bookmarkNodes[0];
204 // After the list reloads, edit the desired bookmark.
205 var editBookmark = function() {
206 var index = bmm.list.dataModel.findIndexById(bookmarkNode.id);
208 var sm = bmm.list.selectionModel;
209 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
210 scrollIntoViewAndMakeEditable(index);
214 var parentId = assert(bookmarkNode.parentId);
215 navigateTo(parentId, editBookmark);
218 // We handle the two cases of navigating to the bookmark to be edited
219 // above. Don't run the standard navigation code below.
221 } else if (/^q=/.test(id)) {
222 // In case we got a search hash, update the text input and the
223 // bmm.treeLookup to use the new id.
224 setSearch(id.slice(2));
228 // Navigate to bookmark 'id' (which may be a query of the form q=query).
232 // We need to verify that this is a correct ID.
233 chrome.bookmarks.get(id, function(items) {
234 if (items && items.length == 1)
240 // Activate is handled by the open-in-same-window-command.
241 function handleDoubleClickForList(e) {
243 $('open-in-same-window-command').execute();
246 // The list dispatches an event when the user clicks on the URL or the Show in
248 function handleUrlClickedForList(e) {
249 getLinkController().openUrlFromEvent(e.url, e.originalEvent);
250 chrome.bookmarkManagerPrivate.recordLaunch();
253 function handleSearch(e) {
254 setSearch(this.value);
258 * Navigates to the search results for the search text.
259 * @param {string} searchText The text to search for.
261 function setSearch(searchText) {
263 // Only update search item if we have a search term. We never want the
264 // search item to be for an empty search.
265 delete bmm.treeLookup[searchTreeItem.bookmarkId];
266 var id = searchTreeItem.bookmarkId = 'q=' + searchText;
267 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
270 var input = $('term');
271 // Do not update the input if the user is actively using the text input.
272 if (document.activeElement != input)
273 input.value = searchText;
276 bmm.tree.add(searchTreeItem);
277 bmm.tree.selectedItem = searchTreeItem;
280 bmm.tree.selectedItem = bmm.tree.items[0];
281 id = bmm.tree.selectedItem.bookmarkId;
288 * This returns the user visible path to the folder where the bookmark is
290 * @param {number} parentId The ID of the parent folder.
291 * @return {string} The path to the the bookmark,
293 function getFolder(parentId) {
294 var parentNode = bmm.tree.getBookmarkNodeById(parentId);
296 var s = parentNode.title;
297 if (parentNode.parentId != bmm.ROOT_ID) {
298 return getFolder(parentNode.parentId) + '/' + s;
304 function handleLoadForTree(e) {
309 * Returns a promise for all the URLs in the {@code nodes} and the direct
310 * children of {@code nodes}.
311 * @param {!Array<BookmarkTreeNode>} nodes .
312 * @return {!Promise<Array<string>>} .
314 function getAllUrls(nodes) {
317 // Adds the node and all its direct children.
318 function addNodes(node) {
319 if (node.id == 'new')
323 node.children.forEach(function(child) {
324 if (!bmm.isFolder(child))
325 urls.push(child.url);
332 // Get a future promise for the nodes.
333 var promises = nodes.map(function(node) {
334 if (bmm.isFolder(assert(node)))
335 return bmm.loadSubtree(node.id);
336 // Not a folder so we already have all the data we need.
337 return Promise.resolve(node);
340 return Promise.all(promises).then(function(nodes) {
341 nodes.forEach(addNodes);
347 * Returns the nodes (non recursive) to use for the open commands.
348 * @param {HTMLElement} target
349 * @return {!Array<BookmarkTreeNode>}
351 function getNodesForOpen(target) {
352 if (target == bmm.tree) {
353 if (bmm.tree.selectedItem != searchTreeItem)
354 return bmm.tree.selectedFolders;
355 // Fall through to use all nodes in the list.
357 var items = bmm.list.selectedItems;
362 // The list starts off with a null dataModel. We can get here during startup.
363 if (!bmm.list.dataModel)
366 // Return an array based on the dataModel.
367 return bmm.list.dataModel.slice();
371 * Returns a promise that will contain all URLs of all the selected bookmarks
372 * and the nested bookmarks for use with the open commands.
373 * @param {HTMLElement} target The target list or tree.
374 * @return {Promise<Array<string>>} .
376 function getUrlsForOpenCommands(target) {
377 return getAllUrls(getNodesForOpen(target));
380 function notNewNode(node) {
381 return node.id != 'new';
385 * Helper function that updates the canExecute and labels for the open-like
387 * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
388 * @param {!cr.ui.Command} command The command we are currently processing.
389 * @param {string} singularId The string id of singular form of the menu label.
390 * @param {string} pluralId The string id of menu label if the singular form is
392 * @param {boolean} commandDisabled Whether the menu item should be disabled
393 no matter what bookmarks are selected.
395 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
397 // The command label reflects the selection which might not reflect
398 // how many bookmarks will be opened. For example if you right click an
399 // empty area in a folder with 1 bookmark the text should still say "all".
400 var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
401 var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
402 command.label = loadTimeData.getString(singular ? singularId : pluralId);
405 if (commandDisabled) {
406 command.disabled = true;
407 e.canExecute = false;
411 getUrlsForOpenCommands(assertInstanceof(e.target, HTMLElement)).then(
413 var disabled = !urls.length;
414 command.disabled = disabled;
415 e.canExecute = !disabled;
420 * Calls the backend to figure out if we can paste the clipboard into the active
422 * @param {Function=} opt_f Function to call after the state has been updated.
424 function updatePasteCommand(opt_f) {
425 function update(commandId, canPaste) {
426 $(commandId).disabled = !canPaste;
432 if (bmm.tree.selectedItem) {
433 promises.push(new Promise(function(resolve) {
434 var id = bmm.tree.selectedItem.bookmarkId;
435 chrome.bookmarkManagerPrivate.canPaste(id, function(canPaste) {
436 update('paste-from-folders-menu-command', canPaste);
441 // Tree's not loaded yet.
442 update('paste-from-folders-menu-command', false);
445 // The organize menu.
446 var listId = bmm.list.parentId;
447 if (bmm.list.isSearch() || !listId) {
448 // We cannot paste into search view or the list isn't ready.
449 update('paste-from-organize-menu-command', false);
451 promises.push(new Promise(function(resolve) {
452 chrome.bookmarkManagerPrivate.canPaste(listId, function(canPaste) {
453 update('paste-from-organize-menu-command', canPaste);
459 Promise.all(promises).then(function() {
461 if (document.activeElement == bmm.list)
462 cmd = 'paste-from-organize-menu-command';
463 else if (document.activeElement == bmm.tree)
464 cmd = 'paste-from-folders-menu-command';
467 update('paste-from-context-menu-command', !$(cmd).disabled);
473 function handleCanExecuteForDocument(e) {
474 var command = e.command;
475 switch (command.id) {
476 case 'import-menu-command':
477 e.canExecute = canEdit;
480 case 'export-menu-command':
481 // We can always execute the export-menu command.
486 e.canExecute = !bmm.list.isSearch() &&
487 bmm.list.dataModel && bmm.list.dataModel.length > 1 &&
488 !isUnmodifiable(bmm.tree.getBookmarkNodeById(bmm.list.parentId));
492 // If the search box is active, pass the undo command through
493 // (fixes http://crbug.com/278112). Otherwise, because
494 // the global undo command has no visible UI, always enable it, and
495 // just make it a no-op if undo is not possible.
496 e.canExecute = e.currentTarget.activeElement !== $('term');
500 canExecuteForList(e);
501 if (!e.defaultPrevented)
502 canExecuteForTree(e);
508 * Helper function for handling canExecute for the list and the tree.
509 * @param {!cr.ui.CanExecuteEvent} e Can execute event object.
510 * @param {boolean} isSearch Whether the user is trying to do a command on
513 function canExecuteShared(e, isSearch) {
514 var command = e.command;
515 switch (command.id) {
516 case 'paste-from-folders-menu-command':
517 case 'paste-from-organize-menu-command':
518 case 'paste-from-context-menu-command':
519 updatePasteCommand();
522 case 'add-new-bookmark-command':
523 case 'new-folder-command':
524 case 'new-folder-from-folders-menu-command':
525 var parentId = computeParentFolderForNewItem();
526 var unmodifiable = isUnmodifiable(
527 bmm.tree.getBookmarkNodeById(parentId));
528 e.canExecute = !isSearch && canEdit && !unmodifiable;
531 case 'open-in-new-tab-command':
532 updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
535 case 'open-in-background-tab-command':
536 updateOpenCommand(e, command, '', '', false);
539 case 'open-in-new-window-command':
540 updateOpenCommand(e, command,
541 'open_in_new_window', 'open_all_new_window',
542 // Disabled when incognito is forced.
543 incognitoModeAvailability == 'forced' || !canOpenNewWindows);
546 case 'open-incognito-window-command':
547 updateOpenCommand(e, command,
548 'open_incognito', 'open_all_incognito',
549 // Not available when incognito is disabled.
550 incognitoModeAvailability == 'disabled');
553 case 'undo-delete-command':
554 e.canExecute = !!lastDeleted;
560 * Helper function for handling canExecute for the list and document.
561 * @param {!cr.ui.CanExecuteEvent} e Can execute event object.
563 function canExecuteForList(e) {
564 function hasSelected() {
565 return !!bmm.list.selectedItem;
568 function hasSingleSelected() {
569 return bmm.list.selectedItems.length == 1;
572 function canCopyItem(item) {
573 return item.id != 'new';
576 function canCopyItems() {
577 var selectedItems = bmm.list.selectedItems;
578 return selectedItems && selectedItems.some(canCopyItem);
581 function isSearch() {
582 return bmm.list.isSearch();
585 var command = e.command;
586 switch (command.id) {
587 case 'rename-folder-command':
588 // Show rename if a single folder is selected.
589 var items = bmm.list.selectedItems;
590 if (items.length != 1) {
591 e.canExecute = false;
592 command.hidden = true;
594 var isFolder = bmm.isFolder(items[0]);
595 e.canExecute = isFolder && canEdit && !hasUnmodifiable(items);
596 command.hidden = !isFolder;
601 // Show the edit command if not a folder.
602 var items = bmm.list.selectedItems;
603 if (items.length != 1) {
604 e.canExecute = false;
605 command.hidden = false;
607 var isFolder = bmm.isFolder(items[0]);
608 e.canExecute = !isFolder && canEdit && !hasUnmodifiable(items);
609 command.hidden = isFolder;
613 case 'show-in-folder-command':
614 e.canExecute = isSearch() && hasSingleSelected();
617 case 'delete-command':
619 e.canExecute = canCopyItems() && canEdit &&
620 !hasUnmodifiable(bmm.list.selectedItems);
624 e.canExecute = canCopyItems();
627 case 'open-in-same-window-command':
628 e.canExecute = hasSelected();
632 canExecuteShared(e, isSearch());
636 // Update canExecute for the commands when the list is the active element.
637 function handleCanExecuteForList(e) {
638 if (e.target != bmm.list) return;
639 canExecuteForList(e);
642 // Update canExecute for the commands when the tree is the active element.
643 function handleCanExecuteForTree(e) {
644 if (e.target != bmm.tree) return;
645 canExecuteForTree(e);
648 function canExecuteForTree(e) {
649 function hasSelected() {
650 return !!bmm.tree.selectedItem;
653 function isSearch() {
654 return bmm.tree.selectedItem == searchTreeItem;
657 function isTopLevelItem() {
658 return bmm.tree.selectedItem &&
659 bmm.tree.selectedItem.parentNode == bmm.tree;
662 var command = e.command;
663 switch (command.id) {
664 case 'rename-folder-command':
665 case 'rename-folder-from-folders-menu-command':
666 command.hidden = false;
667 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
668 !hasUnmodifiable(bmm.tree.selectedFolders);
672 command.hidden = true;
673 e.canExecute = false;
676 case 'delete-command':
677 case 'delete-from-folders-menu-command':
679 case 'cut-from-folders-menu-command':
680 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
681 !hasUnmodifiable(bmm.tree.selectedFolders);
685 case 'copy-from-folders-menu-command':
686 e.canExecute = hasSelected() && !isTopLevelItem();
689 case 'undo-delete-from-folders-menu-command':
690 e.canExecute = lastDeleted && lastDeleted.target == bmm.tree;
694 canExecuteShared(e, isSearch());
699 * Update the canExecute state of all the commands.
701 function updateAllCommands() {
702 var commands = document.querySelectorAll('command');
703 for (var i = 0; i < commands.length; i++) {
704 commands[i].canExecuteChange();
708 function updateEditingCommands() {
709 var editingCommands = [
712 'cut-from-folders-menu',
716 'paste-from-context-menu',
717 'paste-from-folders-menu',
718 'paste-from-organize-menu',
723 chrome.bookmarkManagerPrivate.canEdit(function(result) {
724 if (result != canEdit) {
726 editingCommands.forEach(function(baseId) {
727 $(baseId + '-command').canExecuteChange();
733 function handleChangeForTree(e) {
734 navigateTo(bmm.tree.selectedItem.bookmarkId);
737 function handleMenuButtonClicked(e) {
738 updateEditingCommands();
740 if (e.currentTarget.id == 'folders-menu') {
741 $('copy-from-folders-menu-command').canExecuteChange();
742 $('undo-delete-from-folders-menu-command').canExecuteChange();
744 $('copy-command').canExecuteChange();
748 function handleRename(e) {
750 var bookmarkNode = item.bookmarkNode;
751 chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
752 performGlobalUndo = null; // This can't be undone, so disable global undo.
755 function handleEdit(e) {
757 var bookmarkNode = item.bookmarkNode;
759 title: bookmarkNode.title
761 if (!bmm.isFolder(bookmarkNode))
762 context.url = bookmarkNode.url;
764 if (bookmarkNode.id == 'new') {
765 selectItemsAfterUserAction(/** @type {BookmarkList} */(bmm.list));
768 context.parentId = bookmarkNode.parentId;
769 chrome.bookmarks.create(context, function(node) {
770 // A new node was created and will get added to the list due to the
772 var dataModel = bmm.list.dataModel;
773 var index = dataModel.indexOf(bookmarkNode);
774 dataModel.splice(index, 1);
777 var newIndex = dataModel.findIndexById(node.id);
778 if (newIndex != -1) {
779 var sm = bmm.list.selectionModel;
780 bmm.list.scrollIndexIntoView(newIndex);
781 sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
786 chrome.bookmarks.update(bookmarkNode.id, context);
788 performGlobalUndo = null; // This can't be undone, so disable global undo.
791 function handleCancelEdit(e) {
793 var bookmarkNode = item.bookmarkNode;
794 if (bookmarkNode.id == 'new') {
795 var dataModel = bmm.list.dataModel;
796 var index = dataModel.findIndexById('new');
797 dataModel.splice(index, 1);
802 * Navigates to the folder that the selected item is in and selects it. This is
803 * used for the show-in-folder command.
805 function showInFolder() {
806 var bookmarkNode = bmm.list.selectedItem;
809 var parentId = bookmarkNode.parentId;
811 // After the list is loaded we should select the revealed item.
812 function selectItem() {
813 var index = bmm.list.dataModel.findIndexById(bookmarkNode.id);
816 var sm = bmm.list.selectionModel;
817 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
818 bmm.list.scrollIndexIntoView(index);
821 var treeItem = bmm.treeLookup[parentId];
824 navigateTo(parentId, selectItem);
828 * @return {!cr.LinkController} The link controller used to open links based on
829 * user clicks and keyboard actions.
831 function getLinkController() {
832 return linkController ||
833 (linkController = new cr.LinkController(loadTimeData));
837 * Returns the selected bookmark nodes of the provided tree or list.
838 * If |opt_target| is not provided or null the active element is used.
839 * Only call this if the list or the tree is focused.
840 * @param {EventTarget=} opt_target The target list or tree.
841 * @return {!Array} Array of bookmark nodes.
843 function getSelectedBookmarkNodes(opt_target) {
844 return (opt_target || document.activeElement) == bmm.tree ?
845 bmm.tree.selectedFolders : bmm.list.selectedItems;
849 * @param {EventTarget=} opt_target The target list or tree.
850 * @return {!Array<string>} An array of the selected bookmark IDs.
852 function getSelectedBookmarkIds(opt_target) {
853 var selectedNodes = getSelectedBookmarkNodes(opt_target);
854 selectedNodes.sort(function(a, b) { return a.index - b.index });
855 return selectedNodes.map(function(node) {
861 * @param {BookmarkTreeNode} node The node to test.
862 * @return {boolean} Whether the given node is unmodifiable.
864 function isUnmodifiable(node) {
865 return !!(node && node.unmodifiable);
869 * @param {Array<BookmarkTreeNode>} nodes A list of BookmarkTreeNodes.
870 * @return {boolean} Whether any of the nodes is managed.
872 function hasUnmodifiable(nodes) {
873 return nodes.some(isUnmodifiable);
877 * Opens the selected bookmarks.
878 * @param {cr.LinkKind} kind The kind of link we want to open.
879 * @param {HTMLElement=} opt_eventTarget The target of the user initiated event.
881 function openBookmarks(kind, opt_eventTarget) {
882 // If we have selected any folders, we need to find all the bookmarks one
883 // level down. We use multiple async calls to getSubtree instead of getting
884 // the whole tree since we would like to minimize the amount of data sent.
886 var urlsP = getUrlsForOpenCommands(opt_eventTarget ? opt_eventTarget : null);
887 urlsP.then(function(urls) {
888 getLinkController().openUrls(assert(urls), kind);
889 chrome.bookmarkManagerPrivate.recordLaunch();
894 * Opens an item in the list.
896 function openItem() {
897 var bookmarkNodes = getSelectedBookmarkNodes();
898 // If we double clicked or pressed enter on a single folder, navigate to it.
899 if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0]))
900 navigateTo(bookmarkNodes[0].id);
902 openBookmarks(LinkKind.FOREGROUND_TAB);
906 * Refreshes search results after delete or undo-delete.
907 * This ensures children of deleted folders do not remain in results
909 function updateSearchResults() {
910 if (bmm.list.isSearch())
915 * Deletes the selected bookmarks. The bookmarks are saved in memory in case
916 * the user needs to undo the deletion.
917 * @param {EventTarget=} opt_target The deleter of bookmarks.
919 function deleteBookmarks(opt_target) {
920 var selectedIds = getSelectedBookmarkIds(opt_target);
921 if (!selectedIds.length)
924 var filteredIds = getFilteredSelectedBookmarkIds(opt_target);
925 lastDeleted = {nodes: [], target: opt_target || document.activeElement};
927 function performDelete() {
928 // Only remove filtered ids.
929 chrome.bookmarkManagerPrivate.removeTrees(filteredIds);
930 $('undo-delete-command').canExecuteChange();
931 $('undo-delete-from-folders-menu-command').canExecuteChange();
932 performGlobalUndo = undoDelete;
935 // First, store information about the bookmarks being deleted.
936 // Store all selected ids.
937 selectedIds.forEach(function(id) {
938 chrome.bookmarks.getSubTree(id, function(results) {
939 lastDeleted.nodes.push(results);
941 // When all nodes have been saved, perform the deletion.
942 if (lastDeleted.nodes.length === selectedIds.length) {
944 updateSearchResults();
951 * Restores a tree of bookmarks under a specified folder.
952 * @param {BookmarkTreeNode} node The node to restore.
953 * @param {(string|number)=} opt_parentId If a string is passed, it's the ID of
954 * the folder to restore under. If not specified or a number is passed, the
955 * original parentId of the node will be used.
957 function restoreTree(node, opt_parentId) {
959 parentId: typeof opt_parentId == 'string' ? opt_parentId : node.parentId,
965 chrome.bookmarks.create(bookmarkInfo, function(result) {
967 console.error('Failed to restore bookmark.');
972 // Restore the children using the new ID for this node.
973 node.children.forEach(function(child) {
974 restoreTree(child, result.id);
978 updateSearchResults();
983 * Restores the last set of bookmarks that was deleted.
985 function undoDelete() {
986 lastDeleted.nodes.forEach(function(arr) {
987 arr.forEach(restoreTree);
990 $('undo-delete-command').canExecuteChange();
991 $('undo-delete-from-folders-menu-command').canExecuteChange();
993 // Only a single level of undo is supported, so disable global undo now.
994 performGlobalUndo = null;
998 * Computes folder for "Add Page" and "Add Folder".
999 * @return {string} The id of folder node where we'll create new page/folder.
1001 function computeParentFolderForNewItem() {
1002 if (document.activeElement == bmm.tree)
1003 return bmm.list.parentId;
1004 var selectedItem = bmm.list.selectedItem;
1005 return selectedItem && bmm.isFolder(selectedItem) ?
1006 selectedItem.id : bmm.list.parentId;
1010 * Callback for rename folder and edit command. This starts editing for
1013 function editSelectedItem() {
1014 if (document.activeElement == bmm.tree) {
1015 bmm.tree.selectedItem.editing = true;
1017 var li = bmm.list.getListItem(bmm.list.selectedItem);
1024 * Callback for the new folder command. This creates a new folder and starts
1026 * @param {EventTarget=} opt_target The target to create a new folder in.
1028 function newFolder(opt_target) {
1029 performGlobalUndo = null; // This can't be undone, so disable global undo.
1031 var parentId = computeParentFolderForNewItem();
1032 var selectedItem = bmm.list.selectedItem;
1034 // Callback is called after tree and list data model updated.
1035 function createFolder(callback) {
1036 if (selectedItem && document.activeElement != bmm.tree &&
1037 !bmm.isFolder(selectedItem) && selectedItem.id != 'new') {
1038 newIndex = bmm.list.dataModel.indexOf(selectedItem) + 1;
1040 chrome.bookmarks.create({
1041 title: loadTimeData.getString('new_folder_name'),
1047 if ((opt_target || document.activeElement) == bmm.tree) {
1048 createFolder(function(newNode) {
1049 navigateTo(newNode.id, function() {
1050 bmm.treeLookup[newNode.id].editing = true;
1056 function editNewFolderInList() {
1057 createFolder(function(newNode) {
1058 var index = newNode.index;
1059 var sm = bmm.list.selectionModel;
1060 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
1061 scrollIntoViewAndMakeEditable(index);
1065 navigateTo(parentId, editNewFolderInList);
1069 * Scrolls the list item into view and makes it editable.
1070 * @param {number} index The index of the item to make editable.
1072 function scrollIntoViewAndMakeEditable(index) {
1073 bmm.list.scrollIndexIntoView(index);
1074 // onscroll is now dispatched asynchronously so we have to postpone
1076 setTimeout(function() {
1077 var item = bmm.list.getListItemByIndex(index);
1079 item.editing = true;
1084 * Adds a page to the current folder. This is called by the
1085 * add-new-bookmark-command handler.
1087 function addPage() {
1088 var parentId = computeParentFolderForNewItem();
1089 var selectedItem = bmm.list.selectedItem;
1091 function editNewBookmark() {
1092 if (selectedItem && document.activeElement != bmm.tree &&
1093 !bmm.isFolder(selectedItem)) {
1094 newIndex = bmm.list.dataModel.indexOf(selectedItem) + 1;
1104 var dataModel = bmm.list.dataModel;
1105 var index = dataModel.length;
1106 if (newIndex != undefined)
1108 dataModel.splice(index, 0, fakeNode);
1109 var sm = bmm.list.selectionModel;
1110 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
1111 scrollIntoViewAndMakeEditable(index);
1114 navigateTo(parentId, editNewBookmark);
1118 * This function is used to select items after a user action such as paste, drop
1120 * @param {BookmarkList|BookmarkTree} target The target of the user action.
1121 * @param {string=} opt_selectedTreeId If provided, then select that tree id.
1123 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1124 // We get one onCreated event per item so we delay the handling until we get
1125 // no more events coming.
1130 function handle(id, bookmarkNode) {
1131 clearTimeout(timer);
1132 if (opt_selectedTreeId || bmm.list.parentId == bookmarkNode.parentId)
1134 timer = setTimeout(handleTimeout, 50);
1137 function handleTimeout() {
1138 chrome.bookmarks.onCreated.removeListener(handle);
1139 chrome.bookmarks.onMoved.removeListener(handle);
1141 if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1142 var index = ids.indexOf(opt_selectedTreeId);
1143 if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1144 bmm.tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1146 } else if (target == bmm.list) {
1147 var dataModel = bmm.list.dataModel;
1148 var firstIndex = dataModel.findIndexById(ids[0]);
1149 var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1150 if (firstIndex != -1 && lastIndex != -1) {
1151 var selectionModel = bmm.list.selectionModel;
1152 selectionModel.selectedIndex = -1;
1153 selectionModel.selectRange(firstIndex, lastIndex);
1154 selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1159 bmm.list.endBatchUpdates();
1162 bmm.list.startBatchUpdates();
1164 chrome.bookmarks.onCreated.addListener(handle);
1165 chrome.bookmarks.onMoved.addListener(handle);
1166 timer = setTimeout(handleTimeout, 300);
1170 * Record user action.
1171 * @param {string} name An user action name.
1173 function recordUserAction(name) {
1174 chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1178 * The currently selected bookmark, based on where the user is clicking.
1179 * @return {string} The ID of the currently selected bookmark (could be from
1180 * tree view or list view).
1182 function getSelectedId() {
1183 if (document.activeElement == bmm.tree)
1184 return bmm.tree.selectedItem.bookmarkId;
1185 var selectedItem = bmm.list.selectedItem;
1186 return selectedItem && bmm.isFolder(selectedItem) ?
1187 selectedItem.id : bmm.tree.selectedItem.bookmarkId;
1191 * Pastes the copied/cutted bookmark into the right location depending whether
1192 * if it was called from Organize Menu or from Context Menu.
1193 * @param {string} id The id of the element being pasted from.
1195 function pasteBookmark(id) {
1196 recordUserAction('Paste');
1197 selectItemsAfterUserAction(/** @type {BookmarkList} */(bmm.list));
1198 chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1202 * Returns true if child is contained in another selected folder.
1203 * Traces parent nodes up the tree until a selected ancestor or root is found.
1205 function hasSelectedAncestor(parentNode) {
1206 function contains(arr, item) {
1207 for (var i = 0; i < arr.length; i++)
1208 if (arr[i] === item)
1213 // Don't search top level, cannot select permanent nodes in search.
1214 if (parentNode == null || parentNode.id <= 2)
1217 // Found selected ancestor.
1218 if (contains(getSelectedBookmarkNodes(), parentNode))
1222 return hasSelectedAncestor(
1223 bmm.tree.getBookmarkNodeById(parentNode.parentId));
1227 * @param {EventTarget=} opt_target A target to get bookmark IDs from.
1228 * @return {Array<string>} An array of bookmarks IDs.
1230 function getFilteredSelectedBookmarkIds(opt_target) {
1231 // Remove duplicates from filteredIds and return.
1232 var filteredIds = [];
1233 // Selected nodes to iterate through for matches.
1234 var nodes = getSelectedBookmarkNodes(opt_target);
1236 for (var i = 0; i < nodes.length; i++)
1237 if (!hasSelectedAncestor(bmm.tree.getBookmarkNodeById(nodes[i].parentId)))
1238 filteredIds.splice(0, 0, nodes[i].id);
1244 * Handler for the command event. This is used for context menu of list/tree
1245 * and organized menu.
1246 * @param {!Event} e The event object.
1248 function handleCommand(e) {
1249 var command = e.command;
1251 switch (command.id) {
1252 case 'import-menu-command':
1253 recordUserAction('Import');
1254 chrome.bookmarks.import();
1257 case 'export-menu-command':
1258 recordUserAction('Export');
1259 chrome.bookmarks.export();
1262 case 'undo-command':
1263 if (performGlobalUndo) {
1264 recordUserAction('UndoGlobal');
1265 performGlobalUndo();
1267 recordUserAction('UndoNone');
1271 case 'show-in-folder-command':
1272 recordUserAction('ShowInFolder');
1276 case 'open-in-new-tab-command':
1277 case 'open-in-background-tab-command':
1278 recordUserAction('OpenInNewTab');
1279 openBookmarks(LinkKind.BACKGROUND_TAB,
1280 assertInstanceof(e.target, HTMLElement));
1283 case 'open-in-new-window-command':
1284 recordUserAction('OpenInNewWindow');
1285 openBookmarks(LinkKind.WINDOW,
1286 assertInstanceof(e.target, HTMLElement));
1289 case 'open-incognito-window-command':
1290 recordUserAction('OpenIncognito');
1291 openBookmarks(LinkKind.INCOGNITO,
1292 assertInstanceof(e.target, HTMLElement));
1295 case 'delete-from-folders-menu-command':
1297 case 'delete-command':
1298 recordUserAction('Delete');
1299 deleteBookmarks(target);
1302 case 'copy-from-folders-menu-command':
1304 case 'copy-command':
1305 recordUserAction('Copy');
1306 chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(target),
1307 updatePasteCommand);
1310 case 'cut-from-folders-menu-command':
1313 recordUserAction('Cut');
1314 chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(target),
1316 updatePasteCommand();
1317 updateSearchResults();
1321 case 'paste-from-organize-menu-command':
1322 pasteBookmark(bmm.list.parentId);
1325 case 'paste-from-folders-menu-command':
1326 pasteBookmark(bmm.tree.selectedItem.bookmarkId);
1329 case 'paste-from-context-menu-command':
1330 pasteBookmark(getSelectedId());
1333 case 'sort-command':
1334 recordUserAction('Sort');
1335 chrome.bookmarkManagerPrivate.sortChildren(bmm.list.parentId);
1338 case 'rename-folder-command':
1342 case 'rename-folder-from-folders-menu-command':
1343 bmm.tree.selectedItem.editing = true;
1346 case 'edit-command':
1347 recordUserAction('Edit');
1351 case 'new-folder-from-folders-menu-command':
1353 case 'new-folder-command':
1354 recordUserAction('NewFolder');
1358 case 'add-new-bookmark-command':
1359 recordUserAction('AddPage');
1363 case 'open-in-same-window-command':
1364 recordUserAction('OpenInSame');
1368 case 'undo-delete-command':
1369 case 'undo-delete-from-folders-menu-command':
1370 recordUserAction('UndoDelete');
1376 // Execute the copy, cut and paste commands when those events are dispatched by
1377 // the browser. This allows us to rely on the browser to handle the keyboard
1378 // shortcuts for these commands.
1379 function installEventHandlerForCommand(eventName, commandId) {
1380 function handle(e) {
1381 if (document.activeElement != bmm.list &&
1382 document.activeElement != bmm.tree)
1384 var command = $(commandId);
1385 if (!command.disabled) {
1388 e.preventDefault(); // Prevent the system beep.
1391 if (eventName == 'paste') {
1392 // Paste is a bit special since we need to do an async call to see if we
1393 // can paste because the paste command might not be up to date.
1394 document.addEventListener(eventName, function(e) {
1395 updatePasteCommand(handle);
1398 document.addEventListener(eventName, handle);
1402 function initializeSplitter() {
1403 var splitter = document.querySelector('.main > .splitter');
1404 Splitter.decorate(splitter);
1406 var splitterStyle = splitter.previousElementSibling.style;
1408 // The splitter persists the size of the left component in the local store.
1409 if ('treeWidth' in window.localStorage)
1410 splitterStyle.width = window.localStorage['treeWidth'];
1412 splitter.addEventListener('resize', function(e) {
1413 window.localStorage['treeWidth'] = splitterStyle.width;
1417 function initializeBookmarkManager() {
1418 // Sometimes the extension API is not initialized.
1419 if (!chrome.bookmarks)
1420 console.error('Bookmarks extension API is not available');
1422 chrome.bookmarkManagerPrivate.getStrings(continueInitializeBookmarkManager);
1425 function continueInitializeBookmarkManager(localizedStrings) {
1426 loadLocalizedStrings(localizedStrings);
1428 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1430 cr.ui.decorate('cr-menu', Menu);
1431 cr.ui.decorate('button[menu]', MenuButton);
1432 cr.ui.decorate('command', Command);
1433 BookmarkList.decorate($('list'));
1434 BookmarkTree.decorate($('tree'));
1436 bmm.list.addEventListener('canceledit', handleCancelEdit);
1437 bmm.list.addEventListener('canExecute', handleCanExecuteForList);
1438 bmm.list.addEventListener('change', updateAllCommands);
1439 bmm.list.addEventListener('contextmenu', updateEditingCommands);
1440 bmm.list.addEventListener('dblclick', handleDoubleClickForList);
1441 bmm.list.addEventListener('edit', handleEdit);
1442 bmm.list.addEventListener('rename', handleRename);
1443 bmm.list.addEventListener('urlClicked', handleUrlClickedForList);
1445 bmm.tree.addEventListener('canExecute', handleCanExecuteForTree);
1446 bmm.tree.addEventListener('change', handleChangeForTree);
1447 bmm.tree.addEventListener('contextmenu', updateEditingCommands);
1448 bmm.tree.addEventListener('rename', handleRename);
1449 bmm.tree.addEventListener('load', handleLoadForTree);
1451 cr.ui.contextMenuHandler.addContextMenuProperty(
1452 /** @type {!Element} */(bmm.tree));
1453 bmm.list.contextMenu = $('context-menu');
1454 bmm.tree.contextMenu = $('context-menu');
1456 // We listen to hashchange so that we can update the currently shown folder
1457 // when // the user goes back and forward in the history.
1458 window.addEventListener('hashchange', processHash);
1460 document.querySelector('header form').onsubmit =
1461 /** @type {function(Event=)} */(function(e) {
1462 setSearch($('term').value);
1466 $('term').addEventListener('search', handleSearch);
1468 $('folders-button').addEventListener('click', handleMenuButtonClicked);
1469 $('organize-button').addEventListener('click', handleMenuButtonClicked);
1471 document.addEventListener('canExecute', handleCanExecuteForDocument);
1472 document.addEventListener('command', handleCommand);
1474 // Listen to copy, cut and paste events and execute the associated commands.
1475 installEventHandlerForCommand('copy', 'copy-command');
1476 installEventHandlerForCommand('cut', 'cut-command');
1477 installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1479 // Install shortcuts
1480 for (var name in commandShortcutMap) {
1481 $(name + '-command').shortcut = commandShortcutMap[name];
1484 // Disable almost all commands at startup.
1485 var commands = document.querySelectorAll('command');
1486 for (var i = 0, command; command = commands[i]; ++i) {
1487 if (command.id != 'import-menu-command' &&
1488 command.id != 'export-menu-command') {
1489 command.disabled = true;
1493 chrome.bookmarkManagerPrivate.canEdit(function(result) {
1497 chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1498 // TODO(rustema): propagate policy value to the bookmark manager when it
1500 incognitoModeAvailability = result;
1503 chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1504 canOpenNewWindows = result;
1507 cr.ui.FocusOutlineManager.forDocument(document);
1508 initializeSplitter();
1509 bmm.addBookmarkModelListeners();
1510 dnd.init(selectItemsAfterUserAction);
1514 initializeBookmarkManager();