From 4cc5e954e5a7b0c60f88b5461fd953d377ffe5f9 Mon Sep 17 00:00:00 2001 From: "arv@chromium.org" Date: Wed, 14 Apr 2010 22:32:23 +0000 Subject: 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 --- .../bookmark_manager/js/bmm/bookmarklist.js | 32 +- .../bookmark_manager/js/cr/linkcontroller.js | 168 ++++++++++ .../js/cr/linkcontroller_test.html | 353 +++++++++++++++++++++ .../resources/bookmark_manager/js/cr/promise.js | 50 ++- .../bookmark_manager/js/cr/promise_test.html | 30 +- 5 files changed, 600 insertions(+), 33 deletions(-) create mode 100644 chrome/browser/resources/bookmark_manager/js/cr/linkcontroller.js create mode 100644 chrome/browser/resources/bookmark_manager/js/cr/linkcontroller_test.html (limited to 'chrome/browser/resources/bookmark_manager/js') 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.} 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 @@ + + + + + + + + + + + + + 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.} * @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'); -- cgit v1.1