summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chrome/app/generated_resources.grd9
-rw-r--r--chrome/browser/extensions/extension_bookmark_manager_api.cc9
-rw-r--r--chrome/browser/resources/bookmark_manager/css/list.css1
-rw-r--r--chrome/browser/resources/bookmark_manager/css/menu.css1
-rw-r--r--chrome/browser/resources/bookmark_manager/css/tree.css7
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm.js82
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js69
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js16
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm/treeiterator.js113
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm/treeiterator_test.html94
-rw-r--r--chrome/browser/resources/bookmark_manager/js/bmm_test.html157
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html9
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/promise.js173
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/promise_test.html225
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/list.js6
-rw-r--r--chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js7
-rw-r--r--chrome/browser/resources/bookmark_manager/main.html308
17 files changed, 1123 insertions, 163 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index b358b59..50de6ac 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -6147,6 +6147,15 @@ Keep your key file in a safe place. You will need it to create new versions of y
<message name="IDS_BOOKMARK_MANAGER_SEARCH_TITLE" desc="Text shown before the search text field.">
Search:
</message>
+ <message name="IDS_BOOKMARK_MANAGER_NAME_INPUT_PLACE_HOLDER" desc="Text to show in the URL input field when editing or creating bookmarks.">
+ Name
+ </message>
+ <message name="IDS_BOOKMARK_MANAGER_URL_INPUT_PLACE_HOLDER" desc="Text to show in the URL input field when editing or creating bookmarks.">
+ URL
+ </message>
+ <message name="IDS_BOOKMARK_MANAGER_INVALID_URL" desc="Error message to display when the user tries to edit or create a bookmark with an invalud URL.">
+ Invalid URL.
+ </message>
<!--Bookmark manager tooltip strings-->
<message name="IDS_BOOKMARK_MANAGER_TOOLTIP_NEW_FOLDER_MAC" desc="Tooltip for bookmark manager New Folder button">
diff --git a/chrome/browser/extensions/extension_bookmark_manager_api.cc b/chrome/browser/extensions/extension_bookmark_manager_api.cc
index a67b716..e397111 100644
--- a/chrome/browser/extensions/extension_bookmark_manager_api.cc
+++ b/chrome/browser/extensions/extension_bookmark_manager_api.cc
@@ -376,7 +376,6 @@ bool BookmarkManagerGetStringsFunction::RunImpl() {
l10n_util::GetString(IDS_BOOMARK_BAR_OPEN_ALL_INCOGNITO));
localized_strings->SetString(L"remove",
l10n_util::GetString(IDS_BOOKMARK_BAR_REMOVE));
-
localized_strings->SetString(L"copy",
l10n_util::GetString(IDS_CONTENT_CONTEXT_COPY));
localized_strings->SetString(L"cut",
@@ -385,6 +384,14 @@ bool BookmarkManagerGetStringsFunction::RunImpl() {
l10n_util::GetString(IDS_CONTENT_CONTEXT_PASTE));
localized_strings->SetString(L"delete",
l10n_util::GetString(IDS_CONTENT_CONTEXT_DELETE));
+ localized_strings->SetString(L"new_folder_name",
+ l10n_util::GetString(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME));
+ localized_strings->SetString(L"name_input_placeholder",
+ l10n_util::GetString(IDS_BOOKMARK_MANAGER_NAME_INPUT_PLACE_HOLDER));
+ localized_strings->SetString(L"url_input_placeholder",
+ l10n_util::GetString(IDS_BOOKMARK_MANAGER_URL_INPUT_PLACE_HOLDER));
+ localized_strings->SetString(L"invalid_url",
+ l10n_util::GetString(IDS_BOOKMARK_MANAGER_INVALID_URL));
ChromeURLDataManager::DataSource::SetFontAndTextDirection(localized_strings);
diff --git a/chrome/browser/resources/bookmark_manager/css/list.css b/chrome/browser/resources/bookmark_manager/css/list.css
index 0170566..e148337 100644
--- a/chrome/browser/resources/bookmark_manager/css/list.css
+++ b/chrome/browser/resources/bookmark_manager/css/list.css
@@ -5,7 +5,6 @@ list {
}
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);
diff --git a/chrome/browser/resources/bookmark_manager/css/menu.css b/chrome/browser/resources/bookmark_manager/css/menu.css
index 3fe0261..b7e0942 100644
--- a/chrome/browser/resources/bookmark_manager/css/menu.css
+++ b/chrome/browser/resources/bookmark_manager/css/menu.css
@@ -26,7 +26,6 @@ menu > :not(hr) {
background: transparent;
font: inherit;
border: 0;
-
padding: 3px 8px;
overflow: hidden;
text-overflow: ellipsis;
diff --git a/chrome/browser/resources/bookmark_manager/css/tree.css b/chrome/browser/resources/bookmark_manager/css/tree.css
index 292f76c..d1825e4 100644
--- a/chrome/browser/resources/bookmark_manager/css/tree.css
+++ b/chrome/browser/resources/bookmark_manager/css/tree.css
@@ -6,7 +6,6 @@ tree {
.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);
@@ -116,6 +115,12 @@ html[dir=rtl] .tree-item[expanded] > .tree-row > .expand-icon {
background-image: url("../images/folder_closed.png");
}
+/* We need to ensure that even empty labels take up space */
+.tree-label:empty:after {
+ content: " ";
+ white-space: pre;
+}
+
.tree-rename > .tree-row > .tree-label {
-webkit-user-select: auto;
-webkit-user-modify: read-write-plaintext-only;
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm.js b/chrome/browser/resources/bookmark_manager/js/bmm.js
index 24c4ad0..73cefa6 100644
--- a/chrome/browser/resources/bookmark_manager/js/bmm.js
+++ b/chrome/browser/resources/bookmark_manager/js/bmm.js
@@ -3,10 +3,15 @@
// found in the LICENSE file.
cr.define('bmm', function() {
- function isFolder(bookmarkNode) {
- return !bookmarkNode.url;
- }
+ const TreeIterator = bmm.TreeIterator;
+ const Promise = cr.Promise;
+ /**
+ * Whether a node contains another node.
+ * @param {!BookmarkTreeNode} parent
+ * @param {!BookmarkTreeNode} descendant
+ * @return {boolean} Whether the parent contains the descendant.
+ */
function contains(parent, descendant) {
if (descendant.parentId == parent.id)
return true;
@@ -17,8 +22,77 @@ cr.define('bmm', function() {
return this.contains(parent, parentTreeItem.bookmarkNode);
}
+ /**
+ * @param {!BookmarkTreeNode} node The node to test.
+ * @return {boolean} Whether a bookmark node is a folder.
+ */
+ function isFolder(node) {
+ return !('url' in node);
+ }
+
+ var loadingPromise;
+
+ /**
+ * Loads the entire bookmark tree and returns a {@code cr.Promise} that will
+ * be fulfilled when done. This reuses multiple loads so that we never load
+ * more than one tree at the same time.
+ * @return {!cr.Promise} The future promise for the load.
+ */
+ function loadTree() {
+ var p = new Promise;
+ if (!loadingPromise) {
+ loadingPromise = new Promise;
+ chrome.bookmarks.getTree(function(nodes) {
+ loadingPromise.value = nodes[0];
+ loadingPromise = null;
+ });
+ }
+ loadingPromise.addListener(function(n) {
+ p.value = n;
+ });
+ return p;
+ }
+
+ /**
+ * Helper function for {@code loadSubtree}. This does an in order search of
+ * the tree.
+ * @param {!BookmarkTreeNode} node The node to start searching at.
+ * @param {string} id The ID of the node to find.
+ * @return {BookmarkTreeNode} The found node or null if not found.
+ */
+ function findNode(node, id) {
+ var it = new TreeIterator(node);
+ var n;
+ while (it.moveNext()) {
+ n = it.current;
+ if (n.id == id)
+ return n;
+ }
+ return null;
+ }
+
+ /**
+ * Loads a subtree of the bookmark tree and returns a {@code cr.Promise} that
+ * will be fulfilled when done. This reuses multiple loads so that we never
+ * load more than one tree at the same time. (This actually loads the entire
+ * tree but it will only return the relevant subtree in the value of the
+ * future promise.)
+ * @return {!cr.Promise} The future promise for the load.
+ */
+ function loadSubtree(id) {
+ var p = new Promise;
+ var lp = loadTree();
+ lp.addListener(function(tree) {
+ var node = findNode(tree, id);
+ p.value = node || Error('Failed to load subtree ' + id);
+ });
+ return p;
+ }
+
return {
+ contains: contains,
isFolder: isFolder,
- contains: contains
+ loadSubtree: loadSubtree,
+ loadTree: loadTree
};
});
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js
index e1ea344..f244936 100644
--- a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js
+++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js
@@ -13,6 +13,19 @@ cr.define('bmm', function() {
var listLookup = {};
/**
+ * Removes all children and appends a new child.
+ * @param {!Node} parent The node to remove all children from.
+ * @param {!Node} newChild The new child to append.
+ */
+ function replaceAllChildren(parent, newChild) {
+ var n;
+ while ((n = parent.lastChild)) {
+ parent.removeChild(n);
+ }
+ parent.appendChild(newChild);
+ }
+
+ /**
* Creates a new bookmark list.
* @param {Object=} opt_propertyBag Optional properties.
* @constructor
@@ -122,7 +135,8 @@ cr.define('bmm', function() {
var listItem = listLookup[id];
if (listItem) {
listItem.bookmarkNode.title = changeInfo.title;
- listItem.bookmarkNode.url = changeInfo.url;
+ if ('url' in changeInfo)
+ listItem.bookmarkNode.url = changeInfo['url'];
updateListItem(listItem, listItem.bookmarkNode, list.showFolder());
}
},
@@ -217,7 +231,7 @@ cr.define('bmm', function() {
var url = this.bookmarkNode.url;
var title = this.bookmarkNode.title;
- var isFolder = !url;
+ var isFolder = bmm.isFolder(this.bookmarkNode);
var listItem = this;
var labelEl = this.firstChild;
var urlEl = this.querySelector('.url');
@@ -236,8 +250,10 @@ cr.define('bmm', function() {
if (!isFolder)
urlInput.value = url;
// fall through
+ cr.dispatchSimpleEvent(listItem, 'canceledit', true);
case 'Enter':
- listItem.parentNode.focus();
+ if (listItem.parentNode)
+ listItem.parentNode.focus();
}
}
@@ -260,7 +276,9 @@ cr.define('bmm', function() {
this.draggable = false;
labelInput = doc.createElement('input');
- labelEl.replaceChild(labelInput, labelEl.firstChild);
+ labelInput.placeholder =
+ localStrings.getString('name_input_placeholder');
+ replaceAllChildren(labelEl, labelInput);
labelInput.value = title;
if (!isFolder) {
@@ -270,10 +288,13 @@ cr.define('bmm', function() {
urlInput = doc.createElement('input');
urlInput.type = 'url';
urlInput.required = true;
+ urlInput.placeholder =
+ localStrings.getString('url_input_placeholder');
+
// We also need a name for the input for the CSS to work.
urlInput.name = '-url-input-' + cr.createUid();
form.appendChild(urlInput);
- urlEl.replaceChild(form, urlEl.firstChild);
+ replaceAllChildren(urlEl, form);
urlInput.value = url;
}
@@ -287,7 +308,7 @@ cr.define('bmm', function() {
});
labelInput.addEventListener('keydown', handleKeydown);
labelInput.addEventListener('blur', handleBlur);
- cr.ui.limitInputWidth(labelInput, this, 20);
+ cr.ui.limitInputWidth(labelInput, this, 200);
labelInput.focus();
labelInput.select();
@@ -297,27 +318,41 @@ cr.define('bmm', function() {
});
urlInput.addEventListener('keydown', handleKeydown);
urlInput.addEventListener('blur', handleBlur);
- cr.ui.limitInputWidth(urlInput, this, 20);
+ cr.ui.limitInputWidth(urlInput, this, 200);
}
} else {
// Check that we have a valid URL and if not we do not change the
// editing mode.
- var newUrl;
if (!isFolder) {
var urlInput = this.querySelector('.url input');
+ var newUrl = urlInput.value;
if (!urlInput.validity.valid) {
- return;
-
- } else {
- newUrl = urlInput.value;
- urlEl.textContent = this.bookmarkNode.url = newUrl;
+ // WebKit does not do URL fix up so we manually test if prepending
+ // 'http://' would make the URL valid.
+ // https://bugs.webkit.org/show_bug.cgi?id=29235
+ urlInput.value = 'http://' + newUrl;
+ if (!urlInput.validity.valid) {
+ // still invalid
+ urlInput.value = newUrl;
+
+ // In case the item was removed before getting here we should
+ // not alert.
+ if (listItem.parentNode) {
+ alert(localStrings.getString('invalid_url'));
+ }
+ urlInput.focus();
+ urlInput.select();
+ return;
+ }
+ newUrl = 'http://' + newUrl;
}
+ urlEl.textContent = this.bookmarkNode.url = newUrl;
}
this.removeAttribute('editing');
- this.draggable = false;
+ this.draggable = true;
labelInput = this.querySelector('.label input');
var newLabel = labelInput.value;
@@ -347,9 +382,8 @@ cr.define('bmm', function() {
function updateListItem(el, bookmarkNode, showFolder) {
var labelEl = el.firstChild;
- const NBSP = '\u00a0';
- labelEl.textContent = bookmarkNode.title || NBSP;
- if (bookmarkNode.url) {
+ labelEl.textContent = bookmarkNode.title;
+ if (!bmm.isFolder(bookmarkNode)) {
labelEl.style.backgroundImage = url('chrome://favicon/' +
bookmarkNode.url);
var urlEl = el.childNodes[1].firstChild;
@@ -379,6 +413,7 @@ cr.define('bmm', function() {
})();
return {
+ createListItem: createListItem,
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
index c5f1ad8..8acfa28 100644
--- a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js
+++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarktree.js
@@ -41,8 +41,11 @@ cr.define('bmm', function() {
* 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.
+ * 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.
+ *
+ * This also exoands the parent so that newly added children are revealed.
*
* @param {!cr.ui.TreeItem} parent The parent tree item.
* @param {!cr.ui.TreeItem} treeItem The tree item to add.
@@ -55,6 +58,7 @@ cr.define('bmm', function() {
return item.id;
}).indexOf(treeItem.bookmarkNode.id);
parent.addAt(treeItem, index);
+ parent.expanded = true;
if (opt_f)
opt_f();
});
@@ -104,15 +108,9 @@ cr.define('bmm', function() {
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;
- });
+ addTreeItem(newParentItem, treeItem);
}
},
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/treeiterator.js b/chrome/browser/resources/bookmark_manager/js/bmm/treeiterator.js
new file mode 100644
index 0000000..042f24f
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/bmm/treeiterator.js
@@ -0,0 +1,113 @@
+// 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() {
+ /**
+ * An inorder (document order) iterator for iterating over a bookmark tree.
+ *
+ * <pre>
+ * var it = new TreeIterator(node);
+ * while (it.moveNext()) {
+ * print(it.current.title);
+ * }
+ * </pre>
+ *
+ * @param {!BookmarkTreeNode} node The node to start at.
+ * @constructor
+ */
+ function TreeIterator(node) {
+ this.current_ = node;
+ this.parentStack_ = [];
+ this.indexStack_ = [];
+ }
+
+ /**
+ * Helper function for {@code TreeIterator.prototype.next}. This returns the
+ * next node in document order.
+ * @param {BookmarkTreeNode} node The current node.
+ * @param {!Array.<!BookmarkTreeNode>} parents A stack of parents.
+ * @param {!Array.<number>} index A stack of indexes.
+ * @return {BookmarkTreeNode} The next node or null if no more nodes can be
+ * found.
+ */
+ function getNext(node, parents, index) {
+ var i, p;
+
+ if (!node)
+ return null;
+
+ // If the node has children return first child.
+ if (node.children && node.children.length) {
+ parents.push(node);
+ index.push(0);
+ return node.children[0];
+ }
+
+ if (!parents.length)
+ return null;
+
+ // Walk up the parent stack until we find a node that has a next sibling.
+ while (node) {
+ p = parents[parents.length - 1];
+ if (!p)
+ return null;
+ i = index[index.length - 1];
+ if (i + 1 < p.children.length)
+ break;
+ node = parents.pop();
+ index.pop();
+ }
+
+ // Walked out of subtree.
+ if (!parents.length || !node)
+ return null;
+
+ // Return next child.
+ i = ++index[index.length - 1];
+ p = parents[parents.length - 1];
+ return p.children[i];
+ }
+
+ TreeIterator.prototype = {
+ /**
+ * Whether the next move will be the first move.
+ * @type {boolean}
+ * @private
+ */
+ first_: true,
+
+ /**
+ * Moves the iterator to the next item.
+ * @return {boolean} Whether we succeeded moving to the next item. This
+ * returns false when we have moved off the end of the iterator.
+ */
+ moveNext: function() {
+ // The first call to this should move us to the first node.
+ if (this.first_) {
+ this.first_ = false;
+ return true;
+ }
+ this.current_ = getNext(this.current_, this.parentStack_,
+ this.indexStack_);
+
+ return !!this.current_;
+ },
+
+ /**
+ * The current item. This throws an exception if trying to access after
+ * {@code moveNext} has returned false or before {@code moveNext} has been
+ * called.
+ * @type {!BookmarkTreeNode}
+ */
+ get current() {
+ if (!this.current_ || this.first_)
+ throw Error('No such element');
+ return this.current_;
+ }
+ };
+
+ return {
+ TreeIterator: TreeIterator
+ };
+});
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/treeiterator_test.html b/chrome/browser/resources/bookmark_manager/js/bmm/treeiterator_test.html
new file mode 100644
index 0000000..543ef73
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/bmm/treeiterator_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+<head>
+<!-- TODO(arv): Check in Closue unit tests and make this run as part of the
+ tests -->
+<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+<script src="../cr.js"></script>
+<script src="treeiterator.js"></script>
+<script>
+
+goog.require('goog.testing.jsunit');
+
+</script>
+</head>
+<body>
+<script>
+
+const TreeIterator = bmm.TreeIterator;
+
+var tree = {
+ id: 0,
+ children: [
+ {
+ id: 1,
+ children: [
+ {id: 2},
+ {id: 3, children: []}
+ ]
+ },
+ {id: 4},
+ {id: 5}
+ ]
+};
+
+function testIteration() {
+ var it = new TreeIterator(tree);
+ var expextedIds = [0, 1, 2, 3, 4, 5];
+ var i = 0;
+ while (it.moveNext()) {
+ var node = it.current;
+ assertEquals(expextedIds[i], node.id);
+ i++;
+ }
+}
+
+function testIteration2() {
+ var it = new TreeIterator(tree.children[0]);
+ var expextedIds = [1, 2, 3];
+ var i = 0;
+ while (it.moveNext()) {
+ var node = it.current;
+ assertEquals(expextedIds[i], node.id);
+ i++;
+ }
+}
+
+function testIteration3() {
+ var it = new TreeIterator(tree.children[1]);
+ var expextedIds = [4];
+ var i = 0;
+ while (it.moveNext()) {
+ var node = it.current;
+ assertEquals(expextedIds[i], node.id);
+ i++;
+ }
+}
+
+function testThrowsAfterEnd() {
+ // Same as testIteration3
+ var it = new TreeIterator(tree.children[1]);
+ var expextedIds = [4];
+ var i = 0;
+ while (it.moveNext()) {
+ var node = it.current;
+ assertEquals(expextedIds[i], node.id);
+ i++;
+ }
+
+ assertThrows(function() {
+ it.current;
+ });
+}
+
+function testThrowsBeforeMoveNext() {
+ // Same as testIteration3
+ var it = new TreeIterator(tree);
+ assertThrows(function() {
+ it.current;
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm_test.html b/chrome/browser/resources/bookmark_manager/js/bmm_test.html
new file mode 100644
index 0000000..c336b4e
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/bmm_test.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<html>
+<head>
+<!-- TODO(arv): Check in Closue unit tests and make this run as part of the
+ tests -->
+<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+<script src="cr.js"></script>
+<script src="cr/promise.js"></script>
+<script src="bmm/treeiterator.js"></script>
+<script src="bmm.js"></script>
+<script>
+
+goog.require('goog.testing.jsunit');
+
+</script>
+</head>
+<body>
+<script>
+
+var tree = {
+ id: 0,
+ children: [
+ {
+ id: 1,
+ children: [
+ {id: 2},
+ {id: 3, children: []}
+ ]
+ },
+ {id: 4},
+ {id: 5}
+ ]
+};
+
+// Mock chrome.bookmarks.getTree
+crome = chrome || {};
+crome.bookmarks = chrome.bookmarks || {};
+chrome.bookmarks.getTree = function f(callback) {
+ f.callbacks_ = f.callbacks_ || [];
+ f.callbacks_.push(callback);
+ f.$calls++;
+};
+chrome.bookmarks.getTree.load = function(node) {
+ var callbacks = chrome.bookmarks.getTree.callbacks_;
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i].call(null, [node]);
+ }
+ chrome.bookmarks.getTree.callbacks_ = [];
+};
+
+function setUp() {
+ chrome.bookmarks.getTree.$calls = 0;
+}
+
+function testLoadSingle() {
+ var calls = 0;
+ function f(node) {
+ calls++;
+ assertEquals(tree, node);
+ }
+ var p = bmm.loadTree();
+ p.addListener(f);
+
+ chrome.bookmarks.getTree.load(tree);
+ assertEquals(1, calls);
+ assertEquals(1, chrome.bookmarks.getTree.$calls);
+}
+
+function testLoadMultiple() {
+ var calls1 = 0;
+ var calls2 = 0;
+ function f1(node) {
+ calls1++;
+ assertEquals(tree, node);
+ }
+ function f2(node) {
+ calls2++;
+ assertEquals(tree, node);
+ }
+
+ var p = bmm.loadTree();
+ p.addListener(f1);
+ p.addListener(f2);
+
+ chrome.bookmarks.getTree.load(tree);
+ assertEquals(1, calls1);
+ assertEquals(1, calls2);
+ assertEquals(1, chrome.bookmarks.getTree.$calls);
+}
+
+function testLoadSubtree() {
+ var calls = 0;
+ function f(node) {
+ calls++;
+ assertEquals(tree.children[0], node);
+ }
+ var p = bmm.loadSubtree(1);
+ p.addListener(f);
+
+ chrome.bookmarks.getTree.load(tree);
+ assertEquals(1, calls);
+ assertEquals(1, chrome.bookmarks.getTree.$calls);
+}
+
+function testLoadMixed() {
+ var calls1 = 0;
+ var calls2 = 0;
+ function f1(node) {
+ calls1++;
+ assertEquals(tree.children[0], node);
+ }
+ function f2(node) {
+ calls2++;
+ assertEquals(tree, node);
+ }
+ var p1 = bmm.loadSubtree(1);
+ p1.addListener(f1);
+ var p2 = bmm.loadTree(1);
+ p2.addListener(f2);
+
+ chrome.bookmarks.getTree.load(tree);
+ assertEquals(1, calls1);
+ assertEquals(1, calls2);
+ assertEquals(1, chrome.bookmarks.getTree.$calls);
+}
+
+function testLoadTwice() {
+ var calls1 = 0;
+ var calls2 = 0;
+ function f1(node) {
+ calls1++;
+ assertEquals(tree.children[0], node);
+ }
+ function f2(node) {
+ calls2++;
+ assertEquals(tree, node);
+ }
+ var p1 = bmm.loadSubtree(1);
+ p1.addListener(f1);
+
+ chrome.bookmarks.getTree.load(tree);
+ assertEquals(1, calls1);
+ assertEquals(0, calls2);
+ assertEquals(1, chrome.bookmarks.getTree.$calls);
+
+ var p2 = bmm.loadTree(1);
+ p2.addListener(f2);
+
+ chrome.bookmarks.getTree.load(tree);
+ assertEquals(1, calls1);
+ assertEquals(1, calls2);
+ assertEquals(2, chrome.bookmarks.getTree.$calls);
+}
+
+</script>
+</body>
+</html>
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html
index 4f782d2..998e7f1 100644
--- a/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html
+++ b/chrome/browser/resources/bookmark_manager/js/cr/eventtarget_test.html
@@ -1,10 +1,8 @@
<!DOCTYPE html>
<html>
<head>
-<title></title>
-<style>
-
-</style>
+<!-- TODO(arv): Check in Closue unit tests and make this run as part of the
+ tests -->
<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
<script src="../cr.js"></script>
<script src="event.js"></script>
@@ -14,10 +12,8 @@
goog.require('goog.testing.jsunit');
</script>
-
</head>
<body>
-
<script>
const EventTarget = cr.EventTarget;
@@ -139,6 +135,5 @@ function testReturnFalse() {
}
</script>
-
</body>
</html>
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/promise.js b/chrome/browser/resources/bookmark_manager/js/cr/promise.js
new file mode 100644
index 0000000..e0494a5
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/promise.js
@@ -0,0 +1,173 @@
+// 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 implementes a future promise class.
+ */
+
+cr.define('cr', function() {
+
+ /**
+ * Sentinel used to mark a value as pending.
+ */
+ const PENDING_VALUE = {};
+
+ /**
+ * Creates a future promise.
+ * @param {Function=} opt_callback Callback.
+ * @constructor
+ */
+ function Promise(opt_callback) {
+ /**
+ * An array of the callbacks.
+ * @type {!Array.<!Function>}
+ * @private
+ */
+ this.callbacks_ = opt_callback ? [opt_callback] : [];
+ }
+
+ Promise.prototype = {
+ /**
+ * The current value.
+ * @type {*}
+ * @private
+ */
+ value_: PENDING_VALUE,
+
+ /**
+ * The value of the future promise. Accessing this before the promise has
+ * been fulfilled will throw an error. If this is set to an exception
+ * accessing this will throw as well.
+ * @type {*}
+ */
+ get value() {
+ return this.done ? this.value_ : undefined;
+ },
+ set value(value) {
+ if (!this.done) {
+ this.value_ = value;
+ for (var i = 0; i < this.callbacks_.length; i++) {
+ this.callbacks_[i].call(null, value);
+ }
+ this.callbacks_.length = 0;
+ }
+ },
+
+ /**
+ * Whether the future promise has been fulfilled.
+ * @type {boolean}
+ */
+ get done() {
+ return this.value_ !== PENDING_VALUE;
+ },
+
+ /**
+ * Adds a listener to the future promise. The function will be called when
+ * the promise is fulfilled. If the promise is already fullfilled this will
+ * never call the function.
+ * @param {!Function} fun The function to call.
+ */
+ addListener: function(fun) {
+ if (this.done)
+ fun(this.value);
+ else
+ this.callbacks_.push(fun);
+ },
+
+ /**
+ * Removes a previously added listener from the future promise.
+ * @param {!Function} fun The function to remove.
+ */
+ removeListener: function(fun) {
+ var i = this.callbacks_.indexOf(fun);
+ if (i >= 0)
+ this.callbacks_.splice(i, 1);
+ },
+
+ /**
+ * If the promise is done then this returns the string representation of
+ * the value.
+ * @return {string} The string representation of the promise.
+ * @override
+ */
+ toString: function() {
+ if (this.done)
+ return String(this.value);
+ else
+ return '[object Promise]';
+ },
+
+ /**
+ * Override to allow arithmetic.
+ * @override
+ */
+ valueOf: function() {
+ return this.value;
+ }
+ };
+
+ /**
+ * When a future promise is done call {@code fun}. This also calls the
+ * function if the promise has already been fulfilled.
+ * @param {!Promise} p The promise.
+ * @param {!Function} fun The function to call when the promise is fulfilled.
+ */
+ Promise.when = function(p, fun) {
+ p.addListener(fun);
+ };
+
+ /**
+ * Creates a new promise the will be fulfilled after {@code t} ms.
+ * @param {number} t The time to wait before the promise is fulfilled.
+ * @param {*=} opt_value The value to return after the wait.
+ * @return {!Promise} The new future promise.
+ */
+ Promise.wait = function(t, opt_value) {
+ var p = new Promise;
+ window.setTimeout(function() {
+ p.value = opt_value;
+ }, t);
+ return p;
+ };
+
+ /**
+ * Creates a new future promise that is fulfilled when any of the promises are
+ * fulfilled.
+ * @param {...!Promise} var_args The promises used to build up the new
+ * promise.
+ * @return {!Promise} The new promise that will be fulfilled when any of th
+ * passed in promises are fulfilled.
+ */
+ Promise.any = function(var_args) {
+ var p = new Promise;
+ function f(v) {
+ p.value = v;
+ }
+ for (var i = 0; i < arguments.length; i++) {
+ arguments[i].addListener(f);
+ }
+ return p;
+ };
+
+ /**
+ * Wraps an event in a future promise.
+ * @param {!EventTarget} target The object that dispatches the event.
+ * @param {string} type The type of the event.
+ * @param {boolean=} opt_useCapture Whether to listen to the capture phase or
+ * the bubble phase.
+ * @return {!Promise} The promise that will be fulfilled when the event is
+ * dispatched.
+ */
+ Promise.event = function(target, type, opt_useCapture) {
+ var p = new Promise;
+ target.addEventListener(type, function(e) {
+ p.value = e;
+ }, opt_useCapture);
+ return p;
+ };
+
+ return {
+ Promise: Promise
+ };
+})();
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html b/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html
new file mode 100644
index 0000000..19e3d1a
--- /dev/null
+++ b/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html
@@ -0,0 +1,225 @@
+<!DOCTYPE html>
+<html>
+<head>
+<!-- TODO(arv): Check in Closue unit tests and make this run as part of the
+ tests -->
+<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+<script src="../cr.js"></script>
+<script src="promise.js"></script>
+<script src="event.js"></script>
+<script>
+
+goog.require('goog.testing.jsunit');
+goog.require('goog.testing.MockClock');
+
+</script>
+</head>
+<body>
+<script>
+
+var mockClock;
+
+function setUp() {
+ mockClock = new goog.testing.MockClock();
+ mockClock.install();
+}
+
+function tearDown() {
+ mockClock.uninstall();
+}
+
+const Promise = cr.Promise;
+
+function testCallbacks() {
+ var calls1 = 0;
+ var calls2 = 0;
+ var V = {};
+ function f1(v) {
+ calls1++;
+ assertEquals(V, v);
+ }
+ function f2(v) {
+ calls2++;
+ assertEquals(V, v);
+ }
+ var p = new Promise;
+ p.addListener(f1);
+ p.addListener(f2);
+ p.value = V;
+ assertEquals(1, calls1);
+ assertEquals(1, calls2);
+}
+
+function testCallbacks2() {
+ var calls1 = 0;
+ var calls2 = 0;
+ var V = {};
+ function f1(v) {
+ calls1++;
+ assertEquals(V, v);
+ }
+ function f2(v) {
+ calls2++;
+ assertEquals(V, v);
+ }
+ var p = new Promise;
+ p.addListener(f1);
+ p.addListener(f2);
+ p.removeListener(f1);
+ p.value = V;
+ assertEquals(0, calls1);
+ assertEquals(1, calls2);
+}
+
+function testCallbacks3() {
+ var calls1 = 0;
+ var calls2 = 0;
+ var V = {};
+ function f1(v) {
+ calls1++;
+ assertEquals(V, v);
+ }
+ function f2(v) {
+ calls2++;
+ assertEquals(V, v);
+ }
+ var p = new Promise;
+ p.addListener(f1);
+ assertEquals(0, calls1);
+ assertEquals(0, calls2);
+ p.value = V;
+ assertEquals(1, calls1);
+ assertEquals(0, calls2);
+ p.addListener(f2);
+ assertEquals(1, calls1);
+ assertEquals(1, calls2);
+}
+
+function testCallbacks4() {
+ var calls1 = 0;
+ var calls2 = 0;
+ var V = {};
+ function f1(v) {
+ calls1++;
+ assertEquals(V, v);
+ }
+ function f2(v) {
+ calls2++;
+ assertEquals(V, v);
+ }
+ var p = new Promise(f1);
+ p.addListener(f2);
+ p.value = V;
+ assertEquals(1, calls1);
+ assertEquals(1, calls2);
+}
+
+function testThisInCallback() {
+ var calls = 0;
+ var V = {};
+ function f(v) {
+ calls++;
+ assertEquals(V, v);
+ assertNotEquals(p, this);
+ }
+ var p = new Promise;
+ p.addListener(f);
+ p.value = V;
+ assertEquals(1, calls);
+}
+
+function testPending() {
+ var p = new Promise;
+ assertEquals(undefined, p.value);
+ assertFalse(p.done);
+}
+
+function testValueCanBeUndefined() {
+ var p = new Promise;
+ p.value = undefined;
+ assertEquals(undefined, p.value);
+ assertTrue(p.done);
+}
+
+function testDone() {
+ var p = new Promise;
+ assertFalse(p.done);
+ p.value = 42;
+ assertTrue(p.done);
+}
+
+function testWhen() {
+ const V = {};
+ var calls = 0;
+ var p = new Promise;
+ p.value = V;
+ Promise.when(p, function(v) {
+ assertEquals(V, v);
+ calls++;
+ });
+ assertEquals(1, calls);
+}
+
+function testWhen2() {
+ const V = {};
+ var calls = 0;
+ var p = new Promise;
+ Promise.when(p, function(v) {
+ assertEquals(V, v);
+ calls++;
+ });
+ p.value = V;
+ assertEquals(1, calls);
+}
+
+function testWait() {
+ const S = {};
+ var p = Promise.wait(1000, S);
+ assertFalse(p.done);
+ mockClock.tick(500);
+ assertFalse(p.done);
+ mockClock.tick(500);
+ assertTrue(p.done);
+ assertEquals(S, p.value);
+}
+
+function testAny() {
+ var p1 = new Promise;
+ var p2 = new Promise;
+ var p3 = new Promise;
+
+ var any = Promise.any(p1, p2, p3);
+ p2.value = 2;
+ assertEquals(2, any.value);
+ p1.value = 1;
+ assertEquals(2, any.value);
+}
+
+function testEvent() {
+ var p = Promise.event(document.body, 'foo');
+ var e = new cr.Event('foo');
+ document.body.dispatchEvent(e);
+ assertEquals(e, p.value);
+}
+
+function testToString() {
+ var p1 = new Promise;
+ assertEquals('[object Promise]', String(p1));
+
+ var p2 = new Promise;
+ p2.value = 'Hello world';
+ assertEquals('Hello world', String(p2));
+}
+
+function testValueOf() {
+ var p = new Promise;
+ p.value = 42;
+
+ assertTrue(p < 43);
+ assertTrue(p > 41);
+ assertTrue(p == 42);
+}
+
+</script>
+</body>
+</html>
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js
index c70165d..9bdcd38 100644
--- a/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/list.js
@@ -1,3 +1,6 @@
+// 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.
// require: listselectionmodel.js
@@ -56,6 +59,9 @@ cr.define('cr.ui', function() {
get selectedItem() {
return this.selectionModel.selectedItem;
},
+ set selectedItem(selectedItem) {
+ this.selectionModel.selectedItem = selectedItem;
+ },
/**
* Convenience alias for selectionModel.selectedItems
diff --git a/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js b/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js
index 5956fd5..6f7088f 100644
--- a/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js
+++ b/chrome/browser/resources/bookmark_manager/js/cr/ui/tree.js
@@ -321,7 +321,7 @@ cr.define('cr.ui', function() {
*/
reveal: function() {
var pi = this.parentItem;
- while (!(pi instanceof Tree)) {
+ while (pi && !(pi instanceof Tree)) {
pi.expanded = true;
pi = pi.parentItem;
}
@@ -487,7 +487,10 @@ cr.define('cr.ui', function() {
// the input loses focus we set editing to false again.
input = this.ownerDocument.createElement('input');
input.value = text;
- labelEl.replaceChild(input, labelEl.firstChild);
+ if (labelEl.firstChild)
+ labelEl.replaceChild(input, labelEl.firstChild);
+ else
+ labelEl.appendChild(input);
input.addEventListener('keydown', handleKeydown);
input.addEventListener('blur', cr.bind(function() {
diff --git a/chrome/browser/resources/bookmark_manager/main.html b/chrome/browser/resources/bookmark_manager/main.html
index 7a1ea2b..6b34c58 100644
--- a/chrome/browser/resources/bookmark_manager/main.html
+++ b/chrome/browser/resources/bookmark_manager/main.html
@@ -26,6 +26,7 @@ Favicon of bmm does not work. No icon is showed.
<script src="js/cr.js"></script>
<script src="js/cr/event.js"></script>
<script src="js/cr/eventtarget.js"></script>
+<script src="js/cr/promise.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>
@@ -41,6 +42,7 @@ Favicon of bmm does not work. No icon is showed.
<script src="js/localstrings.js"></script>
<script src="js/i18ntemplate.js"></script>
+<script src="js/bmm/treeiterator.js"></script>
<script src="js/bmm.js"></script>
<script src="js/bmm/bookmarklist.js"></script>
<script src="js/bmm/bookmarktree.js"></script>
@@ -52,7 +54,6 @@ html, body {
width: 100%;
height: 100%;
cursor: default;
- font: 13px arial;
}
list {
@@ -88,7 +89,7 @@ list > * > * > span {
list > * > :first-child {
font-weight: bold;
- font-size: 14px;
+ font-size: 110%;
}
list > * > :last-child {
@@ -130,6 +131,17 @@ list > .folder > .label {
background-image: url("images/folder_closed.png");
}
+/* We need to ensure that even empty labels take up space */
+list > * > .label:empty:after,
+list > * > .url:empty:after {
+ content: " ";
+ white-space: pre;
+}
+
+list > .folder > .url:empty:after {
+ content: "";
+}
+
/*
/* Edit mode
*/
@@ -140,9 +152,9 @@ list .url input {
font-family: inherit;
font-size: inherit;
font-weight: inherit;
- border: 1px solid transparent;
- color: inherit;
- background: transparent;
+ color: black;
+ background: white;
+ border: 1px solid black;
margin: -2px -8px -2px -3px;
padding: 1px 7px 1px 1px;
outline: none;
@@ -167,13 +179,6 @@ list [editing] .url {
color: inherit;
}
-list [editing] input:focus {
- color: black;
- background: white;
- border: 1px solid black;
- outline: none;
-}
-
list .url form {
display: inline;
}
@@ -182,15 +187,11 @@ list .url > form > input {
-webkit-transition: color .15s, background-color .15s;
}
-list .url > form > :focus:invalid {
+list .url > form > :invalid {
background: #fdd;
color: black;
}
-list .url > form > :invalid {
- color: red;
-}
-
/* end editing */
html[dir=rtl] list > .folder > .label {
@@ -442,7 +443,13 @@ tree.addEventListener('change', function() {
*/
function navigateTo(id) {
console.info('navigateTo', window.location.hash, id);
- window.location.hash = id;
+ // Update the location hash using a timer to prevent reentrancy. This is how
+ // often we add history entries and the time here is a bit arbitrary but was
+ // picked as the smallest time a human perceives as instant.
+ clearTimeout(navigateTo.timer_);
+ navigateTo.timer_ = setTimeout(function() {
+ window.location.hash = tree.selectedItem.bookmarkId;
+ }, 300);
updateParentId(id);
}
@@ -596,8 +603,9 @@ function handleImportBegan() {
function handleImportEnded() {
chrome.bookmarks.onCreated.addListener(handleCreated);
- chrome.bookmarks.getTree(function(node) {
- var otherBookmarks = node[0].children[1].children;
+ var p = bmm.loadTree();
+ p.addListener(function(node) {
+ var otherBookmarks = node.children[1].children;
var importedFolder = otherBookmarks[otherBookmarks.length - 1];
var importId = importedFolder.id;
tree.insertSubtree(importedFolder);
@@ -727,9 +735,9 @@ var dnd = {
return false;
// If we are dragging a folder we cannot drop it on any of its descendants
- var dragBookmarkNode = bmm.treeLookup[dragId];
- if (dragBookmarkNode && bmm.isFolder(dragBookmarkNode) &&
- bmm.contains(dragBookmarkNode, overBookmarkNode)) {
+ var dragBookmarkItem = bmm.treeLookup[dragId];
+ var dragBookmarkNode = dragBookmarkItem && dragBookmarkItem.bookmarkNode;
+ if (dragBookmarkNode && bmm.contains(dragBookmarkNode, overBookmarkNode)) {
return false;
}
@@ -1254,28 +1262,53 @@ tree.contextMenu = $('context-menu');
* 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) {
+function updateOpenCommands(e, command) {
+ var selectedItem = e.target.selectedItem;
+ var selectionCount;
+ if (e.target == tree)
+ selectionCount = selectedItem ? 1 : 0;
+ else
+ selectionCount = e.target.selectedItems.length;
+
+ var isFolder = selectionCount == 1 &&
+ selectedItem.bookmarkNode &&
+ bmm.isFolder(selectedItem.bookmarkNode);
+ var multiple = selectionCount != 1 || isFolder;
+
+ function hasBookmarks(node) {
+ var it = new bmm.TreeIterator(node);
+ while (it.moveNext()) {
+ if (!bmm.isFolder(it.current))
+ return true;
+ }
+ return false;
+ }
+
switch (command.id) {
case 'open-in-new-tab-command':
- command.label = selectionCount == 1 ?
- localStrings.getString('open_in_new_tab') :
- localStrings.getString('open_all');
+ command.label = localStrings.getString(multiple ?
+ 'open_all' : 'open_in_new_tab');
break;
case 'open-in-new-window-command':
- command.label = selectionCount == 1 ?
- localStrings.getString('open_in_new_window') :
- localStrings.getString('open_all_new_window');
+ command.label = localStrings.getString(multiple ?
+ 'open_all_new_window' : 'open_in_new_window');
break;
case 'open-incognito-window-command':
- command.label = selectionCount == 1 ?
- localStrings.getString('open_incognito') :
- localStrings.getString('open_all_incognito');
+ command.label = localStrings.getString(multiple ?
+ 'open_all_incognito' : 'open_incognito');
break;
}
- e.canExecute = selectionCount > 0;
+ e.canExecute = selectionCount > 0 && !!selectedItem.bookmarkNode;
+ if (isFolder && e.canExecute) {
+ // We need to get all the bookmark items in this tree. If the tree does not
+ // contain any non-folders we need to disable the command.
+ var p = bmm.loadSubtree(selectedItem.bookmarkId);
+ p.addListener(function(node) {
+ command.disabled = !node || !hasBookmarks(node);
+ });
+ }
}
/**
@@ -1377,7 +1410,7 @@ list.addEventListener('canExecute', function(e) {
case 'open-in-new-tab-command':
case 'open-in-new-window-command':
case 'open-incognito-window-command':
- updateOpenCommands(e, command, e.target.selectedItems.length);
+ updateOpenCommands(e, command);
break;
}
});
@@ -1432,9 +1465,7 @@ tree.addEventListener('canExecute', function(e) {
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);
+ updateOpenCommands(e, command);
break;
}
});
@@ -1477,7 +1508,7 @@ document.addEventListener('command', function(e) {
function handleRename(e) {
var item = e.target;
var bookmarkNode = item.bookmarkNode;
- chrome.bookmarks.update(bookmarkNode.id, {'title': item.label});
+ chrome.bookmarks.update(bookmarkNode.id, {title: item.label});
}
tree.addEventListener('rename', handleRename);
@@ -1486,10 +1517,34 @@ list.addEventListener('rename', handleRename);
list.addEventListener('edit', function(e) {
var item = e.target;
var bookmarkNode = item.bookmarkNode;
- chrome.bookmarks.update(bookmarkNode.id, {
- 'title': bookmarkNode.title,
- 'url': bookmarkNode.url
- });
+ var context = {
+ title: bookmarkNode.title
+ };
+ if (!bmm.isFolder(bookmarkNode))
+ context.url = bookmarkNode.url;
+
+ if (bookmarkNode.id == 'new') {
+ // New page
+ context.parentId = bookmarkNode.parentId;
+ chrome.bookmarks.create(context, function(node) {
+ list.remove(item);
+ list.selectedItem = bmm.listLookup[node.id];
+ });
+ } else {
+ // Edit
+ chrome.bookmarks.update(bookmarkNode.id, context);
+ }
+});
+
+list.addEventListener('canceledit', function(e) {
+ var item = e.target;
+ var bookmarkNode = item.bookmarkNode;
+ if (bookmarkNode.id == 'new') {
+ list.remove(item);
+ list.selectionModel.leadItem = list.lastChild;
+ list.selectionModel.anchorItem = list.lastChild;
+ list.focus();
+ }
});
/**
@@ -1596,6 +1651,7 @@ function openBookmarks(kind) {
// 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) {
+ // This is not using the iterator since it uses breadth first search.
if (node.id in idMap) {
addNodes(node);
} else if (node.children) {
@@ -1607,12 +1663,11 @@ function openBookmarks(kind) {
// 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 it = new bmm.TreeIterator(node);
+ while (it.moveNext()) {
+ var n = it.current;
+ if (!bmm.isFolder(n))
+ urls.push(n.url);
}
}
@@ -1622,8 +1677,9 @@ function openBookmarks(kind) {
nodes.forEach(function(node) {
idMap[node.id] = true;
});
- chrome.bookmarks.getTree(function(node) {
- traverseNodes(node[0]);
+ var p = bmm.loadTree();
+ p.addListener(function(node) {
+ traverseNodes(node);
openUrls(urls, kind);
});
}
@@ -1637,6 +1693,50 @@ function deleteBookmarks() {
});
}
+/**
+ * Callback for the new folder command. This creates a new folder and starts
+ * a rename of it.
+ */
+function newFolder() {
+ var parentId = list.parentId;
+ var isTree = document.activeElement == tree;
+ chrome.bookmarks.create({
+ title: localStrings.getString('new_folder_name'),
+ parentId: parentId
+ }, function(newNode) {
+ // We need to do this in a timeout to be able to focus the newly created
+ // item.
+ setTimeout(function() {
+ var newItem = isTree ? bmm.treeLookup[newNode.id] :
+ bmm.listLookup[newNode.id];
+ document.activeElement.selectedItem = newItem;
+ newItem.editing = true;
+ });
+ });
+}
+
+/**
+ * Adds a page to the current folder. This is called by the
+ * add-new-bookmark-command handler.
+ */
+function addPage() {
+ var parentId = list.parentId;
+ var fakeNode = {
+ title: '',
+ url: '',
+ parentId: parentId,
+ id: 'new'
+ };
+ var newListItem = bmm.createListItem(fakeNode, false);
+ list.add(newListItem);
+ list.selectedItem = newListItem;
+ newListItem.editing = true;
+}
+
+/**
+ * Handler for the command event. This is used both for the tree and the list.
+ * @param {!Event} e The event object.
+ */
function handleCommand(e) {
var command = e.command;
var commandId = command.id;
@@ -1672,6 +1772,12 @@ function handleCommand(e) {
case 'edit-command':
document.activeElement.selectedItem.editing = true;
break;
+ case 'new-folder-command':
+ newFolder();
+ break;
+ case 'add-new-bookmark-command':
+ addPage();
+ break;
}
}
@@ -1683,86 +1789,48 @@ $('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();
-});
+// Execute the copy, cut and paste commands when those events are dispatched by
+// the browser. This allows us to rely on the browser to handle the keyboard
+// shortcuts for these commands.
+(function() {
+ function handle(id) {
+ return function(e) {
+ var command = $(id);
+ if (!command.disabled) {
+ command.execute();
+ e.preventDefault(); // Prevent the system beep
+ }
+ };
+ }
-document.addEventListener('cut', function(e) {
- $('cut-command').execute();
-});
+ // Listen to copy, cut and paste events and execute the associated commands.
+ document.addEventListener('copy', handle('copy-command'));
+ document.addEventListener('cut', handle('cut-command'));
-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();
+ var pasteHandler = handle('paste-command');
+ 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(pasteHandler);
});
-});
-
-</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.
+ * The local strings object which is used to do the translation.
+ * @type {!LocalStrings}
*/
-function setTemplateData(data) {
+var localStrings = new LocalStrings;
+
+// Get the localized strings from the backend.
+chrome.experimental.bookmarkManager.getStrings(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>