Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / bookmark_manager / js / main.js
blobefbac59bd3aa75e3867bbfc290ad7d0e696df657
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 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;
16 /**
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}}
20  */
21 var lastDeleted;
23 /**
24  *
25  * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
26  * view. Zero means pointer doesn't hover on folder.
27  * @type {number}
28  */
29 var lastHoverOnFolderTimeStamp = 0;
31 /**
32  * Holds a function that will undo that last action, if global undo is enabled.
33  * @type {Function}
34  */
35 var performGlobalUndo;
37 /**
38  * Holds a link controller singleton. Use getLinkController() rarther than
39  * accessing this variabie.
40  * @type {cr.LinkController}
41  */
42 var linkController;
44 /**
45  * New Windows are not allowed in Windows 8 metro mode.
46  */
47 var canOpenNewWindows = true;
49 /**
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).
54  */
55 var incognitoModeAvailability = 'enabled';
57 /**
58  * Whether bookmarks can be modified.
59  * @type {boolean}
60  */
61 var canEdit = true;
63 /**
64  * @type {TreeItem}
65  * @const
66  */
67 var searchTreeItem = new TreeItem({
68   bookmarkId: 'q='
69 });
71 /**
72  * Command shortcut mapping.
73  * @const
74  */
75 var commandShortcutMap = cr.isMac ? {
76   'edit': 'Enter',
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',
86 } : {
87   'edit': 'F2',
88   'delete': 'U+007F',
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',
98 /**
99  * Mapping for folder id to suffix of UMA. These names will be appeared
100  * after "BookmarkManager_NavigateTo_" in UMA dashboard.
101  * @const
102  */
103 var folderMetricsNameMap = {
104   '1': 'BookmarkBar',
105   '2': 'Other',
106   '3': 'Mobile',
107   'q=': 'Search',
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.
116  */
117 function addOneShotEventListener(node, name, handler) {
118   var f = function(e) {
119     handler(e);
120     node.removeEventListener(name, f);
121   };
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(/&/, '');
130   }
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.
142  */
143 function updateHash() {
144   window.location.hash = bmm.tree.selectedItem.bookmarkId;
145   updateAllCommands();
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.
153  */
154 function navigateTo(id, opt_callback) {
155   window.location.hash = id;
156   updateAllCommands();
158   var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
159                   folderMetricsNameMap['subfolder'];
160   chrome.metricsPrivate.recordUserAction(
161       'BookmarkManager_NavigateTo_' + metricsId);
163   if (opt_callback) {
164     if (bmm.list.parentId == id)
165       opt_callback();
166     else
167       addOneShotEventListener(bmm.list, 'load', opt_callback);
168   }
172  * Updates the parent ID of the bookmark list and selects the correct tree item.
173  * @param {string} id The id.
174  */
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
185 // is first loaded.
186 function processHash() {
187   var id = window.location.hash.slice(1);
188   if (!id) {
189     // If we do not have a hash, select first item in the tree.
190     id = bmm.tree.items[0].bookmarkId;
191   }
193   var valid = false;
194   if (/^e=/.test(id)) {
195     id = id.slice(2);
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)
201         return;
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);
207         if (index != -1) {
208           var sm = bmm.list.selectionModel;
209           sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
210           scrollIntoViewAndMakeEditable(index);
211         }
212       };
214       var parentId = assert(bookmarkNode.parentId);
215       navigateTo(parentId, editBookmark);
216     });
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;
226   }
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);
236     });
237   }
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.
260  */
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;
268   }
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     bmm.tree.add(searchTreeItem);
277     bmm.tree.selectedItem = searchTreeItem;
278   } else {
279     // Go "home".
280     bmm.tree.selectedItem = bmm.tree.items[0];
281     id = bmm.tree.selectedItem.bookmarkId;
282   }
284   navigateTo(id);
288  * This returns the user visible path to the folder where the bookmark is
289  * located.
290  * @param {number} parentId The ID of the parent folder.
291  * @return {string} The path to the the bookmark,
292  */
293 function getFolder(parentId) {
294   var parentNode = bmm.tree.getBookmarkNodeById(parentId);
295   if (parentNode) {
296     var s = parentNode.title;
297     if (parentNode.parentId != bmm.ROOT_ID) {
298       return getFolder(parentNode.parentId) + '/' + s;
299     }
300     return s;
301   }
304 function handleLoadForTree(e) {
305   processHash();
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>>} .
313  */
314 function getAllUrls(nodes) {
315   var urls = [];
317   // Adds the node and all its direct children.
318   function addNodes(node) {
319     if (node.id == 'new')
320       return;
322     if (node.children) {
323       node.children.forEach(function(child) {
324         if (!bmm.isFolder(child))
325           urls.push(child.url);
326       });
327     } else {
328       urls.push(node.url);
329     }
330   }
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);
338   });
340   return Promise.all(promises).then(function(nodes) {
341     nodes.forEach(addNodes);
342     return urls;
343   });
347  * Returns the nodes (non recursive) to use for the open commands.
348  * @param {HTMLElement} target
349  * @return {!Array<BookmarkTreeNode>}
350  */
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.
356   } else {
357     var items = bmm.list.selectedItems;
358     if (items.length)
359       return items;
360   }
362   // The list starts off with a null dataModel. We can get here during startup.
363   if (!bmm.list.dataModel)
364     return [];
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>>} .
375  */
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
386  * commands.
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
391        not used.
392  * @param {boolean} commandDisabled Whether the menu item should be disabled
393        no matter what bookmarks are selected.
394  */
395 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
396   if (singularId) {
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);
403   }
405   if (commandDisabled) {
406     command.disabled = true;
407     e.canExecute = false;
408     return;
409   }
411   getUrlsForOpenCommands(assertInstanceof(e.target, HTMLElement)).then(
412       function(urls) {
413     var disabled = !urls.length;
414     command.disabled = disabled;
415     e.canExecute = !disabled;
416   });
420  * Calls the backend to figure out if we can paste the clipboard into the active
421  * folder.
422  * @param {Function=} opt_f Function to call after the state has been updated.
423  */
424 function updatePasteCommand(opt_f) {
425   function update(commandId, canPaste) {
426     $(commandId).disabled = !canPaste;
427   }
429   var promises = [];
431   // The folders menu.
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);
437         resolve(canPaste);
438       });
439     }));
440   } else {
441     // Tree's not loaded yet.
442     update('paste-from-folders-menu-command', false);
443   }
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);
450   } else {
451     promises.push(new Promise(function(resolve) {
452       chrome.bookmarkManagerPrivate.canPaste(listId, function(canPaste) {
453         update('paste-from-organize-menu-command', canPaste);
454         resolve(canPaste);
455       });
456     }));
457   }
459   Promise.all(promises).then(function() {
460     var cmd;
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';
466     if (cmd)
467       update('paste-from-context-menu-command', !$(cmd).disabled);
469     if (opt_f) opt_f();
470   });
473 function handleCanExecuteForDocument(e) {
474   var command = e.command;
475   switch (command.id) {
476     case 'import-menu-command':
477       e.canExecute = canEdit;
478       break;
480     case 'export-menu-command':
481       // We can always execute the export-menu command.
482       e.canExecute = true;
483       break;
485     case 'sort-command':
486       e.canExecute = !bmm.list.isSearch() &&
487           bmm.list.dataModel && bmm.list.dataModel.length > 1 &&
488           !isUnmodifiable(bmm.tree.getBookmarkNodeById(bmm.list.parentId));
489       break;
491     case 'undo-command':
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');
497       break;
499     default:
500       canExecuteForList(e);
501       if (!e.defaultPrevented)
502         canExecuteForTree(e);
503       break;
504   }
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
511  *     search.
512  */
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();
520       break;
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;
529       break;
531     case 'open-in-new-tab-command':
532       updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
533       break;
535     case 'open-in-background-tab-command':
536       updateOpenCommand(e, command, '', '', false);
537       break;
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);
544       break;
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');
551       break;
553     case 'undo-delete-command':
554       e.canExecute = !!lastDeleted;
555       break;
556   }
560  * Helper function for handling canExecute for the list and document.
561  * @param {!cr.ui.CanExecuteEvent} e Can execute event object.
562  */
563 function canExecuteForList(e) {
564   function hasSelected() {
565     return !!bmm.list.selectedItem;
566   }
568   function hasSingleSelected() {
569     return bmm.list.selectedItems.length == 1;
570   }
572   function canCopyItem(item) {
573     return item.id != 'new';
574   }
576   function canCopyItems() {
577     var selectedItems = bmm.list.selectedItems;
578     return selectedItems && selectedItems.some(canCopyItem);
579   }
581   function isSearch() {
582     return bmm.list.isSearch();
583   }
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;
593       } else {
594         var isFolder = bmm.isFolder(items[0]);
595         e.canExecute = isFolder && canEdit && !hasUnmodifiable(items);
596         command.hidden = !isFolder;
597       }
598       break;
600     case 'edit-command':
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;
606       } else {
607         var isFolder = bmm.isFolder(items[0]);
608         e.canExecute = !isFolder && canEdit && !hasUnmodifiable(items);
609         command.hidden = isFolder;
610       }
611       break;
613     case 'show-in-folder-command':
614       e.canExecute = isSearch() && hasSingleSelected();
615       break;
617     case 'delete-command':
618     case 'cut-command':
619       e.canExecute = canCopyItems() && canEdit &&
620           !hasUnmodifiable(bmm.list.selectedItems);
621       break;
623     case 'copy-command':
624       e.canExecute = canCopyItems();
625       break;
627     case 'open-in-same-window-command':
628       e.canExecute = hasSelected();
629       break;
631     default:
632       canExecuteShared(e, isSearch());
633   }
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;
651   }
653   function isSearch() {
654     return bmm.tree.selectedItem == searchTreeItem;
655   }
657   function isTopLevelItem() {
658     return bmm.tree.selectedItem &&
659            bmm.tree.selectedItem.parentNode == bmm.tree;
660   }
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);
669       break;
671     case 'edit-command':
672       command.hidden = true;
673       e.canExecute = false;
674       break;
676     case 'delete-command':
677     case 'delete-from-folders-menu-command':
678     case 'cut-command':
679     case 'cut-from-folders-menu-command':
680       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
681           !hasUnmodifiable(bmm.tree.selectedFolders);
682       break;
684     case 'copy-command':
685     case 'copy-from-folders-menu-command':
686       e.canExecute = hasSelected() && !isTopLevelItem();
687       break;
689     case 'undo-delete-from-folders-menu-command':
690       e.canExecute = lastDeleted && lastDeleted.target == bmm.tree;
691       break;
693     default:
694       canExecuteShared(e, isSearch());
695   }
699  * Update the canExecute state of all the commands.
700  */
701 function updateAllCommands() {
702   var commands = document.querySelectorAll('command');
703   for (var i = 0; i < commands.length; i++) {
704     commands[i].canExecuteChange();
705   }
708 function updateEditingCommands() {
709   var editingCommands = [
710     'add-new-bookmark',
711     'cut',
712     'cut-from-folders-menu',
713     'delete',
714     'edit',
715     'new-folder',
716     'paste-from-context-menu',
717     'paste-from-folders-menu',
718     'paste-from-organize-menu',
719     'rename-folder',
720     'sort',
721   ];
723   chrome.bookmarkManagerPrivate.canEdit(function(result) {
724     if (result != canEdit) {
725       canEdit = result;
726       editingCommands.forEach(function(baseId) {
727         $(baseId + '-command').canExecuteChange();
728       });
729     }
730   });
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();
743   } else {
744     $('copy-command').canExecuteChange();
745   }
748 function handleRename(e) {
749   var item = e.target;
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) {
756   var item = e.target;
757   var bookmarkNode = item.bookmarkNode;
758   var context = {
759     title: bookmarkNode.title
760   };
761   if (!bmm.isFolder(bookmarkNode))
762     context.url = bookmarkNode.url;
764   if (bookmarkNode.id == 'new') {
765     selectItemsAfterUserAction(/** @type {BookmarkList} */(bmm.list));
767     // New page
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
771       // handler.
772       var dataModel = bmm.list.dataModel;
773       var index = dataModel.indexOf(bookmarkNode);
774       dataModel.splice(index, 1);
776       // Select new item.
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;
782       }
783     });
784   } else {
785     // Edit
786     chrome.bookmarks.update(bookmarkNode.id, context);
787   }
788   performGlobalUndo = null;  // This can't be undone, so disable global undo.
791 function handleCancelEdit(e) {
792   var item = e.target;
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);
798   }
802  * Navigates to the folder that the selected item is in and selects it. This is
803  * used for the show-in-folder command.
804  */
805 function showInFolder() {
806   var bookmarkNode = bmm.list.selectedItem;
807   if (!bookmarkNode)
808     return;
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);
814     if (index == -1)
815       return;
816     var sm = bmm.list.selectionModel;
817     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
818     bmm.list.scrollIndexIntoView(index);
819   }
821   var treeItem = bmm.treeLookup[parentId];
822   treeItem.reveal();
824   navigateTo(parentId, selectItem);
828  * @return {!cr.LinkController} The link controller used to open links based on
829  *     user clicks and keyboard actions.
830  */
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.
842  */
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.
851  */
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) {
856     return node.id;
857   });
861  * @param {BookmarkTreeNode} node The node to test.
862  * @return {boolean} Whether the given node is unmodifiable.
863  */
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.
871  */
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.
880  */
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();
890   });
894  * Opens an item in the list.
895  */
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);
901   else
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
908  */
909 function updateSearchResults() {
910   if (bmm.list.isSearch())
911     bmm.list.reload();
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.
918  */
919 function deleteBookmarks(opt_target) {
920   var selectedIds = getSelectedBookmarkIds(opt_target);
921   if (!selectedIds.length)
922     return;
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;
933   }
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) {
943         performDelete();
944         updateSearchResults();
945       }
946     });
947   });
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.
956  */
957 function restoreTree(node, opt_parentId) {
958   var bookmarkInfo = {
959     parentId: typeof opt_parentId == 'string' ? opt_parentId : node.parentId,
960     title: node.title,
961     index: node.index,
962     url: node.url
963   };
965   chrome.bookmarks.create(bookmarkInfo, function(result) {
966     if (!result) {
967       console.error('Failed to restore bookmark.');
968       return;
969     }
971     if (node.children) {
972       // Restore the children using the new ID for this node.
973       node.children.forEach(function(child) {
974         restoreTree(child, result.id);
975       });
976     }
978     updateSearchResults();
979   });
983  * Restores the last set of bookmarks that was deleted.
984  */
985 function undoDelete() {
986   lastDeleted.nodes.forEach(function(arr) {
987     arr.forEach(restoreTree);
988   });
989   lastDeleted = null;
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.
1000  */
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
1011  * selected item.
1012  */
1013 function editSelectedItem() {
1014   if (document.activeElement == bmm.tree) {
1015     bmm.tree.selectedItem.editing = true;
1016   } else {
1017     var li = bmm.list.getListItem(bmm.list.selectedItem);
1018     if (li)
1019       li.editing = true;
1020   }
1024  * Callback for the new folder command. This creates a new folder and starts
1025  * a rename of it.
1026  * @param {EventTarget=} opt_target The target to create a new folder in.
1027  */
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;
1033   var newIndex;
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;
1039     }
1040     chrome.bookmarks.create({
1041       title: loadTimeData.getString('new_folder_name'),
1042       parentId: parentId,
1043       index: newIndex
1044     }, callback);
1045   }
1047   if ((opt_target || document.activeElement) == bmm.tree) {
1048     createFolder(function(newNode) {
1049       navigateTo(newNode.id, function() {
1050         bmm.treeLookup[newNode.id].editing = true;
1051       });
1052     });
1053     return;
1054   }
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);
1062     });
1063   }
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.
1071  */
1072 function scrollIntoViewAndMakeEditable(index) {
1073   bmm.list.scrollIndexIntoView(index);
1074   // onscroll is now dispatched asynchronously so we have to postpone
1075   // the rest.
1076   setTimeout(function() {
1077     var item = bmm.list.getListItemByIndex(index);
1078     if (item)
1079       item.editing = true;
1080   }, 0);
1084  * Adds a page to the current folder. This is called by the
1085  * add-new-bookmark-command handler.
1086  */
1087 function addPage() {
1088   var parentId = computeParentFolderForNewItem();
1089   var selectedItem = bmm.list.selectedItem;
1090   var newIndex;
1091   function editNewBookmark() {
1092     if (selectedItem && document.activeElement != bmm.tree &&
1093         !bmm.isFolder(selectedItem)) {
1094       newIndex = bmm.list.dataModel.indexOf(selectedItem) + 1;
1095     }
1097     var fakeNode = {
1098       title: '',
1099       url: '',
1100       parentId: parentId,
1101       index: newIndex,
1102       id: 'new'
1103     };
1104     var dataModel = bmm.list.dataModel;
1105     var index = dataModel.length;
1106     if (newIndex != undefined)
1107       index = newIndex;
1108     dataModel.splice(index, 0, fakeNode);
1109     var sm = bmm.list.selectionModel;
1110     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
1111     scrollIntoViewAndMakeEditable(index);
1112   };
1114   navigateTo(parentId, editNewBookmark);
1118  * This function is used to select items after a user action such as paste, drop
1119  * add page etc.
1120  * @param {BookmarkList|BookmarkTree} target The target of the user action.
1121  * @param {string=} opt_selectedTreeId If provided, then select that tree id.
1122  */
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.
1127   var ids = [];
1128   var timer;
1130   function handle(id, bookmarkNode) {
1131     clearTimeout(timer);
1132     if (opt_selectedTreeId || bmm.list.parentId == bookmarkNode.parentId)
1133       ids.push(id);
1134     timer = setTimeout(handleTimeout, 50);
1135   }
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];
1145       }
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;
1155         bmm.list.focus();
1156       }
1157     }
1159     bmm.list.endBatchUpdates();
1160   }
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.
1172  */
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).
1181  */
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.
1194  */
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.
1204  */
1205 function hasSelectedAncestor(parentNode) {
1206   function contains(arr, item) {
1207     for (var i = 0; i < arr.length; i++)
1208         if (arr[i] === item)
1209           return true;
1210     return false;
1211   }
1213   // Don't search top level, cannot select permanent nodes in search.
1214   if (parentNode == null || parentNode.id <= 2)
1215     return false;
1217   // Found selected ancestor.
1218   if (contains(getSelectedBookmarkNodes(), parentNode))
1219     return true;
1221   // Keep digging.
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.
1229  */
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);
1240   return filteredIds;
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.
1247  */
1248 function handleCommand(e) {
1249   var command = e.command;
1250   var target;
1251   switch (command.id) {
1252     case 'import-menu-command':
1253       recordUserAction('Import');
1254       chrome.bookmarks.import();
1255       break;
1257     case 'export-menu-command':
1258       recordUserAction('Export');
1259       chrome.bookmarks.export();
1260       break;
1262     case 'undo-command':
1263       if (performGlobalUndo) {
1264         recordUserAction('UndoGlobal');
1265         performGlobalUndo();
1266       } else {
1267         recordUserAction('UndoNone');
1268       }
1269       break;
1271     case 'show-in-folder-command':
1272       recordUserAction('ShowInFolder');
1273       showInFolder();
1274       break;
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));
1281       break;
1283     case 'open-in-new-window-command':
1284       recordUserAction('OpenInNewWindow');
1285       openBookmarks(LinkKind.WINDOW,
1286           assertInstanceof(e.target, HTMLElement));
1287       break;
1289     case 'open-incognito-window-command':
1290       recordUserAction('OpenIncognito');
1291       openBookmarks(LinkKind.INCOGNITO,
1292           assertInstanceof(e.target, HTMLElement));
1293       break;
1295     case 'delete-from-folders-menu-command':
1296       target = bmm.tree;
1297     case 'delete-command':
1298       recordUserAction('Delete');
1299       deleteBookmarks(target);
1300       break;
1302     case 'copy-from-folders-menu-command':
1303       target = bmm.tree;
1304     case 'copy-command':
1305       recordUserAction('Copy');
1306       chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(target),
1307                                          updatePasteCommand);
1308       break;
1310     case 'cut-from-folders-menu-command':
1311       target = bmm.tree;
1312     case 'cut-command':
1313       recordUserAction('Cut');
1314       chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(target),
1315                                         function() {
1316                                           updatePasteCommand();
1317                                           updateSearchResults();
1318                                         });
1319       break;
1321     case 'paste-from-organize-menu-command':
1322       pasteBookmark(bmm.list.parentId);
1323       break;
1325     case 'paste-from-folders-menu-command':
1326       pasteBookmark(bmm.tree.selectedItem.bookmarkId);
1327       break;
1329     case 'paste-from-context-menu-command':
1330       pasteBookmark(getSelectedId());
1331       break;
1333     case 'sort-command':
1334       recordUserAction('Sort');
1335       chrome.bookmarkManagerPrivate.sortChildren(bmm.list.parentId);
1336       break;
1338     case 'rename-folder-command':
1339       editSelectedItem();
1340       break;
1342     case 'rename-folder-from-folders-menu-command':
1343       bmm.tree.selectedItem.editing = true;
1344       break;
1346     case 'edit-command':
1347       recordUserAction('Edit');
1348       editSelectedItem();
1349       break;
1351     case 'new-folder-from-folders-menu-command':
1352       target = bmm.tree;
1353     case 'new-folder-command':
1354       recordUserAction('NewFolder');
1355       newFolder(target);
1356       break;
1358     case 'add-new-bookmark-command':
1359       recordUserAction('AddPage');
1360       addPage();
1361       break;
1363     case 'open-in-same-window-command':
1364       recordUserAction('OpenInSame');
1365       openItem();
1366       break;
1368     case 'undo-delete-command':
1369     case 'undo-delete-from-folders-menu-command':
1370       recordUserAction('UndoDelete');
1371       undoDelete();
1372       break;
1373   }
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)
1383       return;
1384     var command = $(commandId);
1385     if (!command.disabled) {
1386       command.execute();
1387       if (e)
1388         e.preventDefault();  // Prevent the system beep.
1389     }
1390   }
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);
1396     });
1397   } else {
1398     document.addEventListener(eventName, handle);
1399   }
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;
1414   });
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);
1463     e.preventDefault();
1464   });
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];
1482   }
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;
1490     }
1491   }
1493   chrome.bookmarkManagerPrivate.canEdit(function(result) {
1494     canEdit = result;
1495   });
1497   chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1498     // TODO(rustema): propagate policy value to the bookmark manager when it
1499     // changes.
1500     incognitoModeAvailability = result;
1501   });
1503   chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1504     canOpenNewWindows = result;
1505   });
1507   cr.ui.FocusOutlineManager.forDocument(document);
1508   initializeSplitter();
1509   bmm.addBookmarkModelListeners();
1510   dnd.init(selectItemsAfterUserAction);
1511   bmm.tree.reload();
1514 initializeBookmarkManager();
1515 })();