4,554
edits
| No edit summary | No edit summary | ||
| Line 1,260: | Line 1,260: | ||
|    }); |    }); | ||
|    //  |    // ========================== | ||
|    //  |    // Softwear — Print (all pages) | ||
|   // ========================== | |||
|   $(function () { | |||
|      var DEBUG = true; // set false to silence console logs | |||
|      //  | |||
|     // 1) Warm the font cache in the *parent* document | |||
|     function preloadFontForPrint() { | |||
|       var link = document.createElement("link"); | |||
|       link.rel = "preload"; | |||
|       link.as = "font"; | |||
|       link.type = "font/woff2"; | |||
|       link.href = "/fonts/HALColant-TextRegular.woff2?v=20250820"; | |||
|       link.crossOrigin = "anonymous"; | |||
|       document.head.appendChild(link); | |||
|     } | |||
|     // 2) Cache buster | |||
|     function cacheBust(url) { | |||
|       return url + (url.indexOf("?") > -1 ? "&" : "?") + "_=" + Date.now(); | |||
|      } | |||
|     // 3) Create the little chooser under [print] if needed | |||
|     function ensurePrintChooser() { | |||
|       var $chooser = $("#print-chooser"); | |||
|       if ($chooser.length) return $chooser; | |||
|       // MediaWiki-safe: <div> + <a>, no <button> | |||
|       $chooser = $( | |||
|         '<div id="print-chooser" class="print-chooser" style="display:none;">' + | |||
|           '<a href="#" id="print-with-border" class="print-choice">border</a> ' + | |||
|           '<a href="#" id="print-no-border" class="print-choice">no border</a>' + | |||
|           "</div>" | |||
|       ); | |||
|        $("#print-button").after($chooser); | |||
|        return $chooser; | |||
|     } | |||
|     // 4) Hide chooser utility (used by close handlers) | |||
|     function hidePrintUI() { | |||
|        $("#print-chooser").hide(); | |||
|        $("#show-article").removeClass("print-opts-open"); | |||
|     } | |||
|     // 5) Main “choice” handler (called by the container click handler below) | |||
|     function handlePrintChoice(id, $btn) { | |||
|        if ($btn.data("busy")) { |        if ($btn.data("busy")) { | ||
|          console.log("[print] busy, ignoring click"); |          if (DEBUG) console.log("[print] busy, ignoring click"); | ||
|          return; |          return; | ||
|        } |        } | ||
| Line 1,351: | Line 1,313: | ||
|        var borderPref = id === "print-no-border" ? "without" : "with"; |        var borderPref = id === "print-no-border" ? "without" : "with"; | ||
|        console.log("[print] option chosen:", borderPref); |        if (DEBUG) console.log("[print] option chosen:", borderPref); | ||
|        preloadFontForPrint(); |        preloadFontForPrint(); | ||
| Line 1,357: | Line 1,319: | ||
|        // Prefer local .print-only (Entry page) |        // Prefer local .print-only (Entry page) | ||
|        var localPrintOnly = $(".print-only").first(); |        var localPrintOnly = $(".print-only").first(); | ||
|        console.log("[print] local .print-only present:", localPrintOnly.length); |        if (DEBUG) | ||
|         console.log( | |||
|           "[print] local .print-only present:", | |||
|           localPrintOnly.length | |||
|         ); | |||
|        if (localPrintOnly.length) { |        if (localPrintOnly.length) { | ||
|          console.log("[print] using local .print-only on this page"); |          if (DEBUG) console.log("[print] using local .print-only on this page"); | ||
|          hidePrintUI(); | |||
|          buildIframeAndPrint(localPrintOnly.prop("outerHTML"), borderPref, $btn); | |||
|          buildIframeAndPrint(localPrintOnly.prop("outerHTML")); | |||
|          return; |          return; | ||
|        } |        } | ||
| Line 1,370: | Line 1,335: | ||
|        var title = |        var title = | ||
|          window.currentEntryTitle || |          window.currentEntryTitle || | ||
|          (mw.config && mw.config.get && mw.config.get("wgPageName")); |          (window.mw && | ||
|        console.log("[print] currentEntryTitle / wgPageName:", title); |           mw.config && | ||
|           mw.config.get && | |||
|           mw.config.get("wgPageName")); | |||
|        if (DEBUG) console.log("[print] currentEntryTitle / wgPageName:", title); | |||
|        if (!title) { |        if (!title) { | ||
| Line 1,380: | Line 1,348: | ||
|        } |        } | ||
|        var pageUrl = mw.util.getUrl(title); |        var pageUrl = | ||
|         window.mw && mw.util && mw.util.getUrl | |||
|           ? mw.util.getUrl(title) | |||
|           : "/wiki/" + String(title); | |||
|        var pageUrlFresh = cacheBust(pageUrl); |        var pageUrlFresh = cacheBust(pageUrl); | ||
|        console.log("[print] fetching page HTML:", pageUrlFresh); |        if (DEBUG) console.log("[print] fetching page HTML:", pageUrlFresh); | ||
|        $.get(pageUrlFresh) |        $.get(pageUrlFresh) | ||
| Line 1,388: | Line 1,359: | ||
|            var $tmp = $("<div>").html(html); |            var $tmp = $("<div>").html(html); | ||
|            var $print = $tmp.find(".print-only").first(); |            var $print = $tmp.find(".print-only").first(); | ||
|            console.log("[print] .print-only in fetched page:", $print.length); |            if (DEBUG) | ||
|             console.log("[print] .print-only in fetched page:", $print.length); | |||
|            if (!$print.length) { |            if (!$print.length) { | ||
|              console.warn( |              console.warn( | ||
| Line 1,397: | Line 1,369: | ||
|              return; |              return; | ||
|            } |            } | ||
|            hidePrintUI(); | |||
|            buildIframeAndPrint($print.prop("outerHTML"), borderPref, $btn); | |||
|            buildIframeAndPrint($print.prop("outerHTML")); | |||
|          }) |          }) | ||
|          .fail(function (xhr) { |          .fail(function (xhr) { | ||
| Line 1,410: | Line 1,381: | ||
|            $("#print-button").data("busy", false); |            $("#print-button").data("busy", false); | ||
|          }); |          }); | ||
|     } | |||
|     // 6) Build a hidden iframe, inject CSS+HTML, and print | |||
|     function buildIframeAndPrint(printHtml, borderPref, $btn) { | |||
|       if (DEBUG) console.log("[print] buildIframeAndPrint: starting"); | |||
|        //  |        // 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 resolve | |||
|       var base = doc.createElement("base"); | |||
|       base.href = location.origin + "/"; | |||
|       doc.head.appendChild(base); | |||
|       // Print CSS | |||
|       var printCssUrl = | |||
|         "/index.php?title=MediaWiki:Print.css&action=raw&ctype=text/css"; | |||
|       var linkCss = doc.createElement("link"); | |||
|       linkCss.rel = "stylesheet"; | |||
|       linkCss.href = cacheBust(printCssUrl); | |||
|       var cssLoaded = new Promise(function (resolve) { | |||
|          linkCss. |          linkCss.onload = function () { | ||
|           if (DEBUG) console.log("[print] print CSS loaded"); | |||
|           resolve(); | |||
|         }; | |||
|          linkCss.onerror = function () { | |||
|           console.warn("[print] CSS failed to load"); | |||
|           resolve(); | |||
|          }; | |||
|       }); | |||
|       // Preload font inside iframe | |||
|       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 printable HTML | |||
|       doc.body.innerHTML = printHtml; | |||
|       if (DEBUG) console.log("[print] injected print HTML"); | |||
|       // Apply border preference to <html> | |||
|          doc. |       (function () { | ||
|          console.log("[print]  |          var htmlEl = doc.documentElement; | ||
|         if (borderPref === "without") { | |||
|           htmlEl.classList | |||
|             ? htmlEl.classList.add("print-no-border") | |||
|             : (htmlEl.className += " print-no-border"); | |||
|          } else { | |||
|           htmlEl.classList | |||
|             ? htmlEl.classList.remove("print-no-border") | |||
|             : (htmlEl.className = htmlEl.className.replace( | |||
|                 /\bprint-no-border\b/g, | |||
|                 "" | |||
|               )); | |||
|         } | |||
|         if (DEBUG) console.log("[print] borderPref applied:", borderPref); | |||
|       })(); | |||
|       // Clean empty paragraphs | |||
|          (function  |       (function () { | ||
|            var  |          var ps = doc.querySelectorAll("#article-content p"); | ||
|         Array.prototype.forEach.call(ps, function (p) { | |||
|            var txt = (p.textContent || "").replace(/\u00a0/g, " ").trim(); | |||
|            var onlyBr = | |||
|             p.children.length === 1 && | |||
|              p.firstElementChild && | |||
|             p.firstElementChild.tagName === "BR"; | |||
|            if ( | |||
|              (!txt && | |||
|                !p.querySelector("img, a, strong, em, span:not(:empty)")) || | |||
|             onlyBr | |||
|           ) { | |||
|             p.parentNode && p.parentNode.removeChild(p); | |||
|            } |            } | ||
|          }); | |||
|          } |          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); | |||
|            Array.prototype. | |||
|              } |              } | ||
|            }); |            }); | ||
|         } | |||
|       })(); | |||
|       // Small inline tweaks for print | |||
|       (function () { | |||
|         var css = | |||
|           "@media print{" + | |||
|           "  .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;}" + | |||
|           "  .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;}" + | |||
|           '  [class^="article-label-"]{margin-top:0!important;}' + | |||
|           '  .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;}' + | |||
|           "  .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;}" + | |||
|           "  .article-title-link{margin:0!important;padding:0!important;}" + | |||
|           "  .article-title-link > *{margin:0!important;}" + | |||
|           "  .link-pdf{margin-top:0!important;}" + | |||
|           "  #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 | |||
|          var  |       (function () { | ||
|          var styleFix = doc.createElement("style"); | |||
|         styleFix.textContent = | |||
|            "@media print {.article-external-reference a,.link-pdf a{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important;text-decoration:underline}.article-external-reference{overflow-wrap:anywhere;word-break:break-word}a[href]{position:relative}}"; |            "@media print {.article-external-reference a,.link-pdf a{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important;text-decoration:underline}.article-external-reference{overflow-wrap:anywhere;word-break:break-word}a[href]{position:relative}}"; | ||
|          doc.head.appendChild(styleFix); |          doc.head.appendChild(styleFix); | ||
|          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); | |||
|              if ( |               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"; | |||
|             } | |||
|           } | |||
|           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 { |                try { | ||
|                  return img.decode().catch(function () {}); | |||
|                } catch (e) {} | |||
|                } 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]); | |||
|       } | |||
|       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 () { | |||
|            if (DEBUG) console.log("[print] resources ready; calling print()"); | |||
|            try { | |||
|             // 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"; | |||
|            if ( | |||
|              doc. | |||
|              var oldIframeTitle = doc.title; | |||
|             var oldParentTitle = document.title; | |||
|             iframe.contentWindow.onafterprint = function () { | |||
|                try { | |||
|                 doc.title = oldIframeTitle; | |||
|                 document.title = oldParentTitle; | |||
|               } catch (e) {} | |||
|               setTimeout(function () { | |||
|                 if (iframe.parentNode) iframe.parentNode.removeChild(iframe); | |||
|               }, 100); | |||
|               $btn.data("busy", false); | |||
|               if (DEBUG) console.log("[print] afterprint cleanup done"); | |||
|             }; | |||
|             doc.title = desiredTitle; | |||
|             document.title = desiredTitle; | |||
|             iframe.contentWindow.focus(); | |||
|             iframe.contentWindow.print(); | |||
|             // Safety cleanup if onafterprint never fires | |||
|             setTimeout(function () { | |||
|               try { | |||
|                 doc.title = oldIframeTitle; | |||
|                 document.title = oldParentTitle; | |||
|               } catch (e) {} | |||
|               if (iframe.parentNode) iframe.parentNode.removeChild(iframe); | |||
|                $btn.data("busy", false); |                $btn.data("busy", false); | ||
|              } |               if (DEBUG) console.log("[print] timeout cleanup done"); | ||
|            } |              }, 1000); | ||
|            } catch (err) { | |||
|              console.warn("[print]  |              console.warn("[print] failed before print:", err); | ||
|              $btn.data("busy", false); |              $btn.data("busy", false); | ||
|            }); |            } | ||
|         }) | |||
|         .catch(function (err) { | |||
|           console.warn("[print] Promise chain error:", err); | |||
|           $btn.data("busy", false); | |||
|         }); | |||
|     } | |||
|     // 7) Event wiring | |||
|     // Remove any prior print handlers so we don’t double-bind after AJAX nav etc. | |||
|     $(document).off("click.print"); | |||
|     // Single tolerant handler bound to: | |||
|     // - #print-button     (to toggle chooser) | |||
|     // - #print-chooser    (so nested <a><button> clicks are caught) | |||
|     // - #print-options    (legacy container if you still render it) | |||
|     $(document).on( | |||
|       "click.print", | |||
|       "#print-button, #print-chooser, #print-options", | |||
|       function (e) { | |||
|         // If the main [print] (or a child of it) was clicked, toggle chooser | |||
|         if ($(e.target).closest("#print-button").length) { | |||
|           e.preventDefault(); | |||
|           var $chooser = ensurePrintChooser(); | |||
|           $chooser.toggle(); | |||
|           $("#show-article").toggleClass( | |||
|             "print-opts-open", | |||
|             $chooser.is(":visible") | |||
|           ); | |||
|           if (DEBUG) | |||
|             console.log( | |||
|               "[print] toggled chooser; visible=", | |||
|               $chooser.is(":visible") | |||
|             ); | |||
|           return; | |||
|         } | |||
|         // Otherwise, check if one of the two choice anchors was clicked | |||
|         var anchor = e.target.closest("a#print-with-border, a#print-no-border"); | |||
|         if (!anchor) return; // click inside container but not on a choice | |||
|         e.preventDefault(); | |||
|         var id = anchor.id; | |||
|         if (DEBUG) console.log("[print] option anchor click:", { id }); | |||
|         handlePrintChoice(id, $(anchor)); | |||
|        } |        } | ||
|      } |     ); | ||
|    ); | |||
|     // 8) Also hide the chooser when the close button is clicked or ESC is pressed | |||
|     $(document).on("click.print", "#close-button", hidePrintUI); | |||
|     $(document).on("keydown.print", function (e) { | |||
|       if (e.key === "Escape") hidePrintUI(); | |||
|      }); | |||
|    }); | |||
|    // Close modal with Close button |    // Close modal with Close button | ||