- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / bookmark_manager / js / main.js
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  */
23 var 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  */
31 var lastHoverOnFolderTimeStamp = 0;
32
33 /**
34  * Holds a function that will undo that last action, if global undo is enabled.
35  * @type {Function}
36  */
37 var performGlobalUndo;
38
39 /**
40  * Holds a link controller singleton. Use getLinkController() rarther than
41  * accessing this variabie.
42  * @type {LinkController}
43  */
44 var linkController;
45
46 /**
47  * New Windows are not allowed in Windows 8 metro mode.
48  */
49 var 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  */
57 var incognitoModeAvailability = 'enabled';
58
59 /**
60  * Whether bookmarks can be modified.
61  * @type {boolean}
62  */
63 var canEdit = true;
64
65 /**
66  * @type {TreeItem}
67  * @const
68  */
69 var searchTreeItem = new TreeItem({
70   bookmarkId: 'q='
71 });
72
73 /**
74  * Command shortcut mapping.
75  * @const
76  */
77 var commandShortcutMap = cr.isMac ? {
78   'edit': 'Enter',
79   // On Mac we also allow Meta+Backspace.
80   'delete': 'U+007F  U+0008 Meta-U+0008',
81   'open-in-background-tab': 'Meta-Enter',
82   'open-in-new-tab': 'Shift-Meta-Enter',
83   'open-in-same-window': 'Meta-Down',
84   'open-in-new-window': 'Shift-Enter',
85   'rename-folder': 'Enter',
86   // Global undo is Command-Z. It is not in any menu.
87   'undo': 'Meta-U+005A',
88 } : {
89   'edit': 'F2',
90   'delete': 'U+007F',
91   'open-in-background-tab': 'Ctrl-Enter',
92   'open-in-new-tab': 'Shift-Ctrl-Enter',
93   'open-in-same-window': 'Enter',
94   'open-in-new-window': 'Shift-Enter',
95   'rename-folder': 'F2',
96   // Global undo is Ctrl-Z. It is not in any menu.
97   'undo': 'Ctrl-U+005A',
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  */
105 var 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  */
119 function addOneShotEventListener(node, name, handler) {
120   var f = function(e) {
121     handler(e);
122     node.removeEventListener(name, f);
123   };
124   node.addEventListener(name, f);
125 }
126
127 // Get the localized strings from the backend via bookmakrManagerPrivate API.
128 function loadLocalizedStrings(data) {
129   // The strings may contain & which we need to strip.
130   for (var key in data) {
131     data[key] = data[key].replace(/&/, '');
132   }
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  */
145 function 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  */
155 function navigateTo(id, callback) {
156   updateHash(id);
157
158   if (list.parentId == id) {
159     callback();
160     return;
161   }
162
163   var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] ||
164                   folderMetricsNameMap['subfolder'];
165   chrome.metricsPrivate.recordUserAction(
166       'BookmarkManager_NavigateTo_' + metricsId);
167
168   addOneShotEventListener(list, 'load', callback);
169   updateParentId(id);
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  */
176 function 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.
187 function 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.
241 function 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.
248 function handleUrlClickedForList(e) {
249   getLinkController().openUrlFromEvent(e.url, e.originalEvent);
250   chrome.bookmarkManagerPrivate.recordLaunch();
251 }
252
253 function 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  */
261 function setSearch(searchText) {
262   if (searchText) {
263     // Only update search item if we have a search term. We never want the
264     // search item to be for an empty search.
265     delete bmm.treeLookup[searchTreeItem.bookmarkId];
266     var id = searchTreeItem.bookmarkId = 'q=' + searchText;
267     bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
268   }
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   // Navigate now and update hash immediately.
285   navigateTo(id, updateHash);
286 }
287
288 // Handle the logo button UI.
289 // When the user clicks the button we should navigate "home" and focus the list.
290 function handleClickOnLogoButton(e) {
291   setSearch('');
292   $('list').focus();
293 }
294
295 /**
296  * This returns the user visible path to the folder where the bookmark is
297  * located.
298  * @param {number} parentId The ID of the parent folder.
299  * @return {string} The path to the the bookmark,
300  */
301 function getFolder(parentId) {
302   var parentNode = tree.getBookmarkNodeById(parentId);
303   if (parentNode) {
304     var s = parentNode.title;
305     if (parentNode.parentId != bmm.ROOT_ID) {
306       return getFolder(parentNode.parentId) + '/' + s;
307     }
308     return s;
309   }
310 }
311
312 function handleLoadForTree(e) {
313   processHash();
314 }
315
316 function getAllUrls(nodes) {
317   var urls = [];
318
319   // Adds the node and all its direct children.
320   function addNodes(node) {
321     if (node.id == 'new')
322       return;
323
324     if (node.children) {
325       node.children.forEach(function(child) {
326         if (!bmm.isFolder(child))
327           urls.push(child.url);
328       });
329     } else {
330       urls.push(node.url);
331     }
332   }
333
334   // Get a future promise for the nodes.
335   var promises = nodes.map(function(node) {
336     if (bmm.isFolder(node))
337       return bmm.loadSubtree(node.id);
338     // Not a folder so we already have all the data we need.
339     return new Promise(node);
340   });
341
342   var urlsPromise = new Promise();
343
344   var p = Promise.all.apply(null, promises);
345   p.addListener(function(nodes) {
346     nodes.forEach(function(node) {
347       addNodes(node);
348     });
349     urlsPromise.value = urls;
350   });
351
352   return urlsPromise;
353 }
354
355 /**
356  * Returns the nodes (non recursive) to use for the open commands.
357  * @param {HTMLElement} target .
358  * @return {Array.<BookmarkTreeNode>} .
359  */
360 function getNodesForOpen(target) {
361   if (target == tree) {
362     var folderItem = tree.selectedItem;
363     return folderItem == searchTreeItem ?
364         list.dataModel.slice() : tree.selectedFolders;
365   }
366   var items = list.selectedItems;
367   return items.length ? items : 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} .
375  */
376 function getUrlsForOpenCommands(target) {
377   return getAllUrls(getNodesForOpen(target));
378 }
379
380 function 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  */
395 function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) {
396   if (singularId) {
397     // The command label reflects the selection which might not reflect
398     // how many bookmarks will be opened. For example if you right click an
399     // empty area in a folder with 1 bookmark the text should still say "all".
400     var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode);
401     var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]);
402     command.label = loadTimeData.getString(singular ? singularId : pluralId);
403   }
404
405   if (commandDisabled) {
406     command.disabled = true;
407     e.canExecute = false;
408     return;
409   }
410
411   getUrlsForOpenCommands(e.target).addListener(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  */
423 function 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
439 function 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() && list.dataModel.length > 1;
451       break;
452     case 'undo-command':
453       // The global undo command has no visible UI, so always enable it, and
454       // just make it a no-op if undo is not possible.
455       e.canExecute = true;
456       break;
457     default:
458       canExecuteForList(e);
459       break;
460   }
461 }
462
463 /**
464  * Helper function for handling canExecute for the list and the tree.
465  * @param {!Event} e Can execute event object.
466  * @param {boolean} isSearch Whether the user is trying to do a command on
467  *     search.
468  */
469 function canExecuteShared(e, isSearch) {
470   var command = e.command;
471   var commandId = command.id;
472   switch (commandId) {
473     case 'paste-from-organize-menu-command':
474     case 'paste-from-context-menu-command':
475       updatePasteCommand();
476       break;
477
478     case 'add-new-bookmark-command':
479     case 'new-folder-command':
480       e.canExecute = !isSearch && canEdit;
481       break;
482
483     case 'open-in-new-tab-command':
484       updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false);
485       break;
486     case 'open-in-background-tab-command':
487       updateOpenCommand(e, command, '', '', false);
488       break;
489     case 'open-in-new-window-command':
490       updateOpenCommand(e, command,
491           'open_in_new_window', 'open_all_new_window',
492           // Disabled when incognito is forced.
493           incognitoModeAvailability == 'forced' || !canOpenNewWindows);
494       break;
495     case 'open-incognito-window-command':
496       updateOpenCommand(e, command,
497           'open_incognito', 'open_all_incognito',
498           // Not available when incognito is disabled.
499           incognitoModeAvailability == 'disabled');
500       break;
501
502     case 'undo-delete-command':
503       e.canExecute = !!lastDeletedNodes;
504       break;
505   }
506 }
507
508 /**
509  * Helper function for handling canExecute for the list and document.
510  * @param {!Event} e Can execute event object.
511  */
512 function canExecuteForList(e) {
513   var command = e.command;
514   var commandId = command.id;
515
516   function hasSelected() {
517     return !!list.selectedItem;
518   }
519
520   function hasSingleSelected() {
521     return list.selectedItems.length == 1;
522   }
523
524   function canCopyItem(item) {
525     return item.id != 'new';
526   }
527
528   function canCopyItems() {
529     var selectedItems = list.selectedItems;
530     return selectedItems && selectedItems.some(canCopyItem);
531   }
532
533   function isSearch() {
534     return list.isSearch();
535   }
536
537   switch (commandId) {
538     case 'rename-folder-command':
539       // Show rename if a single folder is selected.
540       var items = list.selectedItems;
541       if (items.length != 1) {
542         e.canExecute = false;
543         command.hidden = true;
544       } else {
545         var isFolder = bmm.isFolder(items[0]);
546         e.canExecute = isFolder && canEdit;
547         command.hidden = !isFolder;
548       }
549       break;
550
551     case 'edit-command':
552       // Show the edit command if not a folder.
553       var items = list.selectedItems;
554       if (items.length != 1) {
555         e.canExecute = false;
556         command.hidden = false;
557       } else {
558         var isFolder = bmm.isFolder(items[0]);
559         e.canExecute = !isFolder && canEdit;
560         command.hidden = isFolder;
561       }
562       break;
563
564     case 'show-in-folder-command':
565       e.canExecute = isSearch() && hasSingleSelected();
566       break;
567
568     case 'delete-command':
569     case 'cut-command':
570       e.canExecute = canCopyItems() && canEdit;
571       break;
572
573     case 'copy-command':
574       e.canExecute = canCopyItems();
575       break;
576
577     case 'open-in-same-window-command':
578       e.canExecute = hasSelected();
579       break;
580
581     default:
582       canExecuteShared(e, isSearch());
583   }
584 }
585
586 // Update canExecute for the commands when the list is the active element.
587 function handleCanExecuteForList(e) {
588   if (e.target != list) return;
589   canExecuteForList(e);
590 }
591
592 // Update canExecute for the commands when the tree is the active element.
593 function handleCanExecuteForTree(e) {
594   if (e.target != tree) return;
595
596   var command = e.command;
597   var commandId = command.id;
598
599   function hasSelected() {
600     return !!e.target.selectedItem;
601   }
602
603   function isSearch() {
604     var item = e.target.selectedItem;
605     return item == searchTreeItem;
606   }
607
608   function isTopLevelItem() {
609     return e.target.selectedItem.parentNode == tree;
610   }
611
612   switch (commandId) {
613     case 'rename-folder-command':
614       command.hidden = false;
615       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
616       break;
617
618     case 'edit-command':
619       command.hidden = true;
620       e.canExecute = false;
621       break;
622
623     case 'delete-command':
624     case 'cut-command':
625       e.canExecute = hasSelected() && !isTopLevelItem() && canEdit;
626       break;
627
628     case 'copy-command':
629       e.canExecute = hasSelected() && !isTopLevelItem();
630       break;
631
632     default:
633       canExecuteShared(e, isSearch());
634   }
635 }
636
637 /**
638  * Update the canExecute state of the commands when the selection changes.
639  * @param {Event} e The change event object.
640  */
641 function updateCommandsBasedOnSelection(e) {
642   if (e.target == document.activeElement) {
643     // Paste only needs to be updated when the tree selection changes.
644     var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
645       'add-new-bookmark', 'new-folder', 'open-in-new-tab',
646       'open-in-background-tab', 'open-in-new-window', 'open-incognito-window',
647       'open-in-same-window', 'show-in-folder'];
648
649     if (e.target == tree) {
650       commandNames.push('paste-from-context-menu', 'paste-from-organize-menu',
651                         'sort');
652     }
653
654     commandNames.forEach(function(baseId) {
655       $(baseId + '-command').canExecuteChange();
656     });
657   }
658 }
659
660 function updateEditingCommands() {
661   var editingCommands = ['cut', 'delete', 'rename-folder', 'edit',
662       'add-new-bookmark', 'new-folder', 'sort',
663       'paste-from-context-menu', 'paste-from-organize-menu'];
664
665   chrome.bookmarkManagerPrivate.canEdit(function(result) {
666     if (result != canEdit) {
667       canEdit = result;
668       editingCommands.forEach(function(baseId) {
669         $(baseId + '-command').canExecuteChange();
670       });
671     }
672   });
673 }
674
675 function handleChangeForTree(e) {
676   updateCommandsBasedOnSelection(e);
677   navigateTo(tree.selectedItem.bookmarkId, updateHash);
678 }
679
680 function handleOrganizeButtonClick(e) {
681   updateEditingCommands();
682   $('add-new-bookmark-command').canExecuteChange();
683   $('new-folder-command').canExecuteChange();
684   $('sort-command').canExecuteChange();
685 }
686
687 function 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
694 function 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
730 function 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  */
744 function 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  */
770 function 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  */
782 function 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  */
790 function 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  * Opens the selected bookmarks.
800  * @param {LinkKind} kind The kind of link we want to open.
801  * @param {HTMLElement} opt_eventTarget The target of the user initiated event.
802  */
803 function openBookmarks(kind, opt_eventTarget) {
804   // If we have selected any folders, we need to find all the bookmarks one
805   // level down. We use multiple async calls to getSubtree instead of getting
806   // the whole tree since we would like to minimize the amount of data sent.
807
808   var urlsP = getUrlsForOpenCommands(opt_eventTarget);
809   urlsP.addListener(function(urls) {
810     getLinkController().openUrls(urls, kind);
811     chrome.bookmarkManagerPrivate.recordLaunch();
812   });
813 }
814
815 /**
816  * Opens an item in the list.
817  */
818 function openItem() {
819   var bookmarkNodes = getSelectedBookmarkNodes();
820   // If we double clicked or pressed enter on a single folder, navigate to it.
821   if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) {
822     navigateTo(bookmarkNodes[0].id, updateHash);
823   } else {
824     openBookmarks(LinkKind.FOREGROUND_TAB);
825   }
826 }
827
828 /**
829  * Deletes the selected bookmarks. The bookmarks are saved in memory in case
830  * the user needs to undo the deletion.
831  */
832 function deleteBookmarks() {
833   var selectedIds = getSelectedBookmarkIds();
834   lastDeletedNodes = [];
835
836   function performDelete() {
837     chrome.bookmarkManagerPrivate.removeTrees(selectedIds);
838     $('undo-delete-command').canExecuteChange();
839     performGlobalUndo = undoDelete;
840   }
841
842   // First, store information about the bookmarks being deleted.
843   selectedIds.forEach(function(id) {
844     chrome.bookmarks.getSubTree(id, function(results) {
845       lastDeletedNodes.push(results);
846
847       // When all nodes have been saved, perform the deletion.
848       if (lastDeletedNodes.length === selectedIds.length)
849         performDelete();
850     });
851   });
852 }
853
854 /**
855  * Restores a tree of bookmarks under a specified folder.
856  * @param {BookmarkTreeNode} node The node to restore.
857  * @param {=string} parentId The ID of the folder to restore under. If not
858  *     specified, the original parentId of the node will be used.
859  */
860 function restoreTree(node, parentId) {
861   var bookmarkInfo = {
862     parentId: parentId || node.parentId,
863     title: node.title,
864     index: node.index,
865     url: node.url
866   };
867
868   chrome.bookmarks.create(bookmarkInfo, function(result) {
869     if (!result) {
870       console.error('Failed to restore bookmark.');
871       return;
872     }
873
874     if (node.children) {
875       // Restore the children using the new ID for this node.
876       node.children.forEach(function(child) {
877         restoreTree(child, result.id);
878       });
879     }
880   });
881 }
882
883 /**
884  * Restores the last set of bookmarks that was deleted.
885  */
886 function undoDelete() {
887   lastDeletedNodes.forEach(function(arr) {
888     arr.forEach(restoreTree);
889   });
890   lastDeletedNodes = null;
891   $('undo-delete-command').canExecuteChange();
892
893   // Only a single level of undo is supported, so disable global undo now.
894   performGlobalUndo = null;
895 }
896
897 /**
898  * Computes folder for "Add Page" and "Add Folder".
899  * @return {string} The id of folder node where we'll create new page/folder.
900  */
901 function computeParentFolderForNewItem() {
902   if (document.activeElement == tree)
903     return list.parentId;
904   var selectedItem = list.selectedItem;
905   return selectedItem && bmm.isFolder(selectedItem) ?
906       selectedItem.id : list.parentId;
907 }
908
909 /**
910  * Callback for rename folder and edit command. This starts editing for
911  * selected item.
912  */
913 function editSelectedItem() {
914   if (document.activeElement == tree) {
915     tree.selectedItem.editing = true;
916   } else {
917     var li = list.getListItem(list.selectedItem);
918     if (li)
919       li.editing = true;
920   }
921 }
922
923 /**
924  * Callback for the new folder command. This creates a new folder and starts
925  * a rename of it.
926  */
927 function newFolder() {
928   performGlobalUndo = null;  // This can't be undone, so disable global undo.
929
930   var parentId = computeParentFolderForNewItem();
931
932   // Callback is called after tree and list data model updated.
933   function createFolder(callback) {
934     chrome.bookmarks.create({
935       title: loadTimeData.getString('new_folder_name'),
936       parentId: parentId
937     }, callback);
938   }
939
940   if (document.activeElement == tree) {
941     createFolder(function(newNode) {
942       navigateTo(newNode.id, function() {
943         bmm.treeLookup[newNode.id].editing = true;
944       });
945     });
946     return;
947   }
948
949   function editNewFolderInList() {
950     createFolder(function() {
951       var index = list.dataModel.length - 1;
952       var sm = list.selectionModel;
953       sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
954       scrollIntoViewAndMakeEditable(index);
955     });
956   }
957
958   navigateTo(parentId, editNewFolderInList);
959 }
960
961 /**
962  * Scrolls the list item into view and makes it editable.
963  * @param {number} index The index of the item to make editable.
964  */
965 function scrollIntoViewAndMakeEditable(index) {
966   list.scrollIndexIntoView(index);
967   // onscroll is now dispatched asynchronously so we have to postpone
968   // the rest.
969   setTimeout(function() {
970     var item = list.getListItemByIndex(index);
971     if (item)
972       item.editing = true;
973   });
974 }
975
976 /**
977  * Adds a page to the current folder. This is called by the
978  * add-new-bookmark-command handler.
979  */
980 function addPage() {
981   var parentId = computeParentFolderForNewItem();
982
983   function editNewBookmark() {
984     var fakeNode = {
985       title: '',
986       url: '',
987       parentId: parentId,
988       id: 'new'
989     };
990     var dataModel = list.dataModel;
991     var length = dataModel.length;
992     dataModel.splice(length, 0, fakeNode);
993     var sm = list.selectionModel;
994     sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
995     scrollIntoViewAndMakeEditable(length);
996   };
997
998   navigateTo(parentId, editNewBookmark);
999 }
1000
1001 /**
1002  * This function is used to select items after a user action such as paste, drop
1003  * add page etc.
1004  * @param {BookmarkList|BookmarkTree} target The target of the user action.
1005  * @param {=string} opt_selectedTreeId If provided, then select that tree id.
1006  */
1007 function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1008   // We get one onCreated event per item so we delay the handling until we get
1009   // no more events coming.
1010
1011   var ids = [];
1012   var timer;
1013
1014   function handle(id, bookmarkNode) {
1015     clearTimeout(timer);
1016     if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
1017       ids.push(id);
1018     timer = setTimeout(handleTimeout, 50);
1019   }
1020
1021   function handleTimeout() {
1022     chrome.bookmarks.onCreated.removeListener(handle);
1023     chrome.bookmarks.onMoved.removeListener(handle);
1024
1025     if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1026       var index = ids.indexOf(opt_selectedTreeId);
1027       if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1028         tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1029       }
1030     } else if (target == list) {
1031       var dataModel = list.dataModel;
1032       var firstIndex = dataModel.findIndexById(ids[0]);
1033       var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1034       if (firstIndex != -1 && lastIndex != -1) {
1035         var selectionModel = list.selectionModel;
1036         selectionModel.selectedIndex = -1;
1037         selectionModel.selectRange(firstIndex, lastIndex);
1038         selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1039         list.focus();
1040       }
1041     }
1042
1043     list.endBatchUpdates();
1044   }
1045
1046   list.startBatchUpdates();
1047
1048   chrome.bookmarks.onCreated.addListener(handle);
1049   chrome.bookmarks.onMoved.addListener(handle);
1050   timer = setTimeout(handleTimeout, 300);
1051 }
1052
1053 /**
1054  * Record user action.
1055  * @param {string} name An user action name.
1056  */
1057 function recordUserAction(name) {
1058   chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name);
1059 }
1060
1061 /**
1062  * The currently selected bookmark, based on where the user is clicking.
1063  * @return {string} The ID of the currently selected bookmark (could be from
1064  *     tree view or list view).
1065  */
1066 function getSelectedId() {
1067   if (document.activeElement == tree)
1068     return tree.selectedItem.bookmarkId;
1069   var selectedItem = list.selectedItem;
1070   return selectedItem && bmm.isFolder(selectedItem) ?
1071       selectedItem.id : tree.selectedItem.bookmarkId;
1072 }
1073
1074 /**
1075  * Pastes the copied/cutted bookmark into the right location depending whether
1076  * if it was called from Organize Menu or from Context Menu.
1077  * @param {string} id The id of the element being pasted from.
1078  */
1079 function pasteBookmark(id) {
1080   recordUserAction('Paste');
1081   selectItemsAfterUserAction(list);
1082   chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds());
1083 }
1084
1085 /**
1086  * Handler for the command event. This is used for context menu of list/tree
1087  * and organized menu.
1088  * @param {!Event} e The event object.
1089  */
1090 function handleCommand(e) {
1091   var command = e.command;
1092   var commandId = command.id;
1093   switch (commandId) {
1094     case 'import-menu-command':
1095       recordUserAction('Import');
1096       chrome.bookmarks.import();
1097       break;
1098     case 'export-menu-command':
1099       recordUserAction('Export');
1100       chrome.bookmarks.export();
1101       break;
1102     case 'undo-command':
1103       if (performGlobalUndo) {
1104         recordUserAction('UndoGlobal');
1105         performGlobalUndo();
1106       } else {
1107         recordUserAction('UndoNone');
1108       }
1109       break;
1110     case 'show-in-folder-command':
1111       recordUserAction('ShowInFolder');
1112       showInFolder();
1113       break;
1114     case 'open-in-new-tab-command':
1115     case 'open-in-background-tab-command':
1116       recordUserAction('OpenInNewTab');
1117       openBookmarks(LinkKind.BACKGROUND_TAB, e.target);
1118       break;
1119     case 'open-in-new-window-command':
1120       recordUserAction('OpenInNewWindow');
1121       openBookmarks(LinkKind.WINDOW, e.target);
1122       break;
1123     case 'open-incognito-window-command':
1124       recordUserAction('OpenIncognito');
1125       openBookmarks(LinkKind.INCOGNITO, e.target);
1126       break;
1127     case 'delete-command':
1128       recordUserAction('Delete');
1129       deleteBookmarks();
1130       break;
1131     case 'copy-command':
1132       recordUserAction('Copy');
1133       chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(),
1134                                          updatePasteCommand);
1135       break;
1136     case 'cut-command':
1137       recordUserAction('Cut');
1138       chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(),
1139                                         updatePasteCommand);
1140       break;
1141     case 'paste-from-organize-menu-command':
1142       pasteBookmark(list.parentId);
1143       break;
1144     case 'paste-from-context-menu-command':
1145       pasteBookmark(getSelectedId());
1146       break;
1147     case 'sort-command':
1148       recordUserAction('Sort');
1149       chrome.bookmarkManagerPrivate.sortChildren(list.parentId);
1150       break;
1151     case 'rename-folder-command':
1152       editSelectedItem();
1153       break;
1154     case 'edit-command':
1155       recordUserAction('Edit');
1156       editSelectedItem();
1157       break;
1158     case 'new-folder-command':
1159       recordUserAction('NewFolder');
1160       newFolder();
1161       break;
1162     case 'add-new-bookmark-command':
1163       recordUserAction('AddPage');
1164       addPage();
1165       break;
1166     case 'open-in-same-window-command':
1167       recordUserAction('OpenInSame');
1168       openItem();
1169       break;
1170     case 'undo-delete-command':
1171       recordUserAction('UndoDelete');
1172       undoDelete();
1173       break;
1174   }
1175 }
1176
1177 // Execute the copy, cut and paste commands when those events are dispatched by
1178 // the browser. This allows us to rely on the browser to handle the keyboard
1179 // shortcuts for these commands.
1180 function installEventHandlerForCommand(eventName, commandId) {
1181   function handle(e) {
1182     if (document.activeElement != list && document.activeElement != tree)
1183       return;
1184     var command = $(commandId);
1185     if (!command.disabled) {
1186       command.execute();
1187       if (e)
1188         e.preventDefault();  // Prevent the system beep.
1189     }
1190   }
1191   if (eventName == 'paste') {
1192     // Paste is a bit special since we need to do an async call to see if we
1193     // can paste because the paste command might not be up to date.
1194     document.addEventListener(eventName, function(e) {
1195       updatePasteCommand(handle);
1196     });
1197   } else {
1198     document.addEventListener(eventName, handle);
1199   }
1200 }
1201
1202 function initializeSplitter() {
1203   var splitter = document.querySelector('.main > .splitter');
1204   Splitter.decorate(splitter);
1205
1206   // The splitter persists the size of the left component in the local store.
1207   if ('treeWidth' in localStorage)
1208     splitter.previousElementSibling.style.width = localStorage['treeWidth'];
1209
1210   splitter.addEventListener('resize', function(e) {
1211     localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
1212   });
1213 }
1214
1215 function initializeBookmarkManager() {
1216   // Sometimes the extension API is not initialized.
1217   if (!chrome.bookmarks)
1218     console.error('Bookmarks extension API is not available');
1219
1220   chrome.bookmarkManagerPrivate.getStrings(loadLocalizedStrings);
1221
1222   bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
1223
1224   cr.ui.decorate('menu', Menu);
1225   cr.ui.decorate('button[menu]', MenuButton);
1226   cr.ui.decorate('command', Command);
1227   BookmarkList.decorate(list);
1228   BookmarkTree.decorate(tree);
1229
1230   list.addEventListener('canceledit', handleCancelEdit);
1231   list.addEventListener('canExecute', handleCanExecuteForList);
1232   list.addEventListener('change', updateCommandsBasedOnSelection);
1233   list.addEventListener('contextmenu', updateEditingCommands);
1234   list.addEventListener('dblclick', handleDoubleClickForList);
1235   list.addEventListener('edit', handleEdit);
1236   list.addEventListener('rename', handleRename);
1237   list.addEventListener('urlClicked', handleUrlClickedForList);
1238
1239   tree.addEventListener('canExecute', handleCanExecuteForTree);
1240   tree.addEventListener('change', handleChangeForTree);
1241   tree.addEventListener('contextmenu', updateEditingCommands);
1242   tree.addEventListener('rename', handleRename);
1243   tree.addEventListener('load', handleLoadForTree);
1244
1245   cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1246   list.contextMenu = $('context-menu');
1247   tree.contextMenu = $('context-menu');
1248
1249   // We listen to hashchange so that we can update the currently shown folder
1250   // when // the user goes back and forward in the history.
1251   window.addEventListener('hashchange', processHash);
1252
1253   document.querySelector('.header form').onsubmit = function(e) {
1254     setSearch($('term').value);
1255     e.preventDefault();
1256   };
1257
1258   $('term').addEventListener('search', handleSearch);
1259
1260   document.querySelector('.summary > button').addEventListener(
1261       'click', handleOrganizeButtonClick);
1262
1263   document.querySelector('button.logo').addEventListener(
1264       'click', handleClickOnLogoButton);
1265
1266   document.addEventListener('canExecute', handleCanExecuteForDocument);
1267   document.addEventListener('command', handleCommand);
1268
1269   // Listen to copy, cut and paste events and execute the associated commands.
1270   installEventHandlerForCommand('copy', 'copy-command');
1271   installEventHandlerForCommand('cut', 'cut-command');
1272   installEventHandlerForCommand('paste', 'paste-from-organize-menu-command');
1273
1274   // Install shortcuts
1275   for (var name in commandShortcutMap) {
1276     $(name + '-command').shortcut = commandShortcutMap[name];
1277   }
1278
1279   // Disable almost all commands at startup.
1280   var commands = document.querySelectorAll('command');
1281   for (var i = 0, command; command = commands[i]; ++i) {
1282     if (command.id != 'import-menu-command' &&
1283         command.id != 'export-menu-command') {
1284       command.disabled = true;
1285     }
1286   }
1287
1288   chrome.bookmarkManagerPrivate.canEdit(function(result) {
1289     canEdit = result;
1290   });
1291
1292   chrome.systemPrivate.getIncognitoModeAvailability(function(result) {
1293     // TODO(rustema): propagate policy value to the bookmark manager when it
1294     // changes.
1295     incognitoModeAvailability = result;
1296   });
1297
1298   chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) {
1299     canOpenNewWindows = result;
1300   });
1301
1302   cr.ui.FocusOutlineManager.forDocument(document);
1303   initializeSplitter();
1304   bmm.addBookmarkModelListeners();
1305   dnd.init(selectItemsAfterUserAction);
1306   tree.reload();
1307 }
1308
1309 initializeBookmarkManager();
1310 })();