4,476
edits
No edit summary |
No edit summary |
||
Line 1,275: | Line 1,275: | ||
// Kill any previous print handlers | // Kill any previous print handlers | ||
$(document).off("click.print", "#print-button"); | $(document).off( | ||
"click.print", | |||
"#print-button, #print-with-border, #print-no-border" | |||
); | |||
$(document).on("click.print", "#print-button | // One handler for all three | ||
$(document).on( | |||
"click.print", | |||
"#print-button, #print-with-border, #print-no-border", | |||
function (e) { | |||
e.preventDefault(); | |||
var $btn = $(this); | |||
if ($btn.data("busy")) return; | |||
$btn.data("busy", true); | |||
// decide the border preference from the clicked button id | |||
// #print-no-border => no border; everything else => with border | |||
var borderPref = | |||
$btn.attr("id") === "print-no-border" ? "without" : "with"; | |||
preloadFontForPrint(); | |||
var title = window.currentEntryTitle; // e.g. "090" | |||
if (!title) { | |||
console.warn("[print] no currentEntryTitle"); | |||
window.print(); | |||
$btn.data("busy", false); | |||
return; | |||
} | |||
var pageUrl = mw.util.getUrl(title); | |||
var pageUrlFresh = cacheBust(pageUrl); | |||
console.log("[print] fetching page HTML:", pageUrlFresh); | |||
$.get(pageUrlFresh) | |||
.done(function (html) { | |||
var $tmp = $("<div>").html(html); | |||
var $print = $tmp.find(".print-only").first(); | |||
console.log("[print] .print-only found:", $print.length); | |||
if (!$print.length) { | |||
console.warn("[print] no .print-only found; fallback print"); | |||
window.print(); | |||
$btn.data("busy", false); | |||
return; | |||
} | |||
// Build hidden iframe | |||
var iframe = document.createElement("iframe"); | |||
iframe.style.position = "fixed"; | |||
iframe.style.right = "0"; | |||
iframe.style.bottom = "0"; | |||
iframe.style.width = "0"; | |||
iframe.style.height = "0"; | |||
iframe.style.border = "0"; | |||
document.body.appendChild(iframe); | |||
var doc = iframe.contentDocument || iframe.contentWindow.document; | |||
doc.open(); | |||
doc.write( | |||
'<!doctype html><html><head><meta charset="utf-8"><title>Print</title></head><body></body></html>' | |||
); | |||
doc.close(); | |||
// Ensure relative URLs (fonts/images) resolve inside iframe | |||
var base = doc.createElement("base"); | |||
base.href = location.origin + "/"; | |||
doc.head.appendChild(base); | |||
// Inject PRINT CSS (cache-busted) and await it | |||
var printCssUrl = | |||
"/index.php?title=MediaWiki:Print.css&action=raw&ctype=text/css"; | |||
var printCssFresh = cacheBust(printCssUrl); | |||
var linkCss = doc.createElement("link"); | |||
linkCss.rel = "stylesheet"; | |||
linkCss.href = printCssFresh; | |||
var cssLoaded = new Promise(function (resolve) { | |||
linkCss.onload = function () { | |||
resolve(); | |||
}; | |||
linkCss.onerror = function () { | |||
console.warn("[print] CSS failed to load"); | |||
resolve(); | |||
}; // don't block | |||
}); | |||
// Preload the font *inside* iframe (Chrome becomes much happier) | |||
var linkFont = doc.createElement("link"); | |||
linkFont.rel = "preload"; | |||
linkFont.as = "font"; | |||
linkFont.type = "font/woff2"; | |||
linkFont.href = "/fonts/HALColant-TextRegular.woff2?v=20250820"; | |||
linkFont.crossOrigin = "anonymous"; | |||
doc.head.appendChild(linkFont); | |||
doc.head.appendChild(linkCss); | |||
// Inject the printable HTML | |||
doc.body.innerHTML = $print.prop("outerHTML"); | |||
// >>> INSERTED: apply border preference as a class on the iframe document | |||
(function applyBorderPreference() { | |||
var root = doc.documentElement; | |||
if (borderPref === "without") { | |||
if (root.classList) root.classList.add("print-no-border"); | |||
else if (!/\bprint-no-border\b/.test(root.className)) | |||
root.className += " print-no-border"; | |||
} else { | |||
if (root.classList) root.classList.remove("print-no-border"); | |||
else | |||
root.className = root.className.replace( | |||
( | /\bprint-no-border\b/g, | ||
"" | |||
); | |||
} | } | ||
}); | })(); | ||
// | // Remove “empty” optional sections so they don’t leave gaps in print. | ||
var | // --- CLEAN: remove empty paragraphs/whitespace nodes that create phantom gaps | ||
(function () { | |||
Array.prototype. | // 1) Drop <p> that are visually empty or only /whitespace | ||
var ps = doc.querySelectorAll("#article-content p"); | |||
Array.prototype.forEach.call(ps, function (p) { | |||
var txt = (p.textContent || "").replace(/\u00a0/g, " ").trim(); | |||
// also ignore spans that were left with only <br> | |||
var onlyBr = | |||
p.children.length === 1 && p.firstElementChild.tagName === "BR"; | |||
if ( | |||
(!txt && | |||
!p.querySelector("img, a, strong, em, span:not(:empty)")) || | |||
onlyBr | |||
) { | |||
p.parentNode && p.parentNode.removeChild(p); | |||
} | } | ||
}); | }); | ||
// 2) Remove pure-whitespace text nodes directly under #article-content | |||
var css = | var root = doc.getElementById("article-content"); | ||
if (root) { | |||
Array.prototype.slice.call(root.childNodes).forEach(function (n) { | |||
if (n.nodeType === 3 && !n.textContent.replace(/\s+/g, "")) { | |||
root.removeChild(n); | |||
} | |||
}); | |||
} | |||
})(); | |||
(function () { | |||
var css = | |||
"@media print{" + | |||
// Paragraphs inside rich text | |||
" .article-description p,.article-reflection p,.article-external-reference p,.article-quote p{margin:0 0 1.2mm!important;}" + | |||
" .article-description p:last-child,.article-reflection p:last-child,.article-external-reference p:last-child,.article-quote p:last-child{margin-bottom:0!important;}" + | |||
// Ruled sections: one consistent bottom padding for the hairline | |||
" .article-entry-number,.link-pdf,.article-type,.article-metadata,.article-images,.article-description,.article-reflection,.article-external-reference,.article-quote,.article-mod-line{padding-bottom:1mm!important;}" + | |||
// Labels: zero their default margin, then add ONE spacer when following any ruled block | |||
' [class^="article-label-"]{margin-top:0!important;}' + | |||
// <<< NEW: spacer no matter which section precedes (handles skipped sections) | |||
' .article-entry-number + [class^="article-label-"],' + | |||
' .link-pdf + [class^="article-label-"],' + | |||
' .article-type + [class^="article-label-"],' + | |||
' .article-metadata + [class^="article-label-"],' + | |||
' .article-images + [class^="article-label-"],' + | |||
' .article-description + [class^="article-label-"],' + | |||
' .article-reflection + [class^="article-label-"],' + | |||
' .article-external-reference + [class^="article-label-"],' + | |||
' .article-quote + [class^="article-label-"],' + | |||
' .article-mod-line + [class^="article-label-"]{margin-top:0.9mm!important;}' + | |||
// No gap between any label and its own body | |||
" .article-label-description + .article-description," + | |||
" .article-label-reflection + .article-reflection," + | |||
" .article-label-external-reference + .article-external-reference," + | |||
" .article-label-quote + .article-quote," + | |||
" .article-label-modification-date + .article-modification-date{margin-top:0!important;}" + | |||
// Title/link row cleanup | |||
" .article-title-link{margin:0!important;padding:0!important;}" + | |||
" .article-title-link > *{margin:0!important;}" + | |||
" .link-pdf{margin-top:0!important;}" + | |||
// Final block: no trailing hairline | |||
" #article-content > :last-child{padding-bottom:0!important;}" + | |||
" #article-content > :last-child::after{content:none!important;}" + | |||
"}"; | |||
var style = doc.createElement("style"); | |||
style.type = "text/css"; | |||
style.appendChild(doc.createTextNode(css)); | |||
doc.head.appendChild(style); | |||
})(); | |||
// --- PDF-friendly links for Chrome on macOS --- | |||
// 1) Add a tiny print-only CSS override to keep anchors as one box. | |||
var linkCssFix = doc.createElement("style"); | |||
linkCssFix.textContent = | |||
"@media print {\n" + | |||
" /* Keep anchor boxes intact so Chrome preserves the PDF annotation */\n" + | |||
" .article-external-reference a,\n" + | |||
" .link-pdf a {\n" + | |||
" white-space: nowrap !important;\n" + | |||
" word-break: normal !important;\n" + | |||
" overflow-wrap: normal !important;\n" + | |||
" text-decoration: underline;\n" + | |||
" }\n" + | |||
" /* Allow wrapping outside the anchor instead */\n" + | |||
" .article-external-reference {\n" + | |||
" overflow-wrap: anywhere;\n" + | |||
" word-break: break-word;\n" + | |||
" }\n" + | |||
" /* Defensive: make sure anchors have a box */\n" + | |||
" a[href] { position: relative; }\n" + | |||
"}\n"; | |||
doc.head.appendChild(linkCssFix); | |||
// 2) Normalize long link text so it doesn't force wrapping inside anchors. | |||
(function () { | |||
// Shorten long visible URLs in external references, keep href intact | |||
var refs = doc.querySelectorAll( | |||
".article-external-reference a[href]" | |||
); | |||
refs.forEach(function (a) { | |||
var txt = (a.textContent || "").trim(); | |||
var href = a.getAttribute("href") || ""; | |||
var looksLongUrl = /^https?:\/\//i.test(txt) && txt.length > 60; | |||
if (looksLongUrl) { | |||
try { | |||
var u = new URL(href, doc.baseURI); | |||
var label = | |||
u.hostname + | |||
(u.pathname.replace(/\/$/, "") ? u.pathname : ""); | |||
if (label.length > 40) label = label.slice(0, 37) + "…"; | |||
a.textContent = label; | |||
} catch (e) { | |||
a.textContent = "Link"; | |||
} | |||
} | } | ||
// Ensure single-box anchors | |||
a.style.whiteSpace = "nowrap"; | |||
a.style.wordBreak = "normal"; | |||
a.style.overflowWrap = "normal"; | |||
}); | |||
// Icon links ([PDF⤴] [WEB⤴]) are short; still enforce single-box | |||
doc.querySelectorAll(".link-pdf a[href]").forEach(function (a) { | |||
a.style.whiteSpace = "nowrap"; | |||
a.style.wordBreak = "normal"; | |||
a.style.overflowWrap = "normal"; | |||
}); | |||
})(); | |||
// Wait helpers | |||
function waitImages() { | |||
var imgs = [].slice.call(doc.images || []); | |||
if (!imgs.length) return Promise.resolve(); | |||
return Promise.all( | |||
imgs.map(function (img) { | |||
if (img.decode) { | |||
try { | |||
return img.decode().catch(function () {}); | |||
} catch (e) {} | |||
} | |||
return new Promise(function (res) { | |||
if (img.complete) return res(); | |||
img.onload = img.onerror = function () { | |||
res(); | |||
}; | |||
}); | |||
}) | |||
); | |||
} | |||
function waitFonts(timeoutMs) { | |||
if (!doc.fonts || !doc.fonts.ready) return Promise.resolve(); | |||
var ready = doc.fonts.ready; | |||
var t = new Promise(function (res) { | |||
setTimeout(res, timeoutMs || 1200); | |||
}); | |||
return Promise.race([ready, t]); | |||
} | |||
// **Load the specific face** so Chrome actually uses it | |||
function waitSpecificFont(timeoutMs) { | |||
if (!doc.fonts || !doc.fonts.load) return Promise.resolve(); | |||
var p = Promise.all([ | |||
doc.fonts.load('400 16px "HALColant-TextRegular"'), | |||
doc.fonts.load('normal 16px "HALColant-TextRegular"'), | |||
]); | |||
var t = new Promise(function (res) { | |||
setTimeout(res, timeoutMs || 1200); | |||
}); | |||
return Promise.race([p, t]); | |||
} | |||
function nextFrame() { | |||
return new Promise(function (res) { | |||
( | |||
iframe.contentWindow.requestAnimationFrame || setTimeout | |||
)(res, 0); | |||
}); | |||
} | |||
Promise.all([ | |||
cssLoaded, | |||
waitImages(), | |||
waitFonts(1200), | |||
waitSpecificFont(1200), | |||
nextFrame(), | |||
]).then(function () { | |||
try { | |||
// build the desired PDF filename via document.title | |||
var entryNum = ""; | |||
var numEl = doc.querySelector(".article-entry-number"); | |||
if (numEl) { | |||
var m = (numEl.textContent || "").match(/\d+/); | |||
entryNum = m ? m[0] : ""; | |||
} | |||
var desiredTitle = | |||
(entryNum ? entryNum + "." : "") + "softwear.directory"; | |||
// save originals (scoped here) | |||
var oldIframeTitle = doc.title; | |||
var oldParentTitle = document.title; | |||
// define onafterprint AFTER we have originals so it can close over them | |||
iframe.contentWindow.onafterprint = function () { | |||
try { | |||
doc.title = oldIframeTitle; // restore iframe doc title | |||
document.title = oldParentTitle; // restore parent title | |||
} catch (e) {} | |||
setTimeout(function () { | |||
if (iframe.parentNode) iframe.parentNode.removeChild(iframe); | |||
}, 100); | |||
$btn.data("busy", false); | |||
}; | |||
// set temporary titles used by Chrome for the default PDF name | |||
doc.title = desiredTitle; // iframe document | |||
// (next line is optional/redundant; doc === iframe.contentWindow.document) | |||
// iframe.contentWindow.document.title = desiredTitle; | |||
document.title = desiredTitle; // parent (helps on some setups) | |||
// print | |||
iframe.contentWindow.focus(); | |||
iframe.contentWindow.print(); | |||
// fallback cleanup in case onafterprint doesn’t fire | |||
setTimeout(function () { | |||
try { | |||
doc.title = oldIframeTitle; | |||
document.title = oldParentTitle; | |||
} catch (e) {} | |||
if (iframe.parentNode) iframe.parentNode.removeChild(iframe); | |||
$btn.data("busy", false); | |||
}, 1000); | |||
} catch (err) { | |||
console.warn("[print] failed before print:", err); | |||
$btn.data("busy", false); | $btn.data("busy", false); | ||
} | } | ||
}); | |||
}) | |||
.fail(function (xhr) { | |||
console.warn( | |||
"[print] fetch failed:", | |||
xhr && xhr.status, | |||
xhr && xhr.statusText | |||
); | |||
window.print(); | |||
$("#print-button").data("busy", false); | |||
}); | }); | ||
} | |||
); | |||
// Close modal with Close button | // Close modal with Close button |