User:Ioaxxere/PagePreviews.js

function pagePreviews { let popupStyles = document.createElement("style"); popupStyles.textContent = ` .page-preview ol { margin: 0 0.5em 0 1.5em; padding: 0; }		.page-preview dl { margin-bottom: 0; }		.page-preview p { margin: 0; }

.popup-fade-in-up { animation: popup-fade-in-up 0.2s ease forwards; }		.popup-fade-in-down { animation: popup-fade-in-down 0.2s ease forwards; }		.popup-fade-out-up { animation: popup-fade-out-up 0.2s ease forwards; }		.popup-fade-out-down { animation: popup-fade-out-down 0.2s ease forwards; }

@keyframes popup-fade-in-up { 0% {				opacity: 0; transform: translate(0, 20px); }		}		@keyframes popup-fade-in-down { 0% {				opacity: 0; transform: translate(0, -20px); }		}		@keyframes popup-fade-out-up { 100% {				opacity: 0; transform: translate(0, -20px); }		}		@keyframes popup-fade-out-down { 100% {				opacity: 0; transform: translate(0, 20px); }		}

.ring-loader { margin: auto; width: 24px; height: 24px; border-radius: 50%; border-top: 5px solid black; border-bottom: 5px solid black; border-left: 5px solid transparent; border-right: 5px solid transparent; animation: spin 1.2s linear infinite; }		@keyframes spin { 100% {				transform: rotate(360deg); }		}`;	document.head.appendChild(popupStyles);

// SVG adapted from: https://www.svgrepo.com/svg/109705/open-book let icon = document.createElement("img"); icon.setAttribute("src", "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='20' height='20'%3E%3Cstyle%3E path { fill: %2f445c; shape-rendering: geometricPrecision; } %3C/style%3E%3Cpath d='M15 4.7V4a7 7 0 0 0-4.8-2Q8.8 2 8 3a3 3 0 0 0-2.5-1Q2.8 2.1 1 4v.7L0 5v10l6.7-1.4.3.4h2l.3-.4L16 15V5zm-9.5 6.6q-2 0-3.5 1v-8Q3.3 3.2 5.4 3q1.2 0 2.1.7v7.9zm8.5.9q-1.5-.8-3.5-.9h-.1q-1 0-2 .3l.1-8q.9-.4 2.1-.6 2 .1 3.4 1.4z' /%3E%3C/svg%3E"); icon.style.verticalAlign = "top";

let restAPI = new mw.Rest({ ajax: { headers: { "Api-User-Agent": "Userscript developed by User:Ioaxxere" } } }); let validTitleRegex = new RegExp("^(?:Appendix:(?:Belter_Creole/[^/]+|Black_Speech/[^/]+|Communicationssprache/[^/]+|Dothraki/[^/]+|Gestures/[^/]+|Interlingue/[^/]+|Interslavic/[^/]+|Klingon/[^/]+|Lapine/[^/]+|Lingua_Franca_Nova/[^/]+|Láadan/[^/]+|Mandalorian/[^/]+|Mandalorian/[^/]+|Mundolinco/[^/]+|Neo/[^/]+|Novial/[^/]+|Protologisms/Long_words/.+|Snowclones/[^/]+|Toki_Pona/[^/]+|Boldface|Capital_letter|Glossary|Italics|Lowercase_letter|Possessive|Repetition|Small_caps|Strikethrough|Subscript|Superscript|Underline|Uppercase_letter)|Unsupported_titles/.+|Reconstruction:.+|(?:[^:](?!/translations$))+)$"); let popupTimer; let popupInDOM = false; let popupContainer = document.createElement("div"); let popup = document.createElement("div"); popup.className = "page-preview"; popup.style = "display: flex; flex-direction: column; box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3), 0 0 0 1px #d2d4d8; border-radius: 2px; box-sizing: border-box; padding: 14px 16px 8px 16px; color: #202122; font-size: 0.875em; line-height: 20px; overflow: auto; width: 100%; height: 100%; background: #fff; position: absolute; overflow-wrap: break-word"; popupContainer.style = "width: 320px; height: 200px; position: absolute"; popupContainer.appendChild(popup);

let loader = document.createElement("div"); loader.className = "ring-loader";

function closePopup { if (popup.classList.contains("popup-fade-in-up")) { popup.classList.remove("popup-fade-in-up"); popup.classList.add("popup-fade-out-down"); } else { popup.classList.remove("popup-fade-in-down"); popup.classList.add("popup-fade-out-up"); }

let openLink = document.querySelector("[data-popup-open]"); if (openLink) openLink.removeAttribute("data-popup-open");

// Wait for animation to finish. If this line is removed, the popup will be transparent but still absorb clicks. setTimeout( => popupContainer.style.display = "none", 200); }

popup.addEventListener("pointerenter", => {		clearTimeout(popupTimer);	});

popup.addEventListener("pointerleave", => {		clearTimeout(popupTimer);		popupTimer = setTimeout(closePopup, 300);	});

function processLink(link) { if (link.matches("nav a")) return; // ignore navigation links let isPreviewLink = link.closest(".page-preview"); let linkTitle = decodeURIComponent(link.href.split("/wiki/").pop.split("#")[0]); let linkAnchor = decodeURIComponent(link.href.split("#")[1] || "");

if (!linkTitle.match(validTitleRegex)) return; // Filter out certain non-mainspace pages. if (link.href.endsWith("#")) return; // Some gadgets emit href="#" which is invalid. if (linkAnchor && linkAnchor.startsWith("cite_note")) return; // Filter out references.

link.addEventListener("pointerenter", (event) => {			clearTimeout(popupTimer);

if (link.hasAttribute("data-popup-open")) return;

let timeToOpen = 400; // ms			if (isPreviewLink) { timeToOpen = 800; } else { closePopup; }

// Fetch popup text immediately on hover to reduce delay. let responsePromise = restAPI.get("/v1/page/" + encodeURIComponent(linkTitle) + "/html");

popupTimer = setTimeout( => {				popupContainer.style.display = "";				popup.innerHTML = "";				popup.appendChild(loader);

if (isPreviewLink) { // Trigger fade-in-down animation. popup.classList.remove("popup-fade-in-up"); popup.classList.remove("popup-fade-in-down"); popup.offsetLeft; // force reflow popup.classList.add("popup-fade-in-down"); } else { if (!popupInDOM) { document.body.appendChild(popupContainer); popupInDOM = true; }

let mouseX = event.clientX; let mouseY = event.clientY;

// Get list of rects (lines), then find the one closest to mouseY. let mouseLine = Array.from(link.getClientRects).reduce((prev, next) => Math.abs((next.top + next.bottom) / 2 - mouseY) < Math.abs((prev.top + prev.bottom) / 2 - mouseY) ? next : prev); let anchorX = (mouseLine.left + mouseLine.right) / 2; if (mouseLine.width >= 100) { anchorX = mouseX; }					let anchorY = (mouseLine.top + mouseLine.bottom) / 2;

// Vertical position. if (anchorY > document.documentElement.clientHeight / 2) { popupContainer.style.top = Math.round(anchorY + document.documentElement.scrollTop - 210) + "px"; popup.classList.add("popup-fade-in-down"); popup.classList.remove("popup-fade-out-down"); popup.classList.remove("popup-fade-out-up"); } else { popupContainer.style.top = Math.round(anchorY + document.documentElement.scrollTop + 10) + "px"; popup.classList.add("popup-fade-in-up"); popup.classList.remove("popup-fade-out-down"); popup.classList.remove("popup-fade-out-up"); }

// Horizontal position. if (anchorX > document.documentElement.clientWidth / 2) { popupContainer.style.left = Math.round(anchorX + document.documentElement.scrollLeft - 290) + "px"; } else { popupContainer.style.left = "min(" + Math.round(anchorX + document.documentElement.scrollLeft - 20) + "px, calc(100% - 340px))"; }

link.setAttribute("data-popup-open", ""); }

responsePromise.then(response => {					loader.remove;					let responseDocument = new DOMParser.parseFromString(response, "text/html");					let anchoredElement = responseDocument.getElementById(linkAnchor);

let popupHeader = document.createElement("div"); popupHeader.style = "font-size: 90%; color: #2f445c"; popup.appendChild(popupHeader);

let iconContainer = document.createElement("div"); iconContainer.style = "float: right; margin-left: 10px"; iconContainer.appendChild(icon);

if (linkTitle === "Appendix:Glossary") { popupHeader.innerHTML = "Glossary definition of " + linkAnchor.replaceAll("_", " ") + " "; if (anchoredElement && anchoredElement.matches(".template-anchor")) { let glossaryDefinition = anchoredElement.parentElement.nextElementSibling; if (glossaryDefinition && glossaryDefinition.matches("dd")) { glossaryDefinition.style = "margin: 5px 0 0 15px"; popup.appendChild(glossaryDefinition); }						}					} else { // Try to resolve the anchor element to #Chinese. if (!anchoredElement && ["Cantonese", "Gan", "Hakka", "Hokkien", "Literary_Chinese", "Mandarin", "Middle_Chinese", "Old_Chinese", "Wu", "Xiang"].includes(linkAnchor)) { anchoredElement = responseDocument.querySelector("#Chinese"); }

// Try to guess the anchor target in the following order: English, Chinese, Translingual, [first h2 on page] if (!linkAnchor) { anchoredElement = responseDocument.querySelector("#English"); if (!anchoredElement) anchoredElement = responseDocument.querySelector("#Chinese"); if (!anchoredElement) anchoredElement = responseDocument.querySelector("#Translingual"); if (!anchoredElement) anchoredElement = responseDocument.querySelector("h2"); }

let displayTitle = document.createElement("strong"); displayTitle.textContent = linkTitle.split("/").pop.replaceAll("_", " "); if (linkTitle.startsWith("Reconstruction:")) { displayTitle.textContent = "*" + displayTitle.textContent; }

// Make sure that the entry is parsable. if (anchoredElement && anchoredElement.closest("section:has(> h2)")) { let language = anchoredElement.closest("section:has(> h2)").querySelector("h2").textContent;

if (anchoredElement.matches(".senseid")) { let POS = document.createElement("div"); POS.style = "font-size: 110%; font-weight: bold; margin: 8px 0 5px 0"; POS.textContent = anchoredElement.closest("section").firstChild.textContent; // Get the closest h element. popup.appendChild(POS);

let ol = document.createElement("ol"); ol.appendChild(anchoredElement); popup.appendChild(ol); } else { let scrapeSection = anchoredElement.closest("section");

// Find localest section which contains any definitions. while (scrapeSection && !scrapeSection.querySelector("ol, .ja-see, .zh-see")) { scrapeSection = scrapeSection.parentElement.closest("section"); }								if (scrapeSection) { let headwords = scrapeSection.querySelectorAll(".headword-line > strong"); if (new Set(Array.from(headwords).map(h => h.textContent)).size === 1) { // If there is a single unique headword, replace the display title with that. if (headwords[0].querySelector("a:has(> img)")) { displayTitle.innerHTML = headwords[0].querySelector("a:has(> img)").outerHTML; } else { displayTitle = headwords[0]; }									} else if (new Set(Array.from(headwords).map(h => h.cloneNode.outerHTML)).size === 1) { // If there is a single unique set of element attributes, use that with the display title. let temp = displayTitle.innerHTML; displayTitle = headwords[0].cloneNode; // this clears out the inner HTML displayTitle.innerHTML = temp; }									// Remove links in the headword (if there are any). displayTitle.querySelectorAll("a").forEach(elem => elem.outerHTML = elem.innerHTML);

for (let ol of scrapeSection.querySelectorAll(":scope > ol, section > ol")) { let POS = document.createElement("div"); POS.style = "font-size: 110%; font-weight: bold; margin: 8px 0 5px 0"; POS.textContent = ol.parentElement.firstChild.textContent; // Get the closest h element. popup.appendChild(POS); popup.appendChild(ol); }									// and  don't follow the normal format... for (let seeTemplate of scrapeSection.querySelectorAll(".ja-see, .zh-see")) { popup.appendChild(seeTemplate); }								}							}							popupHeader.innerHTML = "Preview definitions of " + language + " " + displayTitle.outerHTML; } else { popupHeader.innerHTML = "Preview definitions of " + displayTitle.outerHTML; }					}

popupHeader.prepend(iconContainer);

if (popup.childElementCount > 1) { // Clean up HTML. // Convert URLs into absolute URLs. popup.querySelectorAll("a[href]").forEach(link => {							link.href = new URL(link.getAttribute("href"), "https://en.wiktionary.org/wiki/" + encodeURIComponent(linkTitle)).href;						});

for (let elem of popup.querySelectorAll("link, .previewonly, .maintenance-line, .mw-empty-elt, li > ul, .reference")) { elem.remove; }						for (let elem of popup.querySelectorAll("*")) { for (let attr of elem.attributes) { if (attr.name != "class" && attr.name != "href" && attr.name != "style" && attr.name != "title" && attr.name != "rel" && attr.name != "src") { elem.removeAttribute(attr.name); }							}						}					} else if (linkAnchor && !anchoredElement) { // Display a message if the anchor is invalid. let noSectionFound = document.createElement("div"); noSectionFound.style = "margin: 10px 0 0 7.5px; font-size: 90%"; noSectionFound.innerHTML = "The " + linkAnchor.replaceAll("_", " ") + " section was not found on this page."; popup.appendChild(noSectionFound); } else { // Display a message if no definitions were found in the section. let noDefinitionsFound = document.createElement("div"); noDefinitionsFound.style = "margin: 5px 0 0 15px; font-size: 90%"; noDefinitionsFound.textContent = "(no definitions found)"; popup.appendChild(noDefinitionsFound); }				});			}, timeToOpen); });

link.addEventListener("pointerleave", => {			clearTimeout(popupTimer);			if (link.hasAttribute("data-popup-open")) {				popupTimer = setTimeout(closePopup, 300);			}		}); }

// Process all links. for (let link of document.querySelectorAll(`a[href^="https://en.wiktionary.org/wiki/"]:not([href*="redlink=1"]), a[href^="/wiki/"], a[href^="#"]`)) { processLink(link); }

// Process links which are added to the DOM after the gadget has run. let mo = new MutationObserver(events => events.forEach(event => event.addedNodes.forEach(node => {		if (node instanceof Element) {			if (node.matches(`a[href^="https://en.wiktionary.org/wiki/"]:not([href*="redlink=1"]), a[href^="/wiki/"], a[href^="#"]`)) {				processLink(node);			}			for (let link of node.querySelectorAll(`a[href^="https://en.wiktionary.org/wiki/"]:not([href*="redlink=1"]), a[href^="/wiki/"], a[href^="#"]`)) {				processLink(link);			}		}	})));

mo.observe(document.body, {childList: true, subtree: true}); }

if (!document.querySelector("body.skin-minerva")) { pagePreviews; }