summaryrefslogtreecommitdiffstats
path: root/chrome/browser/resources
diff options
context:
space:
mode:
authorarv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-02-03 17:22:15 +0000
committerarv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-02-03 17:22:15 +0000
commit9a30588cd1081b702ade98be4eb4a7c1344bb43e (patch)
tree7b2d1b7eae4ea9c6f4117a2a5d20590ec6e9cf33 /chrome/browser/resources
parent5e90f82f1121ec44022d60f3645c8105cc8a6271 (diff)
downloadchromium_src-9a30588cd1081b702ade98be4eb4a7c1344bb43e.zip
chromium_src-9a30588cd1081b702ade98be4eb4a7c1344bb43e.tar.gz
chromium_src-9a30588cd1081b702ade98be4eb4a7c1344bb43e.tar.bz2
Move bookmark manager
BUG=None TEST=None Review URL: http://codereview.chromium.org/560023 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@37980 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources')
-rw-r--r--chrome/browser/resources/bookmark_manager/css/list.css61
-rw-r--r--chrome/browser/resources/bookmark_manager/css/menu.css48
-rw-r--r--chrome/browser/resources/bookmark_manager/css/tree.css143
-rw-r--r--chrome/browser/resources/bookmark_manager/css/tree.css.js77
-rw-r--r--chrome/browser/resources/bookmark_manager/images/bookmark_bar_folder_mac.pngbin0 -> 1345 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/images/bookmark_manager_recent.pngbin0 -> 661 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/images/bookmark_manager_search.pngbin0 -> 863 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/images/bookmarks_section.pngbin0 -> 4984 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/images/folder_closed.pngbin0 -> 412 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/images/folder_closed_rtl.pngbin0 -> 438 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/images/folder_open.pngbin0 -> 466 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/images/folder_open_rtl.pngbin0 -> 469 bytes
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm.js24
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js235
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js191
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr.js314
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/event.js40
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/eventtarget.js96
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html144
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui.js106
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/command.js265
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/contextmenuhandler.js211
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/list.js229
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/listitem.js58
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/listselectionmodel.js423
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/menu.js157
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/menubutton.js166
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/menuitem.js147
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js507
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr_test.html231
-rw-r--r--chrome/browser/resources/bookmark_manager/js/i18ntemplate.js104
-rw-r--r--chrome/browser/resources/bookmark_manager/js/localstrings.js55
-rw-r--r--chrome/browser/resources/bookmark_manager/js/util.js57
-rw-r--r--chrome/browser/resources/bookmark_manager/main.html1684
-rw-r--r--chrome/browser/resources/bookmark_manager/manifest.json10
35 files changed, 5783 insertions, 0 deletions
diff --git a/chrome/browser/resources/bookmark_manager/css/list.css b/chrome/browser/resources/bookmark_manager/css/list.css
new file mode 100644
index 0000000..0170566
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/css/list.css
@@ -0,0 +1,61 @@
+
+list {
+ overflow: auto;
+ outline: none;
+}
+
+list > * {
+ font: 11px verdana;
+ -webkit-user-select: none;
+ border: 1px solid rgba(255,255,255,0); /* transparent white */
+ background-color: rgba(255,255,255,0);
+ -webkit-border-radius: 2px;
+ padding: 0px 3px;
+ line-height: 20px;
+ white-space: nowrap;
+ cursor: default;
+ -webkit-transition: all .12s;
+ position: relative; /* to allow overlap */
+ display: block;
+}
+
+list > [lead] {
+ border-color: transparent;
+}
+
+list:focus > [lead] {
+ border-color: hsl(214, 91%, 65%);
+ z-index: 2;
+}
+
+list > [anchor] {
+
+}
+
+list > :hover {
+ border-color: hsl(214, 91%, 85%);
+ z-index: 1;
+ background-color: hsl(214,91%,97%);
+}
+
+list > :hover,
+list > [selected] {
+ background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.8)), to(rgba(255,255,255,0)));
+}
+
+list > [selected] {
+ border-color: hsl(0,0%,85%);
+ background-color: hsl(0,0%,90%);
+ z-index: 2;
+}
+
+list:focus > [selected] {
+ background-color: hsl(214,91%,89%);
+ border-color: hsl(214, 91%, 65%);
+}
+
+list:focus > [lead][selected],
+list > [selected]:hover {
+ background-color: hsl(214,91%,87%);
+ border-color: hsl(214, 91%, 65%);
+}
diff --git a/chrome/browser/resources/bookmark_manager/css/menu.css b/chrome/browser/resources/bookmark_manager/css/menu.css
new file mode 100644
index 0000000..3fe0261
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/css/menu.css
@@ -0,0 +1,48 @@
+
+menu {
+ display: none;
+ position: absolute;
+ border: 1px solid #999;
+ -webkit-box-shadow: 2px 2px 3px hsla(0, 0%, 0%, .3);
+ color: black;
+ background-color: hsla(213, 0%, 100%, .95);
+ left: 0;
+ white-space: nowrap;
+ z-index: 2;
+ padding: 1px;
+ margin: 0;
+ cursor: default;
+}
+
+menu > * {
+ display: block;
+ margin: 0;
+ width: 100%;
+ text-align: start;
+}
+
+menu > :not(hr) {
+ -webkit-appearance: none;
+ background: transparent;
+ font: inherit;
+ border: 0;
+
+ padding: 3px 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+menu > hr {
+ border: 0;
+ border-top: 1px solid rgb(153, 153, 153);
+ margin: 2px 0;
+}
+
+menu > [hidden] {
+ display: none;
+}
+
+menu > [selected] {
+ background-color: hsl(213, 66%, 57%);
+ color: white;
+}
diff --git a/chrome/browser/resources/bookmark_manager/css/tree.css b/chrome/browser/resources/bookmark_manager/css/tree.css
new file mode 100644
index 0000000..66bdaf3
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/css/tree.css
@@ -0,0 +1,143 @@
+tree {
+ outline: none;
+ overflow: auto;
+ display: block;
+}
+
+.tree-item > .tree-row {
+ color: black;
+ font: 11px verdana;
+ -webkit-user-select: none;
+ border: 1px solid rgba(255,255,255,0); /* transparent white */
+ background-color: rgba(255,255,255,0);
+ -webkit-border-radius: 2px;
+ padding: 0px 3px;
+ /*margin-bottom:-1px;*/
+ line-height: 20px;
+ white-space: nowrap;
+ cursor: default;
+ /*-webkit-transition: all .12s;*/
+ position: relative;
+}
+
+.expand-icon {
+ width: 11px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ background-image: -webkit-canvas(triangle-empty);
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ -webkit-transition: all .15s, opacity 1.5s;
+ opacity: 0;
+}
+
+html[dir=rtl] .expand-icon {
+ -webkit-transform: scale(-1, 1);
+}
+
+tree:hover .expand-icon,
+tree:focus .expand-icon {
+ opacity: 1;
+ -webkit-transition: all .15s, opacity .5s;
+}
+
+.tree-item[expanded] > .tree-row > .expand-icon {
+ background-image: -webkit-canvas(triangle-filled);
+ -webkit-transform: translate(0, 3px) rotate(45deg);
+}
+
+html[dir=rtl] .tree-item[expanded] > .tree-row > .expand-icon {
+ -webkit-transform: scale(-1, 1) translate(0, 3px) rotate(45deg); /* flip */
+}
+
+.tree-item > .tree-row > .expand-icon:hover {
+ background-image: -webkit-canvas(triangle-hover);
+}
+
+.tree-row .expand-icon {
+ visibility: hidden;
+}
+
+.tree-row[may-have-children] .expand-icon {
+ visibility: visible;
+}
+
+.tree-row[has-children=false] .expand-icon {
+ visibility: hidden;
+}
+
+.tree-item > .tree-row:hover {
+ border-color: hsl(214, 91%, 85%);
+ z-index: 1;
+ background-color: hsl(214, 91%, 97%);
+}
+
+/*
+ WebKit has a bug with attribute selectors so we apply selected to the tree row
+ as well.
+
+ https://bugs.webkit.org/show_bug.cgi?id=12519
+
+*/
+.tree-row[selected]:hover,
+.tree-row[selected] {
+ background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.8)), to(rgba(255,255,255,0)));
+}
+
+.tree-row[selected] {
+ border-color: hsl(0,0%,85%);
+ background-color: hsl(0, 0%, 90%);
+ z-index: 2;
+}
+
+:focus .tree-row[selected] {
+ background-color: hsl(214, 91%, 89%);
+ border-color: #7da2ce;
+}
+
+.tree-children[expanded] {
+ display: block;
+}
+
+.tree-children {
+ display: none;
+}
+
+.tree-item > .tree-row > * {
+ display: inline-block;
+ -webkit-box-sizing: border-box;
+}
+
+.tree-label {
+ -webkit-padding-start: 20px;
+ background-repeat: no-repeat;
+ background-position: 0 50%;
+ background-image: url("../images/folder_closed.png");
+}
+
+.tree-rename > .tree-row > .tree-label {
+ -webkit-user-select: auto;
+ -webkit-user-modify: read-write-plaintext-only;
+ background: white;
+ color: black;
+ outline: 1px solid black;
+}
+
+html[dir=rtl] .tree-label {
+ background-position: 100% 50%;
+}
+
+.tree-row[selected] > .tree-label {
+ background-image: url("../images/folder_open.png");
+}
+
+html[dir='rtl'] .tree-label {
+ background-image: url("../images/folder_closed_rtl.png");
+}
+
+html[dir='rtl'] .tree-row[selected] > .tree-label {
+ background-image: url("../images/folder_open_rtl.png");
+}
diff --git a/chrome/browser/resources/bookmark_manager/css/tree.css.js b/chrome/browser/resources/bookmark_manager/css/tree.css.js
new file mode 100644
index 0000000..34cdf8c
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/css/tree.css.js
@@ -0,0 +1,77 @@
+
+
+(function() {
+ var a = 7;
+ var a2 = a / 2;
+ var ctx = document.getCSSCanvasContext('2d', 'triangle-filled', a2 + 2, a + 1);
+
+ ctx.fillStyle = '#000';
+ ctx.translate(.5, .5);
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0, a);
+ ctx.lineTo(a2, a2);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+
+ var ctx = document.getCSSCanvasContext('2d', 'triangle-empty', a2 + 2, a + 1);
+
+ ctx.strokeStyle = '#999';
+ ctx.lineWidth = 1.2;
+ ctx.translate(.5, .5);
+ ctx.fillStyle = '#000';
+ ctx.beginPath();
+
+
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0, a);
+ ctx.lineTo(a2, a2);
+ ctx.closePath();
+ ctx.stroke();
+
+ var ctx = document.getCSSCanvasContext('2d', 'triangle-hover', a2 + 2 + 4, a + 1 + 4);
+
+ ctx.shadowColor = 'hsl(214,91%,89%)'
+ ctx.shadowBlur = 3;
+ ctx.shadowOffsetX = 1;
+ ctx.shadowOffsetY = 0;
+
+ ctx.strokeStyle = 'hsl(214,91%,79%)';
+ ctx.lineWidth = 1.2;
+ ctx.translate(.5 + 2, .5 + 2);
+ ctx.fillStyle = '#000';
+ ctx.beginPath();
+
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0, a);
+ ctx.lineTo(a2, a2);
+ ctx.closePath();
+ ctx.stroke();
+})();
+
+// We need to generate CSS for the indentation.
+(function() {
+ // We need to generat the following
+ //.tree-item > * > .tree-item > .tree-row {
+ // -webkit-padding-start: 20px;
+ //}
+
+ //.tree-item > * .tree-item > * .tree-item > * > .tree-item > .tree-row {
+ // -webkit-padding-start: 60px;
+ //}
+ var style = document.createElement('style');
+
+ function repeat(s, n) {
+ return Array(n + 1).join(s);
+ }
+
+ var s = '';
+ for (var i = 1; i < 10; i++) {
+ s += repeat('.tree-item > * ', i) + '.tree-item > .tree-row {\n' +
+ '-webkit-padding-start:' + i * 20 + 'px\n' +
+ '}\n';
+ }
+ style.textContent = s;
+ document.documentElement.firstElementChild.appendChild(style);
+})();
diff --git a/chrome/browser/resources/bookmark_manager/images/bookmark_bar_folder_mac.png b/chrome/browser/resources/bookmark_manager/images/bookmark_bar_folder_mac.png
new file mode 100644
index 0000000..ec5d21f
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/bookmark_bar_folder_mac.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/images/bookmark_manager_recent.png b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_recent.png
new file mode 100644
index 0000000..9740e90
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_recent.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/images/bookmark_manager_search.png b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_search.png
new file mode 100644
index 0000000..76abc27
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/bookmark_manager_search.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/images/bookmarks_section.png b/chrome/browser/resources/bookmark_manager/images/bookmarks_section.png
new file mode 100644
index 0000000..08682cf
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/bookmarks_section.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/images/folder_closed.png b/chrome/browser/resources/bookmark_manager/images/folder_closed.png
new file mode 100644
index 0000000..746fab9
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/folder_closed.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/images/folder_closed_rtl.png b/chrome/browser/resources/bookmark_manager/images/folder_closed_rtl.png
new file mode 100644
index 0000000..dbd0b0a
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/folder_closed_rtl.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/images/folder_open.png b/chrome/browser/resources/bookmark_manager/images/folder_open.png
new file mode 100644
index 0000000..3276810
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/folder_open.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/images/folder_open_rtl.png b/chrome/browser/resources/bookmark_manager/images/folder_open_rtl.png
new file mode 100644
index 0000000..9ba7069
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/images/folder_open_rtl.png
Binary files differ
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm.js b/chrome/browser/resources/bookmark_manager/js/bmm.js
new file mode 100644
index 0000000..24c4ad0
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/bmm.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('bmm', function() {
+ function isFolder(bookmarkNode) {
+ return !bookmarkNode.url;
+ }
+
+ function contains(parent, descendant) {
+ if (descendant.parentId == parent.id)
+ return true;
+ // the bmm.treeLookup contains all folders
+ var parentTreeItem = bmm.treeLookup[descendant.parentId];
+ if (!parentTreeItem || !parentTreeItem.bookmarkNode)
+ return false;
+ return this.contains(parent, parentTreeItem.bookmarkNode);
+ }
+
+ return {
+ isFolder: isFolder,
+ contains: contains
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js
new file mode 100644
index 0000000..a87e91bf
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js
@@ -0,0 +1,235 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+cr.define('bmm', function() {
+ const List = cr.ui.List;
+
+ var listLookup = {};
+
+ /**
+ * Creates a new bookmark list.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLButtonElement}
+ */
+ var BookmarkList = cr.ui.define('list');
+
+ BookmarkList.prototype = {
+ __proto__: List.prototype,
+
+ decorate: function() {
+ List.prototype.decorate.call(this);
+ this.addEventListener('click', this.handleClick_);
+ },
+
+ parentId_: '',
+ get parentId() {
+ return this.parentId_;
+ },
+ set parentId(parentId) {
+ if (this.parentId_ == parentId)
+ return;
+
+ var oldParentId = this.parentId_;
+ this.parentId_ = parentId;
+
+ var callback = cr.bind(this.handleBookmarkCallback, this);
+
+ if (!parentId) {
+ callback([]);
+ } else if (/^q=/.test(parentId)) {
+ chrome.bookmarks.search(parentId.slice(2), callback);
+ } else if (parentId == 'recent') {
+ chrome.bookmarks.getRecent(50, callback);
+ } else {
+ chrome.bookmarks.getChildren(parentId, callback);
+ }
+
+ cr.dispatchPropertyChange(this, 'parentId', parentId, oldParentId);
+ },
+
+ handleBookmarkCallback: function(items) {
+ if (!items) {
+ // Failed to load bookmarks. Most likely due to the bookmark beeing
+ // removed.
+ cr.dispatchSimpleEvent(this, 'invalidId');
+ return;
+ }
+ listLookup = {};
+ this.clear();
+ var showFolder = this.showFolder();
+ items.forEach(function(item) {
+ var li = createListItem(item, showFolder);
+ this.add(li);
+ }, this);
+ cr.dispatchSimpleEvent(this, 'load');
+ },
+
+ /**
+ * The bookmark node that the list is currently displaying. If we are currently
+ * displaying recent or search this returns null.
+ * @type {BookmarkTreeNode}
+ */
+ get bookmarkNode() {
+ if (this.isSearch() || this.isRecent())
+ return null;
+ var treeItem = bmm.treeLookup[this.parentId];
+ return treeItem && treeItem.bookmarkNode;
+ },
+
+ showFolder: function() {
+ return this.isSearch() || this.isRecent();
+ },
+
+ isSearch: function() {
+ return this.parentId_[0] == 'q';
+ },
+
+ isRecent: function() {
+ return this.parentId_ == 'recent';
+ },
+
+ /**
+ * Handles the clicks on the list so that we can check if the user clicked
+ * on a link or an folder.
+ * @private
+ * @param {Event} e The click event object.
+ */
+ handleClick_: function(e) {
+
+ var el = e.target;
+ if (el.href) {
+ var event = this.ownerDocument.createEvent('Event');
+ event.initEvent('urlClicked', true, false);
+ event.url = el.href;
+ event.kind = e.shiftKey ? 'window' : e.button == 1 ? 'tab' : 'self';
+ this.dispatchEvent(event);
+ }
+ },
+
+ // Bookmark model update callbacks
+ handleBookmarkChanged: function(id, changeInfo) {
+ var listItem = listLookup[id];
+ if (listItem) {
+ listItem.bookmarkNode.title = changeInfo.title;
+ updateListItem(listItem, listItem.bookmarkNode, list.showFolder());
+ }
+ },
+
+ handleChildrenReordered: function(id, reorderInfo) {
+ if (this.parentId == id) {
+ var self = this;
+ reorderInfo.childIds.forEach(function(id, i) {
+ var li = listLookup[id]
+ self.addAt(li, i);
+ // At this point we do not read the index from the bookmark node so we
+ // do not need to update it.
+ li.bookmarkNode.index = i;
+ });
+ }
+ },
+
+ handleCreated: function(id, bookmarkNode) {
+ if (this.parentId == bookmarkNode.parentId) {
+ var li = createListItem(bookmarkNode, false);
+ this.addAt(li, bookmarkNode.index);
+ }
+ },
+
+ handleMoved: function(id, moveInfo) {
+ if (moveInfo.parentId == this.parentId ||
+ moveInfo.oldParentId == this.parentId) {
+
+ if (moveInfo.oldParentId == moveInfo.parentId) {
+ var listItem = listLookup[id];
+ if (listItem) {
+ this.remove(listItem);
+ this.addAt(listItem, moveInfo.index);
+ }
+ } else {
+ if (moveInfo.oldParentId == this.parentId) {
+ var listItem = listLookup[id];
+ if (listItem) {
+ this.remove(listItem);
+ delete listLookup[id];
+ }
+ }
+
+ if (moveInfo.parentId == list.parentId) {
+ var self = this;
+ chrome.bookmarks.get(id, function(bookmarkNodes) {
+ var bookmarkNode = bookmarkNodes[0];
+ var li = createListItem(bookmarkNode, false);
+ self.addAt(li, bookmarkNode.index);
+ });
+ }
+ }
+ }
+ },
+
+ handleRemoved: function(id, removeInfo) {
+ var listItem = listLookup[id];
+ if (listItem) {
+ this.remove(listItem);
+ delete listLookup[id];
+ }
+ }
+ };
+
+ /**
+ * The contextMenu property.
+ * @type {cr.ui.Menu}
+ */
+ cr.ui.contextMenuHandler.addContextMenuProperty(BookmarkList);
+
+ function createListItem(bookmarkNode, showFolder) {
+ var li = listItemPromo.cloneNode(true);
+ ListItem.decorate(li);
+ updateListItem(li, bookmarkNode, showFolder);
+ li.bookmarkId = bookmarkNode.id;
+ li.bookmarkNode = bookmarkNode;
+ li.draggable = true;
+ listLookup[bookmarkNode.id] = li;
+ return li;
+ }
+
+ function updateListItem(el, bookmarkNode, showFolder) {
+ var labelEl = el.firstChild;
+ const NBSP = '\u00a0';
+ labelEl.textContent = bookmarkNode.title || NBSP;
+ if (bookmarkNode.url) {
+ labelEl.style.backgroundImage = url('chrome://favicon/' +
+ bookmarkNode.url);
+ var urlEl = el.childNodes[1].firstChild;
+ urlEl.textContent = urlEl.href = bookmarkNode.url;
+ } else {
+ el.className = 'folder';
+ }
+
+ var folderEl = el.lastChild.firstChild;
+ if (showFolder) {
+ folderEl.style.display = '';
+ folderEl.textContent = getFolder(bookmarkNode.parentId);
+ folderEl.href = '#' + bookmarkNode.parentId;
+ } else {
+ folderEl.style.display = 'none';
+ }
+ }
+
+ var listItemPromo = (function() {
+ var div = cr.doc.createElement('div');
+ div.innerHTML = '<div>' +
+ '<div class=label></div>' +
+ '<div><span class=url></span></div>' +
+ '<div><span class=folder></span></div>' +
+ '</div>';
+ return div.firstChild;
+ })();
+
+ return {
+ BookmarkList: BookmarkList,
+ listLookup: listLookup
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js
new file mode 100644
index 0000000..df83b77
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js
@@ -0,0 +1,191 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+cr.define('bmm', function() {
+ const Tree = cr.ui.Tree;
+ const TreeItem = cr.ui.TreeItem;
+
+ var treeLookup = {};
+
+ /**
+ * Creates a new tree item for a bookmark node.
+ * @param {!Object} bookmarkNode The bookmark node.
+ * @return {!cr.ui.TreeItem} The newly created tree item.
+ */
+ function createTreeItem(bookmarkNode) {
+ var id = bookmarkNode.id;
+ var ti = new TreeItem({
+ bookmarkId: id,
+ bookmarkNode: bookmarkNode,
+ // Bookmark toolbar and Other bookmarks are not draggable.
+ draggable: bookmarkNode.parentId != ROOT_ID
+ });
+ treeLookup[id] = ti;
+ updateTreeItem(ti, bookmarkNode);
+ return ti;
+ }
+
+ /**
+ * Updates an existing tree item to match a bookmark node.
+ * @param {!cr.ui.TreeItem} el The tree item to update.
+ * @param {!Object} bookmarkNode The bookmark node describing the tree item.
+ */
+ function updateTreeItem(el, bookmarkNode) {
+ el.label = bookmarkNode.title;
+ }
+
+ /**
+ * Asynchronousy adds a tree item at the correct index based on the bookmark
+ * backend.
+ *
+ * Since the bookmark tree only contains folders the index we get from certain
+ * callbacks is not very useful so we therefore have this async call which gets
+ * the children of the parent and adds the tree item at the desired index.
+ *
+ * @param {!cr.ui.TreeItem} parent The parent tree item.
+ * @param {!cr.ui.TreeItem} treeItem The tree item to add.
+ * @param {Function=} f A function which gets called after the item has been
+ * added at the right index.
+ */
+ function addTreeItem(parent, treeItem, opt_f) {
+ chrome.bookmarks.getChildren(parent.bookmarkNode.id, function(children) {
+ var index = children.filter(bmm.isFolder).map(function(item) {
+ return item.id;
+ }).indexOf(treeItem.bookmarkNode.id);
+ parent.addAt(treeItem, index);
+ if (opt_f)
+ opt_f();
+ });
+ }
+
+
+ /**
+ * Creates a new bookmark list.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLButtonElement}
+ */
+ var BookmarkTree = cr.ui.define('tree');
+
+ BookmarkTree.prototype = {
+ __proto__: Tree.prototype,
+
+ handleBookmarkChanged: function(id, changeInfo) {
+ var treeItem = treeLookup[id];
+ if (treeItem) {
+ treeItem.bookmarkNode.title = changeInfo.title;
+ updateTreeItem(treeItem, listItem.bookmarkNode);
+ }
+ },
+
+ handleChildrenReordered: function(id, reorderInfo) {
+ var parentItem = treeLookup[id];
+ // The tree only contains folders.
+ var dirIds = reorderInfo.childIds.filter(function(id) {
+ return id in treeLookup;
+ }).forEach(function(id, i) {
+ parentItem.addAt(treeLookup[id], i);
+ });
+ },
+
+ handleCreated: function(id, bookmarkNode) {
+ if (bmm.isFolder(bookmarkNode)) {
+ var parentItem = treeLookup[bookmarkNode.parentId];
+ var newItem = createTreeItem(bookmarkNode);
+ addTreeItem(parentItem, newItem);
+ }
+ },
+
+ handleMoved: function(id, moveInfo) {
+ var treeItem = treeLookup[id];
+ if (treeItem) {
+ var oldParentItem = treeLookup[moveInfo.oldParentId];
+ oldParentItem.remove(treeItem);
+ var newParentItem = treeLookup[moveInfo.parentId];
+ // If the new parent did not have any children before we expand it after
+ // adding the new item because the default state is to expand the folders.
+ var hadChildren = newParentItem.hasChildren;
+ // The tree only shows folders so the index is not the index we want. We
+ // therefore get the children need to adjust the index.
+ addTreeItem(newParentItem, treeItem, function() {
+ if (!hadChildren)
+ newParentItem.expanded = true;
+ });
+ }
+ },
+
+ handleRemoved: function(id, removeInfo) {
+ var parentItem = treeLookup[removeInfo.parentId];
+ var itemToRemove = treeLookup[id];
+ if (parentItem && itemToRemove) {
+ parentItem.remove(itemToRemove);
+ }
+ },
+
+ insertSubtree:function(folder) {
+ if (!bmm.isFolder(folder))
+ return;
+ var children = folder.children;
+ this.handleCreated(folder.id, folder);
+ for(var i = 0; i < children.length; i++) {
+ var child = children[i];
+ this.insertSubtree(child);
+ }
+ },
+
+ /**
+ * Returns the bookmark node with the given ID. The tree only maintains
+ * folder nodes.
+ * @param {string} id The ID of the node to find.
+ * @return {BookmarkTreeNode} The bookmark tree node or null if not found.
+ */
+ getBookmarkNodeById: function(id) {
+ var treeItem = treeLookup[id];
+ if (treeItem)
+ return treeItem.bookmarkNode;
+ return null;
+ },
+
+ /**
+ * Fetches the bookmark items and builds the tree control.
+ */
+ buildTree: function() {
+
+ /**
+ * Recursive helper function that adds all the directories to the
+ * parentTreeItem.
+ * @param {!cr.ui.Tree|!cr.ui.TreeItem} parentTreeItem The parent tree element
+ * to append to.
+ * @param {!Array.<BookmarkTreeNode>} bookmarkNodes
+ * @return {boolean} Whether any directories where added.
+ */
+ function buildTreeItems(parentTreeItem, bookmarkNodes) {
+ var hasDirectories = false;
+ for (var i = 0, bookmarkNode; bookmarkNode = bookmarkNodes[i]; i++) {
+ if (bmm.isFolder(bookmarkNode)) {
+ hasDirectories = true;
+ var item = bmm.createTreeItem(bookmarkNode);
+ parentTreeItem.add(item);
+ var anyChildren = buildTreeItems(item, bookmarkNode.children);
+ item.expanded = anyChildren;
+ }
+ }
+ return hasDirectories;
+ }
+
+ var self = this;
+ chrome.bookmarks.getTree(function(root) {
+ buildTreeItems(self, root[0].children);
+ cr.dispatchSimpleEvent(self, 'load');
+ });
+ }
+ };
+
+ return {
+ BookmarkTree: BookmarkTree,
+ createTreeItem: createTreeItem,
+ treeLookup: treeLookup
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr.js b/chrome/browser/resources/bookmark_manager/js/cr.js
new file mode 100644
index 0000000..9de94da
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr.js
@@ -0,0 +1,314 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const cr = (function() {
+
+ /**
+ * Whether we are using a Mac or not.
+ * @type {boolean}
+ */
+ const isMac = /Mac/.test(navigator.platform);
+
+ /**
+ * Builds an object structure for the provided namespace path,
+ * ensuring that names that already exist are not overwritten. For
+ * example:
+ * "a.b.c" -> a = {};a.b={};a.b.c={};
+ * @param {string} name Name of the object that this file defines.
+ * @param {*=} opt_object The object to expose at the end of the path.
+ * @param {Object=} opt_objectToExportTo The object to add the path to;
+ * default is {@code window}.
+ * @private
+ */
+ function exportPath(name, opt_object, opt_objectToExportTo) {
+ var parts = name.split('.');
+ var cur = opt_objectToExportTo || window /* global */;
+
+ for (var part; parts.length && (part = parts.shift());) {
+ if (!parts.length && opt_object !== undefined) {
+ // last part and we have an object; use it
+ cur[part] = opt_object;
+ } else if (part in cur) {
+ cur = cur[part];
+ } else {
+ cur = cur[part] = {};
+ }
+ }
+ return cur;
+ };
+
+ /**
+ * Fires a property change event on the target.
+ * @param {EventTarget} target The target to dispatch the event on.
+ * @param {string} propertyName The name of the property that changed.
+ * @param {*} newValue The new value for the property.
+ * @param {*} oldValue The old value for the property.
+ */
+ function dispatchPropertyChange(target, propertyName, newValue, oldValue) {
+ // TODO(arv): Depending on cr.Event here is a bit ugly.
+ var e = new cr.Event(propertyName + 'Change');
+ e.propertyName = propertyName;
+ e.newValue = newValue;
+ e.oldValue = oldValue;
+ target.dispatchEvent(e);
+ }
+
+ /**
+ * The kind of property to define in {@code defineProperty}.
+ * @enum {number}
+ */
+ const PropertyKind = {
+ /**
+ * Plain old JS property where the backing data is stored as a "private"
+ * field on the object.
+ */
+ JS: 'js',
+
+ /**
+ * The property backing data is stored as an attribute on an element.
+ */
+ ATTR: 'attr',
+
+ /**
+ * The property backing data is stored as an attribute on an element. If the
+ * element has the attribute then the value is true.
+ */
+ BOOL_ATTR: 'boolAttr'
+ };
+
+ /**
+ * Helper function for defineProperty that returns the getter to use for the
+ * property.
+ * @param {string} name
+ * @param {cr.PropertyKind} kind
+ * @param {*} defaultValue The default value. This is only used for the ATTR
+ * kind.
+ * @return {function():*} The getter for the property.
+ */
+ function getGetter(name, kind, defaultValue) {
+ switch (kind) {
+ case PropertyKind.JS:
+ var privateName = name + '_';
+ return function() {
+ return this[privateName];
+ };
+ case PropertyKind.ATTR:
+ // For attr with default value we return the default value if the
+ // element is missing the attribute.
+ if (defaultValue == undefined) {
+ return function() {
+ return this.getAttribute(name);
+ };
+ } else {
+ return function() {
+ // WebKit uses null for non existant attributes.
+ var value = this.getAttribute(name);
+ return value !== null ? value : defaultValue;
+ };
+ }
+ case PropertyKind.BOOL_ATTR:
+ // Boolean attributes don't support default values.
+ return function() {
+ return this.hasAttribute(name);
+ };
+ }
+ }
+
+ /**
+ * Helper function for defineProperty that returns the setter of the right
+ * kind.
+ * @param {string} name The name of the property we are defining the setter
+ * for.
+ * @param {cr.PropertyKind} kind The kind of property we are getting the
+ * setter for.
+ * @return {function(*):void} The function to use as a setter.
+ */
+ function getSetter(name, kind) {
+ switch (kind) {
+ case PropertyKind.JS:
+ var privateName = name + '_';
+ return function(value) {
+ var oldValue = this[privateName];
+ if (value !== oldValue) {
+ this[privateName] = value;
+ dispatchPropertyChange(this, name, value, oldValue);
+ }
+ };
+
+ case PropertyKind.ATTR:
+ return function(value) {
+ var oldValue = this[name];
+ if (value !== oldValue) {
+ this.setAttribute(name, value);
+ dispatchPropertyChange(this, name, value, oldValue);
+ }
+ };
+
+ case PropertyKind.BOOL_ATTR:
+ return function(value) {
+ var oldValue = this[name];
+ if (value !== oldValue) {
+ if (value)
+ this.setAttribute(name, name);
+ else
+ this.removeAttribute(name);
+ dispatchPropertyChange(this, name, value, oldValue);
+ }
+ };
+ }
+ }
+
+ /**
+ * Defines a property on an object. When the setter changes the value a
+ * property change event with the type {@code name + 'Change'} is fired.
+ * @param {!Object} The object to define the property for.
+ * @param {string} The name of the property.
+ * @param {cr.PropertyKind=} opt_kind What kind of underlying storage to use.
+ * @param {*} opt_defaultValue The default value.
+ */
+ function defineProperty(obj, name, opt_kind, opt_default) {
+ if (typeof obj == 'function')
+ obj = obj.prototype;
+
+ var kind = opt_kind || PropertyKind.JS;
+
+ if (!obj.__lookupGetter__(name)) {
+ // For js properties we set the default value on the prototype.
+ if (kind == PropertyKind.JS && arguments.length > 3) {
+ var privateName = name + '_';
+ obj[privateName] = opt_default;
+ }
+ obj.__defineGetter__(name, getGetter(name, kind, opt_default));
+ }
+
+ if (!obj.__lookupSetter__(name)) {
+ obj.__defineSetter__(name, getSetter(name, kind));
+ }
+ }
+
+ /**
+ * Counter for use with createUid
+ */
+ var uidCounter = 1;
+
+ /**
+ * @return {number} A new unique ID.
+ */
+ function createUid() {
+ return uidCounter++;
+ }
+
+ /**
+ * Returns a unique ID for the item. This mutates the item so it needs to be
+ * an object
+ * @param {!Object} item The item to get the unique ID for.
+ * @return {number} The unique ID for the item.
+ */
+ function getUid(item) {
+ if (item.hasOwnProperty('uid'))
+ return item.uid;
+ return item.uid = createUid();
+ }
+
+ /**
+ * Partially applies this function to a particular 'this object' and zero or
+ * more arguments. The result is a new function with some arguments of the
+ * first function pre-filled and the value of |this| 'pre-specified'.
+ *
+ * Remaining arguments specified at call-time are appended to the pre-
+ * specified ones.
+ *
+ * Usage:
+ * <pre>var barMethBound = bind(myFunction, myObj, 'arg1', 'arg2');
+ * barMethBound('arg3', 'arg4');</pre>
+ *
+ * @param {Function} fn A function to partially apply.
+ * @param {Object|undefined} selfObj Specifies the object which |this| should
+ * point to when the function is run. If the value is null or undefined,
+ * it will default to the global object.
+ * @param {...*} var_args Additional arguments that are partially
+ * applied to the function.
+ *
+ * @return {!Function} A partially-applied form of the function bind() was
+ * invoked as a method of.
+ */
+ function bind(fn, selfObj, var_args) {
+ var boundArgs = Array.prototype.slice.call(arguments, 2);
+ return function() {
+ var args = Array.prototype.slice.call(arguments);
+ args.unshift.apply(args, boundArgs);
+ return fn.apply(selfObj, args);
+ }
+ }
+
+ /**
+ * Dispatches a simple event on an event target.
+ * @param {!EventTarget} target The event target to dispatch the event on.
+ * @param {string} type The type of the event.
+ * @param {boolean=} opt_bubbles Whether the event bubbles or not.
+ * @param {boolean=} opt_cancelable Whether the default action of the event
+ * can be prevented.
+ * @return {boolean} If any of the listeners called {@code preventDefault}
+ * during the dispatch this will return false.
+ */
+ function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
+ var e = new cr.Event(type, opt_bubbles, opt_cancelable);
+ return target.dispatchEvent(e);
+ }
+
+ /**
+ * @param {string} name
+ * @param {!Function} fun
+ */
+ function define(name, fun) {
+ var obj = exportPath(name);
+ var exports = fun();
+ for (var key in exports) {
+ obj[key] = exports[key];
+ }
+ }
+
+ /**
+ * Document used for various document related operations.
+ * @type {!Document}
+ */
+ var doc = document;
+
+
+ /**
+ * Allows you to run func in the context of a different document.
+ * @param {!Document} document The document to use.
+ * @param {function():*} func The function to call.
+ */
+ function withDoc(document, func) {
+ var oldDoc = doc;
+ doc = document;
+ try {
+ func();
+ } finally {
+ doc = oldDoc;
+ }
+ }
+
+ return {
+ isMac: isMac,
+ define: define,
+ defineProperty: defineProperty,
+ PropertyKind: PropertyKind,
+ createUid: createUid,
+ getUid: getUid,
+ bind: bind,
+ dispatchSimpleEvent: dispatchSimpleEvent,
+ dispatchPropertyChange: dispatchPropertyChange,
+
+ /**
+ * The document that we are currently using.
+ * @type {!Document}
+ */
+ get doc() {
+ return doc;
+ },
+ withDoc: withDoc
+ };
+})();
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/event.js b/chrome/browser/resources/bookmark_manager/js/cr/event.js
new file mode 100644
index 0000000..19fee7e
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/event.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview This is a simple pure JS event class that can be used with
+ * {@code cr.ui.EventTarget}. It should not be used with DOM EventTargets.
+ */
+
+cr.define('cr', function() {
+
+ // cr.Event is called CustomEvent in here to prevent naming conflicts. We
+ // alse store the original Event in case someone does a global alias of
+ // cr.Event.
+
+ const DomEvent = Event;
+
+ /**
+ * Creates a new event to be used with cr.EventTarget or DOM EventTarget
+ * objects.
+ * @param {string} type The name of the event.
+ * @param {boolean=}
+ * @constructor
+ */
+ function CustomEvent(type, opt_bubbles, opt_capture) {
+ var e = cr.doc.createEvent('Event');
+ e.initEvent(type, !!opt_bubbles, !!opt_capture);
+ e.__proto__ = CustomEvent.prototype;
+ return e;
+ }
+
+ CustomEvent.prototype = {
+ __proto__: DomEvent.prototype
+ };
+
+ // Export
+ return {
+ Event: CustomEvent
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/eventtarget.js b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget.js
new file mode 100644
index 0000000..f2bc733
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget.js
@@ -0,0 +1,96 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr', function() {
+
+ // TODO(arv): object.handleEvent
+
+ function EventTarget() {
+ }
+
+ EventTarget.prototype = {
+
+ /**
+ * Adds an event listener to the target.
+ * @param {string} type The name of the event.
+ * @param {!Function|{handleEvent:Function}} handler The handler for the
+ * event. This is called when the event is dispatched.
+ */
+ addEventListener: function(type, handler) {
+ if (!this.listeners_)
+ this.listeners_ = {__proto__: null};
+ if (!(type in this.listeners_)) {
+ this.listeners_[type] = [handler];
+ } else {
+ var handlers = this.listeners_[type];
+ if (handlers.indexOf(handler) < 0)
+ handlers.push(handler);
+ }
+ },
+
+ /**
+ * Removes an event listener from the target.
+ * @param {string} type The name of the event.
+ * @param {!Function|{handleEvent:Function}} handler The handler for the
+ * event.
+ */
+ removeEventListener: function(type, handler) {
+ if (!this.listeners_)
+ return;
+ if (type in this.listeners_) {
+ var handlers = this.listeners_[type];
+ var index = handlers.indexOf(handler);
+ if (index >= 0) {
+ // Clean up if this was the last listener.
+ if (handlers.length == 1)
+ delete this.listeners_[type];
+ else
+ handlers.splice(index, 1);
+ }
+ }
+ },
+
+ /**
+ * Dispatches an event and calls all the listeners that are listening to
+ * the type of the event.
+ * @param {!cr.event.Event} event The event to dispatch.
+ * @return {boolean} Whether the default action was prevented. If someone
+ * calls preventDefault on the event object then this returns false.
+ */
+ dispatchEvent: function(event) {
+ if (!this.listeners_)
+ return true;
+
+ // Since we are using DOM Event objects we need to override some of the
+ // properties and methods so that we can emulate this correctly.
+ var self = this;
+ event.__defineGetter__('target', function() {
+ return self;
+ });
+ event.preventDefault = function() {
+ this.returnValue = false;
+ };
+
+ var type = event.type;
+ var prevented = 0;
+ if (type in this.listeners_) {
+ // Clone to prevent removal during dispatch
+ var handlers = this.listeners_[type].concat();
+ for (var i = 0, handler; handler = handlers[i]; i++) {
+ if (handler.handleEvent)
+ prevented |= handler.handleEvent.call(handler, event) === false;
+ else
+ prevented |= handler.call(this, event) === false;
+ }
+ }
+
+ return !prevented && event.returnValue;
+ }
+ };
+
+ // Export
+ return {
+ EventTarget: EventTarget
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html
new file mode 100644
index 0000000..4f782d2
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title></title>
+<style>
+
+</style>
+<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+<script src="../cr.js"></script>
+<script src="event.js"></script>
+<script src="eventtarget.js"></script>
+<script>
+
+goog.require('goog.testing.jsunit');
+
+</script>
+
+</head>
+<body>
+
+<script>
+
+const EventTarget = cr.EventTarget;
+const Event = cr.Event;
+
+function testFunctionListener() {
+ debugger;
+ var fi = 0;
+ function f(e) {
+ fi++;
+ }
+
+ var gi = 0;
+ function g(e) {
+ gi++;
+ }
+
+ var et = new EventTarget;
+ et.addEventListener('f', f);
+ et.addEventListener('g', g);
+
+ // Adding again should not cause it to be called twice
+ et.addEventListener('f', f);
+ et.dispatchEvent(new Event('f'));
+ assertEquals('Should have been called once', 1, fi);
+ assertEquals(0, gi);
+
+ et.removeEventListener('f', f);
+ et.dispatchEvent(new Event('f'));
+ assertEquals('Should not have been called again', 1, fi);
+
+ et.dispatchEvent(new Event('g'));
+ assertEquals('Should have been called once', 1, gi);
+}
+
+function testHandleEvent() {
+ var fi = 0;
+ var f = {
+ handleEvent: function(e) {
+ fi++;
+ }
+ };
+
+ var gi = 0;
+ var g = {
+ handleEvent: function(e) {
+ gi++;
+ }
+ };
+
+ var et = new EventTarget;
+ et.addEventListener('f', f);
+ et.addEventListener('g', g);
+
+ // Adding again should not cause it to be called twice
+ et.addEventListener('f', f);
+ et.dispatchEvent(new Event('f'));
+ assertEquals('Should have been called once', 1, fi);
+ assertEquals(0, gi);
+
+ et.removeEventListener('f', f);
+ et.dispatchEvent(new Event('f'));
+ assertEquals('Should not have been called again', 1, fi);
+
+ et.dispatchEvent(new Event('g'));
+ assertEquals('Should have been called once', 1, gi);
+}
+
+function testPreventDefault() {
+ var i = 0;
+ function prevent(e) {
+ i++;
+ e.preventDefault();
+ }
+
+ var j = 0;
+ function pass(e) {
+ j++;
+ }
+
+ var et = new EventTarget;
+ et.addEventListener('test', pass);
+
+ assertTrue(et.dispatchEvent(new Event('test')));
+ assertEquals(1, j);
+
+ et.addEventListener('test', prevent);
+
+ console.log('NOW');
+ assertFalse(et.dispatchEvent(new Event('test')));
+ assertEquals(2, j);
+ assertEquals(1, i);
+}
+
+
+function testReturnFalse() {
+ var i = 0;
+ function prevent(e) {
+ i++;
+ return false;
+ }
+
+ var j = 0;
+ function pass(e) {
+ j++;
+ }
+
+ var et = new EventTarget;
+ et.addEventListener('test', pass);
+
+ assertTrue(et.dispatchEvent(new Event('test')));
+ assertEquals(1, j);
+
+ et.addEventListener('test', prevent);
+
+ assertFalse(et.dispatchEvent(new Event('test')));
+ assertEquals(2, j);
+ assertEquals(1, i);
+}
+
+</script>
+
+</body>
+</html>
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui.js b/chrome/browser/resources/bookmark_manager/js/cr/ui.js
new file mode 100644
index 0000000..ae90c65
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui.js
@@ -0,0 +1,106 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+
+ /**
+ * Decorates elements as an instance of a class.
+ * @param {string|!Element} source The way to find the element(s) to decorate.
+ * If this is a string then {@code querySeletorAll} is used to find the
+ * elements to decorate.
+ * @param {!Function} constr The constructor to decorate with. The constr
+ * needs to have a {@code decorate} function.
+ */
+ function decorate(source, constr) {
+ var elements;
+ if (typeof source == 'string')
+ elements = cr.doc.querySelectorAll(source);
+ else
+ elements = [source];
+
+ for (var i = 0, el; el = elements[i]; i++) {
+ if (!(el instanceof constr))
+ constr.decorate(el);
+ }
+ }
+
+ /**
+ * Helper function for creating new element for define.
+ */
+ function createElementHelper(tagName, opt_bag) {
+ // Allow passing in ownerDocument to create in a different document.
+ var doc;
+ if (opt_bag && opt_bag.ownerDocument)
+ doc = opt_bag.ownerDocument;
+ else
+ doc = cr.doc;
+ return doc.createElement(tagName);
+ }
+
+ /**
+ * Creates the constructor for a UI element class.
+ *
+ * Usage:
+ * <pre>
+ * var List = cr.ui.define('list');
+ * List.prototype = {
+ * __proto__: HTMLUListElement.prototype,
+ * decorate: function() {
+ * ...
+ * },
+ * ...
+ * };
+ * </pre>
+ *
+ * @param {string|Function} tagNameOrFunction The tagName or
+ * function to use for newly created elements. If this is a function it
+ * needs to return a new element when called.
+ * @return {function(Object=):Element} The constructor function which takes
+ * an optional property bag. The function also has a static
+ * {@code decorate} method added to it.
+ */
+ function define(tagNameOrFunction) {
+ var createFunction, tagName;
+ if (typeof tagNameOrFunction == 'function') {
+ createFunction = tagNameOrFunction;
+ tagName = '';
+ } else {
+ createFunction = createElementHelper;
+ tagName = tagNameOrFunction;
+ }
+
+ /**
+ * Creates a new UI element constructor.
+ * @param {Object=} opt_propertyBag Optional bag of properties to set on the
+ * object after created. The property {@code ownerDocument} is special
+ * cased and it allows you to create the element in a different
+ * document than the default.
+ * @constructor
+ */
+ function f(opt_propertyBag) {
+ var el = createFunction(tagName, opt_propertyBag);
+ f.decorate(el);
+ for (var propertyName in opt_propertyBag) {
+ el[propertyName] = opt_propertyBag[propertyName];
+ }
+ return el;
+ }
+
+ /**
+ * Decorates an element as a UI element class.
+ * @param {!Element} el The element to decorate.
+ */
+ f.decorate = function(el) {
+ el.__proto__ = f.prototype;
+ el.decorate();
+ };
+
+ return f;
+ }
+
+ return {
+ decorate: decorate,
+ define: define
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/command.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/command.js
new file mode 100644
index 0000000..d1a0677
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/command.js
@@ -0,0 +1,265 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview A command is an abstraction of an action a user can do in the
+ * UI.
+ *
+ * When the focus changes in the document for each command a canExecute event
+ * is dispatched on the active element. By listening to this event you can
+ * enable and disable the command by setting the event.canExecute property.
+ *
+ * When a command is executed a command event is dispatched on the active
+ * element. Note that you should stop the propagation after you have handled the
+ * command if there might be other command listeners higher up in the DOM tree.
+ */
+
+cr.define('cr.ui', function() {
+
+ /**
+ * Creates a new command element.
+ * @constructor
+ * @extends {HTMLElement}
+ */
+ var Command = cr.ui.define('command');
+
+ Command.prototype = {
+ __proto__: HTMLElement.prototype,
+
+ /**
+ * Initializes the command.
+ */
+ decorate: function() {
+ CommandManager.init(this.ownerDocument);
+ },
+
+ /**
+ * Executes the command. This dispatches a command event on the active
+ * element. If the command is {@code disabled} this does nothing.
+ */
+ execute: function() {
+ if (this.disabled)
+ return;
+ var doc = this.ownerDocument;
+ if (doc.activeElement) {
+ var e = new cr.Event('command', true, false);
+ e.command = this;
+ doc.activeElement.dispatchEvent(e);
+ }
+ },
+
+ /**
+ * Call this when there have been changes that might change whether the
+ * command can be executed or not.
+ */
+ canExecuteChange: function() {
+ dispatchCanExecuteEvent(this, this.ownerDocument.activeElement);
+ },
+
+ /**
+ * The keyboard shortcut that triggers the command. This is a string
+ * consisting of a keyIdentifier (as reported by WebKit in keydown) as
+ * well as optional key modifiers joinded with a '-'.
+ * For example:
+ * "F1"
+ * "U+0008-Meta" for Apple command backspace.
+ * "U+0041-Ctrl" for Control A
+ *
+ * @type {string}
+ */
+ shortcut_: null,
+ get shortcut() {
+ return this.shortcut_;
+ },
+ set shortcut(shortcut) {
+ var oldShortcut = this.shortcut_;
+ if (shortcut !== oldShortcut) {
+ this.shortcut_ = shortcut;
+
+ // TODO(arv): Multiple shortcuts?
+
+ var mods = {};
+ var ident = '';
+ shortcut.split('-').forEach(function(part) {
+ var partLc = part.toLowerCase();
+ switch (partLc) {
+ case 'alt':
+ case 'ctrl':
+ case 'meta':
+ case 'shift':
+ mods[partLc + 'Key'] = true;
+ break;
+ default:
+ if (ident)
+ throw Error('Multiple keyboard shortcuts are not supported');
+ ident = part;
+ }
+
+ this.keyIdentifier_ = ident;
+ this.keyModifiers_ = mods;
+ }, this);
+
+ cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
+ oldShortcut);
+ }
+ },
+
+ /**
+ * Whether the event object matches the shortcut for this command.
+ * @param {!Event} e The key event object.
+ * @return {boolean} Whether it matched or not.
+ */
+ matchesEvent: function(e) {
+ if (!this.keyIdentifier_)
+ return false;
+
+ if (e.keyIdentifier == this.keyIdentifier_) {
+ // All keyboard modifiers needs to match.
+ var mods = this.keyModifiers_;
+ return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) {
+ return e[k] == !!mods[k];
+ });
+ }
+ return false;
+ }
+ };
+
+ /**
+ * The label of the command.
+ * @type {string}
+ */
+ cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
+
+ /**
+ * Whether the command is disabled or not.
+ * @type {boolean}
+ */
+ cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
+
+ /**
+ * Whether the command is hidden or not.
+ * @type {boolean}
+ */
+ cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
+
+ /**
+ * Dispatches a canExecute event on the target.
+ * @param {cr.ui.Command} command The command that we are testing for.
+ * @param {Element} target The target element to dispatch the event on.
+ */
+ function dispatchCanExecuteEvent(command, target) {
+ var e = new CanExecuteEvent(command, true)
+ target.dispatchEvent(e);
+ command.disabled = !e.canExecute;
+ }
+
+ /**
+ * The command managers for different documents.
+ */
+ var commandManagers = {};
+
+ /**
+ * Keeps track of the focused element and updates the commands when the focus
+ * changes.
+ * @param {!Document} doc The document that we are managing the commands for.
+ * @constructor
+ */
+ function CommandManager(doc) {
+ doc.addEventListener('focus', cr.bind(this.handleFocus_, this), true);
+ doc.addEventListener('keydown', cr.bind(this.handleKeyDown_, this), true);
+ }
+
+ /**
+ * Initializes a command manager for the document as needed.
+ * @param {!Document} doc The document to manage the commands for.
+ */
+ CommandManager.init = function(doc) {
+ var uid = cr.getUid(doc);
+ if (!(uid in commandManagers)) {
+ commandManagers[uid] = new CommandManager(doc);
+ }
+ },
+
+ CommandManager.prototype = {
+
+ /**
+ * Handles focus changes on the document.
+ * @param {Event} e The focus event object.
+ * @private
+ */
+ handleFocus_: function(e) {
+ var target = e.target;
+ var commands = Array.prototype.slice.call(
+ target.ownerDocument.querySelectorAll('command'));
+
+ commands.forEach(function(command) {
+ dispatchCanExecuteEvent(command, target);
+ });
+ },
+
+ /**
+ * Handles the keydown event and routes it to the right command.
+ * @param {!Event} e The keydown event.
+ */
+ handleKeyDown_: function(e) {
+ var target = e.target;
+ var commands = Array.prototype.slice.call(
+ target.ownerDocument.querySelectorAll('command'));
+
+ for (var i = 0, command; command = commands[i]; i++) {
+ if (!command.disabled && command.matchesEvent(e)) {
+ e.preventDefault();
+ // We do not want any other element to handle this.
+ e.stopPropagation();
+
+ command.execute();
+ return;
+ }
+ }
+ }
+ };
+
+ /**
+ * The event type used for canExecute events.
+ * @param {!cr.ui.Command} command The command that we are evaluating.
+ * @extends {Event}
+ */
+ function CanExecuteEvent(command) {
+ var e = command.ownerDocument.createEvent('Event');
+ e.initEvent('canExecute', true, false);
+ e.__proto__ = CanExecuteEvent.prototype;
+ e.command = command;
+ return e;
+ }
+
+ CanExecuteEvent.prototype = {
+ __proto__: Event.prototype,
+
+ /**
+ * The current command
+ * @type {cr.ui.Command}
+ */
+ command: null,
+
+ /**
+ * Whether the target can execute the command. Setting this also stops the
+ * propagation.
+ * @type {boolean}
+ */
+ canExecute_: false,
+ get canExecute() {
+ return this.canExecute_;
+ },
+ set canExecute(canExecute) {
+ this.canExecute_ = canExecute;
+ this.stopPropagation();
+ }
+ };
+
+ // Export
+ return {
+ Command: Command,
+ CanExecuteEvent: CanExecuteEvent
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/contextmenuhandler.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/contextmenuhandler.js
new file mode 100644
index 0000000..5ac0789
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/contextmenuhandler.js
@@ -0,0 +1,211 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+
+ /**
+ * Handles context menus.
+ * @constructor
+ */
+ function ContextMenuHandler() {}
+
+ ContextMenuHandler.prototype = {
+
+ /**
+ * The menu that we are currently showing.
+ * @type {cr.ui.Menu}
+ */
+ menu_: null,
+ get menu() {
+ return this.menu_;
+ },
+
+ /**
+ * Shows a menu as a context menu.
+ * @param {!Event} e The event triggering the show (usally a contextmenu
+ * event).
+ * @param {!cr.ui.Menu} menu The menu to show.
+ */
+ showMenu: function(e, menu) {
+ this.menu_ = menu;
+
+ menu.style.display = 'block';
+ // when the menu is shown we steal all keyboard events.
+ menu.ownerDocument.addEventListener('keydown', this, true);
+ menu.ownerDocument.addEventListener('mousedown', this, true);
+ menu.ownerDocument.addEventListener('blur', this, true);
+ menu.addEventListener('activate', this);
+ this.positionMenu_(e, menu);
+ },
+
+ /**
+ * Hide the currently shown menu.
+ */
+ hideMenu: function() {
+ var menu = this.menu;
+ if (!menu)
+ return;
+
+ menu.style.display = 'none';
+ menu.ownerDocument.removeEventListener('keydown', this, true);
+ menu.ownerDocument.removeEventListener('mousedown', this, true);
+ menu.ownerDocument.removeEventListener('blur', this, true);
+ menu.removeEventListener('activate', this);
+ menu.selectedIndex = -1;
+ this.menu_ = null;
+
+ // On windows we might hide the menu in a right mouse button up and if
+ // that is the case we wait some short period before we allow the menu
+ // to be shown again.
+ this.hideTimestamp_ = Date.now();
+ },
+
+ /**
+ * Positions the menu
+ * @param {!Event} e The event object triggering the showing.
+ * @param {!cr.ui.Menu} menu The menu to position.
+ * @private
+ */
+ positionMenu_: function(e, menu) {
+ // TODO(arv): Handle scrolled documents when needed.
+
+ var x, y;
+ // When the user presses the context menu key (on the keyboard) we need
+ // to detect this.
+ if (e.screenX == 0 && e.screenY == 0) {
+ var rect = e.currentTarget.getBoundingClientRect();
+ x = rect.left;
+ y = rect.top;
+ } else {
+ x = e.clientX;
+ y = e.clientY;
+ }
+
+ var menuRect = menu.getBoundingClientRect();
+ var bodyRect = menu.ownerDocument.body.getBoundingClientRect();
+
+ // Does menu fit below?
+ if (y + menuRect.height > bodyRect.height) {
+ // Does menu fit above?
+ if (y - menuRect.height >= 0) {
+ y -= menuRect.height;
+ } else {
+ // Menu did not fit above nor below.
+ y = 0;
+ // We could resize the menu here but lets not worry about that at this
+ // point.
+ }
+ }
+
+ // Does menu fit to the right?
+ if (x + menuRect.width > bodyRect.width) {
+ // Does menu fit to the left?
+ if (x - menuRect.width >= 0) {
+ x -= menuRect.width;
+ } else {
+ // Menu did not fit to the right nor to the left.
+ x = 0;
+ // We could resize the menu here but lets not worry about that at this
+ // point.
+ }
+ }
+
+ menu.style.left = x + 'px';
+ menu.style.top = y + 'px';
+ },
+
+ /**
+ * Handles event callbacks.
+ * @param {!Event} e The event object.
+ */
+ handleEvent: function(e) {
+ // Context menu is handled even when we have no menu.
+ if (e.type != 'contextmenu' && !this.menu)
+ return;
+
+ switch (e.type) {
+ case 'mousedown':
+ if (!this.menu.contains(e.target))
+ this.hideMenu();
+ else
+ e.preventDefault();
+ break;
+ case 'keydown':
+ // keyIdentifier does not report 'Esc' correctly
+ if (e.keyCode == 27 /* Esc */) {
+ this.hideMenu();
+
+ // If the menu is visible we let it handle all the keyboard events.
+ } else if (this.menu) {
+ this.menu.handleKeyDown(e);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ break;
+
+ case 'activate':
+ case 'blur':
+ this.hideMenu();
+ break;
+
+ case 'contextmenu':
+ if ((!this.menu || !this.menu.contains(e.target)) &&
+ (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
+ this.showMenu(e, e.currentTarget.contextMenu);
+ e.preventDefault();
+ // Don't allow elements further up in the DOM to show their menus.
+ e.stopPropagation();
+ break;
+ }
+ },
+
+ /**
+ * Adds a contextMenu property to an element or element class.
+ * @param {!Element|!Function} element The element or class to add the
+ * contextMenu property to.
+ */
+ addContextMenuProperty: function(element) {
+ if (typeof element == 'function')
+ element = element.prototype;
+
+ element.__defineGetter__('contextMenu', function() {
+ return this.contextMenu_;
+ });
+ element.__defineSetter__('contextMenu', function(menu) {
+ var oldContextMenu = this.contextMenu;
+
+ if (typeof menu == 'string' && menu[0] == '#') {
+ menu = this.ownerDocument.getElementById(menu.slice(1));
+ cr.ui.decorate(menu, Menu);
+ }
+
+ if (menu === oldContextMenu)
+ return;
+
+ if (oldContextMenu && !menu)
+ this.removeEventListener('contextmenu', contextMenuHandler);
+ if (menu && !oldContextMenu)
+ this.addEventListener('contextmenu', contextMenuHandler);
+
+ this.contextMenu_ = menu;
+
+ if (menu && menu.id)
+ this.setAttribute('contextmenu', '#' + menu.id);
+
+ cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
+ });
+ }
+ };
+
+ /**
+ * The singleton context menu handler.
+ * @type {!ContextMenuHandler}
+ */
+ var contextMenuHandler = new ContextMenuHandler;
+
+ // Export
+ return {
+ contextMenuHandler: contextMenuHandler
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js
new file mode 100644
index 0000000..c70165d
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js
@@ -0,0 +1,229 @@
+
+// require: listselectionmodel.js
+
+/**
+ * @fileoverview This implements a list control.
+ */
+
+cr.define('cr.ui', function() {
+ const ListSelectionModel = cr.ui.ListSelectionModel;
+
+ /**
+ * Creates a new list element.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLUListElement}
+ */
+ var List = cr.ui.define('list');
+
+ List.prototype = {
+ __proto__: HTMLUListElement.prototype,
+
+ /**
+ * The selection model to use.
+ * @type {cr.ui.ListSelectionModel}
+ */
+ get selectionModel() {
+ return this.selectionModel_;
+ },
+ set selectionModel(sm) {
+ var oldSm = this.selectionModel_;
+ if (oldSm == sm)
+ return;
+
+ if (!this.boundHandleOnChange_) {
+ this.boundHandleOnChange_ = cr.bind(this.handleOnChange_, this);
+ this.boundHandleLeadChange_ = cr.bind(this.handleLeadChange_, this);
+ }
+
+ if (oldSm) {
+ oldSm.removeEventListener('change', this.boundHandleOnChange_);
+ oldSm.removeEventListener('leadItemChange', this.boundHandleLeadChange_);
+ }
+
+ this.selectionModel_ = sm;
+
+ if (sm) {
+ sm.addEventListener('change', this.boundHandleOnChange_);
+ sm.addEventListener('leadItemChange', this.boundHandleLeadChange_);
+ }
+ },
+
+ /**
+ * Convenience alias for selectionModel.selectedItem
+ * @type {cr.ui.ListItem}
+ */
+ get selectedItem() {
+ return this.selectionModel.selectedItem;
+ },
+
+ /**
+ * Convenience alias for selectionModel.selectedItems
+ * @type {!Array<cr.ui.ListItem>}
+ */
+ get selectedItems() {
+ return this.selectionModel.selectedItems;
+ },
+
+ /**
+ * The HTML elements representing the items. This is just all the element
+ * children but subclasses may override this to filter out certain elements.
+ * @type {HTMLCollection}
+ */
+ get items() {
+ return this.children;
+ },
+
+ add: function(listItem) {
+ this.appendChild(listItem);
+
+ var uid = cr.getUid(listItem);
+ this.uidToListItem_[uid] = listItem;
+
+ this.selectionModel.add(listItem);
+ },
+
+ addAt: function(listItem, index) {
+ this.insertBefore(listItem, this.items[index]);
+
+ var uid = cr.getUid(listItem);
+ this.uidToListItem_[uid] = listItem;
+
+ this.selectionModel.add(listItem);
+ },
+
+ remove: function(listItem) {
+ this.selectionModel.remove(listItem);
+
+ this.removeChild(listItem);
+
+ var uid = cr.getUid(listItem);
+ delete this.uidToListItem_[uid];
+ },
+
+ clear: function() {
+ this.innerHTML = '';
+ this.selectionModel.clear();
+ },
+
+ /**
+ * Initializes the element.
+ */
+ decorate: function() {
+ this.uidToListItem_ = {};
+
+ this.selectionModel = new ListSelectionModel(this);
+
+ this.addEventListener('mousedown', this.handleMouseDownUp_);
+ this.addEventListener('mouseup', this.handleMouseDownUp_);
+ this.addEventListener('keydown', this.handleKeyDown);
+ this.addEventListener('dblclick', this.handleDoubleClick_);
+
+ // Make list focusable
+ if (!this.hasAttribute('tabindex'))
+ this.tabIndex = 0;
+ },
+
+ /**
+ * Callback for mousedown and mouseup events.
+ * @param {Event} e The mouse event object.
+ * @private
+ */
+ handleMouseDownUp_: function(e) {
+ var target = e.target;
+ while (target && target.parentNode != this) {
+ target = target.parentNode;
+ }
+ this.selectionModel.handleMouseDownUp(e, target);
+ },
+
+ /**
+ * Callback for mousedown events.
+ * @param {Event} e The mouse event object.
+ * @private
+ */
+ handleMouseUp_: function(e) {
+ var target = e.target;
+ while (target && target.parentNode != this) {
+ target = target.parentNode;
+ }
+ if (target) {
+ this.selectionModel.handleMouseDown(e, target);
+ } else {
+ this.selectionModel.clear();
+ }
+ },
+
+
+ /**
+ * Handle a keydown event.
+ * @param {Event} e The keydown event.
+ * @return {boolean} Whether the key event was handled.
+ */
+ handleKeyDown: function(e) {
+ if (this.selectionModel.handleKeyDown(e))
+ return true;
+ if (e.keyIdentifier == 'Enter' && this.selectionModel.selectedItem) {
+ cr.dispatchSimpleEvent(this, 'activate');
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Handler for double clicking. When the user double clicks on a selected
+ * item we dispatch an {@code activate} event.
+ * @param {Event} e The mouse event object.
+ * @private
+ */
+ handleDoubleClick_: function(e) {
+ if (e.button == 0 && this.selectionModel.selectedItem) {
+ cr.dispatchSimpleEvent(this, 'activate');
+ }
+ },
+
+ /**
+ * Callback from the selection model. We dispatch {@code change} events
+ * when the selection changes.
+ * @param {!cr.Event} e Event with change info.
+ * @private
+ */
+ handleOnChange_: function(ce) {
+ ce.changes.forEach(function(change) {
+ var listItem = this.uidToListItem_[change.uid];
+ listItem.selected = change.selected;
+ }, this);
+
+ cr.dispatchSimpleEvent(this, 'change');
+ },
+
+ /**
+ * Handles a change of the lead item from the selection model.
+ * @property {Event} pe The property change event.
+ * @private
+ */
+ handleLeadChange_: function(pe) {
+ if (pe.oldValue) {
+ pe.oldValue.lead = false;
+ }
+ if (pe.newValue) {
+ pe.newValue.lead = true;
+ }
+ },
+
+ /**
+ * Gets a unique ID for an item. This needs to be unique to the list but
+ * does not have to be gloabally unique. This uses {@code cr.getUid} by
+ * default. Override to provide a more efficient way to get the unique ID.
+ * @param {cr.ui.ListItem} item The item to get the unique ID for.
+ * @return
+ */
+ itemToUid: function(item) {
+ return cr.getUid(item);
+ }
+ };
+
+ return {
+ List: List
+ }
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/listitem.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/listitem.js
new file mode 100644
index 0000000..0cd8826
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/listitem.js
@@ -0,0 +1,58 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+
+ /**
+ * Creates a new list item element.
+ * @param {string} opt_label The text label for the item.
+ * @constructor
+ * @extends {HTMLLIElement}
+ */
+ var ListItem = cr.ui.define('li');
+
+ ListItem.prototype = {
+ __proto__: HTMLLIElement.prototype,
+
+ /**
+ * Plain text label.
+ * @type {string}
+ */
+ get label() {
+ return this.textContent;
+ },
+ set label(label) {
+ this.textContent = label;
+ },
+
+ /**
+ * Whether the item is the lead in a selection. Setting this does not update
+ * the underlying selection model. This is only used for display purpose.
+ * @type {boolean}
+ */
+ get lead() {
+ return this.hasAttribute('lead');
+ },
+ set lead(lead) {
+ if (lead) {
+ this.setAttribute('lead', '');
+ this.scrollIntoViewIfNeeded(false);
+ } else {
+ this.removeAttribute('lead');
+ }
+ },
+
+ /**
+ * Called when an element is decorated as a list item.
+ */
+ decorate: function() {
+ }
+ };
+
+ cr.defineProperty(ListItem, 'selected', cr.PropertyKind.BOOL_ATTR);
+
+ return {
+ ListItem: ListItem
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/listselectionmodel.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/listselectionmodel.js
new file mode 100644
index 0000000..2ef83ff
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/listselectionmodel.js
@@ -0,0 +1,423 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+ const Event = cr.Event;
+ const EventTarget = cr.EventTarget;
+
+ /**
+ * Creates a new selection model that is to be used with lists. This is
+ * implemented for vertical lists but changing the behavior for horizontal
+ * lists or icon views is a matter of overriding {@code getItemBefore},
+ * {@code getItemAfter}, {@code getItemAbove} as well as {@code getItemBelow}.
+ *
+ * @constructor
+ * @extends {!cr.EventTarget}
+ */
+ function ListSelectionModel(list) {
+ this.list = list;
+ this.selectedItems_ = {};
+ }
+
+ ListSelectionModel.prototype = {
+ __proto__: EventTarget.prototype,
+
+ /**
+ * Returns the item below (y axis) the given element.
+ * @param {*} item The item to get the item below.
+ * @return {*} The item below or null if not found.
+ */
+ getItemBelow: function(item) {
+ return item.nextElementSibling;
+ },
+
+ /**
+ * Returns the item above (y axis) the given element.
+ * @param {*} item The item to get the item above.
+ * @return {*} The item below or null if not found.
+ */
+ getItemAbove: function(item) {
+ return item.previousElementSibling;
+ },
+
+ /**
+ * Returns the item before (x axis) the given element. This returns null
+ * by default but override this for icon view and horizontal selection
+ * models.
+ *
+ * @param {*} item The item to get the item before.
+ * @return {*} The item before or null if not found.
+ */
+ getItemBefore: function(item) {
+ return null;
+ },
+
+ /**
+ * Returns the item after (x axis) the given element. This returns null
+ * by default but override this for icon view and horizontal selection
+ * models.
+ *
+ * @param {*} item The item to get the item after.
+ * @return {*} The item after or null if not found.
+ */
+ getItemAfter: function(item) {
+ return null;
+ },
+
+ /**
+ * Returns the next list item. This is the next logical and should not
+ * depend on any kind of layout of the list.
+ * @param {*} item The item to get the next item for.
+ * @return {*} The next item or null if not found.
+ */
+ getNextItem: function(item) {
+ return item.nextElementSibling;
+ },
+
+ /**
+ * Returns the prevous list item. This is the previous logical and should
+ * not depend on any kind of layout of the list.
+ * @param {*} item The item to get the previous item for.
+ * @return {*} The previous item or null if not found.
+ */
+ getPreviousItem: function(item) {
+ return item.previousElementSibling;
+ },
+
+ /**
+ * @return {*} The first item.
+ */
+ getFirstItem: function() {
+ return this.list.firstElementChild;
+ },
+
+ /**
+ * @return {*} The last item.
+ */
+ getLastItem: function() {
+ return this.list.lastElementChild;
+ },
+
+ /**
+ * Called by the view when the user does a mousedown or mouseup on the list.
+ * @param {!Event} e The browser mousedown event.
+ * @param {*} item The item that was under the mouse pointer, null if none.
+ */
+ handleMouseDownUp: function(e, item) {
+ var anchorItem = this.anchorItem;
+
+ this.beginChange_();
+
+ if (!item && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
+ this.clear();
+ } else {
+ var isDown = e.type == 'mousedown';
+ if (!cr.isMac && e.ctrlKey) {
+ // Handle ctrlKey on mouseup
+ if (!isDown) {
+ // toggle the current one and make it anchor item
+ this.setItemSelected(item, !this.getItemSelected(item));
+ this.leadItem = item;
+ this.anchorItem = item;
+ }
+ } else if (e.shiftKey && anchorItem && anchorItem != item) {
+ // Shift is done in mousedown
+ if (isDown) {
+ this.clearAllSelected_();
+ this.leadItem = item;
+ this.selectRange(anchorItem, item);
+ }
+ } else {
+ // Right click for a context menu need to not clear the selection.
+ var isRightClick = e.button == 2;
+
+ // If the item is selected this is handled in mouseup.
+ var itemSelected = this.getItemSelected(item);
+ if ((itemSelected && !isDown || !itemSelected && isDown) &&
+ !(itemSelected && isRightClick)) {
+ this.clearAllSelected_();
+ this.setItemSelected(item, true);
+ this.leadItem = item;
+ this.anchorItem = item;
+ }
+ }
+ }
+
+ this.endChange_();
+ },
+
+ /**
+ * Called by the view when it recieves a keydown event.
+ * @param {Event} e The keydown event.
+ */
+ handleKeyDown: function(e) {
+ var newItem = null;
+ var leadItem = this.leadItem;
+ var prevent = true;
+
+ // Ctrl/Meta+A
+ if (e.keyCode == 65 &&
+ (cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) {
+ this.selectAll();
+ e.preventDefault();
+ return;
+ }
+
+ // Space
+ if (e.keyCode == 32) {
+ if (leadItem != null) {
+ var selected = this.getItemSelected(leadItem);
+ if (e.ctrlKey || !selected) {
+ this.beginChange_();
+ this.setItemSelected(leadItem, !selected);
+ this.endChange_();
+ return;
+ }
+ }
+ }
+
+ switch (e.keyIdentifier) {
+ case 'Home':
+ newItem = this.getFirstItem();
+ break;
+ case 'End':
+ newItem = this.getLastItem();
+ break;
+ case 'Up':
+ newItem = !leadItem ?
+ this.getLastItem() : this.getItemAbove(leadItem);
+ break;
+ case 'Down':
+ newItem = !leadItem ?
+ this.getFirstItem() : this.getItemBelow(leadItem);
+ break;
+ case 'Left':
+ newItem = !leadItem ?
+ this.getLastItem() : this.getItemBefore(leadItem);
+ break;
+ case 'Right':
+ newItem = !leadItem ?
+ this.getFirstItem() : this.getItemAfter(leadItem);
+ break;
+ default:
+ prevent = false;
+ }
+
+ if (newItem) {
+ this.beginChange_();
+
+ this.leadItem = newItem;
+ if (e.shiftKey) {
+ var anchorItem = this.anchorItem;
+ this.clearAllSelected_();
+ if (!anchorItem) {
+ this.setItemSelected(newItem, true);
+ this.anchorItem = newItem;
+ } else {
+ this.selectRange(anchorItem, newItem);
+ }
+ } else if (e.ctrlKey && !cr.isMac) {
+ // Setting the lead item is done above
+ // Mac does not allow you to change the lead.
+ } else {
+ this.clearAllSelected_();
+ this.setItemSelected(newItem, true);
+ this.anchorItem = newItem;
+ }
+
+ this.endChange_();
+
+ if (prevent)
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * @type {!Array} The selected items.
+ */
+ get selectedItems() {
+ return Object.keys(this.selectedItems_).map(function(uid) {
+ return this.selectedItems_[uid];
+ }, this);
+ },
+
+ /**
+ * Convenience getter which returns the first selected item.
+ * @type {*}
+ */
+ get selectedItem() {
+ for (var uid in this.selectedItems_) {
+ return this.selectedItems_[uid];
+ }
+ return null;
+ },
+
+ /**
+ * Selects a range of items, starting with {@code start} and ends with
+ * {@code end}.
+ * @param {*} start The first item to select.
+ * @param {*} end The last item to select.
+ */
+ selectRange: function(start, end) {
+ // Swap if starts comes after end.
+ if (start.compareDocumentPosition(end) & Node.DOCUMENT_POSITION_PRECEDING) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+
+ this.beginChange_();
+
+ for (var item = start; item != end; item = this.getNextItem(item)) {
+ this.setItemSelected(item, true);
+ }
+ this.setItemSelected(end, true);
+
+ this.endChange_();
+ },
+
+ /**
+ * Selects all items.
+ */
+ selectAll: function() {
+ this.selectRange(this.getFirstItem(), this.getLastItem());
+ },
+
+ /**
+ * Clears the selection
+ */
+ clear: function() {
+ this.beginChange_();
+ this.clearAllSelected_();
+ this.endChange_();
+ },
+
+ /**
+ * Clears the selection and updates the view.
+ * @private
+ */
+ clearAllSelected_: function() {
+ for (var uid in this.selectedItems_) {
+ this.setItemSelected(this.selectedItems_[uid], false);
+ }
+ },
+
+ /**
+ * Sets the selecte state for an item.
+ * @param {*} item The item to set the selected state for.
+ * @param {boolean} b Whether to select the item or not.
+ */
+ setItemSelected: function(item, b) {
+ var uid = this.list.itemToUid(item);
+ var oldSelected = uid in this.selectedItems_;
+ if (oldSelected == b)
+ return;
+
+ if (b)
+ this.selectedItems_[uid] = item;
+ else
+ delete this.selectedItems_[uid];
+
+ this.beginChange_();
+
+ // Changing back?
+ if (uid in this.changedUids_ && this.changedUids_[uid] == !b) {
+ delete this.changedUids_[uid];
+ } else {
+ this.changedUids_[uid] = b;
+ }
+
+ // End change dispatches an event which in turn may update the view.
+ this.endChange_();
+ },
+
+ /**
+ * Whether a given item is selected or not.
+ * @param {*} item The item to check.
+ * @return {boolean} Whether an item is selected.
+ */
+ getItemSelected: function(item) {
+ var uid = this.list.itemToUid(item);
+ return uid in this.selectedItems_;
+ },
+
+ /**
+ * This is used to begin batching changes. Call {@code endChange_} when you
+ * are done making changes.
+ * @private
+ */
+ beginChange_: function() {
+ if (!this.changeCount_) {
+ this.changeCount_ = 0;
+ this.changedUids_ = {};
+ }
+ this.changeCount_++;
+ },
+
+ /**
+ * Call this after changes are done and it will dispatch a change event if
+ * any changes were actually done.
+ * @private
+ */
+ endChange_: function() {
+ this.changeCount_--;
+ if (!this.changeCount_) {
+ var uids = Object.keys(this.changedUids_);
+ if (uids.length) {
+ var e = new Event('change');
+ e.changes = uids.map(function(uid) {
+ return {
+ uid: uid,
+ selected: this.changedUids_[uid]
+ };
+ }, this);
+ this.dispatchEvent(e);
+ }
+ delete this.changedUids_;
+ delete this.changeCount_;
+ }
+ },
+
+ /**
+ * Called when an item is removed from the lisst.
+ * @param {cr.ui.ListItem} item The list item that was removed.
+ */
+ remove: function(item) {
+ if (item == this.leadItem)
+ this.leadItem = this.getNextItem(item) || this.getPreviousItem(item);
+ if (item == this.anchorItem)
+ this.anchorItem = this.getNextItem(item) || this.getPreviousItem(item);
+
+ // Deselect when removing items.
+ if (this.getItemSelected(item))
+ this.setItemSelected(item, false);
+ },
+
+ /**
+ * Called when an item was added to the list.
+ * @param {cr.ui.ListItem} item The list item to add.
+ */
+ add: function(item) {
+ // We could (should?) check if the item is selected here and update the
+ // selection model.
+ }
+ };
+
+ /**
+ * The anchorItem is used with multiple selection.
+ * @type {*}
+ */
+ cr.defineProperty(ListSelectionModel, 'anchorItem', cr.PropertyKind.JS, null);
+
+ /**
+ * The leadItem is used with multiple selection and it is the item that the
+ * user is moving uysing the arrow keys.
+ * @type {*}
+ */
+ cr.defineProperty(ListSelectionModel, 'leadItem', cr.PropertyKind.JS, null);
+
+ return {
+ ListSelectionModel: ListSelectionModel
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/menu.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/menu.js
new file mode 100644
index 0000000..1145d0f
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/menu.js
@@ -0,0 +1,157 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+
+ const MenuItem = cr.ui.MenuItem;
+
+ /**
+ * Creates a new menu element.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLMenuElement}
+ */
+ var Menu = cr.ui.define('menu');
+
+ Menu.prototype = {
+ __proto__: HTMLMenuElement.prototype,
+
+ /**
+ * Initializes the menu element.
+ */
+ decorate: function() {
+ this.addEventListener('mouseover', this.handleMouseOver_);
+ this.addEventListener('mouseout', this.handleMouseOut_);
+
+ // Decorate the children as menu items.
+ var children = this.children;
+ for (var i = 0, child; child = children[i]; i++) {
+ cr.ui.decorate(child, MenuItem);
+ }
+ },
+
+ /**
+ * Walks up the ancestors until a menu item belonging to this menu is found.
+ * @param {Element} el
+ * @return {cr.ui.MenuItem} The found menu item or null.
+ * @private
+ */
+ findMenuItem_: function(el) {
+ while (el && el.parentNode != this) {
+ el = el.parentNode;
+ }
+ return el;
+ },
+
+ /**
+ * Handles mouseover events and selects the hovered item.
+ * @param {Event} e The mouseover event.
+ * @private
+ */
+ handleMouseOver_: function(e) {
+ var overItem = this.findMenuItem_(e.target);
+ this.selectedItem = overItem;
+ },
+
+ /**
+ * Handles mouseout events and deselects any selected item.
+ * @param {Event} e The mouseout event.
+ * @private
+ */
+ handleMouseOut_: function(e) {
+ this.selectedItem = null;
+ },
+
+ /**
+ * The index of the selected item.
+ * @type {boolean}
+ */
+ // getter and default value is defined using cr.defineProperty.
+ set selectedIndex(selectedIndex) {
+ if (this.selectedIndex_ != selectedIndex) {
+ var oldSelectedItem = this.selectedItem;
+ this.selectedIndex_ = selectedIndex;
+ if (oldSelectedItem)
+ oldSelectedItem.selected = false;
+ var item = this.selectedItem;
+ if (item)
+ item.selected = true;
+
+ cr.dispatchSimpleEvent(this, 'change');
+ }
+ },
+
+ /**
+ * The selected menu item or null if none.
+ * @type {cr.ui.MenuItem}
+ */
+ get selectedItem() {
+ return this.children[this.selectedIndex];
+ },
+ set selectedItem(item) {
+ var index = Array.prototype.indexOf.call(this.children, item);
+ this.selectedIndex = index;
+ },
+
+ /**
+ * This is the function that handles keyboard navigation. This is usually
+ * called by the element responsible for managing the menu.
+ * @param {Event} e The keydown event object.
+ * @return {boolean} Whether the event was handled be the menu.
+ */
+ handleKeyDown: function(e) {
+ var item = this.selectedItem;
+
+ var self = this;
+ function selectNextVisible(m) {
+ var children = self.children;
+ var len = children.length;
+ var i = self.selectedIndex;
+ if (i == -1 && m == -1) {
+ // Edge case when we need to go the last item fisrt.
+ i = 0;
+ }
+ while (true) {
+ i = (i + m + len) % len;
+ item = children[i];
+ if (item && !item.isSeparator() && !item.hidden)
+ break;
+ }
+ if (item)
+ self.selectedIndex = i;
+ }
+
+ switch (e.keyIdentifier) {
+ case 'Down':
+ selectNextVisible(1);
+ return true;
+ case 'Up':
+ selectNextVisible(-1);
+ return true;
+ case 'Enter':
+ case 'U+0020': // Space
+ if (item) {
+ if (cr.dispatchSimpleEvent(item, 'activate', true, true)) {
+ if (item.command)
+ item.command.execute();
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+ };
+
+ /**
+ * The selected menu item.
+ * @type {number}
+ */
+ cr.defineProperty(Menu, 'selectedIndex', cr.PropertyKind.JS, -1);
+
+ // Export
+ return {
+ Menu: Menu
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/menubutton.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/menubutton.js
new file mode 100644
index 0000000..469cfd8
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/menubutton.js
@@ -0,0 +1,166 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+ const Menu = cr.ui.Menu;
+
+ /**
+ * Creates a new menu button element.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLButtonElement}
+ */
+ var MenuButton = cr.ui.define('button');
+
+ MenuButton.prototype = {
+ __proto__: HTMLButtonElement.prototype,
+
+ /**
+ * Initializes the menu button.
+ */
+ decorate: function() {
+ this.addEventListener('mousedown', this);
+ this.addEventListener('keydown', this);
+
+ var menu;
+ if ((menu = this.getAttribute('menu')))
+ this.menu = menu;
+ },
+
+ /**
+ * The menu associated with the menu button.
+ * @type {cr.ui.Menu}
+ */
+ get menu() {
+ return this.menu_;
+ },
+ set menu(menu) {
+ if (typeof menu == 'string' && menu[0] == '#') {
+ menu = this.ownerDocument.getElementById(menu.slice(1));
+ cr.ui.decorate(menu, Menu);
+ }
+
+ this.menu_ = menu;
+ if (menu) {
+ if (menu.id)
+ this.setAttribute('menu', '#' + menu.id);
+ }
+ },
+
+ /**
+ * Handles event callbacks.
+ * @param {Event} e The event object.
+ */
+ handleEvent: function(e) {
+ if (!this.menu)
+ return;
+
+ switch (e.type) {
+ case 'mousedown':
+ if (e.currentTarget == this.ownerDocument) {
+ if (!this.contains(e.target) && !this.menu.contains(e.target))
+ this.hideMenu();
+ else
+ e.preventDefault();
+ } else {
+ if (this.isMenuShown()) {
+ this.hideMenu();
+ } else {
+ this.showMenu();
+ // Prevent the button from stealing focus on mousedown.
+ e.preventDefault();
+ }
+ }
+ break;
+ case 'keydown':
+ this.handleKeyDown(e);
+ // If the menu is visible we let it handle all the keyboard events.
+ if (this.isMenuShown() && e.currentTarget == this.ownerDocument) {
+ this.menu.handleKeyDown(e);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ break;
+
+ case 'activate':
+ case 'blur':
+ this.hideMenu();
+ break;
+ }
+ },
+
+ /**
+ * Shows the menu.
+ */
+ showMenu: function() {
+ this.menu.style.display = 'block';
+ // when the menu is shown we steal all keyboard events.
+ this.ownerDocument.addEventListener('keydown', this, true);
+ this.ownerDocument.addEventListener('mousedown', this, true);
+ this.ownerDocument.addEventListener('blur', this, true);
+ this.menu.addEventListener('activate', this);
+ this.positionMenu_();
+ },
+
+ /**
+ * Hides the menu.
+ */
+ hideMenu: function() {
+ this.menu.style.display = 'none';
+ this.ownerDocument.removeEventListener('keydown', this, true);
+ this.ownerDocument.removeEventListener('mousedown', this, true);
+ this.ownerDocument.removeEventListener('blur', this, true);
+ this.menu.removeEventListener('activate', this);
+ this.menu.selectedIndex = -1;
+ },
+
+ /**
+ * Whether the menu is shown.
+ */
+ isMenuShown: function() {
+ return window.getComputedStyle(this.menu).display != 'none';
+ },
+
+ /**
+ * Positions the menu below the menu button. At this point we do not use any
+ * advanced positioning logic to ensure the menu fits in the viewport.
+ * @private
+ */
+ positionMenu_: function() {
+ var buttonRect = this.getBoundingClientRect();
+ this.menu.style.top = buttonRect.bottom + 'px';
+ if (getComputedStyle(this).direction == 'rtl') {
+ var menuRect = this.menu.getBoundingClientRect();
+ this.menu.style.left = buttonRect.right - menuRect.width + 'px';
+ } else {
+ this.menu.style.left = buttonRect.left + 'px';
+ }
+ },
+
+ /**
+ * Handles the keydown event for the menu button.
+ */
+ handleKeyDown: function(e) {
+ switch (e.keyIdentifier) {
+ case 'Down':
+ case 'Up':
+ case 'Enter':
+ case 'U+0020': // Space
+ if (!this.isMenuShown())
+ this.showMenu();
+ e.preventDefault();
+ break;
+ case 'Esc':
+ case 'U+001B': // Maybe this is remote desktop playing a prank?
+ this.hideMenu();
+ break;
+ }
+ }
+ };
+
+ // Export
+ return {
+ MenuButton: MenuButton
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/menuitem.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/menuitem.js
new file mode 100644
index 0000000..5c66f17
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/menuitem.js
@@ -0,0 +1,147 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+ const Command = cr.ui.Command;
+
+ /**
+ * Creates a new menu item element.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLButtonElement}
+ */
+ var MenuItem = cr.ui.define('button');
+
+ /**
+ * Creates a new menu separator element.
+ * @return {cr.ui.MenuItem}
+ */
+ MenuItem.createSeparator = function() {
+ var el = cr.doc.createElement('hr');
+ MenuItem.decorate(el);
+ return el;
+ };
+
+ MenuItem.prototype = {
+ __proto__: HTMLButtonElement.prototype,
+
+ /**
+ * Initializes the menu item.
+ */
+ decorate: function() {
+ var commandId;
+ if ((commandId = this.getAttribute('command')))
+ this.command = commandId;
+
+ this.addEventListener('mouseup', this.handleMouseUp_);
+ },
+
+ /**
+ * The command associated with this menu item. If this is set to a string
+ * of the form "#element-id" then the element is looked up in the document
+ * of the command.
+ * @type {cr.ui.Command}
+ */
+ command_: null,
+ get command() {
+ return this.command_;
+ },
+ set command(command) {
+ if (this.command_) {
+ this.command_.removeEventListener('labelChange', this);
+ this.command_.removeEventListener('disabledChange', this);
+ this.command_.removeEventListener('hiddenChange', this);
+ }
+
+ if (typeof command == 'string' && command[0] == '#') {
+ command = this.ownerDocument.getElementById(command.slice(1));
+ cr.ui.decorate(command, Command);
+ }
+
+ this.command_ = command;
+ if (command) {
+ if (command.id)
+ this.setAttribute('command', '#' + command.id);
+
+ this.label = command.label;
+ this.disabled = command.disabled;
+ this.hidden = command.hidden;
+
+ this.command_.addEventListener('labelChange', this);
+ this.command_.addEventListener('disabledChange', this);
+ this.command_.addEventListener('hiddenChange', this);
+ }
+ },
+
+ /**
+ * The text label.
+ * @type {string}
+ */
+ get label() {
+ return this.textContent;
+ },
+ set label(label) {
+ this.textContent = label;
+ },
+
+ /**
+ * @return {boolean} Whether the menu item is a separator.
+ */
+ isSeparator: function() {
+ return this.tagName == 'HR';
+ },
+
+ /**
+ * Handles mouseup events. This dispatches an active event and if there
+ * is an assiciated command then that is executed.
+ * @param {Event} The mouseup event object.
+ * @private
+ */
+ handleMouseUp_: function(e) {
+ if (!this.disabled && !this.isSeparator()) {
+ // Dispatch command event followed by executing the command object.
+ if (cr.dispatchSimpleEvent(this, 'activate', true, true)) {
+ var command = this.command;
+ if (command)
+ command.execute();
+ }
+ }
+ },
+
+ /**
+ * Handles changes to the associated command.
+ * @param {Event} e The event object.
+ */
+ handleEvent: function(e) {
+ switch (e.type) {
+ case 'disabledChange':
+ this.disabled = this.command.disabled;
+ break;
+ case 'hiddenChange':
+ this.hidden = this.command.hidden;
+ break;
+ case 'labelChange':
+ this.label = this.command.label;
+ break;
+ }
+ }
+ };
+
+ /**
+ * Whether the menu item is hidden or not.
+ * @type {boolean}
+ */
+ cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR);
+
+ /**
+ * Whether the menu item is selected or not.
+ * @type {boolean}
+ */
+ cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR);
+
+ // Export
+ return {
+ MenuItem: MenuItem
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js
new file mode 100644
index 0000000..c06fad6
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js
@@ -0,0 +1,507 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.ui', function() {
+
+ /**
+ * Helper function that finds the first ancestor tree item.
+ * @param {!Element} el The element to start searching from.
+ * @return {cr.ui.TreeItem} The found tree item or null if not found.
+ */
+ function findTreeItem(el) {
+ while (el && !(el instanceof TreeItem)) {
+ el = el.parentNode;
+ }
+ return el;
+ }
+
+ /**
+ * Creates a new tree element.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLElement}
+ */
+ var Tree = cr.ui.define('tree');
+
+ Tree.prototype = {
+ __proto__: HTMLElement.prototype,
+
+ /**
+ * Initializes the element.
+ */
+ decorate: function() {
+ // Make list focusable
+ if (!this.hasAttribute('tabindex'))
+ this.tabIndex = 0;
+
+ this.addEventListener('click', this.handleClick);
+ this.addEventListener('mousedown', this.handleMouseDown);
+ this.addEventListener('dblclick', this.handleDblClick);
+ this.addEventListener('keydown', this.handleKeyDown);
+ },
+
+ /**
+ * Returns the tree item that are children of this tree.
+ */
+ get items() {
+ return this.children;
+ },
+
+ /**
+ * Adds a tree item to the tree.
+ * @param {!cr.ui.TreeItem} treeItem The item to add.
+ */
+ add: function(treeItem) {
+ this.appendChild(treeItem);
+ },
+
+ /**
+ * Adds a tree item at the given index.
+ * @param {!cr.ui.TreeItem} treeItem The item to add.
+ * @param {number} index The index where we want to add the item.
+ */
+ addAt: function(treeItem, index) {
+ this.insertBefore(treeItem, this.children[index]);
+ },
+
+ /**
+ * Removes a tree item child.
+ * @param {!cr.ui.TreeItem} treeItem The tree item to remove.
+ */
+ remove: function(treeItem) {
+ this.removeChild(treeItem);
+ },
+
+ /**
+ * Handles click events on the tree and forwards the event to the relevant
+ * tree items as necesary.
+ * @param {Event} e The click event object.
+ */
+ handleClick: function(e) {
+ var treeItem = findTreeItem(e.target);
+ if (treeItem)
+ treeItem.handleClick(e);
+ },
+
+ handleMouseDown: function(e) {
+ if (e.button == 2) // right
+ this.handleClick(e);
+ },
+
+ /**
+ * Handles double click events on the tree.
+ * @param {Event} e The dblclick event object.
+ */
+ handleDblClick: function(e) {
+ var treeItem = findTreeItem(e.target);
+ if (treeItem)
+ treeItem.expanded = !treeItem.expanded;
+ },
+
+ /**
+ * Handles keydown events on the tree and updates selection and exanding
+ * of tree items.
+ * @param {Event} e The click event object.
+ */
+ handleKeyDown: function(e) {
+ var itemToSelect;
+ if (e.ctrlKey)
+ return;
+
+ var item = this.selectedItem;
+
+ var rtl = window.getComputedStyle(item).direction == 'rtl';
+
+ switch (e.keyIdentifier) {
+ case 'Up':
+ itemToSelect = item ? getPrevious(item) :
+ this.items[this.items.length - 1];
+ break;
+ case 'Down':
+ itemToSelect = item ? getNext(item) :
+ this.items[0];
+ break;
+ case 'Left':
+ case 'Right':
+ // Don't let back/forward keyboard shortcuts be used.
+ if (!cr.isMac && e.altKey || cr.isMac && e.metaKey)
+ break;
+
+ if (e.keyIdentifier == 'Left' && !rtl ||
+ e.keyIdentifier == 'Right' && rtl) {
+ if (item.expanded)
+ item.expanded = false;
+ else
+ itemToSelect = findTreeItem(item.parentNode);
+ } else {
+ if (!item.expanded)
+ item.expanded = true;
+ else
+ itemToSelect = item.items[0];
+ }
+ break;
+ case 'Home':
+ itemToSelect = this.items[0];
+ break;
+ case 'End':
+ itemToSelect = this.items[this.items.length - 1];
+ break;
+ }
+
+ if (itemToSelect) {
+ itemToSelect.selected = true;
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * The selected tree item or null if none.
+ * @type {cr.ui.TreeItem}
+ */
+ get selectedItem() {
+ return this.selectedItem_ || null;
+ },
+ set selectedItem(item) {
+ var oldSelectedItem = this.selectedItem_;
+ if (oldSelectedItem != item) {
+ // Set the selectedItem_ before deselecting the old item since we only
+ // want one change when moving between items.
+ this.selectedItem_ = item;
+
+ if (oldSelectedItem)
+ oldSelectedItem.selected = false;
+
+ if (item)
+ item.selected = true;
+
+ cr.dispatchSimpleEvent(this, 'change');
+ }
+ }
+ };
+
+ /**
+ * This is used as a blueprint for new tree item elements.
+ * @type {!HTMLElement}
+ */
+ var treeItemProto = (function() {
+ var treeItem = cr.doc.createElement('div');
+ treeItem.className = 'tree-item';
+ treeItem.innerHTML = '<div class=tree-row>' +
+ '<span class=expand-icon></span>' +
+ '<span class=tree-label></span>' +
+ '</div>' +
+ '<div class=tree-children></div>';
+ return treeItem;
+ })();
+
+ /**
+ * Creates a new tree item.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLElement}
+ */
+ var TreeItem = cr.ui.define(function() {
+ return treeItemProto.cloneNode(true);
+ });
+
+ TreeItem.prototype = {
+ __proto__: HTMLElement.prototype,
+
+ /**
+ * Initializes the element.
+ */
+ decorate: function() {
+
+ },
+
+ /**
+ * The tree items children.
+ */
+ get items() {
+ return this.lastElementChild.children;
+ },
+
+ /**
+ * Adds a tree item as a child.
+ * @param {!cr.ui.TreeItem} child The child to add.
+ */
+ add: function(child) {
+ this.addAt(child, 0xffffffff);
+ },
+
+ /**
+ * Adds a tree item as a child at a given index.
+ * @param {!cr.ui.TreeItem} child The child to add.
+ * @param {number} index The index where to add the child.
+ */
+ addAt: function(child, index) {
+ this.lastElementChild.insertBefore(child, this.items[index]);
+ if (this.items.length == 1)
+ this.hasChildren_ = true;
+ },
+
+ /**
+ * Removes a child.
+ * @param {!cr.ui.TreeItem} child The tree item child to remove.
+ */
+ remove: function(child) {
+ // If we removed the selected item we should become selected.
+ var tree = this.tree;
+ var selectedItem = tree.selectedItem;
+ if (selectedItem && child.contains(selectedItem))
+ this.selected = true;
+
+ this.lastElementChild.removeChild(child);
+ if (this.items.length == 0)
+ this.hasChildren_ = false;
+ },
+
+ /**
+ * The parent tree item.
+ * @type {!cr.ui.Tree|cr.ui.TreeItem}
+ */
+ get parentItem() {
+ var p = this.parentNode;
+ while (p && !(p instanceof TreeItem) && !(p instanceof Tree)) {
+ p = p.parentNode;
+ }
+ return p;
+ },
+
+ /**
+ * The tree that the tree item belongs to or null of no added to a tree.
+ * @type {cr.ui.Tree}
+ */
+ get tree() {
+ var t = this.parentItem;
+ while (t && !(t instanceof Tree)) {
+ t = t.parentItem;
+ }
+ return t;
+ },
+
+ /**
+ * Whether the tree item is expanded or not.
+ * @type {boolean}
+ */
+ get expanded() {
+ return this.hasAttribute('expanded');
+ },
+ set expanded(b) {
+ if (this.expanded == b)
+ return;
+
+ var treeChildren = this.lastElementChild;
+
+ if (b) {
+ if (this.mayHaveChildren_) {
+ this.setAttribute('expanded', '');
+ treeChildren.setAttribute('expanded', '');
+ cr.dispatchSimpleEvent(this, 'expand', true, false);
+ this.scrollIntoViewIfNeeded(false);
+ }
+ } else {
+ var tree = this.tree;
+ if (tree && !this.selected) {
+ var oldSelected = tree.selectedItem;
+ if (oldSelected && this.contains(oldSelected))
+ this.selected = true;
+ }
+ this.removeAttribute('expanded');
+ treeChildren.removeAttribute('expanded');
+ cr.dispatchSimpleEvent(this, 'collapse', true, false);
+ }
+ },
+
+ /**
+ * Expands all parent items.
+ */
+ reveal: function() {
+ var pi = this.parentItem;
+ while (!(pi instanceof Tree)) {
+ pi.expanded = true;
+ pi = pi.parentItem;
+ }
+ },
+
+ /**
+ * The element representing the row that gets highlighted.
+ * @type {!HTMLElement}
+ */
+ get rowElement() {
+ return this.firstElementChild;
+ },
+
+ /**
+ * The element containing the label text and the icon.
+ * @type {!HTMLElement}
+ */
+ get labelElement() {
+ return this.firstElementChild.lastElementChild;
+ },
+
+ /**
+ * The label text.
+ * @type {string}
+ */
+ get label() {
+ return this.labelElement.textContent;
+ },
+ set label(s) {
+ this.labelElement.textContent = s;
+ },
+
+ /**
+ * The URL for the icon.
+ * @type {string}
+ */
+ get icon() {
+ return window.getComputedStyle(this.labelElement).
+ backgroundImage.slice(4, -1);
+ },
+ set icon(icon) {
+ return this.labelElement.style.backgroundImage = url(icon);
+ },
+
+ /**
+ * Whether the tree item is selected or not.
+ * @type {boolean}
+ */
+ get selected() {
+ return this.hasAttribute('selected');
+ },
+ set selected(b) {
+ if (this.selected == b)
+ return;
+ var rowItem = this.firstElementChild;
+ var tree = this.tree;
+ if (b) {
+ this.setAttribute('selected', '');
+ rowItem.setAttribute('selected', '');
+ this.labelElement.scrollIntoViewIfNeeded(false);
+ if (tree)
+ tree.selectedItem = this;
+ } else {
+ this.removeAttribute('selected');
+ rowItem.removeAttribute('selected');
+ if (tree && tree.selectedItem == this)
+ tree.selectedItem = null;
+ }
+ },
+
+ /**
+ * Whether the tree item has children.
+ * @type {boolean}
+ */
+ get mayHaveChildren_() {
+ return this.hasAttribute('may-have-children');
+ },
+ set mayHaveChildren_(b) {
+ var rowItem = this.firstElementChild;
+ if (b) {
+ this.setAttribute('may-have-children', '');
+ rowItem.setAttribute('may-have-children', '');
+ } else {
+ this.removeAttribute('may-have-children');
+ rowItem.removeAttribute('may-have-children');
+ }
+ },
+
+ /**
+ * Whether the tree item has children.
+ * @type {boolean}
+ */
+ get hasChildren() {
+ return !!this.items[0];
+ },
+
+ /**
+ * Whether the tree item has children.
+ * @type {boolean}
+ * @private
+ */
+ set hasChildren_(b) {
+ var rowItem = this.firstElementChild;
+ this.setAttribute('has-children', b);
+ rowItem.setAttribute('has-children', b);
+ if (b)
+ this.mayHaveChildren_ = true;
+ },
+
+ /**
+ * Called when the user clicks on a tree item. This is forwarded from the
+ * cr.ui.Tree.
+ * @param {Event} e The click event.
+ */
+ handleClick: function(e) {
+ if (e.target.className == 'expand-icon')
+ this.expanded = !this.expanded;
+ else
+ this.selected = true;
+ }
+ };
+
+ /**
+ * Helper function that returns the next visible tree item.
+ * @param {cr.ui.TreeItem} item The tree item.
+ * @retrun {cr.ui.TreeItem} The found item or null.
+ */
+ function getNext(item) {
+ if (item.expanded) {
+ var firstChild = item.items[0];
+ if (firstChild) {
+ return firstChild;
+ }
+ }
+
+ return getNextHelper(item);
+ }
+
+ /**
+ * Another helper function that returns the next visible tree item.
+ * @param {cr.ui.TreeItem} item The tree item.
+ * @retrun {cr.ui.TreeItem} The found item or null.
+ */
+ function getNextHelper(item) {
+ if (!item)
+ return null;
+
+ var nextSibling = item.nextElementSibling;
+ if (nextSibling) {
+ return nextSibling;
+ }
+ return getNextHelper(item.parentItem);
+ }
+
+ /**
+ * Helper function that returns the previous visible tree item.
+ * @param {cr.ui.TreeItem} item The tree item.
+ * @retrun {cr.ui.TreeItem} The found item or null.
+ */
+ function getPrevious(item) {
+ var previousSibling = item.previousElementSibling;
+ return previousSibling ? getLastHelper(previousSibling) : item.parentItem;
+ }
+
+ /**
+ * Helper function that returns the last visible tree item in the subtree.
+ * @param {cr.ui.TreeItem} item The item to find the last visible item for.
+ * @return {cr.ui.TreeItem} The found item or null.
+ */
+ function getLastHelper(item) {
+ if (!item)
+ return null;
+ if (item.expanded && item.hasChildren) {
+ var lastChild = item.items[item.items.length - 1];
+ return getLastHelper(lastChild);
+ }
+ return item;
+ }
+
+ // Export
+ return {
+ Tree: Tree,
+ TreeItem: TreeItem
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/cr_test.html b/chrome/browser/resources/bookmark_manager/js/cr_test.html
new file mode 100644
index 0000000..5d6c16e
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr_test.html
@@ -0,0 +1,231 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title></title>
+<style>
+
+</style>
+<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+<script src="cr.js"></script>
+<script src="cr/event.js"></script>
+<script src="cr/eventtarget.js"></script>
+<script>
+
+goog.require('goog.testing.jsunit');
+
+</script>
+
+</head>
+<body>
+
+<script>
+
+const EventTarget = cr.EventTarget;
+
+function testDefineProperty() {
+ var obj = new EventTarget;
+ cr.defineProperty(obj, 'test');
+
+ obj.test = 1;
+ assertEquals(1, obj.test);
+ assertEquals(1, obj.test_);
+}
+
+function testDefinePropertyOnClass() {
+ function C() {}
+ C.prototype = {
+ __proto__: EventTarget.prototype
+ };
+
+ cr.defineProperty(C, 'test');
+
+ var obj = new C;
+ obj.test = 1;
+ assertEquals(1, obj.test);
+ assertEquals(1, obj.test_);
+}
+
+function testDefinePropertyWithDefault() {
+ var obj = new EventTarget;
+
+ cr.defineProperty(obj, 'test', null, 1);
+
+ assertEquals(1, obj.test);
+ assertEquals(1, obj.test_);
+
+ obj.test = 2;
+ assertEquals(2, obj.test);
+ assertEquals(2, obj.test_);
+}
+
+function testDefinePropertyEvent() {
+ var obj = new EventTarget;
+ cr.defineProperty(obj, 'test');
+ obj.test = 1;
+
+ var count = 0;
+ function f(e) {
+ assertEquals('testChange', e.type);
+ assertEquals('test', e.propertyName);
+ assertEquals(1, e.oldValue);
+ assertEquals(2, e.newValue);
+ count++;
+ }
+
+ obj.addEventListener('testChange', f);
+ obj.test = 2;
+ assertEquals(2, obj.test);
+ assertEquals('Should have called the property change listener', 1, count);
+
+ obj.test = 2;
+ assertEquals(1, count);
+}
+
+function testDefinePropertyEventWithDefault() {
+ var obj = new EventTarget;
+ cr.defineProperty(obj, 'test', cr.PropertyKind.JS, 1);
+
+ var count = 0;
+ function f(e) {
+ assertEquals('testChange', e.type);
+ assertEquals('test', e.propertyName);
+ assertEquals(1, e.oldValue);
+ assertEquals(2, e.newValue);
+ count++;
+ }
+
+ obj.addEventListener('testChange', f);
+
+ obj.test = 1;
+ assertEquals('Should not have called the property change listener', 0, count);
+
+ obj.test = 2;
+ assertEquals(2, obj.test);
+ assertEquals('Should have called the property change listener', 1, count);
+
+ obj.test = 2;
+ assertEquals(1, count);
+}
+
+function testDefinePropertyAttr() {
+ var obj = document.createElement('div');
+ cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR);
+
+ obj.test = 'a';
+ assertEquals('a', obj.test);
+ assertEquals('a', obj.getAttribute('test'));
+}
+
+function testDefinePropertyAttrOnClass() {
+ var obj = document.createElement('button');
+ cr.defineProperty(HTMLButtonElement, 'test', cr.PropertyKind.ATTR);
+
+ obj.test = 'a';
+ assertEquals('a', obj.test);
+ assertEquals('a', obj.getAttribute('test'));
+}
+
+function testDefinePropertyAttrWithDefault() {
+ var obj = document.createElement('div');
+ cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR, 'a');
+
+ assertEquals('a', obj.test);
+ assertFalse(obj.hasAttribute('test'));
+
+ obj.test = 'b';
+ assertEquals('b', obj.test);
+ assertEquals('b', obj.getAttribute('test'));
+}
+
+function testDefinePropertyAttrEvent() {
+ var obj = document.createElement('div');
+ cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR);
+ obj.test = 'a';
+
+ var count = 0;
+ function f(e) {
+ assertEquals('testChange', e.type);
+ assertEquals('test', e.propertyName);
+ assertEquals('a', e.oldValue);
+ assertEquals('b', e.newValue);
+ count++;
+ }
+
+ obj.addEventListener('testChange', f);
+ obj.test = 'b';
+ assertEquals('b', obj.test);
+ assertEquals('Should have called the property change listener', 1, count);
+
+ obj.test = 'b';
+ assertEquals(1, count);
+}
+
+function testDefinePropertyAttrEventWithDefault() {
+ var obj = document.createElement('div');
+ cr.defineProperty(obj, 'test', cr.PropertyKind.ATTR, 'a');
+
+ var count = 0;
+ function f(e) {
+ assertEquals('testChange', e.type);
+ assertEquals('test', e.propertyName);
+ assertEquals('a', e.oldValue);
+ assertEquals('b', e.newValue);
+ count++;
+ }
+
+ obj.addEventListener('testChange', f);
+
+ obj.test = 'a';
+ assertEquals('Should not have called the property change listener', 0, count);
+
+ obj.test = 'b';
+ assertEquals('b', obj.test);
+ assertEquals('Should have called the property change listener', 1, count);
+
+ obj.test = 'b';
+ assertEquals(1, count);
+}
+
+function testDefinePropertyBoolAttr() {
+ var obj = document.createElement('div');
+ cr.defineProperty(obj, 'test', cr.PropertyKind.BOOL_ATTR);
+
+ assertFalse(obj.test);
+ assertFalse(obj.hasAttribute('test'));
+
+ obj.test = true;
+ assertTrue(obj.test);
+ assertTrue(obj.hasAttribute('test'));
+
+ obj.test = false;
+ assertFalse(obj.test);
+ assertFalse(obj.hasAttribute('test'));
+}
+
+function testDefinePropertyBoolAttrEvent() {
+ var obj = document.createElement('div');
+ cr.defineProperty(obj, 'test', cr.PropertyKind.BOOL_ATTR);
+
+ var count = 0;
+ function f(e) {
+ assertEquals('testChange', e.type);
+ assertEquals('test', e.propertyName);
+ assertEquals(false, e.oldValue);
+ assertEquals(true, e.newValue);
+ count++;
+ }
+
+ obj.addEventListener('testChange', f);
+ obj.test = true;
+ assertTrue(obj.test);
+ assertEquals('Should have called the property change listener', 1, count);
+
+ obj.test = true;
+ assertEquals(1, count);
+}
+
+
+</script>
+
+</body>
+</html>
diff --git a/chrome/browser/resources/bookmark_manager/js/i18ntemplate.js b/chrome/browser/resources/bookmark_manager/js/i18ntemplate.js
new file mode 100644
index 0000000..8166ddc
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/i18ntemplate.js
@@ -0,0 +1,104 @@
+/**
+ * @fileoverview This is a simple template engine inspired by JsTemplates
+ * optimized for i18n.
+ *
+ * It currently supports two handlers:
+ *
+ * * i18n-content which sets the textContent of the element
+ *
+ * <span i18n-content="myContent"></span>
+ * i18nTemplate.process(element, {'myContent': 'Content'});
+ *
+ * * i18n-values is a list of attribute-value or property-value pairs.
+ * Properties are prefixed with a '.' and can contain nested properties.
+ *
+ * <span i18n-values="title:myTitle;.style.fontSize:fontSize"></span>
+ * i18nTemplate.process(element, {
+ * 'myTitle': 'Title',
+ * 'fontSize': '13px'
+ * });
+ */
+
+var i18nTemplate = (function() {
+ /**
+ * This provides the handlers for the templating engine. The key is used as
+ * the attribute name and the value is the function that gets called for every
+ * single node that has this attribute.
+ * @type {Object}
+ */
+ var handlers = {
+ /**
+ * This handler sets the textContent of the element.
+ */
+ 'i18n-content': function(element, attributeValue, obj) {
+ element.textContent = obj[attributeValue];
+ },
+
+ /**
+ * This is used to set HTML attributes and DOM properties,. The syntax is:
+ * attributename:key;
+ * .domProperty:key;
+ * .nested.dom.property:key
+ */
+ 'i18n-values': function(element, attributeValue, obj) {
+ var parts = attributeValue.replace(/\s/g, '').split(/;/);
+ for (var j = 0; j < parts.length; j++) {
+ var a = parts[j].match(/^([^:]+):(.+)$/);
+ if (a) {
+ var propName = a[1];
+ var propExpr = a[2];
+
+ // Ignore missing properties
+ if (propExpr in obj) {
+ var value = obj[propExpr];
+ if (propName.charAt(0) == '.') {
+ var path = propName.slice(1).split('.');
+ var object = element;
+ while (object && path.length > 1) {
+ object = object[path.shift()];
+ }
+ if (object) {
+ object[path] = value;
+ // In case we set innerHTML (ignoring others) we need to
+ // recursively check the content
+ if (path == 'innerHTML') {
+ process(element, obj);
+ }
+ }
+ } else {
+ element.setAttribute(propName, value);
+ }
+ } else {
+ console.warn('i18n-values: Missing value for "' + propExpr + '"');
+ }
+ }
+ }
+ }
+ };
+
+ var attributeNames = [];
+ for (var key in handlers) {
+ attributeNames.push(key);
+ }
+ var selector = '[' + attributeNames.join('],[') + ']';
+
+ /**
+ * Processes a DOM tree with the {@code obj} map.
+ */
+ function process(node, obj) {
+ var elements = node.querySelectorAll(selector);
+ for (var element, i = 0; element = elements[i]; i++) {
+ for (var j = 0; j < attributeNames.length; j++) {
+ var name = attributeNames[j];
+ var att = element.getAttribute(name);
+ if (att != null) {
+ handlers[name](element, att, obj);
+ }
+ }
+ }
+ }
+
+ return {
+ process: process
+ };
+})();
diff --git a/chrome/browser/resources/bookmark_manager/js/localstrings.js b/chrome/browser/resources/bookmark_manager/js/localstrings.js
new file mode 100644
index 0000000..86f888b
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/localstrings.js
@@ -0,0 +1,55 @@
+// Copyright (c) 2009-2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TODO(arv): Namespace and share code with DOMUI
+
+/**
+ * The local strings get injected into the page usig a varaible named
+ * {@code templateData}. This class provides a simpler interface to access those
+ * strings.
+ * @constructor
+ */
+function LocalStrings() {
+}
+
+LocalStrings.prototype = {
+
+ /**
+ * The template data object.
+ * @type {Object}
+ */
+ templateData: null,
+
+ /**
+ * Gets a localized string by its id.
+ * @param {string} s The id of the string we want.
+ * @return {string} The localized string.
+ */
+ getString: function(id) {
+ return this.templateData[id] || '';
+ },
+
+ /**
+ * Returns a formatted localized string where all %s contents are replaced
+ * by the second argument and where $1 to $9 are replaced by the second to
+ * tenths arguments.
+ * @param {string} id The ID of the string we want.
+ * @param {string} v The string to include in the formatted string.
+ * @param {...string} The extra values to include in the fomatted output.
+ * @return {string} The formatted string.
+ */
+ getStringF: function(id, v, var_args) {
+ // The localized messages should contain $n but they also use %s from time
+ // to time so we support both until all the messages have been unified.
+ var s = this.getString(id);
+ var args = arguments;
+ return s.replace(/%s|\$[$1-9]/g, function(m) {
+ if (m == '%s')
+ return v;
+ if (m == '$$')
+ return '$';
+ return args[m[1]];
+ });
+ }
+};
diff --git a/chrome/browser/resources/bookmark_manager/js/util.js b/chrome/browser/resources/bookmark_manager/js/util.js
new file mode 100644
index 0000000..3fef5d2
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/util.js
@@ -0,0 +1,57 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * The global object.
+ * @param {!Object}
+ */
+const global = this;
+
+/**
+ * Alias for document.getElementById.
+ * @param {string} id The ID of the element to find.
+ * @return {HTMLElement} The found element or null if not found.
+ */
+function $(id) {
+ return document.getElementById(id);
+}
+
+/**
+ * Calls chrome.send with a callback and restores the original afterwards.
+ * @param {string} name The name of the message to send.
+ * @param {!Array} params The parameters to send.
+ * @param {string} callbackName The name of the function that the backend calls.
+ * @param {!Function} The function to call.
+ */
+function chromeSend(name, params, callbackName, callback) {
+ var old = global[callbackName];
+ global[callbackName] = function() {
+ // restore
+ global[callbackName] = old;
+
+ var args = Array.prototype.slice.call(arguments);
+ return callback.apply(global, args);
+ };
+ chrome.send(name, params);
+}
+
+
+/**
+ * Generates a CSS url string.
+ * @param {string} s The URL to generate the CSS url for.
+ * @return {string} The CSS url string.
+ */
+function url(s) {
+ // http://www.w3.org/TR/css3-values/#uris
+ // Parentheses, commas, whitespace characters, single quotes (') and double
+ // quotes (") appearing in a URI must be escaped with a backslash
+ var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
+ // WebKit has a bug when it comes to URLs that end with \
+ // https://bugs.webkit.org/show_bug.cgi?id=28885
+ if (/\\\\$/.test(s2)) {
+ // Add a space to work around the WebKit bug.
+ s2 += ' ';
+ }
+ return 'url("' + s2 + '")';
+}
diff --git a/chrome/browser/resources/bookmark_manager/main.html b/chrome/browser/resources/bookmark_manager/main.html
new file mode 100644
index 0000000..823e973
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/main.html
@@ -0,0 +1,1684 @@
+<!DOCTYPE html>
+<html>
+<!--
+
+Copyright (c) 2010 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+
+
+This is work in progress:
+
+i18n: Expose a chrome.experimental.bookmarkManager.getLocalStrings
+
+import/export: Expose in experimental extension API.
+
+Internal DnD: Buggy when dragging multiple items (the order of the dropped items
+is not correct.
+
+External DnD: Chrome doesn't follow HTML5 and it limits the data types to text
+and a single URL. Fixing Chrome is unreasonable given our current time frame.
+There are two options here. Disable external DnD or expose enough hooks in the
+experimental bookmarks manager extension API.
+
+Clipboard: Once again Chrome does not correctly implement HTML5 and it only
+allows text and url. We can either disable the clipboard actions, only allow
+internal clipboard or expose the hooks in the extension api.
+
+Favicons: chrome-extension: is not allowed to access chrome://favicon. We need
+to whitelist it or expose a way to get the data URI for the favicon (slow and
+sucky).
+
+Favicon of bmm does not work. No icon is showed.
+
+-->
+<head>
+<title i18n-content="title"></title>
+<link rel="stylesheet" href="css/list.css">
+<link rel="stylesheet" href="css/tree.css">
+<script src="css/tree.css.js"></script>
+<link rel="stylesheet" href="css/menu.css">
+
+<script src="js/cr.js"></script>
+<script src="js/cr/event.js"></script>
+<script src="js/cr/eventtarget.js"></script>
+<script src="js/cr/ui.js"></script>
+<script src="js/cr/ui/listselectionmodel.js"></script>
+<script src="js/cr/ui/listitem.js"></script>
+<script src="js/cr/ui/list.js"></script>
+<script src="js/cr/ui/tree.js"></script>
+<script src="js/cr/ui/command.js"></script>
+<script src="js/cr/ui/menuitem.js"></script>
+<script src="js/cr/ui/menu.js"></script>
+<script src="js/cr/ui/menubutton.js"></script>
+<script src="js/cr/ui/contextmenuhandler.js"></script>
+
+<script src="js/util.js"></script>
+<script src="js/localstrings.js"></script>
+<script src="js/i18ntemplate.js"></script>
+
+<script src="js/bmm.js"></script>
+<script src="js/bmm/bookmarklist.js"></script>
+<script src="js/bmm/bookmarktree.js"></script>
+
+<style>
+
+html, body {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ /*-webkit-user-select: none;*/
+ cursor: default;
+ font: 13px arial;
+}
+
+list {
+ display: block;
+ overflow-x: hidden;
+ overflow-y: visible; /* let the container do the scrolling */
+}
+
+list > * {
+ text-decoration: none;
+ padding: 5px;
+}
+
+list > * > * {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: block;
+ color: black;
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ background: 0 50% no-repeat;
+ -webkit-padding-start: 20px;
+}
+
+list > * > * > span {
+ -webkit-transition: all .15s;
+ text-decoration: none;
+ color: #000;
+ cursor: pointer;
+ opacity: .7;
+}
+
+list > * > :first-child {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+list > * > :last-child {
+ overflow: hidden;
+}
+
+list > * > * > .folder {
+ background-image: url("images/folder_closed.png");
+ background-repeat: no-repeat;
+ background-position: 0% 50%;
+ display: inline-block;
+ -webkit-padding-start: 18px;
+}
+
+html[dir=rtl] list > * > * > .folder {
+ background-image: url("images/folder_closed_rtl.png");
+ background-position: 100% 50%;
+}
+
+html[os=mac] list > * > * > .folder {
+ background-image: url("images/bookmark_bar_folder_mac.png");
+}
+
+list > * > * > :hover {
+ text-decoration: underline;
+ color: -webkit-link;
+ opacity: 1;
+}
+
+list > * > * > :active {
+ color: -webkit-activelink;
+}
+
+html[dir=rtl] list .label {
+ background-position: 100% 50%;
+}
+
+list > .folder > .label {
+ background-image: url("images/folder_closed.png");
+}
+
+html[dir=rtl] list > .folder > .label {
+ background-image: url("images/folder_closed_rtl.png");
+}
+
+html[os=mac] list > .folder > .label {
+ background-image: url("images/bookmark_bar_folder_mac.png");
+}
+
+html[os=mac] .tree-label {
+ background-image: url("images/bookmark_bar_folder_mac.png");
+}
+
+html[os=mac] .tree-row[selected] > .tree-label {
+ background-image: url("images/bookmark_bar_folder_mac.png");
+}
+
+.main {
+ position: absolute;
+ top: 75px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+#tree-container {
+ position: absolute;
+ left: 0;
+ width: 200px;
+ top: 0;
+ bottom: 0;
+ overflow: auto;
+ -webkit-box-sizing: border-box;
+ padding: 0px 5px 5px 5px;
+}
+
+#tree {
+ min-width: 100%;
+ overflow: visible; /* let the container do the scrolling */
+ display: inline-block;
+}
+
+#list {
+ position: absolute;
+ left: 200px;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ -webkit-box-sizing: border-box;
+ padding: 0 5px 5px 5px;
+}
+
+.logo {
+ -webkit-appearance: none;
+ border: 0;
+ background: transparent;
+ background: 50% 50% no-repeat url("images/bookmarks_section.png");
+ width: 67px;
+ height: 67px;
+ cursor: pointer;
+ vertical-align: bottom;
+ margin: 5px;
+}
+
+html[dir=rtl] #tree-container {
+ left: auto;
+ right: 0;
+}
+
+html[dir=rtl] #list {
+ left: 0;
+ right: 200px;
+}
+
+.header > div {
+ display: inline-block;
+ margin: 5px;
+}
+
+#drop-overlay {
+ position: absolute;
+ display: none;
+ pointer-events: none;
+ border: 1px solid hsl(214, 91%, 85%);;
+ -webkit-border-radius: 3px;
+ -webkit-box-sizing: border-box;
+ background-color: hsla(214, 91%, 85%, .5);
+ overflow: hidden;
+ z-index: -1;
+}
+
+#drop-overlay.line {
+ border: 3px solid black;
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+ background-color: black;
+ background-clip: padding-box;
+ height: 8px;
+ -webkit-border-radius: 0;
+ z-index: 10;
+}
+
+.toolbar button {
+ -webkit-appearance: none;
+ border: none;
+ background: transparent;
+ font: inherit;
+ padding: 0;
+}
+
+</style>
+<script>
+
+// Sometimes the extension API is not initialized.
+if (!chrome.bookmarks)
+ window.location.reload();
+
+// Allow platform specific CSS rules.
+if (/Mac/.test(navigator.platform))
+ document.documentElement.setAttribute('os', 'mac');
+
+</script>
+</head>
+<body>
+
+<div class="header">
+ <button onclick="resetSearch()" class="logo" tabindex=3></button>
+ <div>
+ <form onsubmit="setSearch(this.term.value); return false;"
+ class="form">
+ <input type="text" id="term" tabindex=1 autofocus>
+ <input type="submit" i18n-values=".value:search_button" tabindex=1>
+ </form>
+ <div class=toolbar>
+ <button menu="#organize-menu" tabindex="-1" i18n-content="organize_menu"></button>
+ <button menu="#tools-menu" tabindex="-1" i18n-content="tools_menu"></button>
+ </div>
+ </div>
+</div>
+
+<div class=main>
+ <div id=tree-container>
+ <tree id=tree tabindex=2></tree>
+ </div>
+ <list id=list tabindex=2></list>
+</div>
+
+
+<script>
+
+const BookmarkList = bmm.BookmarkList;
+const BookmarkTree = bmm.BookmarkTree;
+const ListItem = cr.ui.ListItem;
+const TreeItem = cr.ui.TreeItem;
+
+/**
+ * The id of the bookmark root.
+ * @type {number}
+ */
+const ROOT_ID = '0';
+
+var bookmarkCache = {
+ /**
+ * This returns a reference to the bookmark node that is cached by the tree
+ * or list. Use this funciton when we need to update the local cachea after
+ * changes. It only returns bookmarks that are used by the tree and/or the
+ * list.
+ * @param {string} The ID of the bookmark that we want to get.
+ * @return {BookmarkTreeNode}
+ */
+ getById: function(id) {
+ var el = bmm.treeLookup[id] || bmm.listLookup[id];
+ return el && el.bookmarkNode;
+ },
+
+ /**
+ * Removes the cached item from both the list and tree lookups.
+ */
+ remove: function(id) {
+ delete bmm.listLookup[id];
+
+ var treeItem = bmm.treeLookup[id];
+ if (treeItem) {
+ var items = treeItem.items; // is an HTMLCollection
+ for (var i = 0, item; item = items[i]; i++) {
+ var bookmarkNode = item.bookmarkNode;
+ delete bmm.treeLookup[bookmarkNode.id];
+ }
+ delete bmm.treeLookup[id];
+ }
+ },
+
+ /**
+ * Updates the underlying bookmark node for the tree items and list items by
+ * querying the bookmark backend.
+ * @param {string} id The id of the node to update the children for.
+ * @param {Function=} opt_f A funciton to call when done.
+ */
+ updateChildren: function(id, opt_f) {
+ function updateItem(bookmarkNode) {
+ var treeItem = bmm.treeLookup[bookmarkNode.id];
+ if (treeItem) {
+ treeItem.bookmarkNode = bookmarkNode;
+ }
+ var listItem = bmm.listLookup[bookmarkNode.id];
+ if (listItem) {
+ listItem.bookmarkNode = bookmarkNode;
+ }
+ }
+
+ chrome.bookmarks.getChildren(id, function(children) {
+ children.forEach(updateItem);
+
+ if (opt_f)
+ opt_f(children);
+ });
+ }
+};
+
+</script>
+<script>
+
+BookmarkList.decorate(list);
+
+var searchTreeItem = new TreeItem({
+ label: 'Search',
+ icon: 'images/bookmark_manager_search.png',
+ bookmarkId: 'q='
+});
+bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem;
+
+var recentTreeItem = new TreeItem({
+ label: 'Recent',
+ icon: 'images/bookmark_manager_recent.png',
+ bookmarkId: 'recent'
+});
+bmm.treeLookup[recentTreeItem.bookmarkId] = recentTreeItem;
+
+BookmarkTree.decorate(tree);
+
+tree.addEventListener('change', function() {
+ navigateTo(tree.selectedItem.bookmarkId);
+});
+
+</script>
+<script>
+
+/**
+ * Navigates to a bookmark ID.
+ * @param {string} id The ID to navigate to.
+ */
+function navigateTo(id) {
+ console.info('navigateTo', window.location.hash, id);
+ window.location.hash = id;
+ updateParentId(id);
+}
+
+/**
+ * Updates the parent ID of the bookmark list and selects the correct tree item.
+ * @param {string} id The id.
+ */
+function updateParentId(id) {
+ list.parentId = id;
+ if (id in bmm.treeLookup)
+ tree.selectedItem = bmm.treeLookup[id];
+}
+
+// We listen to hashchange so that we can update the currently shown folder when
+// the user goes back and forward in the history.
+window.onhashchange = function(e) {
+ var id = window.location.hash.slice(1);
+
+ var valid = false;
+
+ // In case we got a search hash update the text input and the bmm.treeLookup
+ // to use the new id.
+ if (/^q=/.test(id)) {
+ delete bmm.treeLookup[searchTreeItem.bookmarkId];
+ $('term').value = id.slice(2);
+ searchTreeItem.bookmarkId = id;
+ bmm.treeLookup[id] = searchTreeItem;
+ valid = true;
+ } else if (id == 'recent') {
+ valid = true;
+ }
+
+ if (valid) {
+ updateParentId(id);
+ } else {
+ // We need to verify that this is a correct ID.
+ chrome.bookmarks.get(id, function(items) {
+ if (items && items.length == 1)
+ updateParentId(id);
+ });
+ }
+};
+
+list.addEventListener('activate', function(e) {
+ var bookmarkNodes = getSelectedBookmarkNodes();
+
+ // If we double clicked or pressed enter on a single folder navigate to it.
+ if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) {
+ navigateTo(bookmarkNodes[0].id);
+ } else {
+ var command = $('open-in-new-tab-command');
+ command.execute();
+ }
+});
+
+// The list dispatches an event when the user clicks on the URL or the Show in
+// folder part.
+list.addEventListener('urlClicked', function(e) {
+ openUrls([e.url], e.kind);
+});
+
+/**
+ * Timer id used for delaying find-as-you-type
+ */
+var inputDelayTimer;
+
+// Capture input changes to the search term input element and delay searching
+// for 250ms to reduce flicker.
+$('term').oninput = function(e) {
+ clearTimeout(inputDelayTimer);
+ inputDelayTimer = setTimeout(function() {
+ setSearch($('term').value);
+ }, 250);
+};
+
+/**
+ * Navigates to the search results for the search text.
+ * @para {string} searchText The text to search for.
+ */
+function setSearch(searchText) {
+ navigateTo('q=' + searchText);
+}
+
+/**
+ * Clears the search.
+ */
+function resetSearch() {
+ $('term').value = '';
+ setSearch('');
+ $('term').focus();
+}
+
+/**
+ * Called when the title of a bookmark changes.
+ * @param {string} id
+ * @param {!Object} changeInfo
+ */
+function handleBookmarkChanged(id, changeInfo) {
+ // console.log('handleBookmarkChanged', id, changeInfo);
+ list.handleBookmarkChanged(id, changeInfo);
+ tree.handleBookmarkChanged(id, changeInfo);
+}
+
+/**
+ * Callback for when the user reorders by title.
+ * @param {string} id The id of the bookmark folder that was reordered.
+ * @param {!Object} reorderInfo The information about how the items where
+ * reordered.
+ */
+function handleChildrenReordered(id, reorderInfo) {
+ // console.info('handleChildrenReordered', id, reorderInfo);
+ list.handleChildrenReordered(id, reorderInfo);
+ tree.handleChildrenReordered(id, reorderInfo);
+ bookmarkCache.updateChildren(id);
+}
+
+/**
+ * Callback for when a bookmark node is created.
+ * @param {string} id The id of the newly created bookmark node.
+ * @param {!Object} bookmarkNode The new bookmark node.
+ */
+function handleCreated(id, bookmarkNode) {
+ // console.info('handleCreated', id, bookmarkNode);
+ list.handleCreated(id, bookmarkNode);
+ tree.handleCreated(id, bookmarkNode);
+ bookmarkCache.updateChildren(bookmarkNode.parentId);
+}
+
+function handleMoved(id, moveInfo) {
+ // console.info('handleMoved', id, moveInfo);
+ list.handleMoved(id, moveInfo);
+ tree.handleMoved(id, moveInfo);
+
+ bookmarkCache.updateChildren(moveInfo.parentId);
+ if (moveInfo.parentId != moveInfo.oldParentId)
+ bookmarkCache.updateChildren(moveInfo.oldParentId);
+}
+
+function handleRemoved(id, removeInfo) {
+ // console.info('handleRemoved', id, removeInfo);
+ list.handleRemoved(id, removeInfo);
+ tree.handleRemoved(id, removeInfo);
+
+ bookmarkCache.updateChildren(removeInfo.parentId);
+ bookmarkCache.remove(id);
+}
+
+function handleImportBegan() {
+ chrome.bookmarks.onCreated.removeListener(handleCreated);
+}
+
+function handleImportEnded() {
+ chrome.bookmarks.onCreated.addListener(handleCreated);
+ chrome.bookmarks.getTree(function(node) {
+ var otherBookmarks = node[0].children[1].children;
+ var importedFolder = otherBookmarks[otherBookmarks.length - 1];
+ var importId = importedFolder.id;
+ tree.insertSubtree(importedFolder);
+ navigateTo(importId)
+ });
+}
+
+/**
+ * Adds the listeners for the bookmark model change events.
+ */
+function addBookmarkModelListeners() {
+ chrome.bookmarks.onChanged.addListener(handleBookmarkChanged);
+ chrome.bookmarks.onChildrenReordered.addListener(handleChildrenReordered);
+ chrome.bookmarks.onCreated.addListener(handleCreated);
+ chrome.bookmarks.onMoved.addListener(handleMoved);
+ chrome.bookmarks.onRemoved.addListener(handleRemoved);
+ chrome.experimental.bookmarkManager.onImportBegan.addListener(
+ handleImportBegan);
+ chrome.experimental.bookmarkManager.onImportEnded.addListener(
+ handleImportEnded);
+}
+
+/**
+ * This returns the user visible path to the folder where the bookmark is
+ * located.
+ * @param {number} parentId The ID of the parent folder.
+ * @return {string} The path to the the bookmark,
+ */
+function getFolder(parentId) {
+ var parentNode = tree.getBookmarkNodeById(parentId);
+ if (parentNode) {
+ var s = parentNode.title;
+ if (parentNode.parentId != ROOT_ID) {
+ return getFolder(parentNode.parentId) + '/' + s;
+ }
+ return s;
+ }
+}
+
+tree.addEventListener('load', function(e) {
+ // Add hard coded tree items
+ tree.add(recentTreeItem);
+ tree.add(searchTreeItem);
+
+ // Now we can select a tree item.
+ var hash = window.location.hash.slice(1);
+ if (!hash) {
+ // If we do not have a hash select first item in the tree.
+ hash = tree.items[0].bookmarkId;
+ }
+
+ if (/^q=/.test(hash))
+ $('term').value = hash.slice(2);
+ navigateTo(hash);
+});
+
+tree.buildTree();
+addBookmarkModelListeners();
+
+</script>
+<script>
+
+var dnd = {
+
+ DND_EFFECT: cr.isMac ? 'move' : 'copy', // http://crbug.com/14654
+
+ dragBookmarkNodes: [],
+
+ getBookmarkElement: function(el) {
+ while (el && !el.bookmarkNode) {
+ el = el.parentNode;
+ }
+ return el;
+ },
+
+ // If we are over the list and the list is showing recent or search result
+ // we cannot drop.
+ isOverRecentOrSearch: function(overElement) {
+ return (list.isRecent() || list.isSearch()) && list.contains(overElement);
+ },
+
+ checkEvery_: function(f, dragBookmarkNodes, overBookmarkNode, overElement) {
+ return dragBookmarkNodes.every(function(dragBookmarkNode) {
+ return f.call(this, dragBookmarkNode, overBookmarkNode, overElement);
+ }, this);
+ },
+
+ /**
+ * This is a first pass wether we can drop the dragged items.
+ *
+ * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are
+ * currently being dragged.
+ * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
+ * currently dragging over.
+ * @param {!HTMLElement} overElement The element that we are currently
+ * dragging over.
+ * @return {boolean} If this returns false then we know we should not drop
+ * the items. If it returns true we still have to call canDropOn,
+ * canDropAbove and canDropBelow.
+ */
+ canDrop: function(dragBookmarkNodes, overBookmarkNode, overElement) {
+ return this.checkEvery_(this.canDrop_, dragBookmarkNodes,
+ overBookmarkNode, overElement);
+ },
+
+ /**
+ * Helper for canDrop that only checks one bookmark node.
+ * @private
+ */
+ canDrop_: function(dragBookmarkNode, overBookmarkNode, overElement) {
+ if (overBookmarkNode.id == dragBookmarkNode.id)
+ return false;
+
+ if (this.isOverRecentOrSearch(overElement))
+ return false;
+
+ if (dragBookmarkNode.id == overBookmarkNode.id)
+ return false;
+
+ // If we are dragging a folder we cannot drop it on any of its descendants
+ if (bmm.isFolder(dragBookmarkNode) &&
+ bmm.contains(dragBookmarkNode, overBookmarkNode)) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Whether we can drop the dragged items above the drop target.
+ *
+ * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are
+ * currently being dragged.
+ * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
+ * currently dragging over.
+ * @param {!HTMLElement} overElement The element that we are currently
+ * dragging over.
+ * @return {boolean} Whether we can drop the dragged items above the drop
+ * target.
+ */
+ canDropAbove: function(dragBookmarkNodes, overBookmarkNode, overElement) {
+ return this.checkEvery_(this.canDropAbove_, dragBookmarkNodes,
+ overBookmarkNode, overElement);
+ },
+
+ /**
+ * Helper for canDropAbove that only checks one bookmark node.
+ * @private
+ */
+ canDropAbove_: function(dragBookmarkNode, overBookmarkNode, overElement) {
+ if (overElement instanceof BookmarkList)
+ return false;
+
+ // If dragBookmarkNode is a non folder and overElement is a tree item we
+ // cannot drop it above or below.
+ if (!bmm.isFolder(dragBookmarkNode) && overElement instanceof TreeItem)
+ return false;
+
+ // We cannot drop between Bookmarks bar and Other bookmarks
+ if (overBookmarkNode.parentId == ROOT_ID)
+ return false;
+
+ // We cannot drop above if the item below is already in the drag source
+ var previousElement = overElement.previousElementSibling;
+ if (previousElement &&
+ previousElement.bookmarkNode.id == dragBookmarkNode.id)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Whether we can drop the dragged items below the drop target.
+ *
+ * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are
+ * currently being dragged.
+ * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
+ * currently dragging over.
+ * @param {!HTMLElement} overElement The element that we are currently
+ * dragging over.
+ * @return {boolean} Whether we can drop the dragged items below the drop
+ * target.
+ */
+ canDropBelow: function(dragBookmarkNodes, overBookmarkNode, overElement) {
+ return this.checkEvery_(this.canDropBelow_, dragBookmarkNodes,
+ overBookmarkNode, overElement);
+ },
+
+ /**
+ * Helper for canDropBelow that only checks one bookmark node.
+ * @private
+ */
+ canDropBelow_: function(dragBookmarkNode, overBookmarkNode, overElement) {
+ if (overElement instanceof BookmarkList)
+ return false;
+
+ // The tree can only hold folders so if we are over a tree item we cannot
+ // drop a non folder.
+ if (!bmm.isFolder(dragBookmarkNode) && overElement instanceof TreeItem)
+ return false;
+
+ // We cannot drop between Bookmarks bar and Other bookmarks
+ if (overBookmarkNode.parentId == ROOT_ID)
+ return false;
+
+ // We cannot drop below if the item below is already in the drag source
+ var nextElement = overElement.nextElementSibling;
+ if (nextElement &&
+ nextElement.bookmarkNode.id == dragBookmarkNode.id)
+ return false;
+
+ // Don't allow dropping below an expanded tree item since it is confusing
+ // to the user anyway.
+ if (overElement instanceof TreeItem && overElement.expanded)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Whether we can drop the dragged items on the drop target.
+ *
+ * @param {!Array.<BookmarkTreeNode>} dragBookmarkNodes The bookmarks that are
+ * currently being dragged.
+ * @param {!BookmarkTreeNode} overBookmarkNode The bookmark that we are
+ * currently dragging over.
+ * @param {!HTMLElement} overElement The element that we are currently
+ * dragging over.
+ * @return {boolean} Whether we can drop the dragged items on the drop
+ * target.
+ */
+ canDropOn: function(dragBookmarkNodes, overBookmarkNode, overElement) {
+ return this.checkEvery_(this.canDropOn_, dragBookmarkNodes,
+ overBookmarkNode, overElement);
+ },
+
+ /**
+ * Helper for canDropOn that only checks one bookmark node.
+ * @private
+ */
+ canDropOn_: function(dragBookmarkNode, overBookmarkNode, overElement) {
+ // We can only drop on a folder...
+ if (!bmm.isFolder(overBookmarkNode))
+ return false;
+
+ if (overElement instanceof BookmarkList) {
+ // We are trying to drop an item after the last item in the list. This
+ // is allowed if the item is different from the last item in the list
+ var listItems = list.items;
+ var len = listItems.length;
+ if (len == 0 ||
+ listItems[len - 1].bookmarkNode.id != dragBookmarkNode.id) {
+ return true;
+ }
+ }
+
+ // Cannot drop on current parent
+ if (overBookmarkNode.id == dragBookmarkNode.parentId)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Callback for the dragstart event.
+ * @param {Event} e The dragstart event.
+ */
+ handleDragStart: function(e) {
+ // console.log(e.type);
+
+ // Determine the selected bookmarks.
+ var target = e.target;
+ var draggedItems = [];
+ if (target instanceof ListItem) {
+ // Use selected items.
+ draggedItems = target.parentNode.selectedItems;
+ } else if (target instanceof TreeItem) {
+ draggedItems.push(target);
+ }
+
+ this.dragBookmarkNodes = draggedItems.map(function(item) {
+ return item.bookmarkNode;
+ });
+
+ // console.log(draggedItems, this.dragBookmarkNodes)
+
+ // TODO(arv): Fix this once we expose DnD in the extension API
+ // Mac requires setData to be called
+ e.dataTransfer.setData('text/uri-list', 'http://www.google.com/');
+ e.dataTransfer.effectAllowed = this.DND_EFFECT;
+
+ if (!this.dragBookmarkNodes.length) {
+ e.preventDefault();
+ }
+ },
+
+ handleDragEnter: function(e) {
+ // console.log(e.type);
+
+ e.preventDefault();
+ },
+
+ /**
+ * Calback for the dragover event.
+ * @param {Event} e The dragover event.
+ */
+ handleDragOver: function(e) {
+ // console.log(e.type);
+
+ if (!this.dragBookmarkNodes.length)
+ return;
+
+ var overElement = this.getBookmarkElement(e.target);
+ if (!overElement && e.target == list)
+ overElement = list;
+
+ if (!overElement)
+ return;
+
+ var dragBookmarkNodes = this.dragBookmarkNodes;
+ var overBookmarkNode = overElement.bookmarkNode;
+
+ if (!this.canDrop(dragBookmarkNodes, overBookmarkNode, overElement))
+ return;
+
+ var bookmarkNode = overElement.bookmarkNode;
+
+ var canDropAbove = this.canDropAbove(dragBookmarkNodes, overBookmarkNode,
+ overElement);
+ var canDropOn = this.canDropOn(dragBookmarkNodes, overBookmarkNode,
+ overElement);
+ var canDropBelow = this.canDropBelow(dragBookmarkNodes, overBookmarkNode,
+ overElement);
+
+ if (!canDropAbove && !canDropOn && !canDropBelow)
+ return;
+
+ // Now we know that we can drop. Determine if we will drop above, on or
+ // below based on mouse position etc.
+
+ var dropPos;
+ e.preventDefault();
+ // TODO(arv): Fix this once we expose DnD in the extension API
+ e.dataTransfer.dropEffect = this.DND_EFFECT;
+
+ var rect;
+ if (overElement instanceof TreeItem) {
+ // We only want the rect of the row representing the item and not
+ // its children
+ rect = overElement.rowElement.getBoundingClientRect();
+ } else {
+ rect = overElement.getBoundingClientRect();
+ }
+
+ if (canDropAbove && !canDropOn && !canDropBelow) {
+ dropPos = 'above';
+ } else if (!canDropAbove && canDropOn && !canDropBelow) {
+ dropPos = 'on';
+ } else if (!canDropAbove && !canDropOn && canDropBelow) {
+ dropPos = 'below';
+ } else {
+ // We need to compare the mouse position with the element rect.
+
+ var dy = e.clientY - rect.top;
+ var yRatio = dy / rect.height;
+ if (!canDropOn) {
+ dropPos = yRatio < .5 ? 'above' : 'below';
+ } else if (!canDropAbove) {
+ dropPos = yRatio < .5 ? 'on' : 'below';
+ } else if (!canDropBelow) {
+ dropPos = yRatio < .5 ? 'above' : 'on';
+ } else {
+ dropPos = yRatio < .25 ? 'above' : yRatio > .75 ? 'below' : 'on';
+ }
+ }
+
+ function cloneClientRect(rect) {
+ var newRect = {};
+ for (var key in rect) {
+ newRect[key] = rect[key];
+ }
+ return newRect;
+ }
+
+ // If we are dropping above or below a tree item adjust the width so
+ // that it is clearer where the item will be dropped.
+ if ((dropPos == 'above' || dropPos == 'below' ) &&
+ overElement instanceof TreeItem) {
+ // ClientRect is read only so clone in into a read-write object.
+ rect = cloneClientRect(rect);
+ var rtl = getComputedStyle(overElement).direction == 'rtl';
+ var labelElement = overElement.labelElement;
+ var labelRect = labelElement.getBoundingClientRect();
+ if (rtl) {
+ rect.width = labelRect.left + labelRect.width - rect.left;
+ } else {
+ rect.left = labelRect.left;
+ rect.width -= rect.left
+ }
+ }
+
+ var overlayType = dropPos;
+
+ // If we are dropping on a list we want to show a overlay drop line after
+ // the last element
+ if (overElement instanceof BookmarkList) {
+ overlayType = 'below';
+
+ // Get the rect of the last list item.
+ var items = overElement.items;
+ var length = items.length;
+ if (length) {
+ dropPos = 'below';
+ overElement = items[length - 1];
+ rect = overElement.getBoundingClientRect();
+ } else {
+ // If there are no items, collapse the height of the rect
+ rect = cloneClientRect(rect);
+ rect.height = 0;
+ // We do not use bottom so we don't care to adjust it.
+ }
+ }
+
+ this.showDropOverlay_(rect, overlayType);
+
+ // TODO(arv): Multiple selection DnD.
+ this.dropDestination = {
+ dropPos: dropPos,
+ relatedNode: overElement.bookmarkNode
+ };
+ },
+
+ /**
+ * Shows and positions the drop marker overlay.
+ * @param {ClientRect} targetRect The drop target rect
+ * @param {string} overlayType The position relative to the target rect.
+ * @private
+ */
+ showDropOverlay_: function(targetRect, overlayType) {
+ window.clearTimeout(this.hideDropOverlayTimer_);
+ var overlay = $('drop-overlay');
+ if (overlayType == 'on') {
+ overlay.className = '';
+ overlay.style.top = targetRect.top + 'px';
+ overlay.style.height = targetRect.height + 'px';
+ } else {
+ overlay.className = 'line';
+ overlay.style.height = '';
+ }
+ overlay.style.width = targetRect.width + 'px';
+ overlay.style.left = targetRect.left + 'px';
+ overlay.style.display = 'block';
+
+ if (overlayType != 'on') {
+ var overlayRect = overlay.getBoundingClientRect();
+ if (overlayType == 'above') {
+ overlay.style.top = targetRect.top - overlayRect.height / 2 + 'px';
+ } else {
+ overlay.style.top = targetRect.top + targetRect.height -
+ overlayRect.height / 2 + 'px';
+ }
+ }
+ },
+
+ /**
+ * Hides the drop overlay element.
+ * @private
+ */
+ hideDropOverlay_: function() {
+ // Hide the overlay in a timeout to reduce flickering as we move between
+ // valid drop targets.
+ window.clearTimeout(this.hideDropOverlayTimer_);
+ this.hideDropOverlayTimer_ = window.setTimeout(function() {
+ $('drop-overlay').style.display = '';
+ }, 100);
+ },
+
+ handleDragLeave: function(e) {
+ // console.log(e.type);
+
+ this.hideDropOverlay_();
+ },
+
+ handleDrop: function(e) {
+ // console.log(e.type);
+
+ if (this.dropDestination && this.dragBookmarkNodes.length) {
+ // console.log('Drop', this.dragBookmarkNodes, this.dropDestination);
+
+ var dropPos = this.dropDestination.dropPos;
+ var relatedNode = this.dropDestination.relatedNode;
+ var parentId = dropPos == 'on' ? relatedNode.id : relatedNode.parentId;
+
+ var moveInfo = {
+ parentId: parentId
+ };
+
+ if (dropPos == 'above') {
+ moveInfo.index = relatedNode.index;
+ } else if (dropPos == 'below') {
+ moveInfo.index = relatedNode.index + 1;
+ }
+
+ // TODO(arv): Add support for multiple move in bookmarks API?
+ this.dragBookmarkNodes.forEach(function(bookmarkNode) {
+ var id = bookmarkNode.id;
+ // console.info('Calling move', id, moveInfo.index, bookmarkNode);
+ chrome.bookmarks.move(id, moveInfo,
+ function(result) {
+ // console.log('chrome.bookmarks.move', arguments);
+ });
+ moveInfo.index++;
+ });
+
+ // TODO(arv): Select the newly dropped items.
+ }
+ this.dropDestination = null;
+ this.hideDropOverlay_();
+ },
+
+ handleDrag: function(e) {
+ // console.log(e.type);
+ },
+
+ handleDragEnd: function(e) {
+ // console.log(e.type);
+
+ var self = this;
+ // Chromium Win incorrectly fires the dragend event before the drop event.
+ // http://code.google.com/p/chromium/issues/detail?id=31292
+ window.setTimeout(function() {
+ self.dragBookmarkNodes = [];
+ self = null;
+ }, 1)
+ },
+
+ init: function() {
+ document.addEventListener('dragstart', cr.bind(this.handleDragStart, this));
+ document.addEventListener('dragenter', cr.bind(this.handleDragEnter, this));
+ document.addEventListener('dragover', cr.bind(this.handleDragOver, this));
+ document.addEventListener('dragleave', cr.bind(this.handleDragLeave, this));
+ document.addEventListener('drop', cr.bind(this.handleDrop, this));
+ document.addEventListener('dragend', cr.bind(this.handleDragEnd, this));
+ document.addEventListener('drag', cr.bind(this.handleDrag, this));
+ }
+
+};
+
+dnd.init();
+
+
+</script>
+
+<!-- Organize menu -->
+<command i18n-values=".label:rename_folder" id="rename-folder-command"></command>
+<command i18n-values=".label:edit" id="edit-command"></command>
+<command i18n-values=".label:delete" id="delete-command"></command>
+<command i18n-values=".label:show_in_folder" id="show-in-folder-command"></command>
+<command i18n-values=".label:cut" id="cut-command"></command>
+<command i18n-values=".label:copy" id="copy-command"></command>
+<command i18n-values=".label:paste" id="paste-command"></command>
+<command i18n-values=".label:sort" id="sort-command"></command>
+<command i18n-values=".label:add_new_bookmark" id="add-new-bookmark-command"></command>
+<command i18n-values=".label:new_folder" id="new-folder-command"></command>
+
+<!-- Tools menu -->
+<command i18n-values=".label:import_menu" id="import-menu-command"></command>
+<command i18n-values=".label:export_menu" id="export-menu-command"></command>
+
+<!-- open * are handled in canExecute handler -->
+<command id="open-in-new-tab-command"></command>
+<command id="open-in-new-window-command"></command>
+<command id="open-incognito-window-command"></command>
+
+<!-- TODO(arv): I think the commands might be better created in code? -->
+
+<menu id="organize-menu">
+ <button command="#rename-folder-command"></button>
+ <button command="#edit-command"></button>
+ <button command="#delete-command"></button>
+ <button command="#show-in-folder-command"></button>
+ <hr>
+ <button command="#cut-command"></button>
+ <button command="#copy-command"></button>
+ <button command="#paste-command"></button>
+ <hr>
+ <button command="#sort-command"></button>
+ <hr>
+ <button command="#add-new-bookmark-command"></button>
+ <button command="#new-folder-command"></button>
+</menu>
+
+<menu id="tools-menu">
+ <button command="#import-menu-command"></button>
+ <button command="#export-menu-command"></button>
+</menu>
+
+<menu id="context-menu">
+ <button command="#open-in-new-tab-command"></button>
+ <button command="#open-in-new-window-command"></button>
+ <button command="#open-incognito-window-command"></button>
+ <hr>
+ <button command="#rename-folder-command"></button>
+ <button command="#edit-command"></button>
+ <button command="#delete-command"></button>
+ <button command="#show-in-folder-command"></button>
+ <hr>
+ <button command="#cut-command"></button>
+ <button command="#copy-command"></button>
+ <button command="#paste-command"></button>
+ <hr>
+ <button command="#add-new-bookmark-command"></button>
+ <button command="#new-folder-command"></button>
+</menu>
+
+<script>
+
+// Commands
+
+const Command = cr.ui.Command;
+const CommandBinding = cr.ui.CommandBinding;
+const Menu = cr.ui.Menu;
+const MenuButton = cr.ui.MenuButton;
+
+cr.ui.decorate('menu', Menu);
+cr.ui.decorate('button[menu]', MenuButton);
+cr.ui.decorate('command', Command);
+
+cr.ui.contextMenuHandler.addContextMenuProperty(tree);
+list.contextMenu = $('context-menu');
+tree.contextMenu = $('context-menu');
+
+/**
+ * Helper function that updates the canExecute and labels for the open like
+ * commands.
+ * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system.
+ * @param {!cr.ui.Command} command The command we are currently precessing.
+ * @param {number} selectionCount The number of selected bookmarks.
+ */
+function updateOpenCommands(e, command, selectionCount) {
+ switch (command.id) {
+ case 'open-in-new-tab-command':
+ command.label = selectionCount == 1 ?
+ localStrings.getString('open_in_new_tab') :
+ localStrings.getString('open_all');
+ break;
+
+ case 'open-in-new-window-command':
+ command.label = selectionCount == 1 ?
+ localStrings.getString('open_in_new_window') :
+ localStrings.getString('open_all_new_window');
+ break;
+ case 'open-incognito-window-command':
+ command.label = selectionCount == 1 ?
+ localStrings.getString('open_incognito') :
+ localStrings.getString('open_all_incognito');
+ break;
+ }
+ e.canExecute = selectionCount > 0;
+}
+
+/**
+ * Calls the backend to figure out if we can paste the clipboard into the active
+ * folder.
+ * @param {Function=} opt_f Function to call after the state has been
+ * updated.
+ */
+function updatePasteCommand(opt_f) {
+ function update(canPaste) {
+ var command = $('paste-command');
+ command.disabled = !canPaste;
+ if (opt_f)
+ opt_f();
+ }
+ // We cannot paste into search and recent view.
+ if (list.isSearch() || list.isRecent()) {
+ update(false);
+ } else {
+ chrome.experimental.bookmarkManager.canPaste(list.parentId, update);
+ }
+}
+
+// We can always execute the import-menu and export-menu commands.
+document.addEventListener('canExecute', function(e) {
+ var command = e.command;
+ var commandId = command.id;
+ if (commandId == 'import-menu-command' || commandId == 'export-menu-command') {
+ e.canExecute = true;
+ }
+});
+
+// Update canExecute for the commands when the list is the active element.
+list.addEventListener('canExecute', function(e) {
+ var command = e.command;
+ var commandId = command.id;
+
+ function hasSelected() {
+ return !!e.target.selectedItem;
+ }
+
+ function hasSingleSelected() {
+ return e.target.selectedItems.length == 1;
+ }
+
+ function isRecentOrSearch() {
+ return list.isRecent() || list.isSearch();
+ }
+
+ switch (commandId) {
+ case 'rename-folder-command':
+ // Show rename if a single folder is selected
+ var items = e.target.selectedItems;
+ if (items.length != 1) {
+ e.canExecute = false;
+ command.hidden = true;
+ } else {
+ var isFolder = bmm.isFolder(items[0].bookmarkNode);
+ e.canExecute = isFolder;
+ command.hidden = !isFolder;
+ }
+ break;
+
+ case 'edit-command':
+ // Show the edit command if not a folder
+ var items = e.target.selectedItems;
+ if (items.length != 1) {
+ e.canExecute = false;
+ command.hidden = false;
+ } else {
+ var isFolder = bmm.isFolder(items[0].bookmarkNode);
+ e.canExecute = !isFolder;
+ command.hidden = isFolder;
+ }
+ break;
+
+ case 'show-in-folder-command':
+ e.canExecute = isRecentOrSearch() && hasSingleSelected();
+ break;
+
+ case 'delete-command':
+ case 'cut-command':
+ case 'copy-command':
+ e.canExecute = hasSelected();
+ break;
+
+ case 'paste-command':
+ updatePasteCommand();
+ break;
+
+ case 'sort-command':
+ case 'add-new-bookmark-command':
+ case 'new-folder-command':
+ e.canExecute = !isRecentOrSearch();
+ break;
+
+ case 'open-in-new-tab-command':
+ case 'open-in-new-window-command':
+ case 'open-incognito-window-command':
+ updateOpenCommands(e, command, e.target.selectedItems.length);
+ break;
+ }
+});
+
+// Update canExecute for the commands when the tree is the active element.
+tree.addEventListener('canExecute', function(e) {
+ var command = e.command;
+ var commandId = command.id;
+
+ function hasSelected() {
+ return !!e.target.selectedItem;
+ }
+
+ function isRecentOrSearch() {
+ var item = e.target.selectedItem;
+ return item == recentTreeItem || item == searchTreeItem;
+ }
+
+ function isTopLevelItem() {
+ return e.target.selectedItem.parentNode == tree;
+ }
+
+ switch (commandId) {
+ case 'rename-folder-command':
+ command.hidden = false;
+ e.canExecute = hasSelected() && !isTopLevelItem();
+ break;
+
+ case 'edit-command':
+ command.hidden = true;
+ e.canExecute = false;
+ break;
+
+ case 'delete-command':
+ case 'cut-command':
+ case 'copy-command':
+ e.canExecute = hasSelected() && !isTopLevelItem();
+ break;
+
+ case 'paste-command':
+ updatePasteCommand();
+ break;
+
+ case 'sort-command':
+ case 'add-new-bookmark-command':
+ case 'new-folder-command':
+ e.canExecute = !isRecentOrSearch();
+ break;
+
+ case 'open-in-new-tab-command':
+ case 'open-in-new-window-command':
+ case 'open-incognito-window-command':
+ // We use "open all" when the tree is the activeElement and
+ // updateOpenCommands uses 0, 1 and > 1 to determine what to show.
+ updateOpenCommands(e, command, hasSelected() ? 2 : 0);
+ break;
+ }
+});
+
+/**
+ * Update the canExecute state of the commands when the selection changes.
+ * @param {Event} e The change event object.
+ */
+function updateCommandsBasedOnSelection(e) {
+ if (e.target == document.activeElement) {
+ // Paste only needs to updated when the tree selection changes.
+ var commandNames = ['copy', 'cut', 'delete', 'rename-folder', 'edit',
+ 'add-new-bookmark', 'new-folder', 'open-in-new-tab',
+ 'open-in-new-window', 'open-incognito-window'];
+
+ if (e.target == tree) {
+ commandNames.push('paste', 'show-in-folder', 'sort');
+ }
+
+ commandNames.forEach(function(baseId) {
+ $(baseId + '-command').canExecuteChange();
+ });
+ }
+}
+
+list.addEventListener('change', updateCommandsBasedOnSelection);
+tree.addEventListener('change', updateCommandsBasedOnSelection);
+
+document.addEventListener('command', function(e) {
+ var command = e.command;
+ var commandId = command.id;
+ console.log(command.id, 'executed', 'on', e.target);
+ if (commandId == 'import-menu-command') {
+ chrome.experimental.bookmarkManager.import();
+ } else if (command.id == 'export-menu-command') {
+ chrome.experimental.bookmarkManager.export();
+ }
+});
+
+/**
+ * Navigates to the folder that the selected item is in and selects it. This is
+ * used for the show-in-folder command.
+ */
+function showInFolder() {
+ var bookmarkId = list.selectedItem.bookmarkNode.id;
+ var parentId = list.selectedItem.bookmarkNode.parentId;
+
+ // After the list is loaded we should select the revealed item.
+ var f = function(e) {
+ var item = bmm.listLookup[bookmarkId];
+ if (item) {
+ list.selectionModel.leadItem = item;
+ item.selected = true;
+ }
+ list.removeEventListener('load', f);
+ }
+ list.addEventListener('load', f);
+ var treeItem = bmm.treeLookup[parentId];
+ treeItem.reveal();
+
+ navigateTo(parentId);
+}
+
+/**
+ * Opens URLs in new tab, window or incognito mode.
+ * @param {!Array.<string>} urls The URLs to open.
+ * @param {string} kind The kind is either 'tab', 'window', or 'incognito'.
+ */
+function openUrls(urls, kind) {
+ if (urls.length < 1)
+ return;
+
+ if (urls.length > 15) {
+ if (!confirm(localStrings.getStringF('should_open_all', urls.length)))
+ return;
+ }
+
+ // Fix '#124' URLs since open those in a new window does not work. We prepend
+ // the base URL when we encounter those.
+ var base = window.location.href.split('#')[0];
+ urls = urls.map(function(url) {
+ return url[0] == '#' ? base + url : url;
+ });
+
+ // Incognito mode is not yet supported by the extensions APIs.
+ // http://code.google.com/p/chromium/issues/detail?id=12658
+ if (kind == 'window') {
+ chrome.windows.create({url: urls[0]}, function(window) {
+ urls.forEach(function(url, i) {
+ if (i > 0)
+ chrome.tabs.create({url: url, windowId: window.id, selected: false});
+ });
+ });
+ } else if (kind == 'tab') {
+ urls.forEach(function(url, i) {
+ chrome.tabs.create({url: url, selected: !i});
+ });
+ } else {
+ window.location.href = urls[0];
+ }
+}
+
+/**
+ * Returns the selected bookmark nodes of the active element. Only call this
+ * if the list or the tree is focused.
+ * @return {!Array} Array of bookmark nodes.
+ */
+function getSelectedBookmarkNodes() {
+ if (document.activeElement == list) {
+ return list.selectedItems.map(function(item) {
+ return item.bookmarkNode;
+ });
+ } else if (document.activeElement == tree) {
+ return [tree.selectedItem.bookmarkNode];
+ } else {
+ throw Error('getSelectedBookmarkNodes called when wrong element focused.');
+ }
+}
+
+/**
+ * @return {!Array.<string>} An array of the selected bookmark IDs.
+ */
+function getSelectedBookmarkIds() {
+ return getSelectedBookmarkNodes().map(function(node) {
+ return node.id;
+ });
+}
+
+/**
+ * Opens the selected bookmarks.
+ */
+function openBookmarks(kind) {
+ // If we have selected any folders we need to find all items recursively.
+ // We can do several async calls to getChildren but instead we do a single
+ // call to getTree and only add the subtrees of the selected items.
+
+ var urls = [];
+ var idMap = {};
+
+ // Traverses the tree until it finds a node tree that should be added. Then
+ // we switch over to use addNodes. We could merge these two functions into
+ // one but that would make the code less readable.
+ function traverseNodes(node) {
+ if (node.id in idMap) {
+ addNodes(node);
+ } else if (node.children) {
+ for (var i = 0; i < node.children.length; i++) {
+ traverseNodes(node.children[i]);
+ }
+ }
+ }
+
+ // Adds the node and all the descendants
+ function addNodes(node) {
+ if (node.children) {
+ for (var i = 0; i < node.children.length; i++) {
+ addNodes(node.children[i]);
+ }
+ } else {
+ urls.push(node.url);
+ }
+ }
+
+ var nodes = getSelectedBookmarkNodes();
+
+ // Create a map for simpler lookup later.
+ nodes.forEach(function(node) {
+ idMap[node.id] = true;
+ });
+ chrome.bookmarks.getTree(function(node) {
+ traverseNodes(node[0]);
+ openUrls(urls, kind);
+ });
+}
+
+/**
+ * Deletes the selected bookmarks.
+ */
+function deleteBookmarks() {
+ getSelectedBookmarkIds().forEach(function(id) {
+ chrome.bookmarks.removeTree(id);
+ });
+}
+
+function handleCommand(e) {
+ var command = e.command;
+ var commandId = command.id;
+ switch (commandId) {
+ case 'show-in-folder-command':
+ showInFolder();
+ break;
+ case 'open-in-new-tab-command':
+ openBookmarks('tab');
+ break;
+ case 'open-in-new-window-command':
+ openBookmarks('window');
+ break;
+ case 'open-in-new-incognito-command':
+ openBookmarks('incognito');
+ break;
+ case 'delete-command':
+ deleteBookmarks();
+ break;
+ case 'copy-command':
+ chrome.experimental.bookmarkManager.copy(getSelectedBookmarkIds());
+ break;
+ case 'cut-command':
+ chrome.experimental.bookmarkManager.cut(getSelectedBookmarkIds());
+ break;
+ case 'paste-command':
+ chrome.experimental.bookmarkManager.paste(list.parentId);
+ break;
+ case 'sort-command':
+ chrome.experimental.bookmarkManager.sortChildren(list.parentId);
+ break;
+ }
+}
+
+// TODO(arv): Move shortcut to HTML?
+
+// Meta+Backspace on Mac, Del on other platforms.
+$('delete-command').shortcut = cr.isMac ? 'U+0008-meta' : 'U+007F';
+
+list.addEventListener('command', handleCommand);
+tree.addEventListener('command', handleCommand);
+
+// Listen to copy, cut and paste events and execute the associated commands.
+document.addEventListener('copy', function(e) {
+ $('copy-command').execute();
+});
+
+document.addEventListener('cut', function(e) {
+ $('cut-command').execute();
+});
+
+document.addEventListener('paste', function(e) {
+ // Paste is a bit special since we need to do an async call to see if we can
+ // paste because the paste command might not be up to date.
+ updatePasteCommand(function() {
+ $('paste-command').execute();
+ });
+});
+
+</script>
+<script>
+
+// TODO(arv): Remove hack when experimental API is available.
+
+var localStrings = new LocalStrings;
+
+/**
+ * Sets the i18n template data.
+ * @param {!Object} data The object with the i18n messages.
+ */
+function setTemplateData(data) {
+ // The strings may contain & which we need to strip.
+ for (var key in data) {
+ data[key] = data[key].replace(/&/, '');
+ }
+ localStrings.templateData = data;
+ i18nTemplate.process(document, data);
+}
+
+var useFallbackData = true;
+if (chrome.experimental &&
+ chrome.experimental.bookmarkManager &&
+ chrome.experimental.bookmarkManager.getStrings) {
+ useFallbackData = false;
+ chrome.experimental.bookmarkManager.getStrings(function(data) {
+ setTemplateData(data);
+ });
+}
+
+if (useFallbackData) {
+ console.warn('The bookmark manager needs some experimental APIs');
+
+ // TODO(arv): This is just temporary while we are developing so that people
+ // without the experimental API can run this.
+ var fakeData = {
+ 'add_new_bookmark': 'Add page...',
+ 'copy': '&Copy',
+ 'cut': 'Cu&t',
+ 'delete': '&Delete',
+ 'edit': 'Edit...',
+ 'export_menu': 'Export bookmarks...',
+ 'import_menu': 'Import bookmarks...',
+ 'new_folder': 'Add folder...',
+ 'open_all': 'Open all bookmarks',
+ 'open_all_incognito': 'Open all bookmarks in incognito window',
+ 'open_all_new_window': 'Open all bookmarks in new window',
+ 'open_in_new_tab': 'Open in new tab',
+ 'open_in_new_window': 'Open in new window',
+ 'open_incognito': 'Open in incognito window',
+ 'organize_menu': 'Organize',
+ 'paste': '&Paste',
+ 'remove': 'Delete',
+ 'rename_folder': 'Rename...',
+ 'search_button': 'Search bookmarks',
+ 'should_open_all': 'Are you sure you want to open $1 tabs?',
+ 'show_in_folder': 'Show in folder',
+ 'sort': 'Reorder by title',
+ 'title': 'Bookmark Manager',
+ 'tools_menu': 'Tools'
+ };
+ setTemplateData(fakeData);
+}
+
+</script>
+
+<div id="drop-overlay"></div>
+
+</body>
+</html>
diff --git a/chrome/browser/resources/bookmark_manager/manifest.json b/chrome/browser/resources/bookmark_manager/manifest.json
new file mode 100644
index 0000000..eca3190
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/manifest.json
@@ -0,0 +1,10 @@
+{
+ "name": "Bookmark Manager",
+ "version": "0.1",
+ "description": "Bookmark Manager",
+ "permissions": [
+ "bookmarks",
+ "experimental",
+ "tabs"
+ ]
+}