diff options
author | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-02 21:22:36 +0000 |
---|---|---|
committer | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-02 21:22:36 +0000 |
commit | 857bff35929a3af15166bd3e7f885e1d7113c050 (patch) | |
tree | ae104d0f19d371ce39b9239fddb31f280dd358bc | |
parent | 1036feaea6379716f3819c87b83b2018498b5dd8 (diff) | |
download | chromium_src-857bff35929a3af15166bd3e7f885e1d7113c050.zip chromium_src-857bff35929a3af15166bd3e7f885e1d7113c050.tar.gz chromium_src-857bff35929a3af15166bd3e7f885e1d7113c050.tar.bz2 |
Bookmark manager fixes
1. Implement "Add page" and "Add folder".
2. Fix issue where dragging a folder to a subfolder regressed.
3. Disable "Open ..." on folders that have no bookmark descendandts.
4. Add bmm.TreeLoader so we can reuse the call to getTree.
5. Add bmm.TreeIterator so we do not have to duplicate tree traversal code.
6. Fix issue where fast changes in the tree caused infinite navigation hangs.
7. Add some more strings.
8. Remove old temporary hack for the strings since we really need the experimental API at this point anyway.
9. Prevent system beep on copy and cut.
BUG=32194, 36457, 36459
TEST=None
Review URL: http://codereview.chromium.org/660196
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@40433 0039d316-1c4b-4281-b951-d872f2087c98
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> |