PageSourceSearch

https://ignorethecode.net/blog/js/footnotes.js

js ignorethecode.net collected 2026-10-02 04:27:26 UTC 3,240 bytes, 119 lines download raw bytes

1// Hover footnotes, dependency-free (previously required jQuery).
2//
3// Exposes window.Footnotes.setup(root) so dynamically inserted content (the
4// infinite-scroll "load more") can re-bind its footnote links. The fade is
5// handled by a CSS transition on #footnotediv (see blog.css).
6(function () {
7	"use strict";
8
9	var footnotetimeout = false;
10
11	function isTouch() {
12		var agent = navigator.userAgent.toLowerCase();
13		return (
14			agent.indexOf("iphone") >= 0 ||
15			agent.indexOf("ipad") >= 0 ||
16			agent.indexOf("ipod") >= 0 ||
17			agent.indexOf("android") >= 0
18		);
19	}
20
21	function pageOffset(el) {
22		var rect = el.getBoundingClientRect();
23		return {
24			left: rect.left + window.pageXOffset,
25			top: rect.top + window.pageYOffset
26		};
27	}
28
29	function remove() {
30		var div = document.getElementById("footnotediv");
31		if (!div) {
32			return;
33		}
34		div.style.opacity = "0";
35		window.setTimeout(function () {
36			if (div.parentNode) {
37				div.parentNode.removeChild(div);
38			}
39		}, 600);
40	}
41
42	function divOver() {
43		window.clearTimeout(footnotetimeout);
44		var div = document.getElementById("footnotediv");
45		if (div) {
46			div.style.opacity = "0.9";
47		}
48	}
49
50	function footnoteOut() {
51		footnotetimeout = window.setTimeout(remove, 100);
52	}
53
54	function footnoteOver(event) {
55		window.clearTimeout(footnotetimeout);
56		var existing = document.getElementById("footnotediv");
57		if (existing && existing.parentNode) {
58			existing.parentNode.removeChild(existing);
59		}
60
61		var link = event.currentTarget;
62		var id = link.getAttribute("href").substr(1);
63		var target = document.getElementById(id);
64		if (!target) {
65			return;
66		}
67
68		var position = pageOffset(link);
69		var div = document.createElement("div");
70		div.id = "footnotediv";
71		div.innerHTML = "<div>" + target.innerHTML + "</div>";
72
73		if (isTouch()) {
74			// On iOS, binding mouseover here would swallow the first tap; a
75			// click-to-dismiss is the reliable behaviour.
76			div.addEventListener("click", remove);
77		} else {
78			div.addEventListener("mouseover", divOver);
79			div.addEventListener("mouseout", footnoteOut);
80		}
81
82		document.body.appendChild(div);
83
84		var left = position.left;
85		if (left + 420 > window.innerWidth + window.pageXOffset) {
86			left = window.innerWidth - 420 + window.pageXOffset;
87		}
88		var top = position.top + 20;
89		if (top + div.offsetHeight > window.innerHeight + window.pageYOffset) {
90			top = position.top - div.offsetHeight - 15;
91		}
92		div.style.left = left + "px";
93		div.style.top = top + "px";
94		div.style.opacity = "0.9";
95	}
96
97	function setup(root) {
98		root = root || document;
99		// Modern Markdown Extra marks footnote links with class="footnote-ref";
100		// very old posts used rel="footnote". Match both.
101		var links = root.querySelectorAll("a.footnote-ref, a[rel='footnote']");
102		Array.prototype.forEach.call(links, function (link) {
103			link.removeEventListener("mouseover", footnoteOver);
104			link.removeEventListener("mouseout", footnoteOut);
105			link.addEventListener("mouseover", footnoteOver);
106			link.addEventListener("mouseout", footnoteOut);
107		});
108	}
109
110	window.Footnotes = { setup: setup };
111
112	if (document.readyState === "loading") {
113		document.addEventListener("DOMContentLoaded", function () {
114			setup(document);
115		});
116	} else {
117		setup(document);
118	}
119})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.