MediaWiki:Common.js: Difference between revisions

Jump to navigation Jump to search
no edit summary
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", function () {
  // One handler for all three
    // debounce to avoid double/racing prints
   $(document).on(
    var $btn = $("#print-button");
    "click.print",
     if ($btn.data("busy")) return;
    "#print-button, #print-with-border, #print-no-border",
    $btn.data("busy", true);
     function (e) {
      e.preventDefault();


    preloadFontForPrint();
      var $btn = $(this);
      if ($btn.data("busy")) return;
      $btn.data("busy", true);


    var title = window.currentEntryTitle; // e.g. "090"
      // decide the border preference from the clicked button id
    if (!title) {
      // #print-no-border => no border; everything else => with border
      console.warn("[print] no currentEntryTitle");
      var borderPref =
      window.print();
        $btn.attr("id") === "print-no-border" ? "without" : "with";
      $btn.data("busy", false);
 
      return;
      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 pageUrl = mw.util.getUrl(title);
    var pageUrlFresh = cacheBust(pageUrl);
      var pageUrlFresh = cacheBust(pageUrl);
    console.log("[print] fetching page HTML:", pageUrlFresh);
      console.log("[print] fetching page HTML:", pageUrlFresh);


    $.get(pageUrlFresh)
      $.get(pageUrlFresh)
      .done(function (html) {
        .done(function (html) {
        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 found:", $print.length);
          console.log("[print] .print-only found:", $print.length);


        if (!$print.length) {
          if (!$print.length) {
          console.warn("[print] no .print-only found; fallback print");
            console.warn("[print] no .print-only found; fallback print");
          window.print();
            window.print();
          $btn.data("busy", false);
            $btn.data("busy", false);
          return;
            return;
        }
          }


        // Build hidden iframe
          // Build hidden iframe
        var iframe = document.createElement("iframe");
          var iframe = document.createElement("iframe");
        iframe.style.position = "fixed";
          iframe.style.position = "fixed";
        iframe.style.right = "0";
          iframe.style.right = "0";
        iframe.style.bottom = "0";
          iframe.style.bottom = "0";
        iframe.style.width = "0";
          iframe.style.width = "0";
        iframe.style.height = "0";
          iframe.style.height = "0";
        iframe.style.border = "0";
          iframe.style.border = "0";
        document.body.appendChild(iframe);
          document.body.appendChild(iframe);


        var doc = iframe.contentDocument || iframe.contentWindow.document;
          var doc = iframe.contentDocument || iframe.contentWindow.document;
        doc.open();
          doc.open();
        doc.write(
          doc.write(
          '<!doctype html><html><head><meta charset="utf-8"><title>Print</title></head><body></body></html>'
            '<!doctype html><html><head><meta charset="utf-8"><title>Print</title></head><body></body></html>'
        );
          );
        doc.close();
          doc.close();


        // Ensure relative URLs (fonts/images) resolve inside iframe
          // Ensure relative URLs (fonts/images) resolve inside iframe
        var base = doc.createElement("base");
          var base = doc.createElement("base");
        base.href = location.origin + "/";
          base.href = location.origin + "/";
        doc.head.appendChild(base);
          doc.head.appendChild(base);


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


        var linkCss = doc.createElement("link");
          var linkCss = doc.createElement("link");
        linkCss.rel = "stylesheet";
          linkCss.rel = "stylesheet";
        linkCss.href = printCssFresh;
          linkCss.href = printCssFresh;


        var cssLoaded = new Promise(function (resolve) {
          var cssLoaded = new Promise(function (resolve) {
          linkCss.onload = function () {
            linkCss.onload = function () {
            resolve();
              resolve();
          };
            };
          linkCss.onerror = function () {
            linkCss.onerror = function () {
            console.warn("[print] CSS failed to load");
              console.warn("[print] CSS failed to load");
            resolve();
              resolve();
          }; // don't block
            }; // don't block
        });
          });


        // Preload the font *inside* iframe (Chrome becomes much happier)
          // Preload the font *inside* iframe (Chrome becomes much happier)
        var linkFont = doc.createElement("link");
          var linkFont = doc.createElement("link");
        linkFont.rel = "preload";
          linkFont.rel = "preload";
        linkFont.as = "font";
          linkFont.as = "font";
        linkFont.type = "font/woff2";
          linkFont.type = "font/woff2";
        linkFont.href = "/fonts/HALColant-TextRegular.woff2?v=20250820";
          linkFont.href = "/fonts/HALColant-TextRegular.woff2?v=20250820";
        linkFont.crossOrigin = "anonymous";
          linkFont.crossOrigin = "anonymous";


        doc.head.appendChild(linkFont);
          doc.head.appendChild(linkFont);
        doc.head.appendChild(linkCss);
          doc.head.appendChild(linkCss);


        // Inject the printable HTML
          // Inject the printable HTML
        doc.body.innerHTML = $print.prop("outerHTML");
          doc.body.innerHTML = $print.prop("outerHTML");


        // Remove “empty” optional sections so they don’t leave gaps in print.
          // >>> 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;
          // 1) Drop <p> that are visually empty or only &nbsp;/whitespace
            if (borderPref === "without") {
          var ps = doc.querySelectorAll("#article-content p");
              if (root.classList) root.classList.add("print-no-border");
          Array.prototype.forEach.call(ps, function (p) {
              else if (!/\bprint-no-border\b/.test(root.className))
            var txt = (p.textContent || "").replace(/\u00a0/g, " ").trim();
                root.className += " print-no-border";
            // also ignore spans that were left with only <br>
             } else {
            var onlyBr =
               if (root.classList) root.classList.remove("print-no-border");
              p.children.length === 1 && p.firstElementChild.tagName === "BR";
               else
             if (
                root.className = root.className.replace(
               (!txt &&
                  /\bprint-no-border\b/g,
                !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
           // Remove “empty” optional sections so they don’t leave gaps in print.
           var root = doc.getElementById("article-content");
          // --- CLEAN: remove empty paragraphs/whitespace nodes that create phantom gaps
          if (root) {
           (function () {
             Array.prototype.slice.call(root.childNodes).forEach(function (n) {
            // 1) Drop <p> that are visually empty or only &nbsp;/whitespace
               if (n.nodeType === 3 && !n.textContent.replace(/\s+/g, "")) {
            var ps = doc.querySelectorAll("#article-content p");
                 root.removeChild(n);
             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);
               }
               }
             });
             });
          }
        })();


        (function () {
            // 2) Remove pure-whitespace text nodes directly under #article-content
           var css =
            var root = doc.getElementById("article-content");
            "@media print{" +
            if (root) {
            // Paragraphs inside rich text
              Array.prototype.slice.call(root.childNodes).forEach(function (n) {
            "  .article-description p,.article-reflection p,.article-external-reference p,.article-quote p{margin:0 0 1.2mm!important;}" +
                if (n.nodeType === 3 && !n.textContent.replace(/\s+/g, "")) {
            "  .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;}" +
                  root.removeChild(n);
            // 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-"],' +
          (function () {
            '  .link-pdf + [class^="article-label-"],' +
            var css =
            '  .article-type + [class^="article-label-"],' +
              "@media print{" +
            '  .article-metadata + [class^="article-label-"],' +
              // Paragraphs inside rich text
            '  .article-images + [class^="article-label-"],' +
              "  .article-description p,.article-reflection p,.article-external-reference p,.article-quote p{margin:0 0 1.2mm!important;}" +
            '  .article-description + [class^="article-label-"],' +
              "  .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-reflection + [class^="article-label-"],' +
              // Ruled sections: one consistent bottom padding for the hairline
            '  .article-external-reference + [class^="article-label-"],' +
              "  .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;}" +
            '  .article-quote + [class^="article-label-"],' +
              // Labels: zero their default margin, then add ONE spacer when following any ruled block
            '  .article-mod-line + [class^="article-label-"]{margin-top:0.9mm!important;}' +
              '  [class^="article-label-"]{margin-top:0!important;}' +
            // No gap between any label and its own body
              // <<< NEW: spacer no matter which section precedes (handles skipped sections)
            "  .article-label-description + .article-description," +
              '  .article-entry-number + [class^="article-label-"],' +
            "  .article-label-reflection + .article-reflection," +
              '  .link-pdf + [class^="article-label-"],' +
            "  .article-label-external-reference + .article-external-reference," +
              '  .article-type + [class^="article-label-"],' +
            "  .article-label-quote + .article-quote," +
              '  .article-metadata + [class^="article-label-"],' +
            "  .article-label-modification-date + .article-modification-date{margin-top:0!important;}" +
              '  .article-images + [class^="article-label-"],' +
            // Title/link row cleanup
              '  .article-description + [class^="article-label-"],' +
            "  .article-title-link{margin:0!important;padding:0!important;}" +
              '  .article-reflection + [class^="article-label-"],' +
            "  .article-title-link > *{margin:0!important;}" +
              '  .article-external-reference + [class^="article-label-"],' +
            "  .link-pdf{margin-top:0!important;}" +
              '  .article-quote + [class^="article-label-"],' +
            // Final block: no trailing hairline
              '  .article-mod-line + [class^="article-label-"]{margin-top:0.9mm!important;}' +
            "  #article-content > :last-child{padding-bottom:0!important;}" +
              // No gap between any label and its own body
            "  #article-content > :last-child::after{content:none!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;}" +
              // 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");
            var style = doc.createElement("style");
          style.type = "text/css";
            style.type = "text/css";
          style.appendChild(doc.createTextNode(css));
            style.appendChild(doc.createTextNode(css));
          doc.head.appendChild(style);
            doc.head.appendChild(style);
        })();
          })();


        // --- PDF-friendly links for Chrome on macOS ---
          // --- PDF-friendly links for Chrome on macOS ---
        // 1) Add a tiny print-only CSS override to keep anchors as one box.
          // 1) Add a tiny print-only CSS override to keep anchors as one box.
        var linkCssFix = doc.createElement("style");
          var linkCssFix = doc.createElement("style");
        linkCssFix.textContent =
          linkCssFix.textContent =
          "@media print {\n" +
            "@media print {\n" +
          "  /* Keep anchor boxes intact so Chrome preserves the PDF annotation */\n" +
            "  /* Keep anchor boxes intact so Chrome preserves the PDF annotation */\n" +
          "  .article-external-reference a,\n" +
            "  .article-external-reference a,\n" +
          "  .link-pdf a {\n" +
            "  .link-pdf a {\n" +
          "    white-space: nowrap !important;\n" +
            "    white-space: nowrap !important;\n" +
          "    word-break: normal !important;\n" +
            "    word-break: normal !important;\n" +
          "    overflow-wrap: normal !important;\n" +
            "    overflow-wrap: normal !important;\n" +
          "    text-decoration: underline;\n" +
            "    text-decoration: underline;\n" +
          "  }\n" +
            "  }\n" +
          "  /* Allow wrapping outside the anchor instead */\n" +
            "  /* Allow wrapping outside the anchor instead */\n" +
          "  .article-external-reference {\n" +
            "  .article-external-reference {\n" +
          "    overflow-wrap: anywhere;\n" +
            "    overflow-wrap: anywhere;\n" +
          "    word-break: break-word;\n" +
            "    word-break: break-word;\n" +
          "  }\n" +
            "  }\n" +
          "  /* Defensive: make sure anchors have a box */\n" +
            "  /* Defensive: make sure anchors have a box */\n" +
          "  a[href] { position: relative; }\n" +
            "  a[href] { position: relative; }\n" +
          "}\n";
            "}\n";
        doc.head.appendChild(linkCssFix);
          doc.head.appendChild(linkCssFix);


        // 2) Normalize long link text so it doesn't force wrapping inside anchors.
          // 2) Normalize long link text so it doesn't force wrapping inside anchors.
        (function () {
          (function () {
          // Shorten long visible URLs in external references, keep href intact
            // Shorten long visible URLs in external references, keep href intact
          var refs = doc.querySelectorAll(
            var refs = doc.querySelectorAll(
            ".article-external-reference a[href]"
              ".article-external-reference a[href]"
          );
            );
          refs.forEach(function (a) {
            refs.forEach(function (a) {
            var txt = (a.textContent || "").trim();
              var txt = (a.textContent || "").trim();
            var href = a.getAttribute("href") || "";
              var href = a.getAttribute("href") || "";
            var looksLongUrl = /^https?:\/\//i.test(txt) && txt.length > 60;
              var looksLongUrl = /^https?:\/\//i.test(txt) && txt.length > 60;


            if (looksLongUrl) {
              if (looksLongUrl) {
              try {
                try {
                var u = new URL(href, doc.baseURI);
                  var u = new URL(href, doc.baseURI);
                var label =
                  var label =
                  u.hostname +
                    u.hostname +
                  (u.pathname.replace(/\/$/, "") ? u.pathname : "");
                    (u.pathname.replace(/\/$/, "") ? u.pathname : "");
                if (label.length > 40) label = label.slice(0, 37) + "…";
                  if (label.length > 40) label = label.slice(0, 37) + "…";
                a.textContent = label;
                  a.textContent = label;
              } catch (e) {
                } catch (e) {
                a.textContent = "Link";
                  a.textContent = "Link";
                }
               }
               }
            }


            // Ensure single-box anchors
              // Ensure single-box anchors
            a.style.whiteSpace = "nowrap";
              a.style.whiteSpace = "nowrap";
            a.style.wordBreak = "normal";
              a.style.wordBreak = "normal";
            a.style.overflowWrap = "normal";
              a.style.overflowWrap = "normal";
          });
            });


          // Icon links ([PDF⤴] [WEB⤴]) are short; still enforce single-box
            // Icon links ([PDF⤴] [WEB⤴]) are short; still enforce single-box
          doc.querySelectorAll(".link-pdf a[href]").forEach(function (a) {
            doc.querySelectorAll(".link-pdf a[href]").forEach(function (a) {
            a.style.whiteSpace = "nowrap";
              a.style.whiteSpace = "nowrap";
            a.style.wordBreak = "normal";
              a.style.wordBreak = "normal";
            a.style.overflowWrap = "normal";
              a.style.overflowWrap = "normal";
          });
            });
        })();
          })();


        // Wait helpers
          // Wait helpers
        function waitImages() {
          function waitImages() {
          var imgs = [].slice.call(doc.images || []);
            var imgs = [].slice.call(doc.images || []);
          if (!imgs.length) return Promise.resolve();
            if (!imgs.length) return Promise.resolve();
          return Promise.all(
            return Promise.all(
            imgs.map(function (img) {
              imgs.map(function (img) {
              if (img.decode) {
                if (img.decode) {
                try {
                  try {
                  return img.decode().catch(function () {});
                    return img.decode().catch(function () {});
                } catch (e) {}
                  } catch (e) {}
              }
                }
              return new Promise(function (res) {
                return new Promise(function (res) {
                if (img.complete) return res();
                  if (img.complete) return res();
                img.onload = img.onerror = function () {
                  img.onload = img.onerror = function () {
                  res();
                    res();
                };
                  };
              });
                });
            })
              })
          );
            );
        }
          }


        function waitFonts(timeoutMs) {
          function waitFonts(timeoutMs) {
          if (!doc.fonts || !doc.fonts.ready) return Promise.resolve();
            if (!doc.fonts || !doc.fonts.ready) return Promise.resolve();
          var ready = doc.fonts.ready;
            var ready = doc.fonts.ready;
          var t = new Promise(function (res) {
            var t = new Promise(function (res) {
            setTimeout(res, timeoutMs || 1200);
              setTimeout(res, timeoutMs || 1200);
          });
            });
          return Promise.race([ready, t]);
            return Promise.race([ready, t]);
        }
          }


        // **Load the specific face** so Chrome actually uses it
          // **Load the specific face** so Chrome actually uses it
        function waitSpecificFont(timeoutMs) {
          function waitSpecificFont(timeoutMs) {
          if (!doc.fonts || !doc.fonts.load) return Promise.resolve();
            if (!doc.fonts || !doc.fonts.load) return Promise.resolve();
          var p = Promise.all([
            var p = Promise.all([
            doc.fonts.load('400 16px "HALColant-TextRegular"'),
              doc.fonts.load('400 16px "HALColant-TextRegular"'),
            doc.fonts.load('normal 16px "HALColant-TextRegular"'),
              doc.fonts.load('normal 16px "HALColant-TextRegular"'),
          ]);
            ]);
          var t = new Promise(function (res) {
            var t = new Promise(function (res) {
            setTimeout(res, timeoutMs || 1200);
              setTimeout(res, timeoutMs || 1200);
          });
            });
          return Promise.race([p, t]);
            return Promise.race([p, t]);
        }
          }


        function nextFrame() {
          function nextFrame() {
          return new Promise(function (res) {
            return new Promise(function (res) {
            (iframe.contentWindow.requestAnimationFrame || setTimeout)(res, 0);
              (
          });
                iframe.contentWindow.requestAnimationFrame || setTimeout
        }
              )(res, 0);
            });
          }


        Promise.all([
          Promise.all([
          cssLoaded,
            cssLoaded,
          waitImages(),
            waitImages(),
          waitFonts(1200),
            waitFonts(1200),
          waitSpecificFont(1200),
            waitSpecificFont(1200),
          nextFrame(),
            nextFrame(),
        ]).then(function () {
          ]).then(function () {
          try {
            try {
            // build the desired PDF filename via document.title
              // build the desired PDF filename via document.title
            var entryNum = "";
              var entryNum = "";
            var numEl = doc.querySelector(".article-entry-number");
              var numEl = doc.querySelector(".article-entry-number");
            if (numEl) {
              if (numEl) {
              var m = (numEl.textContent || "").match(/\d+/);
                var m = (numEl.textContent || "").match(/\d+/);
              entryNum = m ? m[0] : "";
                entryNum = m ? m[0] : "";
            }
              }
            var desiredTitle =
              var desiredTitle =
              (entryNum ? entryNum + "." : "") + "softwear.directory";
                (entryNum ? entryNum + "." : "") + "softwear.directory";


            // save originals (scoped here)
              // save originals (scoped here)
            var oldIframeTitle = doc.title;
              var oldIframeTitle = doc.title;
            var oldParentTitle = document.title;
              var oldParentTitle = document.title;


            // define onafterprint AFTER we have originals so it can close over them
              // define onafterprint AFTER we have originals so it can close over them
            iframe.contentWindow.onafterprint = function () {
              iframe.contentWindow.onafterprint = function () {
              try {
                try {
                doc.title = oldIframeTitle; // restore iframe doc title
                  doc.title = oldIframeTitle; // restore iframe doc title
                document.title = oldParentTitle; // restore parent title
                  document.title = oldParentTitle; // restore parent title
              } catch (e) {}
                } catch (e) {}
              setTimeout(function () {
                setTimeout(function () {
                if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
                  if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
              }, 100);
                }, 100);
              $btn.data("busy", false);
                $btn.data("busy", false);
            };
              };


            // set temporary titles used by Chrome for the default PDF name
              // set temporary titles used by Chrome for the default PDF name
            doc.title = desiredTitle; // iframe document
              doc.title = desiredTitle; // iframe document
            // (next line is optional/redundant; doc === iframe.contentWindow.document)
              // (next line is optional/redundant; doc === iframe.contentWindow.document)
            // iframe.contentWindow.document.title = desiredTitle;
              // iframe.contentWindow.document.title = desiredTitle;
            document.title = desiredTitle; // parent (helps on some setups)
              document.title = desiredTitle; // parent (helps on some setups)


            // print
              // print
            iframe.contentWindow.focus();
              iframe.contentWindow.focus();
            iframe.contentWindow.print();
              iframe.contentWindow.print();


            // fallback cleanup in case onafterprint doesn’t fire
              // fallback cleanup in case onafterprint doesn’t fire
            setTimeout(function () {
              setTimeout(function () {
              try {
                try {
                doc.title = oldIframeTitle;
                  doc.title = oldIframeTitle;
                document.title = oldParentTitle;
                  document.title = oldParentTitle;
              } catch (e) {}
                } catch (e) {}
              if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
                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);
             }, 1000);
             }
          } catch (err) {
          });
            console.warn("[print] failed before print:", err);
        })
            $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);
         });
         });
      })
    }
      .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

Navigation menu