Bläddra i källkod

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
Inverle 7 månader sedan
förälder
incheckning
149136fbe2
3 ändrade filer med 111 tillägg och 15 borttagningar
  1. 75 13
      p/scripts/main.js
  2. 18 1
      p/themes/base-theme/frss.css
  3. 18 1
      p/themes/base-theme/frss.rtl.css

+ 75 - 13
p/scripts/main.js

@@ -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;
 		}
 

+ 18 - 1
p/themes/base-theme/frss.css

@@ -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;
 	}
 

+ 18 - 1
p/themes/base-theme/frss.rtl.css

@@ -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;
 	}