--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -6,22 +6,28 @@ const TELEMETRY_ENABLED_PREF = "datarepo
const PREF_BRANCH = "extensions.screenshots.";
const USER_DISABLE_PREF = "extensions.screenshots.disabled";
const SYSTEM_DISABLE_PREF = "extensions.screenshots.system-disabled";
const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Console",
"resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+ "resource:///modules/CustomizableUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils",
+ "resource://gre/modules/LegacyExtensionsUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource:///modules/PageActions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils",
- "resource://gre/modules/LegacyExtensionsUtils.jsm");
let addonResourceURI;
let appStartupDone;
let appStartupPromise = new Promise((resolve, reject) => {
appStartupDone = resolve;
});
const prefs = Services.prefs;
@@ -54,17 +60,69 @@ const appStartupObserver = {
},
observe() {
appStartupDone();
this.unregister();
}
}
+const LibraryButton = {
+ ITEM_ID: "appMenu-library-screenshots",
+
+ init(webExtension) {
+ this._initialized = true;
+ let permissionPages = [...webExtension.extension.permissions].filter(p => (/^https?:\/\//i).test(p));
+ this.PAGE_TO_OPEN = permissionPages.length ? permissionPages[0].replace(/\*$/, "") : "https://screenshots.firefox.com/";
+ this.PAGE_TO_OPEN += "shots";
+ this.ICON_URL = webExtension.extension.getURL("icons/icon-16-v2.svg");
+ this.ICON_URL_2X = webExtension.extension.getURL("icons/icon-32-v2.svg");
+ this.LABEL = webExtension.extension.localizeMessage("libraryLabel");
+ CustomizableUI.addListener(this);
+ for (let win of CustomizableUI.windows) {
+ this.onWindowOpened(win);
+ }
+ },
+
+ uninit() {
+ if (!this._initialized) {
+ return;
+ }
+ for (let win of CustomizableUI.windows) {
+ let item = win.document.getElementById(this.ITEM_ID);
+ if (item) {
+ item.remove();
+ }
+ }
+ CustomizableUI.removeListener(this);
+ },
+
+ onWindowOpened(win) {
+ let libraryViewInsertionPoint = win.document.getElementById("appMenu-library-remotetabs-button");
+ // If the library view doesn't exist (on non-photon builds, for instance),
+ // this will be null, and we bail out early.
+ if (!libraryViewInsertionPoint) {
+ return;
+ }
+ let parent = libraryViewInsertionPoint.parentNode;
+ let {nextSibling} = libraryViewInsertionPoint;
+ let item = win.document.createElement("toolbarbutton");
+ item.className = "subviewbutton subviewbutton-iconic";
+ item.addEventListener("command", () => win.openUILinkIn(this.PAGE_TO_OPEN, "tab"));
+ item.id = this.ITEM_ID;
+ let iconURL = win.devicePixelRatio >= 1.1 ? this.ICON_URL_2X : this.ICON_URL;
+ item.setAttribute("image", iconURL);
+ item.setAttribute("label", this.LABEL);
+
+ parent.insertBefore(item, nextSibling);
+ },
+};
+
const APP_STARTUP = 1;
+const APP_SHUTDOWN = 2;
let startupReason;
function startup(data, reason) { // eslint-disable-line no-unused-vars
startupReason = reason;
if (reason === APP_STARTUP) {
appStartupObserver.register();
} else {
appStartupDone();
@@ -113,29 +171,37 @@ function handleStartup() {
} else if (shouldDisable()) {
return stop(webExtension, ADDON_DISABLE);
}
}
function start(webExtension) {
return webExtension.startup(startupReason).then((api) => {
api.browser.runtime.onMessage.addListener(handleMessage);
- return Promise.resolve(null);
+ LibraryButton.init(webExtension);
+ initPhotonPageAction(api);
}).catch((err) => {
// The startup() promise will be rejected if the webExtension was
// already started (a harmless error), or if initializing the
// WebExtension failed and threw (an important error).
console.error(err);
if (err.message !== "This embedded extension has already been started") {
// TODO: Should we send these errors to Sentry? #2420
}
});
}
function stop(webExtension, reason) {
+ if (reason != APP_SHUTDOWN) {
+ LibraryButton.uninit();
+ if (photonPageAction) {
+ photonPageAction.remove();
+ photonPageAction = null;
+ }
+ }
return Promise.resolve(webExtension.shutdown(reason));
}
function handleMessage(msg, sender, sendReply) {
if (!msg) {
return;
}
@@ -151,8 +217,91 @@ function handleMessage(msg, sender, send
if (addon) {
addon.uninstall();
}
sendReply({type: "success", value: !!addon});
});
return true;
}
}
+
+let photonPageAction;
+
+// If the current Firefox version supports Photon (57 and later), this sets up
+// a Photon page action and removes the UI for the WebExtension browser action.
+// Does nothing otherwise. Ideally, in the future, WebExtension page actions
+// and Photon page actions would be one in the same, but they aren't right now.
+function initPhotonPageAction(api) {
+ // The MOZ_PHOTON_THEME ifdef got removed, but we need to support 55 and 56 as well,
+ // so check if the property exists *and* is false before bailing.
+ if (typeof AppConstants.MOZ_PHOTON_THEME != "undefined" && !AppConstants.MOZ_PHOTON_THEME) {
+ // Photon not supported. Use the WebExtension's browser action.
+ return;
+ }
+
+ let id = "screenshots";
+ let port = null;
+ let baseIconPath = addonResourceURI.spec + "webextension/";
+
+ let {Management: {global: {tabTracker}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
+
+ // Make the page action.
+ photonPageAction = PageActions.actionForID(id) || PageActions.addAction(new PageActions.Action({
+ id,
+ title: "Take a Screenshot",
+ iconURL: baseIconPath + "icons/icon-32-v2.svg",
+ _insertBeforeActionID: null,
+ onCommand(event, buttonNode) {
+ if (port) {
+ let browserWin = buttonNode.ownerGlobal;
+ port.postMessage({
+ type: "click",
+ tab: {
+ url: browserWin.gBrowser.selectedBrowser.currentURI.spec,
+ id: tabTracker.getId(browserWin.gBrowser.selectedTab),
+ },
+ });
+ }
+ },
+ }));
+
+ // Remove the navbar button of the WebExtension's browser action.
+ let cuiWidgetID = "screenshots_mozilla_org-browser-action";
+ CustomizableUI.addListener({
+ onWidgetAfterCreation(wid, aArea) {
+ if (wid == cuiWidgetID) {
+ CustomizableUI.destroyWidget(cuiWidgetID);
+ CustomizableUI.removeListener(this);
+ }
+ },
+ });
+
+ // Establish a port to the WebExtension side.
+ api.browser.runtime.onConnect.addListener((listenerPort) => {
+ if (listenerPort.name != "photonPageActionPort") {
+ return;
+ }
+ port = listenerPort;
+ port.onMessage.addListener((message) => {
+ switch (message.type) {
+ case "setProperties":
+ if (message.title) {
+ photonPageAction.title = message.title;
+ }
+ if (message.iconPath) {
+ photonPageAction.iconURL = baseIconPath + message.iconPath;
+ }
+ break;
+ default:
+ console.error("Unrecognized message:", message);
+ break;
+ }
+ });
+
+ // It's necessary to tell the WebExtension not to use its browser action,
+ // due to the CUI widget's removal. Otherwise Firefox's WebExtension
+ // machinery throws exceptions.
+ port.postMessage({
+ type: "setUsePhotonPageAction",
+ value: true
+ });
+ });
+}
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
<em:minVersion>51.0a1</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:type>2</em:type>
- <em:version>10.12.0</em:version>
+ <em:version>16.0.0</em:version>
<em:bootstrap>true</em:bootstrap>
- <em:homepageURL>https://pageshot.net/</em:homepageURL>
+ <em:homepageURL>https://screenshots.firefox.com/</em:homepageURL>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
</Description>
</RDF>
--- a/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
+++ b/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
@@ -8,14 +8,18 @@ function checkElements(expectPresent, l)
add_task(async function() {
await promiseScreenshotsEnabled();
registerCleanupFunction(async function() {
await promiseScreenshotsReset();
});
+ let onPhoton = (typeof AppConstants.MOZ_PHOTON_THEME == "undefined") ||
+ AppConstants.MOZ_PHOTON_THEME;
+ let id = onPhoton ? "pageAction-panel-screenshots" : "screenshots_mozilla_org-browser-action";
+
await BrowserTestUtils.waitForCondition(
- () => document.getElementById("screenshots_mozilla_org-browser-action"),
+ () => document.getElementById(id),
"Screenshots button should be present", 100, 100);
- checkElements(true, ["screenshots_mozilla_org-browser-action"]);
+ checkElements(true, [id]);
});
--- a/browser/extensions/screenshots/test/browser/head.js
+++ b/browser/extensions/screenshots/test/browser/head.js
@@ -1,54 +1,78 @@
+/* globals PageActions */
+
// Currently Screenshots is disabled in tests. We want these tests to work under
// either case that Screenshots is disabled or enabled on startup of the browser,
// and that at the end we're reset to the correct state.
let enabledOnStartup = false;
// ScreenshotsEnabled/Disabled promises return true if it was already
// Enabled/Disabled, and false if it need to Enable/Disable.
function promiseScreenshotsEnabled() {
if (!Services.prefs.getBoolPref("extensions.screenshots.system-disabled", false)) {
info("Screenshots was already enabled, assuming enabled by default for tests");
enabledOnStartup = true;
return Promise.resolve(true);
}
info("Screenshots is not enabled");
return new Promise((resolve, reject) => {
- let listener = {
- onWidgetAfterCreation(widgetid) {
- if (widgetid == "screenshots_mozilla_org-browser-action") {
- info("screenshots_mozilla_org-browser-action button created");
- CustomizableUI.removeListener(listener);
+ if (AppConstants.hasOwnProperty("MOZ_PHOTON_THEME") && !AppConstants.MOZ_PHOTON_THEME) {
+ let listener = {
+ onWidgetAfterCreation(widgetid) {
+ if (widgetid == "screenshots_mozilla_org-browser-action") {
+ info("screenshots_mozilla_org-browser-action button created");
+ CustomizableUI.removeListener(listener);
+ resolve(false);
+ }
+ }
+ }
+ CustomizableUI.addListener(listener);
+ } else {
+ let interval = setInterval(() => {
+ let action = PageActions.actionForID("screenshots");
+ if (action) {
+ info("screenshots page action created");
+ clearInterval(interval);
resolve(false);
}
- }
+ }, 100);
}
- CustomizableUI.addListener(listener);
info("Set Screenshots disabled pref to false.");
Services.prefs.setBoolPref("extensions.screenshots.system-disabled", false);
});
}
function promiseScreenshotsDisabled() {
if (Services.prefs.getBoolPref("extensions.screenshots.system-disabled", false)) {
info("Screenshots already disabled");
return Promise.resolve(true);
}
return new Promise((resolve, reject) => {
- let listener = {
- onWidgetDestroyed(widgetid) {
- if (widgetid == "screenshots_mozilla_org-browser-action") {
- CustomizableUI.removeListener(listener);
- info("screenshots_mozilla_org-browser-action destroyed");
+ if (AppConstants.hasOwnProperty("MOZ_PHOTON_THEME") && !AppConstants.MOZ_PHOTON_THEME) {
+ let listener = {
+ onWidgetDestroyed(widgetid) {
+ if (widgetid == "screenshots_mozilla_org-browser-action") {
+ CustomizableUI.removeListener(listener);
+ info("screenshots_mozilla_org-browser-action destroyed");
+ resolve(false);
+ }
+ }
+ }
+ CustomizableUI.addListener(listener);
+ } else {
+ let interval = setInterval(() => {
+ let action = PageActions.actionForID("screenshots");
+ if (!action) {
+ info("screenshots page action removed");
+ clearInterval(interval);
resolve(false);
}
- }
+ }, 100);
}
- CustomizableUI.addListener(listener);
info("Set Screenshots disabled pref to true.");
Services.prefs.setBoolPref("extensions.screenshots.system-disabled", true);
});
}
function promiseScreenshotsReset() { // eslint-disable-line no-unused-vars
if (enabledOnStartup) {
info("Reset is enabling Screenshots addon");
--- a/browser/extensions/screenshots/webextension/background/deviceInfo.js
+++ b/browser/extensions/screenshots/webextension/background/deviceInfo.js
@@ -6,19 +6,19 @@ this.deviceInfo = (function() {
let manifest = browser.runtime.getManifest();
let platformInfo = {};
catcher.watchPromise(browser.runtime.getPlatformInfo().then((info) => {
platformInfo = info;
}));
return function deviceInfo() {
- let match = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9\.]{1,1000})/);
+ let match = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9.]{1,1000})/);
let chromeVersion = match ? match[1] : null;
- match = navigator.userAgent.match(/Firefox\/([0-9\.]{1,1000})/);
+ match = navigator.userAgent.match(/Firefox\/([0-9.]{1,1000})/);
let firefoxVersion = match ? match[1] : null;
let appName = chromeVersion ? "chrome" : "firefox";
return {
addonVersion: manifest.version,
platform: platformInfo.os,
architecture: platformInfo.arch,
version: firefoxVersion || chromeVersion,
--- a/browser/extensions/screenshots/webextension/background/main.js
+++ b/browser/extensions/screenshots/webextension/background/main.js
@@ -1,9 +1,9 @@
-/* globals selectorLoader, analytics, communication, catcher, log, makeUuid, auth, senderror */
+/* globals selectorLoader, analytics, communication, catcher, log, makeUuid, auth, senderror, startBackground */
"use strict";
this.main = (function() {
let exports = {};
const pasteSymbol = (window.navigator.platform.match(/Mac/i)) ? "\u2318" : "Ctrl";
const { sendEvent } = analytics;
@@ -13,19 +13,26 @@ this.main = (function() {
let hasSeenOnboarding;
browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
hasSeenOnboarding = !!result.hasSeenOnboarding;
if (!hasSeenOnboarding) {
setIconActive(false, null);
// Note that the branded name 'Firefox Screenshots' is not localized:
- browser.browserAction.setTitle({
- title: "Firefox Screenshots"
- });
+ if (!startBackground.usePhotonPageAction) {
+ browser.browserAction.setTitle({
+ title: "Firefox Screenshots"
+ });
+ } else {
+ startBackground.photonPageActionPort.postMessage({
+ type: "setProperties",
+ title: "Firefox Screenshots"
+ });
+ }
}
}).catch((error) => {
log.error("Error getting hasSeenOnboarding:", error);
});
exports.setBackend = function(newBackend) {
backend = newBackend;
backend = backend.replace(/\/*$/, "");
@@ -50,24 +57,31 @@ this.main = (function() {
}
}
function setIconActive(active, tabId) {
let path = active ? "icons/icon-highlight-32-v2.svg" : "icons/icon-32-v2.svg";
if ((!hasSeenOnboarding) && !active) {
path = "icons/icon-starred-32-v2.svg";
}
- browser.browserAction.setIcon({path, tabId}).catch((error) => {
- // FIXME: use errorCode
- if (error.message && /Invalid tab ID/.test(error.message)) {
- // This is a normal exception that we can ignore
- } else {
- catcher.unhandled(error);
- }
- });
+ if (!startBackground.usePhotonPageAction) {
+ browser.browserAction.setIcon({path, tabId}).catch((error) => {
+ // FIXME: use errorCode
+ if (error.message && /Invalid tab ID/.test(error.message)) {
+ // This is a normal exception that we can ignore
+ } else {
+ catcher.unhandled(error);
+ }
+ });
+ } else {
+ startBackground.photonPageActionPort.postMessage({
+ type: "setProperties",
+ iconPath: path
+ });
+ }
}
function toggleSelector(tab) {
return analytics.refreshTelemetryPref()
.then(() => selectorLoader.toggle(tab.id, hasSeenOnboarding))
.then(active => {
setIconActive(active, tab.id);
return active;
@@ -89,20 +103,21 @@ this.main = (function() {
sendEvent("start-shot", "site-request");
setIconActive(true, tab.id);
selectorLoader.toggle(tab.id, false);
}
});
}
function shouldOpenMyShots(url) {
- return /^about:(?:newtab|blank)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url);
+ return /^about:(?:newtab|blank|home)/i.test(url) || /^resource:\/\/activity-streams\//i.test(url);
}
// This is called by startBackground.js, directly in response to browser.browserAction.onClicked
+ // and clicks on the Photon page action
exports.onClicked = catcher.watchFunction((tab) => {
if (tab.incognito) {
senderror.showError({
popupMessage: "PRIVATE_WINDOW"
});
return;
}
if (shouldOpenMyShots(tab.url)) {
@@ -269,19 +284,26 @@ this.main = (function() {
});
}
}));
communication.register("hasSeenOnboarding", () => {
hasSeenOnboarding = true;
catcher.watchPromise(browser.storage.local.set({hasSeenOnboarding}));
setIconActive(false, null);
- browser.browserAction.setTitle({
- title: browser.i18n.getMessage("contextMenuLabel")
- });
+ if (!startBackground.usePhotonPageAction) {
+ browser.browserAction.setTitle({
+ title: browser.i18n.getMessage("contextMenuLabel")
+ });
+ } else {
+ startBackground.photonPageActionPort.postMessage({
+ type: "setProperties",
+ title: browser.i18n.getMessage("contextMenuLabel")
+ });
+ }
});
communication.register("abortFrameset", () => {
sendEvent("abort-start-shot", "frame-page");
// Note, we only show the error but don't report it, as we know that we can't
// take shots of these pages:
senderror.showError({
popupMessage: "UNSHOOTABLE_PAGE"
--- a/browser/extensions/screenshots/webextension/background/startBackground.js
+++ b/browser/extensions/screenshots/webextension/background/startBackground.js
@@ -1,18 +1,21 @@
/* globals browser, main, communication */
/* This file handles:
browser.browserAction.onClicked
+ clicks on the Photon page action
browser.contextMenus.onClicked
browser.runtime.onMessage
and loads the rest of the background page in response to those events, forwarding
the events to main.onClicked, main.onClickedContextMenu, or communication.onMessage
*/
this.startBackground = (function() {
+ let exports = {};
+
const backgroundScripts = [
"log.js",
"makeUuid.js",
"catcher.js",
"background/selectorLoader.js",
"background/communication.js",
"background/auth.js",
"background/senderror.js",
@@ -47,35 +50,50 @@ this.startBackground = (function() {
main.onClickedContextMenu(info, tab);
}).catch((error) => {
console.error("Error loading Screenshots:", error);
});
});
// Note this duplicates functionality in main.js, but we need to change
// the onboarding icon before main.js loads up
+ let iconPath = null;
browser.storage.local.get(["hasSeenOnboarding"]).then((result) => {
let hasSeenOnboarding = !!result.hasSeenOnboarding;
if (!hasSeenOnboarding) {
let path = "icons/icon-starred-32-v2.svg";
- browser.browserAction.setIcon({path});
+ if (!usePhotonPageAction) {
+ browser.browserAction.setIcon({path});
+ } else {
+ iconPath = path;
+ if (photonPageActionPort) {
+ photonPageActionPort.postMessage({
+ type: "setProperties",
+ iconPath
+ });
+ }
+ }
}
}).catch((error) => {
console.error("Error loading Screenshots onboarding flag:", error);
});
browser.runtime.onMessage.addListener((req, sender, sendResponse) => {
loadIfNecessary().then(() => {
return communication.onMessage(req, sender, sendResponse);
}).catch((error) => {
console.error("Error loading Screenshots:", error);
});
return true;
});
+ let usePhotonPageAction = false;
+ let photonPageActionPort = null;
+ initPhotonPageAction();
+
// We delay this check (by CHECK_MIGRATION_DELAY) just to avoid piling too
// many things onto browser/add-on startup
requestIdleCallback(() => {
browser.runtime.sendMessage({funcName: "getOldDeviceInfo"}).then((result) => {
if (result && result.type == "success" && result.value) {
// There is a possible migration to run, so we'll load the entire background
// page and continue the process
return loadIfNecessary();
@@ -117,9 +135,55 @@ this.startBackground = (function() {
};
document.head.appendChild(tag);
});
});
});
return loadedPromise;
}
+ function initPhotonPageAction() {
+ // Set up this side of the Photon page action port. The other side is in
+ // bootstrap.js. Ideally, in the future, WebExtension page actions and
+ // Photon page actions would be one in the same, but they aren't right now.
+ photonPageActionPort = browser.runtime.connect({ name: "photonPageActionPort" });
+ photonPageActionPort.onMessage.addListener((message) => {
+ switch (message.type) {
+ case "setUsePhotonPageAction":
+ usePhotonPageAction = message.value;
+ break;
+ case "click":
+ loadIfNecessary().then(() => {
+ main.onClicked(message.tab);
+ }).catch((error) => {
+ console.error("Error loading Screenshots:", error);
+ });
+ break;
+ default:
+ console.error("Unrecognized message:", message);
+ break;
+ }
+ });
+ photonPageActionPort.postMessage({
+ type: "setProperties",
+ title: browser.i18n.getMessage("contextMenuLabel"),
+ iconPath
+ });
+
+ // Export these so that main.js can use them.
+ Object.defineProperties(exports, {
+ "photonPageActionPort": {
+ enumerable: true,
+ get() {
+ return photonPageActionPort;
+ }
+ },
+ "usePhotonPageAction": {
+ enumerable: true,
+ get() {
+ return usePhotonPageAction;
+ }
+ }
+ });
+ }
+
+ return exports;
})();
--- a/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
+++ b/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
@@ -1,11 +1,11 @@
/* Created from build/server/static/css/inline-selection.css */
window.inlineSelectionCss = `
-.button, .highlight-button-cancel, .highlight-button-save, .highlight-button-download {
+.button, .highlight-button-cancel, .highlight-button-save, .highlight-button-download, .preview-button-save {
display: flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 1px;
cursor: pointer;
font-size: 16px;
font-weight: 400;
@@ -14,150 +14,213 @@ window.inlineSelectionCss = `
outline: none;
padding: 0 10px;
position: relative;
text-align: center;
text-decoration: none;
transition: background 150ms cubic-bezier(0.07, 0.95, 0, 1), border 150ms cubic-bezier(0.07, 0.95, 0, 1);
user-select: none;
white-space: nowrap; }
- .button.small, .small.highlight-button-cancel, .small.highlight-button-save, .small.highlight-button-download {
+ .button.small, .small.highlight-button-cancel, .small.highlight-button-save, .small.highlight-button-download, .small.preview-button-save {
height: 32px;
line-height: 32px;
padding: 0 8px; }
- .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-save, .tiny.highlight-button-download {
+ .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-save, .tiny.highlight-button-download, .tiny.preview-button-save {
font-size: 14px;
height: 26px;
border: 1px solid #c7c7c7; }
- .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-save:hover, .tiny.highlight-button-download:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-save:focus, .tiny.highlight-button-download:focus {
+ .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-save:hover, .tiny.highlight-button-download:hover, .tiny.preview-button-save:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-save:focus, .tiny.highlight-button-download:focus, .tiny.preview-button-save:focus {
background: #ebebeb;
border-color: #989898; }
- .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-save:active, .tiny.highlight-button-download:active {
+ .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-save:active, .tiny.highlight-button-download:active, .tiny.preview-button-save:active {
background: #dedede;
border-color: #989898; }
- .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download {
+ .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.preview-button-save {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
- border: none;
+ border: 0;
border-right: 1px solid #c7c7c7;
- box-shadow: none;
+ box-shadow: 0;
border-radius: 0;
flex-shrink: 0;
font-size: 20px;
height: 100px;
line-height: 100%;
overflow: hidden; }
@media (max-width: 719px) {
- .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download {
+ .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-save, .block-button.highlight-button-download, .block-button.preview-button-save {
justify-content: flex-start;
font-size: 16px;
height: 72px;
margin-right: 10px;
padding: 0 5px; } }
- .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-save:hover, .block-button.highlight-button-download:hover {
+ .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-save:hover, .block-button.highlight-button-download:hover, .block-button.preview-button-save:hover {
background: #ebebeb; }
- .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-save:active, .block-button.highlight-button-download:active {
+ .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-save:active, .block-button.highlight-button-download:active, .block-button.preview-button-save:active {
background: #dedede; }
+ .button.download, .download.highlight-button-cancel, .download.highlight-button-save, .download.highlight-button-download, .download.preview-button-save, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.preview-button-save, .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.preview-button-save, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.preview-button-save {
+ background-image: url("../img/icon-sprite.svg");
+ background-size: 480px 40px;
+ background-repeat: no-repeat;
+ background-position: 0 0;
+ margin-right: 10px;
+ transition: background-color 150ms cubic-bezier(0.07, 0.95, 0, 1); }
+ .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-save:hover, .download.highlight-button-download:hover, .download.preview-button-save:hover {
+ background-position: -40px 0; }
+ .button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-save:active, .download.highlight-button-download:active, .download.preview-button-save:active {
+ background-position: -80px 0; }
+ .button.share, .share.highlight-button-cancel, .share.highlight-button-save, .share.highlight-button-download, .share.preview-button-save {
+ background-position: -120px 0; }
+ .button.share:hover, .share.highlight-button-cancel:hover, .share.highlight-button-save:hover, .share.highlight-button-download:hover, .share.preview-button-save:hover {
+ background-position: -160px 0; }
+ .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-save:active, .share.highlight-button-download:active, .share.preview-button-save:active, .button.share.active, .share.active.highlight-button-cancel, .share.active.highlight-button-save, .share.active.highlight-button-download, .share.active.preview-button-save {
+ background-position: -200px 0; }
+ .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-save, .trash.highlight-button-download, .trash.preview-button-save {
+ background-position: -240px 0; }
+ .button.trash:hover, .trash.highlight-button-cancel:hover, .trash.highlight-button-save:hover, .trash.highlight-button-download:hover, .trash.preview-button-save:hover {
+ background-position: -280px 0; }
+ .button.trash:active, .trash.highlight-button-cancel:active, .trash.highlight-button-save:active, .trash.highlight-button-download:active, .trash.preview-button-save:active {
+ background-position: -320px 0; }
+ .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-save, .flag.highlight-button-download, .flag.preview-button-save {
+ background-position: -360px 0; }
+ .button.flag:hover, .flag.highlight-button-cancel:hover, .flag.highlight-button-save:hover, .flag.highlight-button-download:hover, .flag.preview-button-save:hover {
+ background-position: -400px 0; }
+ .button.flag:active, .flag.highlight-button-cancel:active, .flag.highlight-button-save:active, .flag.highlight-button-download:active, .flag.preview-button-save:active {
+ background-position: -440px 0; }
.inverse-color-scheme {
background: #3e3d40;
- color: #f5f5f7; }
+ color: #f6f6f8; }
.inverse-color-scheme a {
color: #e1e1e6; }
.default-color-scheme {
- background: #f5f5f7;
+ background: #f6f6f8;
color: #3e3d40; }
.default-color-scheme a {
color: #009ec0; }
.highlight-color-scheme {
background: #009ec0;
color: #fff; }
.highlight-color-scheme a {
color: #fff;
text-decoration: underline; }
.alt-color-scheme {
- background: #31365A;
- color: #f5f5f7; }
+ background: #31365a;
+ color: #f6f6f8; }
.alt-color-scheme h1 {
- color: #6F7FB6; }
+ color: #6f7fb6; }
.alt-color-scheme a {
color: #e1e1e6;
text-decoration: underline; }
-.button.primary, .primary.highlight-button-cancel, .highlight-button-save, .primary.highlight-button-download {
+.button.primary, .primary.highlight-button-cancel, .highlight-button-save, .primary.highlight-button-download, .preview-button-save {
background-color: #009ec0;
color: #fff; }
- .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-save:hover, .primary.highlight-button-download:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-save:focus, .primary.highlight-button-download:focus {
+ .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-save:hover, .primary.highlight-button-download:hover, .preview-button-save:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-save:focus, .primary.highlight-button-download:focus, .preview-button-save:focus {
background-color: #00819c; }
- .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-save:active, .primary.highlight-button-download:active {
+ .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-save:active, .primary.highlight-button-download:active, .preview-button-save:active {
background-color: #006c83; }
-.button.secondary, .highlight-button-cancel, .secondary.highlight-button-save, .highlight-button-download {
- background-color: #f5f5f7;
+.button.secondary, .highlight-button-cancel, .secondary.highlight-button-save, .highlight-button-download, .secondary.preview-button-save {
+ background-color: #f6f6f8;
color: #3e3d40; }
- .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-save:hover, .highlight-button-download:hover {
+ .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-save:hover, .highlight-button-download:hover, .secondary.preview-button-save:hover {
background-color: #ebebeb; }
- .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-save:hover, .highlight-button-download:hover {
+ .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-save:active, .highlight-button-download:active, .secondary.preview-button-save:active {
background-color: #dedede; }
-.button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-save, .transparent.highlight-button-download {
+.button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-save, .transparent.highlight-button-download, .transparent.preview-button-save {
background-color: transparent;
color: #3e3d40; }
- .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-save:hover, .transparent.highlight-button-download:hover, .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-save:focus, .transparent.highlight-button-download:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-save:active, .transparent.highlight-button-download:active {
+ .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-save:hover, .transparent.highlight-button-download:hover, .transparent.preview-button-save:hover, .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-save:focus, .transparent.highlight-button-download:focus, .transparent.preview-button-save:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-save:active, .transparent.highlight-button-download:active, .transparent.preview-button-save:active {
background-color: rgba(0, 0, 0, 0.05); }
-.button.warning, .warning.highlight-button-cancel, .warning.highlight-button-save, .warning.highlight-button-download {
+.button.warning, .warning.highlight-button-cancel, .warning.highlight-button-save, .warning.highlight-button-download, .warning.preview-button-save {
color: #fff;
background: #d92215; }
- .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-save:hover, .warning.highlight-button-download:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-save:focus, .warning.highlight-button-download:focus {
+ .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-save:hover, .warning.highlight-button-download:hover, .warning.preview-button-save:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-save:focus, .warning.highlight-button-download:focus, .warning.preview-button-save:focus {
background: #b81d12; }
- .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-save:active, .warning.highlight-button-download:active {
+ .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-save:active, .warning.highlight-button-download:active, .warning.preview-button-save:active {
background: #a11910; }
.subtitle-link {
color: #009ec0; }
+.loader {
+ background: #2e2d30;
+ border-radius: 2px;
+ height: 4px;
+ overflow: hidden;
+ position: relative;
+ width: 200px; }
+ #shot-index .loader {
+ background-color: #dedede; }
+
+.loader-inner {
+ animation: bounce infinite alternate 1250ms cubic-bezier(0.7, 0, 0.3, 1);
+ background: #04d1e6;
+ border-radius: 2px;
+ height: 4px;
+ transform: translateX(-40px);
+ width: 50px; }
+
+@keyframes bounce {
+ 0% {
+ transform: translateX(-40px); }
+ 100% {
+ transform: translate(190px); } }
+
@keyframes fade-in {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@keyframes pop {
0% {
transform: scale(1); }
97% {
transform: scale(1.04); }
100% {
transform: scale(1); } }
@keyframes pulse {
0% {
- opacity: .3;
+ opacity: 0.3;
transform: scale(1); }
70% {
- opacity: .25;
+ opacity: 0.25;
transform: scale(1.04); }
100% {
- opacity: .3;
+ opacity: 0.3;
transform: scale(1); } }
@keyframes slide-left {
0% {
opacity: 0;
transform: translate3d(160px, 0, 0); }
100% {
opacity: 1;
transform: translate3d(0, 0, 0); } }
+@keyframes bounce-in {
+ 0% {
+ opacity: 0;
+ transform: scale(1); }
+ 60% {
+ opacity: 1;
+ transform: scale(1.02); }
+ 100% {
+ transform: scale(1); } }
+
.mover-target {
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
position: absolute;
z-index: 5; }
@@ -172,20 +235,20 @@ window.inlineSelectionCss = `
.hover-highlight {
animation: fade-in 125ms forwards cubic-bezier(0.07, 0.95, 0, 1);
background: rgba(255, 255, 255, 0.2);
border-radius: 1px;
pointer-events: none;
position: absolute;
z-index: 10000000000; }
- .hover-highlight:before {
+ .hover-highlight::before {
border: 2px dashed rgba(255, 255, 255, 0.4);
bottom: 0;
- content: '';
+ content: "";
left: 0;
position: absolute;
right: 0;
top: 0; }
.mover-target.direction-topLeft {
cursor: nwse-resize;
height: 60px;
@@ -351,21 +414,57 @@ window.inlineSelectionCss = `
display: block;
margin: 5px;
width: 40px; }
.pixel-dimensions {
position: absolute;
pointer-events: none;
font-weight: bold;
- font-family: sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
font-size: 70%;
color: #000;
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; }
+.preview-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ right: 0;
+ top: -2px; }
+
+.preview-image {
+ position: relative;
+ height: 80%;
+ max-width: 100%;
+ margin: auto 2em;
+ text-align: center;
+ animation-delay: 50ms;
+ animation: bounce-in 300ms forwards ease-in-out; }
+
+.preview-image img {
+ display: block;
+ width: auto;
+ height: auto;
+ max-width: 100%;
+ max-height: 90%;
+ margin: 50px auto;
+ border: 1px solid rgba(255, 255, 255, 0.8); }
+
+.preview-button-save {
+ background-image: url("MOZ_EXTENSION/icons/cloud.svg");
+ background-position: 8px center;
+ background-repeat: no-repeat;
+ background-size: 20px 18px;
+ font-size: 18px;
+ margin: 5px;
+ min-width: 80px;
+ padding-left: 34px; }
+
.fixed-container {
align-items: center;
display: flex;
flex-direction: column;
height: 100vh;
justify-content: center;
left: 0;
margin: 0;
@@ -413,17 +512,17 @@ window.inlineSelectionCss = `
margin-left: 20px; }
.preview-instructions {
display: flex;
align-items: center;
justify-content: center;
animation: pulse 125mm cubic-bezier(0.07, 0.95, 0, 1);
color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
font-size: 24px;
line-height: 32px;
text-align: center;
padding-top: 20px;
width: 400px; }
.myshots-all-buttons-container {
display: flex;
--- a/browser/extensions/screenshots/webextension/build/onboardingCss.js
+++ b/browser/extensions/screenshots/webextension/build/onboardingCss.js
@@ -1,47 +1,81 @@
/* Created from build/server/static/css/onboarding.css */
window.onboardingCss = `
+.loader {
+ background: #2e2d30;
+ border-radius: 2px;
+ height: 4px;
+ overflow: hidden;
+ position: relative;
+ width: 200px; }
+ #shot-index .loader {
+ background-color: #dedede; }
+
+.loader-inner {
+ animation: bounce infinite alternate 1250ms cubic-bezier(0.7, 0, 0.3, 1);
+ background: #04d1e6;
+ border-radius: 2px;
+ height: 4px;
+ transform: translateX(-40px);
+ width: 50px; }
+
+@keyframes bounce {
+ 0% {
+ transform: translateX(-40px); }
+ 100% {
+ transform: translate(190px); } }
+
@keyframes fade-in {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@keyframes pop {
0% {
transform: scale(1); }
97% {
transform: scale(1.04); }
100% {
transform: scale(1); } }
@keyframes pulse {
0% {
- opacity: .3;
+ opacity: 0.3;
transform: scale(1); }
70% {
- opacity: .25;
+ opacity: 0.25;
transform: scale(1.04); }
100% {
- opacity: .3;
+ opacity: 0.3;
transform: scale(1); } }
@keyframes slide-left {
0% {
opacity: 0;
transform: translate3d(160px, 0, 0); }
100% {
opacity: 1;
transform: translate3d(0, 0, 0); } }
+@keyframes bounce-in {
+ 0% {
+ opacity: 0;
+ transform: scale(1); }
+ 60% {
+ opacity: 1;
+ transform: scale(1.02); }
+ 100% {
+ transform: scale(1); } }
+
html,
body {
box-sizing: border-box;
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
height: 100%;
margin: 0;
width: 100%; }
#slide-overlay {
display: flex;
align-items: center;
flex-direction: column;
@@ -57,17 +91,17 @@ body {
animation: fade-in 250ms forwards cubic-bezier(0.07, 0.95, 0, 1);
opacity: 0; }
.slide {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
- background-color: #f5f5f7;
+ background-color: #f6f6f8;
border-radius: 5px;
height: 520px;
overflow: hidden;
width: 700px; }
.slide .slide-image {
background-size: 700px 378px;
flex: 0 0 360px;
font-size: 16px;
@@ -140,17 +174,17 @@ body {
#slide-status-container {
display: flex;
align-items: center;
justify-content: center;
padding-top: 15px; }
.goto-slide {
background: transparent;
- background-color: #f5f5f7;
+ background-color: #f6f6f8;
border-radius: 50%;
border: 0;
flex: 0 0 9px;
height: 9px;
margin: 0 4px;
opacity: 0.7;
padding: 0;
transition: height 100ms cubic-bezier(0.07, 0.95, 0, 1), opacity 100ms cubic-bezier(0.07, 0.95, 0, 1); }
@@ -226,24 +260,24 @@ body {
top: 50%;
transition: opacity 100ms cubic-bezier(0.07, 0.95, 0, 1);
z-index: 10; }
#skip:hover {
opacity: 1; }
.active-slide-1 #prev,
-.active-slide-3 #next {
+.active-slide-4 #next {
display: none; }
#done {
background-image: url("MOZ_EXTENSION/icons/done.svg");
display: none; }
-.active-slide-3 #done {
+.active-slide-4 #done {
display: inline-block; }
/* for smaller screen sizes */
@media screen and (max-width: 768px) {
.slide {
height: 360px;
width: 450px; }
.slide .slide-image {
--- a/browser/extensions/screenshots/webextension/build/onboardingHtml.js
+++ b/browser/extensions/screenshots/webextension/build/onboardingHtml.js
@@ -5,17 +5,17 @@ window.onboardingHtml = `
<head>
<!-- onboarding.scss is automatically inserted here: -->
<style></style>
<!-- Here and in onboarding.scss use MOZ_EXTENSION/path to refer to local files -->
</head>
<body>
<div id="slide-overlay">
<!-- The current slide is set by having .active-slide-1, .active-slide-2, etc on #slide element: -->
- <div id="slide-container" data-number-of-slides="3" class="active-slide-1">
+ <div id="slide-container" data-number-of-slides="4" class="active-slide-1">
<div class="slide slide-1">
<!-- Note: all images must be listed in manifest.json.template under web_accessible_resources -->
<div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-1.png');"></div>
<div class="slide-content">
<div class="slide-content-aligner">
<h1><span><strong>Firefox</strong> Screenshots</span><sup>Beta</sup></h1>
<p data-l10n-id="tourBodyOne"></p>
</div>
@@ -25,32 +25,40 @@ window.onboardingHtml = `
<div class="slide slide-2">
<div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-2.png');"></div>
<div class="slide-content">
<h1 data-l10n-id="tourHeaderTwo"></h1>
<p data-l10n-id="tourBodyTwo"></p>
</div>
</div>
<div class="slide slide-3">
+ <div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-3.png');"></div>
+ <div class="slide-content">
+ <h1 data-l10n-id="tourHeaderThree"></h1>
+ <p data-l10n-id="tourBodyThree"></p>
+ </div>
+ </div>
+ <div class="slide slide-4">
<div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-4.png');"></div>
<div class="slide-content">
<h1 data-l10n-id="tourHeaderFour"></h1>
<p data-l10n-id="tourBodyFour"></p>
</div>
</div>
<!-- Clickable elements should be buttons for accessibility -->
<button id="skip" data-l10n-id="tourSkip" tabindex=1>Skip</button>
<button id="prev" tabindex=2 data-l10n-label-id="tourPrevious"></button>
<button id="next" tabindex=3 data-l10n-label-id="tourNext"></button>
<button id="done" tabindex=4 data-l10n-label-id="tourDone"></button>
<div id="slide-status-container">
<button class="goto-slide goto-slide-1" data-number="1" tabindex=4></button>
<button class="goto-slide goto-slide-2" data-number="2" tabindex=5></button>
<button class="goto-slide goto-slide-3" data-number="3" tabindex=6></button>
+ <button class="goto-slide goto-slide-4" data-number="4" tabindex=7></button>
</div>
<!-- FIXME: Need to put in privacy / etc links -->
</div>
</div>
</body>
</html>
`;
--- a/browser/extensions/screenshots/webextension/build/raven.js
+++ b/browser/extensions/screenshots/webextension/build/raven.js
@@ -1,9 +1,9 @@
-/*! Raven.js 3.15.0 (d49a1b8) | github.com/getsentry/raven-js */
+/*! Raven.js 3.17.0 (6384830) | github.com/getsentry/raven-js */
/*
* Includes TraceKit
* https://github.com/getsentry/TraceKit
*
* Copyright 2017 Matt Robenolt and other contributors
* Released under the BSD license
* https://github.com/getsentry/raven-js/blob/master/LICENSE
@@ -86,16 +86,23 @@ function now() {
// This is to be defensive in environments where window does not exist (see https://github.com/getsentry/raven-js/pull/785)
var _window = typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global
: typeof self !== 'undefined' ? self
: {};
var _document = _window.document;
var _navigator = _window.navigator;
+
+function keepOriginalCallback(original, callback) {
+ return isFunction(callback) ?
+ function (data) { return callback(data, original) } :
+ callback;
+}
+
// First, check for JSON support
// If there is no JSON, we no-op the core features of Raven
// since JSON is required to encode the payload
function Raven() {
this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify);
// Raven can run in contexts where there's no document (react-native)
this._hasDocument = !isUndefined(_document);
this._hasNavigator = !isUndefined(_navigator);
@@ -151,17 +158,17 @@ function Raven() {
* @this {Raven}
*/
Raven.prototype = {
// Hardcode version string so that raven source can be loaded directly via
// webpack (using a build step causes webpack #1617). Grunt verifies that
// this value matches package.json during build.
// See: https://github.com/getsentry/raven-js/issues/465
- VERSION: '3.15.0',
+ VERSION: '3.17.0',
debug: false,
TraceKit: TraceKit, // alias to TraceKit
/*
* Configure Raven with a DSN and extra options
*
@@ -633,52 +640,46 @@ Raven.prototype = {
* Set the dataCallback option
*
* @param {function} callback The callback to run which allows the
* data blob to be mutated before sending
* @return {Raven}
*/
setDataCallback: function(callback) {
var original = this._globalOptions.dataCallback;
- this._globalOptions.dataCallback = isFunction(callback)
- ? function (data) { return callback(data, original); }
- : callback;
-
+ this._globalOptions.dataCallback =
+ keepOriginalCallback(original, callback);
return this;
},
/*
* Set the breadcrumbCallback option
*
* @param {function} callback The callback to run which allows filtering
* or mutating breadcrumbs
* @return {Raven}
*/
setBreadcrumbCallback: function(callback) {
var original = this._globalOptions.breadcrumbCallback;
- this._globalOptions.breadcrumbCallback = isFunction(callback)
- ? function (data) { return callback(data, original); }
- : callback;
-
+ this._globalOptions.breadcrumbCallback =
+ keepOriginalCallback(original, callback);
return this;
},
/*
* Set the shouldSendCallback option
*
* @param {function} callback The callback to run which allows
* introspecting the blob before sending
* @return {Raven}
*/
setShouldSendCallback: function(callback) {
var original = this._globalOptions.shouldSendCallback;
- this._globalOptions.shouldSendCallback = isFunction(callback)
- ? function (data) { return callback(data, original); }
- : callback;
-
+ this._globalOptions.shouldSendCallback =
+ keepOriginalCallback(original, callback);
return this;
},
/**
* Override the default HTTP transport mechanism that transmits data
* to the Sentry server.
*
* @param {function} transport Function invoked instead of the default
@@ -1444,26 +1445,27 @@ Raven.prototype = {
// TODO: also consider arbitrary prop values that start with (https?)?://
var urlProps = ['to', 'from', 'url'],
urlProp,
crumb,
data;
for (var i = 0; i < breadcrumbs.values.length; ++i) {
crumb = breadcrumbs.values[i];
- if (!crumb.hasOwnProperty('data') || !isObject(crumb.data))
+ if (!crumb.hasOwnProperty('data') || !isObject(crumb.data) || objectFrozen(crumb.data))
continue;
- data = crumb.data;
+ data = objectMerge({}, crumb.data);
for (var j = 0; j < urlProps.length; ++j) {
urlProp = urlProps[j];
if (data.hasOwnProperty(urlProp)) {
data[urlProp] = truncate(data[urlProp], this._globalOptions.maxUrlLength);
}
}
+ breadcrumbs.values[i].data = data;
}
},
_getHttpData: function() {
if (!this._hasNavigator && !this._hasDocument) return;
var httpData = {};
if (this._hasNavigator && _navigator.userAgent) {
@@ -1838,16 +1840,31 @@ function objectMerge(obj1, obj2) {
return obj1;
}
each(obj2, function(key, value){
obj1[key] = value;
});
return obj1;
}
+/**
+ * This function is only used for react-native.
+ * react-native freezes object that have already been sent over the
+ * js bridge. We need this function in order to check if the object is frozen.
+ * So it's ok that objectFrozen returns false if Object.isFrozen is not
+ * supported because it's not relevant for other "platforms". See related issue:
+ * https://github.com/getsentry/react-native-sentry/issues/57
+ */
+function objectFrozen(obj) {
+ if (!Object.isFrozen) {
+ return false;
+ }
+ return Object.isFrozen(obj);
+}
+
function truncate(str, max) {
return !max || str.length <= max ? str : str.substr(0, max) + '\u2026';
}
/**
* hasKey, a better form of hasOwnProperty
* Example: hasKey(MainHostObject, property) === true/false
*
@@ -2163,19 +2180,32 @@ function isError(value) {
switch ({}.toString.call(value)) {
case '[object Error]': return true;
case '[object Exception]': return true;
case '[object DOMException]': return true;
default: return value instanceof Error;
}
}
+function wrappedCallback(callback) {
+ function dataCallback(data, original) {
+ var normalizedData = callback(data) || data;
+ if (original) {
+ return original(normalizedData) || normalizedData;
+ }
+ return normalizedData;
+ }
+
+ return dataCallback;
+}
+
module.exports = {
isObject: isObject,
- isError: isError
+ isError: isError,
+ wrappedCallback: wrappedCallback
};
},{}],6:[function(_dereq_,module,exports){
(function (global){
'use strict';
var utils = _dereq_(5);
@@ -2542,17 +2572,17 @@ TraceKit.computeStackTrace = (function c
* Chrome and Gecko use this property.
* @param {Error} ex
* @return {?Object.<string, *>} Stack trace information.
*/
function computeStackTraceFromStackProp(ex) {
if (typeof ex.stack === 'undefined' || !ex.stack) return;
var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,
- gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i,
+ gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i,
winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,
// Used to additionally parse URL/line/column from eval frames
geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i,
chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/,
lines = ex.stack.split('\n'),
stack = [],
--- a/browser/extensions/screenshots/webextension/build/shot.js
+++ b/browser/extensions/screenshots/webextension/build/shot.js
@@ -24,17 +24,17 @@ function isUrl(url) {
return true;
}
if ((/^chrome:.{0,8000}/i).test(url)) {
return true;
}
if ((/^view-source:/i).test(url)) {
return isUrl(url.substr("view-source:".length));
}
- return (/^https?:\/\/[a-z0-9\.\-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
+ return (/^https?:\/\/[a-z0-9.-]{1,8000}[a-z0-9](:[0-9]{1,8000})?\/?/i).test(url);
}
function assertUrl(url) {
if (!url) {
throw new Error("Empty value is not URL");
}
if (!isUrl(url)) {
let exc = new Error("Not a URL");
@@ -116,17 +116,17 @@ function resolveUrl(base, url) {
return url;
}
if (url.indexOf("//") === 0) {
// Protocol-relative URL
return (/^https?:/i).exec(base)[0] + url;
}
if (url.indexOf("/") === 0) {
// Domain-relative URL
- return (/^https?:\/\/[a-z0-9\.\-]{1,4000}/i).exec(base)[0] + url;
+ return (/^https?:\/\/[a-z0-9.-]{1,4000}/i).exec(base)[0] + url;
}
// Otherwise, a full relative URL
while (url.indexOf("./") === 0) {
url = url.substr(2);
}
if (!base) {
// It's not an absolute URL, and we don't have a base URL, so we have
// to throw away the URL
@@ -196,17 +196,17 @@ function makeRandomId() {
}
return id;
}
class AbstractShot {
constructor(backend, id, attrs) {
attrs = attrs || {};
- assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9\.-]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
+ assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9.-]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
this._backend = backend;
this._id = id;
this.origin = attrs.origin || null;
this.fullUrl = attrs.fullUrl || null;
if ((!attrs.fullUrl) && attrs.url) {
console.warn("Received deprecated attribute .url");
this.fullUrl = attrs.url;
}
@@ -346,17 +346,17 @@ class AbstractShot {
assertOrigin(val);
}
this._origin = val || undefined;
}
get filename() {
let filenameTitle = this.title;
let date = new Date(this.createdDate);
- filenameTitle = filenameTitle.replace(/[:\\<>\/!@&*.|\n\r\t]/g, " ");
+ filenameTitle = filenameTitle.replace(/[:\\<>/!@&*.|\n\r\t]/g, " ");
filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
let clipFilename = `Screenshot-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${filenameTitle}`;
const clipFilenameBytesSize = clipFilename.length * 2; // JS STrings are UTF-16
if (clipFilenameBytesSize > 251) { // 255 bytes (Usual filesystems max) - 4 for the ".png" file extension string
const excedingchars = (clipFilenameBytesSize - 246) / 2; // 251 - 5 for ellipsis "[...]"
clipFilename = clipFilename.substring(0, clipFilename.length - excedingchars);
clipFilename = clipFilename + '[...]';
}
--- a/browser/extensions/screenshots/webextension/domainFromUrl.js
+++ b/browser/extensions/screenshots/webextension/domainFromUrl.js
@@ -9,20 +9,20 @@ this.domainFromUrl = (function() {
return function urlDomainForId(location) { // eslint-disable-line no-unused-vars
let domain = location.hostname;
if (!domain) {
domain = location.origin.split(":")[0];
if (!domain) {
domain = "unknown";
}
}
- if (domain.search(/^[a-z0-9.\-]{1,1000}$/i) === -1) {
+ if (domain.search(/^[a-z0-9.-]{1,1000}$/i) === -1) {
// Probably a unicode domain; we could use punycode but it wouldn't decode
// well in the URL anyway. Instead we'll punt.
- domain = domain.replace(/[^a-z0-9.\-]/ig, "");
+ domain = domain.replace(/[^a-z0-9.-]/ig, "");
if (!domain) {
domain = "site";
}
}
return domain;
};
})();
--- a/browser/extensions/screenshots/webextension/manifest.json
+++ b/browser/extensions/screenshots/webextension/manifest.json
@@ -1,12 +1,12 @@
{
"manifest_version": 2,
"name": "Firefox Screenshots",
- "version": "10.12.0",
+ "version": "16.0.0",
"description": "__MSG_addonDescription__",
"author": "__MSG_addonAuthorsList__",
"homepage_url": "https://github.com/mozilla-services/screenshots",
"applications": {
"gecko": {
"id": "screenshots@mozilla.org"
}
},
--- a/browser/extensions/screenshots/webextension/onboarding/slides.html
+++ b/browser/extensions/screenshots/webextension/onboarding/slides.html
@@ -3,17 +3,17 @@
<head>
<!-- onboarding.scss is automatically inserted here: -->
<style></style>
<!-- Here and in onboarding.scss use MOZ_EXTENSION/path to refer to local files -->
</head>
<body>
<div id="slide-overlay">
<!-- The current slide is set by having .active-slide-1, .active-slide-2, etc on #slide element: -->
- <div id="slide-container" data-number-of-slides="3" class="active-slide-1">
+ <div id="slide-container" data-number-of-slides="4" class="active-slide-1">
<div class="slide slide-1">
<!-- Note: all images must be listed in manifest.json.template under web_accessible_resources -->
<div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-1.png');"></div>
<div class="slide-content">
<div class="slide-content-aligner">
<h1><span><strong>Firefox</strong> Screenshots</span><sup>Beta</sup></h1>
<p data-l10n-id="tourBodyOne"></p>
</div>
@@ -23,30 +23,38 @@
<div class="slide slide-2">
<div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-2.png');"></div>
<div class="slide-content">
<h1 data-l10n-id="tourHeaderTwo"></h1>
<p data-l10n-id="tourBodyTwo"></p>
</div>
</div>
<div class="slide slide-3">
+ <div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-3.png');"></div>
+ <div class="slide-content">
+ <h1 data-l10n-id="tourHeaderThree"></h1>
+ <p data-l10n-id="tourBodyThree"></p>
+ </div>
+ </div>
+ <div class="slide slide-4">
<div class="slide-image" style="background-image: url('MOZ_EXTENSION/icons/onboarding-4.png');"></div>
<div class="slide-content">
<h1 data-l10n-id="tourHeaderFour"></h1>
<p data-l10n-id="tourBodyFour"></p>
</div>
</div>
<!-- Clickable elements should be buttons for accessibility -->
<button id="skip" data-l10n-id="tourSkip" tabindex=1>Skip</button>
<button id="prev" tabindex=2 data-l10n-label-id="tourPrevious"></button>
<button id="next" tabindex=3 data-l10n-label-id="tourNext"></button>
<button id="done" tabindex=4 data-l10n-label-id="tourDone"></button>
<div id="slide-status-container">
<button class="goto-slide goto-slide-1" data-number="1" tabindex=4></button>
<button class="goto-slide goto-slide-2" data-number="2" tabindex=5></button>
<button class="goto-slide goto-slide-3" data-number="3" tabindex=6></button>
+ <button class="goto-slide goto-slide-4" data-number="4" tabindex=7></button>
</div>
<!-- FIXME: Need to put in privacy / etc links -->
</div>
</div>
</body>
</html>
--- a/browser/extensions/screenshots/webextension/onboarding/slides.js
+++ b/browser/extensions/screenshots/webextension/onboarding/slides.js
@@ -27,17 +27,17 @@ this.slides = (function() {
iframe.style.border = "none";
iframe.style.position = "fixed";
iframe.style.top = "0";
iframe.style.left = "0";
iframe.style.margin = "0";
iframe.scrolling = "no";
updateIframeSize();
let html = onboardingHtml.replace('<style></style>', `<style>${onboardingCss}</style>`);
- html = html.replace(/MOZ_EXTENSION([^\"]+)/g, (match, filename) => {
+ html = html.replace(/MOZ_EXTENSION([^"]+)/g, (match, filename) => {
return browser.extension.getURL(filename);
});
iframe.addEventListener("load", catcher.watchFunction(() => {
doc = iframe.contentDocument;
assertIsBlankDocument(doc);
let parsedDom = (new DOMParser()).parseFromString(
html,
"text/html"
@@ -94,17 +94,17 @@ this.slides = (function() {
[termsSentinel]: browser.i18n.getMessage("termsAndPrivacyNoticeTermsLink"),
[privacySentinel]: browser.i18n.getMessage("termsAndPrivacyNoticyPrivacyLink")
};
let linkUrls = {
[termsSentinel]: "https://www.mozilla.org/about/legal/terms/services/",
[privacySentinel]: "https://www.mozilla.org/privacy/firefox/"
};
let text = browser.i18n.getMessage(
- "termsAndPrivacyNoticeCloudServices",
+ "termsAndPrivacyNotice2",
[sentinelSplitter + termsSentinel + sentinelSplitter,
sentinelSplitter + privacySentinel + sentinelSplitter]);
let parts = text.split(sentinelSplitter);
for (let part of parts) {
let el;
if (part === termsSentinel || part === privacySentinel) {
el = doc.createElement("a");
el.href = linkUrls[part];
--- a/browser/extensions/screenshots/webextension/selector/callBackground.js
+++ b/browser/extensions/screenshots/webextension/selector/callBackground.js
@@ -1,27 +1,27 @@
/* globals log */
"use strict";
this.callBackground = function callBackground(funcName, ...args) {
return browser.runtime.sendMessage({funcName, args}).then((result) => {
- if (result.type === "success") {
+ if (result && result.type === "success") {
return result.value;
- } else if (result.type === "error") {
- let exc = new Error(result.message);
+ } else if (result && result.type === "error") {
+ let exc = new Error(result.message || "Unknown error");
exc.name = "BackgroundError";
if ('errorCode' in result) {
exc.errorCode = result.errorCode;
}
if ('popupMessage' in result) {
exc.popupMessage = result.popupMessage;
}
throw exc;
} else {
log.error("Unexpected background result:", result);
- let exc = new Error(`Bad response type from background page: ${result.type || undefined}`);
- exc.resultType = result.type || "undefined";
+ let exc = new Error(`Bad response type from background page: ${result && result.type || undefined}`);
+ exc.resultType = result ? (result.type || "undefined") : "undefined result";
throw exc;
}
});
}
null;
--- a/browser/extensions/screenshots/webextension/selector/shooter.js
+++ b/browser/extensions/screenshots/webextension/selector/shooter.js
@@ -16,17 +16,17 @@ this.shooter = (function() { // eslint-d
function regexpEscape(str) {
// http://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
function sanitizeError(data) {
const href = new RegExp(regexpEscape(window.location.href), 'g');
- const origin = new RegExp(`${regexpEscape(window.location.origin)}[^\s",>]*`, 'g');
+ const origin = new RegExp(`${regexpEscape(window.location.origin)}[^ \t\n\r",>]*`, 'g');
const json = JSON.stringify(data)
.replace(href, 'REDACTED_HREF')
.replace(origin, 'REDACTED_URL');
const result = JSON.parse(json);
return result;
}
catcher.registerHandler((errorObj) => {
@@ -34,41 +34,46 @@ this.shooter = (function() { // eslint-d
});
catcher.watchFunction(() => {
let canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
let ctx = canvas.getContext('2d');
supportsDrawWindow = !!ctx.drawWindow;
})();
- function screenshotPage(selectedPos) {
+ let screenshotPage = exports.screenshotPage = function(selectedPos, captureType) {
if (!supportsDrawWindow) {
return null;
}
let height = selectedPos.bottom - selectedPos.top;
let width = selectedPos.right - selectedPos.left;
let canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
- canvas.width = width * window.devicePixelRatio;
- canvas.height = height * window.devicePixelRatio;
let ctx = canvas.getContext('2d');
- if (window.devicePixelRatio !== 1) {
+ if (captureType == 'fullPage') {
+ canvas.width = width;
+ canvas.height = height;
+ } else {
+ canvas.width = width * window.devicePixelRatio;
+ canvas.height = height * window.devicePixelRatio;
+ }
+ if (window.devicePixelRatio !== 1 && captureType != 'fullPage') {
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
ui.iframe.hide();
try {
ctx.drawWindow(window, selectedPos.left, selectedPos.top, width, height, "#fff");
} finally {
ui.iframe.unhide();
}
return canvas.toDataURL();
- }
+ };
let isSaving = null;
- exports.takeShot = function(captureType, selectedPos) {
+ exports.takeShot = function(captureType, selectedPos, url) {
// isSaving indicates we're aleady in the middle of saving
// we use a timeout so in the case of a failure the button will
// still start working again
if (Math.floor(selectedPos.left) == Math.floor(selectedPos.right) ||
Math.floor(selectedPos.top) == Math.floor(selectedPos.bottom)) {
let exc = new Error("Empty selection");
exc.popupMessage = "EMPTY_SELECTION";
exc.noReport = true;
@@ -87,17 +92,17 @@ this.shooter = (function() { // eslint-d
}
isSaving = null;
}, 1000);
selectedPos = selectedPos.asJson();
let captureText = "";
if (buildSettings.captureText) {
captureText = util.captureEnclosedText(selectedPos);
}
- let dataUrl = screenshotPage(selectedPos);
+ let dataUrl = url || screenshotPage(selectedPos, captureType);
if (dataUrl) {
shotObject.delAllClips();
shotObject.addClip({
createdDate: Date.now(),
image: {
url: dataUrl,
captureType,
text: captureText,
--- a/browser/extensions/screenshots/webextension/selector/ui.js
+++ b/browser/extensions/screenshots/webextension/selector/ui.js
@@ -38,17 +38,17 @@ this.ui = (function() { // eslint-disabl
el.classList.contains("full-page"))) {
return true;
}
el = el.parentNode;
}
return false;
}
- let substitutedCss = inlineSelectionCss.replace(/MOZ_EXTENSION([^\"]+)/g, (match, filename) => {
+ let substitutedCss = inlineSelectionCss.replace(/MOZ_EXTENSION([^"]+)/g, (match, filename) => {
return browser.extension.getURL(filename);
});
function makeEl(tagName, className) {
if (!iframe.document()) {
throw new Error("Attempted makeEl before iframe was initialized");
}
let el = iframe.document().createElement(tagName);
@@ -62,44 +62,60 @@ this.ui = (function() { // eslint-disabl
if (this.sizeTracking.windowDelayer) {
clearTimeout(this.sizeTracking.windowDelayer);
}
this.sizeTracking.windowDelayer = setTimeout(watchFunction(() => {
this.updateElementSize(true);
}), 50);
}
+ function localizeText(doc) {
+ let els = doc.querySelectorAll("[data-l10n-id]");
+ for (let el of els) {
+ let id = el.getAttribute("data-l10n-id");
+ let text = browser.i18n.getMessage(id);
+ el.textContent = text;
+ }
+ }
+
+ function initializeIframe() {
+ let el = document.createElement("iframe");
+ el.src = browser.extension.getURL("blank.html");
+ el.style.zIndex = "99999999999";
+ el.style.border = "none";
+ el.style.top = "0";
+ el.style.left = "0";
+ el.style.margin = "0";
+ el.scrolling = "no";
+ el.style.clip = "auto";
+ return el;
+ }
+
let iframeSelection = exports.iframeSelection = {
element: null,
addClassName: "",
sizeTracking: {
timer: null,
windowDelayer: null,
lastHeight: null,
lastWidth: null
},
document: null,
display(installHandlerOnDocument) {
return new Promise((resolve, reject) => {
if (!this.element) {
- this.element = document.createElement("iframe");
- this.element.src = browser.extension.getURL("blank.html");
+ this.element = initializeIframe();
this.element.id = "firefox-screenshots-selection-iframe";
this.element.style.display = "none";
- this.element.style.zIndex = "99999999999";
- this.element.style.border = "none";
- this.element.style.position = "absolute";
- this.element.style.top = "0";
- this.element.style.left = "0";
- this.element.style.margin = "0";
- this.element.scrolling = "no";
+ this.element.style.setProperty('position', 'absolute', 'important');
this.updateElementSize();
this.element.addEventListener("load", watchFunction(() => {
this.document = this.element.contentDocument;
assertIsBlankDocument(this.document);
+ // eslint-disable-next-line no-unsanitized/property
this.document.documentElement.innerHTML = `
<head>
<style>${substitutedCss}</style>
<title></title>
</head>
<body></body>`;
installHandlerOnDocument(this.document);
if (this.addClassName) {
@@ -198,97 +214,84 @@ this.ui = (function() { // eslint-disabl
}
};
iframeSelection.onResize = watchFunction(assertIsTrusted(onResize.bind(iframeSelection)), true);
let iframePreSelection = exports.iframePreSelection = {
element: null,
document: null,
- sizeTracking: {
- windowDelayer: null
- },
display(installHandlerOnDocument, standardOverlayCallbacks) {
return new Promise((resolve, reject) => {
if (!this.element) {
- this.element = document.createElement("iframe");
- this.element.src = browser.extension.getURL("blank.html");
+ this.element = initializeIframe();
this.element.id = "firefox-screenshots-preselection-iframe";
- this.element.style.zIndex = "99999999999";
- this.element.style.border = "none";
- this.element.style.position = "fixed";
- this.element.style.top = "0";
- this.element.style.left = "0";
- this.element.style.margin = "0";
- this.element.scrolling = "no";
- this.updateElementSize();
+ this.element.style.setProperty('position', 'fixed', 'important');
+ this.element.style.width = "100%";
+ this.element.style.height = "100%";
this.element.addEventListener("load", watchFunction(() => {
this.document = this.element.contentDocument;
assertIsBlankDocument(this.document)
+ // eslint-disable-next-line no-unsanitized/property
this.document.documentElement.innerHTML = `
<head>
<style>${substitutedCss}</style>
<title></title>
</head>
<body>
<div class="preview-overlay">
<div class="fixed-container">
<div class="face-container">
<div class="eye left"><div class="eyeball"></div></div>
<div class="eye right"><div class="eyeball"></div></div>
<div class="face"></div>
</div>
- <div class="preview-instructions"></div>
+ <div class="preview-instructions" data-l10n-id="screenshotInstructions"></div>
<div class="myshots-all-buttons-container">
- <button class="myshots-button myshots-link" tabindex="1"></button>
+ <button class="myshots-button myshots-link" tabindex="1" data-l10n-id="myShotsLink"></button>
+ <div class="spacer"></div>
+ <button class="myshots-button visible" tabindex="2" data-l10n-id="saveScreenshotVisibleArea"></button>
+ <button class="myshots-button full-page" tabindex="3" data-l10n-id="saveScreenshotFullPage"></button>
</div>
</div>
</div>
</body>`;
installHandlerOnDocument(this.document);
if (this.addClassName) {
this.document.body.className = this.addClassName;
}
this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir");
this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale");
const overlay = this.document.querySelector(".preview-overlay");
- overlay.querySelector(".preview-instructions").textContent = browser.i18n.getMessage("screenshotInstructions");
- overlay.querySelector(".myshots-link").textContent = browser.i18n.getMessage("myShotsLink");
+ localizeText(this.document);
overlay.querySelector(".myshots-button").addEventListener(
"click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onOpenMyShots)));
+ overlay.querySelector(".visible").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onClickVisible)));
+ overlay.querySelector(".full-page").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onClickFullPage)));
resolve();
}), {once: true});
document.body.appendChild(this.element);
this.unhide();
} else {
resolve();
}
});
},
- updateElementSize() {
- if (!this.element) {
- // This can happen if the selector is unloaded during the resize adjustment
- // time-delay
- return;
- }
- this.element.style.height = window.innerHeight + "px";
- this.element.style.width = window.innerWidth + "px";
- },
-
hide() {
window.removeEventListener("scroll", watchFunction(assertIsTrusted(this.onScroll)));
window.removeEventListener("resize", this.onResize, true);
if (this.element) {
this.element.style.display = "none";
}
},
unhide() {
- this.updateElementSize();
window.addEventListener("scroll", watchFunction(assertIsTrusted(this.onScroll)));
window.addEventListener("resize", this.onResize, true);
this.element.style.display = "";
this.element.focus();
},
onScroll() {
exports.HoverBox.hide();
@@ -308,23 +311,94 @@ this.ui = (function() { // eslint-disabl
remove() {
this.hide();
util.removeNode(this.element);
this.element = null;
this.document = null;
}
};
+ let iframePreview = exports.iframePreview = {
+ element: null,
+ document: null,
+ display(installHandlerOnDocument, standardOverlayCallbacks) {
+ return new Promise((resolve, reject) => {
+ if (!this.element) {
+ this.element = initializeIframe();
+ this.element.id = "firefox-screenshots-preview-iframe";
+ this.element.style.display = "none";
+ this.element.style.setProperty('position', 'fixed', 'important');
+ this.element.style.height = "100%";
+ this.element.style.width = "100%";
+ this.element.onload = watchFunction(() => {
+ this.document = this.element.contentDocument;
+ // eslint-disable-next-line no-unsanitized/property
+ this.document.documentElement.innerHTML = `
+ <head>
+ <style>${substitutedCss}</style>
+ <title></title>
+ </head>
+ <body>
+ <div class="preview-overlay">
+ <div class="preview-image">
+ <div class="preview-buttons">
+ <button class="highlight-button-cancel"></button>
+ <button class="highlight-button-download"></button>
+ <button class="preview-button-save" data-l10n-id="saveScreenshotSelectedArea"></button>
+ </div>
+ </div>
+ </div>
+ </body>`;
+ installHandlerOnDocument(this.document);
+ this.document.documentElement.dir = browser.i18n.getMessage("@@bidi_dir");
+ this.document.documentElement.lang = browser.i18n.getMessage("@@ui_locale");
+ localizeText(this.document);
+ const overlay = this.document.querySelector(".preview-overlay");
+ overlay.querySelector(".highlight-button-download").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onDownloadPreview)));
+ overlay.querySelector(".preview-button-save").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.onSavePreview)));
+ overlay.querySelector(".highlight-button-cancel").addEventListener(
+ "click", watchFunction(assertIsTrusted(standardOverlayCallbacks.cancel)));
+ resolve();
+ });
+ document.body.appendChild(this.element);
+ } else {
+ resolve();
+ }
+ });
+ },
+
+ hide() {
+ if (this.element) {
+ this.element.style.display = "none";
+ }
+ },
+
+ unhide() {
+ this.element.style.display = "";
+ this.element.focus();
+ },
+
+ remove() {
+ this.hide();
+ util.removeNode(this.element);
+ this.element = null;
+ this.document = null;
+ }
+ };
+
iframePreSelection.onResize = watchFunction(onResize.bind(iframePreSelection), true);
let iframe = exports.iframe = {
currentIframe: iframePreSelection,
display(installHandlerOnDocument, standardOverlayCallbacks) {
return iframeSelection.display(installHandlerOnDocument)
- .then(() => iframePreSelection.display(installHandlerOnDocument, standardOverlayCallbacks));
+ .then(() => iframePreSelection.display(installHandlerOnDocument, standardOverlayCallbacks))
+ .then(() => iframePreview.display(installHandlerOnDocument, standardOverlayCallbacks));
},
hide() {
this.currentIframe.hide();
},
unhide() {
this.currentIframe.unhide();
@@ -332,36 +406,45 @@ this.ui = (function() { // eslint-disabl
getElementFromPoint(x, y) {
return this.currentIframe.getElementFromPoint(x, y);
},
remove() {
iframeSelection.remove();
iframePreSelection.remove();
+ iframePreview.remove();
},
document() {
return this.currentIframe.document;
},
useSelection() {
- if (this.currentIframe === iframePreSelection) {
+ if (this.currentIframe === iframePreSelection || this.currentIframe === iframePreview) {
this.hide();
}
this.currentIframe = iframeSelection;
this.unhide();
},
usePreSelection() {
- if (this.currentIframe === iframeSelection) {
+ if (this.currentIframe === iframeSelection || this.currentIframe === iframePreview) {
this.hide();
}
this.currentIframe = iframePreSelection;
this.unhide();
+ },
+
+ usePreview() {
+ if (this.currentIframe === iframeSelection || this.currentIframe === iframePreSelection) {
+ this.hide();
+ }
+ this.currentIframe = iframePreview;
+ this.unhide();
}
};
let movements = ["topLeft", "top", "topRight", "left", "right", "bottomLeft", "bottom", "bottomRight"];
/** Creates the selection box */
exports.Box = {
@@ -398,17 +481,16 @@ this.ui = (function() { // eslint-disabl
}));
this.download.style.display = "";
} else {
this.download.style.display = "none";
}
let bodyRect = getBodyRect();
// Note, document.documentElement.scrollHeight is zero on some strange pages (such as the page created when you load an image):
let docHeight = Math.max(document.documentElement.scrollHeight || 0, document.body.scrollHeight);
- let docWidth = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
let winBottom = window.innerHeight;
let pageYOffset = window.pageYOffset;
if ((pos.right - pos.left) < 78 || (pos.bottom - pos.top) < 78) {
this.el.classList.add("small-selection");
} else {
this.el.classList.remove("small-selection");
@@ -429,29 +511,29 @@ this.ui = (function() { // eslint-disabl
}
this.el.style.top = (pos.top - bodyRect.top) + "px";
this.el.style.left = (pos.left - bodyRect.left) + "px";
this.el.style.height = (pos.bottom - pos.top - bodyRect.top) + "px";
this.el.style.width = (pos.right - pos.left - bodyRect.left) + "px";
this.bgTop.style.top = "0px";
this.bgTop.style.height = (pos.top - bodyRect.top) + "px";
this.bgTop.style.left = "0px";
- this.bgTop.style.width = docWidth + "px";
+ this.bgTop.style.width = "100%";
this.bgBottom.style.top = (pos.bottom - bodyRect.top) + "px";
this.bgBottom.style.height = docHeight - (pos.bottom - bodyRect.top) + "px";
this.bgBottom.style.left = "0px";
- this.bgBottom.style.width = docWidth + "px";
+ this.bgBottom.style.width = "100%";
this.bgLeft.style.top = (pos.top - bodyRect.top) + "px";
this.bgLeft.style.height = pos.bottom - pos.top + "px";
this.bgLeft.style.left = "0px";
this.bgLeft.style.width = (pos.left - bodyRect.left) + "px";
this.bgRight.style.top = (pos.top - bodyRect.top) + "px";
this.bgRight.style.height = pos.bottom - pos.top + "px";
this.bgRight.style.left = (pos.right - bodyRect.left) + "px";
- this.bgRight.style.width = docWidth - (pos.right - bodyRect.left) + "px";
+ this.bgRight.style.width = "100%";
if (!(this.isElementInViewport(this.buttons))) {
this.cancel.style.position = this.download.style.position = this.save.style.position = "fixed";
this.cancel.style.left = (pos.left - bodyRect.left - 50) + "px";
this.download.style.left = ((pos.left - bodyRect.left - 100)) + "px";
this.save.style.left = ((pos.left - bodyRect.left) - 190) + "px";
this.cancel.style.top = this.download.style.top = this.save.style.top = (pos.top - bodyRect.top) + "px";
} else {
@@ -552,16 +634,21 @@ this.ui = (function() { // eslint-disabl
},
isElementInViewport(el) {
let rect = el.getBoundingClientRect();
return (rect.right <= window.innerWidth);
},
clearSaveDisabled() {
+ if (!this.save) {
+ // Happens if we try to remove the disabled status after the worker
+ // has been shut down
+ return;
+ }
this.save.removeAttribute("disabled");
},
el: null,
boxTopEl: null,
boxLeftEl: null,
boxRightEl: null,
boxBottomEl: null
@@ -614,16 +701,24 @@ this.ui = (function() { // eslint-disabl
this.el.style.left = (xPos + 12) + "px";
},
remove() {
util.removeNode(this.el);
this.el = this.xEl = this.yEl = null;
}
};
+ exports.Preview = {
+ display(dataUrl) {
+ let img = makeEl("IMG");
+ img.src = dataUrl;
+ iframe.document().querySelector(".preview-image").appendChild(img);
+ }
+ };
+
/** Removes every UI this module creates */
exports.remove = function() {
for (let name in exports) {
if (name.startsWith("iframe")) {
continue;
}
if (typeof exports[name] == "object" && exports[name].remove) {
exports[name].remove();
--- a/browser/extensions/screenshots/webextension/selector/uicontrol.js
+++ b/browser/extensions/screenshots/webextension/selector/uicontrol.js
@@ -19,16 +19,18 @@ this.uicontrol = (function() {
"dragging":
The user has pressed down a mouse button, and is dragging out an area far enough to show a selection
"selected":
The user has selected an area
"resizing":
The user is resizing the selection
"cancelled":
Everything has been cancelled
+ "previewing":
+ The user is previewing the full-screen/visible image
A mousedown goes from crosshairs to dragging.
A mouseup goes from dragging to selected
A click outside of the selection goes from selected to crosshairs
A click on one of the draggers goes from selected to resizing
State variables:
@@ -115,44 +117,51 @@ this.uicontrol = (function() {
H1: true,
H2: true,
H3: true,
H4: true,
H5: true,
H6: true
};
+ let captureType;
+
let standardDisplayCallbacks = {
cancel: () => {
sendEvent("cancel-shot", "overlay-cancel-button");
exports.deactivate();
}, save: () => {
sendEvent("save-shot", "overlay-save-button");
shooter.takeShot("selection", selectedPos);
}, download: () => {
sendEvent("download-shot", "overlay-download-button");
shooter.downloadShot(selectedPos);
}
};
let standardOverlayCallbacks = {
+ cancel: () => {
+ sendEvent("cancel-shot", "cancel-preview-button");
+ exports.deactivate();
+ },
onOpenMyShots: () => {
sendEvent("goto-myshots", "selection-button");
callBackground("openMyShots")
.then(() => exports.deactivate())
.catch(() => {
// Handled in communication.js
});
},
onClickVisible: () => {
sendEvent("capture-visible", "selection-button");
selectedPos = new Selection(
window.scrollX, window.scrollY,
window.scrollX + window.innerWidth, window.scrollY + window.innerHeight);
- shooter.takeShot("visible", selectedPos);
+ captureType = 'visible';
+ setState("previewing");
},
onClickFullPage: () => {
sendEvent("capture-full-page", "selection-button");
let width = Math.max(
document.body.clientWidth,
document.documentElement.clientWidth,
document.body.scrollWidth,
document.documentElement.scrollWidth);
@@ -161,19 +170,28 @@ this.uicontrol = (function() {
document.body.clientHeight,
document.documentElement.clientHeight,
document.body.scrollHeight,
document.documentElement.scrollHeight);
height = Math.min(height, MAX_PAGE_HEIGHT);
selectedPos = new Selection(
0, 0,
width, height);
- shooter.takeShot("fullPage", selectedPos);
+ captureType = 'fullPage';
+ setState("previewing");
+ },
+ onSavePreview: () => {
+ sendEvent(`save-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "save-preview-button");
+ shooter.takeShot(captureType, selectedPos, dataUrl);
+ },
+ onDownloadPreview: () => {
+ sendEvent(`download-${captureType.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`, "download-preview-button");
+ shooter.downloadShot(selectedPos);
}
- }
+ };
/** Holds all the objects that handle events for each state: */
let stateHandlers = {};
function getState() {
return getState.state;
}
getState.state = "cancel";
@@ -339,16 +357,26 @@ this.uicontrol = (function() {
return Math.sqrt(Math.pow(this.x - x, 2), Math.pow(this.y - y));
}
}
/** *********************************************
* all stateHandlers
*/
+ let dataUrl;
+
+ stateHandlers.previewing = {
+ start() {
+ dataUrl = shooter.screenshotPage(selectedPos, captureType);
+ ui.iframe.usePreview();
+ ui.Preview.display(dataUrl);
+ }
+ };
+
stateHandlers.onboarding = {
start() {
if (typeof slides == "undefined") {
throw new Error("Attempted to set state to onboarding without loading slides");
}
catcher.watchPromise(slides.display({
onEnd: this.slidesOnEnd.bind(this)
}));