• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5(function() {
6/** @const */ var BookmarkList = bmm.BookmarkList;
7/** @const */ var BookmarkTree = bmm.BookmarkTree;
8/** @const */ var Command = cr.ui.Command;
9/** @const */ var CommandBinding = cr.ui.CommandBinding;
10/** @const */ var LinkKind = cr.LinkKind;
11/** @const */ var ListItem = cr.ui.ListItem;
12/** @const */ var Menu = cr.ui.Menu;
13/** @const */ var MenuButton = cr.ui.MenuButton;
14/** @const */ var Splitter = cr.ui.Splitter;
15/** @const */ var TreeItem = cr.ui.TreeItem;
16
17/**
18 * An array containing the BookmarkTreeNodes that were deleted in the last
19 * deletion action. This is used for implementing undo.
20 * @type {Array.<BookmarkTreeNode>}
21 */
22var lastDeletedNodes;
23
24/**
25 *
26 * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree
27 * view. Zero means pointer doesn't hover on folder.
28 * @type {number}
29 */
30var lastHoverOnFolderTimeStamp = 0;
31
32/**
33 * Holds a function that will undo that last action, if global undo is enabled.
34 * @type {Function}
35 */
36var performGlobalUndo;
37
38/**
39 * Holds a link controller singleton. Use getLinkController() rarther than
40 * accessing this variabie.
41 * @type {LinkController}
42 */
43var linkController;
44
45/**
46 * New Windows are not allowed in Windows 8 metro mode.
47 */
48var canOpenNewWindows = true;
49
50/**
51 * Incognito mode availability can take the following values: ,
52 *   - 'enabled' for when both normal and incognito modes are available;
53 *   - 'disabled' for when incognito mode is disabled;
54 *   - 'forced' for when incognito mode is forced (normal mode is unavailable).
55 */
56var incognitoModeAvailability = 'enabled';
57
58/**
59 * Whether bookmarks can be modified.
60 * @type {boolean}
61 */
62var canEdit = true;
63
64/**
65 * @type {TreeItem}
66 * @const
67 */
68var searchTreeItem = new TreeItem({
69  bookmarkId: 'q='
70});
71
72/**
73 * Command shortcut mapping.
74 * @const
75 */
76var commandShortcutMap = cr.isMac ? {
77  'edit': 'Enter',
78  // On Mac we also allow Meta+Backspace.
79  'delete': 'U+007F  U+0008 Meta-U+0008',
80  'open-in-background-tab': 'Meta-Enter',
81  'open-in-new-tab': 'Shift-Meta-Enter',
82  'open-in-same-window': 'Meta-Down',
83  'open-in-new-window': 'Shift-Enter',
84  'rename-folder': 'Enter',
85  // Global undo is Command-Z. It is not in any menu.
86  'undo': 'Meta-U+005A',
87} : {
88  'edit': 'F2',
89  'delete': 'U+007F',
90  'open-in-background-tab': 'Ctrl-Enter',
91  'open-in-new-tab': 'Shift-Ctrl-Enter',
92  'open-in-same-window': 'Enter',
93  'open-in-new-window': 'Shift-Enter',
94  'rename-folder': 'F2',
95  // Global undo is Ctrl-Z. It is not in any menu.
96  'undo': 'Ctrl-U+005A',
97};
98
99/**
100 * Mapping for folder id to suffix of UMA. These names will be appeared
101 * after "BookmarkManager_NavigateTo_" in UMA dashboard.
102 * @const
103 */
104var folderMetricsNameMap = {
105  '1': 'BookmarkBar',
106  '2': 'Other',
107  '3': 'Mobile',
108  'q=': 'Search',
109  'subfolder': 'SubFolder',
110};
111
112/**
113 * Adds an event listener to a node that will remove itself after firing once.
114 * @param {!Element} node The DOM node to add the listener to.
115 * @param {string} name The name of the event listener to add to.
116 * @param {function(Event)} handler Function called when the event fires.
117 */
118function addOneShotEventListener(node, name, handler) {
119  var f = function(e) {
120    handler(e);
121    node.removeEventListener(name, f);
122  };
123  node.addEventListener(name, f);
124}
125
126// Get the localized strings from the backend via bookmakrManagerPrivate API.
127function loadLocalizedStrings(data) {
128  // The strings may contain & which we need to strip.
129  for (var key in data) {
130    data[key] = data[key].replace(/&/, '');
131  }
132
133  loadTimeData.data = data;
134  i18nTemplate.process(document, loadTimeData);
135
136  searchTreeItem.label = loadTimeData.getString('search');
137  searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' :
138                                  'images/bookmark_manager_search.png';
139}
140
141/**
142 * Updates the location hash to reflect the current state of the application.
143 */
144function updateHash() {
145  window.location.hash = tree.selectedItem.bookmarkId;
146  updateAllCommands();
147}
148
149/**
150 * Navigates to a bookmark ID.
151 * @param {string} id The ID to navigate to.
152 * @param {function()=} opt_callback Function called when list view loaded or
153 *     displayed specified folder.
154 */
155function navigateTo(id, opt_callback) {
156  window.location.hash = id;
157  updateAllCommands();
158
159  var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
160                  folderMetricsNameMap['subfolder'];
161  chrome.metricsPrivate.recordUserAction(
162      'BookmarkManager_NavigateTo_' + metricsId);
163
164  if (opt_callback) {
165    if (list.parentId == id)
166      opt_callback();
167    else
168      addOneShotEventListener(list, 'load', opt_callback);
169  }
170}
171
172/**
173 * Updates the parent ID of the bookmark list and selects the correct tree item.
174 * @param {string} id The id.
175 */
176function updateParentId(id) {
177  // Setting list.parentId fires 'load' event.
178  list.parentId = id;
179
180  // When tree.selectedItem changed, tree view calls navigatTo() then it
181  // calls updateHash() when list view displayed specified folder.
182  tree.selectedItem = bmm.treeLookup[id] || tree.selectedItem;
183}
184
185// Process the location hash. This is called by onhashchange and when the page
186// is first loaded.
187function processHash() {
188  var id = window.location.hash.slice(1);
189  if (!id) {
190    // If we do not have a hash, select first item in the tree.
191    id = tree.items[0].bookmarkId;
192  }
193
194  var valid = false;
195  if (/^e=/.test(id)) {
196    id = id.slice(2);
197
198    // If hash contains e=, edit the item specified.
199    chrome.bookmarks.get(id, function(bookmarkNodes) {
200      // Verify the node to edit is a valid node.
201      if (!bookmarkNodes || bookmarkNodes.length != 1)
202        return;
203      var bookmarkNode = bookmarkNodes[0];
204
205      // After the list reloads, edit the desired bookmark.
206      var editBookmark = function(e) {
207        var index = list.dataModel.findIndexById(bookmarkNode.id);
208        if (index != -1) {
209          var sm = list.selectionModel;
210          sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
211          scrollIntoViewAndMakeEditable(index);
212        }
213      };
214
215      navigateTo(bookmarkNode.parentId, editBookmark);
216    });
217
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  }
227
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  }
238}
239
240// Activate is handled by the open-in-same-window-command.
241function handleDoubleClickForList(e) {
242  if (e.button == 0)
243    $('open-in-same-window-command').execute();
244}
245
246// The list dispatches an event when the user clicks on the URL or the Show in
247// folder part.
248function handleUrlClickedForList(e) {
249  getLinkController().openUrlFromEvent(e.url, e.originalEvent);
250  chrome.bookmarkManagerPrivate.recordLaunch();
251}
252
253function handleSearch(e) {
254  setSearch(this.value);
255}
256
257/**
258 * Navigates to the search results for the search text.
259 * @param {string} searchText The text to search for.
260 */
261function 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  }
269
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;
274
275  if (searchText) {
276    tree.add(searchTreeItem);
277    tree.selectedItem = searchTreeItem;
278  } else {
279    // Go "home".
280    tree.selectedItem = tree.items[0];
281    id = tree.selectedItem.bookmarkId;
282  }
283
284  navigateTo(id);
285}
286
287// Handle the logo button UI.
288// When the user clicks the button we should navigate "home" and focus the list.
289function handleClickOnLogoButton(e) {
290  setSearch('');
291  $('list').focus();
292}
293
294/**
295 * This returns the user visible path to the folder where the bookmark is
296 * located.
297 * @param {number} parentId The ID of the parent folder.
298 * @return {string} The path to the the bookmark,
299 */
300function getFolder(parentId) {
301  var parentNode = tree.getBookmarkNodeById(parentId);
302  if (parentNode) {
303    var s = parentNode.title;
304    if (parentNode.parentId != bmm.ROOT_ID) {
305      return getFolder(parentNode.parentId) + '/' + s;
306    }
307    return s;
308  }
309}
310
311function handleLoadForTree(e) {
312  processHash();
313}
314
315/**
316 * Returns a promise for all the URLs in the {@code nodes} and the direct
317 * children of {@code nodes}.
318 * @param {!Array.<BookmarkTreeNode>} nodes .
319 * @return {!Promise.<Array.<string>>} .
320 */
321function getAllUrls(nodes) {
322  var urls = [];
323
324  // Adds the node and all its direct children.
325  function addNodes(node) {
326    if (node.id == 'new')
327      return;
328
329    if (node.children) {
330      node.children.forEach(function(child) {
331        if (!bmm.isFolder(child))
332          urls.push(child.url);
333      });
334    } else {
335      urls.push(node.url);
336    }
337  }
338
339  // Get a future promise for the nodes.
340  var promises = nodes.map(function(node) {
341    if (bmm.isFolder(node))
342      return bmm.loadSubtree(node.id);
343    // Not a folder so we already have all the data we need.
344    return Promise.resolve(node);
345  });
346
347  return Promise.all(promises).then(function(nodes) {
348    nodes.forEach(addNodes);
349    return urls;
350  });
351}
352
353/**
354 * Returns the nodes (non recursive) to use for the open commands.
355 * @param {HTMLElement} target .
356 * @return {Array.<BookmarkTreeNode>} .
357 */
358function getNodesForOpen(target) {
359  if (target == tree) {
360    if (tree.selectedItem != searchTreeItem)
361      return tree.selectedFolders;
362    // Fall through to use all nodes in the list.
363  } else {
364    var items = list.selectedItems;
365    if (items.length)
366      return items;
367  }
368
369  // The list starts off with a null dataModel. We can get here during startup.
370  if (!list.dataModel)
371    return [];
372
373  // Return an array based on the dataModel.
374  return list.dataModel.slice();
375}
376
377/**
378 * Returns a promise that will contain all URLs of all the selected bookmarks
379 * and the nested bookmarks for use with the open commands.
380 * @param {HTMLElement} target The target list or tree.
381 * @return {Promise.<Array.<string>>} .
382 */
383function getUrlsForOpenCommands(target) {
384  return getAllUrls(getNodesForOpen(target));
385}
386
387function notNewNode(node) {
388  return node.id != 'new';
389}
390
391/**
392 * Helper function that updates the canExecute and labels for the open-like
393 * commands.
394 * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
395 * @param {!cr.ui.Command} command The command we are currently processing.
396 * @param {string} singularId The string id of singular form of the menu label.
397 * @param {string} pluralId The string id of menu label if the singular form is
398       not used.
399 * @param {boolean} commandDisabled Whether the menu item should be disabled
400       no matter what bookmarks are selected.
401 */
402function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
403  if (singularId) {
404    // The command label reflects the selection which might not reflect
405    // how many bookmarks will be opened. For example if you right click an
406    // empty area in a folder with 1 bookmark the text should still say "all".
407    var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
408    var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
409    command.label = loadTimeData.getString(singular ? singularId : pluralId);
410  }
411
412  if (commandDisabled) {
413    command.disabled = true;
414    e.canExecute = false;
415    return;
416  }
417
418  getUrlsForOpenCommands(e.target).then(function(urls) {
419    var disabled = !urls.length;
420    command.disabled = disabled;
421    e.canExecute = !disabled;
422  });
423}
424
425/**
426 * Calls the backend to figure out if we can paste the clipboard into the active
427 * folder.
428 * @param {Function=} opt_f Function to call after the state has been updated.
429 */
430function updatePasteCommand(opt_f) {
431  function update(canPaste) {
432    var organizeMenuCommand = $('paste-from-organize-menu-command');
433    var contextMenuCommand = $('paste-from-context-menu-command');
434    organizeMenuCommand.disabled = !canPaste;
435    contextMenuCommand.disabled = !canPaste;
436    if (opt_f)
437      opt_f();
438  }
439  // We cannot paste into search view.
440  if (list.isSearch())
441    update(false);
442  else
443    chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
444}
445
446function handleCanExecuteForDocument(e) {
447  var command = e.command;
448  switch (command.id) {
449    case 'import-menu-command':
450      e.canExecute = canEdit;
451      break;
452    case 'export-menu-command':
453      // We can always execute the export-menu command.
454      e.canExecute = true;
455      break;
456    case 'sort-command':
457      e.canExecute = !list.isSearch() &&
458          list.dataModel && list.dataModel.length > 1 &&
459          !isUnmodifiable(tree.getBookmarkNodeById(list.parentId));
460      break;
461    case 'undo-command':
462      // The global undo command has no visible UI, so always enable it, and
463      // just make it a no-op if undo is not possible.
464      e.canExecute = true;
465      break;
466    default:
467      canExecuteForList(e);
468      break;
469  }
470}
471
472/**
473 * Helper function for handling canExecute for the list and the tree.
474 * @param {!Event} e Can execute event object.
475 * @param {boolean} isSearch Whether the user is trying to do a command on
476 *     search.
477 */
478function canExecuteShared(e, isSearch) {
479  var command = e.command;
480  var commandId = command.id;
481  switch (commandId) {
482    case 'paste-from-organize-menu-command':
483    case 'paste-from-context-menu-command':
484      updatePasteCommand();
485      break;
486
487    case 'add-new-bookmark-command':
488    case 'new-folder-command':
489      var parentId = computeParentFolderForNewItem();
490      var unmodifiable = isUnmodifiable(tree.getBookmarkNodeById(parentId));
491      e.canExecute = !isSearch && canEdit && !unmodifiable;
492      break;
493
494    case 'open-in-new-tab-command':
495      updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
496      break;
497    case 'open-in-background-tab-command':
498      updateOpenCommand(e, command, '', '', false);
499      break;
500    case 'open-in-new-window-command':
501      updateOpenCommand(e, command,
502          'open_in_new_window', 'open_all_new_window',
503          // Disabled when incognito is forced.
504          incognitoModeAvailability == 'forced' || !canOpenNewWindows);
505      break;
506    case 'open-incognito-window-command':
507      updateOpenCommand(e, command,
508          'open_incognito', 'open_all_incognito',
509          // Not available when incognito is disabled.
510          incognitoModeAvailability == 'disabled');
511      break;
512
513    case 'undo-delete-command':
514      e.canExecute = !!lastDeletedNodes;
515      break;
516  }
517}
518
519/**
520 * Helper function for handling canExecute for the list and document.
521 * @param {!Event} e Can execute event object.
522 */
523function canExecuteForList(e) {
524  var command = e.command;
525  var commandId = command.id;
526
527  function hasSelected() {
528    return !!list.selectedItem;
529  }
530
531  function hasSingleSelected() {
532    return list.selectedItems.length == 1;
533  }
534
535  function canCopyItem(item) {
536    return item.id != 'new';
537  }
538
539  function canCopyItems() {
540    var selectedItems = list.selectedItems;
541    return selectedItems && selectedItems.some(canCopyItem);
542  }
543
544  function isSearch() {
545    return list.isSearch();
546  }
547
548  switch (commandId) {
549    case 'rename-folder-command':
550      // Show rename if a single folder is selected.
551      var items = list.selectedItems;
552      if (items.length != 1) {
553        e.canExecute = false;
554        command.hidden = true;
555      } else {
556        var isFolder = bmm.isFolder(items[0]);
557        e.canExecute = isFolder && canEdit && !hasUnmodifiable(items);
558        command.hidden = !isFolder;
559      }
560      break;
561
562    case 'edit-command':
563      // Show the edit command if not a folder.
564      var items = list.selectedItems;
565      if (items.length != 1) {
566        e.canExecute = false;
567        command.hidden = false;
568      } else {
569        var isFolder = bmm.isFolder(items[0]);
570        e.canExecute = !isFolder && canEdit && !hasUnmodifiable(items);
571        command.hidden = isFolder;
572      }
573      break;
574
575    case 'show-in-folder-command':
576      e.canExecute = isSearch() && hasSingleSelected();
577      break;
578
579    case 'delete-command':
580    case 'cut-command':
581      e.canExecute = canCopyItems() && canEdit &&
582          !hasUnmodifiable(list.selectedItems);
583      break;
584
585    case 'copy-command':
586      e.canExecute = canCopyItems();
587      break;
588
589    case 'open-in-same-window-command':
590      e.canExecute = hasSelected();
591      break;
592
593    default:
594      canExecuteShared(e, isSearch());
595  }
596}
597
598// Update canExecute for the commands when the list is the active element.
599function handleCanExecuteForList(e) {
600  if (e.target != list) return;
601  canExecuteForList(e);
602}
603
604// Update canExecute for the commands when the tree is the active element.
605function handleCanExecuteForTree(e) {
606  if (e.target != tree) return;
607
608  var command = e.command;
609  var commandId = command.id;
610
611  function hasSelected() {
612    return !!e.target.selectedItem;
613  }
614
615  function isSearch() {
616    var item = e.target.selectedItem;
617    return item == searchTreeItem;
618  }
619
620  function isTopLevelItem() {
621    return e.target.selectedItem.parentNode == tree;
622  }
623
624  switch (commandId) {
625    case 'rename-folder-command':
626      command.hidden = false;
627      e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
628          !hasUnmodifiable(tree.selectedFolders);
629      break;
630
631    case 'edit-command':
632      command.hidden = true;
633      e.canExecute = false;
634      break;
635
636    case 'delete-command':
637    case 'cut-command':
638      e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
639          !hasUnmodifiable(tree.selectedFolders);
640      break;
641
642    case 'copy-command':
643      e.canExecute = hasSelected() && !isTopLevelItem();
644      break;
645
646    default:
647      canExecuteShared(e, isSearch());
648  }
649}
650
651/**
652 * Update the canExecute state of all the commands.
653 */
654function updateAllCommands() {
655  var commands = document.querySelectorAll('command');
656  for (var i = 0; i < commands.length; i++) {
657    commands[i].canExecuteChange();
658  }
659}
660
661function updateEditingCommands() {
662  var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
663      'add-new-bookmark', 'new-folder', 'sort',
664      'paste-from-context-menu', 'paste-from-organize-menu'];
665
666  chrome.bookmarkManagerPrivate.canEdit(function(result) {
667    if (result != canEdit) {
668      canEdit = result;
669      editingCommands.forEach(function(baseId) {
670        $(baseId + '-command').canExecuteChange();
671      });
672    }
673  });
674}
675
676function handleChangeForTree(e) {
677  navigateTo(tree.selectedItem.bookmarkId);
678}
679
680function handleOrganizeButtonClick(e) {
681  updateEditingCommands();
682  $('add-new-bookmark-command').canExecuteChange();
683  $('new-folder-command').canExecuteChange();
684  $('sort-command').canExecuteChange();
685}
686
687function handleRename(e) {
688  var item = e.target;
689  var bookmarkNode = item.bookmarkNode;
690  chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
691  performGlobalUndo = null;  // This can't be undone, so disable global undo.
692}
693
694function handleEdit(e) {
695  var item = e.target;
696  var bookmarkNode = item.bookmarkNode;
697  var context = {
698    title: bookmarkNode.title
699  };
700  if (!bmm.isFolder(bookmarkNode))
701    context.url = bookmarkNode.url;
702
703  if (bookmarkNode.id == 'new') {
704    selectItemsAfterUserAction(list);
705
706    // New page
707    context.parentId = bookmarkNode.parentId;
708    chrome.bookmarks.create(context, function(node) {
709      // A new node was created and will get added to the list due to the
710      // handler.
711      var dataModel = list.dataModel;
712      var index = dataModel.indexOf(bookmarkNode);
713      dataModel.splice(index, 1);
714
715      // Select new item.
716      var newIndex = dataModel.findIndexById(node.id);
717      if (newIndex != -1) {
718        var sm = list.selectionModel;
719        list.scrollIndexIntoView(newIndex);
720        sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
721      }
722    });
723  } else {
724    // Edit
725    chrome.bookmarks.update(bookmarkNode.id, context);
726  }
727  performGlobalUndo = null;  // This can't be undone, so disable global undo.
728}
729
730function handleCancelEdit(e) {
731  var item = e.target;
732  var bookmarkNode = item.bookmarkNode;
733  if (bookmarkNode.id == 'new') {
734    var dataModel = list.dataModel;
735    var index = dataModel.findIndexById('new');
736    dataModel.splice(index, 1);
737  }
738}
739
740/**
741 * Navigates to the folder that the selected item is in and selects it. This is
742 * used for the show-in-folder command.
743 */
744function showInFolder() {
745  var bookmarkNode = list.selectedItem;
746  if (!bookmarkNode)
747    return;
748  var parentId = bookmarkNode.parentId;
749
750  // After the list is loaded we should select the revealed item.
751  function selectItem() {
752    var index = list.dataModel.findIndexById(bookmarkNode.id);
753    if (index == -1)
754      return;
755    var sm = list.selectionModel;
756    sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
757    list.scrollIndexIntoView(index);
758  }
759
760  var treeItem = bmm.treeLookup[parentId];
761  treeItem.reveal();
762
763  navigateTo(parentId, selectItem);
764}
765
766/**
767 * @return {!cr.LinkController} The link controller used to open links based on
768 *     user clicks and keyboard actions.
769 */
770function getLinkController() {
771  return linkController ||
772      (linkController = new cr.LinkController(loadTimeData));
773}
774
775/**
776 * Returns the selected bookmark nodes of the provided tree or list.
777 * If |opt_target| is not provided or null the active element is used.
778 * Only call this if the list or the tree is focused.
779 * @param {BookmarkList|BookmarkTree} opt_target The target list or tree.
780 * @return {!Array} Array of bookmark nodes.
781 */
782function getSelectedBookmarkNodes(opt_target) {
783  return (opt_target || document.activeElement) == tree ?
784      tree.selectedFolders : list.selectedItems;
785}
786
787/**
788 * @return {!Array.<string>} An array of the selected bookmark IDs.
789 */
790function getSelectedBookmarkIds() {
791  var selectedNodes = getSelectedBookmarkNodes();
792  selectedNodes.sort(function(a, b) { return a.index - b.index });
793  return selectedNodes.map(function(node) {
794    return node.id;
795  });
796}
797
798/**
799 * @param {BookmarkTreeNode} node The node to test.
800 * @return {boolean} Whether the given node is unmodifiable.
801 */
802function isUnmodifiable(node) {
803  return node && node.unmodifiable;
804}
805
806/**
807 * @param {BookmarkList} A list of BookmarkNodes.
808 * @return {boolean} Whether any of the nodes is managed.
809 */
810function hasUnmodifiable(nodes) {
811  return nodes.some(isUnmodifiable);
812}
813
814/**
815 * Opens the selected bookmarks.
816 * @param {LinkKind} kind The kind of link we want to open.
817 * @param {HTMLElement} opt_eventTarget The target of the user initiated event.
818 */
819function openBookmarks(kind, opt_eventTarget) {
820  // If we have selected any folders, we need to find all the bookmarks one
821  // level down. We use multiple async calls to getSubtree instead of getting
822  // the whole tree since we would like to minimize the amount of data sent.
823
824  var urlsP = getUrlsForOpenCommands(opt_eventTarget);
825  urlsP.then(function(urls) {
826    getLinkController().openUrls(urls, kind);
827    chrome.bookmarkManagerPrivate.recordLaunch();
828  });
829}
830
831/**
832 * Opens an item in the list.
833 */
834function openItem() {
835  var bookmarkNodes = getSelectedBookmarkNodes();
836  // If we double clicked or pressed enter on a single folder, navigate to it.
837  if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0]))
838    navigateTo(bookmarkNodes[0].id);
839  else
840    openBookmarks(LinkKind.FOREGROUND_TAB);
841}
842
843/**
844 * Refreshes search results after delete or undo-delete.
845 * This ensures children of deleted folders do not remain in results
846 */
847function updateSearchResults() {
848  if (list.isSearch()) {
849    list.reload();
850  }
851}
852
853/**
854 * Deletes the selected bookmarks. The bookmarks are saved in memory in case
855 * the user needs to undo the deletion.
856 */
857function deleteBookmarks() {
858  var selectedIds = getSelectedBookmarkIds();
859  var filteredIds = getFilteredSelectedBookmarkIds();
860  lastDeletedNodes = [];
861
862  function performDelete() {
863    // Only remove filtered ids.
864    chrome.bookmarkManagerPrivate.removeTrees(filteredIds);
865    $('undo-delete-command').canExecuteChange();
866    performGlobalUndo = undoDelete;
867  }
868
869  // First, store information about the bookmarks being deleted.
870  // Store all selected ids.
871  selectedIds.forEach(function(id) {
872    chrome.bookmarks.getSubTree(id, function(results) {
873      lastDeletedNodes.push(results);
874
875      // When all nodes have been saved, perform the deletion.
876      if (lastDeletedNodes.length === selectedIds.length) {
877        performDelete();
878        updateSearchResults();
879      }
880    });
881  });
882}
883
884/**
885 * Restores a tree of bookmarks under a specified folder.
886 * @param {BookmarkTreeNode} node The node to restore.
887 * @param {=string} parentId The ID of the folder to restore under. If not
888 *     specified, the original parentId of the node will be used.
889 */
890function restoreTree(node, parentId) {
891  var bookmarkInfo = {
892    parentId: parentId || node.parentId,
893    title: node.title,
894    index: node.index,
895    url: node.url
896  };
897
898  chrome.bookmarks.create(bookmarkInfo, function(result) {
899    if (!result) {
900      console.error('Failed to restore bookmark.');
901      return;
902    }
903
904    if (node.children) {
905      // Restore the children using the new ID for this node.
906      node.children.forEach(function(child) {
907        restoreTree(child, result.id);
908      });
909    }
910
911    updateSearchResults();
912  });
913}
914
915/**
916 * Restores the last set of bookmarks that was deleted.
917 */
918function undoDelete() {
919  lastDeletedNodes.forEach(function(arr) {
920    arr.forEach(restoreTree);
921  });
922  lastDeletedNodes = null;
923  $('undo-delete-command').canExecuteChange();
924
925  // Only a single level of undo is supported, so disable global undo now.
926  performGlobalUndo = null;
927}
928
929/**
930 * Computes folder for "Add Page" and "Add Folder".
931 * @return {string} The id of folder node where we'll create new page/folder.
932 */
933function computeParentFolderForNewItem() {
934  if (document.activeElement == tree)
935    return list.parentId;
936  var selectedItem = list.selectedItem;
937  return selectedItem && bmm.isFolder(selectedItem) ?
938      selectedItem.id : list.parentId;
939}
940
941/**
942 * Callback for rename folder and edit command. This starts editing for
943 * selected item.
944 */
945function editSelectedItem() {
946  if (document.activeElement == tree) {
947    tree.selectedItem.editing = true;
948  } else {
949    var li = list.getListItem(list.selectedItem);
950    if (li)
951      li.editing = true;
952  }
953}
954
955/**
956 * Callback for the new folder command. This creates a new folder and starts
957 * a rename of it.
958 */
959function newFolder() {
960  performGlobalUndo = null;  // This can't be undone, so disable global undo.
961
962  var parentId = computeParentFolderForNewItem();
963
964  // Callback is called after tree and list data model updated.
965  function createFolder(callback) {
966    chrome.bookmarks.create({
967      title: loadTimeData.getString('new_folder_name'),
968      parentId: parentId
969    }, callback);
970  }
971
972  if (document.activeElement == tree) {
973    createFolder(function(newNode) {
974      navigateTo(newNode.id, function() {
975        bmm.treeLookup[newNode.id].editing = true;
976      });
977    });
978    return;
979  }
980
981  function editNewFolderInList() {
982    createFolder(function() {
983      var index = list.dataModel.length - 1;
984      var sm = list.selectionModel;
985      sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
986      scrollIntoViewAndMakeEditable(index);
987    });
988  }
989
990  navigateTo(parentId, editNewFolderInList);
991}
992
993/**
994 * Scrolls the list item into view and makes it editable.
995 * @param {number} index The index of the item to make editable.
996 */
997function scrollIntoViewAndMakeEditable(index) {
998  list.scrollIndexIntoView(index);
999  // onscroll is now dispatched asynchronously so we have to postpone
1000  // the rest.
1001  setTimeout(function() {
1002    var item = list.getListItemByIndex(index);
1003    if (item)
1004      item.editing = true;
1005  });
1006}
1007
1008/**
1009 * Adds a page to the current folder. This is called by the
1010 * add-new-bookmark-command handler.
1011 */
1012function addPage() {
1013  var parentId = computeParentFolderForNewItem();
1014
1015  function editNewBookmark() {
1016    var fakeNode = {
1017      title: '',
1018      url: '',
1019      parentId: parentId,
1020      id: 'new'
1021    };
1022    var dataModel = list.dataModel;
1023    var length = dataModel.length;
1024    dataModel.splice(length, 0, fakeNode);
1025    var sm = list.selectionModel;
1026    sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
1027    scrollIntoViewAndMakeEditable(length);
1028  };
1029
1030  navigateTo(parentId, editNewBookmark);
1031}
1032
1033/**
1034 * This function is used to select items after a user action such as paste, drop
1035 * add page etc.
1036 * @param {BookmarkList|BookmarkTree} target The target of the user action.
1037 * @param {=string} opt_selectedTreeId If provided, then select that tree id.
1038 */
1039function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1040  // We get one onCreated event per item so we delay the handling until we get
1041  // no more events coming.
1042
1043  var ids = [];
1044  var timer;
1045
1046  function handle(id, bookmarkNode) {
1047    clearTimeout(timer);
1048    if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
1049      ids.push(id);
1050    timer = setTimeout(handleTimeout, 50);
1051  }
1052
1053  function handleTimeout() {
1054    chrome.bookmarks.onCreated.removeListener(handle);
1055    chrome.bookmarks.onMoved.removeListener(handle);
1056
1057    if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1058      var index = ids.indexOf(opt_selectedTreeId);
1059      if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1060        tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1061      }
1062    } else if (target == list) {
1063      var dataModel = list.dataModel;
1064      var firstIndex = dataModel.findIndexById(ids[0]);
1065      var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1066      if (firstIndex != -1 && lastIndex != -1) {
1067        var selectionModel = list.selectionModel;
1068        selectionModel.selectedIndex = -1;
1069        selectionModel.selectRange(firstIndex, lastIndex);
1070        selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1071        list.focus();
1072      }
1073    }
1074
1075    list.endBatchUpdates();
1076  }
1077
1078  list.startBatchUpdates();
1079
1080  chrome.bookmarks.onCreated.addListener(handle);
1081  chrome.bookmarks.onMoved.addListener(handle);
1082  timer = setTimeout(handleTimeout, 300);
1083}
1084
1085/**
1086 * Record user action.
1087 * @param {string} name An user action name.
1088 */
1089function recordUserAction(name) {
1090  chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1091}
1092
1093/**
1094 * The currently selected bookmark, based on where the user is clicking.
1095 * @return {string} The ID of the currently selected bookmark (could be from
1096 *     tree view or list view).
1097 */
1098function getSelectedId() {
1099  if (document.activeElement == tree)
1100    return tree.selectedItem.bookmarkId;
1101  var selectedItem = list.selectedItem;
1102  return selectedItem && bmm.isFolder(selectedItem) ?
1103      selectedItem.id : tree.selectedItem.bookmarkId;
1104}
1105
1106/**
1107 * Pastes the copied/cutted bookmark into the right location depending whether
1108 * if it was called from Organize Menu or from Context Menu.
1109 * @param {string} id The id of the element being pasted from.
1110 */
1111function pasteBookmark(id) {
1112  recordUserAction('Paste');
1113  selectItemsAfterUserAction(list);
1114  chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1115}
1116
1117/**
1118 * Returns true if child is contained in another selected folder.
1119 * Traces parent nodes up the tree until a selected ancestor or root is found.
1120 */
1121function hasSelectedAncestor(parentNode) {
1122  function contains(arr, item) {
1123    for (var i = 0; i < arr.length; i++)
1124        if (arr[i] === item)
1125          return true;
1126    return false;
1127  }
1128
1129  // Don't search top level, cannot select permanent nodes in search.
1130  if (parentNode == null || parentNode.id <= 2)
1131    return false;
1132
1133  // Found selected ancestor.
1134  if (contains(getSelectedBookmarkNodes(), parentNode))
1135    return true;
1136
1137  // Keep digging.
1138  return hasSelectedAncestor(tree.getBookmarkNodeById(parentNode.parentId));
1139}
1140
1141function getFilteredSelectedBookmarkIds() {
1142  // Remove duplicates from filteredIds and return.
1143  var filteredIds = new Array();
1144  // Selected nodes to iterate through for matches.
1145  var nodes = getSelectedBookmarkNodes();
1146
1147  for (var i = 0; i < nodes.length; i++)
1148    if (!hasSelectedAncestor(tree.getBookmarkNodeById(nodes[i].parentId)))
1149      filteredIds.splice(0, 0, nodes[i].id);
1150
1151  return filteredIds;
1152}
1153
1154/**
1155 * Handler for the command event. This is used for context menu of list/tree
1156 * and organized menu.
1157 * @param {!Event} e The event object.
1158 */
1159function handleCommand(e) {
1160  var command = e.command;
1161  var commandId = command.id;
1162  switch (commandId) {
1163    case 'import-menu-command':
1164      recordUserAction('Import');
1165      chrome.bookmarks.import();
1166      break;
1167    case 'export-menu-command':
1168      recordUserAction('Export');
1169      chrome.bookmarks.export();
1170      break;
1171    case 'undo-command':
1172      if (performGlobalUndo) {
1173        recordUserAction('UndoGlobal');
1174        performGlobalUndo();
1175      } else {
1176        recordUserAction('UndoNone');
1177      }
1178      break;
1179    case 'show-in-folder-command':
1180      recordUserAction('ShowInFolder');
1181      showInFolder();
1182      break;
1183    case 'open-in-new-tab-command':
1184    case 'open-in-background-tab-command':
1185      recordUserAction('OpenInNewTab');
1186      openBookmarks(LinkKind.BACKGROUND_TAB, e.target);
1187      break;
1188    case 'open-in-new-window-command':
1189      recordUserAction('OpenInNewWindow');
1190      openBookmarks(LinkKind.WINDOW, e.target);
1191      break;
1192    case 'open-incognito-window-command':
1193      recordUserAction('OpenIncognito');
1194      openBookmarks(LinkKind.INCOGNITO, e.target);
1195      break;
1196    case 'delete-command':
1197      recordUserAction('Delete');
1198      deleteBookmarks();
1199      break;
1200    case 'copy-command':
1201      recordUserAction('Copy');
1202      chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
1203                                         updatePasteCommand);
1204      break;
1205    case 'cut-command':
1206      recordUserAction('Cut');
1207      chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
1208                                        function() {
1209                                          updatePasteCommand();
1210                                          updateSearchResults();
1211                                        });
1212      break;
1213    case 'paste-from-organize-menu-command':
1214      pasteBookmark(list.parentId);
1215      break;
1216    case 'paste-from-context-menu-command':
1217      pasteBookmark(getSelectedId());
1218      break;
1219    case 'sort-command':
1220      recordUserAction('Sort');
1221      chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
1222      break;
1223    case 'rename-folder-command':
1224      editSelectedItem();
1225      break;
1226    case 'edit-command':
1227      recordUserAction('Edit');
1228      editSelectedItem();
1229      break;
1230    case 'new-folder-command':
1231      recordUserAction('NewFolder');
1232      newFolder();
1233      break;
1234    case 'add-new-bookmark-command':
1235      recordUserAction('AddPage');
1236      addPage();
1237      break;
1238    case 'open-in-same-window-command':
1239      recordUserAction('OpenInSame');
1240      openItem();
1241      break;
1242    case 'undo-delete-command':
1243      recordUserAction('UndoDelete');
1244      undoDelete();
1245      break;
1246  }
1247}
1248
1249// Execute the copy, cut and paste commands when those events are dispatched by
1250// the browser. This allows us to rely on the browser to handle the keyboard
1251// shortcuts for these commands.
1252function installEventHandlerForCommand(eventName, commandId) {
1253  function handle(e) {
1254    if (document.activeElement != list && document.activeElement != tree)
1255      return;
1256    var command = $(commandId);
1257    if (!command.disabled) {
1258      command.execute();
1259      if (e)
1260        e.preventDefault();  // Prevent the system beep.
1261    }
1262  }
1263  if (eventName == 'paste') {
1264    // Paste is a bit special since we need to do an async call to see if we
1265    // can paste because the paste command might not be up to date.
1266    document.addEventListener(eventName, function(e) {
1267      updatePasteCommand(handle);
1268    });
1269  } else {
1270    document.addEventListener(eventName, handle);
1271  }
1272}
1273
1274function initializeSplitter() {
1275  var splitter = document.querySelector('.main > .splitter');
1276  Splitter.decorate(splitter);
1277
1278  // The splitter persists the size of the left component in the local store.
1279  if ('treeWidth' in localStorage)
1280    splitter.previousElementSibling.style.width = localStorage['treeWidth'];
1281
1282  splitter.addEventListener('resize', function(e) {
1283    localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
1284  });
1285}
1286
1287function initializeBookmarkManager() {
1288  // Sometimes the extension API is not initialized.
1289  if (!chrome.bookmarks)
1290    console.error('Bookmarks extension API is not available');
1291
1292  chrome.bookmarkManagerPrivate.getStrings(continueInitializeBookmarkManager);
1293}
1294
1295function continueInitializeBookmarkManager(localizedStrings) {
1296  loadLocalizedStrings(localizedStrings);
1297
1298  bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1299
1300  cr.ui.decorate('menu', Menu);
1301  cr.ui.decorate('button[menu]', MenuButton);
1302  cr.ui.decorate('command', Command);
1303  BookmarkList.decorate(list);
1304  BookmarkTree.decorate(tree);
1305
1306  list.addEventListener('canceledit', handleCancelEdit);
1307  list.addEventListener('canExecute', handleCanExecuteForList);
1308  list.addEventListener('change', updateAllCommands);
1309  list.addEventListener('contextmenu', updateEditingCommands);
1310  list.addEventListener('dblclick', handleDoubleClickForList);
1311  list.addEventListener('edit', handleEdit);
1312  list.addEventListener('rename', handleRename);
1313  list.addEventListener('urlClicked', handleUrlClickedForList);
1314
1315  tree.addEventListener('canExecute', handleCanExecuteForTree);
1316  tree.addEventListener('change', handleChangeForTree);
1317  tree.addEventListener('contextmenu', updateEditingCommands);
1318  tree.addEventListener('rename', handleRename);
1319  tree.addEventListener('load', handleLoadForTree);
1320
1321  cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1322  list.contextMenu = $('context-menu');
1323  tree.contextMenu = $('context-menu');
1324
1325  // We listen to hashchange so that we can update the currently shown folder
1326  // when // the user goes back and forward in the history.
1327  window.addEventListener('hashchange', processHash);
1328
1329  document.querySelector('.header form').onsubmit = function(e) {
1330    setSearch($('term').value);
1331    e.preventDefault();
1332  };
1333
1334  $('term').addEventListener('search', handleSearch);
1335
1336  document.querySelector('.summary > button').addEventListener(
1337      'click', handleOrganizeButtonClick);
1338
1339  document.querySelector('button.logo').addEventListener(
1340      'click', handleClickOnLogoButton);
1341
1342  document.addEventListener('canExecute', handleCanExecuteForDocument);
1343  document.addEventListener('command', handleCommand);
1344
1345  // Listen to copy, cut and paste events and execute the associated commands.
1346  installEventHandlerForCommand('copy', 'copy-command');
1347  installEventHandlerForCommand('cut', 'cut-command');
1348  installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1349
1350  // Install shortcuts
1351  for (var name in commandShortcutMap) {
1352    $(name + '-command').shortcut = commandShortcutMap[name];
1353  }
1354
1355  // Disable almost all commands at startup.
1356  var commands = document.querySelectorAll('command');
1357  for (var i = 0, command; command = commands[i]; ++i) {
1358    if (command.id != 'import-menu-command' &&
1359        command.id != 'export-menu-command') {
1360      command.disabled = true;
1361    }
1362  }
1363
1364  chrome.bookmarkManagerPrivate.canEdit(function(result) {
1365    canEdit = result;
1366  });
1367
1368  chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1369    // TODO(rustema): propagate policy value to the bookmark manager when it
1370    // changes.
1371    incognitoModeAvailability = result;
1372  });
1373
1374  chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1375    canOpenNewWindows = result;
1376  });
1377
1378  cr.ui.FocusOutlineManager.forDocument(document);
1379  initializeSplitter();
1380  bmm.addBookmarkModelListeners();
1381  dnd.init(selectItemsAfterUserAction);
1382  tree.reload();
1383}
1384
1385initializeBookmarkManager();
1386})();
1387