MediaWiki:Common.js: Difference between revisions

Jump to navigation Jump to search
no edit summary
No edit summary
No edit summary
Line 1,347: Line 1,347:
       console.log("[print] fetching page HTML:", pageUrlFresh);
       console.log("[print] fetching page HTML:", pageUrlFresh);


       $.get(pageUrlFresh)
       // Decide source: if this page already contains a .print-only (Entry page),
        .done(function (html) {
      // use it directly. Otherwise fetch (modal/homepage flow).
          var $tmp = $("<div>").html(html);
      var localPrintOnly = $(".print-only").first();
          var $print = $tmp.find(".print-only").first();
          console.log("[print] .print-only found:", $print.length);


           if (!$print.length) {
      if (localPrintOnly.length) {
            console.warn("[print] no .print-only found; fallback print");
        console.log("[print] using local .print-only on this page");
        // optional: hide chooser when starting print
        $("#print-chooser").hide();
        $("#show-article").removeClass("print-opts-open");
        buildIframeAndPrint(localPrintOnly.prop("outerHTML"));
      } else {
        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 in fetched page:",
              $print.length
            );
            if (!$print.length) {
              console.warn("[print] no .print-only found; fallback print");
              window.print();
              $btn.data("busy", false);
              return;
            }
            // optional: hide chooser when starting print
            $("#print-chooser").hide();
            $("#show-article").removeClass("print-opts-open");
            buildIframeAndPrint($print.prop("outerHTML"));
          })
          .fail(function (xhr) {
            console.warn(
              "[print] fetch failed:",
              xhr && xhr.status,
              xhr && xhr.statusText
            );
             window.print();
             window.print();
             $btn.data("busy", false);
             $("#print-button").data("busy", false);
            return;
          });
          }
      }
      function buildIframeAndPrint(printHtml) {
        // 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);


          // Build hidden iframe
        var doc = iframe.contentDocument || iframe.contentWindow.document;
          var iframe = document.createElement("iframe");
        doc.open();
          iframe.style.position = "fixed";
        doc.write(
          iframe.style.right = "0";
           '<!doctype html><html><head><meta charset="utf-8"><title>Print</title></head><body></body></html>'
          iframe.style.bottom = "0";
        );
          iframe.style.width = "0";
        doc.close();
           iframe.style.height = "0";
          iframe.style.border = "0";
          document.body.appendChild(iframe);


          var doc = iframe.contentDocument || iframe.contentWindow.document;
        // Ensure relative URLs (fonts/images) resolve inside iframe
          doc.open();
        var base = doc.createElement("base");
          doc.write(
        base.href = location.origin + "/";
            '<!doctype html><html><head><meta charset="utf-8"><title>Print</title></head><body></body></html>'
        doc.head.appendChild(base);
          );
          doc.close();


          // Ensure relative URLs (fonts/images) resolve inside iframe
        // Inject PRINT CSS (cache-busted) and await it
          var base = doc.createElement("base");
        var printCssUrl =
           base.href = location.origin + "/";
           "/index.php?title=MediaWiki:Print.css&action=raw&ctype=text/css";
          doc.head.appendChild(base);
        var printCssFresh = cacheBust(printCssUrl);


          // Inject PRINT CSS (cache-busted) and await it
        var linkCss = doc.createElement("link");
          var printCssUrl =
        linkCss.rel = "stylesheet";
            "/index.php?title=MediaWiki:Print.css&action=raw&ctype=text/css";
        linkCss.href = printCssFresh;
          var printCssFresh = cacheBust(printCssUrl);


           var linkCss = doc.createElement("link");
        var cssLoaded = new Promise(function (resolve) {
           linkCss.rel = "stylesheet";
           linkCss.onload = function () {
           linkCss.href = printCssFresh;
            resolve();
          };
           linkCss.onerror = function () {
            console.warn("[print] CSS failed to load");
            resolve();
           };
        });


          var cssLoaded = new Promise(function (resolve) {
        // Preload the font *inside* iframe (Chrome becomes much happier)
            linkCss.onload = function () {
        var linkFont = doc.createElement("link");
              resolve();
        linkFont.rel = "preload";
            };
        linkFont.as = "font";
            linkCss.onerror = function () {
        linkFont.type = "font/woff2";
              console.warn("[print] CSS failed to load");
        linkFont.href = "/fonts/HALColant-TextRegular.woff2?v=20250820";
              resolve();
        linkFont.crossOrigin = "anonymous";
            }; // don't block
          });


          // Preload the font *inside* iframe (Chrome becomes much happier)
        doc.head.appendChild(linkFont);
          var linkFont = doc.createElement("link");
        doc.head.appendChild(linkCss);
          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);
        // Inject the printable HTML
          doc.head.appendChild(linkCss);
        doc.body.innerHTML = printHtml;


          // Inject the printable HTML
        // Apply border preference (class on <html>)
           doc.body.innerHTML = $print.prop("outerHTML");
        (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,
                ""
              );
          }
        })();


          // >>> INSERTED: apply border preference as a class on the iframe document
        // CLEAN: remove empty paragraphs/whitespace nodes that create phantom gaps
          (function applyBorderPreference() {
        (function () {
            var root = doc.documentElement;
          var ps = doc.querySelectorAll("#article-content p");
            if (borderPref === "without") {
          Array.prototype.forEach.call(ps, function (p) {
              if (root.classList) root.classList.add("print-no-border");
            var txt = (p.textContent || "").replace(/\u00a0/g, " ").trim();
              else if (!/\bprint-no-border\b/.test(root.className))
            var onlyBr =
                root.className += " print-no-border";
              p.children.length === 1 && p.firstElementChild.tagName === "BR";
             } else {
             if (
               if (root.classList) root.classList.remove("print-no-border");
               (!txt &&
               else
                !p.querySelector("img, a, strong, em, span:not(:empty)")) ||
                root.className = root.className.replace(
               onlyBr
                  /\bprint-no-border\b/g,
            ) {
                  ""
              p.parentNode && p.parentNode.removeChild(p);
                );
             }
             }
           })();
           });
 
           var root = doc.getElementById("article-content");
          // Remove “empty” optional sections so they don’t leave gaps in print.
          if (root) {
          // --- CLEAN: remove empty paragraphs/whitespace nodes that create phantom gaps
             Array.prototype.slice.call(root.childNodes).forEach(function (n) {
           (function () {
               if (n.nodeType === 3 && !n.textContent.replace(/\s+/g, "")) {
            // 1) Drop <p> that are visually empty or only &nbsp;/whitespace
                 root.removeChild(n);
            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 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
        // Inline print-tweaks CSS
          function waitSpecificFont(timeoutMs) {
        (function () {
             if (!doc.fonts || !doc.fonts.load) return Promise.resolve();
          var css =
             var p = Promise.all([
            "@media print{" +
              doc.fonts.load('400 16px "HALColant-TextRegular"'),
             "  .article-description p,.article-reflection p,.article-external-reference p,.article-quote p{margin:0 0 1.2mm!important;}" +
              doc.fonts.load('normal 16px "HALColant-TextRegular"'),
            "  .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;}" +
             var t = new Promise(function (res) {
             '  [class^="article-label-"]{margin-top:0!important;}' +
              setTimeout(res, timeoutMs || 1200);
            '  .article-entry-number + [class^="article-label-"],' +
            });
            '  .link-pdf + [class^="article-label-"],' +
            return Promise.race([p, t]);
            '  .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);
        })();


          function nextFrame() {
        // PDF-friendly links
            return new Promise(function (res) {
        var linkCssFix = doc.createElement("style");
              (
        linkCssFix.textContent =
                iframe.contentWindow.requestAnimationFrame || setTimeout
          "@media print {\n" +
              )(res, 0);
          "  .article-external-reference a,\n" +
            });
          "  .link-pdf a { white-space: nowrap !important; word-break: normal !important; overflow-wrap: normal !important; text-decoration: underline; }\n" +
          }
          "  .article-external-reference { overflow-wrap: anywhere; word-break: break-word; }\n" +
          "  a[href] { position: relative; }\n" +
          "}\n";
        doc.head.appendChild(linkCssFix);


          Promise.all([
        (function () {
            cssLoaded,
          var refs = doc.querySelectorAll(
            waitImages(),
             ".article-external-reference a[href]"
            waitFonts(1200),
          );
             waitSpecificFont(1200),
           refs.forEach(function (a) {
            nextFrame(),
             var txt = (a.textContent || "").trim();
           ]).then(function () {
            var href = a.getAttribute("href") || "";
             try {
            var looksLongUrl = /^https?:\/\//i.test(txt) && txt.length > 60;
              // build the desired PDF filename via document.title
            if (looksLongUrl) {
              var entryNum = "";
              try {
              var numEl = doc.querySelector(".article-entry-number");
                 var u = new URL(href, doc.baseURI);
              if (numEl) {
                var label =
                 var m = (numEl.textContent || "").match(/\d+/);
                  u.hostname +
                 entryNum = m ? m[0] : "";
                  (u.pathname.replace(/\/$/, "") ? u.pathname : "");
                 if (label.length > 40) label = label.slice(0, 37) + "…";
                a.textContent = label;
              } catch (e) {
                a.textContent = "Link";
               }
               }
              var desiredTitle =
            }
                (entryNum ? entryNum + "." : "") + "softwear.directory";
            a.style.whiteSpace = "nowrap";
 
            a.style.wordBreak = "normal";
              // save originals (scoped here)
            a.style.overflowWrap = "normal";
              var oldIframeTitle = doc.title;
          });
              var oldParentTitle = document.title;
          doc.querySelectorAll(".link-pdf a[href]").forEach(function (a) {
            a.style.whiteSpace = "nowrap";
            a.style.wordBreak = "normal";
            a.style.overflowWrap = "normal";
          });
        })();


              // define onafterprint AFTER we have originals so it can close over them
        // Wait helpers
              iframe.contentWindow.onafterprint = function () {
        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 {
                   doc.title = oldIframeTitle; // restore iframe doc title
                   return img.decode().catch(function () {});
                  document.title = oldParentTitle; // restore parent title
                 } catch (e) {}
                 } catch (e) {}
                 setTimeout(function () {
              }
                   if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
              return new Promise(function (res) {
                }, 100);
                 if (img.complete) return res();
                $btn.data("busy", false);
                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);
          });
        }


              // set temporary titles used by Chrome for the default PDF name
        Promise.all([
              doc.title = desiredTitle; // iframe document
          cssLoaded,
              // (next line is optional/redundant; doc === iframe.contentWindow.document)
          waitImages(),
               // iframe.contentWindow.document.title = desiredTitle;
          waitFonts(1200),
               document.title = desiredTitle; // parent (helps on some setups)
          waitSpecificFont(1200),
          nextFrame(),
        ]).then(function () {
          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";


              // print
            var oldIframeTitle = doc.title;
              iframe.contentWindow.focus();
            var oldParentTitle = document.title;
              iframe.contentWindow.print();


               // fallback cleanup in case onafterprint doesn’t fire
            iframe.contentWindow.onafterprint = function () {
               try {
                doc.title = oldIframeTitle;
                document.title = oldParentTitle;
              } catch (e) {}
               setTimeout(function () {
               setTimeout(function () {
                try {
                  doc.title = oldIframeTitle;
                  document.title = oldParentTitle;
                } catch (e) {}
                 if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
                 if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
                $btn.data("busy", false);
               }, 100);
               }, 1000);
            } catch (err) {
              console.warn("[print] failed before print:", err);
               $btn.data("busy", false);
               $btn.data("busy", false);
             }
             };
          });
 
        })
            doc.title = desiredTitle;
        .fail(function (xhr) {
            document.title = desiredTitle;
          console.warn(
 
            "[print] fetch failed:",
            iframe.contentWindow.focus();
             xhr && xhr.status,
            iframe.contentWindow.print();
            xhr && xhr.statusText
 
          );
            setTimeout(function () {
           window.print();
              try {
          $("#print-button").data("busy", false);
                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);
          }
         });
         });
      }
     }
     }
   );
   );

Navigation menu