Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / enhanced_bookmark_manager / js / main.js
blob5c23f40337e5d5fcc364cf62a81922ada7980325
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 (function() {
6 /** @const */ var BookmarkList = bmm.BookmarkList;
7 /** @const */ var BookmarkTree = bmm.BookmarkTree;
8 /** @const */ var Command = cr.ui.Command;
9 /** @const */ var CommandBinding = cr.ui.CommandBinding;
10 /** @const */ var LinkKind = cr.LinkKind;
11 /** @const */ var ListItem = cr.ui.ListItem;
12 /** @const */ var Menu = cr.ui.Menu;
13 /** @const */ var MenuButton = cr.ui.MenuButton;
14 /** @const */ var Promise = cr.Promise;
15 /** @const */ var Splitter = cr.ui.Splitter;
16 /** @const */ var TreeItem = cr.ui.TreeItem;
18 /**
19  * An array containing the BookmarkTreeNodes that were deleted in the last
20  * deletion action. This is used for implementing undo.
21  * @type {Array.<BookmarkTreeNode>}
22  */
23 var lastDeletedNodes;
25 /**
26  *
27  * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
28  * view. Zero means pointer doesn't hover on folder.
29  * @type {number}
30  */
31 var lastHoverOnFolderTimeStamp = 0;
33 /**
34  * Holds a function that will undo that last action, if global undo is enabled.
35  * @type {Function}
36  */
37 var performGlobalUndo;
39 /**
40  * Holds a link controller singleton. Use getLinkController() rarther than
41  * accessing this variabie.
42  * @type {LinkController}
43  */
44 var linkController;
46 /**
47  * New Windows are not allowed in Windows 8 metro mode.
48  */
49 var canOpenNewWindows = true;
51 /**
52  * Incognito mode availability can take the following values: ,
53  *   - 'enabled' for when both normal and incognito modes are available;
54  *   - 'disabled' for when incognito mode is disabled;
55  *   - 'forced' for when incognito mode is forced (normal mode is unavailable).
56  */
57 var incognitoModeAvailability = 'enabled';
59 /**
60  * Whether bookmarks can be modified.
61  * @type {boolean}
62  */
63 var canEdit = true;
65 /**
66  * @type {TreeItem}
67  * @const
68  */
69 var searchTreeItem = new TreeItem({
70   bookmarkId: 'q='
71 });
73 /**
74  * Command shortcut mapping.
75  * @const
76  */
77 var commandShortcutMap = cr.isMac ? {
78   'edit': 'Enter',
79   // On Mac we also allow Meta+Backspace.
80   'delete': 'U+007F  U+0008 Meta-U+0008',
81   'open-in-background-tab': 'Meta-Enter',
82   'open-in-new-tab': 'Shift-Meta-Enter',
83   'open-in-same-window': 'Meta-Down',
84   'open-in-new-window': 'Shift-Enter',
85   'rename-folder': 'Enter',
86   // Global undo is Command-Z. It is not in any menu.
87   'undo': 'Meta-U+005A',
88 } : {
89   'edit': 'F2',
90   'delete': 'U+007F',
91   'open-in-background-tab': 'Ctrl-Enter',
92   'open-in-new-tab': 'Shift-Ctrl-Enter',
93   'open-in-same-window': 'Enter',
94   'open-in-new-window': 'Shift-Enter',
95   'rename-folder': 'F2',
96   // Global undo is Ctrl-Z. It is not in any menu.
97   'undo': 'Ctrl-U+005A',
101  * Mapping for folder id to suffix of UMA. These names will be appeared
102  * after "BookmarkManager_NavigateTo_" in UMA dashboard.
103  * @const
104  */
105 var folderMetricsNameMap = {
106   '1': 'BookmarkBar',
107   '2': 'Other',
108   '3': 'Mobile',
109   'q=': 'Search',
110   'subfolder': 'SubFolder',
114  * Adds an event listener to a node that will remove itself after firing once.
115  * @param {!Element} node The DOM node to add the listener to.
116  * @param {string} name The name of the event listener to add to.
117  * @param {function(Event)} handler Function called when the event fires.
118  */
119 function addOneShotEventListener(node, name, handler) {
120   var f = function(e) {
121     handler(e);
122     node.removeEventListener(name, f);
123   };
124   node.addEventListener(name, f);
127 // Get the localized strings from the backend via bookmakrManagerPrivate API.
128 function loadLocalizedStrings(data) {
129   // The strings may contain & which we need to strip.
130   for (var key in data) {
131     data[key] = data[key].replace(/&/, '');
132   }
134   loadTimeData.data = data;
135   i18nTemplate.process(document, loadTimeData);
137   searchTreeItem.label = loadTimeData.getString('search');
138   searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' :
139                                   'images/bookmark_manager_search.png';
143  * Updates the location hash to reflect the current state of the application.
144  */
145 function updateHash() {
146   window.location.hash = tree.selectedItem.bookmarkId;
150  * Navigates to a bookmark ID.
151  * @param {string} id The ID to navigate to.
152  * @param {function()} callback Function called when list view loaded or
153  *     displayed specified folder.
154  */
155 function navigateTo(id, callback) {
156   if (list.parentId == id) {
157     callback();
158     return;
159   }
161   var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
162                   folderMetricsNameMap['subfolder'];
163   chrome.metricsPrivate.recordUserAction(
164       'BookmarkManager_NavigateTo_' + metricsId);
166   addOneShotEventListener(list, 'load', callback);
167   updateParentId(id);
171  * Updates the parent ID of the bookmark list and selects the correct tree item.
172  * @param {string} id The id.
173  */
174 function updateParentId(id) {
175   // Setting list.parentId fires 'load' event.
176   list.parentId = id;
178   // When tree.selectedItem changed, tree view calls navigatTo() then it
179   // calls updateHash() when list view displayed specified folder.
180   tree.selectedItem = bmm.treeLookup[id] || tree.selectedItem;
183 // Process the location hash. This is called by onhashchange and when the page
184 // is first loaded.
185 function processHash() {
186   var id = window.location.hash.slice(1);
187   if (!id) {
188     // If we do not have a hash, select first item in the tree.
189     id = tree.items[0].bookmarkId;
190   }
192   var valid = false;
193   if (/^e=/.test(id)) {
194     id = id.slice(2);
196     // If hash contains e=, edit the item specified.
197     chrome.bookmarks.get(id, function(bookmarkNodes) {
198       // Verify the node to edit is a valid node.
199       if (!bookmarkNodes || bookmarkNodes.length != 1)
200         return;
201       var bookmarkNode = bookmarkNodes[0];
203       // After the list reloads, edit the desired bookmark.
204       var editBookmark = function(e) {
205         var index = list.dataModel.findIndexById(bookmarkNode.id);
206         if (index != -1) {
207           var sm = list.selectionModel;
208           sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
209           scrollIntoViewAndMakeEditable(index);
210         }
211       };
213       navigateTo(bookmarkNode.parentId, editBookmark);
214     });
216     // We handle the two cases of navigating to the bookmark to be edited
217     // above. Don't run the standard navigation code below.
218     return;
219   } else if (/^q=/.test(id)) {
220     // In case we got a search hash, update the text input and the
221     // bmm.treeLookup to use the new id.
222     setSearch(id.slice(2));
223     valid = true;
224   }
226   // Navigate to bookmark 'id' (which may be a query of the form q=query).
227   if (valid) {
228     updateParentId(id);
229   } else {
230     // We need to verify that this is a correct ID.
231     chrome.bookmarks.get(id, function(items) {
232       if (items && items.length == 1)
233         updateParentId(id);
234     });
235   }
238 // Activate is handled by the open-in-same-window-command.
239 function handleDoubleClickForList(e) {
240   if (e.button == 0)
241     $('open-in-same-window-command').execute();
244 // The list dispatches an event when the user clicks on the URL or the Show in
245 // folder part.
246 function handleUrlClickedForList(e) {
247   getLinkController().openUrlFromEvent(e.url, e.originalEvent);
248   chrome.bookmarkManagerPrivate.recordLaunch();
251 function handleSearch(e) {
252   setSearch(this.value);
256  * Navigates to the search results for the search text.
257  * @param {string} searchText The text to search for.
258  */
259 function setSearch(searchText) {
260   if (searchText) {
261     // Only update search item if we have a search term. We never want the
262     // search item to be for an empty search.
263     delete bmm.treeLookup[searchTreeItem.bookmarkId];
264     var id = searchTreeItem.bookmarkId = 'q=' + searchText;
265     bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
266   }
268   var input = $('term');
269   // Do not update the input if the user is actively using the text input.
270   if (document.activeElement != input)
271     input.value = searchText;
273   if (searchText) {
274     tree.add(searchTreeItem);
275     tree.selectedItem = searchTreeItem;
276   } else {
277     // Go "home".
278     tree.selectedItem = tree.items[0];
279     id = tree.selectedItem.bookmarkId;
280   }
282   // Navigate now and update hash immediately.
283   navigateTo(id, updateHash);
286 // Handle the logo button UI.
287 // When the user clicks the button we should navigate "home" and focus the list.
288 function handleClickOnLogoButton(e) {
289   setSearch('');
290   $('list').focus();
294  * This returns the user visible path to the folder where the bookmark is
295  * located.
296  * @param {number} parentId The ID of the parent folder.
297  * @return {string} The path to the the bookmark,
298  */
299 function getFolder(parentId) {
300   var parentNode = tree.getBookmarkNodeById(parentId);
301   if (parentNode) {
302     var s = parentNode.title;
303     if (parentNode.parentId != bmm.ROOT_ID) {
304       return getFolder(parentNode.parentId) + '/' + s;
305     }
306     return s;
307   }
310 function handleLoadForTree(e) {
311   processHash();
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(node))
335       return bmm.loadSubtree(node.id);
336     // Not a folder so we already have all the data we need.
337     return new Promise(node);
338   });
340   var urlsPromise = new Promise();
342   var p = Promise.all.apply(null, promises);
343   p.addListener(function(nodes) {
344     nodes.forEach(function(node) {
345       addNodes(node);
346     });
347     urlsPromise.value = urls;
348   });
350   return urlsPromise;
354  * Returns the nodes (non recursive) to use for the open commands.
355  * @param {HTMLElement} target .
356  * @return {Array.<BookmarkTreeNode>} .
357  */
358 function getNodesForOpen(target) {
359   if (target == tree) {
360     var folderItem = tree.selectedItem;
361     return folderItem == searchTreeItem ?
362         list.dataModel.slice() : tree.selectedFolders;
363   }
364   var items = list.selectedItems;
365   return items.length ? items : list.dataModel.slice();
369  * Returns a promise that will contain all URLs of all the selected bookmarks
370  * and the nested bookmarks for use with the open commands.
371  * @param {HTMLElement} target The target list or tree.
372  * @return {Promise} .
373  */
374 function getUrlsForOpenCommands(target) {
375   return getAllUrls(getNodesForOpen(target));
378 function notNewNode(node) {
379   return node.id != 'new';
383  * Helper function that updates the canExecute and labels for the open-like
384  * commands.
385  * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
386  * @param {!cr.ui.Command} command The command we are currently processing.
387  * @param {string} singularId The string id of singular form of the menu label.
388  * @param {string} pluralId The string id of menu label if the singular form is
389        not used.
390  * @param {boolean} commandDisabled Whether the menu item should be disabled
391        no matter what bookmarks are selected.
392  */
393 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
394   if (singularId) {
395     // The command label reflects the selection which might not reflect
396     // how many bookmarks will be opened. For example if you right click an
397     // empty area in a folder with 1 bookmark the text should still say "all".
398     var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
399     var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
400     command.label = loadTimeData.getString(singular ? singularId : pluralId);
401   }
403   if (commandDisabled) {
404     command.disabled = true;
405     e.canExecute = false;
406     return;
407   }
409   getUrlsForOpenCommands(e.target).addListener(function(urls) {
410     var disabled = !urls.length;
411     command.disabled = disabled;
412     e.canExecute = !disabled;
413   });
417  * Calls the backend to figure out if we can paste the clipboard into the active
418  * folder.
419  * @param {Function=} opt_f Function to call after the state has been updated.
420  */
421 function updatePasteCommand(opt_f) {
422   function update(canPaste) {
423     var organizeMenuCommand = $('paste-from-organize-menu-command');
424     var contextMenuCommand = $('paste-from-context-menu-command');
425     organizeMenuCommand.disabled = !canPaste;
426     contextMenuCommand.disabled = !canPaste;
427     if (opt_f)
428       opt_f();
429   }
430   // We cannot paste into search view.
431   if (list.isSearch())
432     update(false);
433   else
434     chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
437 function handleCanExecuteForDocument(e) {
438   var command = e.command;
439   switch (command.id) {
440     case 'import-menu-command':
441       e.canExecute = canEdit;
442       break;
443     case 'export-menu-command':
444       // We can always execute the export-menu command.
445       e.canExecute = true;
446       break;
447     case 'sort-command':
448       e.canExecute = !list.isSearch() && list.dataModel.length > 1;
449       break;
450     case 'undo-command':
451       // The global undo command has no visible UI, so always enable it, and
452       // just make it a no-op if undo is not possible.
453       e.canExecute = true;
454       break;
455     default:
456       canExecuteForList(e);
457       break;
458   }
462  * Helper function for handling canExecute for the list and the tree.
463  * @param {!Event} e Can execute event object.
464  * @param {boolean} isSearch Whether the user is trying to do a command on
465  *     search.
466  */
467 function canExecuteShared(e, isSearch) {
468   var command = e.command;
469   var commandId = command.id;
470   switch (commandId) {
471     case 'paste-from-organize-menu-command':
472     case 'paste-from-context-menu-command':
473       updatePasteCommand();
474       break;
476     case 'add-new-bookmark-command':
477     case 'new-folder-command':
478       e.canExecute = !isSearch && canEdit;
479       break;
481     case 'open-in-new-tab-command':
482       updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
483       break;
484     case 'open-in-background-tab-command':
485       updateOpenCommand(e, command, '', '', false);
486       break;
487     case 'open-in-new-window-command':
488       updateOpenCommand(e, command,
489           'open_in_new_window', 'open_all_new_window',
490           // Disabled when incognito is forced.
491           incognitoModeAvailability == 'forced' || !canOpenNewWindows);
492       break;
493     case 'open-incognito-window-command':
494       updateOpenCommand(e, command,
495           'open_incognito', 'open_all_incognito',
496           // Not available when incognito is disabled.
497           incognitoModeAvailability == 'disabled');
498       break;
500     case 'undo-delete-command':
501       e.canExecute = !!lastDeletedNodes;
502       break;
503   }
507  * Helper function for handling canExecute for the list and document.
508  * @param {!Event} e Can execute event object.
509  */
510 function canExecuteForList(e) {
511   var command = e.command;
512   var commandId = command.id;
514   function hasSelected() {
515     return !!list.selectedItem;
516   }
518   function hasSingleSelected() {
519     return list.selectedItems.length == 1;
520   }
522   function canCopyItem(item) {
523     return item.id != 'new';
524   }
526   function canCopyItems() {
527     var selectedItems = list.selectedItems;
528     return selectedItems && selectedItems.some(canCopyItem);
529   }
531   function isSearch() {
532     return list.isSearch();
533   }
535   switch (commandId) {
536     case 'rename-folder-command':
537       // Show rename if a single folder is selected.
538       var items = list.selectedItems;
539       if (items.length != 1) {
540         e.canExecute = false;
541         command.hidden = true;
542       } else {
543         var isFolder = bmm.isFolder(items[0]);
544         e.canExecute = isFolder && canEdit;
545         command.hidden = !isFolder;
546       }
547       break;
549     case 'edit-command':
550       // Show the edit command if not a folder.
551       var items = list.selectedItems;
552       if (items.length != 1) {
553         e.canExecute = false;
554         command.hidden = false;
555       } else {
556         var isFolder = bmm.isFolder(items[0]);
557         e.canExecute = !isFolder && canEdit;
558         command.hidden = isFolder;
559       }
560       break;
562     case 'show-in-folder-command':
563       e.canExecute = isSearch() && hasSingleSelected();
564       break;
566     case 'delete-command':
567     case 'cut-command':
568       e.canExecute = canCopyItems() && canEdit;
569       break;
571     case 'copy-command':
572       e.canExecute = canCopyItems();
573       break;
575     case 'open-in-same-window-command':
576       e.canExecute = hasSelected();
577       break;
579     default:
580       canExecuteShared(e, isSearch());
581   }
584 // Update canExecute for the commands when the list is the active element.
585 function handleCanExecuteForList(e) {
586   if (e.target != list) return;
587   canExecuteForList(e);
590 // Update canExecute for the commands when the tree is the active element.
591 function handleCanExecuteForTree(e) {
592   if (e.target != tree) return;
594   var command = e.command;
595   var commandId = command.id;
597   function hasSelected() {
598     return !!e.target.selectedItem;
599   }
601   function isSearch() {
602     var item = e.target.selectedItem;
603     return item == searchTreeItem;
604   }
606   function isTopLevelItem() {
607     return e.target.selectedItem.parentNode == tree;
608   }
610   switch (commandId) {
611     case 'rename-folder-command':
612       command.hidden = false;
613       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
614       break;
616     case 'edit-command':
617       command.hidden = true;
618       e.canExecute = false;
619       break;
621     case 'delete-command':
622     case 'cut-command':
623       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
624       break;
626     case 'copy-command':
627       e.canExecute = hasSelected() && !isTopLevelItem();
628       break;
630     default:
631       canExecuteShared(e, isSearch());
632   }
636  * Update the canExecute state of the commands when the selection changes.
637  * @param {Event} e The change event object.
638  */
639 function updateCommandsBasedOnSelection(e) {
640   if (e.target == document.activeElement) {
641     // Paste only needs to be updated when the tree selection changes.
642     var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
643       'add-new-bookmark', 'new-folder', 'open-in-new-tab',
644       'open-in-background-tab', 'open-in-new-window', 'open-incognito-window',
645       'open-in-same-window', 'show-in-folder'];
647     if (e.target == tree) {
648       commandNames.push('paste-from-context-menu', 'paste-from-organize-menu',
649                         'sort');
650     }
652     commandNames.forEach(function(baseId) {
653       $(baseId + '-command').canExecuteChange();
654     });
655   }
658 function updateEditingCommands() {
659   var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
660       'add-new-bookmark', 'new-folder', 'sort',
661       'paste-from-context-menu', 'paste-from-organize-menu'];
663   chrome.bookmarkManagerPrivate.canEdit(function(result) {
664     if (result != canEdit) {
665       canEdit = result;
666       editingCommands.forEach(function(baseId) {
667         $(baseId + '-command').canExecuteChange();
668       });
669     }
670   });
673 function handleChangeForTree(e) {
674   updateCommandsBasedOnSelection(e);
675   navigateTo(tree.selectedItem.bookmarkId, updateHash);
678 function handleOrganizeButtonClick(e) {
679   updateEditingCommands();
680   $('add-new-bookmark-command').canExecuteChange();
681   $('new-folder-command').canExecuteChange();
682   $('sort-command').canExecuteChange();
685 function handleRename(e) {
686   var item = e.target;
687   var bookmarkNode = item.bookmarkNode;
688   chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
689   performGlobalUndo = null;  // This can't be undone, so disable global undo.
692 function handleEdit(e) {
693   var item = e.target;
694   var bookmarkNode = item.bookmarkNode;
695   var context = {
696     title: bookmarkNode.title
697   };
698   if (!bmm.isFolder(bookmarkNode))
699     context.url = bookmarkNode.url;
701   if (bookmarkNode.id == 'new') {
702     selectItemsAfterUserAction(list);
704     // New page
705     context.parentId = bookmarkNode.parentId;
706     chrome.bookmarks.create(context, function(node) {
707       // A new node was created and will get added to the list due to the
708       // handler.
709       var dataModel = list.dataModel;
710       var index = dataModel.indexOf(bookmarkNode);
711       dataModel.splice(index, 1);
713       // Select new item.
714       var newIndex = dataModel.findIndexById(node.id);
715       if (newIndex != -1) {
716         var sm = list.selectionModel;
717         list.scrollIndexIntoView(newIndex);
718         sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
719       }
720     });
721   } else {
722     // Edit
723     chrome.bookmarks.update(bookmarkNode.id, context);
724   }
725   performGlobalUndo = null;  // This can't be undone, so disable global undo.
728 function handleCancelEdit(e) {
729   var item = e.target;
730   var bookmarkNode = item.bookmarkNode;
731   if (bookmarkNode.id == 'new') {
732     var dataModel = list.dataModel;
733     var index = dataModel.findIndexById('new');
734     dataModel.splice(index, 1);
735   }
739  * Navigates to the folder that the selected item is in and selects it. This is
740  * used for the show-in-folder command.
741  */
742 function showInFolder() {
743   var bookmarkNode = list.selectedItem;
744   if (!bookmarkNode)
745     return;
746   var parentId = bookmarkNode.parentId;
748   // After the list is loaded we should select the revealed item.
749   function selectItem() {
750     var index = list.dataModel.findIndexById(bookmarkNode.id);
751     if (index == -1)
752       return;
753     var sm = list.selectionModel;
754     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
755     list.scrollIndexIntoView(index);
756   }
758   var treeItem = bmm.treeLookup[parentId];
759   treeItem.reveal();
761   navigateTo(parentId, selectItem);
765  * @return {!cr.LinkController} The link controller used to open links based on
766  *     user clicks and keyboard actions.
767  */
768 function getLinkController() {
769   return linkController ||
770       (linkController = new cr.LinkController(loadTimeData));
774  * Returns the selected bookmark nodes of the provided tree or list.
775  * If |opt_target| is not provided or null the active element is used.
776  * Only call this if the list or the tree is focused.
777  * @param {BookmarkList|BookmarkTree} opt_target The target list or tree.
778  * @return {!Array} Array of bookmark nodes.
779  */
780 function getSelectedBookmarkNodes(opt_target) {
781   return (opt_target || document.activeElement) == tree ?
782       tree.selectedFolders : list.selectedItems;
786  * @return {!Array.<string>} An array of the selected bookmark IDs.
787  */
788 function getSelectedBookmarkIds() {
789   var selectedNodes = getSelectedBookmarkNodes();
790   selectedNodes.sort(function(a, b) { return a.index - b.index });
791   return selectedNodes.map(function(node) {
792     return node.id;
793   });
797  * Opens the selected bookmarks.
798  * @param {LinkKind} kind The kind of link we want to open.
799  * @param {HTMLElement} opt_eventTarget The target of the user initiated event.
800  */
801 function openBookmarks(kind, opt_eventTarget) {
802   // If we have selected any folders, we need to find all the bookmarks one
803   // level down. We use multiple async calls to getSubtree instead of getting
804   // the whole tree since we would like to minimize the amount of data sent.
806   var urlsP = getUrlsForOpenCommands(opt_eventTarget);
807   urlsP.addListener(function(urls) {
808     getLinkController().openUrls(urls, kind);
809     chrome.bookmarkManagerPrivate.recordLaunch();
810   });
814  * Opens an item in the list.
815  */
816 function openItem() {
817   var bookmarkNodes = getSelectedBookmarkNodes();
818   // If we double clicked or pressed enter on a single folder, navigate to it.
819   if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) {
820     navigateTo(bookmarkNodes[0].id, updateHash);
821   } else {
822     openBookmarks(LinkKind.FOREGROUND_TAB);
823   }
827  * Deletes the selected bookmarks. The bookmarks are saved in memory in case
828  * the user needs to undo the deletion.
829  */
830 function deleteBookmarks() {
831   var selectedIds = getSelectedBookmarkIds();
832   lastDeletedNodes = [];
834   function performDelete() {
835     chrome.bookmarkManagerPrivate.removeTrees(selectedIds);
836     $('undo-delete-command').canExecuteChange();
837     performGlobalUndo = undoDelete;
838   }
840   // First, store information about the bookmarks being deleted.
841   selectedIds.forEach(function(id) {
842     chrome.bookmarks.getSubTree(id, function(results) {
843       lastDeletedNodes.push(results);
845       // When all nodes have been saved, perform the deletion.
846       if (lastDeletedNodes.length === selectedIds.length)
847         performDelete();
848     });
849   });
853  * Restores a tree of bookmarks under a specified folder.
854  * @param {BookmarkTreeNode} node The node to restore.
855  * @param {=string} parentId The ID of the folder to restore under. If not
856  *     specified, the original parentId of the node will be used.
857  */
858 function restoreTree(node, parentId) {
859   var bookmarkInfo = {
860     parentId: parentId || node.parentId,
861     title: node.title,
862     index: node.index,
863     url: node.url
864   };
866   chrome.bookmarks.create(bookmarkInfo, function(result) {
867     if (!result) {
868       console.error('Failed to restore bookmark.');
869       return;
870     }
872     if (node.children) {
873       // Restore the children using the new ID for this node.
874       node.children.forEach(function(child) {
875         restoreTree(child, result.id);
876       });
877     }
878   });
882  * Restores the last set of bookmarks that was deleted.
883  */
884 function undoDelete() {
885   lastDeletedNodes.forEach(function(arr) {
886     arr.forEach(restoreTree);
887   });
888   lastDeletedNodes = null;
889   $('undo-delete-command').canExecuteChange();
891   // Only a single level of undo is supported, so disable global undo now.
892   performGlobalUndo = null;
896  * Computes folder for "Add Page" and "Add Folder".
897  * @return {string} The id of folder node where we'll create new page/folder.
898  */
899 function computeParentFolderForNewItem() {
900   if (document.activeElement == tree)
901     return list.parentId;
902   var selectedItem = list.selectedItem;
903   return selectedItem && bmm.isFolder(selectedItem) ?
904       selectedItem.id : list.parentId;
908  * Callback for rename folder and edit command. This starts editing for
909  * selected item.
910  */
911 function editSelectedItem() {
912   if (document.activeElement == tree) {
913     tree.selectedItem.editing = true;
914   } else {
915     var li = list.getListItem(list.selectedItem);
916     if (li)
917       li.editing = true;
918   }
922  * Callback for the new folder command. This creates a new folder and starts
923  * a rename of it.
924  */
925 function newFolder() {
926   performGlobalUndo = null;  // This can't be undone, so disable global undo.
928   var parentId = computeParentFolderForNewItem();
930   // Callback is called after tree and list data model updated.
931   function createFolder(callback) {
932     chrome.bookmarks.create({
933       title: loadTimeData.getString('new_folder_name'),
934       parentId: parentId
935     }, callback);
936   }
938   if (document.activeElement == tree) {
939     createFolder(function(newNode) {
940       navigateTo(newNode.id, function() {
941         bmm.treeLookup[newNode.id].editing = true;
942       });
943     });
944     return;
945   }
947   function editNewFolderInList() {
948     createFolder(function() {
949       var index = list.dataModel.length - 1;
950       var sm = list.selectionModel;
951       sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
952       scrollIntoViewAndMakeEditable(index);
953     });
954   }
956   navigateTo(parentId, editNewFolderInList);
960  * Scrolls the list item into view and makes it editable.
961  * @param {number} index The index of the item to make editable.
962  */
963 function scrollIntoViewAndMakeEditable(index) {
964   list.scrollIndexIntoView(index);
965   // onscroll is now dispatched asynchronously so we have to postpone
966   // the rest.
967   setTimeout(function() {
968     var item = list.getListItemByIndex(index);
969     if (item)
970       item.editing = true;
971   });
975  * Adds a page to the current folder. This is called by the
976  * add-new-bookmark-command handler.
977  */
978 function addPage() {
979   var parentId = computeParentFolderForNewItem();
981   function editNewBookmark() {
982     var fakeNode = {
983       title: '',
984       url: '',
985       parentId: parentId,
986       id: 'new'
987     };
988     var dataModel = list.dataModel;
989     var length = dataModel.length;
990     dataModel.splice(length, 0, fakeNode);
991     var sm = list.selectionModel;
992     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
993     scrollIntoViewAndMakeEditable(length);
994   };
996   navigateTo(parentId, editNewBookmark);
1000  * This function is used to select items after a user action such as paste, drop
1001  * add page etc.
1002  * @param {BookmarkList|BookmarkTree} target The target of the user action.
1003  * @param {=string} opt_selectedTreeId If provided, then select that tree id.
1004  */
1005 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1006   // We get one onCreated event per item so we delay the handling until we get
1007   // no more events coming.
1009   var ids = [];
1010   var timer;
1012   function handle(id, bookmarkNode) {
1013     clearTimeout(timer);
1014     if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
1015       ids.push(id);
1016     timer = setTimeout(handleTimeout, 50);
1017   }
1019   function handleTimeout() {
1020     chrome.bookmarks.onCreated.removeListener(handle);
1021     chrome.bookmarks.onMoved.removeListener(handle);
1023     if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1024       var index = ids.indexOf(opt_selectedTreeId);
1025       if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1026         tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1027       }
1028     } else if (target == list) {
1029       var dataModel = list.dataModel;
1030       var firstIndex = dataModel.findIndexById(ids[0]);
1031       var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1032       if (firstIndex != -1 && lastIndex != -1) {
1033         var selectionModel = list.selectionModel;
1034         selectionModel.selectedIndex = -1;
1035         selectionModel.selectRange(firstIndex, lastIndex);
1036         selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1037         list.focus();
1038       }
1039     }
1041     list.endBatchUpdates();
1042   }
1044   list.startBatchUpdates();
1046   chrome.bookmarks.onCreated.addListener(handle);
1047   chrome.bookmarks.onMoved.addListener(handle);
1048   timer = setTimeout(handleTimeout, 300);
1052  * Record user action.
1053  * @param {string} name An user action name.
1054  */
1055 function recordUserAction(name) {
1056   chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1060  * The currently selected bookmark, based on where the user is clicking.
1061  * @return {string} The ID of the currently selected bookmark (could be from
1062  *     tree view or list view).
1063  */
1064 function getSelectedId() {
1065   if (document.activeElement == tree)
1066     return tree.selectedItem.bookmarkId;
1067   var selectedItem = list.selectedItem;
1068   return selectedItem && bmm.isFolder(selectedItem) ?
1069       selectedItem.id : tree.selectedItem.bookmarkId;
1073  * Pastes the copied/cutted bookmark into the right location depending whether
1074  * if it was called from Organize Menu or from Context Menu.
1075  * @param {string} id The id of the element being pasted from.
1076  */
1077 function pasteBookmark(id) {
1078   recordUserAction('Paste');
1079   selectItemsAfterUserAction(list);
1080   chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1084  * Handler for the command event. This is used for context menu of list/tree
1085  * and organized menu.
1086  * @param {!Event} e The event object.
1087  */
1088 function handleCommand(e) {
1089   var command = e.command;
1090   var commandId = command.id;
1091   switch (commandId) {
1092     case 'import-menu-command':
1093       recordUserAction('Import');
1094       chrome.bookmarks.import();
1095       break;
1096     case 'export-menu-command':
1097       recordUserAction('Export');
1098       chrome.bookmarks.export();
1099       break;
1100     case 'undo-command':
1101       if (performGlobalUndo) {
1102         recordUserAction('UndoGlobal');
1103         performGlobalUndo();
1104       } else {
1105         recordUserAction('UndoNone');
1106       }
1107       break;
1108     case 'show-in-folder-command':
1109       recordUserAction('ShowInFolder');
1110       showInFolder();
1111       break;
1112     case 'open-in-new-tab-command':
1113     case 'open-in-background-tab-command':
1114       recordUserAction('OpenInNewTab');
1115       openBookmarks(LinkKind.BACKGROUND_TAB, e.target);
1116       break;
1117     case 'open-in-new-window-command':
1118       recordUserAction('OpenInNewWindow');
1119       openBookmarks(LinkKind.WINDOW, e.target);
1120       break;
1121     case 'open-incognito-window-command':
1122       recordUserAction('OpenIncognito');
1123       openBookmarks(LinkKind.INCOGNITO, e.target);
1124       break;
1125     case 'delete-command':
1126       recordUserAction('Delete');
1127       deleteBookmarks();
1128       break;
1129     case 'copy-command':
1130       recordUserAction('Copy');
1131       chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
1132                                          updatePasteCommand);
1133       break;
1134     case 'cut-command':
1135       recordUserAction('Cut');
1136       chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
1137                                         updatePasteCommand);
1138       break;
1139     case 'paste-from-organize-menu-command':
1140       pasteBookmark(list.parentId);
1141       break;
1142     case 'paste-from-context-menu-command':
1143       pasteBookmark(getSelectedId());
1144       break;
1145     case 'sort-command':
1146       recordUserAction('Sort');
1147       chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
1148       break;
1149     case 'rename-folder-command':
1150       editSelectedItem();
1151       break;
1152     case 'edit-command':
1153       recordUserAction('Edit');
1154       editSelectedItem();
1155       break;
1156     case 'new-folder-command':
1157       recordUserAction('NewFolder');
1158       newFolder();
1159       break;
1160     case 'add-new-bookmark-command':
1161       recordUserAction('AddPage');
1162       addPage();
1163       break;
1164     case 'open-in-same-window-command':
1165       recordUserAction('OpenInSame');
1166       openItem();
1167       break;
1168     case 'undo-delete-command':
1169       recordUserAction('UndoDelete');
1170       undoDelete();
1171       break;
1172   }
1175 // Execute the copy, cut and paste commands when those events are dispatched by
1176 // the browser. This allows us to rely on the browser to handle the keyboard
1177 // shortcuts for these commands.
1178 function installEventHandlerForCommand(eventName, commandId) {
1179   function handle(e) {
1180     if (document.activeElement != list && document.activeElement != tree)
1181       return;
1182     var command = $(commandId);
1183     if (!command.disabled) {
1184       command.execute();
1185       if (e)
1186         e.preventDefault();  // Prevent the system beep.
1187     }
1188   }
1189   if (eventName == 'paste') {
1190     // Paste is a bit special since we need to do an async call to see if we
1191     // can paste because the paste command might not be up to date.
1192     document.addEventListener(eventName, function(e) {
1193       updatePasteCommand(handle);
1194     });
1195   } else {
1196     document.addEventListener(eventName, handle);
1197   }
1200 function initializeSplitter() {
1201   var splitter = document.querySelector('.main > .splitter');
1202   Splitter.decorate(splitter);
1204   // The splitter persists the size of the left component in the local store.
1205   if ('treeWidth' in localStorage)
1206     splitter.previousElementSibling.style.width = localStorage['treeWidth'];
1208   splitter.addEventListener('resize', function(e) {
1209     localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
1210   });
1213 function initializeBookmarkManager() {
1214   // Sometimes the extension API is not initialized.
1215   if (!chrome.bookmarks)
1216     console.error('Bookmarks extension API is not available');
1218   chrome.bookmarkManagerPrivate.getStrings(loadLocalizedStrings);
1220   bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1222   cr.ui.decorate('menu', Menu);
1223   cr.ui.decorate('button[menu]', MenuButton);
1224   cr.ui.decorate('command', Command);
1225   BookmarkList.decorate(list);
1226   BookmarkTree.decorate(tree);
1228   list.addEventListener('canceledit', handleCancelEdit);
1229   list.addEventListener('canExecute', handleCanExecuteForList);
1230   list.addEventListener('change', updateCommandsBasedOnSelection);
1231   list.addEventListener('contextmenu', updateEditingCommands);
1232   list.addEventListener('dblclick', handleDoubleClickForList);
1233   list.addEventListener('edit', handleEdit);
1234   list.addEventListener('rename', handleRename);
1235   list.addEventListener('urlClicked', handleUrlClickedForList);
1237   tree.addEventListener('canExecute', handleCanExecuteForTree);
1238   tree.addEventListener('change', handleChangeForTree);
1239   tree.addEventListener('contextmenu', updateEditingCommands);
1240   tree.addEventListener('rename', handleRename);
1241   tree.addEventListener('load', handleLoadForTree);
1243   cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1244   list.contextMenu = $('context-menu');
1245   tree.contextMenu = $('context-menu');
1247   // We listen to hashchange so that we can update the currently shown folder
1248   // when // the user goes back and forward in the history.
1249   window.addEventListener('hashchange', processHash);
1251   document.querySelector('.header form').onsubmit = function(e) {
1252     setSearch($('term').value);
1253     e.preventDefault();
1254   };
1256   $('term').addEventListener('search', handleSearch);
1258   document.querySelector('.summary > button').addEventListener(
1259       'click', handleOrganizeButtonClick);
1261   document.querySelector('button.logo').addEventListener(
1262       'click', handleClickOnLogoButton);
1264   document.addEventListener('canExecute', handleCanExecuteForDocument);
1265   document.addEventListener('command', handleCommand);
1267   // Listen to copy, cut and paste events and execute the associated commands.
1268   installEventHandlerForCommand('copy', 'copy-command');
1269   installEventHandlerForCommand('cut', 'cut-command');
1270   installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1272   // Install shortcuts
1273   for (var name in commandShortcutMap) {
1274     $(name + '-command').shortcut = commandShortcutMap[name];
1275   }
1277   // Disable almost all commands at startup.
1278   var commands = document.querySelectorAll('command');
1279   for (var i = 0, command; command = commands[i]; ++i) {
1280     if (command.id != 'import-menu-command' &&
1281         command.id != 'export-menu-command') {
1282       command.disabled = true;
1283     }
1284   }
1286   chrome.bookmarkManagerPrivate.canEdit(function(result) {
1287     canEdit = result;
1288   });
1290   chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1291     // TODO(rustema): propagate policy value to the bookmark manager when it
1292     // changes.
1293     incognitoModeAvailability = result;
1294   });
1296   chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1297     canOpenNewWindows = result;
1298   });
1300   cr.ui.FocusOutlineManager.forDocument(document);
1301   initializeSplitter();
1302   bmm.addBookmarkModelListeners();
1303   dnd.init(selectItemsAfterUserAction);
1304   tree.reload();
1307 initializeBookmarkManager();
1308 })();