Merge fx-team to m-c a=merge CLOSED TREE

This commit is contained in:
Wes Kocher 2015-03-17 18:58:05 -07:00
commit ed7713496a
18 changed files with 540 additions and 274 deletions

View File

@ -10,15 +10,34 @@ XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
const READINGLIST_COMMAND_ID = "readingListSidebar";
let ReadingListUI = {
/**
* Frame-script messages we want to listen to.
* @type {[string]}
*/
MESSAGES: [
"ReadingList:GetVisibility",
"ReadingList:ToggleVisibility",
],
/**
* Add-to-ReadingList toolbar button in the URLbar.
* @type {Element}
*/
toolbarButton: null,
/**
* Whether this object is currently registered as a listener with ReadingList.
* Used to avoid inadvertantly loading the ReadLingList.jsm module on startup.
* @type {Boolean}
*/
listenerRegistered: false,
/**
* Initialize the ReadingList UI.
*/
init() {
this.toolbarButton = document.getElementById("readinglist-addremove-button");
Preferences.observe("browser.readinglist.enabled", this.updateUI, this);
const mm = window.messageManager;
@ -63,7 +82,18 @@ let ReadingListUI = {
*/
updateUI() {
let enabled = this.enabled;
if (!enabled) {
if (enabled) {
// This is a no-op if we're already registered.
ReadingList.addListener(this);
this.listenerRegistered = true;
} else {
if (this.listenerRegistered) {
// This is safe to call if we're not currently registered, but we don't
// want to forcibly load the normally lazy-loaded module on startup.
ReadingList.removeListener(this);
this.listenerRegistered = true;
}
this.hideSidebar();
}
@ -89,6 +119,11 @@ let ReadingListUI = {
}
},
/**
* Re-refresh the ReadingList bookmarks submenu when it opens.
*
* @param {Element} target - Menu element opening.
*/
onReadingListPopupShowing: Task.async(function* (target) {
if (target.id == "BMB_readingListPopup") {
// Setting this class in the .xul file messes with the way
@ -184,4 +219,96 @@ let ReadingListUI = {
}
}
},
/**
* Handles toolbar button styling based on page proxy state changes.
*
* @see SetPageProxyState()
*
* @param {string} state - New state. Either "valid" or "invalid".
*/
onPageProxyStateChanged: Task.async(function* (state) {
if (!this.toolbarButton) {
// nothing to do if we have no button.
return;
}
if (!this.enabled || state == "invalid") {
this.toolbarButton.setAttribute("hidden", true);
return;
}
let isInList = yield ReadingList.containsURL(gBrowser.currentURI);
this.setToolbarButtonState(isInList);
}),
/**
* Set the state of the ReadingList toolbar button in the urlbar.
* If the current tab's page is in the ReadingList (active), sets the button
* to allow removing the page. Otherwise, sets the button to allow adding the
* page (not active).
*
* @param {boolean} active - True if the button should be active (page is
* already in the list).
*/
setToolbarButtonState(active) {
this.toolbarButton.setAttribute("already-added", active);
let type = (active ? "remove" : "add");
let tooltip = gNavigatorBundle.getString(`readingList.urlbar.${type}`);
this.toolbarButton.setAttribute("tooltiptext", tooltip);
this.toolbarButton.removeAttribute("hidden");
},
/**
* Toggle a page (from a browser) in the ReadingList, adding if it's not already added, or
* removing otherwise.
*
* @param {<xul:browser>} browser - Browser with page to toggle.
* @returns {Promise} Promise resolved when operation has completed.
*/
togglePageByBrowser: Task.async(function* (browser) {
let item = yield ReadingList.getItemForURL(browser.currentURI);
if (item) {
yield item.delete();
} else {
yield ReadingList.addItemFromBrowser(browser);
}
}),
/**
* Checks if a given item matches the current tab in this window.
*
* @param {ReadingListItem} item - Item to check
* @returns True if match, false otherwise.
*/
isItemForCurrentBrowser(item) {
let currentURL = gBrowser.currentURI.spec;
if (item.url == currentURL || item.resolvedURL == currentURL) {
return true;
}
return false;
},
/**
* ReadingList event handler for when an item is added.
*
* @param {ReadingListItem} item - Item added.
*/
onItemAdded(item) {
if (this.isItemForCurrentBrowser(item)) {
this.setToolbarButtonState(true);
}
},
/**
* ReadingList event handler for when an item is deleted.
*
* @param {ReadingListItem} item - Item deleted.
*/
onItemDeleted(item) {
if (this.isItemForCurrentBrowser(item)) {
this.setToolbarButtonState(false);
}
},
};

View File

@ -1022,6 +1022,7 @@ var gBrowserInit = {
CombinedStopReload.init();
gPrivateBrowsingUI.init();
TabsInTitlebar.init();
ReadingListUI.init();
#ifdef XP_WIN
if (window.matchMedia("(-moz-os-version: windows-win8)").matches &&
@ -1379,7 +1380,6 @@ var gBrowserInit = {
SocialUI.init();
TabView.init();
ReadingListUI.init();
// Telemetry for master-password - we do this after 5 seconds as it
// can cause IO if NSS/PSM has not already initialized.
@ -2421,6 +2421,7 @@ function UpdatePageProxyState()
function SetPageProxyState(aState)
{
BookmarkingUI.onPageProxyStateChanged(aState);
ReadingListUI.onPageProxyStateChanged(aState);
if (!gURLBar)
return;

View File

@ -827,6 +827,10 @@
hidden="true"
tooltiptext="&pageReportIcon.tooltip;"
onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>
<toolbarbutton id="readinglist-addremove-button"
class="tabbable urlbar-icon"
hidden="true"
oncommand="ReadingListUI.togglePageByBrowser(gBrowser.selectedBrowser);"/>
<toolbarbutton id="reader-mode-button"
class="tabbable"
hidden="true"

View File

@ -1017,12 +1017,12 @@ addEventListener("pageshow", function(event) {
});
let PageMetadataMessenger = {
init: function() {
init() {
addMessageListener("PageMetadata:GetPageData", this);
addMessageListener("PageMetadata:GetMicrodata", this);
},
receiveMessage: function(aMessage) {
switch(aMessage.name) {
receiveMessage(message) {
switch(message.name) {
case "PageMetadata:GetPageData": {
let result = PageMetadata.getData(content.document);
sendAsyncMessage("PageMetadata:PageDataResult", result);
@ -1030,7 +1030,7 @@ let PageMetadataMessenger = {
}
case "PageMetadata:GetMicrodata": {
let target = aMessage.objects;
let target = message.objects;
let result = PageMetadata.getMicrodata(content.document, target);
sendAsyncMessage("PageMetadata:MicrodataResult", result);
break;

View File

@ -1,12 +1,43 @@
function test()
{
var embed = '<embed type="application/x-test" allowscriptaccess="always" allowfullscreen="true" wmode="window" width="640" height="480"></embed>'
function swapTabsAndCloseOther(a, b) {
gBrowser.swapBrowsersAndCloseOther(gBrowser.tabs[b], gBrowser.tabs[a]);
}
waitForExplicitFinish();
let getClicks = function(tab) {
return ContentTask.spawn(tab.linkedBrowser, {}, function() {
return content.wrappedJSObject.clicks;
});
}
let clickTest = Task.async(function*(tab) {
let clicks = yield getClicks(tab);
yield ContentTask.spawn(tab.linkedBrowser, {}, function() {
let target = content.document.body;
let rect = target.getBoundingClientRect();
let left = (rect.left + rect.right) / 2;
let top = (rect.top + rect.bottom) / 2;
let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
});
let newClicks = yield getClicks(tab);
is(newClicks, clicks + 1, "adding 1 more click on BODY");
});
function loadURI(tab, url) {
tab.linkedBrowser.loadURI(url);
return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
}
add_task(function*() {
let embed = '<embed type="application/x-test" allowscriptaccess="always" allowfullscreen="true" wmode="window" width="640" height="480"></embed>'
setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
// create a few tabs
var tabs = [
let tabs = [
gBrowser.tabs[0],
gBrowser.addTab("about:blank", {skipAnimation: true}),
gBrowser.addTab("about:blank", {skipAnimation: true}),
@ -14,118 +45,73 @@ function test()
gBrowser.addTab("about:blank", {skipAnimation: true})
];
function setLocation(i, url) {
tabs[i].linkedBrowser.contentWindow.location = url;
}
function moveTabTo(a, b) {
gBrowser.swapBrowsersAndCloseOther(gBrowser.tabs[b], gBrowser.tabs[a]);
}
function clickTest(tab, doc, win) {
var clicks = doc.defaultView.clicks;
yield ContentTask.spawn(tab.linkedBrowser, {}, function() {
let target = content.document.body;
let rect = target.getBoundingClientRect();
let left = (rect.left + rect.right) / 2;
let top = (rect.top + rect.bottom) / 2;
let utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
});
is(doc.defaultView.clicks, clicks+1, "adding 1 more click on BODY");
}
function test1() {
moveTabTo(2, 3); // now: 0 1 2 4
is(gBrowser.tabs[1], tabs[1], "tab1");
is(gBrowser.tabs[2], tabs[3], "tab3");
var plugin = tabs[4].linkedBrowser.contentDocument.wrappedJSObject.body.firstChild;
var tab4_plugin_object = plugin.getObjectValue();
gBrowser.selectedTab = gBrowser.tabs[2];
moveTabTo(3, 2); // now: 0 1 4
gBrowser.selectedTab = tabs[4];
var doc = gBrowser.tabs[2].linkedBrowser.contentDocument.wrappedJSObject;
plugin = doc.body.firstChild;
ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance");
is(gBrowser.tabs[1], tabs[1], "tab1");
is(gBrowser.tabs[2], tabs[3], "tab4");
is(doc.defaultView.clicks, 0, "no click on BODY so far");
clickTest(gBrowser.tabs[2], doc, window);
moveTabTo(2, 1); // now: 0 4
is(gBrowser.tabs[1], tabs[1], "tab1");
doc = gBrowser.tabs[1].linkedBrowser.contentDocument.wrappedJSObject;
plugin = doc.body.firstChild;
ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance");
clickTest(gBrowser.tabs[1], doc, window);
// Load a new document (about:blank) in tab4, then detach that tab into a new window.
// In the new window, navigate back to the original document and click on its <body>,
// verify that its onclick was called.
var t = tabs[1];
var b = t.linkedBrowser;
gBrowser.selectedTab = t;
b.addEventListener("load", function() {
b.removeEventListener("load", arguments.callee, true);
executeSoon(function () {
var win = gBrowser.replaceTabWithWindow(t);
whenDelayedStartupFinished(win, function () {
// Verify that the original window now only has the initial tab left in it.
is(gBrowser.tabs[0], tabs[0], "tab0");
is(gBrowser.tabs[0].linkedBrowser.contentWindow.location, "about:blank", "tab0 uri");
executeSoon(function () {
win.gBrowser.addEventListener("pageshow", function () {
win.gBrowser.removeEventListener("pageshow", arguments.callee, false);
executeSoon(function () {
t = win.gBrowser.tabs[0];
b = t.linkedBrowser;
var doc = b.contentDocument.wrappedJSObject;
clickTest(t, doc, win);
win.close();
finish();
});
}, false);
win.gBrowser.goBack();
});
});
});
}, true);
b.loadURI("about:blank");
}
var loads = 0;
function waitForLoad(event, tab, listenerContainer) {
var b = tabs[tab].linkedBrowser;
if (b.contentDocument != event.target) {
return;
}
gBrowser.tabs[tab].linkedBrowser.removeEventListener("load", listenerContainer.listener, true);
++loads;
if (loads == tabs.length - 1) {
executeSoon(test1);
}
}
function fn(f, arg) {
var listenerContainer = { listener: null }
listenerContainer.listener = function (event) { return f(event, arg, listenerContainer); };
return listenerContainer.listener;
}
for (var i = 1; i < tabs.length; ++i) {
tabs[i].linkedBrowser.addEventListener("load", fn(waitForLoad,i), true);
}
setLocation(1, "data:text/html;charset=utf-8,<title>tab1</title><body>tab1<iframe>");
setLocation(2, "data:text/plain;charset=utf-8,tab2");
setLocation(3, "data:text/html;charset=utf-8,<title>tab3</title><body>tab3<iframe>");
setLocation(4, "data:text/html;charset=utf-8,<body onload='clicks=0' onclick='++clicks'>"+embed);
// Initially 0 1 2 3 4
yield loadURI(tabs[1], "data:text/html;charset=utf-8,<title>tab1</title><body>tab1<iframe>");
yield loadURI(tabs[2], "data:text/plain;charset=utf-8,tab2");
yield loadURI(tabs[3], "data:text/html;charset=utf-8,<title>tab3</title><body>tab3<iframe>");
yield loadURI(tabs[4], "data:text/html;charset=utf-8,<body onload='clicks=0' onclick='++clicks'>"+embed);
gBrowser.selectedTab = tabs[3];
}
swapTabsAndCloseOther(2, 3); // now: 0 1 2 4
is(gBrowser.tabs[1], tabs[1], "tab1");
is(gBrowser.tabs[2], tabs[3], "tab3");
is(gBrowser.tabs[3], tabs[4], "tab4");
let plugin = tabs[4].linkedBrowser.contentDocument.wrappedJSObject.body.firstChild;
let tab4_plugin_object = plugin.getObjectValue();
swapTabsAndCloseOther(3, 2); // now: 0 1 4
gBrowser.selectedTab = gBrowser.tabs[2];
let doc = gBrowser.tabs[2].linkedBrowser.contentDocument.wrappedJSObject;
plugin = doc.body.firstChild;
ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance");
is(gBrowser.tabs[1], tabs[1], "tab1");
is(gBrowser.tabs[2], tabs[3], "tab4");
let clicks = yield getClicks(gBrowser.tabs[2]);
is(clicks, 0, "no click on BODY so far");
yield clickTest(gBrowser.tabs[2]);
swapTabsAndCloseOther(2, 1); // now: 0 4
is(gBrowser.tabs[1], tabs[1], "tab1");
doc = gBrowser.tabs[1].linkedBrowser.contentDocument.wrappedJSObject;
plugin = doc.body.firstChild;
ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance");
yield clickTest(gBrowser.tabs[1]);
// Load a new document (about:blank) in tab4, then detach that tab into a new window.
// In the new window, navigate back to the original document and click on its <body>,
// verify that its onclick was called.
gBrowser.selectedTab = tabs[1];
yield loadURI(tabs[1], "about:blank");
let key = tabs[1].linkedBrowser.permanentKey;
let win = gBrowser.replaceTabWithWindow(tabs[1]);
yield new Promise(resolve => whenDelayedStartupFinished(win, resolve));
// Verify that the original window now only has the initial tab left in it.
is(gBrowser.tabs[0], tabs[0], "tab0");
is(gBrowser.tabs[0].linkedBrowser.currentURI.spec, "about:blank", "tab0 uri");
let tab = win.gBrowser.tabs[0];
is(tab.linkedBrowser.permanentKey, key, "Should have kept the key");
let pageshowPromise = ContentTask.spawn(tab.linkedBrowser, {}, function*() {
return new Promise(resolve => {
let listener = function() {
removeEventListener("pageshow", listener, false);
resolve();
}
addEventListener("pageshow", listener, false);
});
});
win.gBrowser.goBack();
yield pageshowPromise;
yield clickTest(tab);
promiseWindowClosed(win);
});

View File

@ -55,8 +55,7 @@ const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directory.source";
* This test ensures that there are no unexpected
* uninterruptible reflows when opening new tabs.
*/
function test() {
waitForExplicitFinish();
add_task(function*() {
let DirectoryLinksProvider = Cu.import("resource:///modules/DirectoryLinksProvider.jsm", {}).DirectoryLinksProvider;
let NewTabUtils = Cu.import("resource://gre/modules/NewTabUtils.jsm", {}).NewTabUtils;
let Promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
@ -85,25 +84,36 @@ function test() {
return watchLinksChangeOnce();
});
// run tests when directory source change completes
watchLinksChangeOnce().then(() => {
// Add a reflow observer and open a new tab.
docShell.addWeakReflowObserver(observer);
BrowserOpenTab();
// Wait until the tabopen animation has finished.
waitForTransitionEnd(function () {
// Remove reflow observer and clean up.
docShell.removeWeakReflowObserver(observer);
gBrowser.removeCurrentTab();
finish();
});
});
Services.prefs.setBoolPref(PREF_PRELOAD, false);
// set directory source to dummy/empty links
Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, 'data:application/json,{"test":1}');
}
// run tests when directory source change completes
yield watchLinksChangeOnce();
// Perform a click in the top left of content to ensure the mouse isn't
// hovering over any of the tiles
let target = gBrowser.selectedBrowser;
let rect = target.getBoundingClientRect();
let left = rect.left + 1;
let top = rect.top + 1;
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
// Add a reflow observer and open a new tab.
docShell.addWeakReflowObserver(observer);
BrowserOpenTab();
// Wait until the tabopen animation has finished.
yield waitForTransitionEnd();
// Remove reflow observer and clean up.
docShell.removeWeakReflowObserver(observer);
gBrowser.removeCurrentTab();
});
let observer = {
reflow: function (start, end) {
@ -137,12 +147,14 @@ let observer = {
Ci.nsISupportsWeakReference])
};
function waitForTransitionEnd(callback) {
let tab = gBrowser.selectedTab;
tab.addEventListener("transitionend", function onEnd(event) {
if (event.propertyName === "max-width") {
tab.removeEventListener("transitionend", onEnd);
executeSoon(callback);
}
function waitForTransitionEnd() {
return new Promise(resolve => {
let tab = gBrowser.selectedTab;
tab.addEventListener("transitionend", function onEnd(event) {
if (event.propertyName === "max-width") {
tab.removeEventListener("transitionend", onEnd);
resolve();
}
});
});
}

View File

@ -129,6 +129,38 @@ ReadingListImpl.prototype = {
return (yield this._store.count(...optsList));
}),
/**
* Checks whether a given URL is in the ReadingList already.
*
* @param {String/nsIURI} url - URL to check.
* @returns {Promise} Promise that is fulfilled with a boolean indicating
* whether the URL is in the list or not.
*/
containsURL: Task.async(function* (url) {
url = normalizeURI(url).spec;
// This is used on every tab switch and page load of the current tab, so we
// want it to be quick and avoid a DB query whenever possible.
// First check if any cached items have a direct match.
if (this._itemsByURL.has(url)) {
return true;
}
// Then check if any cached items may have a different resolved URL
// that matches.
for (let itemWeakRef of this._itemsByURL.values()) {
let item = itemWeakRef.get();
if (item && item.resolvedURL == url) {
return true;
}
}
// Finally, fall back to the DB.
let count = yield this.count({url: url}, {resolvedURL: url});
return (count > 0);
}),
/**
* Enumerates the items in the list that match the given options.
*
@ -190,6 +222,7 @@ ReadingListImpl.prototype = {
*/
addItem: Task.async(function* (obj) {
obj = stripNonItemProperties(obj);
normalizeReadingListProperties(obj);
yield this._store.addItem(obj);
this._invalidateIterators();
let item = this._itemFromObject(obj);
@ -248,11 +281,38 @@ ReadingListImpl.prototype = {
* @param {String/nsIURI} uri - URI to match against. This will be normalized.
*/
getItemForURL: Task.async(function* (uri) {
let url = this._normalizeURI(uri).spec;
let url = normalizeURI(uri).spec;
let [item] = yield this.iterator({url: url}, {resolvedURL: url}).items(1);
return item;
}),
/**
* Add to the ReadingList the page that is loaded in a given browser.
*
* @param {<xul:browser>} browser - Browser element for the document.
* @return {Promise} Promise that is fullfilled with the added item.
*/
addItemFromBrowser: Task.async(function* (browser) {
let metadata = yield getMetadataFromBrowser(browser);
let itemData = {
url: browser.currentURI,
title: metadata.title,
resolvedURL: metadata.url,
excerpt: metadata.description,
};
if (metadata.description) {
itemData.exerpt = metadata.description;
}
if (metadata.previews.length > 0) {
itemData.image = metadata.previews[0];
}
let item = yield ReadingList.addItem(itemData);
return item;
}),
/**
* Adds a listener that will be notified when the list changes. Listeners
* are objects with the following optional methods:
@ -304,22 +364,6 @@ ReadingListImpl.prototype = {
// A Set containing listener objects.
_listeners: null,
/**
* Normalize a URI, stripping away extraneous parts we don't want to store
* or compare against.
*
* @param {nsIURI/String} uri - URI to normalize.
* @returns {nsIURI} Cloned and normalized version of the input URI.
*/
_normalizeURI(uri) {
if (typeof uri == "string") {
uri = Services.io.newURI(uri, "", null);
}
uri = uri.cloneIgnoringRef();
uri.userPass = "";
return uri;
},
/**
* Returns the ReadingListItem represented by the given simple object. If
* the item doesn't exist yet, it's created first.
@ -375,12 +419,26 @@ ReadingListImpl.prototype = {
},
_ensureItemBelongsToList(item) {
if (item.list != this) {
throw new Error("The item does not belong to this list");
if (!item || !item._ensureBelongsToList) {
throw new Error("The item is not a ReadingListItem");
}
item._ensureBelongsToList();
},
};
/*
* normalize the properties of a "regular" object that reflects a ReadingListItem
*/
function normalizeReadingListProperties(obj) {
if (obj.url) {
obj.url = normalizeURI(obj.url).spec;
}
if (obj.resolvedURL) {
obj.resolvedURL = normalizeURI(obj.resolvedURL).spec;
}
}
let _unserializable = () => {}; // See comments in the ReadingListItem ctor.
/**
@ -431,9 +489,6 @@ ReadingListItem.prototype = {
},
set guid(val) {
this._properties.guid = val;
if (this.list) {
this.commit();
}
},
/**
@ -447,9 +502,6 @@ ReadingListItem.prototype = {
},
set lastModified(val) {
this._properties.lastModified = val.valueOf();
if (this.list) {
this.commit();
}
},
/**
@ -460,10 +512,7 @@ ReadingListItem.prototype = {
return this._properties.url;
},
set url(val) {
this._properties.url = val;
if (this.list) {
this.commit();
}
this._properties.url = normalizeURI(val).spec;
},
/**
@ -476,10 +525,7 @@ ReadingListItem.prototype = {
undefined;
},
set uri(val) {
this.url = val.spec;
if (this.list) {
this.commit();
}
this.url = normalizeURI(val).spec;
},
/**
@ -502,10 +548,7 @@ ReadingListItem.prototype = {
return this._properties.resolvedURL;
},
set resolvedURL(val) {
this._properties.resolvedURL = val;
if (this.list) {
this.commit();
}
this._properties.resolvedURL = normalizeURI(val).spec;
},
/**
@ -519,9 +562,6 @@ ReadingListItem.prototype = {
},
set resolvedURI(val) {
this.resolvedURL = val.spec;
if (this.list) {
this.commit();
}
},
/**
@ -533,9 +573,6 @@ ReadingListItem.prototype = {
},
set title(val) {
this._properties.title = val;
if (this.list) {
this.commit();
}
},
/**
@ -547,9 +584,6 @@ ReadingListItem.prototype = {
},
set resolvedTitle(val) {
this._properties.resolvedTitle = val;
if (this.list) {
this.commit();
}
},
/**
@ -561,9 +595,6 @@ ReadingListItem.prototype = {
},
set excerpt(val) {
this._properties.excerpt = val;
if (this.list) {
this.commit();
}
},
/**
@ -575,9 +606,6 @@ ReadingListItem.prototype = {
},
set status(val) {
this._properties.status = val;
if (this.list) {
this.commit();
}
},
/**
@ -589,9 +617,6 @@ ReadingListItem.prototype = {
},
set favorite(val) {
this._properties.favorite = !!val;
if (this.list) {
this.commit();
}
},
/**
@ -603,9 +628,6 @@ ReadingListItem.prototype = {
},
set isArticle(val) {
this._properties.isArticle = !!val;
if (this.list) {
this.commit();
}
},
/**
@ -617,9 +639,6 @@ ReadingListItem.prototype = {
},
set wordCount(val) {
this._properties.wordCount = val;
if (this.list) {
this.commit();
}
},
/**
@ -631,9 +650,6 @@ ReadingListItem.prototype = {
},
set unread(val) {
this._properties.unread = !!val;
if (this.list) {
this.commit();
}
},
/**
@ -647,9 +663,6 @@ ReadingListItem.prototype = {
},
set addedOn(val) {
this._properties.addedOn = val.valueOf();
if (this.list) {
this.commit();
}
},
/**
@ -663,9 +676,6 @@ ReadingListItem.prototype = {
},
set storedOn(val) {
this._properties.storedOn = val.valueOf();
if (this.list) {
this.commit();
}
},
/**
@ -677,9 +687,6 @@ ReadingListItem.prototype = {
},
set markedReadBy(val) {
this._properties.markedReadBy = val;
if (this.list) {
this.commit();
}
},
/**
@ -693,9 +700,6 @@ ReadingListItem.prototype = {
},
set markedReadOn(val) {
this._properties.markedReadOn = val.valueOf();
if (this.list) {
this.commit();
}
},
/**
@ -707,25 +711,24 @@ ReadingListItem.prototype = {
},
set readPosition(val) {
this._properties.readPosition = val;
if (this.list) {
this.commit();
}
},
/**
* Sets the given properties of the item, optionally calling commit().
* Sets the given properties of the item, optionally calling list.updateItem().
*
* @param props A simple object containing the properties to set.
* @param commit If true, commit() is called.
* @return Promise<null> If commit is true, resolved when the commit
* @param update If true, updateItem() is called for this item.
* @return Promise<null> If update is true, resolved when the update
* completes; otherwise resolved immediately.
*/
setProperties: Task.async(function* (props, commit=true) {
setProperties: Task.async(function* (props, update=true) {
for (let name in props) {
this._properties[name] = props[name];
}
if (commit) {
yield this.commit();
// make sure everything is normalized.
normalizeReadingListProperties(this._properties);
if (update) {
yield this.list.updateItem(this);
}
}),
@ -740,17 +743,6 @@ ReadingListItem.prototype = {
this.delete = () => Promise.reject("The item has already been deleted");
}),
/**
* Notifies the item's list that the item has changed so that the list can
* update itself.
*
* @return Promise<null> Resolved when the list has been updated.
*/
commit: Task.async(function* () {
this._ensureBelongsToList();
yield this.list.updateItem(this);
}),
toJSON() {
return this._properties;
},
@ -855,6 +847,23 @@ ReadingListItemIterator.prototype = {
},
};
/**
* Normalize a URI, stripping away extraneous parts we don't want to store
* or compare against.
*
* @param {nsIURI/String} uri - URI to normalize.
* @returns {nsIURI} Cloned and normalized version of the input URI.
*/
function normalizeURI(uri) {
if (typeof uri == "string") {
uri = Services.io.newURI(uri, "", null);
}
uri = uri.cloneIgnoringRef();
try {
uri.userPass = "";
} catch (ex) {} // Not all nsURI impls (eg, nsSimpleURI) support .userPass
return uri;
};
function stripNonItemProperties(item) {
let obj = {};
@ -885,6 +894,24 @@ function clone(obj) {
return Cu.cloneInto(obj, {}, { cloneFunctions: false });
}
/**
* Get page metadata from the content document in a given <xul:browser>.
* @see PageMetadata.jsm
*
* @param {<xul:browser>} browser - Browser element for the document.
* @returns {Promise} Promise that is fulfilled with an object describing the metadata.
*/
function getMetadataFromBrowser(browser) {
let mm = browser.messageManager;
return new Promise(resolve => {
function handleResult(msg) {
mm.removeMessageListener("PageMetadata:PageDataResult", handleResult);
resolve(msg.json);
}
mm.addMessageListener("PageMetadata:PageDataResult", handleResult);
mm.sendAsyncMessage("PageMetadata:GetPageData");
});
}
Object.defineProperty(this, "ReadingList", {
get() {

View File

@ -122,10 +122,11 @@ add_task(function* constraints() {
checkError(err);
// update an item with an existing url
item.guid = gItems[1].guid;
let rlitem = yield gList.getItemForURL(gItems[0].url);
rlitem.guid = gItems[1].guid;
err = null;
try {
yield gList.updateItem(item);
yield gList.updateItem(rlitem);
}
catch (e) {
err = e;
@ -145,10 +146,11 @@ add_task(function* constraints() {
checkError(err);
// update an item with an existing resolvedURL
item.url = gItems[1].url;
rlitem = yield gList.getItemForURL(gItems[0].url);
rlitem.url = gItems[1].url;
err = null;
try {
yield gList.updateItem(item);
yield gList.updateItem(rlitem);
}
catch (e) {
err = e;
@ -159,35 +161,33 @@ add_task(function* constraints() {
item = kindOfClone(gItems[0]);
delete item.guid;
err = null;
let rlitem1;
try {
yield gList.addItem(item);
rlitem1 = yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(!err, err ? err.message : undefined);
let item1 = item;
// add a second item with no guid, which is allowed
item = kindOfClone(gItems[1]);
delete item.guid;
err = null;
let rlitem2;
try {
yield gList.addItem(item);
rlitem2 = yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(!err, err ? err.message : undefined);
let item2 = item;
// Delete both items since other tests assume the store contains only gItems.
item1.list = gList;
item2.list = gList;
yield gList.deleteItem(item1);
yield gList.deleteItem(item2);
yield gList.deleteItem(rlitem1);
yield gList.deleteItem(rlitem2);
let items = [];
yield gList.forEachItem(i => items.push(i), { url: [item1.url, item2.url] });
yield gList.forEachItem(i => items.push(i), { url: [rlitem1.url, rlitem2.url] });
Assert.equal(items.length, 0);
// add a new item with no url
@ -513,15 +513,12 @@ add_task(function* updateItem() {
guid: gItems[0].guid,
});
Assert.equal(items.length, 1);
let item = {
_properties: items[0]._properties,
list: items[0].list,
};
let item = items[0];
// update its title
let newTitle = "updateItem new title";
Assert.notEqual(item.title, newTitle);
item._properties.title = newTitle;
item.title = newTitle;
yield gList.updateItem(item);
// get the item again
@ -542,7 +539,7 @@ add_task(function* item_setProperties() {
let item = (yield iter.items(1))[0];
Assert.ok(item);
// item.setProperties(commit=false). After fetching the item again, its title
// item.setProperties(update=false). After fetching the item again, its title
// should be the old title.
let oldTitle = item.title;
let newTitle = "item_setProperties title 1";
@ -556,7 +553,7 @@ add_task(function* item_setProperties() {
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, oldTitle);
// item.setProperties(commit=true). After fetching the item again, its title
// item.setProperties(update=true). After fetching the item again, its title
// should be the new title.
newTitle = "item_setProperties title 2";
item.setProperties({ title: newTitle }, true);
@ -572,6 +569,7 @@ add_task(function* item_setProperties() {
// be the new title.
newTitle = "item_setProperties title 3";
item.title = newTitle;
gList.updateItem(item);
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
@ -582,6 +580,7 @@ add_task(function* item_setProperties() {
});
add_task(function* listeners() {
Assert.equal((yield gList.count()), gItems.length);
// add an item
let resolve;
let listenerPromise = new Promise(r => resolve = r);
@ -594,6 +593,7 @@ add_task(function* listeners() {
Assert.ok(items[0]);
Assert.ok(items[0] === items[1]);
gList.removeListener(listener);
Assert.equal((yield gList.count()), gItems.length + 1);
// update an item
listenerPromise = new Promise(r => resolve = r);
@ -602,10 +602,12 @@ add_task(function* listeners() {
};
gList.addListener(listener);
items[0].title = "listeners new title";
gList.updateItem(items[0]);
let listenerItem = yield listenerPromise;
Assert.ok(listenerItem);
Assert.ok(listenerItem === items[0]);
gList.removeListener(listener);
Assert.equal((yield gList.count()), gItems.length + 1);
// delete an item
listenerPromise = new Promise(r => resolve = r);
@ -618,6 +620,7 @@ add_task(function* listeners() {
Assert.ok(listenerItem);
Assert.ok(listenerItem === items[0]);
gList.removeListener(listener);
Assert.equal((yield gList.count()), gItems.length);
});
// This test deletes items so it should probably run last.
@ -638,7 +641,7 @@ add_task(function* deleteItem() {
checkItems(items, gItems.slice(1));
// delete second item with list.deleteItem()
yield gList.deleteItem(gItems[1]);
yield gList.deleteItem(items[0]);
gItems[1].list = null;
Assert.equal((yield gList.count()), gItems.length - 2);
items = [];
@ -648,7 +651,7 @@ add_task(function* deleteItem() {
checkItems(items, gItems.slice(2));
// delete third item with list.deleteItem()
yield gList.deleteItem(gItems[2]);
yield gList.deleteItem(items[0]);
gItems[2].list = null;
Assert.equal((yield gList.count()), gItems.length - 3);
items = [];
@ -673,7 +676,7 @@ function checkItems(actualItems, expectedItems) {
function checkError(err) {
Assert.ok(err);
Assert.ok(err instanceof Cu.getGlobalForObject(Sqlite).Error);
Assert.ok(err instanceof Cu.getGlobalForObject(Sqlite).Error, err);
}
function kindOfClone(item) {

View File

@ -60,12 +60,12 @@ let ReaderParent = {
break;
}
case "Reader:ListStatusRequest":
ReadingList.count(message.data).then(count => {
ReadingList.containsURL(message.data.url).then(inList => {
let mm = message.target.messageManager
// Make sure the target browser is still alive before trying to send data back.
if (mm) {
mm.sendAsyncMessage("Reader:ListStatusData",
{ inReadingList: !!count, url: message.data.url });
{ inReadingList: inList, url: message.data.url });
}
});
break;

View File

@ -1629,6 +1629,8 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
list-style-image: url("chrome://browser/skin/Info.png");
}
%include ../shared/readinglist.inc.css
/* Reader mode button */
#reader-mode-button {

View File

@ -91,6 +91,7 @@ browser.jar:
skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/browser/readinglist/icons.svg (../shared/readinglist/icons.svg)
skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/browser/webRTC-shareDevice-16.png

View File

@ -2523,6 +2523,8 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-
}
}
%include ../shared/readinglist.inc.css
/* Reader mode button */
#reader-mode-button {

View File

@ -142,6 +142,7 @@ browser.jar:
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/browser/reader-mode-16@2x.png (../shared/reader/reader-mode-16@2x.png)
skin/classic/browser/readinglist/icons.svg (../shared/readinglist/icons.svg)
skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/browser/webRTC-shareDevice-16.png

View File

@ -0,0 +1,38 @@
/* Reading List button */
#readinglist-addremove-button {
-moz-appearance: none;
border: none;
list-style-image: url("chrome://browser/skin/readinglist/icons.svg#addpage");
padding: 3px;
}
#readinglist-addremove-button:hover {
border: none;
}
#readinglist-addremove-button > .toolbarbutton-icon {
width: 16px;
height: 16px
}
#readinglist-addremove-button:not([already-added="true"]):hover {
list-style-image: url("chrome://browser/skin/readinglist/icons.svg#addpage-hover");
}
#readinglist-addremove-button:not([already-added="true"]):active {
list-style-image: url("chrome://browser/skin/readinglist/icons.svg#addpage-active");
}
#readinglist-addremove-button[already-added="true"] {
list-style-image: url("chrome://browser/skin/readinglist/icons.svg#alreadyadded");
}
#readinglist-addremove-button[already-added="true"]:hover {
list-style-image: url("chrome://browser/skin/readinglist/icons.svg#alreadyadded-hover");
}
#readinglist-addremove-button[already-added="true"]:active {
list-style-image: url("chrome://browser/skin/readinglist/icons.svg#alreadyadded-active");
}

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 16 16"
xml:space="preserve">
<defs>
<style type="text/css">
use:not(:target) {
display: none;
}
#addpage {
fill: #808080;
}
#addpage-hover {
fill: #555555;
}
#addpage-active {
fill: #0095DD;
}
#alreadyadded {
fill: #0095DD;
}
#alreadyadded-hover {
fill: #555555;
}
#alreadyadded-active {
fill: #808080;
}
</style>
<mask id="plus-mask">
<rect width="100%" height="100%" fill="white"/>
<rect x="4" y="7.5" width="8" height="1"/>
<rect x="7.5" y="4" width="1" height="8"/>
</mask>
<g id="addpage-shape">
<circle cx="8" cy="8" r="7" mask="url(#plus-mask)"/>
</g>
</defs>
<use id="addpage" xlink:href="#addpage-shape"/>
<use id="addpage-hover" xlink:href="#addpage-shape"/>
<use id="addpage-active" xlink:href="#addpage-shape"/>
<use id="alreadyadded" xlink:href="#addpage-shape"/>
<use id="alreadyadded-hover" xlink:href="#addpage-shape"/>
<use id="alreadyadded-active" xlink:href="#addpage-shape"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1576,6 +1576,8 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
-moz-image-region: rect(0, 48px, 16px, 32px);
}
%include ../shared/readinglist.inc.css
/* Reader mode button */
#reader-mode-button {

View File

@ -110,6 +110,7 @@ browser.jar:
skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/browser/readinglist/icons.svg (../shared/readinglist/icons.svg)
skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png)
@ -577,6 +578,7 @@ browser.jar:
skin/classic/aero/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
skin/classic/aero/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/aero/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/aero/browser/readinglist/icons.svg (../shared/readinglist/icons.svg)
skin/classic/aero/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
skin/classic/aero/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/aero/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png)

View File

@ -10,7 +10,7 @@ this.EXPORTED_SYMBOLS = [
"ContentTask"
];
const Cu = Components.utils;
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Promise.jsm");
/**
@ -50,13 +50,12 @@ this.ContentTask = {
* @rejects An error message if execution fails.
*/
spawn: function ContentTask_spawn(browser, arg, task) {
if(!gScriptLoadedSet.has(browser)) {
if(!gScriptLoadedSet.has(browser.permanentKey)) {
let mm = browser.messageManager;
mm.addMessageListener("content-task:complete", ContentMessageListener);
mm.loadFrameScript(
"chrome://mochikit/content/tests/BrowserTestUtils/content-task.js", true);
gScriptLoadedSet.add(browser);
gScriptLoadedSet.add(browser.permanentKey);
}
let deferred = {};
@ -93,3 +92,5 @@ let ContentMessageListener = {
}
},
};
Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager)
.addMessageListener("content-task:complete", ContentMessageListener);