fix: print note to PDF bug
This commit is contained in:
parent
c0c58a5ea6
commit
a4d58600cc
|
|
@ -0,0 +1,375 @@
|
|||
/* 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/. */
|
||||
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
var { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
SubDialogManager: "resource://gre/modules/SubDialog.jsm",
|
||||
});
|
||||
|
||||
// Load PrintUtils lazily and modify it to suit.
|
||||
XPCOMUtils.defineLazyGetter(this, "PrintUtils", () => {
|
||||
let scope = {};
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://global/content/printUtils.js",
|
||||
scope
|
||||
);
|
||||
scope.PrintUtils.getTabDialogBox = function(browser) {
|
||||
if (!browser.tabDialogBox) {
|
||||
browser.tabDialogBox = new TabDialogBox(browser);
|
||||
}
|
||||
return browser.tabDialogBox;
|
||||
};
|
||||
scope.PrintUtils.createBrowser = function({
|
||||
remoteType,
|
||||
initialBrowsingContextGroupId,
|
||||
userContextId,
|
||||
skipLoad,
|
||||
initiallyActive,
|
||||
} = {}) {
|
||||
let b = document.createXULElement("browser");
|
||||
// Use the JSM global to create the permanentKey, so that if the
|
||||
// permanentKey is held by something after this window closes, it
|
||||
// doesn't keep the window alive.
|
||||
b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
|
||||
|
||||
const defaultBrowserAttributes = {
|
||||
maychangeremoteness: "true",
|
||||
messagemanagergroup: "browsers",
|
||||
type: "content",
|
||||
};
|
||||
for (let attribute in defaultBrowserAttributes) {
|
||||
b.setAttribute(attribute, defaultBrowserAttributes[attribute]);
|
||||
}
|
||||
|
||||
if (userContextId) {
|
||||
b.setAttribute("usercontextid", userContextId);
|
||||
}
|
||||
|
||||
if (remoteType) {
|
||||
b.setAttribute("remoteType", remoteType);
|
||||
b.setAttribute("remote", "true");
|
||||
}
|
||||
|
||||
// Ensure that the browser will be created in a specific initial
|
||||
// BrowsingContextGroup. This may change the process selection behaviour
|
||||
// of the newly created browser, and is often used in combination with
|
||||
// "remoteType" to ensure that the initial about:blank load occurs
|
||||
// within the same process as another window.
|
||||
if (initialBrowsingContextGroupId) {
|
||||
b.setAttribute(
|
||||
"initialBrowsingContextGroupId",
|
||||
initialBrowsingContextGroupId
|
||||
);
|
||||
}
|
||||
|
||||
// We set large flex on both containers to allow the devtools toolbox to
|
||||
// set a flex attribute. We don't want the toolbox to actually take up free
|
||||
// space, but we do want it to collapse when the window shrinks, and with
|
||||
// flex=0 it can't. When the toolbox is on the bottom it's a sibling of
|
||||
// browserStack, and when it's on the side it's a sibling of
|
||||
// browserContainer.
|
||||
let stack = document.createXULElement("stack");
|
||||
stack.className = "browserStack";
|
||||
stack.appendChild(b);
|
||||
stack.setAttribute("flex", "10000");
|
||||
|
||||
let browserContainer = document.createXULElement("vbox");
|
||||
browserContainer.className = "browserContainer";
|
||||
browserContainer.appendChild(stack);
|
||||
browserContainer.setAttribute("flex", "10000");
|
||||
|
||||
let browserSidebarContainer = document.createXULElement("hbox");
|
||||
browserSidebarContainer.className = "browserSidebarContainer";
|
||||
browserSidebarContainer.appendChild(browserContainer);
|
||||
|
||||
// Prevent the superfluous initial load of a blank document
|
||||
// if we're going to load something other than about:blank.
|
||||
if (skipLoad) {
|
||||
b.setAttribute("nodefaultsrc", "true");
|
||||
}
|
||||
|
||||
return b;
|
||||
};
|
||||
return scope.PrintUtils;
|
||||
});
|
||||
|
||||
/**
|
||||
* The TabDialogBox supports opening window dialogs as SubDialogs on the tab and content
|
||||
* level. Both tab and content dialogs have their own separate managers.
|
||||
* Dialogs will be queued FIFO and cover the web content.
|
||||
* Dialogs are closed when the user reloads or leaves the page.
|
||||
* While a dialog is open PopupNotifications, such as permission prompts, are
|
||||
* suppressed.
|
||||
*/
|
||||
class TabDialogBox {
|
||||
constructor(browser) {
|
||||
this._weakBrowserRef = Cu.getWeakReference(browser);
|
||||
|
||||
// Create parent element for tab dialogs
|
||||
let template = document.getElementById("dialogStackTemplate");
|
||||
this.dialogStack = template.content.cloneNode(true).firstElementChild;
|
||||
this.dialogStack.classList.add("tab-prompt-dialog");
|
||||
|
||||
// This differs from Firefox by using a specific ancestor <stack> rather
|
||||
// than the parent of the <browser>, so that a larger area of the screen
|
||||
// is used for the preview.
|
||||
this.printPreviewStack = document.querySelector(".printPreviewStack");
|
||||
if (this.printPreviewStack && this.printPreviewStack.contains(browser)) {
|
||||
this.printPreviewStack.appendChild(this.dialogStack);
|
||||
} else {
|
||||
this.printPreviewStack = this.browser.parentNode;
|
||||
this.browser.parentNode.insertBefore(
|
||||
this.dialogStack,
|
||||
this.browser.nextElementSibling
|
||||
);
|
||||
}
|
||||
|
||||
// Initially the stack only contains the template
|
||||
let dialogTemplate = this.dialogStack.firstElementChild;
|
||||
|
||||
// Create dialog manager for prompts at the tab level.
|
||||
this._tabDialogManager = new SubDialogManager({
|
||||
dialogStack: this.dialogStack,
|
||||
dialogTemplate,
|
||||
orderType: SubDialogManager.ORDER_QUEUE,
|
||||
allowDuplicateDialogs: true,
|
||||
dialogOptions: {
|
||||
consumeOutsideClicks: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a dialog on tab or content level.
|
||||
* @param {String} aURL - URL of the dialog to load in the tab box.
|
||||
* @param {Object} [aOptions]
|
||||
* @param {String} [aOptions.features] - Comma separated list of window
|
||||
* features.
|
||||
* @param {Boolean} [aOptions.allowDuplicateDialogs] - Whether to allow
|
||||
* showing multiple dialogs with aURL at the same time. If false calls for
|
||||
* duplicate dialogs will be dropped.
|
||||
* @param {String} [aOptions.sizeTo] - Pass "available" to stretch dialog to
|
||||
* roughly content size.
|
||||
* @param {Boolean} [aOptions.keepOpenSameOriginNav] - By default dialogs are
|
||||
* aborted on any navigation.
|
||||
* Set to true to keep the dialog open for same origin navigation.
|
||||
* @param {Number} [aOptions.modalType] - The modal type to create the dialog for.
|
||||
* By default, we show the dialog for tab prompts.
|
||||
* @returns {Promise} - Resolves once the dialog has been closed.
|
||||
*/
|
||||
open(
|
||||
aURL,
|
||||
{
|
||||
features = null,
|
||||
allowDuplicateDialogs = true,
|
||||
sizeTo,
|
||||
keepOpenSameOriginNav,
|
||||
modalType = null,
|
||||
allowFocusCheckbox = false,
|
||||
} = {},
|
||||
...aParams
|
||||
) {
|
||||
return new Promise(resolve => {
|
||||
// Get the dialog manager to open the prompt with.
|
||||
let dialogManager =
|
||||
modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT
|
||||
? this.getContentDialogManager()
|
||||
: this._tabDialogManager;
|
||||
let hasDialogs =
|
||||
this._tabDialogManager.hasDialogs ||
|
||||
this._contentDialogManager?.hasDialogs;
|
||||
|
||||
if (!hasDialogs) {
|
||||
this._onFirstDialogOpen();
|
||||
}
|
||||
|
||||
let closingCallback = event => {
|
||||
if (!hasDialogs) {
|
||||
this._onLastDialogClose();
|
||||
}
|
||||
|
||||
if (allowFocusCheckbox && !event.detail?.abort) {
|
||||
this.maybeSetAllowTabSwitchPermission(event.target);
|
||||
}
|
||||
};
|
||||
|
||||
if (modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT) {
|
||||
sizeTo = "limitheight";
|
||||
}
|
||||
|
||||
// Open dialog and resolve once it has been closed
|
||||
let dialog = dialogManager.open(
|
||||
aURL,
|
||||
{
|
||||
features,
|
||||
allowDuplicateDialogs,
|
||||
sizeTo,
|
||||
closingCallback,
|
||||
closedCallback: resolve,
|
||||
},
|
||||
...aParams
|
||||
);
|
||||
|
||||
// Marking the dialog externally, instead of passing it as an option.
|
||||
// The SubDialog(Manager) does not care about navigation.
|
||||
// dialog can be null here if allowDuplicateDialogs = false.
|
||||
if (dialog) {
|
||||
dialog._keepOpenSameOriginNav = keepOpenSameOriginNav;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onFirstDialogOpen() {
|
||||
for (let element of this.printPreviewStack.children) {
|
||||
if (element != this.dialogStack) {
|
||||
element.setAttribute("tabDialogShowing", true);
|
||||
}
|
||||
}
|
||||
|
||||
// Register listeners
|
||||
this._lastPrincipal = this.browser.contentPrincipal;
|
||||
this.browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
|
||||
}
|
||||
|
||||
_onLastDialogClose() {
|
||||
for (let element of this.printPreviewStack.children) {
|
||||
if (element != this.dialogStack) {
|
||||
element.removeAttribute("tabDialogShowing");
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up listeners
|
||||
this.browser.removeProgressListener(this);
|
||||
this._lastPrincipal = null;
|
||||
}
|
||||
|
||||
_buildContentPromptDialog() {
|
||||
let template = document.getElementById("dialogStackTemplate");
|
||||
let contentDialogStack = template.content.cloneNode(true).firstElementChild;
|
||||
contentDialogStack.classList.add("content-prompt-dialog");
|
||||
|
||||
// Create a dialog manager for content prompts.
|
||||
let tabPromptDialog = this.browser.parentNode.querySelector(
|
||||
".tab-prompt-dialog"
|
||||
);
|
||||
this.browser.parentNode.insertBefore(contentDialogStack, tabPromptDialog);
|
||||
|
||||
let contentDialogTemplate = contentDialogStack.firstElementChild;
|
||||
this._contentDialogManager = new SubDialogManager({
|
||||
dialogStack: contentDialogStack,
|
||||
dialogTemplate: contentDialogTemplate,
|
||||
orderType: SubDialogManager.ORDER_QUEUE,
|
||||
allowDuplicateDialogs: true,
|
||||
dialogOptions: {
|
||||
consumeOutsideClicks: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
if (event.type !== "TabClose") {
|
||||
return;
|
||||
}
|
||||
this.abortAllDialogs();
|
||||
}
|
||||
|
||||
abortAllDialogs() {
|
||||
this._tabDialogManager.abortDialogs();
|
||||
this._contentDialogManager?.abortDialogs();
|
||||
}
|
||||
|
||||
focus() {
|
||||
// Prioritize focusing the dialog manager for tab prompts
|
||||
if (this._tabDialogManager._dialogs.length) {
|
||||
this._tabDialogManager.focusTopDialog();
|
||||
return;
|
||||
}
|
||||
this._contentDialogManager?.focusTopDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user navigates away or refreshes the page, close all dialogs for
|
||||
* the current browser.
|
||||
*/
|
||||
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
|
||||
if (
|
||||
!aWebProgress.isTopLevel ||
|
||||
aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dialogs can be exempt from closing on same origin location change.
|
||||
let filterFn;
|
||||
|
||||
// Test for same origin location change
|
||||
if (
|
||||
this._lastPrincipal?.isSameOrigin(
|
||||
aLocation,
|
||||
this.browser.browsingContext.usePrivateBrowsing
|
||||
)
|
||||
) {
|
||||
filterFn = dialog => !dialog._keepOpenSameOriginNav;
|
||||
}
|
||||
|
||||
this._lastPrincipal = this.browser.contentPrincipal;
|
||||
|
||||
this._tabDialogManager.abortDialogs(filterFn);
|
||||
this._contentDialogManager?.abortDialogs(filterFn);
|
||||
}
|
||||
|
||||
get tab() {
|
||||
return document.getElementById("tabmail").getTabForBrowser(this.browser);
|
||||
}
|
||||
|
||||
get browser() {
|
||||
let browser = this._weakBrowserRef.get();
|
||||
if (!browser) {
|
||||
throw new Error("Stale dialog box! The associated browser is gone.");
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
getTabDialogManager() {
|
||||
return this._tabDialogManager;
|
||||
}
|
||||
|
||||
getContentDialogManager() {
|
||||
if (!this._contentDialogManager) {
|
||||
this._buildContentPromptDialog();
|
||||
}
|
||||
return this._contentDialogManager;
|
||||
}
|
||||
|
||||
onNextPromptShowAllowFocusCheckboxFor(principal) {
|
||||
this._allowTabFocusByPromptPrincipal = principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "focus-tab-by-prompt" permission for the dialog.
|
||||
*/
|
||||
maybeSetAllowTabSwitchPermission(dialog) {
|
||||
let checkbox = dialog.querySelector("checkbox");
|
||||
|
||||
if (checkbox.checked) {
|
||||
Services.perms.addFromPrincipal(
|
||||
this._allowTabFocusByPromptPrincipal,
|
||||
"focus-tab-by-prompt",
|
||||
Services.perms.ALLOW_ACTION
|
||||
);
|
||||
}
|
||||
|
||||
// Don't show the "allow tab switch checkbox" for subsequent prompts.
|
||||
this._allowTabFocusByPromptPrincipal = null;
|
||||
}
|
||||
}
|
||||
|
||||
TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([
|
||||
"nsIWebProgressListener",
|
||||
"nsISupportsWeakReference",
|
||||
]);
|
||||
|
|
@ -19,18 +19,6 @@
|
|||
// Add message to print window
|
||||
window.onmessage = async function (e) {
|
||||
if (e.data.type === "print") {
|
||||
var { Services } = ChromeUtils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
);
|
||||
var { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm",
|
||||
);
|
||||
window.XPCOMUtils = XPCOMUtils;
|
||||
delete window.PrintPreview;
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://global/content/printUtils.js",
|
||||
window,
|
||||
);
|
||||
// Serialize the HTMLDocument as an XHTML string
|
||||
var parser = new DOMParser();
|
||||
var serializer = new XMLSerializer();
|
||||
|
|
@ -38,24 +26,6 @@
|
|||
var htmlDoc = parser.parseFromString(htmlString, "text/html");
|
||||
var xhtmlString = serializer.serializeToString(htmlDoc);
|
||||
document.querySelector(".markdown-body").innerHTML = xhtmlString;
|
||||
const settings = PrintUtils.getPrintSettings("", false);
|
||||
const doPrint = await PrintUtils.handleSystemPrintDialog(
|
||||
window.browsingContext.topChromeWindow,
|
||||
false,
|
||||
settings,
|
||||
);
|
||||
if (doPrint) {
|
||||
window.browsingContext.print(settings);
|
||||
// An ugly hack to close the browser window that has a static clone
|
||||
// of the content that is being printed. Without this, the window
|
||||
// will be open while transferring the content into system print queue,
|
||||
// which can take time for large PDF files
|
||||
const emptyWin =
|
||||
Services.wm.getMostRecentWindow("navigator:browser");
|
||||
if (emptyWin?.document?.getElementById("statuspanel")) {
|
||||
emptyWin.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- prettier-ignore -->
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
browser {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var browser;
|
||||
var { Services } = ChromeUtils.import(
|
||||
"resource://gre/modules/Services.jsm",
|
||||
);
|
||||
var { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm",
|
||||
);
|
||||
window.XPCOMUtils = XPCOMUtils;
|
||||
|
||||
const scope = {};
|
||||
Services.scriptloader.loadSubScript(
|
||||
// Borrowed from https://github.com/mozilla/releases-comm-central/commit/32a80889f13918c8776e2a3cf42abb85f6d84cdd#diff-6bebd8bc8a129ee1f12c757394ed9d549f3a20296277cd95b9cc0c3c5d75b739
|
||||
"chrome://__addonRef__/content/lib/js/printUtils.js",
|
||||
scope,
|
||||
);
|
||||
window.PrintUtils = scope.PrintUtils;
|
||||
|
||||
const args = window.arguments[0];
|
||||
let loaded = false;
|
||||
document.addEventListener("DOMContentLoaded", async (ev) => {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
browser = PrintUtils.createBrowser();
|
||||
document.body.appendChild(browser);
|
||||
browser.contentWindow.location.href = args.url;
|
||||
console.log("loading");
|
||||
loaded = true;
|
||||
await waitUtilAsync(() => {
|
||||
console.log(browser, browser.contentWindow);
|
||||
return browser.contentWindow?.document.readyState === "complete";
|
||||
});
|
||||
console.log("loaded");
|
||||
args.browser = browser;
|
||||
args._initPromise.resolve();
|
||||
});
|
||||
async function waitUtilAsync(condition, interval = 100, timeout = 10000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const intervalId = setInterval(() => {
|
||||
if (condition()) {
|
||||
clearInterval(intervalId);
|
||||
resolve();
|
||||
} else if (Date.now() - start > timeout) {
|
||||
clearInterval(intervalId);
|
||||
reject();
|
||||
}
|
||||
}, interval);
|
||||
});
|
||||
}
|
||||
window.print = async () => {
|
||||
scope.PrintUtils.startPrintWindow(browser.browsingContext);
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
@ -1,18 +1,24 @@
|
|||
import { config } from "../../../package.json";
|
||||
import { showHint } from "../../utils/hint";
|
||||
import { renderNoteHTML } from "../../utils/note";
|
||||
import { waitUtilAsync } from "../../utils/wait";
|
||||
|
||||
export async function savePDF(noteId: number) {
|
||||
const html = await renderNoteHTML(Zotero.Items.get(noteId));
|
||||
disablePrintFooterHeader();
|
||||
const args = {
|
||||
_initPromise: Zotero.Promise.defer(),
|
||||
browser: undefined as any,
|
||||
url: `chrome://${config.addonRef}/content/printTemplate.xhtml`,
|
||||
};
|
||||
const win = window.openDialog(
|
||||
`chrome://${config.addonRef}/content/pdfPrinter.xhtml`,
|
||||
`${config.addonRef}-pdfPrinter`,
|
||||
`chrome://${config.addonRef}/content/printWrapper.xhtml`,
|
||||
`${config.addonRef}-printWrapper`,
|
||||
`chrome,centerscreen,resizable,status,width=900,height=650,dialog=no`,
|
||||
args,
|
||||
)!;
|
||||
await waitUtilAsync(() => win.document.readyState === "complete");
|
||||
win.postMessage({ type: "print", html }, "*");
|
||||
await args._initPromise.promise;
|
||||
args.browser?.contentWindow.postMessage({ type: "print", html }, "*");
|
||||
win.print();
|
||||
showHint("Note Saved as PDF");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue