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