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 CommandBinding = cr.ui.CommandBinding;
10 /** @const */ var LinkKind = cr.LinkKind;
11 /** @const */ var ListItem = cr.ui.ListItem;
12 /** @const */ var Menu = cr.ui.Menu;
13 /** @const */ var MenuButton = cr.ui.MenuButton;
14 /** @const */ var Promise = cr.Promise;
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 {Array.<BookmarkTreeNode>}
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 {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 = tree.selectedItem.bookmarkId;
150 * Navigates to a bookmark ID.
151 * @param {string} id The ID to navigate to.
152 * @param {function()} callback Function called when list view loaded or
153 * displayed specified folder.
155 function navigateTo(id, callback) {
158 if (list.parentId == id) {
163 var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
164 folderMetricsNameMap['subfolder'];
165 chrome.metricsPrivate.recordUserAction(
166 'BookmarkManager_NavigateTo_' + metricsId);
168 addOneShotEventListener(list, 'load', callback);
173 * Updates the parent ID of the bookmark list and selects the correct tree item.
174 * @param {string} id The id.
176 function updateParentId(id) {
177 // Setting list.parentId fires 'load' event.
180 // When tree.selectedItem changed, tree view calls navigatTo() then it
181 // calls updateHash() when list view displayed specified folder.
182 tree.selectedItem = bmm.treeLookup[id] || tree.selectedItem;
185 // Process the location hash. This is called by onhashchange and when the page
187 function processHash() {
188 var id = window.location.hash.slice(1);
190 // If we do not have a hash, select first item in the tree.
191 id = tree.items[0].bookmarkId;
195 if (/^e=/.test(id)) {
198 // If hash contains e=, edit the item specified.
199 chrome.bookmarks.get(id, function(bookmarkNodes) {
200 // Verify the node to edit is a valid node.
201 if (!bookmarkNodes || bookmarkNodes.length != 1)
203 var bookmarkNode = bookmarkNodes[0];
205 // After the list reloads, edit the desired bookmark.
206 var editBookmark = function(e) {
207 var index = list.dataModel.findIndexById(bookmarkNode.id);
209 var sm = list.selectionModel;
210 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
211 scrollIntoViewAndMakeEditable(index);
215 navigateTo(bookmarkNode.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 tree.add(searchTreeItem);
277 tree.selectedItem = searchTreeItem;
280 tree.selectedItem = tree.items[0];
281 id = tree.selectedItem.bookmarkId;
284 // Navigate now and update hash immediately.
285 navigateTo(id, updateHash);
288 // Handle the logo button UI.
289 // When the user clicks the button we should navigate "home" and focus the list.
290 function handleClickOnLogoButton(e) {
296 * This returns the user visible path to the folder where the bookmark is
298 * @param {number} parentId The ID of the parent folder.
299 * @return {string} The path to the the bookmark,
301 function getFolder(parentId) {
302 var parentNode = tree.getBookmarkNodeById(parentId);
304 var s = parentNode.title;
305 if (parentNode.parentId != bmm.ROOT_ID) {
306 return getFolder(parentNode.parentId) + '/' + s;
312 function handleLoadForTree(e) {
316 function getAllUrls(nodes) {
319 // Adds the node and all its direct children.
320 function addNodes(node) {
321 if (node.id == 'new')
325 node.children.forEach(function(child) {
326 if (!bmm.isFolder(child))
327 urls.push(child.url);
334 // Get a future promise for the nodes.
335 var promises = nodes.map(function(node) {
336 if (bmm.isFolder(node))
337 return bmm.loadSubtree(node.id);
338 // Not a folder so we already have all the data we need.
339 return new Promise(node);
342 var urlsPromise = new Promise();
344 var p = Promise.all.apply(null, promises);
345 p.addListener(function(nodes) {
346 nodes.forEach(function(node) {
349 urlsPromise.value = urls;
356 * Returns the nodes (non recursive) to use for the open commands.
357 * @param {HTMLElement} target .
358 * @return {Array.<BookmarkTreeNode>} .
360 function getNodesForOpen(target) {
361 if (target == tree) {
362 var folderItem = tree.selectedItem;
363 return folderItem == searchTreeItem ?
364 list.dataModel.slice() : tree.selectedFolders;
366 var items = list.selectedItems;
367 return items.length ? items : 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} .
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(e.target).addListener(function(urls) {
412 var disabled = !urls.length;
413 command.disabled = disabled;
414 e.canExecute = !disabled;
419 * Calls the backend to figure out if we can paste the clipboard into the active
421 * @param {Function=} opt_f Function to call after the state has been updated.
423 function updatePasteCommand(opt_f) {
424 function update(canPaste) {
425 var organizeMenuCommand = $('paste-from-organize-menu-command');
426 var contextMenuCommand = $('paste-from-context-menu-command');
427 organizeMenuCommand.disabled = !canPaste;
428 contextMenuCommand.disabled = !canPaste;
432 // We cannot paste into search view.
436 chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
439 function handleCanExecuteForDocument(e) {
440 var command = e.command;
441 switch (command.id) {
442 case 'import-menu-command':
443 e.canExecute = canEdit;
445 case 'export-menu-command':
446 // We can always execute the export-menu command.
450 e.canExecute = !list.isSearch() && list.dataModel.length > 1;
453 // The global undo command has no visible UI, so always enable it, and
454 // just make it a no-op if undo is not possible.
458 canExecuteForList(e);
464 * Helper function for handling canExecute for the list and the tree.
465 * @param {!Event} e Can execute event object.
466 * @param {boolean} isSearch Whether the user is trying to do a command on
469 function canExecuteShared(e, isSearch) {
470 var command = e.command;
471 var commandId = command.id;
473 case 'paste-from-organize-menu-command':
474 case 'paste-from-context-menu-command':
475 updatePasteCommand();
478 case 'add-new-bookmark-command':
479 case 'new-folder-command':
480 e.canExecute = !isSearch && canEdit;
483 case 'open-in-new-tab-command':
484 updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
486 case 'open-in-background-tab-command':
487 updateOpenCommand(e, command, '', '', false);
489 case 'open-in-new-window-command':
490 updateOpenCommand(e, command,
491 'open_in_new_window', 'open_all_new_window',
492 // Disabled when incognito is forced.
493 incognitoModeAvailability == 'forced' || !canOpenNewWindows);
495 case 'open-incognito-window-command':
496 updateOpenCommand(e, command,
497 'open_incognito', 'open_all_incognito',
498 // Not available when incognito is disabled.
499 incognitoModeAvailability == 'disabled');
502 case 'undo-delete-command':
503 e.canExecute = !!lastDeletedNodes;
509 * Helper function for handling canExecute for the list and document.
510 * @param {!Event} e Can execute event object.
512 function canExecuteForList(e) {
513 var command = e.command;
514 var commandId = command.id;
516 function hasSelected() {
517 return !!list.selectedItem;
520 function hasSingleSelected() {
521 return list.selectedItems.length == 1;
524 function canCopyItem(item) {
525 return item.id != 'new';
528 function canCopyItems() {
529 var selectedItems = list.selectedItems;
530 return selectedItems && selectedItems.some(canCopyItem);
533 function isSearch() {
534 return list.isSearch();
538 case 'rename-folder-command':
539 // Show rename if a single folder is selected.
540 var items = list.selectedItems;
541 if (items.length != 1) {
542 e.canExecute = false;
543 command.hidden = true;
545 var isFolder = bmm.isFolder(items[0]);
546 e.canExecute = isFolder && canEdit;
547 command.hidden = !isFolder;
552 // Show the edit command if not a folder.
553 var items = list.selectedItems;
554 if (items.length != 1) {
555 e.canExecute = false;
556 command.hidden = false;
558 var isFolder = bmm.isFolder(items[0]);
559 e.canExecute = !isFolder && canEdit;
560 command.hidden = isFolder;
564 case 'show-in-folder-command':
565 e.canExecute = isSearch() && hasSingleSelected();
568 case 'delete-command':
570 e.canExecute = canCopyItems() && canEdit;
574 e.canExecute = canCopyItems();
577 case 'open-in-same-window-command':
578 e.canExecute = hasSelected();
582 canExecuteShared(e, isSearch());
586 // Update canExecute for the commands when the list is the active element.
587 function handleCanExecuteForList(e) {
588 if (e.target != list) return;
589 canExecuteForList(e);
592 // Update canExecute for the commands when the tree is the active element.
593 function handleCanExecuteForTree(e) {
594 if (e.target != tree) return;
596 var command = e.command;
597 var commandId = command.id;
599 function hasSelected() {
600 return !!e.target.selectedItem;
603 function isSearch() {
604 var item = e.target.selectedItem;
605 return item == searchTreeItem;
608 function isTopLevelItem() {
609 return e.target.selectedItem.parentNode == tree;
613 case 'rename-folder-command':
614 command.hidden = false;
615 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
619 command.hidden = true;
620 e.canExecute = false;
623 case 'delete-command':
625 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
629 e.canExecute = hasSelected() && !isTopLevelItem();
633 canExecuteShared(e, isSearch());
638 * Update the canExecute state of the commands when the selection changes.
639 * @param {Event} e The change event object.
641 function updateCommandsBasedOnSelection(e) {
642 if (e.target == document.activeElement) {
643 // Paste only needs to be updated when the tree selection changes.
644 var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
645 'add-new-bookmark', 'new-folder', 'open-in-new-tab',
646 'open-in-background-tab', 'open-in-new-window', 'open-incognito-window',
647 'open-in-same-window', 'show-in-folder'];
649 if (e.target == tree) {
650 commandNames.push('paste-from-context-menu', 'paste-from-organize-menu',
654 commandNames.forEach(function(baseId) {
655 $(baseId + '-command').canExecuteChange();
660 function updateEditingCommands() {
661 var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
662 'add-new-bookmark', 'new-folder', 'sort',
663 'paste-from-context-menu', 'paste-from-organize-menu'];
665 chrome.bookmarkManagerPrivate.canEdit(function(result) {
666 if (result != canEdit) {
668 editingCommands.forEach(function(baseId) {
669 $(baseId + '-command').canExecuteChange();
675 function handleChangeForTree(e) {
676 updateCommandsBasedOnSelection(e);
677 navigateTo(tree.selectedItem.bookmarkId, updateHash);
680 function handleOrganizeButtonClick(e) {
681 updateEditingCommands();
682 $('add-new-bookmark-command').canExecuteChange();
683 $('new-folder-command').canExecuteChange();
684 $('sort-command').canExecuteChange();
687 function handleRename(e) {
689 var bookmarkNode = item.bookmarkNode;
690 chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
691 performGlobalUndo = null; // This can't be undone, so disable global undo.
694 function handleEdit(e) {
696 var bookmarkNode = item.bookmarkNode;
698 title: bookmarkNode.title
700 if (!bmm.isFolder(bookmarkNode))
701 context.url = bookmarkNode.url;
703 if (bookmarkNode.id == 'new') {
704 selectItemsAfterUserAction(list);
707 context.parentId = bookmarkNode.parentId;
708 chrome.bookmarks.create(context, function(node) {
709 // A new node was created and will get added to the list due to the
711 var dataModel = list.dataModel;
712 var index = dataModel.indexOf(bookmarkNode);
713 dataModel.splice(index, 1);
716 var newIndex = dataModel.findIndexById(node.id);
717 if (newIndex != -1) {
718 var sm = list.selectionModel;
719 list.scrollIndexIntoView(newIndex);
720 sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
725 chrome.bookmarks.update(bookmarkNode.id, context);
727 performGlobalUndo = null; // This can't be undone, so disable global undo.
730 function handleCancelEdit(e) {
732 var bookmarkNode = item.bookmarkNode;
733 if (bookmarkNode.id == 'new') {
734 var dataModel = list.dataModel;
735 var index = dataModel.findIndexById('new');
736 dataModel.splice(index, 1);
741 * Navigates to the folder that the selected item is in and selects it. This is
742 * used for the show-in-folder command.
744 function showInFolder() {
745 var bookmarkNode = list.selectedItem;
748 var parentId = bookmarkNode.parentId;
750 // After the list is loaded we should select the revealed item.
751 function selectItem() {
752 var index = list.dataModel.findIndexById(bookmarkNode.id);
755 var sm = list.selectionModel;
756 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
757 list.scrollIndexIntoView(index);
760 var treeItem = bmm.treeLookup[parentId];
763 navigateTo(parentId, selectItem);
767 * @return {!cr.LinkController} The link controller used to open links based on
768 * user clicks and keyboard actions.
770 function getLinkController() {
771 return linkController ||
772 (linkController = new cr.LinkController(loadTimeData));
776 * Returns the selected bookmark nodes of the provided tree or list.
777 * If |opt_target| is not provided or null the active element is used.
778 * Only call this if the list or the tree is focused.
779 * @param {BookmarkList|BookmarkTree} opt_target The target list or tree.
780 * @return {!Array} Array of bookmark nodes.
782 function getSelectedBookmarkNodes(opt_target) {
783 return (opt_target || document.activeElement) == tree ?
784 tree.selectedFolders : list.selectedItems;
788 * @return {!Array.<string>} An array of the selected bookmark IDs.
790 function getSelectedBookmarkIds() {
791 var selectedNodes = getSelectedBookmarkNodes();
792 selectedNodes.sort(function(a, b) { return a.index - b.index });
793 return selectedNodes.map(function(node) {
799 * Opens the selected bookmarks.
800 * @param {LinkKind} kind The kind of link we want to open.
801 * @param {HTMLElement} opt_eventTarget The target of the user initiated event.
803 function openBookmarks(kind, opt_eventTarget) {
804 // If we have selected any folders, we need to find all the bookmarks one
805 // level down. We use multiple async calls to getSubtree instead of getting
806 // the whole tree since we would like to minimize the amount of data sent.
808 var urlsP = getUrlsForOpenCommands(opt_eventTarget);
809 urlsP.addListener(function(urls) {
810 getLinkController().openUrls(urls, kind);
811 chrome.bookmarkManagerPrivate.recordLaunch();
816 * Opens an item in the list.
818 function openItem() {
819 var bookmarkNodes = getSelectedBookmarkNodes();
820 // If we double clicked or pressed enter on a single folder, navigate to it.
821 if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) {
822 navigateTo(bookmarkNodes[0].id, updateHash);
824 openBookmarks(LinkKind.FOREGROUND_TAB);
829 * Deletes the selected bookmarks. The bookmarks are saved in memory in case
830 * the user needs to undo the deletion.
832 function deleteBookmarks() {
833 var selectedIds = getSelectedBookmarkIds();
834 lastDeletedNodes = [];
836 function performDelete() {
837 chrome.bookmarkManagerPrivate.removeTrees(selectedIds);
838 $('undo-delete-command').canExecuteChange();
839 performGlobalUndo = undoDelete;
842 // First, store information about the bookmarks being deleted.
843 selectedIds.forEach(function(id) {
844 chrome.bookmarks.getSubTree(id, function(results) {
845 lastDeletedNodes.push(results);
847 // When all nodes have been saved, perform the deletion.
848 if (lastDeletedNodes.length === selectedIds.length)
855 * Restores a tree of bookmarks under a specified folder.
856 * @param {BookmarkTreeNode} node The node to restore.
857 * @param {=string} parentId The ID of the folder to restore under. If not
858 * specified, the original parentId of the node will be used.
860 function restoreTree(node, parentId) {
862 parentId: parentId || node.parentId,
868 chrome.bookmarks.create(bookmarkInfo, function(result) {
870 console.error('Failed to restore bookmark.');
875 // Restore the children using the new ID for this node.
876 node.children.forEach(function(child) {
877 restoreTree(child, result.id);
884 * Restores the last set of bookmarks that was deleted.
886 function undoDelete() {
887 lastDeletedNodes.forEach(function(arr) {
888 arr.forEach(restoreTree);
890 lastDeletedNodes = null;
891 $('undo-delete-command').canExecuteChange();
893 // Only a single level of undo is supported, so disable global undo now.
894 performGlobalUndo = null;
898 * Computes folder for "Add Page" and "Add Folder".
899 * @return {string} The id of folder node where we'll create new page/folder.
901 function computeParentFolderForNewItem() {
902 if (document.activeElement == tree)
903 return list.parentId;
904 var selectedItem = list.selectedItem;
905 return selectedItem && bmm.isFolder(selectedItem) ?
906 selectedItem.id : list.parentId;
910 * Callback for rename folder and edit command. This starts editing for
913 function editSelectedItem() {
914 if (document.activeElement == tree) {
915 tree.selectedItem.editing = true;
917 var li = list.getListItem(list.selectedItem);
924 * Callback for the new folder command. This creates a new folder and starts
927 function newFolder() {
928 performGlobalUndo = null; // This can't be undone, so disable global undo.
930 var parentId = computeParentFolderForNewItem();
932 // Callback is called after tree and list data model updated.
933 function createFolder(callback) {
934 chrome.bookmarks.create({
935 title: loadTimeData.getString('new_folder_name'),
940 if (document.activeElement == tree) {
941 createFolder(function(newNode) {
942 navigateTo(newNode.id, function() {
943 bmm.treeLookup[newNode.id].editing = true;
949 function editNewFolderInList() {
950 createFolder(function() {
951 var index = list.dataModel.length - 1;
952 var sm = list.selectionModel;
953 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
954 scrollIntoViewAndMakeEditable(index);
958 navigateTo(parentId, editNewFolderInList);
962 * Scrolls the list item into view and makes it editable.
963 * @param {number} index The index of the item to make editable.
965 function scrollIntoViewAndMakeEditable(index) {
966 list.scrollIndexIntoView(index);
967 // onscroll is now dispatched asynchronously so we have to postpone
969 setTimeout(function() {
970 var item = list.getListItemByIndex(index);
977 * Adds a page to the current folder. This is called by the
978 * add-new-bookmark-command handler.
981 var parentId = computeParentFolderForNewItem();
983 function editNewBookmark() {
990 var dataModel = list.dataModel;
991 var length = dataModel.length;
992 dataModel.splice(length, 0, fakeNode);
993 var sm = list.selectionModel;
994 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
995 scrollIntoViewAndMakeEditable(length);
998 navigateTo(parentId, editNewBookmark);
1002 * This function is used to select items after a user action such as paste, drop
1004 * @param {BookmarkList|BookmarkTree} target The target of the user action.
1005 * @param {=string} opt_selectedTreeId If provided, then select that tree id.
1007 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1008 // We get one onCreated event per item so we delay the handling until we get
1009 // no more events coming.
1014 function handle(id, bookmarkNode) {
1015 clearTimeout(timer);
1016 if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
1018 timer = setTimeout(handleTimeout, 50);
1021 function handleTimeout() {
1022 chrome.bookmarks.onCreated.removeListener(handle);
1023 chrome.bookmarks.onMoved.removeListener(handle);
1025 if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1026 var index = ids.indexOf(opt_selectedTreeId);
1027 if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1028 tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1030 } else if (target == list) {
1031 var dataModel = list.dataModel;
1032 var firstIndex = dataModel.findIndexById(ids[0]);
1033 var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1034 if (firstIndex != -1 && lastIndex != -1) {
1035 var selectionModel = list.selectionModel;
1036 selectionModel.selectedIndex = -1;
1037 selectionModel.selectRange(firstIndex, lastIndex);
1038 selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1043 list.endBatchUpdates();
1046 list.startBatchUpdates();
1048 chrome.bookmarks.onCreated.addListener(handle);
1049 chrome.bookmarks.onMoved.addListener(handle);
1050 timer = setTimeout(handleTimeout, 300);
1054 * Record user action.
1055 * @param {string} name An user action name.
1057 function recordUserAction(name) {
1058 chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1062 * The currently selected bookmark, based on where the user is clicking.
1063 * @return {string} The ID of the currently selected bookmark (could be from
1064 * tree view or list view).
1066 function getSelectedId() {
1067 if (document.activeElement == tree)
1068 return tree.selectedItem.bookmarkId;
1069 var selectedItem = list.selectedItem;
1070 return selectedItem && bmm.isFolder(selectedItem) ?
1071 selectedItem.id : tree.selectedItem.bookmarkId;
1075 * Pastes the copied/cutted bookmark into the right location depending whether
1076 * if it was called from Organize Menu or from Context Menu.
1077 * @param {string} id The id of the element being pasted from.
1079 function pasteBookmark(id) {
1080 recordUserAction('Paste');
1081 selectItemsAfterUserAction(list);
1082 chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1086 * Handler for the command event. This is used for context menu of list/tree
1087 * and organized menu.
1088 * @param {!Event} e The event object.
1090 function handleCommand(e) {
1091 var command = e.command;
1092 var commandId = command.id;
1093 switch (commandId) {
1094 case 'import-menu-command':
1095 recordUserAction('Import');
1096 chrome.bookmarks.import();
1098 case 'export-menu-command':
1099 recordUserAction('Export');
1100 chrome.bookmarks.export();
1102 case 'undo-command':
1103 if (performGlobalUndo) {
1104 recordUserAction('UndoGlobal');
1105 performGlobalUndo();
1107 recordUserAction('UndoNone');
1110 case 'show-in-folder-command':
1111 recordUserAction('ShowInFolder');
1114 case 'open-in-new-tab-command':
1115 case 'open-in-background-tab-command':
1116 recordUserAction('OpenInNewTab');
1117 openBookmarks(LinkKind.BACKGROUND_TAB, e.target);
1119 case 'open-in-new-window-command':
1120 recordUserAction('OpenInNewWindow');
1121 openBookmarks(LinkKind.WINDOW, e.target);
1123 case 'open-incognito-window-command':
1124 recordUserAction('OpenIncognito');
1125 openBookmarks(LinkKind.INCOGNITO, e.target);
1127 case 'delete-command':
1128 recordUserAction('Delete');
1131 case 'copy-command':
1132 recordUserAction('Copy');
1133 chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
1134 updatePasteCommand);
1137 recordUserAction('Cut');
1138 chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
1139 updatePasteCommand);
1141 case 'paste-from-organize-menu-command':
1142 pasteBookmark(list.parentId);
1144 case 'paste-from-context-menu-command':
1145 pasteBookmark(getSelectedId());
1147 case 'sort-command':
1148 recordUserAction('Sort');
1149 chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
1151 case 'rename-folder-command':
1154 case 'edit-command':
1155 recordUserAction('Edit');
1158 case 'new-folder-command':
1159 recordUserAction('NewFolder');
1162 case 'add-new-bookmark-command':
1163 recordUserAction('AddPage');
1166 case 'open-in-same-window-command':
1167 recordUserAction('OpenInSame');
1170 case 'undo-delete-command':
1171 recordUserAction('UndoDelete');
1177 // Execute the copy, cut and paste commands when those events are dispatched by
1178 // the browser. This allows us to rely on the browser to handle the keyboard
1179 // shortcuts for these commands.
1180 function installEventHandlerForCommand(eventName, commandId) {
1181 function handle(e) {
1182 if (document.activeElement != list && document.activeElement != tree)
1184 var command = $(commandId);
1185 if (!command.disabled) {
1188 e.preventDefault(); // Prevent the system beep.
1191 if (eventName == 'paste') {
1192 // Paste is a bit special since we need to do an async call to see if we
1193 // can paste because the paste command might not be up to date.
1194 document.addEventListener(eventName, function(e) {
1195 updatePasteCommand(handle);
1198 document.addEventListener(eventName, handle);
1202 function initializeSplitter() {
1203 var splitter = document.querySelector('.main > .splitter');
1204 Splitter.decorate(splitter);
1206 // The splitter persists the size of the left component in the local store.
1207 if ('treeWidth' in localStorage)
1208 splitter.previousElementSibling.style.width = localStorage['treeWidth'];
1210 splitter.addEventListener('resize', function(e) {
1211 localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
1215 function initializeBookmarkManager() {
1216 // Sometimes the extension API is not initialized.
1217 if (!chrome.bookmarks)
1218 console.error('Bookmarks extension API is not available');
1220 chrome.bookmarkManagerPrivate.getStrings(loadLocalizedStrings);
1222 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1224 cr.ui.decorate('menu', Menu);
1225 cr.ui.decorate('button[menu]', MenuButton);
1226 cr.ui.decorate('command', Command);
1227 BookmarkList.decorate(list);
1228 BookmarkTree.decorate(tree);
1230 list.addEventListener('canceledit', handleCancelEdit);
1231 list.addEventListener('canExecute', handleCanExecuteForList);
1232 list.addEventListener('change', updateCommandsBasedOnSelection);
1233 list.addEventListener('contextmenu', updateEditingCommands);
1234 list.addEventListener('dblclick', handleDoubleClickForList);
1235 list.addEventListener('edit', handleEdit);
1236 list.addEventListener('rename', handleRename);
1237 list.addEventListener('urlClicked', handleUrlClickedForList);
1239 tree.addEventListener('canExecute', handleCanExecuteForTree);
1240 tree.addEventListener('change', handleChangeForTree);
1241 tree.addEventListener('contextmenu', updateEditingCommands);
1242 tree.addEventListener('rename', handleRename);
1243 tree.addEventListener('load', handleLoadForTree);
1245 cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1246 list.contextMenu = $('context-menu');
1247 tree.contextMenu = $('context-menu');
1249 // We listen to hashchange so that we can update the currently shown folder
1250 // when // the user goes back and forward in the history.
1251 window.addEventListener('hashchange', processHash);
1253 document.querySelector('.header form').onsubmit = function(e) {
1254 setSearch($('term').value);
1258 $('term').addEventListener('search', handleSearch);
1260 document.querySelector('.summary > button').addEventListener(
1261 'click', handleOrganizeButtonClick);
1263 document.querySelector('button.logo').addEventListener(
1264 'click', handleClickOnLogoButton);
1266 document.addEventListener('canExecute', handleCanExecuteForDocument);
1267 document.addEventListener('command', handleCommand);
1269 // Listen to copy, cut and paste events and execute the associated commands.
1270 installEventHandlerForCommand('copy', 'copy-command');
1271 installEventHandlerForCommand('cut', 'cut-command');
1272 installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1274 // Install shortcuts
1275 for (var name in commandShortcutMap) {
1276 $(name + '-command').shortcut = commandShortcutMap[name];
1279 // Disable almost all commands at startup.
1280 var commands = document.querySelectorAll('command');
1281 for (var i = 0, command; command = commands[i]; ++i) {
1282 if (command.id != 'import-menu-command' &&
1283 command.id != 'export-menu-command') {
1284 command.disabled = true;
1288 chrome.bookmarkManagerPrivate.canEdit(function(result) {
1292 chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1293 // TODO(rustema): propagate policy value to the bookmark manager when it
1295 incognitoModeAvailability = result;
1298 chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1299 canOpenNewWindows = result;
1302 cr.ui.FocusOutlineManager.forDocument(document);
1303 initializeSplitter();
1304 bmm.addBookmarkModelListeners();
1305 dnd.init(selectItemsAfterUserAction);
1309 initializeBookmarkManager();