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 Splitter
= cr
.ui
.Splitter
;
15 /** @const */ var TreeItem
= cr
.ui
.TreeItem
;
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>}
26 * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
27 * view. Zero means pointer doesn't hover on folder.
30 var lastHoverOnFolderTimeStamp
= 0;
33 * Holds a function that will undo that last action, if global undo is enabled.
36 var performGlobalUndo
;
39 * Holds a link controller singleton. Use getLinkController() rarther than
40 * accessing this variabie.
41 * @type {LinkController}
46 * New Windows are not allowed in Windows 8 metro mode.
48 var canOpenNewWindows
= true;
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';
59 * Whether bookmarks can be modified.
68 var searchTreeItem
= new TreeItem({
73 * Command shortcut mapping.
76 var commandShortcutMap
= cr
.isMac
? {
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',
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',
100 * Mapping for folder id to suffix of UMA. These names will be appeared
101 * after "BookmarkManager_NavigateTo_" in UMA dashboard.
104 var folderMetricsNameMap
= {
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
) {
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
;
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
;
159 var metricsId
= folderMetricsNameMap
[id
.replace(/^q=.*/, 'q=')] ||
160 folderMetricsNameMap
['subfolder'];
161 chrome
.metricsPrivate
.recordUserAction(
162 'BookmarkManager_NavigateTo_' + metricsId
);
165 if (list
.parentId
== id
)
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.
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
;
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
) {
295 * This returns the user visible path to the folder where the bookmark is
297 * @param {number} parentId The ID of the parent folder.
298 * @return {string} The path to the the bookmark,
300 function getFolder(parentId
) {
301 var parentNode
= tree
.getBookmarkNodeById(parentId
);
303 var s
= parentNode
.title
;
304 if (parentNode
.parentId
!= bmm
.ROOT_ID
) {
305 return getFolder(parentNode
.parentId
) + '/' + s
;
311 function handleLoadForTree(e
) {
316 * Returns a promise for all the URLs in the {@code nodes} and the direct
317 * children of {@code nodes}.
318 * @param {!Array.<BookmarkTreeNode>} nodes .
319 * @return {!Promise.<Array.<string>>} .
321 function getAllUrls(nodes
) {
324 // Adds the node and all its direct children.
325 function addNodes(node
) {
326 if (node
.id
== 'new')
330 node
.children
.forEach(function(child
) {
331 if (!bmm
.isFolder(child
))
332 urls
.push(child
.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
);
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.
364 var items
= list
.selectedItems
;
369 // The list starts off with a null dataModel. We can get here during startup.
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
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
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
) {
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;
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
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
;
439 // We cannot paste into search view.
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
;
452 case 'export-menu-command':
453 // We can always execute the export-menu command.
457 e
.canExecute
= !list
.isSearch() &&
458 list
.dataModel
&& list
.dataModel
.length
> 1;
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.
466 canExecuteForList(e
);
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
477 function canExecuteShared(e
, isSearch
) {
478 var command
= e
.command
;
479 var commandId
= command
.id
;
481 case 'paste-from-organize-menu-command':
482 case 'paste-from-context-menu-command':
483 updatePasteCommand();
486 case 'add-new-bookmark-command':
487 case 'new-folder-command':
488 e
.canExecute
= !isSearch
&& canEdit
;
491 case 'open-in-new-tab-command':
492 updateOpenCommand(e
, command
, 'open_in_new_tab', 'open_all', false);
494 case 'open-in-background-tab-command':
495 updateOpenCommand(e
, command
, '', '', false);
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
);
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');
510 case 'undo-delete-command':
511 e
.canExecute
= !!lastDeletedNodes
;
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();
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;
553 var isFolder
= bmm
.isFolder(items
[0]);
554 e
.canExecute
= isFolder
&& canEdit
;
555 command
.hidden
= !isFolder
;
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;
566 var isFolder
= bmm
.isFolder(items
[0]);
567 e
.canExecute
= !isFolder
&& canEdit
;
568 command
.hidden
= isFolder
;
572 case 'show-in-folder-command':
573 e
.canExecute
= isSearch() && hasSingleSelected();
576 case 'delete-command':
578 e
.canExecute
= canCopyItems() && canEdit
;
582 e
.canExecute
= canCopyItems();
585 case 'open-in-same-window-command':
586 e
.canExecute
= hasSelected();
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
;
621 case 'rename-folder-command':
622 command
.hidden
= false;
623 e
.canExecute
= hasSelected() && !isTopLevelItem() && canEdit
;
627 command
.hidden
= true;
628 e
.canExecute
= false;
631 case 'delete-command':
633 e
.canExecute
= hasSelected() && !isTopLevelItem() && canEdit
;
637 e
.canExecute
= hasSelected() && !isTopLevelItem();
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
) {
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
) {
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
) {
690 var bookmarkNode
= item
.bookmarkNode
;
692 title
: bookmarkNode
.title
694 if (!bmm
.isFolder(bookmarkNode
))
695 context
.url
= bookmarkNode
.url
;
697 if (bookmarkNode
.id
== 'new') {
698 selectItemsAfterUserAction(list
);
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
705 var dataModel
= list
.dataModel
;
706 var index
= dataModel
.indexOf(bookmarkNode
);
707 dataModel
.splice(index
, 1);
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
;
719 chrome
.bookmarks
.update(bookmarkNode
.id
, context
);
721 performGlobalUndo
= null; // This can't be undone, so disable global undo.
724 function handleCancelEdit(e
) {
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
;
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
);
749 var sm
= list
.selectionModel
;
750 sm
.anchorIndex
= sm
.leadIndex
= sm
.selectedIndex
= index
;
751 list
.scrollIndexIntoView(index
);
754 var treeItem
= bmm
.treeLookup
[parentId
];
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
) {
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
);
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()) {
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
) {
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
) {
870 parentId
: parentId
|| node
.parentId
,
876 chrome
.bookmarks
.create(bookmarkInfo
, function(result
) {
878 console
.error('Failed to restore bookmark.');
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
923 function editSelectedItem() {
924 if (document
.activeElement
== tree
) {
925 tree
.selectedItem
.editing
= true;
927 var li
= list
.getListItem(list
.selectedItem
);
934 * Callback for the new folder command. This creates a new folder and starts
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'),
950 if (document
.activeElement
== tree
) {
951 createFolder(function(newNode
) {
952 navigateTo(newNode
.id
, function() {
953 bmm
.treeLookup
[newNode
.id
].editing
= true;
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
979 setTimeout(function() {
980 var item
= list
.getListItemByIndex(index
);
987 * Adds a page to the current folder. This is called by the
988 * add-new-bookmark-command handler.
991 var parentId
= computeParentFolderForNewItem();
993 function editNewBookmark() {
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
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.
1024 function handle(id
, bookmarkNode
) {
1025 clearTimeout(timer
);
1026 if (opt_selectedTreeId
|| list
.parentId
== bookmarkNode
.parentId
)
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
;
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
)
1107 // Don't search top level, cannot select permanent nodes in search.
1108 if (parentNode
== null || parentNode
.id
<= 2)
1111 // Found selected ancestor.
1112 if (contains(getSelectedBookmarkNodes(), parentNode
))
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
);
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();
1145 case 'export-menu-command':
1146 recordUserAction('Export');
1147 chrome
.bookmarks
.export();
1149 case 'undo-command':
1150 if (performGlobalUndo
) {
1151 recordUserAction('UndoGlobal');
1152 performGlobalUndo();
1154 recordUserAction('UndoNone');
1157 case 'show-in-folder-command':
1158 recordUserAction('ShowInFolder');
1161 case 'open-in-new-tab-command':
1162 case 'open-in-background-tab-command':
1163 recordUserAction('OpenInNewTab');
1164 openBookmarks(LinkKind
.BACKGROUND_TAB
, e
.target
);
1166 case 'open-in-new-window-command':
1167 recordUserAction('OpenInNewWindow');
1168 openBookmarks(LinkKind
.WINDOW
, e
.target
);
1170 case 'open-incognito-window-command':
1171 recordUserAction('OpenIncognito');
1172 openBookmarks(LinkKind
.INCOGNITO
, e
.target
);
1174 case 'delete-command':
1175 recordUserAction('Delete');
1178 case 'copy-command':
1179 recordUserAction('Copy');
1180 chrome
.bookmarkManagerPrivate
.copy(getSelectedBookmarkIds(),
1181 updatePasteCommand
);
1184 recordUserAction('Cut');
1185 chrome
.bookmarkManagerPrivate
.cut(getSelectedBookmarkIds(),
1187 updatePasteCommand();
1188 updateSearchResults();
1191 case 'paste-from-organize-menu-command':
1192 pasteBookmark(list
.parentId
);
1194 case 'paste-from-context-menu-command':
1195 pasteBookmark(getSelectedId());
1197 case 'sort-command':
1198 recordUserAction('Sort');
1199 chrome
.bookmarkManagerPrivate
.sortChildren(list
.parentId
);
1201 case 'rename-folder-command':
1204 case 'edit-command':
1205 recordUserAction('Edit');
1208 case 'new-folder-command':
1209 recordUserAction('NewFolder');
1212 case 'add-new-bookmark-command':
1213 recordUserAction('AddPage');
1216 case 'open-in-same-window-command':
1217 recordUserAction('OpenInSame');
1220 case 'undo-delete-command':
1221 recordUserAction('UndoDelete');
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
)
1234 var command
= $(commandId
);
1235 if (!command
.disabled
) {
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
);
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
);
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
) {
1346 chrome
.systemPrivate
.getIncognitoModeAvailability(function(result
) {
1347 // TODO(rustema): propagate policy value to the bookmark manager when it
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
);
1363 initializeBookmarkManager();