[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / resources / bookmark_manager / js / main.js
blobf6f2eb32370d16bff44d2a61a19cf584273eae3c
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.
5 (function() {
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 Splitter = cr.ui.Splitter;
15 /** @const */ var TreeItem = cr.ui.TreeItem;
17 /**
18 * An array containing the BookmarkTreeNodes that were deleted in the last
19 * deletion action. This is used for implementing undo.
20 * @type {Array.<BookmarkTreeNode>}
22 var lastDeletedNodes;
24 /**
26 * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
27 * view. Zero means pointer doesn't hover on folder.
28 * @type {number}
30 var lastHoverOnFolderTimeStamp = 0;
32 /**
33 * Holds a function that will undo that last action, if global undo is enabled.
34 * @type {Function}
36 var performGlobalUndo;
38 /**
39 * Holds a link controller singleton. Use getLinkController() rarther than
40 * accessing this variabie.
41 * @type {LinkController}
43 var linkController;
45 /**
46 * New Windows are not allowed in Windows 8 metro mode.
48 var canOpenNewWindows = true;
50 /**
51 * Incognito mode availability can take the following values: ,
52 * - 'enabled' for when both normal and incognito modes are available;
53 * - 'disabled' for when incognito mode is disabled;
54 * - 'forced' for when incognito mode is forced (normal mode is unavailable).
56 var incognitoModeAvailability = 'enabled';
58 /**
59 * Whether bookmarks can be modified.
60 * @type {boolean}
62 var canEdit = true;
64 /**
65 * @type {TreeItem}
66 * @const
68 var searchTreeItem = new TreeItem({
69 bookmarkId: 'q='
70 });
72 /**
73 * Command shortcut mapping.
74 * @const
76 var commandShortcutMap = cr.isMac ? {
77 'edit': 'Enter',
78 // On Mac we also allow Meta+Backspace.
79 'delete': 'U+007F U+0008 Meta-U+0008',
80 'open-in-background-tab': 'Meta-Enter',
81 'open-in-new-tab': 'Shift-Meta-Enter',
82 'open-in-same-window': 'Meta-Down',
83 'open-in-new-window': 'Shift-Enter',
84 'rename-folder': 'Enter',
85 // Global undo is Command-Z. It is not in any menu.
86 'undo': 'Meta-U+005A',
87 } : {
88 'edit': 'F2',
89 'delete': 'U+007F',
90 'open-in-background-tab': 'Ctrl-Enter',
91 'open-in-new-tab': 'Shift-Ctrl-Enter',
92 'open-in-same-window': 'Enter',
93 'open-in-new-window': 'Shift-Enter',
94 'rename-folder': 'F2',
95 // Global undo is Ctrl-Z. It is not in any menu.
96 'undo': 'Ctrl-U+005A',
99 /**
100 * Mapping for folder id to suffix of UMA. These names will be appeared
101 * after "BookmarkManager_NavigateTo_" in UMA dashboard.
102 * @const
104 var folderMetricsNameMap = {
105 '1': 'BookmarkBar',
106 '2': 'Other',
107 '3': 'Mobile',
108 'q=': 'Search',
109 'subfolder': 'SubFolder',
113 * Adds an event listener to a node that will remove itself after firing once.
114 * @param {!Element} node The DOM node to add the listener to.
115 * @param {string} name The name of the event listener to add to.
116 * @param {function(Event)} handler Function called when the event fires.
118 function addOneShotEventListener(node, name, handler) {
119 var f = function(e) {
120 handler(e);
121 node.removeEventListener(name, f);
123 node.addEventListener(name, f);
126 // Get the localized strings from the backend via bookmakrManagerPrivate API.
127 function loadLocalizedStrings(data) {
128 // The strings may contain & which we need to strip.
129 for (var key in data) {
130 data[key] = data[key].replace(/&/, '');
133 loadTimeData.data = data;
134 i18nTemplate.process(document, loadTimeData);
136 searchTreeItem.label = loadTimeData.getString('search');
137 searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' :
138 'images/bookmark_manager_search.png';
142 * Updates the location hash to reflect the current state of the application.
144 function updateHash() {
145 window.location.hash = tree.selectedItem.bookmarkId;
146 updateAllCommands();
150 * Navigates to a bookmark ID.
151 * @param {string} id The ID to navigate to.
152 * @param {function()=} opt_callback Function called when list view loaded or
153 * displayed specified folder.
155 function navigateTo(id, opt_callback) {
156 window.location.hash = id;
157 updateAllCommands();
159 var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
160 folderMetricsNameMap['subfolder'];
161 chrome.metricsPrivate.recordUserAction(
162 'BookmarkManager_NavigateTo_' + metricsId);
164 if (opt_callback) {
165 if (list.parentId == id)
166 opt_callback();
167 else
168 addOneShotEventListener(list, 'load', opt_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.
178 list.parentId = id;
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
186 // is first loaded.
187 function processHash() {
188 var id = window.location.hash.slice(1);
189 if (!id) {
190 // If we do not have a hash, select first item in the tree.
191 id = tree.items[0].bookmarkId;
194 var valid = false;
195 if (/^e=/.test(id)) {
196 id = id.slice(2);
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)
202 return;
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);
208 if (index != -1) {
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.
220 return;
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));
225 valid = true;
228 // Navigate to bookmark 'id' (which may be a query of the form q=query).
229 if (valid) {
230 updateParentId(id);
231 } else {
232 // We need to verify that this is a correct ID.
233 chrome.bookmarks.get(id, function(items) {
234 if (items && items.length == 1)
235 updateParentId(id);
240 // Activate is handled by the open-in-same-window-command.
241 function handleDoubleClickForList(e) {
242 if (e.button == 0)
243 $('open-in-same-window-command').execute();
246 // The list dispatches an event when the user clicks on the URL or the Show in
247 // folder part.
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) {
262 if (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;
275 if (searchText) {
276 tree.add(searchTreeItem);
277 tree.selectedItem = searchTreeItem;
278 } else {
279 // Go "home".
280 tree.selectedItem = tree.items[0];
281 id = tree.selectedItem.bookmarkId;
284 navigateTo(id);
287 // Handle the logo button UI.
288 // When the user clicks the button we should navigate "home" and focus the list.
289 function handleClickOnLogoButton(e) {
290 setSearch('');
291 $('list').focus();
295 * This returns the user visible path to the folder where the bookmark is
296 * located.
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 = tree.getBookmarkNodeById(parentId);
302 if (parentNode) {
303 var s = parentNode.title;
304 if (parentNode.parentId != bmm.ROOT_ID) {
305 return getFolder(parentNode.parentId) + '/' + s;
307 return s;
311 function handleLoadForTree(e) {
312 processHash();
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) {
322 var urls = [];
324 // Adds the node and all its direct children.
325 function addNodes(node) {
326 if (node.id == 'new')
327 return;
329 if (node.children) {
330 node.children.forEach(function(child) {
331 if (!bmm.isFolder(child))
332 urls.push(child.url);
334 } else {
335 urls.push(node.url);
339 // Get a future promise for the nodes.
340 var promises = nodes.map(function(node) {
341 if (bmm.isFolder(node))
342 return bmm.loadSubtree(node.id);
343 // Not a folder so we already have all the data we need.
344 return Promise.resolve(node);
347 return Promise.all(promises).then(function(nodes) {
348 nodes.forEach(addNodes);
349 return urls;
354 * Returns the nodes (non recursive) to use for the open commands.
355 * @param {HTMLElement} target .
356 * @return {Array.<BookmarkTreeNode>} .
358 function getNodesForOpen(target) {
359 if (target == tree) {
360 if (tree.selectedItem != searchTreeItem)
361 return tree.selectedFolders;
362 // Fall through to use all nodes in the list.
363 } else {
364 var items = list.selectedItems;
365 if (items.length)
366 return items;
369 // The list starts off with a null dataModel. We can get here during startup.
370 if (!list.dataModel)
371 return [];
373 // Return an array based on the dataModel.
374 return list.dataModel.slice();
378 * Returns a promise that will contain all URLs of all the selected bookmarks
379 * and the nested bookmarks for use with the open commands.
380 * @param {HTMLElement} target The target list or tree.
381 * @return {Promise.<Array.<string>>} .
383 function getUrlsForOpenCommands(target) {
384 return getAllUrls(getNodesForOpen(target));
387 function notNewNode(node) {
388 return node.id != 'new';
392 * Helper function that updates the canExecute and labels for the open-like
393 * commands.
394 * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
395 * @param {!cr.ui.Command} command The command we are currently processing.
396 * @param {string} singularId The string id of singular form of the menu label.
397 * @param {string} pluralId The string id of menu label if the singular form is
398 not used.
399 * @param {boolean} commandDisabled Whether the menu item should be disabled
400 no matter what bookmarks are selected.
402 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
403 if (singularId) {
404 // The command label reflects the selection which might not reflect
405 // how many bookmarks will be opened. For example if you right click an
406 // empty area in a folder with 1 bookmark the text should still say "all".
407 var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
408 var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
409 command.label = loadTimeData.getString(singular ? singularId : pluralId);
412 if (commandDisabled) {
413 command.disabled = true;
414 e.canExecute = false;
415 return;
418 getUrlsForOpenCommands(e.target).then(function(urls) {
419 var disabled = !urls.length;
420 command.disabled = disabled;
421 e.canExecute = !disabled;
426 * Calls the backend to figure out if we can paste the clipboard into the active
427 * folder.
428 * @param {Function=} opt_f Function to call after the state has been updated.
430 function updatePasteCommand(opt_f) {
431 function update(canPaste) {
432 var organizeMenuCommand = $('paste-from-organize-menu-command');
433 var contextMenuCommand = $('paste-from-context-menu-command');
434 organizeMenuCommand.disabled = !canPaste;
435 contextMenuCommand.disabled = !canPaste;
436 if (opt_f)
437 opt_f();
439 // We cannot paste into search view.
440 if (list.isSearch())
441 update(false);
442 else
443 chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
446 function handleCanExecuteForDocument(e) {
447 var command = e.command;
448 switch (command.id) {
449 case 'import-menu-command':
450 e.canExecute = canEdit;
451 break;
452 case 'export-menu-command':
453 // We can always execute the export-menu command.
454 e.canExecute = true;
455 break;
456 case 'sort-command':
457 e.canExecute = !list.isSearch() &&
458 list.dataModel && list.dataModel.length > 1;
459 break;
460 case 'undo-command':
461 // The global undo command has no visible UI, so always enable it, and
462 // just make it a no-op if undo is not possible.
463 e.canExecute = true;
464 break;
465 default:
466 canExecuteForList(e);
467 break;
472 * Helper function for handling canExecute for the list and the tree.
473 * @param {!Event} e Can execute event object.
474 * @param {boolean} isSearch Whether the user is trying to do a command on
475 * search.
477 function canExecuteShared(e, isSearch) {
478 var command = e.command;
479 var commandId = command.id;
480 switch (commandId) {
481 case 'paste-from-organize-menu-command':
482 case 'paste-from-context-menu-command':
483 updatePasteCommand();
484 break;
486 case 'add-new-bookmark-command':
487 case 'new-folder-command':
488 e.canExecute = !isSearch && canEdit;
489 break;
491 case 'open-in-new-tab-command':
492 updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
493 break;
494 case 'open-in-background-tab-command':
495 updateOpenCommand(e, command, '', '', false);
496 break;
497 case 'open-in-new-window-command':
498 updateOpenCommand(e, command,
499 'open_in_new_window', 'open_all_new_window',
500 // Disabled when incognito is forced.
501 incognitoModeAvailability == 'forced' || !canOpenNewWindows);
502 break;
503 case 'open-incognito-window-command':
504 updateOpenCommand(e, command,
505 'open_incognito', 'open_all_incognito',
506 // Not available when incognito is disabled.
507 incognitoModeAvailability == 'disabled');
508 break;
510 case 'undo-delete-command':
511 e.canExecute = !!lastDeletedNodes;
512 break;
517 * Helper function for handling canExecute for the list and document.
518 * @param {!Event} e Can execute event object.
520 function canExecuteForList(e) {
521 var command = e.command;
522 var commandId = command.id;
524 function hasSelected() {
525 return !!list.selectedItem;
528 function hasSingleSelected() {
529 return list.selectedItems.length == 1;
532 function canCopyItem(item) {
533 return item.id != 'new';
536 function canCopyItems() {
537 var selectedItems = list.selectedItems;
538 return selectedItems && selectedItems.some(canCopyItem);
541 function isSearch() {
542 return list.isSearch();
545 switch (commandId) {
546 case 'rename-folder-command':
547 // Show rename if a single folder is selected.
548 var items = list.selectedItems;
549 if (items.length != 1) {
550 e.canExecute = false;
551 command.hidden = true;
552 } else {
553 var isFolder = bmm.isFolder(items[0]);
554 e.canExecute = isFolder && canEdit;
555 command.hidden = !isFolder;
557 break;
559 case 'edit-command':
560 // Show the edit command if not a folder.
561 var items = list.selectedItems;
562 if (items.length != 1) {
563 e.canExecute = false;
564 command.hidden = false;
565 } else {
566 var isFolder = bmm.isFolder(items[0]);
567 e.canExecute = !isFolder && canEdit;
568 command.hidden = isFolder;
570 break;
572 case 'show-in-folder-command':
573 e.canExecute = isSearch() && hasSingleSelected();
574 break;
576 case 'delete-command':
577 case 'cut-command':
578 e.canExecute = canCopyItems() && canEdit;
579 break;
581 case 'copy-command':
582 e.canExecute = canCopyItems();
583 break;
585 case 'open-in-same-window-command':
586 e.canExecute = hasSelected();
587 break;
589 default:
590 canExecuteShared(e, isSearch());
594 // Update canExecute for the commands when the list is the active element.
595 function handleCanExecuteForList(e) {
596 if (e.target != list) return;
597 canExecuteForList(e);
600 // Update canExecute for the commands when the tree is the active element.
601 function handleCanExecuteForTree(e) {
602 if (e.target != tree) return;
604 var command = e.command;
605 var commandId = command.id;
607 function hasSelected() {
608 return !!e.target.selectedItem;
611 function isSearch() {
612 var item = e.target.selectedItem;
613 return item == searchTreeItem;
616 function isTopLevelItem() {
617 return e.target.selectedItem.parentNode == tree;
620 switch (commandId) {
621 case 'rename-folder-command':
622 command.hidden = false;
623 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
624 break;
626 case 'edit-command':
627 command.hidden = true;
628 e.canExecute = false;
629 break;
631 case 'delete-command':
632 case 'cut-command':
633 e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
634 break;
636 case 'copy-command':
637 e.canExecute = hasSelected() && !isTopLevelItem();
638 break;
640 default:
641 canExecuteShared(e, isSearch());
646 * Update the canExecute state of all the commands.
648 function updateAllCommands() {
649 var commands = document.querySelectorAll('command');
650 for (var i = 0; i < commands.length; i++) {
651 commands[i].canExecuteChange();
655 function updateEditingCommands() {
656 var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
657 'add-new-bookmark', 'new-folder', 'sort',
658 'paste-from-context-menu', 'paste-from-organize-menu'];
660 chrome.bookmarkManagerPrivate.canEdit(function(result) {
661 if (result != canEdit) {
662 canEdit = result;
663 editingCommands.forEach(function(baseId) {
664 $(baseId + '-command').canExecuteChange();
670 function handleChangeForTree(e) {
671 navigateTo(tree.selectedItem.bookmarkId);
674 function handleOrganizeButtonClick(e) {
675 updateEditingCommands();
676 $('add-new-bookmark-command').canExecuteChange();
677 $('new-folder-command').canExecuteChange();
678 $('sort-command').canExecuteChange();
681 function handleRename(e) {
682 var item = e.target;
683 var bookmarkNode = item.bookmarkNode;
684 chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
685 performGlobalUndo = null; // This can't be undone, so disable global undo.
688 function handleEdit(e) {
689 var item = e.target;
690 var bookmarkNode = item.bookmarkNode;
691 var context = {
692 title: bookmarkNode.title
694 if (!bmm.isFolder(bookmarkNode))
695 context.url = bookmarkNode.url;
697 if (bookmarkNode.id == 'new') {
698 selectItemsAfterUserAction(list);
700 // New page
701 context.parentId = bookmarkNode.parentId;
702 chrome.bookmarks.create(context, function(node) {
703 // A new node was created and will get added to the list due to the
704 // handler.
705 var dataModel = list.dataModel;
706 var index = dataModel.indexOf(bookmarkNode);
707 dataModel.splice(index, 1);
709 // Select new item.
710 var newIndex = dataModel.findIndexById(node.id);
711 if (newIndex != -1) {
712 var sm = list.selectionModel;
713 list.scrollIndexIntoView(newIndex);
714 sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
717 } else {
718 // Edit
719 chrome.bookmarks.update(bookmarkNode.id, context);
721 performGlobalUndo = null; // This can't be undone, so disable global undo.
724 function handleCancelEdit(e) {
725 var item = e.target;
726 var bookmarkNode = item.bookmarkNode;
727 if (bookmarkNode.id == 'new') {
728 var dataModel = list.dataModel;
729 var index = dataModel.findIndexById('new');
730 dataModel.splice(index, 1);
735 * Navigates to the folder that the selected item is in and selects it. This is
736 * used for the show-in-folder command.
738 function showInFolder() {
739 var bookmarkNode = list.selectedItem;
740 if (!bookmarkNode)
741 return;
742 var parentId = bookmarkNode.parentId;
744 // After the list is loaded we should select the revealed item.
745 function selectItem() {
746 var index = list.dataModel.findIndexById(bookmarkNode.id);
747 if (index == -1)
748 return;
749 var sm = list.selectionModel;
750 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
751 list.scrollIndexIntoView(index);
754 var treeItem = bmm.treeLookup[parentId];
755 treeItem.reveal();
757 navigateTo(parentId, selectItem);
761 * @return {!cr.LinkController} The link controller used to open links based on
762 * user clicks and keyboard actions.
764 function getLinkController() {
765 return linkController ||
766 (linkController = new cr.LinkController(loadTimeData));
770 * Returns the selected bookmark nodes of the provided tree or list.
771 * If |opt_target| is not provided or null the active element is used.
772 * Only call this if the list or the tree is focused.
773 * @param {BookmarkList|BookmarkTree} opt_target The target list or tree.
774 * @return {!Array} Array of bookmark nodes.
776 function getSelectedBookmarkNodes(opt_target) {
777 return (opt_target || document.activeElement) == tree ?
778 tree.selectedFolders : list.selectedItems;
782 * @return {!Array.<string>} An array of the selected bookmark IDs.
784 function getSelectedBookmarkIds() {
785 var selectedNodes = getSelectedBookmarkNodes();
786 selectedNodes.sort(function(a, b) { return a.index - b.index });
787 return selectedNodes.map(function(node) {
788 return node.id;
793 * Opens the selected bookmarks.
794 * @param {LinkKind} kind The kind of link we want to open.
795 * @param {HTMLElement} opt_eventTarget The target of the user initiated event.
797 function openBookmarks(kind, opt_eventTarget) {
798 // If we have selected any folders, we need to find all the bookmarks one
799 // level down. We use multiple async calls to getSubtree instead of getting
800 // the whole tree since we would like to minimize the amount of data sent.
802 var urlsP = getUrlsForOpenCommands(opt_eventTarget);
803 urlsP.then(function(urls) {
804 getLinkController().openUrls(urls, kind);
805 chrome.bookmarkManagerPrivate.recordLaunch();
810 * Opens an item in the list.
812 function openItem() {
813 var bookmarkNodes = getSelectedBookmarkNodes();
814 // If we double clicked or pressed enter on a single folder, navigate to it.
815 if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0]))
816 navigateTo(bookmarkNodes[0].id);
817 else
818 openBookmarks(LinkKind.FOREGROUND_TAB);
822 * Refreshes search results after delete or undo-delete.
823 * This ensures children of deleted folders do not remain in results
825 function updateSearchResults() {
826 if (list.isSearch()) {
827 list.reload();
832 * Deletes the selected bookmarks. The bookmarks are saved in memory in case
833 * the user needs to undo the deletion.
835 function deleteBookmarks() {
836 var selectedIds = getSelectedBookmarkIds();
837 var filteredIds = getFilteredSelectedBookmarkIds();
838 lastDeletedNodes = [];
840 function performDelete() {
841 // Only remove filtered ids.
842 chrome.bookmarkManagerPrivate.removeTrees(filteredIds);
843 $('undo-delete-command').canExecuteChange();
844 performGlobalUndo = undoDelete;
847 // First, store information about the bookmarks being deleted.
848 // Store all selected ids.
849 selectedIds.forEach(function(id) {
850 chrome.bookmarks.getSubTree(id, function(results) {
851 lastDeletedNodes.push(results);
853 // When all nodes have been saved, perform the deletion.
854 if (lastDeletedNodes.length === selectedIds.length) {
855 performDelete();
856 updateSearchResults();
863 * Restores a tree of bookmarks under a specified folder.
864 * @param {BookmarkTreeNode} node The node to restore.
865 * @param {=string} parentId The ID of the folder to restore under. If not
866 * specified, the original parentId of the node will be used.
868 function restoreTree(node, parentId) {
869 var bookmarkInfo = {
870 parentId: parentId || node.parentId,
871 title: node.title,
872 index: node.index,
873 url: node.url
876 chrome.bookmarks.create(bookmarkInfo, function(result) {
877 if (!result) {
878 console.error('Failed to restore bookmark.');
879 return;
882 if (node.children) {
883 // Restore the children using the new ID for this node.
884 node.children.forEach(function(child) {
885 restoreTree(child, result.id);
889 updateSearchResults();
894 * Restores the last set of bookmarks that was deleted.
896 function undoDelete() {
897 lastDeletedNodes.forEach(function(arr) {
898 arr.forEach(restoreTree);
900 lastDeletedNodes = null;
901 $('undo-delete-command').canExecuteChange();
903 // Only a single level of undo is supported, so disable global undo now.
904 performGlobalUndo = null;
908 * Computes folder for "Add Page" and "Add Folder".
909 * @return {string} The id of folder node where we'll create new page/folder.
911 function computeParentFolderForNewItem() {
912 if (document.activeElement == tree)
913 return list.parentId;
914 var selectedItem = list.selectedItem;
915 return selectedItem && bmm.isFolder(selectedItem) ?
916 selectedItem.id : list.parentId;
920 * Callback for rename folder and edit command. This starts editing for
921 * selected item.
923 function editSelectedItem() {
924 if (document.activeElement == tree) {
925 tree.selectedItem.editing = true;
926 } else {
927 var li = list.getListItem(list.selectedItem);
928 if (li)
929 li.editing = true;
934 * Callback for the new folder command. This creates a new folder and starts
935 * a rename of it.
937 function newFolder() {
938 performGlobalUndo = null; // This can't be undone, so disable global undo.
940 var parentId = computeParentFolderForNewItem();
942 // Callback is called after tree and list data model updated.
943 function createFolder(callback) {
944 chrome.bookmarks.create({
945 title: loadTimeData.getString('new_folder_name'),
946 parentId: parentId
947 }, callback);
950 if (document.activeElement == tree) {
951 createFolder(function(newNode) {
952 navigateTo(newNode.id, function() {
953 bmm.treeLookup[newNode.id].editing = true;
956 return;
959 function editNewFolderInList() {
960 createFolder(function() {
961 var index = list.dataModel.length - 1;
962 var sm = list.selectionModel;
963 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
964 scrollIntoViewAndMakeEditable(index);
968 navigateTo(parentId, editNewFolderInList);
972 * Scrolls the list item into view and makes it editable.
973 * @param {number} index The index of the item to make editable.
975 function scrollIntoViewAndMakeEditable(index) {
976 list.scrollIndexIntoView(index);
977 // onscroll is now dispatched asynchronously so we have to postpone
978 // the rest.
979 setTimeout(function() {
980 var item = list.getListItemByIndex(index);
981 if (item)
982 item.editing = true;
987 * Adds a page to the current folder. This is called by the
988 * add-new-bookmark-command handler.
990 function addPage() {
991 var parentId = computeParentFolderForNewItem();
993 function editNewBookmark() {
994 var fakeNode = {
995 title: '',
996 url: '',
997 parentId: parentId,
998 id: 'new'
1000 var dataModel = list.dataModel;
1001 var length = dataModel.length;
1002 dataModel.splice(length, 0, fakeNode);
1003 var sm = list.selectionModel;
1004 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
1005 scrollIntoViewAndMakeEditable(length);
1008 navigateTo(parentId, editNewBookmark);
1012 * This function is used to select items after a user action such as paste, drop
1013 * add page etc.
1014 * @param {BookmarkList|BookmarkTree} target The target of the user action.
1015 * @param {=string} opt_selectedTreeId If provided, then select that tree id.
1017 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1018 // We get one onCreated event per item so we delay the handling until we get
1019 // no more events coming.
1021 var ids = [];
1022 var timer;
1024 function handle(id, bookmarkNode) {
1025 clearTimeout(timer);
1026 if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
1027 ids.push(id);
1028 timer = setTimeout(handleTimeout, 50);
1031 function handleTimeout() {
1032 chrome.bookmarks.onCreated.removeListener(handle);
1033 chrome.bookmarks.onMoved.removeListener(handle);
1035 if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1036 var index = ids.indexOf(opt_selectedTreeId);
1037 if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1038 tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1040 } else if (target == list) {
1041 var dataModel = list.dataModel;
1042 var firstIndex = dataModel.findIndexById(ids[0]);
1043 var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1044 if (firstIndex != -1 && lastIndex != -1) {
1045 var selectionModel = list.selectionModel;
1046 selectionModel.selectedIndex = -1;
1047 selectionModel.selectRange(firstIndex, lastIndex);
1048 selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1049 list.focus();
1053 list.endBatchUpdates();
1056 list.startBatchUpdates();
1058 chrome.bookmarks.onCreated.addListener(handle);
1059 chrome.bookmarks.onMoved.addListener(handle);
1060 timer = setTimeout(handleTimeout, 300);
1064 * Record user action.
1065 * @param {string} name An user action name.
1067 function recordUserAction(name) {
1068 chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1072 * The currently selected bookmark, based on where the user is clicking.
1073 * @return {string} The ID of the currently selected bookmark (could be from
1074 * tree view or list view).
1076 function getSelectedId() {
1077 if (document.activeElement == tree)
1078 return tree.selectedItem.bookmarkId;
1079 var selectedItem = list.selectedItem;
1080 return selectedItem && bmm.isFolder(selectedItem) ?
1081 selectedItem.id : tree.selectedItem.bookmarkId;
1085 * Pastes the copied/cutted bookmark into the right location depending whether
1086 * if it was called from Organize Menu or from Context Menu.
1087 * @param {string} id The id of the element being pasted from.
1089 function pasteBookmark(id) {
1090 recordUserAction('Paste');
1091 selectItemsAfterUserAction(list);
1092 chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1096 * Returns true if child is contained in another selected folder.
1097 * Traces parent nodes up the tree until a selected ancestor or root is found.
1099 function hasSelectedAncestor(parentNode) {
1100 function contains(arr, item) {
1101 for (var i = 0; i < arr.length; i++)
1102 if (arr[i] === item)
1103 return true;
1104 return false;
1107 // Don't search top level, cannot select permanent nodes in search.
1108 if (parentNode == null || parentNode.id <= 2)
1109 return false;
1111 // Found selected ancestor.
1112 if (contains(getSelectedBookmarkNodes(), parentNode))
1113 return true;
1115 // Keep digging.
1116 return hasSelectedAncestor(tree.getBookmarkNodeById(parentNode.parentId));
1119 function getFilteredSelectedBookmarkIds() {
1120 // Remove duplicates from filteredIds and return.
1121 var filteredIds = new Array();
1122 // Selected nodes to iterate through for matches.
1123 var nodes = getSelectedBookmarkNodes();
1125 for (var i = 0; i < nodes.length; i++)
1126 if (!hasSelectedAncestor(tree.getBookmarkNodeById(nodes[i].parentId)))
1127 filteredIds.splice(0, 0, nodes[i].id);
1129 return filteredIds;
1133 * Handler for the command event. This is used for context menu of list/tree
1134 * and organized menu.
1135 * @param {!Event} e The event object.
1137 function handleCommand(e) {
1138 var command = e.command;
1139 var commandId = command.id;
1140 switch (commandId) {
1141 case 'import-menu-command':
1142 recordUserAction('Import');
1143 chrome.bookmarks.import();
1144 break;
1145 case 'export-menu-command':
1146 recordUserAction('Export');
1147 chrome.bookmarks.export();
1148 break;
1149 case 'undo-command':
1150 if (performGlobalUndo) {
1151 recordUserAction('UndoGlobal');
1152 performGlobalUndo();
1153 } else {
1154 recordUserAction('UndoNone');
1156 break;
1157 case 'show-in-folder-command':
1158 recordUserAction('ShowInFolder');
1159 showInFolder();
1160 break;
1161 case 'open-in-new-tab-command':
1162 case 'open-in-background-tab-command':
1163 recordUserAction('OpenInNewTab');
1164 openBookmarks(LinkKind.BACKGROUND_TAB, e.target);
1165 break;
1166 case 'open-in-new-window-command':
1167 recordUserAction('OpenInNewWindow');
1168 openBookmarks(LinkKind.WINDOW, e.target);
1169 break;
1170 case 'open-incognito-window-command':
1171 recordUserAction('OpenIncognito');
1172 openBookmarks(LinkKind.INCOGNITO, e.target);
1173 break;
1174 case 'delete-command':
1175 recordUserAction('Delete');
1176 deleteBookmarks();
1177 break;
1178 case 'copy-command':
1179 recordUserAction('Copy');
1180 chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
1181 updatePasteCommand);
1182 break;
1183 case 'cut-command':
1184 recordUserAction('Cut');
1185 chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
1186 function() {
1187 updatePasteCommand();
1188 updateSearchResults();
1190 break;
1191 case 'paste-from-organize-menu-command':
1192 pasteBookmark(list.parentId);
1193 break;
1194 case 'paste-from-context-menu-command':
1195 pasteBookmark(getSelectedId());
1196 break;
1197 case 'sort-command':
1198 recordUserAction('Sort');
1199 chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
1200 break;
1201 case 'rename-folder-command':
1202 editSelectedItem();
1203 break;
1204 case 'edit-command':
1205 recordUserAction('Edit');
1206 editSelectedItem();
1207 break;
1208 case 'new-folder-command':
1209 recordUserAction('NewFolder');
1210 newFolder();
1211 break;
1212 case 'add-new-bookmark-command':
1213 recordUserAction('AddPage');
1214 addPage();
1215 break;
1216 case 'open-in-same-window-command':
1217 recordUserAction('OpenInSame');
1218 openItem();
1219 break;
1220 case 'undo-delete-command':
1221 recordUserAction('UndoDelete');
1222 undoDelete();
1223 break;
1227 // Execute the copy, cut and paste commands when those events are dispatched by
1228 // the browser. This allows us to rely on the browser to handle the keyboard
1229 // shortcuts for these commands.
1230 function installEventHandlerForCommand(eventName, commandId) {
1231 function handle(e) {
1232 if (document.activeElement != list && document.activeElement != tree)
1233 return;
1234 var command = $(commandId);
1235 if (!command.disabled) {
1236 command.execute();
1237 if (e)
1238 e.preventDefault(); // Prevent the system beep.
1241 if (eventName == 'paste') {
1242 // Paste is a bit special since we need to do an async call to see if we
1243 // can paste because the paste command might not be up to date.
1244 document.addEventListener(eventName, function(e) {
1245 updatePasteCommand(handle);
1247 } else {
1248 document.addEventListener(eventName, handle);
1252 function initializeSplitter() {
1253 var splitter = document.querySelector('.main > .splitter');
1254 Splitter.decorate(splitter);
1256 // The splitter persists the size of the left component in the local store.
1257 if ('treeWidth' in localStorage)
1258 splitter.previousElementSibling.style.width = localStorage['treeWidth'];
1260 splitter.addEventListener('resize', function(e) {
1261 localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
1265 function initializeBookmarkManager() {
1266 // Sometimes the extension API is not initialized.
1267 if (!chrome.bookmarks)
1268 console.error('Bookmarks extension API is not available');
1270 chrome.bookmarkManagerPrivate.getStrings(continueInitializeBookmarkManager);
1273 function continueInitializeBookmarkManager(localizedStrings) {
1274 loadLocalizedStrings(localizedStrings);
1276 bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1278 cr.ui.decorate('menu', Menu);
1279 cr.ui.decorate('button[menu]', MenuButton);
1280 cr.ui.decorate('command', Command);
1281 BookmarkList.decorate(list);
1282 BookmarkTree.decorate(tree);
1284 list.addEventListener('canceledit', handleCancelEdit);
1285 list.addEventListener('canExecute', handleCanExecuteForList);
1286 list.addEventListener('change', updateAllCommands);
1287 list.addEventListener('contextmenu', updateEditingCommands);
1288 list.addEventListener('dblclick', handleDoubleClickForList);
1289 list.addEventListener('edit', handleEdit);
1290 list.addEventListener('rename', handleRename);
1291 list.addEventListener('urlClicked', handleUrlClickedForList);
1293 tree.addEventListener('canExecute', handleCanExecuteForTree);
1294 tree.addEventListener('change', handleChangeForTree);
1295 tree.addEventListener('contextmenu', updateEditingCommands);
1296 tree.addEventListener('rename', handleRename);
1297 tree.addEventListener('load', handleLoadForTree);
1299 cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1300 list.contextMenu = $('context-menu');
1301 tree.contextMenu = $('context-menu');
1303 // We listen to hashchange so that we can update the currently shown folder
1304 // when // the user goes back and forward in the history.
1305 window.addEventListener('hashchange', processHash);
1307 document.querySelector('.header form').onsubmit = function(e) {
1308 setSearch($('term').value);
1309 e.preventDefault();
1312 $('term').addEventListener('search', handleSearch);
1314 document.querySelector('.summary > button').addEventListener(
1315 'click', handleOrganizeButtonClick);
1317 document.querySelector('button.logo').addEventListener(
1318 'click', handleClickOnLogoButton);
1320 document.addEventListener('canExecute', handleCanExecuteForDocument);
1321 document.addEventListener('command', handleCommand);
1323 // Listen to copy, cut and paste events and execute the associated commands.
1324 installEventHandlerForCommand('copy', 'copy-command');
1325 installEventHandlerForCommand('cut', 'cut-command');
1326 installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1328 // Install shortcuts
1329 for (var name in commandShortcutMap) {
1330 $(name + '-command').shortcut = commandShortcutMap[name];
1333 // Disable almost all commands at startup.
1334 var commands = document.querySelectorAll('command');
1335 for (var i = 0, command; command = commands[i]; ++i) {
1336 if (command.id != 'import-menu-command' &&
1337 command.id != 'export-menu-command') {
1338 command.disabled = true;
1342 chrome.bookmarkManagerPrivate.canEdit(function(result) {
1343 canEdit = result;
1346 chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1347 // TODO(rustema): propagate policy value to the bookmark manager when it
1348 // changes.
1349 incognitoModeAvailability = result;
1352 chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1353 canOpenNewWindows = result;
1356 cr.ui.FocusOutlineManager.forDocument(document);
1357 initializeSplitter();
1358 bmm.addBookmarkModelListeners();
1359 dnd.init(selectItemsAfterUserAction);
1360 tree.reload();
1363 initializeBookmarkManager();
1364 })();