Improve sharing via Print (#7728)

List of changes:
* The temporary document for printing is now in an `<iframe>` instead of a new tab
* The whole `<head>` element is copied to the temporary document, except for `<script>` tags to copy over the `<meta>` tags as well
* URLs that contain the instance base URL are now removed from the printed PDF
* The saved filename (PDF) will now default to the article title
* `<details>` is auto expanded
* Styling:
   * The main document's `<html>` class is copied over to preserve some styling that might use those classes
   * Instead of writing `content_el.innerHTML` to the temporary document, `content_el.outerHTML` is now written instead to apply the styles that select `.content`
   * `.dropdown-menu` is now hidden in the printed document, because it can't be expanded anyway
   * Headers and footers are hidden in the printed document
* The printed document will now display correctly all the time, by waiting for it to load before calling `print()`
   * Before, the stylesheets might've not finished loading and the document was broken
* Better browser support on mobile for this feature
   * Before, the document would fail to print on Chrome Mobile
   
Tested on:
* Firefox - both desktop and mobile, works 
* Chrome - both desktop and mobile, works 
* Opera - desktop, works (same as Chrome) 
* Brave - both desktop and mobile (same as Chrome), works 
* Safari - both desktop and mobile, works
* Microsoft Edge - both desktop and mobile, works 
* GNOME Web - desktop, works 
* SeaMonkey - desktop, works 

Known issues:
* Images may not finish loading the first time the print dialog is opened

TODO:
* [x] Test on Safari
* [x] Try to fix GNOME Web
This commit is contained in:
Inverle
2025-08-06 21:49:13 +02:00
committed by GitHub
parent 9faf2c1fa3
commit 149136fbe2
3 changed files with 111 additions and 15 deletions

View File

@@ -37,6 +37,7 @@ function xmlHttpRequestJson(req) {
// <Global context>
/* eslint-disable no-var */
var context;
var prevTitle;
/* eslint-enable no-var */
(function parseJsonVars() {
@@ -155,9 +156,10 @@ function incUnreadsFeed(article, feed_id, nb) {
}
}
let isCurrentView = false;
// Update unread: title
document.title = document.title.replace(/^((?:\([\s0-9]+\) )?)/, function (m, p1) {
let isCurrentView = false;
const currentTitle = prevTitle || document.title;
const newTitle = currentTitle.replace(/^((?:\([\s0-9]+\) )?)/, function (m, p1) {
const feed = document.getElementById(feed_id);
if (article || (feed && feed.closest('.active'))) {
isCurrentView = true;
@@ -169,6 +171,11 @@ function incUnreadsFeed(article, feed_id, nb) {
return p1;
}
});
if (prevTitle) {
prevTitle = newTitle;
} else {
document.title = newTitle;
}
return isCurrentView;
}
@@ -1235,24 +1242,79 @@ function init_stream(stream) {
el = ev.target.closest('.item.share > button[data-type="print"]');
if (el) { // Print
const tmp_window = window.open();
for (let i = 0; i < document.styleSheets.length; i++) {
tmp_window.document.writeln('<link href="' + document.styleSheets[i].href + '" rel="stylesheet" type="text/css" />');
}
const html = document.documentElement;
const head = document.head.cloneNode(true);
head.querySelectorAll('script').forEach(js => js.remove());
const flux_content = el.closest('.flux_content');
let content_el = null;
if (flux_content) {
content_el = el.closest('.flux_content').querySelector('.content');
content_el = el.closest('.flux_content').querySelector('.content').cloneNode(true);
}
if (content_el === null) {
content_el = el.closest('.flux').querySelector('.flux_content .content');
content_el = el.closest('.flux').querySelector('.flux_content .content').cloneNode(true);
}
content_el.querySelectorAll('a').forEach(link => {
// Avoid leaking the instance URL in PDFs
if (link.href.startsWith(location.origin)) {
link.removeAttribute('href');
}
});
content_el.querySelectorAll('details').forEach(el => el.setAttribute('open', 'open'));
const articleTitle = content_el.querySelector('.title a').innerText;
prevTitle = document.title;
// Chrome uses the parent's title to get the PDF save filename
document.title = articleTitle;
// Firefox uses the iframe's title to get the PDF save filename
// Note: Firefox Mobile saves PDFs with a filename that looks like: `temp[19 random digits].PDF` regardless of title
head.querySelector('title').innerText = articleTitle;
loadLazyImages(content_el);
tmp_window.document.writeln(content_el.innerHTML);
tmp_window.document.close();
tmp_window.focus();
tmp_window.print();
tmp_window.close();
const print_frame = document.createElement('iframe');
print_frame.style.display = 'none';
print_frame.srcdoc = `
<!DOCTYPE html>
<html class="${html.getAttribute('class')}">
<head>
${head.innerHTML}
</head>
<body>
${content_el.outerHTML}
</body>
</html>
`;
document.body.prepend(print_frame);
function afterPrint() {
print_frame.remove();
document.title = prevTitle;
prevTitle = '';
window.removeEventListener('focus', afterPrint);
}
print_frame.onload = () => {
const tmp_window = print_frame.contentWindow;
// Needed for Chrome
tmp_window.matchMedia('print').onchange = (e) => {
// UA check is needed to not trigger on Chrome Mobile
if (!e.matches && !navigator.userAgent.includes('Mobi')) {
afterPrint();
}
};
tmp_window.print();
};
// Needed for Firefox and Chrome Mobile
window.addEventListener('focus', afterPrint);
return false;
}

View File

@@ -2796,13 +2796,30 @@ html.slider-active {
/*============*/
@media print {
/* This hides the headers and footers in the printed document on Chrome */
/* Supported since Chrome 131: https://developer.chrome.com/release-notes/131#page_margin_boxes */
@page {
/* Firefox and Safari do not support those yet */
/* See: https://developer.mozilla.org/en-US/docs/Web/CSS/@page#browser_compatibility */
@top-left { content: ''; }
@top-right { content: ''; }
@bottom-left { content: ''; }
@bottom-right { content: ''; }
}
.header, .aside,
.nav_menu, .day,
.flux_header,
.flux_content .bottom,
.pagination,
#stream-footer,
#nav_entries {
#nav_entries,
.dropdown-toggle {
display: none;
}

View File

@@ -2796,13 +2796,30 @@ html.slider-active {
/*============*/
@media print {
/* This hides the headers and footers in the printed document on Chrome */
/* Supported since Chrome 131: https://developer.chrome.com/release-notes/131#page_margin_boxes */
@page {
/* Firefox and Safari do not support those yet */
/* See: https://developer.mozilla.org/en-US/docs/Web/CSS/@page#browser_compatibility */
@top-left { content: ''; }
@top-right { content: ''; }
@bottom-left { content: ''; }
@bottom-right { content: ''; }
}
.header, .aside,
.nav_menu, .day,
.flux_header,
.flux_content .bottom,
.pagination,
#stream-footer,
#nav_entries {
#nav_entries,
.dropdown-toggle {
display: none;
}