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