mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
f4781ef545
@ -1856,3 +1856,6 @@ pref("dom.ipc.reportProcessHangs", true);
|
||||
|
||||
// Disable reader mode by default.
|
||||
pref("reader.parse-on-load.enabled", false);
|
||||
|
||||
// Disable ReadingList by default.
|
||||
pref("browser.readinglist.enabled", false);
|
||||
|
@ -156,7 +156,7 @@
|
||||
<menuitem id="context-video-fullscreen"
|
||||
accesskey="&videoFullScreen.accesskey;"
|
||||
label="&videoFullScreen.label;"
|
||||
oncommand="gContextMenu.fullScreenVideo();"/>
|
||||
oncommand="gContextMenu.mediaCommand('fullscreen');"/>
|
||||
<menuitem id="context-leave-dom-fullscreen"
|
||||
accesskey="&leaveDOMFullScreen.accesskey;"
|
||||
label="&leaveDOMFullScreen.label;"
|
||||
|
@ -223,6 +223,11 @@
|
||||
key="key_gotoHistory"
|
||||
observes="viewHistorySidebar"
|
||||
label="&historyButton.label;"/>
|
||||
<menuitem id="menu_readingListSidebar"
|
||||
key="key_readingListSidebar"
|
||||
observes="readingListSidebar"
|
||||
label="&readingList.label;"/>
|
||||
|
||||
<!-- Service providers with sidebars are inserted between these two menuseperators -->
|
||||
<menuseparator hidden="true"/>
|
||||
<menuseparator class="social-provider-menu" hidden="true"/>
|
||||
|
68
browser/base/content/browser-readinglist.js
Normal file
68
browser/base/content/browser-readinglist.js
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
# 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/.
|
||||
*/
|
||||
|
||||
let ReadingListUI = {
|
||||
/**
|
||||
* Initialize the ReadingList UI.
|
||||
*/
|
||||
init() {
|
||||
Preferences.observe("browser.readinglist.enabled", this.updateUI, this);
|
||||
this.updateUI();
|
||||
},
|
||||
|
||||
/**
|
||||
* Un-initialize the ReadingList UI.
|
||||
*/
|
||||
uninit() {
|
||||
Preferences.ignore("browser.readinglist.enabled", this.updateUI, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the ReadingList feature is enabled or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled() {
|
||||
return Preferences.get("browser.readinglist.enabled", false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the ReadingList sidebar is currently open or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isSidebarOpen() {
|
||||
return SidebarUI.isOpen && SidebarUI.currentID == "readingListSidebar";
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the UI status, ensuring the UI is shown or hidden depending on
|
||||
* whether the feature is enabled or not.
|
||||
*/
|
||||
updateUI() {
|
||||
let enabled = this.enabled;
|
||||
if (!enabled) {
|
||||
this.hideSidebar();
|
||||
}
|
||||
|
||||
document.getElementById("readingListSidebar").setAttribute("hidden", !enabled);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the ReadingList sidebar.
|
||||
* @return {Promise}
|
||||
*/
|
||||
showSidebar() {
|
||||
return SidebarUI.show("readingListSidebar");
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the ReadingList sidebar, if it is currently shown.
|
||||
*/
|
||||
hideSidebar() {
|
||||
if (this.isSidebarOpen) {
|
||||
SidebarUI.hide();
|
||||
}
|
||||
},
|
||||
};
|
@ -138,7 +138,7 @@
|
||||
<broadcaster id="Social:PageShareOrMark" disabled="true"/>
|
||||
<broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
|
||||
type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
|
||||
oncommand="toggleSidebar('viewBookmarksSidebar');"/>
|
||||
oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
|
||||
|
||||
<!-- for both places and non-places, the sidebar lives at
|
||||
chrome://browser/content/history/history-panel.xul so there are no
|
||||
@ -146,11 +146,16 @@
|
||||
<broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
|
||||
type="checkbox" group="sidebar"
|
||||
sidebarurl="chrome://browser/content/history/history-panel.xul"
|
||||
oncommand="toggleSidebar('viewHistorySidebar');"/>
|
||||
oncommand="SidebarUI.toggle('viewHistorySidebar');"/>
|
||||
|
||||
<broadcaster id="readingListSidebar" hidden="true" autoCheck="false"
|
||||
sidebartitle="&readingList.label;" type="checkbox" group="sidebar"
|
||||
sidebarurl="chrome://browser/content/readinglist/sidebar.xhtml"
|
||||
oncommand="SidebarUI.toggle('readingListSidebar');"/>
|
||||
|
||||
<broadcaster id="viewWebPanelsSidebar" autoCheck="false"
|
||||
type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul"
|
||||
oncommand="toggleSidebar('viewWebPanelsSidebar');"/>
|
||||
oncommand="SidebarUI.toggle('viewWebPanelsSidebar');"/>
|
||||
|
||||
<broadcaster id="bookmarkThisPageBroadcaster"
|
||||
label="&bookmarkThisPageCmd.label;"
|
||||
@ -418,6 +423,11 @@
|
||||
#endif
|
||||
command="viewHistorySidebar"/>
|
||||
|
||||
<key id="key_readingListSidebar"
|
||||
key="&readingList.sidebar.commandKey;"
|
||||
modifiers="accel,alt"
|
||||
command="readingListSidebar"/>
|
||||
|
||||
<key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/>
|
||||
<key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/>
|
||||
<key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
|
||||
|
309
browser/base/content/browser-sidebar.js
Normal file
309
browser/base/content/browser-sidebar.js
Normal file
@ -0,0 +1,309 @@
|
||||
# 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/.
|
||||
|
||||
/**
|
||||
* SidebarUI controls showing and hiding the browser sidebar.
|
||||
*
|
||||
* @note
|
||||
* Some of these methods take a commandID argument - we expect to find a
|
||||
* xul:broadcaster element with the specified ID.
|
||||
* The following attributes on that element may be used and/or modified:
|
||||
* - id (required) the string to match commandID. The convention
|
||||
* is to use this naming scheme: 'view<sidebar-name>Sidebar'.
|
||||
* - sidebarurl (required) specifies the URL to load in this sidebar.
|
||||
* - sidebartitle or label (in that order) specify the title to
|
||||
* display on the sidebar.
|
||||
* - checked indicates whether the sidebar is currently displayed.
|
||||
* Note that toggleSidebar updates this attribute when
|
||||
* it changes the sidebar's visibility.
|
||||
* - group this attribute must be set to "sidebar".
|
||||
*/
|
||||
let SidebarUI = {
|
||||
browser: null,
|
||||
|
||||
_box: null,
|
||||
_title: null,
|
||||
_splitter: null,
|
||||
|
||||
init() {
|
||||
this._box = document.getElementById("sidebar-box");
|
||||
this.browser = document.getElementById("sidebar");
|
||||
this._title = document.getElementById("sidebar-title");
|
||||
this._splitter = document.getElementById("sidebar-splitter");
|
||||
|
||||
if (window.opener && !window.opener.closed &&
|
||||
window.opener.document.documentURIObject.schemeIs("chrome") &&
|
||||
PrivateBrowsingUtils.isWindowPrivate(window) == PrivateBrowsingUtils.isWindowPrivate(window.opener)) {
|
||||
this.adoptFromWindow(window.opener);
|
||||
} else {
|
||||
let commandID = this._box.getAttribute("sidebarcommand");
|
||||
if (commandID) {
|
||||
let command = document.getElementById(commandID);
|
||||
if (command) {
|
||||
this._delayedLoad = true;
|
||||
this._box.hidden = false;
|
||||
this._splitter.hidden = false;
|
||||
command.setAttribute("checked", "true");
|
||||
} else {
|
||||
// Remove the |sidebarcommand| attribute, because the element it
|
||||
// refers to no longer exists, so we should assume this sidebar
|
||||
// panel has been uninstalled. (249883)
|
||||
this._box.removeAttribute("sidebarcommand");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
uninit() {
|
||||
let enumerator = Services.wm.getEnumerator(null);
|
||||
enumerator.getNext();
|
||||
if (!enumerator.hasMoreElements()) {
|
||||
document.persist("sidebar-box", "sidebarcommand");
|
||||
document.persist("sidebar-box", "width");
|
||||
document.persist("sidebar-box", "src");
|
||||
document.persist("sidebar-title", "value");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adopt the status of the sidebar from another window.
|
||||
* @param {Window} sourceWindow - Window to use as a source for sidebar status.
|
||||
*/
|
||||
adoptFromWindow(sourceWindow) {
|
||||
// If the opener had a sidebar, open the same sidebar in our window.
|
||||
// The opener can be the hidden window too, if we're coming from the state
|
||||
// where no windows are open, and the hidden window has no sidebar box.
|
||||
let sourceUI = sourceWindow.SidebarUI;
|
||||
if (!sourceUI || sourceUI._box.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commandID = sourceUI._box.getAttribute("sidebarcommand");
|
||||
let commandElem = document.getElementById(commandID);
|
||||
|
||||
// dynamically generated sidebars will fail this check.
|
||||
if (!commandElem) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._title.setAttribute("value",
|
||||
sourceUI._title.getAttribute("value"));
|
||||
this._box.setAttribute("width", sourceUI._box.boxObject.width);
|
||||
|
||||
this._box.setAttribute("sidebarcommand", commandID);
|
||||
// Note: we're setting 'src' on this._box, which is a <vbox>, not on
|
||||
// the <browser id="sidebar">. This lets us delay the actual load until
|
||||
// delayedStartup().
|
||||
this._box.setAttribute("src", sourceUI.browser.getAttribute("src"));
|
||||
this._delayedLoad = true;
|
||||
|
||||
this._box.hidden = false;
|
||||
this._splitter.hidden = false;
|
||||
commandElem.setAttribute("checked", "true");
|
||||
},
|
||||
|
||||
/**
|
||||
* If loading a sidebar was delayed on startup, start the load now.
|
||||
*/
|
||||
startDelayedLoad() {
|
||||
if (!this._delayedLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.browser.setAttribute("src", this._box.getAttribute("src"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
|
||||
* a chance to adjust focus as needed. An additional event is needed, because
|
||||
* we don't want to focus the sidebar when it's opened on startup or in a new
|
||||
* window, only when the user opens the sidebar.
|
||||
*/
|
||||
_fireFocusedEvent() {
|
||||
let event = new CustomEvent("SidebarFocused", {bubbles: true});
|
||||
this.browser.contentWindow.dispatchEvent(event);
|
||||
|
||||
// Run the original function for backwards compatibility.
|
||||
fireSidebarFocusedEvent();
|
||||
},
|
||||
|
||||
/**
|
||||
* True if the sidebar is currently open.
|
||||
*/
|
||||
get isOpen() {
|
||||
return !this._box.hidden;
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID of the current sidebar (ie, the ID of the broadcaster being used).
|
||||
* This can be set even if the sidebar is hidden.
|
||||
*/
|
||||
get currentID() {
|
||||
return this._box.getAttribute("sidebarcommand");
|
||||
},
|
||||
|
||||
get title() {
|
||||
return this._title.value;
|
||||
},
|
||||
|
||||
set title(value) {
|
||||
this._title.value = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the visibility of the sidebar. If the sidebar is hidden or is open
|
||||
* with a different commandID, then the sidebar will be opened using the
|
||||
* specified commandID. Otherwise the sidebar will be hidden.
|
||||
*
|
||||
* @param {string} commandID ID of the xul:broadcaster element to use.
|
||||
* @return {Promise}
|
||||
*/
|
||||
toggle(commandID = this.currentID) {
|
||||
if (this.isOpen && commandID == this.currentID) {
|
||||
this.hide();
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return this.show(commandID);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the sidebar, using the parameters from the specified broadcaster.
|
||||
* @see SidebarUI note.
|
||||
*
|
||||
* @param {string} commandID ID of the xul:broadcaster element to use.
|
||||
*/
|
||||
show(commandID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let sidebarBroadcaster = document.getElementById(commandID);
|
||||
if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") {
|
||||
reject(new Error("Invalid sidebar broadcaster specified"));
|
||||
return;
|
||||
}
|
||||
|
||||
let broadcasters = document.getElementsByAttribute("group", "sidebar");
|
||||
for (let broadcaster of broadcasters) {
|
||||
// skip elements that observe sidebar broadcasters and random
|
||||
// other elements
|
||||
if (broadcaster.localName != "broadcaster") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (broadcaster != sidebarBroadcaster) {
|
||||
broadcaster.removeAttribute("checked");
|
||||
} else {
|
||||
sidebarBroadcaster.setAttribute("checked", "true");
|
||||
}
|
||||
}
|
||||
|
||||
this._box.hidden = false;
|
||||
this._splitter.hidden = false;
|
||||
|
||||
this._box.setAttribute("sidebarcommand", sidebarBroadcaster.id);
|
||||
|
||||
let title = sidebarBroadcaster.getAttribute("sidebartitle");
|
||||
if (!title) {
|
||||
title = sidebarBroadcaster.getAttribute("label");
|
||||
}
|
||||
this._title.value = title;
|
||||
|
||||
let url = sidebarBroadcaster.getAttribute("sidebarurl");
|
||||
this.browser.setAttribute("src", url); // kick off async load
|
||||
|
||||
// We set this attribute here in addition to setting it on the <browser>
|
||||
// element itself, because the code in SidebarUI.uninit() persists this
|
||||
// attribute, not the "src" of the <browser id="sidebar">. The reason it
|
||||
// does that is that we want to delay sidebar load a bit when a browser
|
||||
// window opens. See delayedStartup() and SidebarUI.startDelayedLoad().
|
||||
this._box.setAttribute("src", url);
|
||||
|
||||
if (this.browser.contentDocument.location.href != url) {
|
||||
let onLoad = event => {
|
||||
this.browser.removeEventListener("load", onLoad, true);
|
||||
|
||||
// We're handling the 'load' event before it bubbles up to the usual
|
||||
// (non-capturing) event handlers. Let it bubble up before firing the
|
||||
// SidebarFocused event.
|
||||
setTimeout(() => this._fireFocusedEvent(), 0);
|
||||
|
||||
// Run the original function for backwards compatibility.
|
||||
sidebarOnLoad(event);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.browser.addEventListener("load", onLoad, true);
|
||||
} else {
|
||||
// Older code handled this case, so we do it too.
|
||||
this._fireFocusedEvent();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the sidebar.
|
||||
*/
|
||||
hide() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commandID = this._box.getAttribute("sidebarcommand");
|
||||
let sidebarBroadcaster = document.getElementById(commandID);
|
||||
|
||||
if (sidebarBroadcaster.getAttribute("checked") != "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the document currently displayed in the sidebar with about:blank
|
||||
// so that we can free memory by unloading the page. We need to explicitly
|
||||
// create a new content viewer because the old one doesn't get destroyed
|
||||
// until about:blank has loaded (which does not happen as long as the
|
||||
// element is hidden).
|
||||
this.browser.setAttribute("src", "about:blank");
|
||||
this.browser.docShell.createAboutBlankContentViewer(null);
|
||||
|
||||
sidebarBroadcaster.removeAttribute("checked");
|
||||
this._box.setAttribute("sidebarcommand", "");
|
||||
this._title.value = "";
|
||||
this._box.hidden = true;
|
||||
this._splitter.hidden = true;
|
||||
gBrowser.selectedBrowser.focus();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This exists for backards compatibility - it will be called once a sidebar is
|
||||
* ready, following any request to show it.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
function fireSidebarFocusedEvent() {}
|
||||
|
||||
/**
|
||||
* This exists for backards compatibility - it gets called when a sidebar has
|
||||
* been loaded.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
function sidebarOnLoad(event) {}
|
||||
|
||||
/**
|
||||
* This exists for backards compatibility, and is equivilent to
|
||||
* SidebarUI.toggle() without the forceOpen param. With forceOpen set to true,
|
||||
* it is equalivent to SidebarUI.show().
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
function toggleSidebar(commandID, forceOpen = false) {
|
||||
Deprecated.warning("toggleSidebar() is deprecated, please use SidebarUI.toggle() or SidebarUI.show() instead",
|
||||
"https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Sidebar");
|
||||
|
||||
if (forceOpen) {
|
||||
SidebarUI.show(commandID);
|
||||
} else {
|
||||
SidebarUI.toggle(commandID);
|
||||
}
|
||||
}
|
@ -12,6 +12,11 @@ Cu.import("resource://gre/modules/NotificationDB.jsm");
|
||||
Cu.import("resource:///modules/RecentWindow.jsm");
|
||||
Cu.import("resource://gre/modules/WindowsPrefSync.jsm");
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
|
||||
"resource://gre/modules/Preferences.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
|
||||
"resource://gre/modules/Deprecated.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
|
||||
"resource:///modules/BrowserUITelemetry.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
|
||||
@ -222,7 +227,9 @@ let gInitialPages = [
|
||||
#include browser-loop.js
|
||||
#include browser-places.js
|
||||
#include browser-plugins.js
|
||||
#include browser-readinglist.js
|
||||
#include browser-safebrowsing.js
|
||||
#include browser-sidebar.js
|
||||
#include browser-social.js
|
||||
#include browser-tabview.js
|
||||
#include browser-thumbnails.js
|
||||
@ -899,8 +906,6 @@ var gBrowserInit = {
|
||||
delayedStartupFinished: false,
|
||||
|
||||
onLoad: function() {
|
||||
var mustLoadSidebar = false;
|
||||
|
||||
gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false);
|
||||
|
||||
Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false);
|
||||
@ -952,61 +957,7 @@ var gBrowserInit = {
|
||||
// setup history swipe animation
|
||||
gHistorySwipeAnimation.init();
|
||||
|
||||
if (window.opener && !window.opener.closed &&
|
||||
window.opener.document.documentURIObject.schemeIs("chrome") &&
|
||||
PrivateBrowsingUtils.isWindowPrivate(window) == PrivateBrowsingUtils.isWindowPrivate(window.opener)) {
|
||||
let openerSidebarBox = window.opener.document.getElementById("sidebar-box");
|
||||
// If the opener had a sidebar, open the same sidebar in our window.
|
||||
// The opener can be the hidden window too, if we're coming from the state
|
||||
// where no windows are open, and the hidden window has no sidebar box.
|
||||
if (openerSidebarBox && !openerSidebarBox.hidden) {
|
||||
let sidebarCmd = openerSidebarBox.getAttribute("sidebarcommand");
|
||||
let sidebarCmdElem = document.getElementById(sidebarCmd);
|
||||
|
||||
// dynamically generated sidebars will fail this check.
|
||||
if (sidebarCmdElem) {
|
||||
let sidebarBox = document.getElementById("sidebar-box");
|
||||
let sidebarTitle = document.getElementById("sidebar-title");
|
||||
|
||||
sidebarTitle.setAttribute(
|
||||
"value", window.opener.document.getElementById("sidebar-title").getAttribute("value"));
|
||||
sidebarBox.setAttribute("width", openerSidebarBox.boxObject.width);
|
||||
|
||||
sidebarBox.setAttribute("sidebarcommand", sidebarCmd);
|
||||
// Note: we're setting 'src' on sidebarBox, which is a <vbox>, not on
|
||||
// the <browser id="sidebar">. This lets us delay the actual load until
|
||||
// delayedStartup().
|
||||
sidebarBox.setAttribute(
|
||||
"src", window.opener.document.getElementById("sidebar").getAttribute("src"));
|
||||
mustLoadSidebar = true;
|
||||
|
||||
sidebarBox.hidden = false;
|
||||
document.getElementById("sidebar-splitter").hidden = false;
|
||||
sidebarCmdElem.setAttribute("checked", "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
let box = document.getElementById("sidebar-box");
|
||||
if (box.hasAttribute("sidebarcommand")) {
|
||||
let commandID = box.getAttribute("sidebarcommand");
|
||||
if (commandID) {
|
||||
let command = document.getElementById(commandID);
|
||||
if (command) {
|
||||
mustLoadSidebar = true;
|
||||
box.hidden = false;
|
||||
document.getElementById("sidebar-splitter").hidden = false;
|
||||
command.setAttribute("checked", "true");
|
||||
}
|
||||
else {
|
||||
// Remove the |sidebarcommand| attribute, because the element it
|
||||
// refers to no longer exists, so we should assume this sidebar
|
||||
// panel has been uninstalled. (249883)
|
||||
box.removeAttribute("sidebarcommand");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SidebarUI.init();
|
||||
|
||||
// Certain kinds of automigration rely on this notification to complete
|
||||
// their tasks BEFORE the browser window is shown. SessionStore uses it to
|
||||
@ -1095,7 +1046,7 @@ var gBrowserInit = {
|
||||
ToolbarIconColor.init();
|
||||
|
||||
// Wait until chrome is painted before executing code not critical to making the window visible
|
||||
this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar);
|
||||
this._boundDelayedStartup = this._delayedStartup.bind(this);
|
||||
window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
|
||||
|
||||
this._loadHandled = true;
|
||||
@ -1106,7 +1057,7 @@ var gBrowserInit = {
|
||||
this._boundDelayedStartup = null;
|
||||
},
|
||||
|
||||
_delayedStartup: function(mustLoadSidebar) {
|
||||
_delayedStartup: function() {
|
||||
let tmp = {};
|
||||
Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
|
||||
let TelemetryTimestamps = tmp.TelemetryTimestamps;
|
||||
@ -1269,11 +1220,7 @@ var gBrowserInit = {
|
||||
|
||||
Services.telemetry.getHistogramById("E10S_WINDOW").add(gMultiProcessBrowser);
|
||||
|
||||
if (mustLoadSidebar) {
|
||||
let sidebar = document.getElementById("sidebar");
|
||||
let sidebarBox = document.getElementById("sidebar-box");
|
||||
sidebar.setAttribute("src", sidebarBox.getAttribute("src"));
|
||||
}
|
||||
SidebarUI.startDelayedLoad();
|
||||
|
||||
UpdateUrlbarSearchSplitterState();
|
||||
|
||||
@ -1427,6 +1374,7 @@ 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.
|
||||
@ -1535,14 +1483,9 @@ var gBrowserInit = {
|
||||
|
||||
gMenuButtonUpdateBadge.uninit();
|
||||
|
||||
var enumerator = Services.wm.getEnumerator(null);
|
||||
enumerator.getNext();
|
||||
if (!enumerator.hasMoreElements()) {
|
||||
document.persist("sidebar-box", "sidebarcommand");
|
||||
document.persist("sidebar-box", "width");
|
||||
document.persist("sidebar-box", "src");
|
||||
document.persist("sidebar-title", "value");
|
||||
}
|
||||
ReadingListUI.uninit();
|
||||
|
||||
SidebarUI.uninit();
|
||||
|
||||
// Now either cancel delayedStartup, or clean up the services initialized from
|
||||
// it.
|
||||
@ -1769,7 +1712,7 @@ function HandleAppCommandEvent(evt) {
|
||||
BrowserSearch.webSearch();
|
||||
break;
|
||||
case "Bookmarks":
|
||||
toggleSidebar('viewBookmarksSidebar');
|
||||
SidebarUI.toggle("viewBookmarksSidebar");
|
||||
break;
|
||||
case "Home":
|
||||
BrowserHome();
|
||||
@ -3139,23 +3082,19 @@ var PrintPreviewListener = {
|
||||
else
|
||||
this._showChrome();
|
||||
|
||||
if (this._chromeState.sidebarOpen)
|
||||
toggleSidebar(this._sidebarCommand);
|
||||
|
||||
TabsInTitlebar.allowedBy("print-preview", !gInPrintPreviewMode);
|
||||
},
|
||||
_hideChrome: function () {
|
||||
this._chromeState = {};
|
||||
|
||||
var sidebar = document.getElementById("sidebar-box");
|
||||
this._chromeState.sidebarOpen = !sidebar.hidden;
|
||||
this._sidebarCommand = sidebar.getAttribute("sidebarcommand");
|
||||
this._chromeState.sidebarOpen = SidebarUI.isOpen;
|
||||
this._sidebarCommand = SidebarUI.currentID;
|
||||
SidebarUI.hide();
|
||||
|
||||
var notificationBox = gBrowser.getNotificationBox();
|
||||
this._chromeState.notificationsOpen = !notificationBox.notificationsHidden;
|
||||
notificationBox.notificationsHidden = true;
|
||||
|
||||
document.getElementById("sidebar").setAttribute("src", "about:blank");
|
||||
gBrowser.updateWindowResizers();
|
||||
|
||||
this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden;
|
||||
@ -3185,6 +3124,9 @@ var PrintPreviewListener = {
|
||||
|
||||
if (this._chromeState.syncNotificationsOpen)
|
||||
document.getElementById("sync-notifications").notificationsHidden = false;
|
||||
|
||||
if (this._chromeState.sidebarOpen)
|
||||
SidebarUI.show(this._sidebarCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5213,121 +5155,6 @@ function displaySecurityInfo()
|
||||
BrowserPageInfo(null, "securityTab");
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or closes the sidebar identified by commandID.
|
||||
*
|
||||
* @param commandID a string identifying the sidebar to toggle; see the
|
||||
* note below. (Optional if a sidebar is already open.)
|
||||
* @param forceOpen boolean indicating whether the sidebar should be
|
||||
* opened regardless of its current state (optional).
|
||||
* @note
|
||||
* We expect to find a xul:broadcaster element with the specified ID.
|
||||
* The following attributes on that element may be used and/or modified:
|
||||
* - id (required) the string to match commandID. The convention
|
||||
* is to use this naming scheme: 'view<sidebar-name>Sidebar'.
|
||||
* - sidebarurl (required) specifies the URL to load in this sidebar.
|
||||
* - sidebartitle or label (in that order) specify the title to
|
||||
* display on the sidebar.
|
||||
* - checked indicates whether the sidebar is currently displayed.
|
||||
* Note that toggleSidebar updates this attribute when
|
||||
* it changes the sidebar's visibility.
|
||||
* - group this attribute must be set to "sidebar".
|
||||
*/
|
||||
function toggleSidebar(commandID, forceOpen) {
|
||||
|
||||
var sidebarBox = document.getElementById("sidebar-box");
|
||||
if (!commandID)
|
||||
commandID = sidebarBox.getAttribute("sidebarcommand");
|
||||
|
||||
var sidebarBroadcaster = document.getElementById(commandID);
|
||||
var sidebar = document.getElementById("sidebar"); // xul:browser
|
||||
var sidebarTitle = document.getElementById("sidebar-title");
|
||||
var sidebarSplitter = document.getElementById("sidebar-splitter");
|
||||
|
||||
if (sidebarBroadcaster.getAttribute("checked") == "true") {
|
||||
if (!forceOpen) {
|
||||
// Replace the document currently displayed in the sidebar with about:blank
|
||||
// so that we can free memory by unloading the page. We need to explicitly
|
||||
// create a new content viewer because the old one doesn't get destroyed
|
||||
// until about:blank has loaded (which does not happen as long as the
|
||||
// element is hidden).
|
||||
sidebar.setAttribute("src", "about:blank");
|
||||
sidebar.docShell.createAboutBlankContentViewer(null);
|
||||
|
||||
sidebarBroadcaster.removeAttribute("checked");
|
||||
sidebarBox.setAttribute("sidebarcommand", "");
|
||||
sidebarTitle.value = "";
|
||||
sidebarBox.hidden = true;
|
||||
sidebarSplitter.hidden = true;
|
||||
gBrowser.selectedBrowser.focus();
|
||||
} else {
|
||||
fireSidebarFocusedEvent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// now we need to show the specified sidebar
|
||||
|
||||
// ..but first update the 'checked' state of all sidebar broadcasters
|
||||
var broadcasters = document.getElementsByAttribute("group", "sidebar");
|
||||
for (let broadcaster of broadcasters) {
|
||||
// skip elements that observe sidebar broadcasters and random
|
||||
// other elements
|
||||
if (broadcaster.localName != "broadcaster")
|
||||
continue;
|
||||
|
||||
if (broadcaster != sidebarBroadcaster)
|
||||
broadcaster.removeAttribute("checked");
|
||||
else
|
||||
sidebarBroadcaster.setAttribute("checked", "true");
|
||||
}
|
||||
|
||||
sidebarBox.hidden = false;
|
||||
sidebarSplitter.hidden = false;
|
||||
|
||||
var url = sidebarBroadcaster.getAttribute("sidebarurl");
|
||||
var title = sidebarBroadcaster.getAttribute("sidebartitle");
|
||||
if (!title)
|
||||
title = sidebarBroadcaster.getAttribute("label");
|
||||
sidebar.setAttribute("src", url); // kick off async load
|
||||
sidebarBox.setAttribute("sidebarcommand", sidebarBroadcaster.id);
|
||||
sidebarTitle.value = title;
|
||||
|
||||
// We set this attribute here in addition to setting it on the <browser>
|
||||
// element itself, because the code in gBrowserInit.onUnload persists this
|
||||
// attribute, not the "src" of the <browser id="sidebar">. The reason it
|
||||
// does that is that we want to delay sidebar load a bit when a browser
|
||||
// window opens. See delayedStartup().
|
||||
sidebarBox.setAttribute("src", url);
|
||||
|
||||
if (sidebar.contentDocument.location.href != url)
|
||||
sidebar.addEventListener("load", sidebarOnLoad, true);
|
||||
else // older code handled this case, so we do it too
|
||||
fireSidebarFocusedEvent();
|
||||
}
|
||||
|
||||
function sidebarOnLoad(event) {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
sidebar.removeEventListener("load", sidebarOnLoad, true);
|
||||
// We're handling the 'load' event before it bubbles up to the usual
|
||||
// (non-capturing) event handlers. Let it bubble up before firing the
|
||||
// SidebarFocused event.
|
||||
setTimeout(fireSidebarFocusedEvent, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
|
||||
* a chance to adjust focus as needed. An additional event is needed, because
|
||||
* we don't want to focus the sidebar when it's opened on startup or in a new
|
||||
* window, only when the user opens the sidebar.
|
||||
*/
|
||||
function fireSidebarFocusedEvent() {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var event = document.createEvent("Events");
|
||||
event.initEvent("SidebarFocused", true, false);
|
||||
sidebar.contentWindow.dispatchEvent(event);
|
||||
}
|
||||
|
||||
|
||||
var gHomeButton = {
|
||||
prefDomain: "browser.startup.homepage",
|
||||
@ -5474,38 +5301,37 @@ function getBrowserSelection(aCharLen) {
|
||||
}
|
||||
|
||||
var gWebPanelURI;
|
||||
function openWebPanel(aTitle, aURI)
|
||||
{
|
||||
// Ensure that the web panels sidebar is open.
|
||||
toggleSidebar('viewWebPanelsSidebar', true);
|
||||
function openWebPanel(title, uri) {
|
||||
// Ensure that the web panels sidebar is open.
|
||||
SidebarUI.show("viewWebPanelsSidebar");
|
||||
|
||||
// Set the title of the panel.
|
||||
document.getElementById("sidebar-title").value = aTitle;
|
||||
// Set the title of the panel.
|
||||
SidebarUI.title = title;
|
||||
|
||||
// Tell the Web Panels sidebar to load the bookmark.
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
if (sidebar.docShell && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) {
|
||||
sidebar.contentWindow.loadWebPanel(aURI);
|
||||
if (gWebPanelURI) {
|
||||
gWebPanelURI = "";
|
||||
sidebar.removeEventListener("load", asyncOpenWebPanel, true);
|
||||
}
|
||||
// Tell the Web Panels sidebar to load the bookmark.
|
||||
if (SidebarUI.browser.docShell && SidebarUI.browser.contentDocument &&
|
||||
SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) {
|
||||
SidebarUI.browser.contentWindow.loadWebPanel(uri);
|
||||
if (gWebPanelURI) {
|
||||
gWebPanelURI = "";
|
||||
SidebarUI.browser.removeEventListener("load", asyncOpenWebPanel, true);
|
||||
}
|
||||
else {
|
||||
// The panel is still being constructed. Attach an onload handler.
|
||||
if (!gWebPanelURI)
|
||||
sidebar.addEventListener("load", asyncOpenWebPanel, true);
|
||||
gWebPanelURI = aURI;
|
||||
} else {
|
||||
// The panel is still being constructed. Attach an onload handler.
|
||||
if (!gWebPanelURI) {
|
||||
SidebarUI.browser.addEventListener("load", asyncOpenWebPanel, true);
|
||||
}
|
||||
gWebPanelURI = uri;
|
||||
}
|
||||
}
|
||||
|
||||
function asyncOpenWebPanel(event)
|
||||
{
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
if (gWebPanelURI && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser'))
|
||||
sidebar.contentWindow.loadWebPanel(gWebPanelURI);
|
||||
gWebPanelURI = "";
|
||||
sidebar.removeEventListener("load", asyncOpenWebPanel, true);
|
||||
function asyncOpenWebPanel(event) {
|
||||
if (gWebPanelURI && SidebarUI.browser.contentDocument &&
|
||||
SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) {
|
||||
SidebarUI.browser.contentWindow.loadWebPanel(gWebPanelURI);
|
||||
}
|
||||
gWebPanelURI = "";
|
||||
SidebarUI.browser.removeEventListener("load", asyncOpenWebPanel, true);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -885,7 +885,7 @@
|
||||
class="subviewbutton"
|
||||
label="&viewBookmarksSidebar2.label;"
|
||||
type="checkbox"
|
||||
oncommand="toggleSidebar('viewBookmarksSidebar');">
|
||||
oncommand="SidebarUI.toggle('viewBookmarksSidebar');">
|
||||
<observes element="viewBookmarksSidebar" attribute="checked"/>
|
||||
</menuitem>
|
||||
<!-- NB: temporary solution for bug 985024, this should go away soon. -->
|
||||
@ -1138,10 +1138,10 @@
|
||||
<sidebarheader id="sidebar-header" align="center">
|
||||
<label id="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/>
|
||||
<image id="sidebar-throbber"/>
|
||||
<toolbarbutton class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="toggleSidebar();"/>
|
||||
<toolbarbutton class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="SidebarUI.hide();"/>
|
||||
</sidebarheader>
|
||||
<browser id="sidebar" flex="1" autoscroll="false" disablehistory="true"
|
||||
style="min-width: 14em; width: 18em; max-width: 36em;"/>
|
||||
style="min-width: 14em; width: 18em; max-width: 36em;" tooltip="aHTMLTooltip"/>
|
||||
</vbox>
|
||||
|
||||
<splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/>
|
||||
|
@ -1097,5 +1097,9 @@ addMessageListener("ContextMenu:MediaCommand", (message) => {
|
||||
message.data.command == "showstats");
|
||||
media.dispatchEvent(event);
|
||||
break;
|
||||
case "fullscreen":
|
||||
if (content.document.mozFullScreenEnabled)
|
||||
media.mozRequestFullScreen();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -1078,12 +1078,6 @@ nsContextMenu.prototype = {
|
||||
mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
|
||||
},
|
||||
|
||||
fullScreenVideo: function () {
|
||||
let video = this.target;
|
||||
if (document.mozFullScreenEnabled)
|
||||
video.mozRequestFullScreen();
|
||||
},
|
||||
|
||||
leaveDOMFullScreen: function() {
|
||||
document.mozCancelFullScreen();
|
||||
},
|
||||
|
70
browser/base/content/test/BrowserUITestUtils.jsm
Normal file
70
browser/base/content/test/BrowserUITestUtils.jsm
Normal file
@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"BrowserUITestUtils",
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
|
||||
|
||||
/**
|
||||
* Default wait period in millseconds, when waiting for the expected event to occur.
|
||||
* @type {number}
|
||||
*/
|
||||
const DEFAULT_WAIT = 2000;
|
||||
|
||||
|
||||
/**
|
||||
* Test utility functions for dealing with the browser UI DOM.
|
||||
*/
|
||||
this.BrowserUITestUtils = {
|
||||
|
||||
/**
|
||||
* Waits a specified number of miliseconds for a specified event to be
|
||||
* fired on a specified element.
|
||||
*
|
||||
* Usage:
|
||||
* let receivedEvent = BrowserUITestUtils.waitForEvent(element, "eventName");
|
||||
* // Do some processing here that will cause the event to be fired
|
||||
* // ...
|
||||
* // Now yield until the Promise is fulfilled
|
||||
* yield receivedEvent;
|
||||
* if (receivedEvent && !(receivedEvent instanceof Error)) {
|
||||
* receivedEvent.msg == "eventName";
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* @param {Element} subject - The element that should receive the event.
|
||||
* @param {string} eventName - The event to wait for.
|
||||
* @param {number} timeoutMs - The number of miliseconds to wait before giving up.
|
||||
* @param {Element} target - Expected target of the event.
|
||||
* @returns {Promise} A Promise that resolves to the received event, or
|
||||
* rejects with an Error.
|
||||
*/
|
||||
waitForEvent(subject, eventName, timeoutMs, target) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function listener(event) {
|
||||
if (target && target !== event.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
subject.removeEventListener(eventName, listener);
|
||||
clearTimeout(timerID);
|
||||
resolve(event);
|
||||
}
|
||||
|
||||
timeoutMs = timeoutMs || DEFAULT_WAIT;
|
||||
let stack = new Error().stack;
|
||||
|
||||
let timerID = setTimeout(() => {
|
||||
subject.removeEventListener(eventName, listener);
|
||||
reject(new Error(`${eventName} event timeout at ${stack}`));
|
||||
}, timeoutMs);
|
||||
|
||||
|
||||
subject.addEventListener(eventName, listener);
|
||||
});
|
||||
},
|
||||
};
|
@ -4,7 +4,7 @@ function test() {
|
||||
// XXX This looks a bit odd, but is needed to avoid throwing when removing the
|
||||
// event listeners below. See bug 310955.
|
||||
document.getElementById("sidebar").addEventListener("load", delayedOpenUrl, true);
|
||||
toggleSidebar("viewWebPanelsSidebar", true);
|
||||
SidebarUI.show("viewWebPanelsSidebar");
|
||||
}
|
||||
|
||||
function delayedOpenUrl() {
|
||||
@ -75,7 +75,7 @@ function contextMenuClosed()
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
sidebar.contentDocument.removeEventListener("popuphidden", contextMenuClosed, false);
|
||||
|
||||
toggleSidebar("viewWebPanelsSidebar");
|
||||
SidebarUI.hide();
|
||||
|
||||
ok(document.getElementById("sidebar-box").hidden, "Sidebar successfully hidden");
|
||||
|
||||
|
@ -6,6 +6,10 @@
|
||||
|
||||
SPHINX_TREES['sslerrorreport'] = 'content/docs/sslerrorreport'
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
'content/test/BrowserUITestUtils.jsm',
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'content/test/general/mochitest.ini',
|
||||
]
|
||||
|
@ -120,6 +120,7 @@ static RedirEntry kRedirMap[] = {
|
||||
{ "reader", "chrome://global/content/reader/aboutReader.html",
|
||||
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
|
||||
nsIAboutModule::ALLOW_SCRIPT |
|
||||
nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
|
||||
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
|
||||
};
|
||||
static const int kRedirTotal = ArrayLength(kRedirMap);
|
||||
|
@ -64,7 +64,7 @@
|
||||
type="checkbox"
|
||||
class="subviewbutton"
|
||||
key="key_gotoHistory"
|
||||
oncommand="toggleSidebar('viewHistorySidebar'); PanelUI.hide();">
|
||||
oncommand="SidebarUI.toggle('viewHistorySidebar'); PanelUI.hide();">
|
||||
<observes element="viewHistorySidebar" attribute="checked"/>
|
||||
</toolbarbutton>
|
||||
<toolbarbutton id="appMenuClearRecentHistory"
|
||||
@ -108,7 +108,7 @@
|
||||
label="&viewBookmarksSidebar2.label;"
|
||||
class="subviewbutton"
|
||||
key="viewBookmarksSidebarKb"
|
||||
oncommand="toggleSidebar('viewBookmarksSidebar'); PanelUI.hide();">
|
||||
oncommand="SidebarUI.toggle('viewBookmarksSidebar'); PanelUI.hide();">
|
||||
<observes element="viewBookmarksSidebar" attribute="checked"/>
|
||||
</toolbarbutton>
|
||||
<toolbarbutton id="panelMenu_viewBookmarksToolbar"
|
||||
|
@ -22,7 +22,7 @@ registerCleanupFunction(() => {
|
||||
|
||||
// Ensure sidebar is hidden after each test:
|
||||
if (!document.getElementById("sidebar-box").hidden) {
|
||||
toggleSidebar("", false);
|
||||
SidebarUI.hide();
|
||||
}
|
||||
});
|
||||
|
||||
@ -53,9 +53,13 @@ function removeWidget() {
|
||||
|
||||
// Filters out the trailing menuseparators from the sidebar list
|
||||
function getSidebarList() {
|
||||
let sidebars = [...gSidebarMenu.children];
|
||||
while (sidebars[sidebars.length - 1].localName == "menuseparator")
|
||||
sidebars.pop();
|
||||
let sidebars = [...gSidebarMenu.children].filter(sidebar => {
|
||||
if (sidebar.localName == "menuseparator")
|
||||
return false;
|
||||
if (sidebar.getAttribute("hidden") == "true")
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
return sidebars;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ DIRS += [
|
||||
'places',
|
||||
'preferences',
|
||||
'privatebrowsing',
|
||||
'readinglist',
|
||||
'search',
|
||||
'sessionstore',
|
||||
'shell',
|
||||
|
@ -16,7 +16,9 @@ add_task(function* test() {
|
||||
}
|
||||
|
||||
let sidebar = yield promiseLoadedSidebar("viewBookmarksSidebar");
|
||||
registerCleanupFunction(toggleSidebar);
|
||||
registerCleanupFunction(() => {
|
||||
SidebarUI.hide();
|
||||
});
|
||||
|
||||
// Focus the tree and check if its controller is returned.
|
||||
let tree = sidebar.contentDocument.getElementById("bookmarks-view");
|
||||
@ -59,6 +61,6 @@ function promiseLoadedSidebar(cmd) {
|
||||
resolve(sidebar);
|
||||
}, true);
|
||||
|
||||
toggleSidebar(cmd, true);
|
||||
SidebarUI.show(cmd);
|
||||
});
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ gTests.push({
|
||||
finish: function() {
|
||||
// Close window, toggle sidebar and goto next test.
|
||||
this.window.document.documentElement.cancelDialog();
|
||||
toggleSidebar(this.sidebar, false);
|
||||
SidebarUI.hide();
|
||||
runNextTest();
|
||||
},
|
||||
|
||||
@ -138,7 +138,7 @@ gTests.push({
|
||||
|
||||
finish: function() {
|
||||
this.window.document.documentElement.cancelDialog();
|
||||
toggleSidebar(this.sidebar, false);
|
||||
SidebarUI.hide();
|
||||
runNextTest();
|
||||
},
|
||||
|
||||
@ -232,7 +232,7 @@ gTests.push({
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
toggleSidebar(this.sidebar, false);
|
||||
SidebarUI.hide();
|
||||
runNextTest();
|
||||
},
|
||||
|
||||
@ -291,7 +291,7 @@ gTests.push({
|
||||
|
||||
finish: function() {
|
||||
// Window is already closed.
|
||||
toggleSidebar(this.sidebar, false);
|
||||
SidebarUI.hide();
|
||||
runNextTest();
|
||||
},
|
||||
|
||||
@ -390,7 +390,7 @@ gTests.push({
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
toggleSidebar(this.sidebar, false);
|
||||
SidebarUI.hide();
|
||||
runNextTest();
|
||||
},
|
||||
|
||||
@ -468,7 +468,7 @@ gTests.push({
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
toggleSidebar(this.sidebar, false);
|
||||
SidebarUI.hide();
|
||||
runNextTest();
|
||||
},
|
||||
|
||||
@ -529,7 +529,7 @@ function execute_test_in_sidebar() {
|
||||
// Need to executeSoon since the tree is initialized on sidebar load.
|
||||
executeSoon(open_properties_dialog);
|
||||
}, true);
|
||||
toggleSidebar(gCurrentTest.sidebar, true);
|
||||
SidebarUI.show(gCurrentTest.sidebar);
|
||||
}
|
||||
|
||||
function open_properties_dialog() {
|
||||
|
@ -44,7 +44,7 @@ function continue_test() {
|
||||
transition: hs.TRANSITION_TYPED});
|
||||
}
|
||||
PlacesTestUtils.addVisits(places).then(() => {
|
||||
toggleSidebar("viewHistorySidebar", true);
|
||||
SidebarUI.show("viewHistorySidebar");
|
||||
});
|
||||
|
||||
sidebar.addEventListener("load", function() {
|
||||
@ -63,7 +63,7 @@ function continue_test() {
|
||||
check_sidebar_tree_order(pages.length);
|
||||
|
||||
// Cleanup.
|
||||
toggleSidebar("viewHistorySidebar", false);
|
||||
SidebarUI.hide();
|
||||
PlacesTestUtils.clearHistory().then(finish);
|
||||
});
|
||||
}, true);
|
||||
|
@ -18,7 +18,7 @@ function test() {
|
||||
// If a sidebar is already open, close it.
|
||||
if (!document.getElementById("sidebar-box").hidden) {
|
||||
info("Unexpected sidebar found - a previous test failed to cleanup correctly");
|
||||
toggleSidebar();
|
||||
SidebarUI.hide();
|
||||
}
|
||||
|
||||
let sidebar = document.getElementById("sidebar");
|
||||
@ -76,7 +76,7 @@ function test() {
|
||||
|
||||
function testPlacesPanel(preFunc, postFunc) {
|
||||
currentTest.init(function() {
|
||||
toggleSidebar(currentTest.sidebarName);
|
||||
SidebarUI.show(currentTest.sidebarName);
|
||||
});
|
||||
|
||||
sidebar.addEventListener("load", function() {
|
||||
@ -95,7 +95,7 @@ function test() {
|
||||
aSubject.Dialog.ui.button0.click();
|
||||
|
||||
executeSoon(function () {
|
||||
toggleSidebar(currentTest.sidebarName);
|
||||
SidebarUI.hide();
|
||||
currentTest.cleanup(postFunc);
|
||||
});
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ function openBookmarksSidebar() {
|
||||
// Need to executeSoon since the tree is initialized on sidebar load.
|
||||
executeSoon(startTest);
|
||||
}, true);
|
||||
toggleSidebar("viewBookmarksSidebar", true);
|
||||
SidebarUI.show("viewBookmarksSidebar");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -171,7 +171,7 @@ function startTest() {
|
||||
*/
|
||||
function finishTest() {
|
||||
// Close bookmarks sidebar.
|
||||
toggleSidebar("viewBookmarksSidebar", false);
|
||||
SidebarUI.hide();
|
||||
|
||||
// Collapse the personal toolbar if needed.
|
||||
if (wasCollapsed) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
let gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
|
||||
let gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(Ci.nsIHandlerService);
|
||||
|
||||
Services.prefs.setBoolPref("browser.preferences.inContent", true);
|
||||
|
||||
function setupFakeHandler() {
|
||||
let info = gMimeSvc.getFromTypeAndExtension("text/plain", "foo.txt");
|
||||
ok(info.possibleLocalHandlers.length, "Should have at least one known handler");
|
||||
@ -92,5 +94,6 @@ add_task(function*() {
|
||||
registerCleanupFunction(function() {
|
||||
let infoToModify = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
|
||||
gHandlerSvc.remove(infoToModify);
|
||||
Services.prefs.clearUserPref("browser.preferences.inContent");
|
||||
});
|
||||
|
||||
|
@ -33,7 +33,7 @@ function test() {
|
||||
resolve(win);
|
||||
}, true);
|
||||
|
||||
win.toggleSidebar(sidebarID, true);
|
||||
win.SidebarUI.show(sidebarID);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
356
browser/components/readinglist/ReadingList.jsm
Normal file
356
browser/components/readinglist/ReadingList.jsm
Normal file
@ -0,0 +1,356 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["ReadingList"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
|
||||
(function() {
|
||||
let parentLog = Log.repository.getLogger("readinglist");
|
||||
parentLog.level = Preferences.get("browser.readinglist.logLevel", Log.Level.Warn);
|
||||
Preferences.observe("browser.readinglist.logLevel", value => {
|
||||
parentLog.level = value;
|
||||
});
|
||||
let formatter = new Log.BasicFormatter();
|
||||
parentLog.addAppender(new Log.ConsoleAppender(formatter));
|
||||
parentLog.addAppender(new Log.DumpAppender(formatter));
|
||||
})();
|
||||
|
||||
let log = Log.repository.getLogger("readinglist.api");
|
||||
|
||||
/**
|
||||
* Represents an item in the Reading List.
|
||||
* @constructor
|
||||
* @see https://github.com/mozilla-services/readinglist/wiki/API-Design-proposal#data-model
|
||||
*/
|
||||
function Item(data) {
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
Item.prototype = {
|
||||
/**
|
||||
* UUID
|
||||
* @type {string}
|
||||
*/
|
||||
get id() {
|
||||
return this._data.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Server timestamp
|
||||
* @type {string}
|
||||
*/
|
||||
get lastModified() {
|
||||
return this._data.last_modified;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {nsIURL}
|
||||
*/
|
||||
get originalUrl() {
|
||||
return Services.io.newURI(this._data.url, null, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get originalTitle() {
|
||||
return this._data.title || "";
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {nsIURL}
|
||||
*/
|
||||
get resolvedUrl() {
|
||||
return Services.io.newURI(this._data.resolved_url || this._data.url, null, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get resolvedTitle() {
|
||||
return this._data.resolved_title || this.originalTitle;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get excerpt() {
|
||||
return this._data.excerpt || "";
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {ItemStates}
|
||||
*/
|
||||
get state() {
|
||||
return ReadingList.ItemStates[this._data.state] || ReadingList.ItemStates.OK;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isFavorite() {
|
||||
return !!this._data.favorite;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isArticle() {
|
||||
return !!this._data.is_article;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get wordCount() {
|
||||
return this._data.word_count || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnread() {
|
||||
return !!this._data.unread;
|
||||
},
|
||||
|
||||
/**
|
||||
* Device name
|
||||
* @type {string}
|
||||
*/
|
||||
get addedBy() {
|
||||
return this._data.added_by;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {Date}
|
||||
*/
|
||||
get addedOn() {
|
||||
return new Date(this._data.added_on);
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {Date}
|
||||
*/
|
||||
get storedOn() {
|
||||
return new Date(this._data.stored_on);
|
||||
},
|
||||
|
||||
/**
|
||||
* Device name
|
||||
* @type {string}
|
||||
*/
|
||||
get markedReadBy() {
|
||||
return this._data.marked_read_by;
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {Date}
|
||||
*/
|
||||
get markedReadOn() {
|
||||
return new date(this._data.marked_read_on);
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get readPosition() {
|
||||
return this._data.read_position;
|
||||
},
|
||||
|
||||
// Data not specified by the current server API
|
||||
|
||||
/**
|
||||
* Array of scraped or captured summary images for this page.
|
||||
* TODO: Implement this.
|
||||
* @type {[nsIURL]}
|
||||
*/
|
||||
get images() {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Favicon for this site.
|
||||
* @type {nsIURL}
|
||||
* TODO: Generate moz-anno: URI for favicon.
|
||||
*/
|
||||
get favicon() {
|
||||
return null;
|
||||
},
|
||||
|
||||
// Helpers
|
||||
|
||||
/**
|
||||
* Alias for resolvedUrl.
|
||||
* TODO: This url/resolvedUrl alias makes it feel like the server API hasn't got this right.
|
||||
*/
|
||||
get url() {
|
||||
return this.resolvedUrl;
|
||||
},
|
||||
/**
|
||||
* Alias for resolvedTitle
|
||||
*/
|
||||
get title() {
|
||||
return this.resolvedTitle;
|
||||
},
|
||||
|
||||
/**
|
||||
* Domain portion of the URL, with prefixes stripped. For display purposes.
|
||||
* @type {string}
|
||||
*/
|
||||
get domain() {
|
||||
let host = this.resolvedUrl.host;
|
||||
if (host.startsWith("www.")) {
|
||||
host = host.slice(4);
|
||||
}
|
||||
return host;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert this Item to a string representation.
|
||||
*/
|
||||
toString() {
|
||||
return `[Item url=${this.url.spec}]`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the value that should be used for a JSON representation of this Item.
|
||||
*/
|
||||
toJSON() {
|
||||
return this._data;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
let ItemStates = {
|
||||
OK: Symbol("ok"),
|
||||
ARCHIVED: Symbol("archived"),
|
||||
DELETED: Symbol("deleted"),
|
||||
};
|
||||
|
||||
|
||||
this.ReadingList = {
|
||||
Item: Item,
|
||||
ItemStates: ItemStates,
|
||||
|
||||
_listeners: new Set(),
|
||||
_items: [],
|
||||
|
||||
/**
|
||||
* Initialize the ReadingList component.
|
||||
*/
|
||||
_init() {
|
||||
log.debug("Init");
|
||||
|
||||
// Initialize mock data
|
||||
let mockData = JSON.parse(Preferences.get("browser.readinglist.mockData", "[]"));
|
||||
for (let itemData of mockData) {
|
||||
this._items.push(new Item(itemData));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add an event listener.
|
||||
* @param {object} listener - Listener object to start notifying.
|
||||
*/
|
||||
addListener(listener) {
|
||||
this._listeners.add(listener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a specified event listener.
|
||||
* @param {object} listener - Listener object to stop notifying.
|
||||
*/
|
||||
removeListener(listener) {
|
||||
this._listeners.delete(listener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notify all registered event listeners of an event.
|
||||
* @param {string} eventName - Event name, which will be used as a method name
|
||||
* on listeners to call.
|
||||
*/
|
||||
_notifyListeners(eventName, ...args) {
|
||||
for (let listener of this._listeners) {
|
||||
if (typeof listener[eventName] != "function") {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
listener[eventName](...args);
|
||||
} catch (e) {
|
||||
log.error(`Error calling listener.${eventName}`, e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the number of items that match a set of given conditions.
|
||||
* TODO: Implement filtering, sorting, etc. Needs backend storage work.
|
||||
*
|
||||
* @param {Object} conditions Object specifying a set of conditions for
|
||||
* filtering items.
|
||||
* @return {Promise}
|
||||
* @resolves {number}
|
||||
*/
|
||||
getNumItems(conditions = {unread: false}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this._items.length);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch items matching a set of conditions, in a sorted list.
|
||||
* TODO: Implement filtering, sorting, etc. Needs backend storage work.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves {[Item]}
|
||||
*/
|
||||
getItems(options = {sort: "addedOn", conditions: {unread: false}}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve([...this._items]);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Find an item based on its ID.
|
||||
* TODO: Implement. Needs backend storage work.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves {Item}
|
||||
*/
|
||||
getItemByID(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(null);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Find an item based on its URL.
|
||||
*
|
||||
* TODO: Implement. Needs backend storage work.
|
||||
* TODO: Does this match original or resolved URL, or both?
|
||||
* TODO: Should this just be a generic findItem API?
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves {Item}
|
||||
*/
|
||||
getItemByURL(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(null);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
ReadingList._init();
|
7
browser/components/readinglist/jar.mn
Normal file
7
browser/components/readinglist/jar.mn
Normal file
@ -0,0 +1,7 @@
|
||||
# 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/.
|
||||
|
||||
browser.jar:
|
||||
content/browser/readinglist/sidebar.xhtml
|
||||
content/browser/readinglist/sidebar.js
|
15
browser/components/readinglist/moz.build
Normal file
15
browser/components/readinglist/moz.build
Normal file
@ -0,0 +1,15 @@
|
||||
# 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/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
EXTRA_JS_MODULES.readinglist += [
|
||||
'ReadingList.jsm',
|
||||
]
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
'test/ReadingListTestUtils.jsm',
|
||||
]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
|
403
browser/components/readinglist/sidebar.js
Normal file
403
browser/components/readinglist/sidebar.js
Normal file
@ -0,0 +1,403 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
|
||||
|
||||
let log = Cu.import("resource://gre/modules/Log.jsm", {})
|
||||
.Log.repository.getLogger("readinglist.sidebar");
|
||||
|
||||
|
||||
let RLSidebar = {
|
||||
/**
|
||||
* Container element for all list item elements.
|
||||
* @type {Element}
|
||||
*/
|
||||
list: null,
|
||||
|
||||
/**
|
||||
* <template> element used for constructing list item elements.
|
||||
* @type {Element}
|
||||
*/
|
||||
itemTemplate: null,
|
||||
|
||||
/**
|
||||
* Map of ReadingList Item objects, keyed by their ID.
|
||||
* @type {Map}
|
||||
*/
|
||||
itemsById: new Map(),
|
||||
/**
|
||||
* Map of list item elements, keyed by their corresponding Item's ID.
|
||||
* @type {Map}
|
||||
*/
|
||||
itemNodesById: new Map(),
|
||||
|
||||
/**
|
||||
* Initialize the sidebar UI.
|
||||
*/
|
||||
init() {
|
||||
log.debug("Initializing");
|
||||
|
||||
addEventListener("unload", () => this.uninit());
|
||||
|
||||
this.list = document.getElementById("list");
|
||||
this.itemTemplate = document.getElementById("item-template");
|
||||
|
||||
this.list.addEventListener("click", event => this.onListClick(event));
|
||||
this.list.addEventListener("mousemove", event => this.onListMouseMove(event));
|
||||
this.list.addEventListener("keydown", event => this.onListKeyDown(event), true);
|
||||
|
||||
this.ensureListItems();
|
||||
ReadingList.addListener(this);
|
||||
|
||||
let initEvent = new CustomEvent("Initialized", {bubbles: true});
|
||||
document.documentElement.dispatchEvent(initEvent);
|
||||
},
|
||||
|
||||
/**
|
||||
* Un-initialize the sidebar UI.
|
||||
*/
|
||||
uninit() {
|
||||
log.debug("Shutting down");
|
||||
|
||||
ReadingList.removeListener(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle an item being added to the ReadingList.
|
||||
* TODO: We may not want to show this new item right now.
|
||||
* TODO: We should guard against the list growing here.
|
||||
*
|
||||
* @param {Readinglist.Item} item - Item that was added.
|
||||
*/
|
||||
onItemAdded(item) {
|
||||
log.trace(`onItemAdded: ${item}`);
|
||||
|
||||
let itemNode = document.importNode(this.itemTemplate.content, true).firstElementChild;
|
||||
this.updateItem(item, itemNode);
|
||||
this.list.appendChild(itemNode);
|
||||
this.itemNodesById.set(item.id, itemNode);
|
||||
this.itemsById.set(item.id, item);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle an item being deleted from the ReadingList.
|
||||
* @param {ReadingList.Item} item - Item that was deleted.
|
||||
*/
|
||||
onItemDeleted(item) {
|
||||
log.trace(`onItemDeleted: ${item}`);
|
||||
|
||||
let itemNode = this.itemNodesById.get(item.id);
|
||||
itemNode.remove();
|
||||
this.itemNodesById.delete(item.id);
|
||||
this.itemsById.delete(item.id);
|
||||
// TODO: ensureListItems doesn't yet cope with needing to add one item.
|
||||
//this.ensureListItems();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle an item in the ReadingList having any of its properties changed.
|
||||
* @param {ReadingList.Item} item - Item that was updated.
|
||||
*/
|
||||
onItemUpdated(item) {
|
||||
log.trace(`onItemUpdated: ${item}`);
|
||||
|
||||
let itemNode = this.itemNodesById.get(item.id);
|
||||
if (!itemNode)
|
||||
return;
|
||||
|
||||
this.updateItem(item, itemNode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the element representing an item, ensuring it's in sync with the
|
||||
* underlying data.
|
||||
* @param {ReadingList.Item} item - Item to use as a source.
|
||||
* @param {Element} itemNode - Element to update.
|
||||
*/
|
||||
updateItem(item, itemNode) {
|
||||
itemNode.setAttribute("id", "item-" + item.id);
|
||||
itemNode.setAttribute("title", `${item.title}\n${item.url.spec}`);
|
||||
|
||||
itemNode.querySelector(".item-title").textContent = item.title;
|
||||
itemNode.querySelector(".item-domain").textContent = item.domain;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure that the list is populated with the correct items.
|
||||
*/
|
||||
ensureListItems() {
|
||||
ReadingList.getItems().then(items => {
|
||||
for (let item of items) {
|
||||
// TODO: Should be batch inserting via DocumentFragment
|
||||
try {
|
||||
this.onItemAdded(item);
|
||||
} catch (e) {
|
||||
log.warn("Error adding item", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the number of items currently displayed in the list.
|
||||
* @type {number}
|
||||
*/
|
||||
get numItems() {
|
||||
return this.list.childElementCount;
|
||||
},
|
||||
|
||||
/**
|
||||
* The currently active element in the list.
|
||||
* @type {Element}
|
||||
*/
|
||||
get activeItem() {
|
||||
return document.querySelector("#list > .item.active");
|
||||
},
|
||||
|
||||
set activeItem(node) {
|
||||
if (node && node.parentNode != this.list) {
|
||||
log.error(`Unable to set activeItem to invalid node ${node}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Setting activeItem: ${node ? node.id : null}`);
|
||||
|
||||
if (node) {
|
||||
if (!node.classList.contains("selected")) {
|
||||
this.selectedItem = node;
|
||||
}
|
||||
|
||||
if (node.classList.contains("active")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let prevItem = document.querySelector("#list > .item.active");
|
||||
if (prevItem) {
|
||||
prevItem.classList.remove("active");
|
||||
}
|
||||
|
||||
if (node) {
|
||||
node.classList.add("active");
|
||||
}
|
||||
|
||||
let event = new CustomEvent("ActiveItemChanged", {bubbles: true});
|
||||
this.list.dispatchEvent(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* The currently selected item in the list.
|
||||
* @type {Element}
|
||||
*/
|
||||
get selectedItem() {
|
||||
return document.querySelector("#list > .item.selected");
|
||||
},
|
||||
|
||||
set selectedItem(node) {
|
||||
if (node && node.parentNode != this.list) {
|
||||
log.error(`Unable to set selectedItem to invalid node ${node}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Setting activeItem: ${node ? node.id : null}`);
|
||||
|
||||
let prevItem = document.querySelector("#list > .item.selected");
|
||||
if (prevItem) {
|
||||
prevItem.classList.remove("selected");
|
||||
}
|
||||
|
||||
if (node) {
|
||||
node.classList.add("selected");
|
||||
let itemId = this.getItemIdFromNode(node);
|
||||
this.list.setAttribute("aria-activedescendant", "item-" + itemId);
|
||||
} else {
|
||||
this.list.removeAttribute("aria-activedescendant");
|
||||
}
|
||||
|
||||
let event = new CustomEvent("SelectedItemChanged", {bubbles: true});
|
||||
this.list.dispatchEvent(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* The index of the currently selected item in the list.
|
||||
* @type {number}
|
||||
*/
|
||||
get selectedIndex() {
|
||||
for (let i = 0; i < this.numItems; i++) {
|
||||
let item = this.list.children.item(i);
|
||||
if (!item) {
|
||||
break;
|
||||
}
|
||||
if (item.classList.contains("selected")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
set selectedIndex(index) {
|
||||
log.debug(`Setting selectedIndex: ${index}`);
|
||||
|
||||
if (index == -1) {
|
||||
this.selectedItem = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let item = this.list.children.item(index);
|
||||
if (!item) {
|
||||
log.warn(`Unable to set selectedIndex to invalid index ${index}`);
|
||||
return;
|
||||
}
|
||||
this.selectedItem = item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a given URL. The event is used to determine where it should be opened
|
||||
* (current tab, new tab, new window).
|
||||
* @param {string} url - URL to open.
|
||||
* @param {Event} event - KeyEvent or MouseEvent that triggered this action.
|
||||
*/
|
||||
openURL(url, event) {
|
||||
// TODO: Disabled while working on the listbox mechanics.
|
||||
log.debug(`Opening page ${url}`);
|
||||
return;
|
||||
|
||||
let mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
mainWindow.openUILink(url, event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the ID of the Item associated with a given list item element.
|
||||
* @param {element} node - List item element to get an ID for.
|
||||
* @return {string} Assocated Item ID.
|
||||
*/
|
||||
getItemIdFromNode(node) {
|
||||
let id = node.getAttribute("id");
|
||||
if (id && id.startsWith("item-")) {
|
||||
return id.slice(5);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the Item associated with a given list item element.
|
||||
* @param {element} node - List item element to get an Item for.
|
||||
* @return {string} Associated Item.
|
||||
*/
|
||||
getItemFromNode(node) {
|
||||
let itemId = this.getItemIdFromNode(node);
|
||||
if (!itemId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.itemsById.get(itemId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the active item in the list.
|
||||
* @param {Event} event - Event triggering this.
|
||||
*/
|
||||
openActiveItem(event) {
|
||||
let itemNode = this.activeItem;
|
||||
if (!itemNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let item = this.getItemFromNode(itemNode);
|
||||
this.openURL(item.url.spec, event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the parent item element, from a given child element.
|
||||
* @param {Element} node - Child element.
|
||||
* @return {Element} Element for the item, or null if not found.
|
||||
*/
|
||||
findParentItemNode(node) {
|
||||
while (node && node != this.list && node != document.documentElement &&
|
||||
!node.classList.contains("item")) {
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
if (node != this.list && node != document.documentElement) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a click event on the list box.
|
||||
* @param {Event} event - Triggering event.
|
||||
*/
|
||||
onListClick(event) {
|
||||
let itemNode = this.findParentItemNode(event.target);
|
||||
if (!itemNode)
|
||||
return;
|
||||
|
||||
this.activeItem = itemNode;
|
||||
this.openActiveItem(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a mousemove event over the list box.
|
||||
* @param {Event} event - Triggering event.
|
||||
*/
|
||||
onListMouseMove(event) {
|
||||
let itemNode = this.findParentItemNode(event.target);
|
||||
if (!itemNode)
|
||||
return;
|
||||
|
||||
this.selectedItem = itemNode;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a keydown event on the list box.
|
||||
* @param {Event} event - Triggering event.
|
||||
*/
|
||||
onListKeyDown(event) {
|
||||
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
|
||||
// TODO: Refactor this so we pass a direction to a generic method.
|
||||
// See autocomplete.xml's getNextIndex
|
||||
event.preventDefault();
|
||||
let index = this.selectedIndex + 1;
|
||||
if (index >= this.numItems) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
this.selectedIndex = index;
|
||||
this.selectedItem.focus();
|
||||
} else if (event.keyCode == KeyEvent.DOM_VK_UP) {
|
||||
event.preventDefault();
|
||||
|
||||
let index = this.selectedIndex - 1;
|
||||
if (index < 0) {
|
||||
index = this.numItems - 1;
|
||||
}
|
||||
|
||||
this.selectedIndex = index;
|
||||
this.selectedItem.focus();
|
||||
} else if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
|
||||
let selectedItem = this.selectedItem;
|
||||
if (selectedItem) {
|
||||
this.activeItem = this.selectedItem;
|
||||
this.openActiveItem(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
addEventListener("DOMContentLoaded", () => RLSidebar.init());
|
30
browser/components/readinglist/sidebar.xhtml
Normal file
30
browser/components/readinglist/sidebar.xhtml
Normal file
@ -0,0 +1,30 @@
|
||||
<?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/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
|
||||
%browserDTD;
|
||||
]>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<script src="chrome://browser/content/readinglist/sidebar.js" type="application/javascript;version=1.8"></script>
|
||||
<link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/readinglist/sidebar.css"/>
|
||||
<!-- <title>&readingList.label;</title> -->
|
||||
</head>
|
||||
|
||||
<body role="application">
|
||||
<template id="item-template">
|
||||
<div class="item" role="option" tabindex="-1">
|
||||
<div class="item-thumb-container"></div>
|
||||
<div class="item-summary-container">
|
||||
<div class="item-title"></div>
|
||||
<div class="item-domain"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="list" role="listbox" tabindex="1"></div>
|
||||
</body>
|
||||
</html>
|
159
browser/components/readinglist/test/ReadingListTestUtils.jsm
Normal file
159
browser/components/readinglist/test/ReadingListTestUtils.jsm
Normal file
@ -0,0 +1,159 @@
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"ReadingListTestUtils",
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
|
||||
|
||||
|
||||
/** Preference name controlling whether the ReadingList feature is enabled/disabled. */
|
||||
const PREF_RL_ENABLED = "browser.readinglist.enabled";
|
||||
|
||||
|
||||
/**
|
||||
* Utilities for testing the ReadingList sidebar.
|
||||
*/
|
||||
function SidebarUtils(window, assert) {
|
||||
this.window = window;
|
||||
this.Assert = assert;
|
||||
}
|
||||
|
||||
SidebarUtils.prototype = {
|
||||
/**
|
||||
* Reference to the RLSidebar object controlling the ReadingList sidebar UI.
|
||||
* @type {object}
|
||||
*/
|
||||
get RLSidebar() {
|
||||
return this.window.SidebarUI.browser.contentWindow.RLSidebar;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reference to the list container element in the sidebar.
|
||||
* @type {Element}
|
||||
*/
|
||||
get list() {
|
||||
return this.RLSidebar.list;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check that the number of elements in the list matches the expected count.
|
||||
* @param {number} count - Expected number of items.
|
||||
*/
|
||||
expectNumItems(count) {
|
||||
this.Assert.equal(this.list.childElementCount, count,
|
||||
"Should have expected number of items in the sidebar list");
|
||||
},
|
||||
|
||||
/**
|
||||
* Check all items in the sidebar list, ensuring the DOM matches the data.
|
||||
*/
|
||||
checkAllItems() {
|
||||
for (let itemNode of this.list.children) {
|
||||
this.checkSidebarItem(itemNode);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Run a series of sanity checks for an element in the list associated with
|
||||
* an Item, ensuring the DOM matches the data.
|
||||
*/
|
||||
checkItem(node) {
|
||||
let item = this.RLSidebar.getItemFromNode(node);
|
||||
|
||||
this.Assert.ok(node.classList.contains("item"),
|
||||
"Node should have .item class");
|
||||
this.Assert.equal(node.id, "item-" + item.id,
|
||||
"Node should have correct ID");
|
||||
this.Assert.equal(node.getAttribute("title"), item.title + "\n" + item.url.spec,
|
||||
"Node should have correct title attribute");
|
||||
this.Assert.equal(node.querySelector(".item-title").textContent, item.title,
|
||||
"Node's title element's text should match item title");
|
||||
this.Assert.equal(node.querySelector(".item-domain").textContent, item.domain,
|
||||
"Node's domain element's text should match item title");
|
||||
},
|
||||
|
||||
expectSelectedId(itemId) {
|
||||
let selectedItem = this.RLSidebar.selectedItem;
|
||||
if (itemId == null) {
|
||||
this.Assert.equal(selectedItem, null, "Should have no selected item");
|
||||
} else {
|
||||
this.Assert.notEqual(selectedItem, null, "selectedItem should not be null");
|
||||
let selectedId = this.RLSidebar.getItemIdFromNode(selectedItem);
|
||||
this.Assert.equal(itemId, selectedId, "Should have currect item selected");
|
||||
}
|
||||
},
|
||||
|
||||
expectActiveId(itemId) {
|
||||
let activeItem = this.RLSidebar.activeItem;
|
||||
if (itemId == null) {
|
||||
this.Assert.equal(activeItem, null, "Should have no active item");
|
||||
} else {
|
||||
this.Assert.notEqual(activeItem, null, "activeItem should not be null");
|
||||
let activeId = this.RLSidebar.getItemIdFromNode(activeItem);
|
||||
this.Assert.equal(itemId, activeId, "Should have correct item active");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Utilities for testing the ReadingList.
|
||||
*/
|
||||
this.ReadingListTestUtils = {
|
||||
/**
|
||||
* Whether the ReadingList feature is enabled or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled() {
|
||||
return Preferences.get(PREF_RL_ENABLED, false);
|
||||
},
|
||||
set enabled(value) {
|
||||
Preferences.set(PREF_RL_ENABLED, !!value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Utilities for testing the ReadingList sidebar.
|
||||
*/
|
||||
SidebarUtils: SidebarUtils,
|
||||
|
||||
/**
|
||||
* Synthetically add an item to the ReadingList.
|
||||
* @param {object|[object]} data - Object or array of objects to pass to the
|
||||
* Item constructor.
|
||||
* @return {Promise} Promise that gets fulfilled with the item or items added.
|
||||
*/
|
||||
addItem(data) {
|
||||
if (Array.isArray(data)) {
|
||||
let promises = [];
|
||||
for (let itemData of data) {
|
||||
promises.push(this.addItem(itemData));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
let item = new ReadingList.Item(data);
|
||||
ReadingList._items.push(item);
|
||||
ReadingList._notifyListeners("onItemAdded", item);
|
||||
resolve(item);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup all data, resetting to a blank state.
|
||||
*/
|
||||
cleanup() {
|
||||
return new Promise(resolve => {
|
||||
ReadingList._items = [];
|
||||
ReadingList._listeners.clear();
|
||||
Preferences.reset(PREF_RL_ENABLED);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
};
|
7
browser/components/readinglist/test/browser/browser.ini
Normal file
7
browser/components/readinglist/test/browser/browser.ini
Normal file
@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[browser_ui_enable_disable.js]
|
||||
[browser_sidebar_list.js]
|
||||
;[browser_sidebar_mouse_nav.js]
|
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* This tests the basic functionality of the sidebar to list items.
|
||||
*/
|
||||
|
||||
add_task(function*() {
|
||||
registerCleanupFunction(function*() {
|
||||
ReadingListUI.hideSidebar();
|
||||
yield RLUtils.cleanup();
|
||||
});
|
||||
|
||||
RLUtils.enabled = true;
|
||||
|
||||
yield ReadingListUI.showSidebar();
|
||||
let RLSidebar = RLSidebarUtils.RLSidebar;
|
||||
let sidebarDoc = SidebarUI.browser.contentDocument;
|
||||
Assert.equal(RLSidebar.numItems, 0, "Should start with no items");
|
||||
Assert.equal(RLSidebar.activeItem, null, "Should start with no active item");
|
||||
Assert.equal(RLSidebar.activeItem, null, "Should start with no selected item");
|
||||
|
||||
info("Adding first item");
|
||||
yield RLUtils.addItem({
|
||||
id: "c3502a49-bcef-4a94-b222-d4834463de33",
|
||||
url: "http://example.com/article1",
|
||||
title: "Article 1",
|
||||
});
|
||||
RLSidebarUtils.expectNumItems(1);
|
||||
|
||||
info("Adding more items");
|
||||
yield RLUtils.addItem([{
|
||||
id: "e054f5b7-1f4f-463f-bb96-d64c02448c31",
|
||||
url: "http://example.com/article2",
|
||||
title: "Article 2",
|
||||
}, {
|
||||
id: "4207230b-2364-4e97-9587-01312b0ce4e6",
|
||||
url: "http://example.com/article3",
|
||||
title: "Article 3",
|
||||
}]);
|
||||
RLSidebarUtils.expectNumItems(3);
|
||||
|
||||
info("Closing sidebar");
|
||||
ReadingListUI.hideSidebar();
|
||||
|
||||
info("Adding another item");
|
||||
yield RLUtils.addItem({
|
||||
id: "dae0e855-607e-4df3-b27f-73a5e35c94fe",
|
||||
url: "http://example.com/article4",
|
||||
title: "Article 4",
|
||||
});
|
||||
|
||||
info("Re-eopning sidebar");
|
||||
yield ReadingListUI.showSidebar();
|
||||
RLSidebarUtils.expectNumItems(4);
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Test mouse navigation for selecting items in the sidebar.
|
||||
*/
|
||||
|
||||
|
||||
function mouseInteraction(mouseEvent, responseEvent, itemNode) {
|
||||
let eventPromise = BrowserUITestUtils.waitForEvent(RLSidebarUtils.list, responseEvent);
|
||||
let details = {};
|
||||
if (mouseEvent != "click") {
|
||||
details.type = mouseEvent;
|
||||
}
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(itemNode, details, itemNode.ownerDocument.defaultView);
|
||||
return eventPromise;
|
||||
}
|
||||
|
||||
add_task(function*() {
|
||||
registerCleanupFunction(function*() {
|
||||
ReadingListUI.hideSidebar();
|
||||
yield RLUtils.cleanup();
|
||||
});
|
||||
|
||||
RLUtils.enabled = true;
|
||||
|
||||
let itemData = [{
|
||||
id: "00bd24c7-3629-40b0-acde-37aa81768735",
|
||||
url: "http://example.com/article1",
|
||||
title: "Article 1",
|
||||
}, {
|
||||
id: "28bf7f19-cf94-4ceb-876a-ac1878342e0d",
|
||||
url: "http://example.com/article2",
|
||||
title: "Article 2",
|
||||
}, {
|
||||
id: "7e5064ea-f45d-4fc7-8d8c-c067b7781e78",
|
||||
url: "http://example.com/article3",
|
||||
title: "Article 3",
|
||||
}, {
|
||||
id: "8e72a472-8db8-4904-ba39-9672f029e2d0",
|
||||
url: "http://example.com/article4",
|
||||
title: "Article 4",
|
||||
}, {
|
||||
id: "8d332744-37bc-4a1a-a26b-e9953b9f7d91",
|
||||
url: "http://example.com/article5",
|
||||
title: "Article 5",
|
||||
}];
|
||||
info("Adding initial mock data");
|
||||
yield RLUtils.addItem(itemData);
|
||||
|
||||
info("Opening sidebar");
|
||||
yield ReadingListUI.showSidebar();
|
||||
RLSidebarUtils.expectNumItems(5);
|
||||
RLSidebarUtils.expectSelectedId(null);
|
||||
RLSidebarUtils.expectActiveId(null);
|
||||
|
||||
info("Mouse move over item 1");
|
||||
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
|
||||
RLSidebarUtils.expectSelectedId(itemData[0].id);
|
||||
RLSidebarUtils.expectActiveId(null);
|
||||
|
||||
info("Mouse move over item 2");
|
||||
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[1]);
|
||||
RLSidebarUtils.expectSelectedId(itemData[1].id);
|
||||
RLSidebarUtils.expectActiveId(null);
|
||||
|
||||
info("Mouse move over item 5");
|
||||
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[4]);
|
||||
RLSidebarUtils.expectSelectedId(itemData[4].id);
|
||||
RLSidebarUtils.expectActiveId(null);
|
||||
|
||||
info("Mouse move over item 1 again");
|
||||
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
|
||||
RLSidebarUtils.expectSelectedId(itemData[0].id);
|
||||
RLSidebarUtils.expectActiveId(null);
|
||||
|
||||
info("Mouse click on item 1");
|
||||
yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[0]);
|
||||
RLSidebarUtils.expectSelectedId(itemData[0].id);
|
||||
RLSidebarUtils.expectActiveId(itemData[0].id);
|
||||
|
||||
info("Mouse click on item 3");
|
||||
yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[2]);
|
||||
RLSidebarUtils.expectSelectedId(itemData[2].id);
|
||||
RLSidebarUtils.expectActiveId(itemData[2].id);
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Test enabling/disabling the entire ReadingList feature via the
|
||||
* browser.readinglist.enabled preference.
|
||||
*/
|
||||
|
||||
function checkRLState() {
|
||||
let enabled = RLUtils.enabled;
|
||||
info("Checking ReadingList UI is " + (enabled ? "enabled" : "disabled"));
|
||||
|
||||
let sidebarBroadcaster = document.getElementById("readingListSidebar");
|
||||
let sidebarMenuitem = document.getElementById("menu_readingListSidebar");
|
||||
|
||||
if (enabled) {
|
||||
Assert.notEqual(sidebarBroadcaster.getAttribute("hidden"), "true",
|
||||
"Sidebar broadcaster should not be hidden");
|
||||
Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
|
||||
"Sidebar menuitem should be visible");
|
||||
} else {
|
||||
Assert.equal(sidebarBroadcaster.getAttribute("hidden"), "true",
|
||||
"Sidebar broadcaster should be hidden");
|
||||
Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true",
|
||||
"Sidebar menuitem should be hidden");
|
||||
Assert.equal(ReadingListUI.isSidebarOpen, false,
|
||||
"ReadingListUI should not think sidebar is open");
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
Assert.equal(SidebarUI.isOpen, false, "Sidebar should not be open");
|
||||
}
|
||||
}
|
||||
|
||||
add_task(function*() {
|
||||
info("Start with ReadingList disabled");
|
||||
RLUtils.enabled = false;
|
||||
checkRLState();
|
||||
info("Enabling ReadingList");
|
||||
RLUtils.enabled = true;
|
||||
checkRLState();
|
||||
|
||||
info("Opening ReadingList sidebar");
|
||||
yield ReadingListUI.showSidebar();
|
||||
Assert.ok(SidebarUI.isOpen, "Sidebar should be open");
|
||||
Assert.equal(SidebarUI.currentID, "readingListSidebar", "Sidebar should have ReadingList loaded");
|
||||
|
||||
info("Disabling ReadingList");
|
||||
RLUtils.enabled = false;
|
||||
Assert.ok(!SidebarUI.isOpen, "Sidebar should be closed");
|
||||
checkRLState();
|
||||
});
|
15
browser/components/readinglist/test/browser/head.js
Normal file
15
browser/components/readinglist/test/browser/head.js
Normal file
@ -0,0 +1,15 @@
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
|
||||
"resource:///modules/readinglist/ReadingList.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ReadingListTestUtils",
|
||||
"resource://testing-common/ReadingListTestUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITestUtils",
|
||||
"resource://testing-common/BrowserUITestUtils.jsm");
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "RLUtils", () => {
|
||||
return ReadingListTestUtils;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "RLSidebarUtils", () => {
|
||||
return new RLUtils.SidebarUtils(window, Assert);
|
||||
});
|
@ -2865,7 +2865,7 @@ let SessionStoreInternal = {
|
||||
}
|
||||
var sidebar = aWindow.document.getElementById("sidebar-box");
|
||||
if (sidebar.getAttribute("sidebarcommand") != aSidebar) {
|
||||
aWindow.toggleSidebar(aSidebar);
|
||||
aWindow.SidebarUI.show(aSidebar);
|
||||
}
|
||||
// since resizing/moving a window brings it to the foreground,
|
||||
// we might want to re-focus the last focused window
|
||||
|
@ -10,6 +10,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Console.jsm");
|
||||
|
||||
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
|
||||
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
@ -123,26 +124,26 @@ let EventsHandler = {
|
||||
_onTabNavigated: function(event, {isFrameSwitching}) {
|
||||
switch (event) {
|
||||
case "will-navigate": {
|
||||
Task.spawn(function*() {
|
||||
// Make sure the backend is prepared to handle WebGL contexts.
|
||||
if (!isFrameSwitching) {
|
||||
gFront.setup({ reload: false });
|
||||
}
|
||||
// Make sure the backend is prepared to handle WebGL contexts.
|
||||
if (!isFrameSwitching) {
|
||||
gFront.setup({ reload: false });
|
||||
}
|
||||
|
||||
// Reset UI.
|
||||
ShadersListView.empty();
|
||||
// When switching to an iframe, ensure displaying the reload button.
|
||||
// As the document has already been loaded without being hooked.
|
||||
if (isFrameSwitching) {
|
||||
$("#reload-notice").hidden = false;
|
||||
$("#waiting-notice").hidden = true;
|
||||
} else {
|
||||
$("#reload-notice").hidden = true;
|
||||
$("#waiting-notice").hidden = false;
|
||||
}
|
||||
|
||||
$("#content").hidden = true;
|
||||
window.emit(EVENTS.UI_RESET);
|
||||
|
||||
// Reset UI.
|
||||
ShadersListView.empty();
|
||||
// When switching to an iframe, ensure displaying the reload button.
|
||||
// As the document has already been loaded without being hooked.
|
||||
if (isFrameSwitching) {
|
||||
$("#reload-notice").hidden = false;
|
||||
$("#waiting-notice").hidden = true;
|
||||
} else {
|
||||
$("#reload-notice").hidden = true;
|
||||
$("#waiting-notice").hidden = false;
|
||||
}
|
||||
yield ShadersEditorsView.setText({ vs: "", fs: "" });
|
||||
$("#content").hidden = true;
|
||||
}).then(() => window.emit(EVENTS.UI_RESET));
|
||||
break;
|
||||
}
|
||||
case "navigate": {
|
||||
@ -370,7 +371,7 @@ let ShadersEditorsView = {
|
||||
*/
|
||||
destroy: Task.async(function*() {
|
||||
this._destroyed = true;
|
||||
this._toggleListeners("off");
|
||||
yield this._toggleListeners("off");
|
||||
for (let p of this._editorPromises.values()) {
|
||||
let editor = yield p;
|
||||
editor.destroy();
|
||||
@ -414,9 +415,6 @@ let ShadersEditorsView = {
|
||||
* Returns a promise that resolves to an editor instance
|
||||
*/
|
||||
_getEditor: function(type) {
|
||||
if ($("#content").hidden) {
|
||||
return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created."));
|
||||
}
|
||||
if (this._editorPromises.has(type)) {
|
||||
return this._editorPromises.get(type);
|
||||
}
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if the shader editor leaks on initialization and sudden destruction.
|
||||
* You can also use this initialization format as a template for other tests.
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if the editors contain the correct text when a program
|
||||
* becomes available.
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if error indicators are shown in the editor's gutter and text area
|
||||
* when there's a shader compilation error.
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if error tooltips can be opened from the editor's gutter when there's
|
||||
* a shader compilation error.
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if source editors are lazily initialized.
|
||||
*/
|
||||
@ -16,20 +9,6 @@ function ifWebGLSupported() {
|
||||
let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
|
||||
let { gFront, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
try {
|
||||
yield ShadersEditorsView._getEditor("vs");
|
||||
ok(false, "The promise for a vertex shader editor should be rejected.");
|
||||
} catch (e) {
|
||||
ok(true, "The vertex shader editors wasn't initialized.");
|
||||
}
|
||||
|
||||
try {
|
||||
yield ShadersEditorsView._getEditor("fs");
|
||||
ok(false, "The promise for a fragment shader editor should be rejected.");
|
||||
} catch (e) {
|
||||
ok(true, "The fragment shader editors wasn't initialized.");
|
||||
}
|
||||
|
||||
reload(target);
|
||||
yield once(gFront, "program-linked");
|
||||
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if the shader editor shows the appropriate UI when opened.
|
||||
*/
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests target navigations are handled correctly in the UI.
|
||||
*/
|
||||
@ -48,8 +41,7 @@ function ifWebGLSupported() {
|
||||
let navigated = once(target, "will-navigate");
|
||||
navigate(target, "about:blank");
|
||||
|
||||
yield navigating;
|
||||
yield once(panel.panelWin, EVENTS.UI_RESET);
|
||||
yield promise.all([navigating, once(panel.panelWin, EVENTS.UI_RESET) ]);
|
||||
|
||||
is($("#reload-notice").hidden, true,
|
||||
"The 'reload this page' notice should be hidden while navigating.");
|
||||
@ -65,18 +57,6 @@ function ifWebGLSupported() {
|
||||
is(ShadersListView.selectedIndex, -1,
|
||||
"The shaders list has a negative index.");
|
||||
|
||||
yield ShadersEditorsView._getEditor("vs").then(() => {
|
||||
ok(false, "The promise for a vertex shader editor should be rejected.");
|
||||
}, () => {
|
||||
ok(true, "The vertex shader editors wasn't initialized.");
|
||||
});
|
||||
|
||||
yield ShadersEditorsView._getEditor("fs").then(() => {
|
||||
ok(false, "The promise for a fragment shader editor should be rejected.");
|
||||
}, () => {
|
||||
ok(true, "The fragment shader editors wasn't initialized.");
|
||||
});
|
||||
|
||||
yield navigated;
|
||||
|
||||
is($("#reload-notice").hidden, true,
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if blackboxing a program works properly.
|
||||
*/
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if blackboxing a program works properly in tandem with blended
|
||||
* overlapping geometry.
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if the programs list contains an entry after vertex and fragment
|
||||
* shaders are linked.
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if editing a vertex and a fragment shader works properly.
|
||||
*/
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if compile or linkage errors are emitted when a shader source
|
||||
* gets malformed after being edited.
|
||||
|
@ -1,13 +1,6 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
///////////////////
|
||||
//
|
||||
// Whitelisting this test.
|
||||
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
|
||||
//
|
||||
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
|
||||
|
||||
/**
|
||||
* Tests if editing a vertex and a fragment shader would permanently store
|
||||
* their new source on the backend and reshow it in the frontend when required.
|
||||
|
@ -838,6 +838,8 @@ just addresses the organization to follow, e.g. "This site is run by " -->
|
||||
<!ENTITY processHang.terminateProcess.label "Kill Web Process">
|
||||
<!ENTITY processHang.terminateProcess.accessKey "K">
|
||||
|
||||
<!ENTITY readingList.label "Reading List">
|
||||
<!ENTITY readingList.sidebar.commandKey "R">
|
||||
<!ENTITY emeLearnMoreContextMenu.label "Learn more about DRM…">
|
||||
<!ENTITY emeLearnMoreContextMenu.accesskey "D">
|
||||
|
||||
|
@ -28,6 +28,8 @@ let ReaderParent = {
|
||||
"Reader:Share",
|
||||
"Reader:SystemUIVisibility",
|
||||
"Reader:UpdateReaderButton",
|
||||
"Reader:SetIntPref",
|
||||
"Reader:SetCharPref",
|
||||
],
|
||||
|
||||
init: function() {
|
||||
@ -80,6 +82,18 @@ let ReaderParent = {
|
||||
this.updateReaderButton(browser);
|
||||
break;
|
||||
}
|
||||
case "Reader:SetIntPref": {
|
||||
if (message.data && message.data.name !== undefined) {
|
||||
Services.prefs.setIntPref(message.data.name, message.data.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Reader:SetCharPref": {
|
||||
if (message.data && message.data.name !== undefined) {
|
||||
Services.prefs.setCharPref(message.data.name, message.data.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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/sidebar.css (../shared/readinglist/sidebar.css)
|
||||
skin/classic/browser/webRTC-shareDevice-16.png
|
||||
skin/classic/browser/webRTC-shareDevice-64.png
|
||||
skin/classic/browser/webRTC-sharingDevice-16.png (../shared/webrtc/webRTC-sharingDevice-16.png)
|
||||
|
@ -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/sidebar.css (../shared/readinglist/sidebar.css)
|
||||
skin/classic/browser/webRTC-shareDevice-16.png
|
||||
skin/classic/browser/webRTC-shareDevice-16@2x.png
|
||||
skin/classic/browser/webRTC-shareDevice-64.png
|
||||
|
72
browser/themes/shared/readinglist/sidebar.css
Normal file
72
browser/themes/shared/readinglist/sidebar.css
Normal file
@ -0,0 +1,72 @@
|
||||
/* 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/. */
|
||||
|
||||
:root, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font: message-box;
|
||||
background: #F8F7F8;
|
||||
color: #333333;
|
||||
-moz-user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#list {
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.item.active {
|
||||
background: #FEFEFE;
|
||||
}
|
||||
|
||||
.item.selected {
|
||||
background: #FDFDFD;
|
||||
}
|
||||
|
||||
.item-thumb-container {
|
||||
min-width: 64px;
|
||||
max-width: 64px;
|
||||
min-height: 40px;
|
||||
max-height: 40px;
|
||||
background: #EBEBEB;
|
||||
border: 1px solid white;
|
||||
box-shadow: 0px 1px 2px rgba(0,0,0,.35);
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.item-summary-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
-moz-padding-start: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
overflow: hidden;
|
||||
height: 2.8em;
|
||||
}
|
||||
|
||||
.item-domain {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-height: 1.4em;
|
||||
color: #0095DD;
|
||||
}
|
||||
|
||||
.item:hover .item-domain {
|
||||
color: #008ACB;
|
||||
}
|
@ -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/sidebar.css (../shared/readinglist/sidebar.css)
|
||||
skin/classic/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png)
|
||||
skin/classic/browser/notification-pluginAlert.png (../shared/plugins/notification-pluginAlert.png)
|
||||
skin/classic/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png)
|
||||
@ -575,6 +576,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/sidebar.css (../shared/readinglist/sidebar.css)
|
||||
skin/classic/aero/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png)
|
||||
skin/classic/aero/browser/notification-pluginAlert.png (../shared/plugins/notification-pluginAlert.png)
|
||||
skin/classic/aero/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png)
|
||||
|
@ -10,6 +10,7 @@
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "nsLayoutUtils.h"
|
||||
#include "nsGkAtoms.h"
|
||||
#include "nsFontMetrics.h"
|
||||
#include "nsPrintfCString.h"
|
||||
#include "mozilla/dom/Element.h"
|
||||
#include "nsRegion.h"
|
||||
@ -76,6 +77,7 @@ struct EventRadiusPrefs
|
||||
bool mTouchOnly;
|
||||
bool mRepositionEventCoords;
|
||||
bool mTouchClusterDetection;
|
||||
uint32_t mLimitReadableSize;
|
||||
};
|
||||
|
||||
static EventRadiusPrefs sMouseEventRadiusPrefs;
|
||||
@ -125,6 +127,9 @@ GetPrefsFor(EventClassID aEventClassID)
|
||||
|
||||
nsPrintfCString touchClusterPref("ui.zoomedview.enabled", prefBranch);
|
||||
Preferences::AddBoolVarCache(&prefs->mTouchClusterDetection, touchClusterPref.get(), false);
|
||||
|
||||
nsPrintfCString limitReadableSizePref("ui.zoomedview.limitReadableSize", prefBranch);
|
||||
Preferences::AddUintVarCache(&prefs->mLimitReadableSize, limitReadableSizePref.get(), 8);
|
||||
}
|
||||
|
||||
return prefs;
|
||||
@ -382,6 +387,46 @@ GetClosest(nsIFrame* aRoot, const nsPoint& aPointRelativeToRootFrame,
|
||||
return bestTarget;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return always true when touch cluster detection is OFF.
|
||||
* When cluster detection is ON, return true if the text inside
|
||||
* the frame is readable (by human eyes):
|
||||
* in this case, the frame is really clickable.
|
||||
* Frames with a too small size will return false:
|
||||
* in this case, the frame is considered not clickable.
|
||||
*/
|
||||
static bool
|
||||
IsElementClickableAndReadable(nsIFrame* aFrame, WidgetGUIEvent* aEvent, const EventRadiusPrefs* aPrefs)
|
||||
{
|
||||
if (!aPrefs->mTouchClusterDetection) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aEvent->mClass != eMouseEventClass) {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t limitReadableSize = aPrefs->mLimitReadableSize;
|
||||
nsSize frameSize = aFrame->GetSize();
|
||||
nsPresContext* pc = aFrame->PresContext();
|
||||
nsIPresShell* presShell = pc->PresShell();
|
||||
gfxSize cumulativeResolution = presShell->GetCumulativeResolution();
|
||||
if ((pc->AppUnitsToGfxUnits(frameSize.height) * cumulativeResolution.height) < limitReadableSize ||
|
||||
(pc->AppUnitsToGfxUnits(frameSize.width) * cumulativeResolution.width) < limitReadableSize) {
|
||||
return false;
|
||||
}
|
||||
nsRefPtr<nsFontMetrics> fm;
|
||||
nsLayoutUtils::GetFontMetricsForFrame(aFrame, getter_AddRefs(fm),
|
||||
nsLayoutUtils::FontSizeInflationFor(aFrame));
|
||||
if (fm) {
|
||||
if ((pc->AppUnitsToGfxUnits(fm->EmHeight()) * cumulativeResolution.height) < limitReadableSize) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
nsIFrame*
|
||||
FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent,
|
||||
nsIFrame* aRootFrame,
|
||||
@ -398,8 +443,15 @@ FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent,
|
||||
mozilla::layers::Stringify(aPointRelativeToRootFrame).c_str(), aRootFrame);
|
||||
|
||||
const EventRadiusPrefs* prefs = GetPrefsFor(aEvent->mClass);
|
||||
if (!prefs || !prefs->mEnabled || (target && IsElementClickable(target, nsGkAtoms::body))) {
|
||||
PET_LOG("Retargeting disabled or target %p is clickable\n", target);
|
||||
if (!prefs || !prefs->mEnabled) {
|
||||
PET_LOG("Retargeting disabled\n");
|
||||
return target;
|
||||
}
|
||||
if (target && IsElementClickable(target, nsGkAtoms::body)) {
|
||||
if (!IsElementClickableAndReadable(target, aEvent, prefs)) {
|
||||
aEvent->AsMouseEventBase()->hitCluster = true;
|
||||
}
|
||||
PET_LOG("Target %p is clickable\n", target);
|
||||
return target;
|
||||
}
|
||||
|
||||
@ -437,7 +489,8 @@ FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent,
|
||||
GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs,
|
||||
restrictToDescendants, candidates, &elementsInCluster);
|
||||
if (closestClickable) {
|
||||
if (prefs->mTouchClusterDetection && elementsInCluster > 1) {
|
||||
if ((prefs->mTouchClusterDetection && elementsInCluster > 1) ||
|
||||
(!IsElementClickableAndReadable(closestClickable, aEvent, prefs))) {
|
||||
if (aEvent->mClass == eMouseEventClass) {
|
||||
WidgetMouseEventBase* mouseEventBase = aEvent->AsMouseEventBase();
|
||||
mouseEventBase->hitCluster = true;
|
||||
|
@ -402,6 +402,7 @@ pref("font.size.inflation.minTwips", 120);
|
||||
pref("browser.ui.zoom.force-user-scalable", false);
|
||||
|
||||
pref("ui.zoomedview.enabled", false);
|
||||
pref("ui.zoomedview.limitReadableSize", 8); // value in layer pixels
|
||||
|
||||
pref("ui.touch.radius.enabled", false);
|
||||
pref("ui.touch.radius.leftmm", 3);
|
||||
|
@ -40,12 +40,13 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
LayerView.ZoomedViewListener, GeckoEventListener {
|
||||
private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
|
||||
|
||||
private static final int ZOOM_FACTOR = 2;
|
||||
private static final int DEFAULT_ZOOM_FACTOR = 3;
|
||||
private static final int W_CAPTURED_VIEW_IN_PERCENT = 80;
|
||||
private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
|
||||
private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
|
||||
private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
|
||||
|
||||
private int zoomFactor;
|
||||
private ImageView zoomedImageView;
|
||||
private LayerView layerView;
|
||||
private int viewWidth;
|
||||
@ -86,6 +87,8 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
if (dragged) {
|
||||
dragged = false;
|
||||
} else {
|
||||
GeckoEvent eClickInZoomedView = GeckoEvent.createBroadcastEvent("Gesture:ClickInZoomedView", "");
|
||||
GeckoAppShell.sendEventToGecko(eClickInZoomedView);
|
||||
layerView.dispatchTouchEvent(actionDownEvent);
|
||||
actionDownEvent.recycle();
|
||||
PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
|
||||
@ -184,7 +187,7 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
final float parentHeight = metrics.getHeight();
|
||||
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
returnValue.x = (int) ((x / ZOOM_FACTOR) + // Conversion of the x offset inside the zoomed view (using the scale factor)
|
||||
returnValue.x = (int) ((x / zoomFactor) + // Conversion of the x offset inside the zoomed view (using the scale factor)
|
||||
|
||||
offset.x + // The offset of the layerView
|
||||
|
||||
@ -192,17 +195,17 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
* Minimum value for the left side of the zoomed view is 0
|
||||
* and we return 0 after conversion
|
||||
* Maximum value for the left side of the zoomed view is (parentWidth - offset.x - viewWidth)
|
||||
* and we return (parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) after conversion.
|
||||
* and we return (parentWidth - offset.x - (viewWidth / zoomFactor)) after conversion.
|
||||
*/
|
||||
(((float) params.leftMargin) - offset.x) *
|
||||
((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
|
||||
((parentWidth - offset.x - (viewWidth / zoomFactor)) /
|
||||
(parentWidth - offset.x - viewWidth)));
|
||||
|
||||
// Same comments here vertically
|
||||
returnValue.y = (int) ((y / ZOOM_FACTOR) +
|
||||
returnValue.y = (int) ((y / zoomFactor) +
|
||||
offset.y +
|
||||
(((float) params.topMargin) - offset.y) *
|
||||
((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
|
||||
((parentHeight - offset.y - (viewHeight / zoomFactor)) /
|
||||
(parentHeight - offset.y - viewHeight)));
|
||||
|
||||
return returnValue;
|
||||
@ -219,7 +222,7 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
final float parentWidth = metrics.getWidth();
|
||||
final float parentHeight = metrics.getHeight();
|
||||
|
||||
returnValue.x = (int) ((((x - (viewWidth / (2 * ZOOM_FACTOR)))) / // Translation to get the left side position of the zoomed view
|
||||
returnValue.x = (int) ((((x - (viewWidth / (2 * zoomFactor)))) / // Translation to get the left side position of the zoomed view
|
||||
// centered on x (the value 2 to get the middle).
|
||||
|
||||
/* Conversion of the left side position of the zoomed view.
|
||||
@ -228,14 +231,14 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
* and not in a multiplication to convert the position from
|
||||
* the LayerView to the ZoomedView.
|
||||
*/
|
||||
((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
|
||||
((parentWidth - offset.x - (viewWidth / zoomFactor)) /
|
||||
(parentWidth - offset.x - viewWidth)))
|
||||
|
||||
+ offset.x); // The offset of the layerView
|
||||
|
||||
// Same comments here vertically
|
||||
returnValue.y = (int) ((((y - (viewHeight / (2 * ZOOM_FACTOR)))) /
|
||||
((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
|
||||
returnValue.y = (int) ((((y - (viewHeight / (2 * zoomFactor)))) /
|
||||
((parentHeight - offset.y - (viewHeight / zoomFactor)) /
|
||||
(parentHeight - offset.y - viewHeight)))
|
||||
+ offset.y);
|
||||
|
||||
@ -296,8 +299,16 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
|
||||
private void setCapturedSize(ImmutableViewportMetrics metrics) {
|
||||
float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
|
||||
viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR;
|
||||
viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR;
|
||||
// For metrics.zoomFactor lower than 1, the zoom factor of the zoomed view is calculated
|
||||
// to get always the same size for the content in the zoomed view.
|
||||
// For metrics.zoomFactor greater than 1, the zoom factor is always set to the default
|
||||
// value DEFAULT_ZOOM_FACTOR, thus the zoomed view is always a zoom of the normal view.
|
||||
zoomFactor = Math.max(DEFAULT_ZOOM_FACTOR, (int) (DEFAULT_ZOOM_FACTOR / metrics.zoomFactor));
|
||||
viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor;
|
||||
viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor;
|
||||
// Display in zoomedview is corrupted when width is an odd number
|
||||
// More details about this issue here: bug 776906 comment 11
|
||||
viewWidth &= ~0x1;
|
||||
}
|
||||
|
||||
private void shouldBlockUpdate(boolean shouldBlockUpdate) {
|
||||
@ -474,7 +485,7 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
|
||||
final int yPos = (int) (origin.y - offset.y) + lastPosition.y;
|
||||
|
||||
GeckoEvent e = GeckoEvent.createZoomedViewEvent(tabId, xPos, yPos, viewWidth,
|
||||
viewHeight, ZOOM_FACTOR * metrics.zoomFactor, buffer);
|
||||
viewHeight, zoomFactor * metrics.zoomFactor, buffer);
|
||||
GeckoAppShell.sendEventToGecko(e);
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,18 @@ let Reader = {
|
||||
this.updatePageAction(tab);
|
||||
break;
|
||||
}
|
||||
case "Reader:SetIntPref": {
|
||||
if (message.data && message.data.name !== undefined) {
|
||||
Services.prefs.setIntPref(message.data.name, message.data.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Reader:SetCharPref": {
|
||||
if (message.data && message.data.name !== undefined) {
|
||||
Services.prefs.setCharPref(message.data.name, message.data.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -182,6 +182,8 @@ lazilyLoadedObserverScripts.forEach(function (aScript) {
|
||||
"Reader:ToolbarHidden",
|
||||
"Reader:SystemUIVisibility",
|
||||
"Reader:UpdateReaderButton",
|
||||
"Reader:SetIntPref",
|
||||
"Reader:SetCharPref",
|
||||
], "chrome://browser/content/Reader.js"],
|
||||
].forEach(aScript => {
|
||||
let [name, messages, script] = aScript;
|
||||
@ -4908,8 +4910,10 @@ Tab.prototype = {
|
||||
|
||||
var BrowserEventHandler = {
|
||||
init: function init() {
|
||||
this._clickInZoomedView = false;
|
||||
Services.obs.addObserver(this, "Gesture:SingleTap", false);
|
||||
Services.obs.addObserver(this, "Gesture:CancelTouch", false);
|
||||
Services.obs.addObserver(this, "Gesture:ClickInZoomedView", false);
|
||||
Services.obs.addObserver(this, "Gesture:DoubleTap", false);
|
||||
Services.obs.addObserver(this, "Gesture:Scroll", false);
|
||||
Services.obs.addObserver(this, "dom-touch-listener-added", false);
|
||||
@ -5103,6 +5107,10 @@ var BrowserEventHandler = {
|
||||
this._cancelTapHighlight();
|
||||
break;
|
||||
|
||||
case "Gesture:ClickInZoomedView":
|
||||
this._clickInZoomedView = true;
|
||||
break;
|
||||
|
||||
case "Gesture:SingleTap": {
|
||||
try {
|
||||
// If the element was previously focused, show the caret attached to it.
|
||||
@ -5120,7 +5128,7 @@ var BrowserEventHandler = {
|
||||
let data = JSON.parse(aData);
|
||||
let {x, y} = data;
|
||||
|
||||
if (this._inCluster) {
|
||||
if (this._inCluster && this._clickInZoomedView != true) {
|
||||
this._clusterClicked(x, y);
|
||||
} else {
|
||||
// The _highlightElement was chosen after fluffing the touch events
|
||||
@ -5130,6 +5138,7 @@ var BrowserEventHandler = {
|
||||
this._sendMouseEvent("mousedown", x, y);
|
||||
this._sendMouseEvent("mouseup", x, y);
|
||||
}
|
||||
this._clickInZoomedView = false;
|
||||
// scrollToFocusedInput does its own checks to find out if an element should be zoomed into
|
||||
BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
|
||||
|
||||
|
@ -301,7 +301,10 @@ AboutReader.prototype = {
|
||||
this._fontSize = newFontSize;
|
||||
htmlClasses.add("font-size" + this._fontSize);
|
||||
|
||||
Services.prefs.setIntPref("reader.font_size", this._fontSize);
|
||||
this._mm.sendAsyncMessage("Reader:SetIntPref", {
|
||||
name: "reader.font_size",
|
||||
value: this._fontSize
|
||||
});
|
||||
},
|
||||
|
||||
_handleDeviceLight: function Reader_handleDeviceLight(newLux) {
|
||||
@ -392,7 +395,10 @@ AboutReader.prototype = {
|
||||
this._enableAmbientLighting(colorSchemePref === "auto");
|
||||
this._setColorScheme(colorSchemePref);
|
||||
|
||||
Services.prefs.setCharPref("reader.color_scheme", colorSchemePref);
|
||||
this._mm.sendAsyncMessage("Reader:SetCharPref", {
|
||||
name: "reader.color_scheme",
|
||||
value: colorSchemePref
|
||||
});
|
||||
},
|
||||
|
||||
_setFontType: function Reader_setFontType(newFontType) {
|
||||
@ -407,7 +413,10 @@ AboutReader.prototype = {
|
||||
this._fontType = newFontType;
|
||||
bodyClasses.add(this._fontType);
|
||||
|
||||
Services.prefs.setCharPref("reader.font_type", this._fontType);
|
||||
this._mm.sendAsyncMessage("Reader:SetCharPref", {
|
||||
name: "reader.font_type",
|
||||
value: this._fontType
|
||||
});
|
||||
},
|
||||
|
||||
_getToolbarVisibility: function Reader_getToolbarVisibility() {
|
||||
|
@ -16,6 +16,7 @@ DIRS += [
|
||||
'security',
|
||||
'server',
|
||||
'sourcemap',
|
||||
'shared',
|
||||
'styleinspector',
|
||||
'tern',
|
||||
'transport',
|
||||
|
193
toolkit/devtools/shared/async-storage.js
Normal file
193
toolkit/devtools/shared/async-storage.js
Normal file
@ -0,0 +1,193 @@
|
||||
/**
|
||||
*
|
||||
* Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/shared/js/async_storage.js
|
||||
* (converted to use Promises instead of callbacks).
|
||||
*
|
||||
* This file defines an asynchronous version of the localStorage API, backed by
|
||||
* an IndexedDB database. It creates a global asyncStorage object that has
|
||||
* methods like the localStorage object.
|
||||
*
|
||||
* To store a value use setItem:
|
||||
*
|
||||
* asyncStorage.setItem("key", "value");
|
||||
*
|
||||
* This returns a promise in case you want confirmation that the value has been stored.
|
||||
*
|
||||
* asyncStorage.setItem("key", "newvalue").then(function() {
|
||||
* console.log("new value stored");
|
||||
* });
|
||||
*
|
||||
* To read a value, call getItem(), but note that you must wait for a promise
|
||||
* resolution for the value to be retrieved.
|
||||
*
|
||||
* asyncStorage.getItem("key").then(function(value) {
|
||||
* console.log("The value of key is:", value);
|
||||
* });
|
||||
*
|
||||
* Note that unlike localStorage, asyncStorage does not allow you to store and
|
||||
* retrieve values by setting and querying properties directly. You cannot just
|
||||
* write asyncStorage.key; you have to explicitly call setItem() or getItem().
|
||||
*
|
||||
* removeItem(), clear(), length(), and key() are like the same-named methods of
|
||||
* localStorage, and all return a promise.
|
||||
*
|
||||
* The asynchronous nature of getItem() makes it tricky to retrieve multiple
|
||||
* values. But unlike localStorage, asyncStorage does not require the values you
|
||||
* store to be strings. So if you need to save multiple values and want to
|
||||
* retrieve them together, in a single asynchronous operation, just group the
|
||||
* values into a single object. The properties of this object may not include
|
||||
* DOM elements, but they may include things like Blobs and typed arrays.
|
||||
*
|
||||
*/
|
||||
const {Cc, Ci, Cu, Cr} = require("chrome");
|
||||
const {indexedDB} = require("sdk/indexed-db");
|
||||
const {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
|
||||
module.exports = (function() {
|
||||
"use strict";
|
||||
|
||||
var DBNAME = "devtools-async-storage";
|
||||
var DBVERSION = 1;
|
||||
var STORENAME = "keyvaluepairs";
|
||||
var db = null;
|
||||
|
||||
function withStore(type, onsuccess, onerror) {
|
||||
if (db) {
|
||||
var transaction = db.transaction(STORENAME, type);
|
||||
var store = transaction.objectStore(STORENAME);
|
||||
onsuccess(store);
|
||||
} else {
|
||||
var openreq = indexedDB.open(DBNAME, DBVERSION);
|
||||
openreq.onerror = function withStoreOnError() {
|
||||
onerror();
|
||||
};
|
||||
openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
|
||||
// First time setup: create an empty object store
|
||||
openreq.result.createObjectStore(STORENAME);
|
||||
};
|
||||
openreq.onsuccess = function withStoreOnSuccess() {
|
||||
db = openreq.result;
|
||||
var transaction = db.transaction(STORENAME, type);
|
||||
var store = transaction.objectStore(STORENAME);
|
||||
onsuccess(store);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getItem(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var req;
|
||||
withStore("readonly", (store) => {
|
||||
store.transaction.oncomplete = function onComplete() {
|
||||
var value = req.result;
|
||||
if (value === undefined) {
|
||||
value = null;
|
||||
}
|
||||
resolve(value);
|
||||
};
|
||||
req = store.get(key);
|
||||
req.onerror = function getItemOnError() {
|
||||
reject("Error in asyncStorage.getItem(): ", req.error.name);
|
||||
};
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function setItem(key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
withStore("readwrite", (store) => {
|
||||
store.transaction.oncomplete = resolve;
|
||||
var req = store.put(value, key);
|
||||
req.onerror = function setItemOnError() {
|
||||
reject("Error in asyncStorage.setItem(): ", req.error.name);
|
||||
};
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function removeItem(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
withStore("readwrite", (store) => {
|
||||
store.transaction.oncomplete = resolve;
|
||||
var req = store.delete(key);
|
||||
req.onerror = function removeItemOnError() {
|
||||
reject("Error in asyncStorage.removeItem(): ", req.error.name);
|
||||
};
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
return new Promise((resolve, reject) => {
|
||||
withStore("readwrite", (store) => {
|
||||
store.transaction.oncomplete = resolve;
|
||||
var req = store.clear();
|
||||
req.onerror = function clearOnError() {
|
||||
reject("Error in asyncStorage.clear(): ", req.error.name);
|
||||
};
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function length() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var req;
|
||||
withStore("readonly", (store) => {
|
||||
store.transaction.oncomplete = function onComplete() {
|
||||
resolve(req.result);
|
||||
}
|
||||
req = store.count();
|
||||
req.onerror = function lengthOnError() {
|
||||
reject("Error in asyncStorage.length(): ", req.error.name);
|
||||
};
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function key(n) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (n < 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var req;
|
||||
withStore("readonly", (store) => {
|
||||
store.transaction.oncomplete = function onComplete() {
|
||||
var cursor = req.result;
|
||||
resolve(cursor ? cursor.key : null);
|
||||
};
|
||||
var advanced = false;
|
||||
req = store.openCursor();
|
||||
req.onsuccess = function keyOnSuccess() {
|
||||
var cursor = req.result;
|
||||
if (!cursor) {
|
||||
// this means there weren"t enough keys
|
||||
return;
|
||||
}
|
||||
if (n === 0 || advanced) {
|
||||
// Either 1) we have the first key, return it if that's what they
|
||||
// wanted, or 2) we"ve got the nth key.
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, ask the cursor to skip ahead n records
|
||||
advanced = true;
|
||||
cursor.advance(n);
|
||||
};
|
||||
req.onerror = function keyOnError() {
|
||||
reject("Error in asyncStorage.key(): ", req.error.name);
|
||||
};
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
getItem: getItem,
|
||||
setItem: setItem,
|
||||
removeItem: removeItem,
|
||||
clear: clear,
|
||||
length: length,
|
||||
key: key
|
||||
};
|
||||
}());
|
11
toolkit/devtools/shared/moz.build
Normal file
11
toolkit/devtools/shared/moz.build
Normal file
@ -0,0 +1,11 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
|
||||
|
||||
EXTRA_JS_MODULES.devtools.shared += [
|
||||
'async-storage.js'
|
||||
]
|
6
toolkit/devtools/shared/tests/browser/browser.ini
Normal file
6
toolkit/devtools/shared/tests/browser/browser.ini
Normal file
@ -0,0 +1,6 @@
|
||||
[DEFAULT]
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
../../../server/tests/browser/head.js
|
||||
|
||||
[browser_async_storage.js]
|
@ -0,0 +1,77 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test the basic functionality of async-storage.
|
||||
// Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/apps/sharedtest/test/unit/async_storage_test.js.
|
||||
|
||||
const asyncStorage = require("devtools/toolkit/shared/async-storage");
|
||||
add_task(function*() {
|
||||
is(typeof asyncStorage.length, 'function', "API exists.");
|
||||
is(typeof asyncStorage.key, 'function', "API exists.");
|
||||
is(typeof asyncStorage.getItem, 'function', "API exists.");
|
||||
is(typeof asyncStorage.setItem, 'function', "API exists.");
|
||||
is(typeof asyncStorage.removeItem, 'function', "API exists.");
|
||||
is(typeof asyncStorage.clear, 'function', "API exists.");
|
||||
});
|
||||
|
||||
add_task(function*() {
|
||||
yield asyncStorage.setItem('foo', 'bar');
|
||||
let value = yield asyncStorage.getItem('foo');
|
||||
is(value, 'bar', 'value is correct');
|
||||
yield asyncStorage.setItem('foo', 'overwritten');
|
||||
value = yield asyncStorage.getItem('foo');
|
||||
is(value, 'overwritten', 'value is correct');
|
||||
yield asyncStorage.removeItem('foo');
|
||||
value = yield asyncStorage.getItem('foo');
|
||||
is(value, null, 'value is correct');
|
||||
});
|
||||
|
||||
add_task(function*() {
|
||||
var object = {
|
||||
x: 1,
|
||||
y: 'foo',
|
||||
z: true
|
||||
};
|
||||
|
||||
yield asyncStorage.setItem('myobj', object);
|
||||
let value = yield asyncStorage.getItem('myobj');
|
||||
is(object.x, value.x, 'value is correct');
|
||||
is(object.y, value.y, 'value is correct');
|
||||
is(object.z, value.z, 'value is correct');
|
||||
yield asyncStorage.removeItem('myobj');
|
||||
value = yield asyncStorage.getItem('myobj');
|
||||
is(value, null, 'value is correct');
|
||||
});
|
||||
|
||||
add_task(function*() {
|
||||
yield asyncStorage.clear();
|
||||
let len = yield asyncStorage.length();
|
||||
is(len, 0, 'length is correct');
|
||||
yield asyncStorage.setItem('key1', 'value1');
|
||||
len = yield asyncStorage.length();
|
||||
is(len, 1, 'length is correct');
|
||||
yield asyncStorage.setItem('key2', 'value2');
|
||||
len = yield asyncStorage.length();
|
||||
is(len, 2, 'length is correct');
|
||||
yield asyncStorage.setItem('key3', 'value3');
|
||||
len = yield asyncStorage.length();
|
||||
is(len, 3, 'length is correct');
|
||||
|
||||
let key = yield asyncStorage.key(0);
|
||||
is(key, 'key1', 'key is correct');
|
||||
key = yield asyncStorage.key(1);
|
||||
is(key, 'key2', 'key is correct');
|
||||
key = yield asyncStorage.key(2);
|
||||
is(key, 'key3', 'key is correct');
|
||||
key = yield asyncStorage.key(3);
|
||||
is(key, null, 'key is correct');
|
||||
yield asyncStorage.clear();
|
||||
key = yield asyncStorage.key(0);
|
||||
is(key, null, 'key is correct');
|
||||
|
||||
len = yield asyncStorage.length();
|
||||
is(len, 0, 'length is correct');
|
||||
});
|
@ -44,6 +44,20 @@ function run_test() {
|
||||
regSvc.close();
|
||||
}
|
||||
|
||||
let isLinux = ("@mozilla.org/gio-service;1" in Components.classes);
|
||||
if (isLinux) {
|
||||
// Check mailto handler from GIO
|
||||
// If there isn't one, then we have no mailto handler
|
||||
let gIOSvc = Cc["@mozilla.org/gio-service;1"].
|
||||
createInstance(Ci.nsIGIOService);
|
||||
try {
|
||||
gIOSvc.getAppForURIScheme("mailto");
|
||||
noMailto = false;
|
||||
} catch (ex) {
|
||||
noMailto = true;
|
||||
}
|
||||
}
|
||||
|
||||
//**************************************************************************//
|
||||
// Sample Data
|
||||
|
||||
@ -157,7 +171,7 @@ function run_test() {
|
||||
else
|
||||
do_check_eq(0, protoInfo.possibleApplicationHandlers.length);
|
||||
|
||||
// Win7+ might not have a default mailto: handler
|
||||
// Win7+ or Linux's GIO might not have a default mailto: handler
|
||||
if (noMailto)
|
||||
do_check_true(protoInfo.alwaysAskBeforeHandling);
|
||||
else
|
||||
@ -168,7 +182,7 @@ function run_test() {
|
||||
protoInfo = protoSvc.getProtocolHandlerInfo("mailto");
|
||||
if (haveDefaultHandlersVersion) {
|
||||
do_check_eq(2, protoInfo.possibleApplicationHandlers.length);
|
||||
// Win7+ might not have a default mailto: handler, but on other platforms
|
||||
// Win7+ or Linux's GIO may have no default mailto: handler. Otherwise
|
||||
// alwaysAskBeforeHandling is expected to be false here, because although
|
||||
// the pref is true, the value in RDF is false. The injected mailto handler
|
||||
// carried over the default pref value, and so when we set the pref above
|
||||
|
Loading…
Reference in New Issue
Block a user