Skip to content

Instantly share code, notes, and snippets.

@rf5860
Last active March 31, 2024 08:22
Show Gist options
  • Save rf5860/1c160f5955a746eb1bf59725e2696763 to your computer and use it in GitHub Desktop.
Save rf5860/1c160f5955a746eb1bf59725e2696763 to your computer and use it in GitHub Desktop.
Adds on-hover tooltips to Fandom Wiki links
// ==UserScript==
// @name Fandom Tooltips
// @namespace https://fandom.com/
// @version 0.15
// @description Adds on-hover tooltips to Fandom Wiki links
// @author rjf89
// @match *://*.fandom.com/*
// @match *://*.wiki.gg/*
// @updateURL https://gist.github.com/rf5860/1c160f5955a746eb1bf59725e2696763/raw/FandomTooltips.user.js
// @downloadURL https://gist.github.com/rf5860/1c160f5955a746eb1bf59725e2696763/raw/FandomTooltips.user.js
// @grant GM_xmlhttpRequest
// ==/UserScript==
/**
* Function to load the article
* @param e The element that triggered the event
* @param link The link to the article
*/
const hoverFunc = async (e, link) => {
// If a tooltip is already showing, don't show another one
if (document.querySelector('.fl-tooltip')) return;
const content = await loadArticle(link.href);
if (content) showToolTip(e, content);
};
const DomParser = new DOMParser()
const Color = getComputedStyle(document.querySelector('.page__main,#content')).color
const Background = getComputedStyle(document.querySelector('.page__main,#content')).backgroundColor
const getBaseUrl = url => url.split("/").slice(0, 3).join("/");
const isEmptyOrAnchor = href => href == "" || href[0] == "#";
const parseDoc = data => DomParser.parseFromString(data, "text/html")
const extractInfobox = data => parseDoc(data).querySelector("div.infobox,.portable-infobox")
const isRelativeLink = (e, attr) => {
const link = e.getAttribute(attr);
return link && !isEmptyOrAnchor(link) && !link.startsWith("http");
}
const updateRelativeLink = (e, attr, baseUrl) => {
const link = e.getAttribute(attr);
if (link && !isEmptyOrAnchor(link) && !link.startsWith("http")) {
e.setAttribute(attr, `${baseUrl}/${link}`);
}
}
/**
* Loads the article with the given URL.
*
* @param {string} articleName - The article URL.
* @returns {Object|null} - The loaded article object or null if the article is invalid.
*/
async function loadArticle(href) {
if (isEmptyOrAnchor(href)) return null;
try {
// Get the base URL of the current page we're on
const infobox = await ajaxFunc(href);
return extractInfobox(infobox);
} catch (error) {
return null;
}
}
/**
* Makes an AJAX request to the specified URL and returns a promise that resolves with the response data.
* Usage example:
* ajaxFunc('https://example.com/data.json')
* .then(data => {
* // Use the data here
* console.log(data);
* })
* .catch(error => {
* // Handle any errors here
* console.error('An error occurred:', error);
* });
* @param {string} url - The URL to make the AJAX request to.
* @returns {Promise} A promise that resolves with the response data if the request is successful, or rejects with an error if the request fails.
*/
const ajaxFunc = url => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: response => {
const finalUrl = getBaseUrl(response.finalUrl);
const currentUrl = getBaseUrl(window.location.href);
// Check if it's a redirect
if (finalUrl != currentUrl) {
// Update any relative links to absolute links
const content = parseDoc(response.responseText);
const imgs = [...content.querySelectorAll("img")].filter(e => isRelativeLink(e, "src"));
const links = [...content.querySelectorAll("a")].filter(e => isRelativeLink(e, "href"));
links.forEach(e => updateRelativeLink(e, "href", finalUrl));
imgs.forEach(e => updateRelativeLink(e, "src", finalUrl));
// Note that we convert to match the type of response.responseText
return resolve(content.documentElement.outerHTML);
}
return resolve(response.responseText);
},
onerror: error => reject(error)
});
});
/**
* Displays a tooltip with the given content at the specified event position.
*
* @param {Event} event - The event object that triggered the tooltip.
* @param {Object} content - The content to be displayed in the tooltip.
*/
const showToolTip = (event, content) => {
hoverOut()
const tooltip = document.createElement('div');
tooltip.className = 'fl-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.zIndex = 9999;
tooltip.style.backgroundColor = Background;
tooltip.style.color = Color;
tooltip.style.border = '1px solid black';
tooltip.style.overflow = 'auto';
tooltip.addEventListener('mouseout', hoverOut);
tooltip.appendChild(content);
content.style.position = 'unset';
document.body.appendChild(tooltip);
const tooltipRect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Check if the tooltip goes off the right of the viewport
if (event.pageX + tooltipRect.width + 10 > viewportWidth) {
tooltip.style.left = `${event.pageX - tooltipRect.width - 10}px`;
} else {
tooltip.style.left = `${event.pageX + 10}px`;
}
// Check if the tooltip goes off the bottom of the viewport
if (event.pageY + tooltipRect.height + 10 > viewportHeight) {
tooltip.style.top = `${event.pageY - tooltipRect.height - 10}px`;
} else {
tooltip.style.top = `${event.pageY + 10}px`;
}
}
const hoverOut = () => [...document.querySelectorAll('.fl-tooltip')].forEach(tooltip => tooltip.remove());
const hoverOver = (e, link) => {
if (document.querySelectorAll('.fl-tooltip').length != 0) return;
hoverFunc(e, link);
};
(function () {
let links = [...document.querySelectorAll('#content a')];
links.forEach(function (link) {
link.addEventListener('mouseover', function (e) { hoverOver(e, this) });
link.addEventListener('mouseout', hoverOut);
});
console.info("Tooltips loaded");
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment