• 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/**
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 */
293function getFolder(parentId) {
294  var parentNode = 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  }
302}
303
304function handleLoadForTree(e) {
305  processHash();
306}
307
308/**
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 */
314function getAllUrls(nodes) {
315  var urls = [];
316
317  // Adds the node and all its direct children.
318  function addNodes(node) {
319    if (node.id == 'new')
320      return;
321
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  }
331
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 Promise.resolve(node);
338  });
339
340  return Promise.all(promises).then(function(nodes) {
341    nodes.forEach(addNodes);
342    return urls;
343  });
344}
345
346/**
347 * Returns the nodes (non recursive) to use for the open commands.
348 * @param {HTMLElement} target .
349 * @return {Array.<BookmarkTreeNode>} .
350 */
351function getNodesForOpen(target) {
352  if (target == tree) {
353    if (tree.selectedItem != searchTreeItem)
354      return tree.selectedFolders;
355    // Fall through to use all nodes in the list.
356  } else {
357    var items = list.selectedItems;
358    if (items.length)
359      return items;
360  }
361
362  // The list starts off with a null dataModel. We can get here during startup.
363  if (!list.dataModel)
364    return [];
365
366  // Return an array based on the dataModel.
367  return list.dataModel.slice();
368}
369
370/**
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 */
376function getUrlsForOpenCommands(target) {
377  return getAllUrls(getNodesForOpen(target));
378}
379
380function notNewNode(node) {
381  return node.id != 'new';
382}
383
384/**
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 */
395function 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  }
404
405  if (commandDisabled) {
406    command.disabled = true;
407    e.canExecute = false;
408    return;
409  }
410
411  getUrlsForOpenCommands(e.target).then(function(urls) {
412    var disabled = !urls.length;
413    command.disabled = disabled;
414    e.canExecute = !disabled;
415  });
416}
417
418/**
419 * Calls the backend to figure out if we can paste the clipboard into the active
420 * folder.
421 * @param {Function=} opt_f Function to call after the state has been updated.
422 */
423function updatePasteCommand(opt_f) {
424  function update(canPaste) {
425    var organizeMenuCommand = $('paste-from-organize-menu-command');
426    var contextMenuCommand = $('paste-from-context-menu-command');
427    organizeMenuCommand.disabled = !canPaste;
428    contextMenuCommand.disabled = !canPaste;
429    if (opt_f)
430      opt_f();
431  }
432  // We cannot paste into search view.
433  if (list.isSearch())
434    update(false);
435  else
436    chrome.bookmarkManagerPrivate.canPaste(list.parentId, update);
437}
438
439function handleCanExecuteForDocument(e) {
440  var command = e.command;
441  switch (command.id) {
442    case 'import-menu-command':
443      e.canExecute = canEdit;
444      break;
445    case 'export-menu-command':
446      // We can always execute the export-menu command.
447      e.canExecute = true;
448      break;
449    case 'sort-command':
450      e.canExecute = !list.isSearch() &&
451          list.dataModel && list.dataModel.length > 1 &&
452          !isUnmodifiable(tree.getBookmarkNodeById(list.parentId));
453      break;
454    case 'undo-command':
455      // If the search box is active, pass the undo command through
456      // (fixes http://crbug.com/278112). Otherwise, because
457      // the global undo command has no visible UI, always enable it, and
458      // just make it a no-op if undo is not possible.
459      e.canExecute = e.currentTarget.activeElement !== $('term');
460      break;
461    default:
462      canExecuteForList(e);
463      break;
464  }
465}
466
467/**
468 * Helper function for handling canExecute for the list and the tree.
469 * @param {!Event} e Can execute event object.
470 * @param {boolean} isSearch Whether the user is trying to do a command on
471 *     search.
472 */
473function canExecuteShared(e, isSearch) {
474  var command = e.command;
475  var commandId = command.id;
476  switch (commandId) {
477    case 'paste-from-organize-menu-command':
478    case 'paste-from-context-menu-command':
479      updatePasteCommand();
480      break;
481
482    case 'add-new-bookmark-command':
483    case 'new-folder-command':
484      var parentId = computeParentFolderForNewItem();
485      var unmodifiable = isUnmodifiable(tree.getBookmarkNodeById(parentId));
486      e.canExecute = !isSearch && canEdit && !unmodifiable;
487      break;
488
489    case 'open-in-new-tab-command':
490      updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
491      break;
492    case 'open-in-background-tab-command':
493      updateOpenCommand(e, command, '', '', false);
494      break;
495    case 'open-in-new-window-command':
496      updateOpenCommand(e, command,
497          'open_in_new_window', 'open_all_new_window',
498          // Disabled when incognito is forced.
499          incognitoModeAvailability == 'forced' || !canOpenNewWindows);
500      break;
501    case 'open-incognito-window-command':
502      updateOpenCommand(e, command,
503          'open_incognito', 'open_all_incognito',
504          // Not available when incognito is disabled.
505          incognitoModeAvailability == 'disabled');
506      break;
507
508    case 'undo-delete-command':
509      e.canExecute = !!lastDeletedNodes;
510      break;
511  }
512}
513
514/**
515 * Helper function for handling canExecute for the list and document.
516 * @param {!Event} e Can execute event object.
517 */
518function canExecuteForList(e) {
519  var command = e.command;
520  var commandId = command.id;
521
522  function hasSelected() {
523    return !!list.selectedItem;
524  }
525
526  function hasSingleSelected() {
527    return list.selectedItems.length == 1;
528  }
529
530  function canCopyItem(item) {
531    return item.id != 'new';
532  }
533
534  function canCopyItems() {
535    var selectedItems = list.selectedItems;
536    return selectedItems && selectedItems.some(canCopyItem);
537  }
538
539  function isSearch() {
540    return list.isSearch();
541  }
542
543  switch (commandId) {
544    case 'rename-folder-command':
545      // Show rename if a single folder is selected.
546      var items = list.selectedItems;
547      if (items.length != 1) {
548        e.canExecute = false;
549        command.hidden = true;
550      } else {
551        var isFolder = bmm.isFolder(items[0]);
552        e.canExecute = isFolder && canEdit && !hasUnmodifiable(items);
553        command.hidden = !isFolder;
554      }
555      break;
556
557    case 'edit-command':
558      // Show the edit command if not a folder.
559      var items = list.selectedItems;
560      if (items.length != 1) {
561        e.canExecute = false;
562        command.hidden = false;
563      } else {
564        var isFolder = bmm.isFolder(items[0]);
565        e.canExecute = !isFolder && canEdit && !hasUnmodifiable(items);
566        command.hidden = isFolder;
567      }
568      break;
569
570    case 'show-in-folder-command':
571      e.canExecute = isSearch() && hasSingleSelected();
572      break;
573
574    case 'delete-command':
575    case 'cut-command':
576      e.canExecute = canCopyItems() && canEdit &&
577          !hasUnmodifiable(list.selectedItems);
578      break;
579
580    case 'copy-command':
581      e.canExecute = canCopyItems();
582      break;
583
584    case 'open-in-same-window-command':
585      e.canExecute = hasSelected();
586      break;
587
588    default:
589      canExecuteShared(e, isSearch());
590  }
591}
592
593// Update canExecute for the commands when the list is the active element.
594function handleCanExecuteForList(e) {
595  if (e.target != list) return;
596  canExecuteForList(e);
597}
598
599// Update canExecute for the commands when the tree is the active element.
600function handleCanExecuteForTree(e) {
601  if (e.target != tree) return;
602
603  var command = e.command;
604  var commandId = command.id;
605
606  function hasSelected() {
607    return !!e.target.selectedItem;
608  }
609
610  function isSearch() {
611    var item = e.target.selectedItem;
612    return item == searchTreeItem;
613  }
614
615  function isTopLevelItem() {
616    return e.target.selectedItem.parentNode == tree;
617  }
618
619  switch (commandId) {
620    case 'rename-folder-command':
621      command.hidden = false;
622      e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
623          !hasUnmodifiable(tree.selectedFolders);
624      break;
625
626    case 'edit-command':
627      command.hidden = true;
628      e.canExecute = false;
629      break;
630
631    case 'delete-command':
632    case 'cut-command':
633      e.canExecute = hasSelected() && !isTopLevelItem() && canEdit &&
634          !hasUnmodifiable(tree.selectedFolders);
635      break;
636
637    case 'copy-command':
638      e.canExecute = hasSelected() && !isTopLevelItem();
639      break;
640
641    default:
642      canExecuteShared(e, isSearch());
643  }
644}
645
646/**
647 * Update the canExecute state of all the commands.
648 */
649function updateAllCommands() {
650  var commands = document.querySelectorAll('command');
651  for (var i = 0; i < commands.length; i++) {
652    commands[i].canExecuteChange();
653  }
654}
655
656function updateEditingCommands() {
657  var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
658      'add-new-bookmark', 'new-folder', 'sort',
659      'paste-from-context-menu', 'paste-from-organize-menu'];
660
661  chrome.bookmarkManagerPrivate.canEdit(function(result) {
662    if (result != canEdit) {
663      canEdit = result;
664      editingCommands.forEach(function(baseId) {
665        $(baseId + '-command').canExecuteChange();
666      });
667    }
668  });
669}
670
671function handleChangeForTree(e) {
672  navigateTo(tree.selectedItem.bookmarkId);
673}
674
675function handleOrganizeButtonClick(e) {
676  updateEditingCommands();
677  $('add-new-bookmark-command').canExecuteChange();
678  $('new-folder-command').canExecuteChange();
679  $('sort-command').canExecuteChange();
680}
681
682function handleRename(e) {
683  var item = e.target;
684  var bookmarkNode = item.bookmarkNode;
685  chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
686  performGlobalUndo = null;  // This can't be undone, so disable global undo.
687}
688
689function handleEdit(e) {
690  var item = e.target;
691  var bookmarkNode = item.bookmarkNode;
692  var context = {
693    title: bookmarkNode.title
694  };
695  if (!bmm.isFolder(bookmarkNode))
696    context.url = bookmarkNode.url;
697
698  if (bookmarkNode.id == 'new') {
699    selectItemsAfterUserAction(list);
700
701    // New page
702    context.parentId = bookmarkNode.parentId;
703    chrome.bookmarks.create(context, function(node) {
704      // A new node was created and will get added to the list due to the
705      // handler.
706      var dataModel = list.dataModel;
707      var index = dataModel.indexOf(bookmarkNode);
708      dataModel.splice(index, 1);
709
710      // Select new item.
711      var newIndex = dataModel.findIndexById(node.id);
712      if (newIndex != -1) {
713        var sm = list.selectionModel;
714        list.scrollIndexIntoView(newIndex);
715        sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
716      }
717    });
718  } else {
719    // Edit
720    chrome.bookmarks.update(bookmarkNode.id, context);
721  }
722  performGlobalUndo = null;  // This can't be undone, so disable global undo.
723}
724
725function handleCancelEdit(e) {
726  var item = e.target;
727  var bookmarkNode = item.bookmarkNode;
728  if (bookmarkNode.id == 'new') {
729    var dataModel = list.dataModel;
730    var index = dataModel.findIndexById('new');
731    dataModel.splice(index, 1);
732  }
733}
734
735/**
736 * Navigates to the folder that the selected item is in and selects it. This is
737 * used for the show-in-folder command.
738 */
739function showInFolder() {
740  var bookmarkNode = list.selectedItem;
741  if (!bookmarkNode)
742    return;
743  var parentId = bookmarkNode.parentId;
744
745  // After the list is loaded we should select the revealed item.
746  function selectItem() {
747    var index = list.dataModel.findIndexById(bookmarkNode.id);
748    if (index == -1)
749      return;
750    var sm = list.selectionModel;
751    sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
752    list.scrollIndexIntoView(index);
753  }
754
755  var treeItem = bmm.treeLookup[parentId];
756  treeItem.reveal();
757
758  navigateTo(parentId, selectItem);
759}
760
761/**
762 * @return {!cr.LinkController} The link controller used to open links based on
763 *     user clicks and keyboard actions.
764 */
765function getLinkController() {
766  return linkController ||
767      (linkController = new cr.LinkController(loadTimeData));
768}
769
770/**
771 * Returns the selected bookmark nodes of the provided tree or list.
772 * If |opt_target| is not provided or null the active element is used.
773 * Only call this if the list or the tree is focused.
774 * @param {BookmarkList|BookmarkTree} opt_target The target list or tree.
775 * @return {!Array} Array of bookmark nodes.
776 */
777function getSelectedBookmarkNodes(opt_target) {
778  return (opt_target || document.activeElement) == tree ?
779      tree.selectedFolders : list.selectedItems;
780}
781
782/**
783 * @return {!Array.<string>} An array of the selected bookmark IDs.
784 */
785function getSelectedBookmarkIds() {
786  var selectedNodes = getSelectedBookmarkNodes();
787  selectedNodes.sort(function(a, b) { return a.index - b.index });
788  return selectedNodes.map(function(node) {
789    return node.id;
790  });
791}
792
793/**
794 * @param {BookmarkTreeNode} node The node to test.
795 * @return {boolean} Whether the given node is unmodifiable.
796 */
797function isUnmodifiable(node) {
798  return node && node.unmodifiable;
799}
800
801/**
802 * @param {BookmarkList} A list of BookmarkNodes.
803 * @return {boolean} Whether any of the nodes is managed.
804 */
805function hasUnmodifiable(nodes) {
806  return nodes.some(isUnmodifiable);
807}
808
809/**
810 * Opens the selected bookmarks.
811 * @param {LinkKind} kind The kind of link we want to open.
812 * @param {HTMLElement} opt_eventTarget The target of the user initiated event.
813 */
814function openBookmarks(kind, opt_eventTarget) {
815  // If we have selected any folders, we need to find all the bookmarks one
816  // level down. We use multiple async calls to getSubtree instead of getting
817  // the whole tree since we would like to minimize the amount of data sent.
818
819  var urlsP = getUrlsForOpenCommands(opt_eventTarget);
820  urlsP.then(function(urls) {
821    getLinkController().openUrls(urls, kind);
822    chrome.bookmarkManagerPrivate.recordLaunch();
823  });
824}
825
826/**
827 * Opens an item in the list.
828 */
829function openItem() {
830  var bookmarkNodes = getSelectedBookmarkNodes();
831  // If we double clicked or pressed enter on a single folder, navigate to it.
832  if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0]))
833    navigateTo(bookmarkNodes[0].id);
834  else
835    openBookmarks(LinkKind.FOREGROUND_TAB);
836}
837
838/**
839 * Refreshes search results after delete or undo-delete.
840 * This ensures children of deleted folders do not remain in results
841 */
842function updateSearchResults() {
843  if (list.isSearch()) {
844    list.reload();
845  }
846}
847
848/**
849 * Deletes the selected bookmarks. The bookmarks are saved in memory in case
850 * the user needs to undo the deletion.
851 */
852function deleteBookmarks() {
853  var selectedIds = getSelectedBookmarkIds();
854  var filteredIds = getFilteredSelectedBookmarkIds();
855  lastDeletedNodes = [];
856
857  function performDelete() {
858    // Only remove filtered ids.
859    chrome.bookmarkManagerPrivate.removeTrees(filteredIds);
860    $('undo-delete-command').canExecuteChange();
861    performGlobalUndo = undoDelete;
862  }
863
864  // First, store information about the bookmarks being deleted.
865  // Store all selected ids.
866  selectedIds.forEach(function(id) {
867    chrome.bookmarks.getSubTree(id, function(results) {
868      lastDeletedNodes.push(results);
869
870      // When all nodes have been saved, perform the deletion.
871      if (lastDeletedNodes.length === selectedIds.length) {
872        performDelete();
873        updateSearchResults();
874      }
875    });
876  });
877}
878
879/**
880 * Restores a tree of bookmarks under a specified folder.
881 * @param {BookmarkTreeNode} node The node to restore.
882 * @param {=string} parentId The ID of the folder to restore under. If not
883 *     specified, the original parentId of the node will be used.
884 */
885function restoreTree(node, parentId) {
886  var bookmarkInfo = {
887    parentId: parentId || node.parentId,
888    title: node.title,
889    index: node.index,
890    url: node.url
891  };
892
893  chrome.bookmarks.create(bookmarkInfo, function(result) {
894    if (!result) {
895      console.error('Failed to restore bookmark.');
896      return;
897    }
898
899    if (node.children) {
900      // Restore the children using the new ID for this node.
901      node.children.forEach(function(child) {
902        restoreTree(child, result.id);
903      });
904    }
905
906    updateSearchResults();
907  });
908}
909
910/**
911 * Restores the last set of bookmarks that was deleted.
912 */
913function undoDelete() {
914  lastDeletedNodes.forEach(function(arr) {
915    arr.forEach(restoreTree);
916  });
917  lastDeletedNodes = null;
918  $('undo-delete-command').canExecuteChange();
919
920  // Only a single level of undo is supported, so disable global undo now.
921  performGlobalUndo = null;
922}
923
924/**
925 * Computes folder for "Add Page" and "Add Folder".
926 * @return {string} The id of folder node where we'll create new page/folder.
927 */
928function computeParentFolderForNewItem() {
929  if (document.activeElement == tree)
930    return list.parentId;
931  var selectedItem = list.selectedItem;
932  return selectedItem && bmm.isFolder(selectedItem) ?
933      selectedItem.id : list.parentId;
934}
935
936/**
937 * Callback for rename folder and edit command. This starts editing for
938 * selected item.
939 */
940function editSelectedItem() {
941  if (document.activeElement == tree) {
942    tree.selectedItem.editing = true;
943  } else {
944    var li = list.getListItem(list.selectedItem);
945    if (li)
946      li.editing = true;
947  }
948}
949
950/**
951 * Callback for the new folder command. This creates a new folder and starts
952 * a rename of it.
953 */
954function newFolder() {
955  performGlobalUndo = null;  // This can't be undone, so disable global undo.
956
957  var parentId = computeParentFolderForNewItem();
958
959  // Callback is called after tree and list data model updated.
960  function createFolder(callback) {
961    chrome.bookmarks.create({
962      title: loadTimeData.getString('new_folder_name'),
963      parentId: parentId
964    }, callback);
965  }
966
967  if (document.activeElement == tree) {
968    createFolder(function(newNode) {
969      navigateTo(newNode.id, function() {
970        bmm.treeLookup[newNode.id].editing = true;
971      });
972    });
973    return;
974  }
975
976  function editNewFolderInList() {
977    createFolder(function() {
978      var index = list.dataModel.length - 1;
979      var sm = list.selectionModel;
980      sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
981      scrollIntoViewAndMakeEditable(index);
982    });
983  }
984
985  navigateTo(parentId, editNewFolderInList);
986}
987
988/**
989 * Scrolls the list item into view and makes it editable.
990 * @param {number} index The index of the item to make editable.
991 */
992function scrollIntoViewAndMakeEditable(index) {
993  list.scrollIndexIntoView(index);
994  // onscroll is now dispatched asynchronously so we have to postpone
995  // the rest.
996  setTimeout(function() {
997    var item = list.getListItemByIndex(index);
998    if (item)
999      item.editing = true;
1000  });
1001}
1002
1003/**
1004 * Adds a page to the current folder. This is called by the
1005 * add-new-bookmark-command handler.
1006 */
1007function addPage() {
1008  var parentId = computeParentFolderForNewItem();
1009
1010  function editNewBookmark() {
1011    var fakeNode = {
1012      title: '',
1013      url: '',
1014      parentId: parentId,
1015      id: 'new'
1016    };
1017    var dataModel = list.dataModel;
1018    var length = dataModel.length;
1019    dataModel.splice(length, 0, fakeNode);
1020    var sm = list.selectionModel;
1021    sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
1022    scrollIntoViewAndMakeEditable(length);
1023  };
1024
1025  navigateTo(parentId, editNewBookmark);
1026}
1027
1028/**
1029 * This function is used to select items after a user action such as paste, drop
1030 * add page etc.
1031 * @param {BookmarkList|BookmarkTree} target The target of the user action.
1032 * @param {=string} opt_selectedTreeId If provided, then select that tree id.
1033 */
1034function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1035  // We get one onCreated event per item so we delay the handling until we get
1036  // no more events coming.
1037
1038  var ids = [];
1039  var timer;
1040
1041  function handle(id, bookmarkNode) {
1042    clearTimeout(timer);
1043    if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
1044      ids.push(id);
1045    timer = setTimeout(handleTimeout, 50);
1046  }
1047
1048  function handleTimeout() {
1049    chrome.bookmarks.onCreated.removeListener(handle);
1050    chrome.bookmarks.onMoved.removeListener(handle);
1051
1052    if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1053      var index = ids.indexOf(opt_selectedTreeId);
1054      if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1055        tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1056      }
1057    } else if (target == list) {
1058      var dataModel = list.dataModel;
1059      var firstIndex = dataModel.findIndexById(ids[0]);
1060      var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1061      if (firstIndex != -1 && lastIndex != -1) {
1062        var selectionModel = list.selectionModel;
1063        selectionModel.selectedIndex = -1;
1064        selectionModel.selectRange(firstIndex, lastIndex);
1065        selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1066        list.focus();
1067      }
1068    }
1069
1070    list.endBatchUpdates();
1071  }
1072
1073  list.startBatchUpdates();
1074
1075  chrome.bookmarks.onCreated.addListener(handle);
1076  chrome.bookmarks.onMoved.addListener(handle);
1077  timer = setTimeout(handleTimeout, 300);
1078}
1079
1080/**
1081 * Record user action.
1082 * @param {string} name An user action name.
1083 */
1084function recordUserAction(name) {
1085  chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1086}
1087
1088/**
1089 * The currently selected bookmark, based on where the user is clicking.
1090 * @return {string} The ID of the currently selected bookmark (could be from
1091 *     tree view or list view).
1092 */
1093function getSelectedId() {
1094  if (document.activeElement == tree)
1095    return tree.selectedItem.bookmarkId;
1096  var selectedItem = list.selectedItem;
1097  return selectedItem && bmm.isFolder(selectedItem) ?
1098      selectedItem.id : tree.selectedItem.bookmarkId;
1099}
1100
1101/**
1102 * Pastes the copied/cutted bookmark into the right location depending whether
1103 * if it was called from Organize Menu or from Context Menu.
1104 * @param {string} id The id of the element being pasted from.
1105 */
1106function pasteBookmark(id) {
1107  recordUserAction('Paste');
1108  selectItemsAfterUserAction(list);
1109  chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1110}
1111
1112/**
1113 * Returns true if child is contained in another selected folder.
1114 * Traces parent nodes up the tree until a selected ancestor or root is found.
1115 */
1116function hasSelectedAncestor(parentNode) {
1117  function contains(arr, item) {
1118    for (var i = 0; i < arr.length; i++)
1119        if (arr[i] === item)
1120          return true;
1121    return false;
1122  }
1123
1124  // Don't search top level, cannot select permanent nodes in search.
1125  if (parentNode == null || parentNode.id <= 2)
1126    return false;
1127
1128  // Found selected ancestor.
1129  if (contains(getSelectedBookmarkNodes(), parentNode))
1130    return true;
1131
1132  // Keep digging.
1133  return hasSelectedAncestor(tree.getBookmarkNodeById(parentNode.parentId));
1134}
1135
1136function getFilteredSelectedBookmarkIds() {
1137  // Remove duplicates from filteredIds and return.
1138  var filteredIds = new Array();
1139  // Selected nodes to iterate through for matches.
1140  var nodes = getSelectedBookmarkNodes();
1141
1142  for (var i = 0; i < nodes.length; i++)
1143    if (!hasSelectedAncestor(tree.getBookmarkNodeById(nodes[i].parentId)))
1144      filteredIds.splice(0, 0, nodes[i].id);
1145
1146  return filteredIds;
1147}
1148
1149/**
1150 * Handler for the command event. This is used for context menu of list/tree
1151 * and organized menu.
1152 * @param {!Event} e The event object.
1153 */
1154function handleCommand(e) {
1155  var command = e.command;
1156  var commandId = command.id;
1157  switch (commandId) {
1158    case 'import-menu-command':
1159      recordUserAction('Import');
1160      chrome.bookmarks.import();
1161      break;
1162    case 'export-menu-command':
1163      recordUserAction('Export');
1164      chrome.bookmarks.export();
1165      break;
1166    case 'undo-command':
1167      if (performGlobalUndo) {
1168        recordUserAction('UndoGlobal');
1169        performGlobalUndo();
1170      } else {
1171        recordUserAction('UndoNone');
1172      }
1173      break;
1174    case 'show-in-folder-command':
1175      recordUserAction('ShowInFolder');
1176      showInFolder();
1177      break;
1178    case 'open-in-new-tab-command':
1179    case 'open-in-background-tab-command':
1180      recordUserAction('OpenInNewTab');
1181      openBookmarks(LinkKind.BACKGROUND_TAB, e.target);
1182      break;
1183    case 'open-in-new-window-command':
1184      recordUserAction('OpenInNewWindow');
1185      openBookmarks(LinkKind.WINDOW, e.target);
1186      break;
1187    case 'open-incognito-window-command':
1188      recordUserAction('OpenIncognito');
1189      openBookmarks(LinkKind.INCOGNITO, e.target);
1190      break;
1191    case 'delete-command':
1192      recordUserAction('Delete');
1193      deleteBookmarks();
1194      break;
1195    case 'copy-command':
1196      recordUserAction('Copy');
1197      chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
1198                                         updatePasteCommand);
1199      break;
1200    case 'cut-command':
1201      recordUserAction('Cut');
1202      chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
1203                                        function() {
1204                                          updatePasteCommand();
1205                                          updateSearchResults();
1206                                        });
1207      break;
1208    case 'paste-from-organize-menu-command':
1209      pasteBookmark(list.parentId);
1210      break;
1211    case 'paste-from-context-menu-command':
1212      pasteBookmark(getSelectedId());
1213      break;
1214    case 'sort-command':
1215      recordUserAction('Sort');
1216      chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
1217      break;
1218    case 'rename-folder-command':
1219      editSelectedItem();
1220      break;
1221    case 'edit-command':
1222      recordUserAction('Edit');
1223      editSelectedItem();
1224      break;
1225    case 'new-folder-command':
1226      recordUserAction('NewFolder');
1227      newFolder();
1228      break;
1229    case 'add-new-bookmark-command':
1230      recordUserAction('AddPage');
1231      addPage();
1232      break;
1233    case 'open-in-same-window-command':
1234      recordUserAction('OpenInSame');
1235      openItem();
1236      break;
1237    case 'undo-delete-command':
1238      recordUserAction('UndoDelete');
1239      undoDelete();
1240      break;
1241  }
1242}
1243
1244// Execute the copy, cut and paste commands when those events are dispatched by
1245// the browser. This allows us to rely on the browser to handle the keyboard
1246// shortcuts for these commands.
1247function installEventHandlerForCommand(eventName, commandId) {
1248  function handle(e) {
1249    if (document.activeElement != list && document.activeElement != tree)
1250      return;
1251    var command = $(commandId);
1252    if (!command.disabled) {
1253      command.execute();
1254      if (e)
1255        e.preventDefault();  // Prevent the system beep.
1256    }
1257  }
1258  if (eventName == 'paste') {
1259    // Paste is a bit special since we need to do an async call to see if we
1260    // can paste because the paste command might not be up to date.
1261    document.addEventListener(eventName, function(e) {
1262      updatePasteCommand(handle);
1263    });
1264  } else {
1265    document.addEventListener(eventName, handle);
1266  }
1267}
1268
1269function initializeSplitter() {
1270  var splitter = document.querySelector('.main > .splitter');
1271  Splitter.decorate(splitter);
1272
1273  // The splitter persists the size of the left component in the local store.
1274  if ('treeWidth' in localStorage)
1275    splitter.previousElementSibling.style.width = localStorage['treeWidth'];
1276
1277  splitter.addEventListener('resize', function(e) {
1278    localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
1279  });
1280}
1281
1282function initializeBookmarkManager() {
1283  // Sometimes the extension API is not initialized.
1284  if (!chrome.bookmarks)
1285    console.error('Bookmarks extension API is not available');
1286
1287  chrome.bookmarkManagerPrivate.getStrings(continueInitializeBookmarkManager);
1288}
1289
1290function continueInitializeBookmarkManager(localizedStrings) {
1291  loadLocalizedStrings(localizedStrings);
1292
1293  bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1294
1295  cr.ui.decorate('menu', Menu);
1296  cr.ui.decorate('button[menu]', MenuButton);
1297  cr.ui.decorate('command', Command);
1298  BookmarkList.decorate(list);
1299  BookmarkTree.decorate(tree);
1300
1301  list.addEventListener('canceledit', handleCancelEdit);
1302  list.addEventListener('canExecute', handleCanExecuteForList);
1303  list.addEventListener('change', updateAllCommands);
1304  list.addEventListener('contextmenu', updateEditingCommands);
1305  list.addEventListener('dblclick', handleDoubleClickForList);
1306  list.addEventListener('edit', handleEdit);
1307  list.addEventListener('rename', handleRename);
1308  list.addEventListener('urlClicked', handleUrlClickedForList);
1309
1310  tree.addEventListener('canExecute', handleCanExecuteForTree);
1311  tree.addEventListener('change', handleChangeForTree);
1312  tree.addEventListener('contextmenu', updateEditingCommands);
1313  tree.addEventListener('rename', handleRename);
1314  tree.addEventListener('load', handleLoadForTree);
1315
1316  cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1317  list.contextMenu = $('context-menu');
1318  tree.contextMenu = $('context-menu');
1319
1320  // We listen to hashchange so that we can update the currently shown folder
1321  // when // the user goes back and forward in the history.
1322  window.addEventListener('hashchange', processHash);
1323
1324  document.querySelector('header form').onsubmit = function(e) {
1325    setSearch($('term').value);
1326    e.preventDefault();
1327  };
1328
1329  $('term').addEventListener('search', handleSearch);
1330
1331  document.querySelector('.summary button').addEventListener(
1332      'click', handleOrganizeButtonClick);
1333
1334  document.addEventListener('canExecute', handleCanExecuteForDocument);
1335  document.addEventListener('command', handleCommand);
1336
1337  // Listen to copy, cut and paste events and execute the associated commands.
1338  installEventHandlerForCommand('copy', 'copy-command');
1339  installEventHandlerForCommand('cut', 'cut-command');
1340  installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1341
1342  // Install shortcuts
1343  for (var name in commandShortcutMap) {
1344    $(name + '-command').shortcut = commandShortcutMap[name];
1345  }
1346
1347  // Disable almost all commands at startup.
1348  var commands = document.querySelectorAll('command');
1349  for (var i = 0, command; command = commands[i]; ++i) {
1350    if (command.id != 'import-menu-command' &&
1351        command.id != 'export-menu-command') {
1352      command.disabled = true;
1353    }
1354  }
1355
1356  chrome.bookmarkManagerPrivate.canEdit(function(result) {
1357    canEdit = result;
1358  });
1359
1360  chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1361    // TODO(rustema): propagate policy value to the bookmark manager when it
1362    // changes.
1363    incognitoModeAvailability = result;
1364  });
1365
1366  chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1367    canOpenNewWindows = result;
1368  });
1369
1370  cr.ui.FocusOutlineManager.forDocument(document);
1371  initializeSplitter();
1372  bmm.addBookmarkModelListeners();
1373  dnd.init(selectItemsAfterUserAction);
1374  tree.reload();
1375}
1376
1377initializeBookmarkManager();
1378})();
1379