main.html revision c407dc5cd9bdc5668497f21b26b09d988ab439de
1<!DOCTYPE html>
2<html i18n-values="dir:textdirection">
3<!--
4
5Copyright (c) 2010 The Chromium Authors. All rights reserved.
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
8
9-->
10<head>
11<title i18n-content="title"></title>
12
13<link rel="stylesheet" href="chrome://resources/css/list.css">
14<link rel="stylesheet" href="chrome://resources/css/tree.css">
15<link rel="stylesheet" href="chrome://resources/css/menu.css">
16<link rel="stylesheet" href="css/bmm.css">
17
18<script src="chrome://resources/css/tree.css.js"></script>
19<script src="css/bmm.css.js"></script>
20
21<script src="chrome://resources/js/cr.js"></script>
22<script src="chrome://resources/js/cr/event_target.js"></script>
23<script src="chrome://resources/js/cr/link_controller.js"></script>
24<script src="chrome://resources/js/cr/promise.js"></script>
25<script src="chrome://resources/js/cr/ui.js"></script>
26<script src="chrome://resources/js/cr/ui/array_data_model.js"></script>
27<script src="chrome://resources/js/cr/ui/command.js"></script>
28<script src="chrome://resources/js/cr/ui/menu_item.js"></script>
29<script src="chrome://resources/js/cr/ui/menu.js"></script>
30<script src="chrome://resources/js/cr/ui/position_util.js"></script>
31<script src="chrome://resources/js/cr/ui/menu_button.js"></script>
32<script src="chrome://resources/js/cr/ui/context_menu_button.js"></script>
33<script src="chrome://resources/js/cr/ui/context_menu_handler.js"></script>
34<script src="chrome://resources/js/cr/ui/list_selection_model.js"></script>
35<script src="chrome://resources/js/cr/ui/list_item.js"></script>
36<script src="chrome://resources/js/cr/ui/list.js"></script>
37<script src="chrome://resources/js/cr/ui/tree.js"></script>
38<script src="chrome://resources/js/cr/ui/splitter.js"></script>
39
40<script src="chrome://resources/js/util.js"></script>
41<script src="chrome://resources/js/local_strings.js"></script>
42<script src="chrome://resources/js/i18n_template.js"></script>
43
44<script src="js/bmm/tree_iterator.js"></script>
45<script src="js/bmm.js"></script>
46<script src="js/bmm/bookmark_list.js"></script>
47<script src="js/bmm/bookmark_tree.js"></script>
48</head>
49<body i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize">
50
51<div class="header">
52  <button class="logo" tabindex=3></button>
53  <form onsubmit="setSearch(this.term.value); return false;"
54      class="form">
55    <input type="search" id="term" tabindex=1 autofocus
56        i18n-values="placeholder:search_button">
57  </form>
58</div>
59
60<div class="summary">
61  <h3 i18n-content="title"></h3>
62  <button menu="#organize-menu" tabindex="-1" i18n-content="organize_menu"></button>
63</div>
64
65<div class=main>
66  <div id=tree-container>
67    <tree id=tree tabindex=2></tree>
68  </div>
69  <div class=splitter></div>
70  <list id=list tabindex=2></list>
71</div>
72
73<!-- Organize menu -->
74<command i18n-values=".label:rename_folder" id="rename-folder-command"></command>
75<command i18n-values=".label:edit" id="edit-command"></command>
76<command i18n-values=".label:show_in_folder" id="show-in-folder-command"></command>
77<command i18n-values=".label:cut" id="cut-command"></command>
78<command i18n-values=".label:copy" id="copy-command"></command>
79<command i18n-values=".label:paste" id="paste-command"></command>
80<command i18n-values=".label:delete" id="delete-command"></command>
81<command i18n-values=".label:sort" id="sort-command"></command>
82<command i18n-values=".label:add_new_bookmark" id="add-new-bookmark-command"></command>
83<command i18n-values=".label:new_folder" id="new-folder-command"></command>
84
85<!-- Tools menu -->
86<command i18n-values=".label:import_menu" id="import-menu-command"></command>
87<command i18n-values=".label:export_menu" id="export-menu-command"></command>
88
89<!-- open * are handled in canExecute handler -->
90<command id="open-in-new-tab-command"></command>
91<command id="open-in-background-tab-command"></command>
92<command id="open-in-new-window-command"></command>
93<command id="open-incognito-window-command"></command>
94<command id="open-in-same-window-command"></command>
95
96<!-- TODO(arv): I think the commands might be better created in code? -->
97
98<menu id="organize-menu">
99  <button command="#add-new-bookmark-command"></button>
100  <button command="#new-folder-command"></button>
101  <hr>
102  <button command="#rename-folder-command"></button>
103  <button command="#edit-command"></button>
104  <button command="#show-in-folder-command"></button>
105  <hr>
106  <button command="#cut-command"></button>
107  <button command="#copy-command"></button>
108  <button command="#paste-command"></button>
109  <hr>
110  <button command="#delete-command"></button>
111  <hr>
112  <button command="#sort-command"></button>
113  <hr>
114  <button command="#import-menu-command"></button>
115  <button command="#export-menu-command"></button>
116</menu>
117
118<menu id="context-menu">
119  <button command="#open-in-new-tab-command"></button>
120  <button command="#open-in-new-window-command"></button>
121  <button command="#open-incognito-window-command"></button>
122  <hr>
123  <button command="#rename-folder-command"></button>
124  <button command="#edit-command"></button>
125  <button command="#show-in-folder-command"></button>
126  <hr>
127  <button command="#cut-command"></button>
128  <button command="#copy-command"></button>
129  <button command="#paste-command"></button>
130  <hr>
131  <button command="#delete-command"></button>
132  <hr>
133  <button command="#add-new-bookmark-command"></button>
134  <button command="#new-folder-command"></button>
135</menu>
136
137<div id="drop-overlay"></div>
138
139<script>
140
141const BookmarkList = bmm.BookmarkList;
142const BookmarkTree = bmm.BookmarkTree;
143const ListItem = cr.ui.ListItem;
144const TreeItem = cr.ui.TreeItem;
145const LinkKind = cr.LinkKind;
146const Command = cr.ui.Command;
147const CommandBinding = cr.ui.CommandBinding;
148const Menu = cr.ui.Menu;
149const MenuButton  = cr.ui.MenuButton;
150const Promise = cr.Promise;
151
152// Sometimes the extension API is not initialized.
153if (!chrome.bookmarks)
154  console.error('Bookmarks extension API is not avaiable');
155
156// Allow platform specific CSS rules.
157if (cr.isMac)
158  document.documentElement.setAttribute('os', 'mac');
159
160/**
161 * The local strings object which is used to do the translation.
162 * @type {!LocalStrings}
163 */
164var localStrings = new LocalStrings;
165
166// Get the localized strings from the backend.
167chrome.experimental.bookmarkManager.getStrings(function(data) {
168  // The strings may contain & which we need to strip.
169  for (var key in data) {
170    data[key] = data[key].replace(/&/, '');
171  }
172
173  localStrings.templateData = data;
174  i18nTemplate.process(document, data);
175
176  recentTreeItem.label = localStrings.getString('recent');
177  searchTreeItem.label = localStrings.getString('search');
178});
179
180/**
181 * The id of the bookmark root.
182 * @type {number}
183 */
184const ROOT_ID = '0';
185
186var bookmarkCache = {
187  /**
188   * Removes the cached item from both the list and tree lookups.
189   */
190  remove: function(id) {
191    var treeItem = bmm.treeLookup[id];
192    if (treeItem) {
193      var items = treeItem.items; // is an HTMLCollection
194      for (var i = 0, item; item = items[i]; i++) {
195        var bookmarkNode = item.bookmarkNode;
196        delete bmm.treeLookup[bookmarkNode.id];
197      }
198      delete bmm.treeLookup[id];
199    }
200  },
201
202  /**
203   * Updates the underlying bookmark node for the tree items and list items by
204   * querying the bookmark backend.
205   * @param {string} id The id of the node to update the children for.
206   * @param {Function=} opt_f A funciton to call when done.
207   */
208  updateChildren: function(id, opt_f) {
209    function updateItem(bookmarkNode) {
210      var treeItem = bmm.treeLookup[bookmarkNode.id];
211      if (treeItem) {
212        treeItem.bookmarkNode = bookmarkNode;
213      }
214    }
215
216    chrome.bookmarks.getChildren(id, function(children) {
217      if (children)
218        children.forEach(updateItem);
219
220      if (opt_f)
221        opt_f(children);
222    });
223  }
224};
225
226var splitter = document.querySelector('.main > .splitter');
227cr.ui.Splitter.decorate(splitter);
228
229// The splitter persists the size of the left component in the local store.
230if ('treeWidth' in localStorage)
231  splitter.previousElementSibling.style.width = localStorage['treeWidth'];
232splitter.addEventListener('resize', function(e) {
233  localStorage['treeWidth'] = splitter.previousElementSibling.style.width;
234});
235
236BookmarkList.decorate(list);
237
238var searchTreeItem = new TreeItem({
239  icon: 'images/bookmark_manager_search.png',
240  bookmarkId: 'q='
241});
242bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
243
244var recentTreeItem = new TreeItem({
245  icon: 'images/bookmark_manager_recent.png',
246  bookmarkId: 'recent'
247});
248bmm.treeLookup[recentTreeItem.bookmarkId] = recentTreeItem;
249
250BookmarkTree.decorate(tree);
251
252tree.addEventListener('change', function() {
253  navigateTo(tree.selectedItem.bookmarkId);
254});
255
256/**
257 * Navigates to a bookmark ID.
258 * @param {string} id The ID to navigate to.
259 * @param {boolean=} opt_updateHashNow Whether to immediately update the
260 *     location.hash. If false then it is updated in a timeout.
261 */
262function navigateTo(id, opt_updateHashNow) {
263  console.info('navigateTo', 'from', window.location.hash, 'to', id);
264  // Update the location hash using a timer to prevent reentrancy. This is how
265  // often we add history entries and the time here is a bit arbitrary but was
266  // picked as the smallest time a human perceives as instant.
267
268  function f() {
269    window.location.hash = tree.selectedItem.bookmarkId;
270  }
271
272  clearTimeout(navigateTo.timer_);
273  if (opt_updateHashNow)
274    f();
275  else
276    navigateTo.timer_ = setTimeout(f, 250);
277
278  updateParentId(id);
279}
280
281/**
282 * Updates the parent ID of the bookmark list and selects the correct tree item.
283 * @param {string} id The id.
284 */
285function updateParentId(id) {
286  list.parentId = id;
287  if (id in bmm.treeLookup)
288    tree.selectedItem = bmm.treeLookup[id];
289}
290
291// We listen to hashchange so that we can update the currently shown folder when
292// the user goes back and forward in the history.
293window.onhashchange = function(e) {
294  var id = window.location.hash.slice(1);
295
296  var valid = false;
297
298  // In case we got a search hash update the text input and the bmm.treeLookup
299  // to use the new id.
300  if (/^q=/.test(id)) {
301    setSearch(id.slice(2));
302    valid = true;
303  } else if (id == 'recent') {
304    valid = true;
305  }
306
307  if (valid) {
308    updateParentId(id);
309  } else {
310    // We need to verify that this is a correct ID.
311    chrome.bookmarks.get(id, function(items) {
312      if (items && items.length == 1)
313        updateParentId(id);
314    });
315  }
316};
317
318// Activate is handled by the open-in-same-window-command.
319list.addEventListener('dblclick', function(e) {
320  if (e.button == 0)
321    $('open-in-same-window-command').execute();
322});
323
324// The list dispatches an event when the user clicks on the URL or the Show in
325// folder part.
326list.addEventListener('urlClicked', function(e) {
327  getLinkController().openUrlFromEvent(e.url, e.originalEvent);
328});
329
330/**
331 * Timer id used for delaying find-as-you-type
332 */
333var inputDelayTimer;
334
335// Capture input changes to the search term input element and delay searching
336// for 250ms to reduce flicker.
337$('term').oninput = function(e) {
338  clearTimeout(inputDelayTimer);
339  inputDelayTimer = setTimeout(function() {
340    setSearch($('term').value);
341  }, 250);
342};
343
344/**
345 * Navigates to the search results for the search text.
346 * @para {string} searchText The text to search for.
347 */
348function setSearch(searchText) {
349  if (searchText) {
350    // Only update search item if we have a search term. We never want the
351    // search item to be for an empty search.
352    delete bmm.treeLookup[searchTreeItem.bookmarkId];
353    var id = searchTreeItem.bookmarkId = 'q=' + searchText;
354    bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
355  }
356
357  var input = $('term');
358  // Do not update the input if the user is actively using the text input.
359  if (document.activeElement != input)
360    input.value = searchText;
361
362  if (searchText) {
363    tree.add(searchTreeItem);
364    tree.selectedItem = searchTreeItem;
365  } else {
366    // Go "home".
367    tree.selectedItem = tree.items[0];
368    id = tree.selectedItem.bookmarkId;
369  }
370
371  // Navigate now and update hash immediately.
372  navigateTo(id, true);
373}
374
375// Handle the logo button UI.
376// When the user clicks the button we should navigate "home" and focus the list
377document.querySelector('button.logo').onclick = function(e) {
378  setSearch('');
379  $('list').focus();
380};
381
382/**
383 * Called when the title of a bookmark changes.
384 * @param {string} id
385 * @param {!Object} changeInfo
386 */
387function handleBookmarkChanged(id, changeInfo) {
388  // console.info('handleBookmarkChanged', id, changeInfo);
389  list.handleBookmarkChanged(id, changeInfo);
390  tree.handleBookmarkChanged(id, changeInfo);
391}
392
393/**
394 * Callback for when the user reorders by title.
395 * @param {string} id The id of the bookmark folder that was reordered.
396 * @param {!Object} reorderInfo The information about how the items where
397 *     reordered.
398 */
399function handleChildrenReordered(id, reorderInfo) {
400  // console.info('handleChildrenReordered', id, reorderInfo);
401  list.handleChildrenReordered(id, reorderInfo);
402  tree.handleChildrenReordered(id, reorderInfo);
403  bookmarkCache.updateChildren(id);
404}
405
406/**
407 * Callback for when a bookmark node is created.
408 * @param {string} id The id of the newly created bookmark node.
409 * @param {!Object} bookmarkNode The new bookmark node.
410 */
411function handleCreated(id, bookmarkNode) {
412  // console.info('handleCreated', id, bookmarkNode);
413  list.handleCreated(id, bookmarkNode);
414  tree.handleCreated(id, bookmarkNode);
415  bookmarkCache.updateChildren(bookmarkNode.parentId);
416}
417
418function handleMoved(id, moveInfo) {
419  // console.info('handleMoved', id, moveInfo);
420  list.handleMoved(id, moveInfo);
421  tree.handleMoved(id, moveInfo);
422
423  bookmarkCache.updateChildren(moveInfo.parentId);
424  if (moveInfo.parentId != moveInfo.oldParentId)
425    bookmarkCache.updateChildren(moveInfo.oldParentId);
426}
427
428function handleRemoved(id, removeInfo) {
429  // console.info('handleRemoved', id, removeInfo);
430  list.handleRemoved(id, removeInfo);
431  tree.handleRemoved(id, removeInfo);
432
433  bookmarkCache.updateChildren(removeInfo.parentId);
434  bookmarkCache.remove(id);
435}
436
437function handleImportBegan() {
438  chrome.bookmarks.onCreated.removeListener(handleCreated);
439  chrome.bookmarks.onChanged.removeListener(handleBookmarkChanged);
440}
441
442function handleImportEnded() {
443  // When importing is done we reload the tree and the list.
444
445  function f() {
446    tree.removeEventListener('load', f);
447
448    chrome.bookmarks.onCreated.addListener(handleCreated);
449    chrome.bookmarks.onChanged.addListener(handleBookmarkChanged);
450
451    if (list.selectImportedFolder) {
452      var otherBookmarks = tree.items[1].items;
453      var importedFolder = otherBookmarks[otherBookmarks.length - 1];
454      navigateTo(importedFolder.bookmarkId)
455      list.selectImportedFolder = false
456    } else {
457      list.reload();
458    }
459  }
460
461  tree.addEventListener('load', f);
462  tree.reload();
463}
464
465/**
466 * Adds the listeners for the bookmark model change events.
467 */
468function addBookmarkModelListeners() {
469  chrome.bookmarks.onChanged.addListener(handleBookmarkChanged);
470  chrome.bookmarks.onChildrenReordered.addListener(handleChildrenReordered);
471  chrome.bookmarks.onCreated.addListener(handleCreated);
472  chrome.bookmarks.onMoved.addListener(handleMoved);
473  chrome.bookmarks.onRemoved.addListener(handleRemoved);
474  chrome.bookmarks.onImportBegan.addListener(handleImportBegan);
475  chrome.bookmarks.onImportEnded.addListener(handleImportEnded);
476}
477
478/**
479 * This returns the user visible path to the folder where the bookmark is
480 * located.
481 * @param {number} parentId The ID of the parent folder.
482 * @return {string} The path to the the bookmark,
483 */
484function getFolder(parentId) {
485  var parentNode = tree.getBookmarkNodeById(parentId);
486  if (parentNode) {
487    var s = parentNode.title;
488    if (parentNode.parentId != ROOT_ID) {
489      return getFolder(parentNode.parentId) + '/' + s;
490    }
491    return s;
492  }
493}
494
495tree.addEventListener('load', function(e) {
496  // Add hard coded tree items
497  tree.add(recentTreeItem);
498
499  // Now we can select a tree item.
500  var hash = window.location.hash.slice(1);
501  if (!hash) {
502    // If we do not have a hash select first item in the tree.
503    hash = tree.items[0].bookmarkId;
504  }
505
506  if (/^q=/.test(hash)) {
507    var searchTerm = hash.slice(2);
508    $('term').value = searchTerm;
509    setSearch(searchTerm);
510  } else {
511    navigateTo(hash);
512  }
513});
514
515tree.reload();
516addBookmarkModelListeners();
517
518var dnd = {
519  dragData: null,
520
521  getBookmarkElement: function(el) {
522    while (el && !el.bookmarkNode) {
523      el = el.parentNode;
524    }
525    return el;
526  },
527
528  // If we are over the list and the list is showing recent or search result
529  // we cannot drop.
530  isOverRecentOrSearch: function(overElement) {
531    return (list.isRecent() || list.isSearch()) && list.contains(overElement);
532  },
533
534  checkEvery_: function(f, overBookmarkNode, overElement) {
535    return this.dragData.elements.every(function(element) {
536      return f.call(this, element, overBookmarkNode, overElement);
537    }, this);
538  },
539
540  /**
541   * @return {boolean} Whether we are currently dragging any folders.
542   */
543  isDraggingFolders: function() {
544    return !!this.dragData && this.dragData.elements.some(function(node) {
545      return !node.url;
546    });
547  },
548
549  /**
550   * This is a first pass wether we can drop the dragged items.
551   *
552   * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
553   *     currently dragging over.
554   * @param {!HTMLElement} overElement The element that we are currently
555   *     dragging over.
556   * @return {boolean} If this returns false then we know we should not drop
557   *     the items. If it returns true we still have to call canDropOn,
558   *     canDropAbove and canDropBelow.
559   */
560  canDrop: function(overBookmarkNode, overElement) {
561    var dragData = this.dragData;
562    if (!dragData)
563      return false;
564
565    if (this.isOverRecentOrSearch(overElement))
566      return false;
567
568    if (!dragData.sameProfile)
569      return true;
570
571    return this.checkEvery_(this.canDrop_, overBookmarkNode, overElement);
572  },
573
574  /**
575   * Helper for canDrop that only checks one bookmark node.
576   * @private
577   */
578  canDrop_: function(dragNode, overBookmarkNode, overElement) {
579    var dragId = dragNode.id;
580
581    if (overBookmarkNode.id == dragId)
582      return false;
583
584    // If we are dragging a folder we cannot drop it on any of its descendants
585    var dragBookmarkItem = bmm.treeLookup[dragId];
586    var dragBookmarkNode = dragBookmarkItem && dragBookmarkItem.bookmarkNode;
587    if (dragBookmarkNode && bmm.contains(dragBookmarkNode, overBookmarkNode)) {
588      return false;
589    }
590
591    return true;
592  },
593
594  /**
595   * Whether we can drop the dragged items above the drop target.
596   *
597   * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
598   *     currently dragging over.
599   * @param {!HTMLElement} overElement The element that we are currently
600   *     dragging over.
601   * @return {boolean} Whether we can drop the dragged items above the drop
602   *     target.
603   */
604  canDropAbove: function(overBookmarkNode, overElement) {
605    if (overElement instanceof BookmarkList)
606      return false;
607
608    // We cannot drop between Bookmarks bar and Other bookmarks
609    if (overBookmarkNode.parentId == ROOT_ID)
610      return false;
611
612    var isOverTreeItem = overElement instanceof TreeItem;
613
614    // We can only drop between items in the tree if we have any folders.
615    if (isOverTreeItem && !this.isDraggingFolders())
616      return false;
617
618    if (!this.dragData.sameProfile)
619      return this.isDraggingFolders() || !isOverTreeItem;
620
621    return this.checkEvery_(this.canDropAbove_, overBookmarkNode, overElement);
622  },
623
624  /**
625   * Helper for canDropAbove that only checks one bookmark node.
626   * @private
627   */
628  canDropAbove_: function(dragNode, overBookmarkNode, overElement) {
629    var dragId = dragNode.id;
630
631    // We cannot drop above if the item below is already in the drag source
632    var previousElement = overElement.previousElementSibling;
633    if (previousElement &&
634        previousElement.bookmarkId == dragId)
635      return false;
636
637    return true;
638  },
639
640  /**
641   * Whether we can drop the dragged items below the drop target.
642   *
643   * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
644   *     currently dragging over.
645   * @param {!HTMLElement} overElement The element that we are currently
646   *     dragging over.
647   * @return {boolean} Whether we can drop the dragged items below the drop
648   *     target.
649   */
650  canDropBelow: function(overBookmarkNode, overElement) {
651    if (overElement instanceof BookmarkList)
652      return false;
653
654    // We cannot drop between Bookmarks bar and Other bookmarks
655    if (overBookmarkNode.parentId == ROOT_ID)
656      return false;
657
658    // We can only drop between items in the tree if we have any folders.
659    if (!this.isDraggingFolders() && overElement instanceof TreeItem)
660      return false;
661
662    var isOverTreeItem = overElement instanceof TreeItem;
663
664    // Don't allow dropping below an expanded tree item since it is confusing
665    // to the user anyway.
666    if (isOverTreeItem && overElement.expanded)
667      return false;
668
669    if (!this.dragData.sameProfile)
670      return this.isDraggingFolders() || !isOverTreeItem;
671
672    return this.checkEvery_(this.canDropBelow_, overBookmarkNode, overElement);
673  },
674
675  /**
676   * Helper for canDropBelow that only checks one bookmark node.
677   * @private
678   */
679  canDropBelow_: function(dragNode, overBookmarkNode, overElement) {
680    var dragId = dragNode.id;
681
682    // We cannot drop below if the item below is already in the drag source
683    var nextElement = overElement.nextElementSibling;
684    if (nextElement &&
685        nextElement.bookmarkId == dragId)
686      return false;
687
688    return true;
689  },
690
691  /**
692   * Whether we can drop the dragged items on the drop target.
693   *
694   * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
695   *     currently dragging over.
696   * @param {!HTMLElement} overElement The element that we are currently
697   *     dragging over.
698   * @return {boolean} Whether we can drop the dragged items on the drop
699   *     target.
700   */
701  canDropOn: function(overBookmarkNode, overElement) {
702    // We can only drop on a folder.
703    if (!bmm.isFolder(overBookmarkNode))
704      return false;
705
706    if (!this.dragData.sameProfile)
707      return true;
708
709    return this.checkEvery_(this.canDropOn_, overBookmarkNode, overElement);
710  },
711
712  /**
713   * Helper for canDropOn that only checks one bookmark node.
714   * @private
715   */
716  canDropOn_: function(dragNode, overBookmarkNode, overElement) {
717    var dragId = dragNode.id;
718
719    if (overElement instanceof BookmarkList) {
720      // We are trying to drop an item after the last item in the list. This
721      // is allowed if the item is different from the last item in the list
722      var listItems = list.items;
723      var len = listItems.length;
724      if (len == 0 ||
725          listItems[len - 1].bookmarkId != dragId) {
726        return true;
727      }
728    }
729
730    // Cannot drop on current parent.
731    if (overBookmarkNode.id == dragNode.parentId)
732      return false;
733
734    return true;
735  },
736
737  /**
738   * Callback for the dragstart event.
739   * @param {Event} e The dragstart event.
740   */
741  handleDragStart: function(e) {
742    // Determine the selected bookmarks.
743    var target = e.target;
744    var draggedNodes = [];
745    if (target instanceof ListItem) {
746      // Use selected items.
747      draggedNodes = target.parentNode.selectedItems;
748    } else if (target instanceof TreeItem) {
749      draggedNodes.push(target.bookmarkNode);
750    }
751
752    // We manage starting the drag by using the extension API.
753    e.preventDefault();
754
755    if (draggedNodes.length) {
756      // If we are dragging a single link we can do the *Link* effect, otherwise
757      // we only allow copy and move.
758      var effectAllowed;
759      if (draggedNodes.length == 1 &&
760          !bmm.isFolder(draggedNodes[0])) {
761        effectAllowed = 'copyMoveLink';
762      } else {
763        effectAllowed = 'copyMove';
764      }
765      e.dataTransfer.effectAllowed = effectAllowed;
766
767      var ids = draggedNodes.map(function(node) {
768        return node.id;
769      });
770
771      chrome.experimental.bookmarkManager.startDrag(ids);
772    }
773  },
774
775  handleDragEnter: function(e) {
776    e.preventDefault();
777  },
778
779  /**
780   * Calback for the dragover event.
781   * @param {Event} e The dragover event.
782   */
783  handleDragOver: function(e) {
784    // TODO(arv): This function is way too long. Please refactor it.
785
786    // Allow DND on text inputs.
787    if (e.target.tagName != 'INPUT') {
788      // The default operation is to allow dropping links etc to do navigation.
789      // We never want to do that for the bookmark manager.
790      e.preventDefault();
791
792      // Set to none. This will get set to something if we can do the drop.
793      e.dataTransfer.dropEffect = 'none';
794    }
795
796    if (!this.dragData)
797      return;
798
799    var overElement = this.getBookmarkElement(e.target);
800    if (!overElement && e.target == list)
801      overElement = list;
802
803    if (!overElement)
804      return;
805
806    var overBookmarkNode = overElement.bookmarkNode;
807
808    if (!this.canDrop(overBookmarkNode, overElement))
809      return;
810
811    var bookmarkNode = overElement.bookmarkNode;
812
813    var canDropAbove = this.canDropAbove(overBookmarkNode, overElement);
814    var canDropOn = this.canDropOn(overBookmarkNode, overElement);
815    var canDropBelow = this.canDropBelow(overBookmarkNode, overElement);
816
817    if (!canDropAbove && !canDropOn && !canDropBelow)
818      return;
819
820    // Now we know that we can drop. Determine if we will drop above, on or
821    // below based on mouse position etc.
822
823    var dropPos;
824
825    e.dataTransfer.dropEffect = this.dragData.sameProfile ? 'move' : 'copy';
826
827    var rect;
828    if (overElement instanceof TreeItem) {
829      // We only want the rect of the row representing the item and not
830      // its children
831      rect = overElement.rowElement.getBoundingClientRect();
832    } else {
833      rect = overElement.getBoundingClientRect();
834    }
835
836    var dy = e.clientY - rect.top;
837    var yRatio = dy / rect.height;
838
839    //  above
840    if (canDropAbove &&
841        (yRatio <= .25 || yRatio <= .5 && !(canDropBelow && canDropOn))) {
842      dropPos = 'above';
843
844    // below
845    } else if (canDropBelow &&
846               (yRatio > .75 || yRatio > .5 && !(canDropAbove && canDropOn))) {
847      dropPos = 'below';
848
849    // on
850    } else if (canDropOn) {
851      dropPos = 'on';
852
853    // none
854    } else {
855      // No drop can happen. Exit now.
856      e.dataTransfer.dropEffect = 'none';
857      return;
858    }
859
860    function cloneClientRect(rect) {
861      var newRect = {};
862      for (var key in rect) {
863        newRect[key] = rect[key];
864      }
865      return newRect;
866    }
867
868    // If we are dropping above or below a tree item adjust the width so
869    // that it is clearer where the item will be dropped.
870    if ((dropPos == 'above' || dropPos == 'below') &&
871        overElement instanceof TreeItem) {
872      // ClientRect is read only so clone in into a read-write object.
873      rect = cloneClientRect(rect);
874      var rtl = getComputedStyle(overElement).direction == 'rtl';
875      var labelElement = overElement.labelElement;
876      var labelRect = labelElement.getBoundingClientRect();
877      if (rtl) {
878        rect.width = labelRect.left + labelRect.width - rect.left;
879      } else {
880        rect.left = labelRect.left;
881        rect.width -= rect.left
882      }
883    }
884
885    var overlayType = dropPos;
886
887    // If we are dropping on a list we want to show a overlay drop line after
888    // the last element
889    if (overElement instanceof BookmarkList) {
890      overlayType = 'below';
891
892      // Get the rect of the last list item.
893      var length = overElement.dataModel.length;
894      if (length) {
895        dropPos = 'below';
896        overElement = overElement.getListItemByIndex(length - 1);
897        rect = overElement.getBoundingClientRect();
898      } else {
899        // If there are no items, collapse the height of the rect
900        rect = cloneClientRect(rect);
901        rect.height = 0;
902        // We do not use bottom so we don't care to adjust it.
903      }
904    }
905
906    this.showDropOverlay_(rect, overlayType);
907
908    this.dropDestination = {
909      dropPos: dropPos,
910      relatedNode: overElement.bookmarkNode
911    };
912  },
913
914  /**
915   * Shows and positions the drop marker overlay.
916   * @param {ClientRect} targetRect The drop target rect
917   * @param {string} overlayType The position relative to the target rect.
918   * @private
919   */
920  showDropOverlay_: function(targetRect, overlayType) {
921    window.clearTimeout(this.hideDropOverlayTimer_);
922    var overlay = $('drop-overlay');
923    if (overlayType == 'on') {
924      overlay.className = '';
925      overlay.style.top = targetRect.top + 'px';
926      overlay.style.height = targetRect.height + 'px';
927    } else {
928      overlay.className = 'line';
929      overlay.style.height = '';
930    }
931    overlay.style.width = targetRect.width + 'px';
932    overlay.style.left = targetRect.left + 'px';
933    overlay.style.display = 'block';
934
935    if (overlayType != 'on') {
936      var overlayRect = overlay.getBoundingClientRect();
937      if (overlayType == 'above') {
938        overlay.style.top = targetRect.top - overlayRect.height / 2 + 'px';
939      } else {
940        overlay.style.top = targetRect.top + targetRect.height -
941            overlayRect.height / 2 + 'px';
942      }
943    }
944  },
945
946  /**
947   * Hides the drop overlay element.
948   * @private
949   */
950  hideDropOverlay_: function() {
951    // Hide the overlay in a timeout to reduce flickering as we move between
952    // valid drop targets.
953    window.clearTimeout(this.hideDropOverlayTimer_);
954    this.hideDropOverlayTimer_ = window.setTimeout(function() {
955      $('drop-overlay').style.display = '';
956    }, 100);
957  },
958
959  handleDragLeave: function(e) {
960    this.hideDropOverlay_();
961  },
962
963  handleDrop: function(e) {
964    if (this.dropDestination && this.dragData) {
965      var dropPos = this.dropDestination.dropPos;
966      var relatedNode = this.dropDestination.relatedNode;
967      var parentId = dropPos == 'on' ? relatedNode.id : relatedNode.parentId;
968
969      var selectTarget;
970      var selectedTreeId;
971      var index;
972      var relatedIndex;
973      // Try to find the index in the dataModel so we don't have to always keep
974      // the index for the list items up to date.
975      var overElement = this.getBookmarkElement(e.target);
976      if (overElement instanceof ListItem) {
977        relatedIndex = overElement.parentNode.dataModel.indexOf(relatedNode);
978        selectTarget = list;
979      } else if (overElement instanceof BookmarkList) {
980        relatedIndex = overElement.dataModel.length - 1;
981        selectTarget = list;
982      } else {
983        // Tree
984        relatedIndex = relatedNode.index;
985        selectTarget = tree;
986        selectedTreeId =
987            tree.selectedItem ? tree.selectedItem.bookmarkId : null;
988      }
989
990      if (dropPos == 'above')
991        index = relatedIndex;
992      else if (dropPos == 'below')
993        index = relatedIndex + 1;
994
995      selectItemsAfterUserAction(selectTarget, selectedTreeId);
996
997      if (index != undefined && index != -1)
998        chrome.experimental.bookmarkManager.drop(parentId, index);
999      else
1000        chrome.experimental.bookmarkManager.drop(parentId);
1001
1002      // TODO(arv): Select the newly dropped items.
1003    }
1004    this.dropDestination = null;
1005    this.hideDropOverlay_();
1006  },
1007
1008  clearDragData: function() {
1009    this.dragData = null;
1010  },
1011
1012  handleChromeDragEnter: function(dragData) {
1013    this.dragData = dragData;
1014  },
1015
1016  init: function() {
1017    var boundClearData = cr.bind(this.clearDragData, this);
1018    function deferredClearData() {
1019      setTimeout(boundClearData);
1020    }
1021
1022    document.addEventListener('dragstart', cr.bind(this.handleDragStart, this));
1023    document.addEventListener('dragenter', cr.bind(this.handleDragEnter, this));
1024    document.addEventListener('dragover', cr.bind(this.handleDragOver, this));
1025    document.addEventListener('dragleave', cr.bind(this.handleDragLeave, this));
1026    document.addEventListener('drop', cr.bind(this.handleDrop, this));
1027    document.addEventListener('dragend', deferredClearData);
1028    document.addEventListener('mouseup', deferredClearData);
1029
1030    chrome.experimental.bookmarkManager.onDragEnter.addListener(cr.bind(
1031        this.handleChromeDragEnter, this));
1032    chrome.experimental.bookmarkManager.onDragLeave.addListener(
1033        deferredClearData);
1034    chrome.experimental.bookmarkManager.onDrop.addListener(deferredClearData);
1035  }
1036};
1037
1038dnd.init();
1039
1040// Commands
1041
1042cr.ui.decorate('menu', Menu);
1043cr.ui.decorate('button[menu]', MenuButton);
1044cr.ui.decorate('command', Command);
1045
1046cr.ui.contextMenuHandler.addContextMenuProperty(tree);
1047list.contextMenu = $('context-menu');
1048tree.contextMenu = $('context-menu');
1049
1050// Disable almost all commands at startup.
1051var commands = document.querySelectorAll('command');
1052for (var i = 0, command; command = commands[i]; i++) {
1053  if (command.id != 'import-menu-command' &&
1054      command.id != 'export-menu-command') {
1055    command.disabled = true;
1056  }
1057}
1058
1059/**
1060 * Helper function that updates the canExecute and labels for the open like
1061 * commands.
1062 * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
1063 * @param {!cr.ui.Command} command The command we are currently precessing.
1064 */
1065function updateOpenCommands(e, command) {
1066  var selectedItem = e.target.selectedItem;
1067  var selectionCount;
1068  if (e.target == tree) {
1069    selectionCount = selectedItem ? 1 : 0;
1070    selectedItem = selectedItem.bookmarkNode;
1071  } else {
1072    selectionCount = e.target.selectedItems.length;
1073  }
1074
1075  var isFolder = selectionCount == 1 &&
1076                 selectedItem &&
1077                 bmm.isFolder(selectedItem);
1078  var multiple = selectionCount != 1 || isFolder;
1079
1080  function hasBookmarks(node) {
1081    var it = new bmm.TreeIterator(node);
1082    while (it.moveNext()) {
1083      if (!bmm.isFolder(it.current))
1084        return true;
1085    }
1086    return false;
1087  }
1088
1089  switch (command.id) {
1090    case 'open-in-new-tab-command':
1091      command.label = localStrings.getString(multiple ?
1092          'open_all' : 'open_in_new_tab');
1093      break;
1094
1095    case 'open-in-new-window-command':
1096      command.label = localStrings.getString(multiple ?
1097          'open_all_new_window' : 'open_in_new_window');
1098      break;
1099    case 'open-incognito-window-command':
1100      command.label = localStrings.getString(multiple ?
1101          'open_all_incognito' : 'open_incognito');
1102      break;
1103  }
1104  e.canExecute = selectionCount > 0 && !!selectedItem;
1105  if (isFolder && e.canExecute) {
1106    // We need to get all the bookmark items in this tree. If the tree does not
1107    // contain any non-folders we need to disable the command.
1108    var p = bmm.loadSubtree(selectedItem.id);
1109    p.addListener(function(node) {
1110      command.disabled = !node || !hasBookmarks(node);
1111    });
1112  }
1113}
1114
1115/**
1116 * Calls the backend to figure out if we can paste the clipboard into the active
1117 * folder.
1118 * @param {Function=} opt_f Function to call after the state has been
1119 *     updated.
1120 */
1121function updatePasteCommand(opt_f) {
1122  function update(canPaste) {
1123    var command = $('paste-command');
1124    command.disabled = !canPaste;
1125    if (opt_f)
1126      opt_f();
1127  }
1128  // We cannot paste into search and recent view.
1129  if (list.isSearch() || list.isRecent()) {
1130    update(false);
1131  } else {
1132    chrome.experimental.bookmarkManager.canPaste(list.parentId, update);
1133  }
1134}
1135
1136// We can always execute the import-menu and export-menu commands.
1137document.addEventListener('canExecute', function(e) {
1138  var command = e.command;
1139  var commandId = command.id;
1140  if (commandId == 'import-menu-command' || commandId == 'export-menu-command') {
1141    e.canExecute = true;
1142  }
1143});
1144
1145/**
1146 * Helper function for handling canExecute for the list and the tree.
1147 * @param {!Event} e Can exectue event object.
1148 * @param {boolean} isRecentOrSearch Whether the user is trying to do a command
1149 *     on recent or search.
1150 */
1151function canExcuteShared(e, isRecentOrSearch) {
1152  var command = e.command;
1153  var commandId = command.id;
1154  switch (commandId) {
1155    case 'paste-command':
1156      updatePasteCommand();
1157      break;
1158
1159    case 'sort-command':
1160      if (isRecentOrSearch) {
1161        e.canExecute = false;
1162      } else {
1163        e.canExecute = list.dataModel.length > 0;
1164
1165        // The list might be loading so listen to the load event.
1166        var f = function() {
1167          list.removeEventListener('load', f);
1168          command.disabled = list.dataModel.length == 0;
1169        };
1170        list.addEventListener('load', f);
1171      }
1172      break;
1173
1174    case 'add-new-bookmark-command':
1175    case 'new-folder-command':
1176      e.canExecute = !isRecentOrSearch;
1177      break;
1178
1179    case 'open-in-new-tab-command':
1180    case 'open-in-background-tab-command':
1181    case 'open-in-new-window-command':
1182    case 'open-incognito-window-command':
1183      updateOpenCommands(e, command);
1184      break;
1185  }
1186}
1187
1188// Update canExecute for the commands when the list is the active element.
1189list.addEventListener('canExecute', function(e) {
1190  if (e.target != list) return;
1191
1192  var command = e.command;
1193  var commandId = command.id;
1194
1195  function hasSelected() {
1196    return !!e.target.selectedItem;
1197  }
1198
1199  function hasSingleSelected() {
1200    return e.target.selectedItems.length == 1;
1201  }
1202
1203  function isRecentOrSearch() {
1204    return list.isRecent() || list.isSearch();
1205  }
1206
1207  switch (commandId) {
1208    case 'rename-folder-command':
1209      // Show rename if a single folder is selected
1210      var items = e.target.selectedItems;
1211      if (items.length != 1) {
1212        e.canExecute = false;
1213        command.hidden = true;
1214      } else {
1215        var isFolder = bmm.isFolder(items[0]);
1216        e.canExecute = isFolder;
1217        command.hidden = !isFolder;
1218      }
1219      break;
1220
1221    case 'edit-command':
1222      // Show the edit command if not a folder
1223      var items = e.target.selectedItems;
1224      if (items.length != 1) {
1225        e.canExecute = false;
1226        command.hidden = false;
1227      } else {
1228        var isFolder = bmm.isFolder(items[0]);
1229        e.canExecute = !isFolder;
1230        command.hidden = isFolder;
1231      }
1232      break;
1233
1234    case 'show-in-folder-command':
1235      e.canExecute = isRecentOrSearch() && hasSingleSelected();
1236      break;
1237
1238    case 'delete-command':
1239    case 'cut-command':
1240    case 'copy-command':
1241      e.canExecute = hasSelected();
1242      break;
1243
1244    case 'open-in-same-window-command':
1245      e.canExecute = hasSelected();
1246      break;
1247
1248    default:
1249      canExcuteShared(e, isRecentOrSearch());
1250  }
1251});
1252
1253// Update canExecute for the commands when the tree is the active element.
1254tree.addEventListener('canExecute', function(e) {
1255  if (e.target != tree) return;
1256
1257  var command = e.command;
1258  var commandId = command.id;
1259
1260  function hasSelected() {
1261    return !!e.target.selectedItem;
1262  }
1263
1264  function isRecentOrSearch() {
1265    var item = e.target.selectedItem;
1266    return item == recentTreeItem || item == searchTreeItem;
1267  }
1268
1269  function isTopLevelItem() {
1270    return e.target.selectedItem.parentNode == tree;
1271  }
1272
1273  switch (commandId) {
1274    case 'rename-folder-command':
1275      command.hidden = false;
1276      e.canExecute = hasSelected() && !isTopLevelItem();
1277      break;
1278
1279    case 'edit-command':
1280      command.hidden = true;
1281      e.canExecute = false;
1282      break;
1283
1284    case 'delete-command':
1285    case 'cut-command':
1286    case 'copy-command':
1287      e.canExecute = hasSelected() && !isTopLevelItem();
1288      break;
1289
1290    default:
1291      canExcuteShared(e, isRecentOrSearch());
1292  }
1293});
1294
1295/**
1296 * Update the canExecute state of the commands when the selection changes.
1297 * @param {Event} e The change event object.
1298 */
1299function updateCommandsBasedOnSelection(e) {
1300  if (e.target == document.activeElement) {
1301    // Paste only needs to updated when the tree selection changes.
1302    var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
1303        'add-new-bookmark', 'new-folder', 'open-in-new-tab',
1304        'open-in-new-window', 'open-incognito-window', 'open-in-same-window'];
1305
1306    if (e.target == tree) {
1307      commandNames.push('paste', 'show-in-folder', 'sort');
1308    }
1309
1310    commandNames.forEach(function(baseId) {
1311      $(baseId + '-command').canExecuteChange();
1312    });
1313  }
1314}
1315
1316list.addEventListener('change', updateCommandsBasedOnSelection);
1317tree.addEventListener('change', updateCommandsBasedOnSelection);
1318
1319document.addEventListener('command', function(e) {
1320  var command = e.command;
1321  var commandId = command.id;
1322  console.log(command.id, 'executed', 'on', e.target);
1323  if (commandId == 'import-menu-command') {
1324    // Set a flag on the list so we can select the newly imported folder.
1325    list.selectImportedFolder = true;
1326    chrome.bookmarks.import();
1327  } else if (command.id == 'export-menu-command') {
1328    chrome.bookmarks.export();
1329  }
1330});
1331
1332function handleRename(e) {
1333  var item = e.target;
1334  var bookmarkNode = item.bookmarkNode;
1335  chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
1336}
1337
1338tree.addEventListener('rename', handleRename);
1339list.addEventListener('rename', handleRename);
1340
1341list.addEventListener('edit', function(e) {
1342  var item = e.target;
1343  var bookmarkNode = item.bookmarkNode;
1344  var context = {
1345    title: bookmarkNode.title
1346  };
1347  if (!bmm.isFolder(bookmarkNode))
1348    context.url = bookmarkNode.url;
1349
1350  if (bookmarkNode.id == 'new') {
1351    selectItemsAfterUserAction(list);
1352
1353    // New page
1354    context.parentId = bookmarkNode.parentId;
1355    chrome.bookmarks.create(context, function(node) {
1356      // A new node was created and will get added to the list due to the
1357      // handler.
1358      var dataModel = list.dataModel;
1359      var index = dataModel.indexOf(bookmarkNode);
1360      dataModel.splice(index, 1);
1361
1362      // Select new item.
1363      var newIndex = dataModel.findIndexById(node.id);
1364      if (newIndex != -1) {
1365        var sm = list.selectionModel;
1366        list.scrollIndexIntoView(newIndex);
1367        sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex;
1368      }
1369    });
1370  } else {
1371    // Edit
1372    chrome.bookmarks.update(bookmarkNode.id, context);
1373  }
1374});
1375
1376list.addEventListener('canceledit', function(e) {
1377  var item = e.target;
1378  var bookmarkNode = item.bookmarkNode;
1379  if (bookmarkNode.id == 'new') {
1380    list.remove(item);
1381    list.selectionModel.leadItem = list.lastChild;
1382    list.selectionModel.anchorItem = list.lastChild;
1383    list.focus();
1384  }
1385});
1386
1387/**
1388 * Navigates to the folder that the selected item is in and selects it. This is
1389 * used for the show-in-folder command.
1390 */
1391function showInFolder() {
1392  var bookmarkNode = list.selectedItem;
1393  var parentId = bookmarkNode.parentId;
1394
1395  // After the list is loaded we should select the revealed item.
1396  function f(e) {
1397    var index;
1398    if (bookmarkNode && (index = list.dataModel.indexOf(bookmarkNode))) {
1399      var sm = list.selectionModel;
1400      sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
1401      list.scrollIndexIntoView(index);
1402    }
1403    list.removeEventListener('load', f);
1404  }
1405  list.addEventListener('load', f);
1406  var treeItem = bmm.treeLookup[parentId];
1407  treeItem.reveal();
1408
1409  navigateTo(parentId);
1410}
1411
1412var linkController;
1413
1414/**
1415 * @return {!cr.LinkController} The link controller used to open links based on
1416 *     user clicks and keyboard actions.
1417 */
1418function getLinkController() {
1419  return linkController ||
1420      (linkController = new cr.LinkController(localStrings));
1421}
1422
1423/**
1424 * Returns the selected bookmark nodes of the active element. Only call this
1425 * if the list or the tree is focused.
1426 * @return {!Array} Array of bookmark nodes.
1427 */
1428function getSelectedBookmarkNodes() {
1429  if (document.activeElement == list) {
1430    return list.selectedItems;
1431  } else if (document.activeElement == tree) {
1432    return [tree.selectedItem.bookmarkNode];
1433  } else {
1434    throw Error('getSelectedBookmarkNodes called when wrong element focused.');
1435  }
1436}
1437
1438/**
1439 * @return {!Array.<string>} An array of the selected bookmark IDs.
1440 */
1441function getSelectedBookmarkIds() {
1442  return getSelectedBookmarkNodes().map(function(node) {
1443    return node.id;
1444  });
1445}
1446
1447/**
1448 * Opens the selected bookmarks.
1449 * @param {LinkKind} kind The kind of link we want to open.
1450 */
1451function openBookmarks(kind) {
1452  // If we have selected any folders we need to find all items recursively.
1453  // We use multiple async calls to getSubtree instead of getting the whole
1454  // tree since we would like to minimize the amount of data sent.
1455
1456  var urls = [];
1457
1458  // Adds the node and all the descendants
1459  function addNodes(node) {
1460    var it = new bmm.TreeIterator(node);
1461    while (it.moveNext()) {
1462      var n = it.current;
1463      if (!bmm.isFolder(n))
1464        urls.push(n.url);
1465    }
1466  }
1467
1468  var nodes = getSelectedBookmarkNodes();
1469
1470  // Get a future promise for every selected item.
1471  var promises = nodes.map(function(node) {
1472    if (bmm.isFolder(node))
1473      return bmm.loadSubtree(node.id);
1474    // Not a folder so we already have all the data we need.
1475    return new Promise(node.url);
1476  });
1477
1478  var p = Promise.all.apply(null, promises);
1479  p.addListener(function(values) {
1480    values.forEach(function(v) {
1481      if (typeof v == 'string')
1482        urls.push(v);
1483      else
1484        addNodes(v);
1485    });
1486    getLinkController().openUrls(urls, kind);
1487  });
1488}
1489
1490/**
1491 * Opens an item in the list.
1492 */
1493function openItem() {
1494  var bookmarkNodes = getSelectedBookmarkNodes();
1495  // If we double clicked or pressed enter on a single folder navigate to it.
1496  if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) {
1497    navigateTo(bookmarkNodes[0].id);
1498  } else {
1499    openBookmarks(LinkKind.FOREGROUND_TAB);
1500  }
1501}
1502
1503/**
1504 * Deletes the selected bookmarks.
1505 */
1506function deleteBookmarks() {
1507  getSelectedBookmarkIds().forEach(function(id) {
1508    chrome.bookmarks.removeTree(id);
1509  });
1510}
1511
1512/**
1513 * Callback for the new folder command. This creates a new folder and starts
1514 * a rename of it.
1515 */
1516function newFolder() {
1517  var parentId = list.parentId;
1518  var isTree = document.activeElement == tree;
1519  chrome.bookmarks.create({
1520    title: localStrings.getString('new_folder_name'),
1521    parentId: parentId
1522  }, function(newNode) {
1523    // We need to do this in a timeout to be able to focus the newly created
1524    // item.
1525    setTimeout(function() {
1526      var newItem;
1527      if (isTree) {
1528        newItem = bmm.treeLookup[newNode.id];
1529        tree.selectedItem = newItem;
1530      } else {
1531        var index = list.dataModel.findIndexById(newNode.id);
1532        var sm = list.selectionModel;
1533        sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
1534        list.scrollIndexIntoView(index);
1535        newItem = list.getListItemByIndex(index);
1536      }
1537
1538      newItem.editing = true;
1539    });
1540  });
1541}
1542
1543/**
1544 * Adds a page to the current folder. This is called by the
1545 * add-new-bookmark-command handler.
1546 */
1547function addPage() {
1548  var parentId = list.parentId;
1549  var fakeNode = {
1550    title: '',
1551    url: '',
1552    parentId: parentId,
1553    id: 'new'
1554  };
1555
1556  var dataModel = list.dataModel;
1557  var length = dataModel.length;
1558  dataModel.splice(length, 0, fakeNode);
1559  var sm = list.selectionModel;
1560  sm.anchorIndex = sm.leadIndex = sm.selectedIndex = length;
1561  list.scrollIndexIntoView(length);
1562  var li = list.getListItemByIndex(length);
1563  li.editing = true;
1564}
1565
1566/**
1567 * This function is used to select items after a user action such as paste, drop
1568 * add page etc.
1569 * @param {BookmarkList|BookmarkTree} target The target of the user action.
1570 * @param {=string} opt_selectedTreeId If provided, then select that tree id.
1571 */
1572function selectItemsAfterUserAction(target, opt_selectedTreeId) {
1573  // We get one onCreated event per item so we delay the handling until we got
1574  // no more events coming.
1575
1576  var ids = [];
1577  var timer;
1578
1579  function handle(id, bookmarkNode) {
1580    clearTimeout(timer);
1581    if (opt_selectedTreeId || list.parentId == bookmarkNode.parentId)
1582      ids.push(id);
1583    timer = setTimeout(handleTimeout, 50);
1584  }
1585
1586  function handleTimeout() {
1587    chrome.bookmarks.onCreated.removeListener(handle);
1588    chrome.bookmarks.onMoved.removeListener(handle);
1589
1590    if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) {
1591      var index = ids.indexOf(opt_selectedTreeId);
1592      if (index != -1 && opt_selectedTreeId in bmm.treeLookup) {
1593        tree.selectedItem = bmm.treeLookup[opt_selectedTreeId];
1594      }
1595    } else if (target == list) {
1596      var dataModel = list.dataModel;
1597      var firstIndex = dataModel.findIndexById(ids[0]);
1598      var lastIndex = dataModel.findIndexById(ids[ids.length - 1]);
1599      if (firstIndex != -1 && lastIndex != -1) {
1600        var selectionModel = list.selectionModel;
1601        selectionModel.selectedIndex = -1;
1602        selectionModel.selectRange(firstIndex, lastIndex);
1603        selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex;
1604        list.focus();
1605      }
1606    }
1607
1608    list.endBatchUpdates();
1609  }
1610
1611  list.startBatchUpdates();
1612
1613  chrome.bookmarks.onCreated.addListener(handle);
1614  chrome.bookmarks.onMoved.addListener(handle);
1615  timer = setTimeout(handleTimeout, 300);
1616}
1617
1618/**
1619 * Handler for the command event. This is used both for the tree and the list.
1620 * @param {!Event} e The event object.
1621 */
1622function handleCommand(e) {
1623  var command = e.command;
1624  var commandId = command.id;
1625  switch (commandId) {
1626    case 'show-in-folder-command':
1627      showInFolder();
1628      break;
1629    case 'open-in-new-tab-command':
1630      openBookmarks(LinkKind.FOREGROUND_TAB);
1631      break;
1632    case 'open-in-background-tab-command':
1633      openBookmarks(LinkKind.BACKGROUND_TAB);
1634      break;
1635    case 'open-in-new-window-command':
1636      openBookmarks(LinkKind.WINDOW);
1637      break;
1638    case 'open-incognito-window-command':
1639      openBookmarks(LinkKind.INCOGNITO);
1640      break;
1641    case 'delete-command':
1642      deleteBookmarks();
1643      break;
1644    case 'copy-command':
1645      chrome.experimental.bookmarkManager.copy(getSelectedBookmarkIds());
1646      break;
1647    case 'cut-command':
1648      chrome.experimental.bookmarkManager.cut(getSelectedBookmarkIds());
1649      break;
1650    case 'paste-command':
1651      selectItemsAfterUserAction(list);
1652      chrome.experimental.bookmarkManager.paste(list.parentId);
1653      break;
1654    case 'sort-command':
1655      chrome.experimental.bookmarkManager.sortChildren(list.parentId);
1656      break;
1657    case 'rename-folder-command':
1658    case 'edit-command':
1659      if (document.activeElement == list) {
1660        var li = list.getListItem(list.selectedItem);
1661        if (li)
1662          li.editing = true;
1663      } else {
1664        document.activeElement.selectedItem.editing = true;
1665      }
1666      break;
1667    case 'new-folder-command':
1668      newFolder();
1669      break;
1670    case 'add-new-bookmark-command':
1671      addPage();
1672      break;
1673    case 'open-in-same-window-command':
1674      openItem();
1675      break;
1676  }
1677}
1678
1679// Delete on all platforms. On Mac we also allow Meta+Backspace.
1680$('delete-command').shortcut = 'U+007F' +
1681                               (cr.isMac ? ' U+0008 Meta-U+0008' : '');
1682
1683// Enter on all platforms. Mac we also support space and Command-Down !?!
1684$('open-in-same-window-command').shortcut = 'Enter' +
1685    (cr.isMac ? ' U+0020 Meta-Down' : '');
1686
1687//
1688$('open-in-new-window-command').shortcut = 'Shift-Enter';
1689$('open-in-background-tab-command').shortcut = cr.isMac ? 'Meta-Enter' :
1690                                                          'Ctrl-Enter';
1691$('open-in-new-tab-command').shortcut = cr.isMac ? 'Shift-Meta-Enter' :
1692                                                   'Shift-Ctrl-Enter';
1693
1694if (!cr.isMac) {
1695  $('rename-folder-command').shortcut = 'F2';
1696  $('edit-command').shortcut = 'F2';
1697}
1698
1699list.addEventListener('command', handleCommand);
1700tree.addEventListener('command', handleCommand);
1701
1702// Execute the copy, cut and paste commands when those events are dispatched by
1703// the browser. This allows us to rely on the browser to handle the keyboard
1704// shortcuts for these commands.
1705(function() {
1706  function handle(id) {
1707    return function(e) {
1708      var command = $(id);
1709      if (!command.disabled) {
1710        command.execute();
1711        if (e) e.preventDefault(); // Prevent the system beep
1712      }
1713    };
1714  }
1715
1716  // Listen to copy, cut and paste events and execute the associated commands.
1717  document.addEventListener('copy', handle('copy-command'));
1718  document.addEventListener('cut', handle('cut-command'));
1719
1720  var pasteHandler = handle('paste-command');
1721  document.addEventListener('paste', function(e) {
1722    // Paste is a bit special since we need to do an async call to see if we can
1723    // paste because the paste command might not be up to date.
1724    updatePasteCommand(pasteHandler);
1725  });
1726})();
1727
1728</script>
1729
1730</body>
1731</html>
1732