diff options
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> |