diff options
author | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-14 22:32:23 +0000 |
---|---|---|
committer | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-14 22:32:23 +0000 |
commit | 4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9 (patch) | |
tree | 5a2e69c5caacc2251599c3691ed662e072a28995 /chrome/browser/resources/bookmark_manager/js | |
parent | fdabdc748152405ebdd2f7910ee9d5aebe26588c (diff) | |
download | chromium_src-4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9.zip chromium_src-4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9.tar.gz chromium_src-4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9.tar.bz2 |
Bookmark manager: Middle click, Ctrl click, Shift+Control click should open bookmark link in a background tab etc..
BUG=40359
TEST=Middle click on a bookmark link. It should open in a background tab
Review URL: http://codereview.chromium.org/1553026
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@44560 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources/bookmark_manager/js')
5 files changed, 600 insertions, 33 deletions
diff --git a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js index a683208..a81ca23 100644 --- a/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js +++ b/chrome/browser/resources/bookmark_manager/js/bmm/bookmarklist.js @@ -152,11 +152,10 @@ cr.define('bmm', function() { handleClick_: function(e) { var self = this; - function dispatch(url, kind) { - var event = self.ownerDocument.createEvent('Event'); - event.initEvent('urlClicked', true, false); + function dispatch(url) { + var event = new cr.Event('urlClicked', true, false); event.url = url; - event.kind = kind; + event.originalEvent = e; self.dispatchEvent(event); } @@ -164,8 +163,7 @@ cr.define('bmm', function() { // Handle clicks on the links to URLs. if (el.href) { - dispatch(el.href, - e.shiftKey ? 'window' : e.button == 1 ? 'tab' : 'self'); + dispatch(el.href); // Handle middle click to open bookmark in a new tab. } else if (e.button == 1) { @@ -174,7 +172,7 @@ cr.define('bmm', function() { } var node = el.bookmarkNode; if (!bmm.isFolder(node)) - dispatch(node.url, 'tab'); + dispatch(node.url); } }, @@ -460,6 +458,7 @@ cr.define('bmm', function() { var urlEl = el.childNodes[1]; labelEl.href = urlEl.textContent = bookmarkNode.url; } else { + labelEl.href = '#' + bookmarkNode.id; el.className = 'folder'; } } @@ -470,25 +469,6 @@ cr.define('bmm', function() { return div; })(); - - /** - * Workaround for http://crbug.com/40902 - * @param {!HTMLElement} list The element to fix the width for. - */ - function fixListWidth(list) { - // The width of the list is wrong after its content has changed. Fortunately - // the reported offsetWidth is correct so we can detect the incorrect width. - if (list.offsetWidth != list.parentNode.clientWidth - list.offsetLeft) { - // Set the width to the correct size. This causes the relayout. - list.style.width = list.parentNode.clientWidth - list.offsetLeft + 'px'; - // Remove the temporary style.width in a timeout. Once the timer fires the - // size should not change since we already fixed the width. - window.setTimeout(function() { - list.style.width = ''; - }, 0); - } - } - return { createListItem: createListItem, BookmarkList: BookmarkList, diff --git a/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller.js b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller.js new file mode 100644 index 0000000..e6241de --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller.js @@ -0,0 +1,168 @@ +// 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 file provides a class that can be used to open URLs based + * on user interactions. It ensures a consistent behavior when it comes to + * holding down Ctrl and Shift while clicking or activating the a link. + * + * This depends on the {@code chrome.windows} and {@code chrome.tabs} + * extensions API. + */ + +cr.define('cr', function() { + + /** + * The kind of link open we want to perform. + * @enum {number} + */ + const LinkKind = { + FOREGROUND_TAB: 0, + BACKGROUND_TAB: 1, + WINDOW: 2, + SELF: 3, + INCOGNITO: 4 + }; + + /** + * This class is used to handle opening of links based on user actions. The + * following actions are currently implemented: + * + * * Press Ctrl and click a link. Or click a link with your middle mouse + * button (or mousewheel). Or press Enter while holding Ctrl. + * Opens the link in a new tab in the background . + * * Press Ctrl+Shift and click a link. Or press Shift and click a link with + * your middle mouse button (or mousewheel). Or press Enter while holding + * Ctrl+Shift. + * Opens the link in a new tab and switches to the newly opened tab. + * * Press Shift and click a link. Or press Enter while holding Shift. + * Opens the link in a new window. + * + * On Mac, uses Command instead of Ctrl. + * For keyboard support you need to use keydown. + * + * @param {!LocalStrings} localStrings The local strings object which is used + * to localize the warning prompt in case the user tries to open a lot of + * links. + * @constructor + */ + function LinkController(localStrings) { + this.localStrings_ = localStrings; + } + + LinkController.prototype = { + /** + * The number of links that can be opened before showing a warning confirm + * message. + */ + warningLimit: 15, + + /** + * The DOM window that we want to open links into in case we are opening + * links in the same window. + * @type {!Window} + */ + window: window, + + /** + * This method is used for showing the warning confirm message when the + * user is trying to open a lot of links. + * @param {number} The number of URLs to open. + * @return {string} The message to show the user. + */ + getWarningMessage: function(count) { + return this.localStrings_.getStringF('should_open_all', count); + }, + + /** + * Open an URL from a mouse or keyboard event. + * @param {string} url The URL to open. + * @param {!Event} e The event triggering the opening of the URL. + */ + openUrlFromEvent: function(url, e) { + // We only support keydown Enter and non right click events. + if (e.type == 'keydown') { + if(e.keyIdentifier != 'Enter') + return; + } else if (e.type != 'click' || e.button == 2) { + return; + } + + var kind; + var ctrl = cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey; + + if (e.button == 1 || ctrl) // middle, ctrl or keyboard + kind = e.shiftKey ? LinkKind.FOREGROUND_TAB : LinkKind.BACKGROUND_TAB; + else // left or keyboard + kind = e.shiftKey ? LinkKind.WINDOW : LinkKind.SELF; + + this.openUrls([url], kind); + }, + + + /** + * Opens a URL in a new tab, window or incognito window. + * @param {string} url The URL to open. + * @param {LinkKind} kind The kind of open we want to do. + */ + openUrl: function (url, kind) { + this.openUrls([url], kind); + }, + + /** + * Opens URLs in new tab, window or incognito mode. + * @param {!Array.<string>} urls The URLs to open. + * @param {LinkKind} kind The kind of open we want to do. + */ + openUrls: function (urls, kind) { + if (urls.length < 1) + return; + + if (urls.length > this.warningLimit) { + if (!this.window.confirm(this.getWarningMessage(urls.length))) + return; + } + + // Fix '#124' URLs since opening those in a new window does not work. We + // prepend the base URL when we encounter those. + var base = this.window.location.href.split('#')[0]; + urls = urls.map(function(url) { + return url[0] == '#' ? base + url : url; + }); + + var incognito = kind == LinkKind.INCOGNITO; + if (kind == LinkKind.WINDOW || incognito) { + chrome.windows.create({ + url: urls[0], + incognito: incognito + }, function(window) { + urls.forEach(function(url, i) { + if (i > 0) + chrome.tabs.create({ + url: url, + windowId: window.id, + selected: false + }); + }); + }); + } else if (kind == LinkKind.FOREGROUND_TAB || + kind == LinkKind.BACKGROUND_TAB) { + urls.forEach(function(url, i) { + chrome.tabs.create({ + url: url, + selected: kind == LinkKind.FOREGROUND_TAB && !i + }); + }); + } else { + this.window.location.href = urls[0]; + } + } + }; + + // Export + return { + LinkController: LinkController, + LinkKind: LinkKind + }; +}); diff --git a/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller_test.html b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller_test.html new file mode 100644 index 0000000..eb7ebd5 --- /dev/null +++ b/chrome/browser/resources/bookmark_manager/js/cr/linkcontroller_test.html @@ -0,0 +1,353 @@ +<!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="linkcontroller.js"></script> +<script> + +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.mockmatchers'); + +</script> +</head> +<body> +<script> + +var mockControl, propertyReplacer, mockWindow; +var chrome = chrome || {}; +chrome.tabs = chrome.tabs || {}; +chrome.windows = chrome.windows || {}; + +var ObjectEqualsMatcher = goog.testing.mockmatchers.ObjectEquals; +var SaveArgumentMatcher = goog.testing.mockmatchers.SaveArgument; + +var MSG = 'MSG'; +var localStrings = { + getStringF: function(msg, number) { + assertEquals('should_open_all', msg); + return MSG + number; + } +}; + +var URL1 = 'http://chromium.org/'; +var URL2 = '#hash'; +var WINDOW_ID = 'WINDOW_ID'; + +function setUp() { + mockControl = new goog.testing.MockControl; + chrome.tabs.create = mockControl.createFunctionMock(); + chrome.windows.create = mockControl.createFunctionMock(); + + propertyReplacer = new goog.testing.PropertyReplacer; + + mockWindow = { + confirm: mockControl.createFunctionMock(), + location: { + get href() { + return 'http://www.google.com/'; + }, + set href(url) { + assertEquals(URL1, url); + } + } + }; +} + +function tearDown() { + mockControl.$tearDown(); + propertyReplacer.reset(); +} + +function testGetWarningMessage() { + var lc = new cr.LinkController(localStrings); + var msg = lc.getWarningMessage(10); + assertEquals(MSG + 10, msg); +} + +function openUrlFromEventHelper(event, isMac, expectedKind) { + var lc = new cr.LinkController(localStrings); + + lc.openUrls = mockControl.createFunctionMock(); + lc.openUrls([URL1], expectedKind); + + propertyReplacer.set(cr, 'isMac', isMac); + + mockControl.$replayAll(); + + lc.openUrlFromEvent(URL1, event); + + mockControl.$verifyAll(); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventForegroundTab() { + var e = { + type: 'click', + button: 0, + shiftKey: true, + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabMac() { + var e = { + type: 'click', + button: 0, + shiftKey: true, + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + shiftKey: true, + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabEnterMac() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + shiftKey: true, + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.FOREGROUND_TAB); +} + +function testOpenUrlFromEventForegroundTabMiddleClick() { + var e = { + type: 'click', + button: 1, + shiftKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.FOREGROUND_TAB); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventBackgroundTab() { + var e = { + type: 'click', + button: 0, + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabMac() { + var e = { + type: 'click', + button: 0, + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + ctrlKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabEnterMac() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + metaKey: true + }; + openUrlFromEventHelper(e, true, cr.LinkKind.BACKGROUND_TAB); +} + +function testOpenUrlFromEventBackgroundTabMiddleClick() { + var e = { + type: 'click', + button: 1 + }; + openUrlFromEventHelper(e, false, cr.LinkKind.BACKGROUND_TAB); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventWindow() { + var e = { + type: 'click', + button: 0, + shiftKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.WINDOW); +} + +function testOpenUrlFromEventWindowEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter', + shiftKey: true + }; + openUrlFromEventHelper(e, false, cr.LinkKind.WINDOW); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrlFromEventSelf() { + var e = { + type: 'click', + button: 0 + }; + openUrlFromEventHelper(e, false, cr.LinkKind.SELF); +} + +function testOpenUrlFromEventSelfEnter() { + var e = { + type: 'keydown', + keyIdentifier: 'Enter' + }; + openUrlFromEventHelper(e, false, cr.LinkKind.SELF); +} + +/////////////////////////////////////////////////////////////////////////////// + +function testOpenUrl() { + var lc = new cr.LinkController(localStrings); + lc.openUrls = mockControl.createFunctionMock(); + + lc.openUrls(new ObjectEqualsMatcher([URL1]), cr.LinkKind.SELF); + mockControl.$replayAll(); + + lc.openUrl(URL1, cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + +/////////////////////////////// OpenUrls ////////////////////////////////////// + +function testOpenUrlsTooFew() { + var lc = new cr.LinkController(localStrings); + + mockControl.$replayAll(); + + lc.openUrls([], cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + + +function testOpenUrlsTooMany() { + var lc = new cr.LinkController(localStrings); + lc.warningLimit = 9; + + var urls = new Array(lc.warningLimit + 1); + lc.window = mockWindow; + + lc.window.confirm('MSG10').$returns(false); + mockControl.$replayAll(); + + lc.openUrls(urls, cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + +function testOpenUrlsSelf() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + mockControl.$replayAll(); + + lc.openUrls([URL1], cr.LinkKind.SELF); + + mockControl.$verifyAll(); +} + +function testOpenUrlsForegroundTab() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + chrome.tabs.create(new ObjectEqualsMatcher({url: URL1, selected: true})); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.FOREGROUND_TAB); + + mockControl.$verifyAll(); +} + +function testOpenUrlsBackgroundTab() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + chrome.tabs.create(new ObjectEqualsMatcher({url: URL1, selected: false})); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.BACKGROUND_TAB); + + mockControl.$verifyAll(); +} + +function testOpenUrlsWindow() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + var callbackMatcher = new SaveArgumentMatcher; + chrome.windows.create(new ObjectEqualsMatcher({url: URL1, incognito: false}), + callbackMatcher); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + windowId: WINDOW_ID, + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.WINDOW); + callbackMatcher.arg({id: WINDOW_ID}); + + mockControl.$verifyAll(); +} + +function testOpenUrlsIncognito() { + var lc = new cr.LinkController(localStrings); + lc.window = mockWindow; + + var callbackMatcher = new SaveArgumentMatcher; + chrome.windows.create(new ObjectEqualsMatcher({url: URL1, incognito: true}), + callbackMatcher); + chrome.tabs.create(new ObjectEqualsMatcher({ + url: 'http://www.google.com/#hash', + windowId: WINDOW_ID, + selected: false + })); + + mockControl.$replayAll(); + + lc.openUrls([URL1, URL2], cr.LinkKind.INCOGNITO); + callbackMatcher.arg({id: WINDOW_ID}); + + mockControl.$verifyAll(); +} + +</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 index 3e8944f..a9d233a 100644 --- a/chrome/browser/resources/bookmark_manager/js/cr/promise.js +++ b/chrome/browser/resources/bookmark_manager/js/cr/promise.js @@ -15,16 +15,20 @@ cr.define('cr', function() { /** * Creates a future promise. - * @param {Function=} opt_callback Callback. + * @param {*=} opt_value The value to set the promise to. If set completes + * the promise immediately. * @constructor */ - function Promise(opt_callback) { + function Promise(opt_value) { /** * An array of the callbacks. * @type {!Array.<!Function>} * @private */ - this.callbacks_ = opt_callback ? [opt_callback] : []; + this.callbacks_ = []; + + if (arguments.length > 0) + this.value = opt_value; } Promise.prototype = { @@ -133,10 +137,11 @@ cr.define('cr', function() { /** * Creates a new future promise that is fulfilled when any of the promises are - * fulfilled. + * fulfilled. The value of the returned promise will be the value of the first + * fulfilled promise. * @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 + * @return {!Promise} The new promise that will be fulfilled when any of the * passed in promises are fulfilled. */ Promise.any = function(var_args) { @@ -151,6 +156,41 @@ cr.define('cr', function() { }; /** + * Creates a new future promise that is fulfilled when all of the promises are + * fulfilled. The value of the returned promise is an array of the values of + * the promises passed in. + * @param {...!Promise} var_args The promises used to build up the new + * promise. + * @return {!Promise} The promise that wraps all the promises in the array. + */ + Promise.all = function(var_args) { + var p = new Promise; + var args = Array.prototype.slice.call(arguments); + var count = args.length; + if (!count) { + p.value = []; + return p; + } + + function f(v) { + count--; + if (!count) { + p.value = args.map(function(argP) { + return argP.value; + }); + } + } + + // Do not use count here since count may be decremented in the call to + // addListener if the promise is already done. + for (var i = 0; i < args.length; i++) { + args[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. diff --git a/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html b/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html index 19e3d1a..5c9ae1d 100644 --- a/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html +++ b/chrome/browser/resources/bookmark_manager/js/cr/promise_test.html @@ -107,9 +107,9 @@ function testCallbacks4() { calls2++; assertEquals(V, v); } - var p = new Promise(f1); + var p = new Promise(V); + p.addListener(f1); p.addListener(f2); - p.value = V; assertEquals(1, calls1); assertEquals(1, calls2); } @@ -195,6 +195,32 @@ function testAny() { assertEquals(2, any.value); } +function testAll() { + var p1 = new Promise; + var p2 = new Promise; + var p3 = new Promise; + + var pAll = Promise.all(p1, p2, p3); + p1.value = 1; + p2.value = 2; + p3.value = 3; + assertArrayEquals([1, 2, 3], pAll.value); +} + +function testAllEmpty() { + var pAll = Promise.all(); + assertArrayEquals([], pAll.value); +} + +function testAllAlreadyDone() { + var p1 = new Promise(1); + var p2 = new Promise(2); + var p3 = new Promise(3); + + var pAll = Promise.all(p1, p2, p3); + assertArrayEquals([1, 2, 3], pAll.value); +} + function testEvent() { var p = Promise.event(document.body, 'foo'); var e = new cr.Event('foo'); |