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 Promise = cr.Promise;
15/** @const */ var Splitter = cr.ui.Splitter;
16/** @const */ var TreeItem = cr.ui.TreeItem;
17
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 */
23var lastDeletedNodes;
24
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 */
31var lastHoverOnFolderTimeStamp = 0;
32
33/**
34 * Holds a function that will undo that last action, if global undo is enabled.
35 * @type {Function}
36 */
37var performGlobalUndo;
38
39/**
40 * Holds a link controller singleton. Use getLinkController() rarther than
41 * accessing this variabie.
42 * @type {LinkController}
43 */
44var linkController;
45
46/**
47 * New Windows are not allowed in Windows 8 metro mode.
48 */
49var canOpenNewWindows = true;
50
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 */
57var incognitoModeAvailability = 'enabled';
58
59/**
60 * Whether bookmarks can be modified.
61 * @type {boolean}
62 */
63var canEdit = true;
64
65/**
66 * @type {TreeItem}
67 * @const
68 */
69var searchTreeItem = new TreeItem({
70  bookmarkId: 'q='
71});
72
73/**
74 * Command shortcut mapping.
75 * @const
76 */
77var 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',
98};
99
100/**
101 * Mapping for folder id to suffix of UMA. These names will be appeared
102 * after "BookmarkManager_NavigateTo_" in UMA dashboard.
103 * @const
104 */
105var folderMetricsNameMap = {
106  '1': 'BookmarkBar',
107  '2': 'Other',
108  '3': 'Mobile',
109  'q=': 'Search',
110  'subfolder': 'SubFolder',
111};
112
113/**
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 */
119function addOneShotEventListener(node, name, handler) {
120  var f = function(e) {
121    handler(e);
122    node.removeEventListener(name, f);
123  };
124  node.addEventListener(name, f);
125}
126
127// Get the localized strings from the backend via bookmakrManagerPrivate API.
128function 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  }
133
134  loadTimeData.data = data;
135  i18nTemplate.process(document, loadTimeData);
136
137  searchTreeItem.label = loadTimeData.getString('search');
138  searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' :
139                                  'images/bookmark_manager_search.png';
140}
141
142/**
143 * Updates the location hash to reflect the current state of the application.
144 */
145function updateHash() {
146  window.location.hash = tree.selectedItem.bookmarkId;
147}
148
149/**
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 */
155function navigateTo(id, callback) {
156  if (list.parentId == id) {
157    callback();
158    return;
159  }
160
161  var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
162                  folderMetricsNameMap['subfolder'];
163  chrome.metricsPrivate.recordUserAction(
164      'BookmarkManager_NavigateTo_' + metricsId);
165
166  addOneShotEventListener(list, 'load', callback);
167  updateParentId(id);
168}
169
170/**
171 * Updates the parent ID of the bookmark list and selects the correct tree item.
172 * @param {string} id The id.
173 */
174function updateParentId(id) {
175  // Setting list.parentId fires 'load' event.
176  list.parentId = id;
177
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;
181}
182
183// Process the location hash. This is called by onhashchange and when the page
184// is first loaded.
185function 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  }
191
192  var valid = false;
193  if (/^e=/.test(id)) {
194    id = id.slice(2);
195
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];
202
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      };
212
213      navigateTo(bookmarkNode.parentId, editBookmark);
214    });
215
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  }
225
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  }
236}
237
238// Activate is handled by the open-in-same-window-command.
239function handleDoubleClickForList(e) {
240  if (e.button == 0)
241    $('open-in-same-window-command').execute();
242}
243
244// The list dispatches an event when the user clicks on the URL or the Show in
245// folder part.
246function handleUrlClickedForList(e) {
247  getLinkController().openUrlFromEvent(e.url, e.originalEvent);
248  chrome.bookmarkManagerPrivate.recordLaunch();
249}
250
251function handleSearch(e) {
252  setSearch(this.value);
253}
254
255/**
256 * Navigates to the search results for the search text.
257 * @param {string} searchText The text to search for.
258 */
259function 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  }
267
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;
272
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  }
281
282  // Navigate now and update hash immediately.
283  navigateTo(id, updateHash);
284}
285
286// Handle the logo button UI.
287// When the user clicks the button we should navigate "home" and focus the list.
288function handleClickOnLogoButton(e) {
289  setSearch('');
290  $('list').focus();
291}
292
293/**
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 */
299function 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  }
308}
309
310function handleLoadForTree(e) {
311  processHash();
312}
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 new Promise(node);
338  });
339
340  var urlsPromise = new Promise();
341
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  });
349
350  return urlsPromise;
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    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();
366}
367
368/**
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 */
374function getUrlsForOpenCommands(target) {
375  return getAllUrls(getNodesForOpen(target));
376}
377
378function notNewNode(node) {
379  return node.id != 'new';
380}
381
382/**
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 */
393function 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  }
402
403  if (commandDisabled) {
404    command.disabled = true;
405    e.canExecute = false;
406    return;
407  }
408
409  getUrlsForOpenCommands(e.target).addListener(function(urls) {
410    var disabled = !urls.length;
411    command.disabled = disabled;
412    e.canExecute = !disabled;
413  });
414}
415
416/**
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 */
421function 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);
435}
436
437function 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  }
459}
460
461/**
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 */
467function 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;
475
476    case 'add-new-bookmark-command':
477    case 'new-folder-command':
478      e.canExecute = !isSearch && canEdit;
479      break;
480
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;
499
500    case 'undo-delete-command':
501      e.canExecute = !!lastDeletedNodes;
502      break;
503  }
504}
505
506/**
507 * Helper function for handling canExecute for the list and document.
508 * @param {!Event} e Can execute event object.
509 */
510function canExecuteForList(e) {
511  var command = e.command;
512  var commandId = command.id;
513
514  function hasSelected() {
515    return !!list.selectedItem;
516  }
517
518  function hasSingleSelected() {
519    return list.selectedItems.length == 1;
520  }
521
522  function canCopyItem(item) {
523    return item.id != 'new';
524  }
525
526  function canCopyItems() {
527    var selectedItems = list.selectedItems;
528    return selectedItems && selectedItems.some(canCopyItem);
529  }
530
531  function isSearch() {
532    return list.isSearch();
533  }
534
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;
548
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;
561
562    case 'show-in-folder-command':
563      e.canExecute = isSearch() && hasSingleSelected();
564      break;
565
566    case 'delete-command':
567    case 'cut-command':
568      e.canExecute = canCopyItems() && canEdit;
569      break;
570
571    case 'copy-command':
572      e.canExecute = canCopyItems();
573      break;
574
575    case 'open-in-same-window-command':
576      e.canExecute = hasSelected();
577      break;
578
579    default:
580      canExecuteShared(e, isSearch());
581  }
582}
583
584// Update canExecute for the commands when the list is the active element.
585function handleCanExecuteForList(e) {
586  if (e.target != list) return;
587  canExecuteForList(e);
588}
589
590// Update canExecute for the commands when the tree is the active element.
591function handleCanExecuteForTree(e) {
592  if (e.target != tree) return;
593
594  var command = e.command;
595  var commandId = command.id;
596
597  function hasSelected() {
598    return !!e.target.selectedItem;
599  }
600
601  function isSearch() {
602    var item = e.target.selectedItem;
603    return item == searchTreeItem;
604  }
605
606  function isTopLevelItem() {
607    return e.target.selectedItem.parentNode == tree;
608  }
609
610  switch (commandId) {
611    case 'rename-folder-command':
612      command.hidden = false;
613      e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
614      break;
615
616    case 'edit-command':
617      command.hidden = true;
618      e.canExecute = false;
619      break;
620
621    case 'delete-command':
622    case 'cut-command':
623      e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
624      break;
625
626    case 'copy-command':
627      e.canExecute = hasSelected() && !isTopLevelItem();
628      break;
629
630    default:
631      canExecuteShared(e, isSearch());
632  }
633}
634
635/**
636 * Update the canExecute state of the commands when the selection changes.
637 * @param {Event} e The change event object.
638 */
639function 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'];
646
647    if (e.target == tree) {
648      commandNames.push('paste-from-context-menu', 'paste-from-organize-menu',
649                        'sort');
650    }
651
652    commandNames.forEach(function(baseId) {
653      $(baseId + '-command').canExecuteChange();
654    });
655  }
656}
657
658function 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'];
662
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  });
671}
672
673function handleChangeForTree(e) {
674  updateCommandsBasedOnSelection(e);
675  navigateTo(tree.selectedItem.bookmarkId, updateHash);
676}
677
678function handleOrganizeButtonClick(e) {
679  updateEditingCommands();
680  $('add-new-bookmark-command').canExecuteChange();
681  $('new-folder-command').canExecuteChange();
682  $('sort-command').canExecuteChange();
683}
684
685function 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.
690}
691
692function 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;
700
701  if (bookmarkNode.id == 'new') {
702    selectItemsAfterUserAction(list);
703
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);
712
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.
726}
727
728function 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  }
736}
737
738/**
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 */
742function showInFolder() {
743  var bookmarkNode = list.selectedItem;
744  if (!bookmarkNode)
745    return;
746  var parentId = bookmarkNode.parentId;
747
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  }
757
758  var treeItem = bmm.treeLookup[parentId];
759  treeItem.reveal();
760
761  navigateTo(parentId, selectItem);
762}
763
764/**
765 * @return {!cr.LinkController} The link controller used to open links based on
766 *     user clicks and keyboard actions.
767 */
768function getLinkController() {
769  return linkController ||
770      (linkController = new cr.LinkController(loadTimeData));
771}
772
773/**
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 */
780function getSelectedBookmarkNodes(opt_target) {
781  return (opt_target || document.activeElement) == tree ?
782      tree.selectedFolders : list.selectedItems;
783}
784
785/**
786 * @return {!Array.<string>} An array of the selected bookmark IDs.
787 */
788function 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  });
794}
795
796/**
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 */
801function 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.
805
806  var urlsP = getUrlsForOpenCommands(opt_eventTarget);
807  urlsP.addListener(function(urls) {
808    getLinkController().openUrls(urls, kind);
809    chrome.bookmarkManagerPrivate.recordLaunch();
810  });
811}
812
813/**
814 * Opens an item in the list.
815 */
816function 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  }
824}
825
826/**
827 * Deletes the selected bookmarks. The bookmarks are saved in memory in case
828 * the user needs to undo the deletion.
829 */
830function deleteBookmarks() {
831  var selectedIds = getSelectedBookmarkIds();
832  lastDeletedNodes = [];
833
834  function performDelete() {
835    chrome.bookmarkManagerPrivate.removeTrees(selectedIds);
836    $('undo-delete-command').canExecuteChange();
837    performGlobalUndo = undoDelete;
838  }
839
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);
844
845      // When all nodes have been saved, perform the deletion.
846      if (lastDeletedNodes.length === selectedIds.length)
847        performDelete();
848    });
849  });
850}
851
852/**
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 */
858function restoreTree(node, parentId) {
859  var bookmarkInfo = {
860    parentId: parentId || node.parentId,
861    title: node.title,
862    index: node.index,
863    url: node.url
864  };
865
866  chrome.bookmarks.create(bookmarkInfo, function(result) {
867    if (!result) {
868      console.error('Failed to restore bookmark.');
869      return;
870    }
871
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  });
879}
880
881/**
882 * Restores the last set of bookmarks that was deleted.
883 */
884function undoDelete() {
885  lastDeletedNodes.forEach(function(arr) {
886    arr.forEach(restoreTree);
887  });
888  lastDeletedNodes = null;
889  $('undo-delete-command').canExecuteChange();
890
891  // Only a single level of undo is supported, so disable global undo now.
892  performGlobalUndo = null;
893}
894
895/**
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 */
899function 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;
905}
906
907/**
908 * Callback for rename folder and edit command. This starts editing for
909 * selected item.
910 */
911function 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  }
919}
920
921/**
922 * Callback for the new folder command. This creates a new folder and starts
923 * a rename of it.
924 */
925function newFolder() {
926  performGlobalUndo = null;  // This can't be undone, so disable global undo.
927
928  var parentId = computeParentFolderForNewItem();
929
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  }
937
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  }
946
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  }
955
956  navigateTo(parentId, editNewFolderInList);
957}
958
959/**
960 * Scrolls the list item into view and makes it editable.
961 * @param {number} index The index of the item to make editable.
962 */
963function 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  });
972}
973
974/**
975 * Adds a page to the current folder. This is called by the
976 * add-new-bookmark-command handler.
977 */
978function addPage() {
979  var parentId = computeParentFolderForNewItem();
980
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  };
995
996  navigateTo(parentId, editNewBookmark);
997}
998
999/**
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 */
1005function 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.
1008
1009  var ids = [];
1010  var timer;
1011
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  }
1018
1019  function handleTimeout() {
1020    chrome.bookmarks.onCreated.removeListener(handle);
1021    chrome.bookmarks.onMoved.removeListener(handle);
1022
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    }
1040
1041    list.endBatchUpdates();
1042  }
1043
1044  list.startBatchUpdates();
1045
1046  chrome.bookmarks.onCreated.addListener(handle);
1047  chrome.bookmarks.onMoved.addListener(handle);
1048  timer = setTimeout(handleTimeout, 300);
1049}
1050
1051/**
1052 * Record user action.
1053 * @param {string} name An user action name.
1054 */
1055function recordUserAction(name) {
1056  chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1057}
1058
1059/**
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 */
1064function 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;
1070}
1071
1072/**
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 */
1077function pasteBookmark(id) {
1078  recordUserAction('Paste');
1079  selectItemsAfterUserAction(list);
1080  chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1081}
1082
1083/**
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 */
1088function 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  }
1173}
1174
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.
1178function 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  }
1198}
1199
1200function initializeSplitter() {
1201  var splitter = document.querySelector('.main > .splitter');
1202  Splitter.decorate(splitter);
1203
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'];
1207
1208  splitter.addEventListener('resize', function(e) {
1209    localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
1210  });
1211}
1212
1213function initializeBookmarkManager() {
1214  // Sometimes the extension API is not initialized.
1215  if (!chrome.bookmarks)
1216    console.error('Bookmarks extension API is not available');
1217
1218  chrome.bookmarkManagerPrivate.getStrings(loadLocalizedStrings);
1219
1220  bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1221
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);
1227
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);
1236
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);
1242
1243  cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1244  list.contextMenu = $('context-menu');
1245  tree.contextMenu = $('context-menu');
1246
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);
1250
1251  document.querySelector('.header form').onsubmit = function(e) {
1252    setSearch($('term').value);
1253    e.preventDefault();
1254  };
1255
1256  $('term').addEventListener('search', handleSearch);
1257
1258  document.querySelector('.summary > button').addEventListener(
1259      'click', handleOrganizeButtonClick);
1260
1261  document.querySelector('button.logo').addEventListener(
1262      'click', handleClickOnLogoButton);
1263
1264  document.addEventListener('canExecute', handleCanExecuteForDocument);
1265  document.addEventListener('command', handleCommand);
1266
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');
1271
1272  // Install shortcuts
1273  for (var name in commandShortcutMap) {
1274    $(name + '-command').shortcut = commandShortcutMap[name];
1275  }
1276
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  }
1285
1286  chrome.bookmarkManagerPrivate.canEdit(function(result) {
1287    canEdit = result;
1288  });
1289
1290  chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1291    // TODO(rustema): propagate policy value to the bookmark manager when it
1292    // changes.
1293    incognitoModeAvailability = result;
1294  });
1295
1296  chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1297    canOpenNewWindows = result;
1298  });
1299
1300  cr.ui.FocusOutlineManager.forDocument(document);
1301  initializeSplitter();
1302  bmm.addBookmarkModelListeners();
1303  dnd.init(selectItemsAfterUserAction);
1304  tree.reload();
1305}
1306
1307initializeBookmarkManager();
1308})();
1309