jekyll-hover-popup 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1010 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ const config = window.__JHP_CONFIG__ || {};
5
+ const HOVER_DELAY_MS = Number(config.hoverDelayMs) || 0;
6
+ const NAV_HOVER_PREVIEW = config.navHoverPreview !== false;
7
+ const TRANSIENT_TITLE_HINT = "SHIFT to persist";
8
+ const MAX_VIEWPORT_WIDTH = 0.92;
9
+ const MAX_VIEWPORT_HEIGHT = 0.92;
10
+ const MIN_WIDTH = 150;
11
+ const MIN_HEIGHT = 80;
12
+ const BAR_HEIGHT = 20;
13
+ const GAP = 10;
14
+
15
+ const DRAG_NONE = 0;
16
+ const DRAG_MOVE = 1;
17
+ const DRAG_N = 2;
18
+ const DRAG_NE = 3;
19
+ const DRAG_E = 4;
20
+ const DRAG_SE = 5;
21
+ const DRAG_S = 6;
22
+ const DRAG_SW = 7;
23
+ const DRAG_W = 8;
24
+ const DRAG_NW = 9;
25
+
26
+ const pageCache = new Map();
27
+ const linkMeta = new WeakMap();
28
+ const persistentUrls = new Set();
29
+ const tempAnchors = new Set();
30
+ const openWindows = new Map();
31
+ let nextWindowId = 0;
32
+ let nextZIndex = 10000;
33
+
34
+ function readStyle(el, prop) {
35
+ if (!el) return "";
36
+ return getComputedStyle(el).getPropertyValue(prop);
37
+ }
38
+
39
+ function syncThemeVars() {
40
+ const root = document.documentElement;
41
+ const body = document.body;
42
+ const main = document.querySelector("#main-content main") || document.querySelector("main");
43
+ const sidebar = document.querySelector(".side-bar");
44
+ const link = document.querySelector("#main-content a[href], main a[href], .site-nav a[href]");
45
+ const blockquote = document.querySelector("#main-content blockquote, main blockquote");
46
+ const code = document.querySelector("#main-content code, main code");
47
+
48
+ const bodyStyle = body ? getComputedStyle(body) : null;
49
+ const mainStyle = main ? getComputedStyle(main) : bodyStyle;
50
+ const sidebarStyle = sidebar ? getComputedStyle(sidebar) : bodyStyle;
51
+ const linkStyle = link ? getComputedStyle(link) : null;
52
+ const blockquoteStyle = blockquote ? getComputedStyle(blockquote) : null;
53
+ const codeStyle = code ? getComputedStyle(code) : null;
54
+
55
+ const set = (name, value) => {
56
+ if (value) root.style.setProperty(name, value);
57
+ };
58
+
59
+ set("--jhp-body-bg", bodyStyle?.backgroundColor);
60
+ set("--jhp-body-color", mainStyle?.color || bodyStyle?.color);
61
+ set("--jhp-header-bg", sidebarStyle?.backgroundColor || bodyStyle?.backgroundColor);
62
+ set("--jhp-link-color", linkStyle?.color);
63
+ set(
64
+ "--jhp-border-color",
65
+ blockquoteStyle?.borderLeftColor ||
66
+ codeStyle?.borderColor ||
67
+ (bodyStyle?.color ? `color-mix(in srgb, ${bodyStyle.color} 18%, transparent)` : "")
68
+ );
69
+ set(
70
+ "--jhp-shadow-color",
71
+ bodyStyle?.color ? `color-mix(in srgb, ${bodyStyle.color} 28%, transparent)` : ""
72
+ );
73
+ set(
74
+ "--jhp-accent-hover",
75
+ bodyStyle?.color ? `color-mix(in srgb, ${bodyStyle.color} 10%, transparent)` : ""
76
+ );
77
+ set("--jhp-font-family", mainStyle?.fontFamily || bodyStyle?.fontFamily);
78
+ set("--jhp-font-size", mainStyle?.fontSize || bodyStyle?.fontSize);
79
+ set("--jhp-line-height", mainStyle?.lineHeight || bodyStyle?.lineHeight);
80
+
81
+ const colorScheme = readStyle(body, "color-scheme") || readStyle(root, "color-scheme");
82
+ if (colorScheme) root.style.colorScheme = colorScheme.trim();
83
+ }
84
+
85
+ function getClientX(evt) {
86
+ if (evt.touches && evt.touches.length) return evt.touches[0].clientX;
87
+ return evt.clientX;
88
+ }
89
+
90
+ function getClientY(evt) {
91
+ if (evt.touches && evt.touches.length) return evt.touches[0].clientY;
92
+ return evt.clientY;
93
+ }
94
+
95
+ function resolveUrl(href) {
96
+ return new URL(href, window.location.href);
97
+ }
98
+
99
+ function resolveUrlAgainst(href, basePageUrl) {
100
+ const base = basePageUrl.startsWith("http")
101
+ ? basePageUrl
102
+ : new URL(basePageUrl, window.location.origin).href;
103
+ return new URL(href, base);
104
+ }
105
+
106
+ function rewriteContentLinks(content, basePageUrl) {
107
+ if (!content || !basePageUrl) return;
108
+
109
+ content.querySelectorAll("a[href]").forEach((anchor) => {
110
+ if (anchor.closest(".jhp-hwin__actions")) return;
111
+
112
+ const href = anchor.getAttribute("href");
113
+ if (!href || href.startsWith("mailto:") || href.startsWith("javascript:")) return;
114
+
115
+ try {
116
+ const resolved = resolveUrlAgainst(href, basePageUrl);
117
+ if (resolved.origin !== window.location.origin) return;
118
+ anchor.setAttribute("href", resolved.pathname + resolved.search + resolved.hash);
119
+ } catch {
120
+ /* ignore malformed href */
121
+ }
122
+ });
123
+ }
124
+
125
+ function normalizePath(pathname) {
126
+ if (!pathname) return "/";
127
+ const normalized = pathname.endsWith("/") ? pathname : `${pathname}/`;
128
+ return normalized;
129
+ }
130
+
131
+ function normalizeLinkKey(url) {
132
+ return normalizePath(url.pathname) + url.search + url.hash;
133
+ }
134
+
135
+ function isSamePage(url) {
136
+ return (
137
+ normalizePath(url.pathname) === normalizePath(window.location.pathname) &&
138
+ url.search === window.location.search
139
+ );
140
+ }
141
+
142
+ function isImagePath(pathname) {
143
+ return /\.(avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(pathname);
144
+ }
145
+
146
+ function isImageLink(anchor, url) {
147
+ if (isImagePath(url.pathname)) return true;
148
+ return !!anchor.querySelector("img[src]");
149
+ }
150
+
151
+ // don't show preview for links inside a navigation element
152
+ function isNavLink(anchor) {
153
+ return !!anchor.closest("nav, .site-nav, .nav-list, [role='navigation']");
154
+ }
155
+
156
+ // don't show preview for heading permalinks
157
+ function isHeadingPermalink(anchor) {
158
+ if (!anchor) return false;
159
+ if (anchor.classList.contains("anchor-heading")) return true;
160
+ const use = anchor.querySelector("use");
161
+ if (!use) return false;
162
+ const ref = use.getAttribute("href") || use.getAttributeNS("http://www.w3.org/1999/xlink", "href");
163
+ return ref === "#svg-link";
164
+ }
165
+
166
+ function isInternalPageLink(anchor) {
167
+ if (!anchor || anchor.closest(".jhp-hwin__actions")) return false;
168
+ if (isHeadingPermalink(anchor)) return false;
169
+ if (!NAV_HOVER_PREVIEW && isNavLink(anchor)) return false;
170
+
171
+ const href = anchor.getAttribute("href");
172
+ if (!href || href.startsWith("mailto:") || href.startsWith("javascript:")) return false;
173
+
174
+ let url;
175
+ try {
176
+ url = resolveUrl(href);
177
+ } catch {
178
+ return false;
179
+ }
180
+
181
+ if (url.origin !== window.location.origin) return false;
182
+ if (url.pathname.endsWith(".pdf") || url.pathname.endsWith(".zip")) return false;
183
+
184
+ if (isImageLink(anchor, url)) return false;
185
+
186
+ if (isSamePage(url)) return !!url.hash;
187
+ if (url.hash) return true;
188
+ return !!url.pathname;
189
+ }
190
+
191
+ function getMainContent(doc) {
192
+ return doc.querySelector("#main-content main") || doc.querySelector("main");
193
+ }
194
+
195
+ function headingLevel(node) {
196
+ const match = node.tagName && node.tagName.match(/^H([1-6])$/);
197
+ return match ? Number(match[1]) : null;
198
+ }
199
+
200
+ // Preview shows to the end of the callout section
201
+ // not to the beginning of the next parent header
202
+ const CALLOUT_BOUNDARY_SELECTOR =
203
+ "blockquote[id], div[id], section[id], article[id], aside[id], details[id], li[id], p[id], table[id], dl[id], figure[id], pre[id]";
204
+
205
+ function isCalloutBoundary(el) {
206
+ if (!el || !el.id) return false;
207
+ if (el.tagName === "A") return false;
208
+ if (headingLevel(el) != null) return true;
209
+ return /^(BLOCKQUOTE|DIV|SECTION|ARTICLE|ASIDE|DETAILS|LI|P|TABLE|DL|FIGURE|PRE)$/i.test(el.tagName);
210
+ }
211
+
212
+ function isSectionBoundary(el) {
213
+ if (!el || !el.id) return false;
214
+ if (el.tagName === "A") return true;
215
+ return isCalloutBoundary(el);
216
+ }
217
+
218
+ function findCalloutRoot(el) {
219
+ if (!el) return null;
220
+ if (isCalloutBoundary(el)) return el;
221
+ const callout = el.closest(CALLOUT_BOUNDARY_SELECTOR);
222
+ return callout && isCalloutBoundary(callout) ? callout : null;
223
+ }
224
+
225
+ function findSectionRoot(el) {
226
+ if (!el) return null;
227
+ if (headingLevel(el) != null) return el;
228
+ const callout = findCalloutRoot(el);
229
+ if (callout) return callout;
230
+ const heading = el.closest("h1,h2,h3,h4,h5,h6");
231
+ return heading || el;
232
+ }
233
+
234
+ function cloneSectionFromRoot(root, doc) {
235
+ const wrapper = doc.createElement("div");
236
+ wrapper.className = "jhp-section";
237
+
238
+ const level = headingLevel(root);
239
+ const isIdSection = level == null && !!root.id;
240
+ wrapper.appendChild(root.cloneNode(true));
241
+
242
+ let sibling = root.nextElementSibling;
243
+ while (sibling) {
244
+ const siblingLevel = headingLevel(sibling);
245
+ if (level != null && siblingLevel != null && siblingLevel <= level) break;
246
+ if (level == null && siblingLevel != null) break;
247
+ if (isIdSection && isSectionBoundary(sibling)) break;
248
+ wrapper.appendChild(sibling.cloneNode(true));
249
+ sibling = sibling.nextElementSibling;
250
+ }
251
+
252
+ return wrapper;
253
+ }
254
+
255
+ function extractContent(doc, hash) {
256
+ const main = getMainContent(doc);
257
+ if (!main) return null;
258
+
259
+ if (!hash) {
260
+ const clone = main.cloneNode(true);
261
+ clone.querySelectorAll(".site-nav, .toc, #toc, .child-nav").forEach((el) => el.remove());
262
+ return clone;
263
+ }
264
+
265
+ const id = decodeURIComponent(hash.slice(1));
266
+ let target = doc.getElementById(id);
267
+ if (!target) {
268
+ target = doc.querySelector(`a.anchor[name="${CSS.escape(id)}"]`);
269
+ }
270
+ if (!target) return null;
271
+
272
+ const root = findSectionRoot(target);
273
+ return cloneSectionFromRoot(root, doc);
274
+ }
275
+
276
+ async function fetchPageDocument(pathname) {
277
+ const cacheKey = pathname.endsWith("/") ? pathname : `${pathname}/`;
278
+ if (pageCache.has(cacheKey)) return pageCache.get(cacheKey);
279
+
280
+ const fetchPath = cacheKey;
281
+ const response = await fetch(fetchPath, { credentials: "same-origin" });
282
+ if (!response.ok) {
283
+ const fallback = pathname.endsWith("/") ? pathname.slice(0, -1) : `${pathname}/`;
284
+ if (fallback !== fetchPath) {
285
+ const retry = await fetch(fallback, { credentials: "same-origin" });
286
+ if (retry.ok) {
287
+ const html = await retry.text();
288
+ const doc = new DOMParser().parseFromString(html, "text/html");
289
+ pageCache.set(cacheKey, doc);
290
+ pageCache.set(fallback, doc);
291
+ return doc;
292
+ }
293
+ }
294
+ throw new Error(`Failed to load ${fetchPath}`);
295
+ }
296
+
297
+ const html = await response.text();
298
+ const doc = new DOMParser().parseFromString(html, "text/html");
299
+ pageCache.set(cacheKey, doc);
300
+ return doc;
301
+ }
302
+
303
+ async function loadLinkContent(anchor) {
304
+ const url = resolveUrl(anchor.href);
305
+ const hash = url.hash;
306
+
307
+ let doc;
308
+ if (url.pathname === window.location.pathname && url.search === window.location.search) {
309
+ doc = document;
310
+ } else {
311
+ doc = await fetchPageDocument(url.pathname);
312
+ }
313
+
314
+ const content = extractContent(doc, hash);
315
+ if (!content) throw new Error("Section not found");
316
+
317
+ const titleNode = content.querySelector("h1,h2,h3,h4,h5,h6");
318
+ const title = titleNode ? titleNode.textContent.trim() : anchor.textContent.trim() || url.pathname;
319
+ const pageUrl = url.pathname + url.search + url.hash;
320
+
321
+ rewriteContentLinks(content, pageUrl);
322
+
323
+ return {
324
+ content,
325
+ title,
326
+ pageUrl,
327
+ };
328
+ }
329
+
330
+ function cleanTempWindows(exceptAnchor) {
331
+ for (const anchor of tempAnchors) {
332
+ if (anchor === exceptAnchor) continue;
333
+ const meta = linkMeta.get(anchor);
334
+ if (!meta || meta.isPermanent) continue;
335
+ meta.isHovered = false;
336
+ clearTimeout(meta.hoverTimer);
337
+ if (meta.windowMeta) {
338
+ meta.windowMeta.close();
339
+ meta.windowMeta = null;
340
+ }
341
+ tempAnchors.delete(anchor);
342
+ }
343
+ }
344
+
345
+ function closeAllWindows() {
346
+ openWindows.forEach((win) => win.close());
347
+ openWindows.clear();
348
+ persistentUrls.clear();
349
+ }
350
+
351
+ function getPositionFromEvent(evt, anchor) {
352
+ const bcr = anchor.getBoundingClientRect();
353
+ return {
354
+ isFromBottom: bcr.top > window.innerHeight / 2,
355
+ isFromRight: bcr.left > window.innerWidth / 2,
356
+ clientX: getClientX(evt),
357
+ bcr,
358
+ isPreventFlicker: true,
359
+ };
360
+ }
361
+
362
+ function getPositionFromPoint(clientX, clientY) {
363
+ const bcr = {
364
+ top: clientY,
365
+ left: clientX,
366
+ bottom: clientY + 1,
367
+ right: clientX + 1,
368
+ height: 1,
369
+ width: 1,
370
+ };
371
+ return {
372
+ isFromBottom: clientY > window.innerHeight / 2,
373
+ isFromRight: clientX > window.innerWidth / 2,
374
+ clientX,
375
+ bcr,
376
+ isPreventFlicker: true,
377
+ };
378
+ }
379
+
380
+ function setWindowPosition(winEl, contentEl, position) {
381
+ const rect = winEl.getBoundingClientRect();
382
+ const width = rect.width || winEl.offsetWidth || MIN_WIDTH;
383
+
384
+ let top;
385
+ if (position.isFromBottom) {
386
+ top = position.bcr.top - rect.height - GAP;
387
+ } else {
388
+ top = position.bcr.top + position.bcr.height + GAP;
389
+ }
390
+
391
+ let left;
392
+ if (position.isFromRight) {
393
+ left = (position.clientX || position.bcr.left) - width - GAP;
394
+ } else {
395
+ left = (position.clientX || position.bcr.left + position.bcr.width) + GAP;
396
+ }
397
+
398
+ winEl.style.top = `${Math.max(0, top)}px`;
399
+ winEl.style.left = `${Math.max(0, left)}px`;
400
+
401
+ adjustWindowToViewport(winEl, contentEl, position);
402
+ }
403
+
404
+ function adjustWindowToViewport(winEl, contentEl, position) {
405
+ const screenW = window.innerWidth;
406
+ const screenH = window.innerHeight;
407
+ let rect = winEl.getBoundingClientRect();
408
+
409
+ if (rect.left < 0) {
410
+ winEl.style.left = "0px";
411
+ } else if (rect.right > screenW) {
412
+ winEl.style.left = `${Math.max(0, screenW - rect.width - 8)}px`;
413
+ }
414
+
415
+ rect = winEl.getBoundingClientRect();
416
+ if (rect.top < 0) {
417
+ winEl.style.top = "0px";
418
+ } else if (rect.bottom > screenH) {
419
+ winEl.style.top = `${Math.max(0, screenH - rect.height - 8)}px`;
420
+ }
421
+
422
+ if (position && position.isPreventFlicker && position.bcr) {
423
+ rect = winEl.getBoundingClientRect();
424
+ const overlap =
425
+ rect.left < position.bcr.right &&
426
+ rect.right > position.bcr.left &&
427
+ rect.top < position.bcr.bottom &&
428
+ rect.bottom > position.bcr.top;
429
+
430
+ if (overlap) {
431
+ const available = position.isFromBottom
432
+ ? position.bcr.top - GAP
433
+ : screenH - position.bcr.bottom - GAP - BAR_HEIGHT;
434
+ contentEl.style.maxHeight = `${Math.max(MIN_HEIGHT, available)}px`;
435
+ }
436
+ }
437
+ }
438
+
439
+ function fitWindowToContent(winEl, contentEl) {
440
+ contentEl.style.height = "";
441
+ contentEl.style.maxHeight = `${Math.floor(window.innerHeight * MAX_VIEWPORT_HEIGHT) - BAR_HEIGHT}px`;
442
+
443
+ const img = contentEl.querySelector("img");
444
+ let contentWidth = contentEl.scrollWidth;
445
+ if (img && img.naturalWidth) {
446
+ contentWidth = Math.max(contentWidth, img.naturalWidth);
447
+ }
448
+
449
+ const targetWidth = Math.min(
450
+ Math.max(contentWidth + 24, MIN_WIDTH),
451
+ Math.floor(window.innerWidth * MAX_VIEWPORT_WIDTH)
452
+ );
453
+ winEl.style.width = `${targetWidth}px`;
454
+
455
+ let naturalHeight = contentEl.scrollHeight;
456
+ if (img && img.naturalHeight) {
457
+ naturalHeight = Math.max(naturalHeight, img.naturalHeight);
458
+ }
459
+
460
+ const maxContentHeight = Math.floor(window.innerHeight * MAX_VIEWPORT_HEIGHT) - BAR_HEIGHT;
461
+ contentEl.style.maxHeight = `${Math.min(naturalHeight, maxContentHeight)}px`;
462
+ }
463
+
464
+ function addResizeHandle(winEl, className, type, onBegin) {
465
+ const handle = document.createElement("div");
466
+ handle.className = `jhp-resize ${className}`;
467
+ handle.addEventListener("mousedown", (evt) => {
468
+ if (evt.button !== 0) return;
469
+ evt.preventDefault();
470
+ evt.stopPropagation();
471
+ onBegin(evt, type);
472
+ });
473
+ winEl.appendChild(handle);
474
+ return handle;
475
+ }
476
+
477
+ function setupWindowInteraction(winEl, header, contentEl, windowMeta) {
478
+ const drag = {
479
+ type: DRAG_NONE,
480
+ startX: 0,
481
+ startY: 0,
482
+ baseTop: 0,
483
+ baseLeft: 0,
484
+ baseWidth: 0,
485
+ baseHeight: 0,
486
+ };
487
+
488
+ function beginInteraction(evt, type) {
489
+ drag.type = type;
490
+ drag.startX = getClientX(evt);
491
+ drag.startY = getClientY(evt);
492
+ drag.baseTop = parseFloat(winEl.style.top) || winEl.getBoundingClientRect().top;
493
+ drag.baseLeft = parseFloat(winEl.style.left) || winEl.getBoundingClientRect().left;
494
+ drag.baseWidth = winEl.offsetWidth;
495
+ drag.baseHeight = contentEl.offsetHeight;
496
+
497
+ if (type !== DRAG_MOVE) {
498
+ contentEl.style.maxHeight = "none";
499
+ contentEl.style.height = `${drag.baseHeight}px`;
500
+ winEl.style.maxWidth = "none";
501
+ }
502
+
503
+ winEl.style.zIndex = String(++nextZIndex);
504
+ document.addEventListener("mousemove", onPointerMove);
505
+ document.addEventListener("mouseup", onPointerUp);
506
+ }
507
+
508
+ function handleNorthDrag(evt) {
509
+ const diffY = Math.max(drag.startY - getClientY(evt), MIN_HEIGHT - drag.baseHeight);
510
+ contentEl.style.height = `${drag.baseHeight + diffY}px`;
511
+ winEl.style.top = `${drag.baseTop - diffY}px`;
512
+ drag.startY = getClientY(evt);
513
+ drag.baseHeight = contentEl.offsetHeight;
514
+ drag.baseTop = parseFloat(winEl.style.top) || winEl.getBoundingClientRect().top;
515
+ }
516
+
517
+ function handleEastDrag(evt) {
518
+ const diffX = drag.startX - getClientX(evt);
519
+ winEl.style.width = `${Math.max(MIN_WIDTH, drag.baseWidth - diffX)}px`;
520
+ drag.startX = getClientX(evt);
521
+ drag.baseWidth = winEl.offsetWidth;
522
+ }
523
+
524
+ function handleSouthDrag(evt) {
525
+ const diffY = drag.startY - getClientY(evt);
526
+ contentEl.style.height = `${Math.max(MIN_HEIGHT, drag.baseHeight - diffY)}px`;
527
+ drag.startY = getClientY(evt);
528
+ drag.baseHeight = contentEl.offsetHeight;
529
+ }
530
+
531
+ function handleWestDrag(evt) {
532
+ const diffX = Math.max(drag.startX - getClientX(evt), MIN_WIDTH - drag.baseWidth);
533
+ winEl.style.width = `${drag.baseWidth + diffX}px`;
534
+ winEl.style.left = `${drag.baseLeft - diffX}px`;
535
+ drag.startX = getClientX(evt);
536
+ drag.baseWidth = winEl.offsetWidth;
537
+ drag.baseLeft = parseFloat(winEl.style.left) || winEl.getBoundingClientRect().left;
538
+ }
539
+
540
+ function onPointerMove(evt) {
541
+ switch (drag.type) {
542
+ case DRAG_MOVE: {
543
+ const dx = drag.startX - getClientX(evt);
544
+ const dy = drag.startY - getClientY(evt);
545
+ winEl.style.left = `${drag.baseLeft - dx}px`;
546
+ winEl.style.top = `${drag.baseTop - dy}px`;
547
+ drag.startX = getClientX(evt);
548
+ drag.startY = getClientY(evt);
549
+ drag.baseLeft = parseFloat(winEl.style.left) || winEl.getBoundingClientRect().left;
550
+ drag.baseTop = parseFloat(winEl.style.top) || winEl.getBoundingClientRect().top;
551
+ break;
552
+ }
553
+ case DRAG_N:
554
+ handleNorthDrag(evt);
555
+ break;
556
+ case DRAG_NE:
557
+ handleNorthDrag(evt);
558
+ handleEastDrag(evt);
559
+ break;
560
+ case DRAG_E:
561
+ handleEastDrag(evt);
562
+ break;
563
+ case DRAG_SE:
564
+ handleSouthDrag(evt);
565
+ handleEastDrag(evt);
566
+ break;
567
+ case DRAG_S:
568
+ handleSouthDrag(evt);
569
+ break;
570
+ case DRAG_SW:
571
+ handleSouthDrag(evt);
572
+ handleWestDrag(evt);
573
+ break;
574
+ case DRAG_W:
575
+ handleWestDrag(evt);
576
+ break;
577
+ case DRAG_NW:
578
+ handleNorthDrag(evt);
579
+ handleWestDrag(evt);
580
+ break;
581
+ default:
582
+ break;
583
+ }
584
+
585
+ if (drag.type !== DRAG_NONE) {
586
+ adjustWindowToViewport(winEl, contentEl, null);
587
+ }
588
+ }
589
+
590
+ function onPointerUp() {
591
+ if (drag.type === DRAG_NONE) return;
592
+ drag.type = DRAG_NONE;
593
+ document.removeEventListener("mousemove", onPointerMove);
594
+ document.removeEventListener("mouseup", onPointerUp);
595
+ adjustWindowToViewport(winEl, contentEl, null);
596
+ }
597
+
598
+ windowMeta.endInteraction = onPointerUp;
599
+
600
+ header.addEventListener("mousedown", (evt) => {
601
+ if (winEl.dataset.perm !== "true") return;
602
+ if (evt.button !== 0) return;
603
+ if (evt.target.closest(".jhp-hwin__btn")) return;
604
+ evt.preventDefault();
605
+ beginInteraction(evt, DRAG_MOVE);
606
+ });
607
+
608
+ addResizeHandle(winEl, "jhp-resize-n", DRAG_N, beginInteraction);
609
+ addResizeHandle(winEl, "jhp-resize-ne", DRAG_NE, beginInteraction);
610
+ addResizeHandle(winEl, "jhp-resize-e", DRAG_E, beginInteraction);
611
+ addResizeHandle(winEl, "jhp-resize-se", DRAG_SE, beginInteraction);
612
+ addResizeHandle(winEl, "jhp-resize-s", DRAG_S, beginInteraction);
613
+ addResizeHandle(winEl, "jhp-resize-sw", DRAG_SW, beginInteraction);
614
+ addResizeHandle(winEl, "jhp-resize-w", DRAG_W, beginInteraction);
615
+ addResizeHandle(winEl, "jhp-resize-nw", DRAG_NW, beginInteraction);
616
+ }
617
+
618
+ function createWindow({ title, pageUrl, isPermanent, onClose }) {
619
+ syncThemeVars();
620
+
621
+ const id = ++nextWindowId;
622
+ const zIndex = ++nextZIndex;
623
+
624
+ const winEl = document.createElement("div");
625
+ winEl.className = "jhp-hwin";
626
+ winEl.dataset.perm = isPermanent ? "true" : "false";
627
+ winEl.style.zIndex = String(zIndex);
628
+ winEl.style.visibility = "hidden";
629
+
630
+ const topBorder = document.createElement("div");
631
+ topBorder.className = "jhp-border jhp-border--top";
632
+
633
+ const header = document.createElement("div");
634
+ header.className = "jhp-hwin__header";
635
+
636
+ const titleEl = document.createElement("span");
637
+ titleEl.className = "jhp-hwin__title";
638
+ let pageTitle = title;
639
+
640
+ function updateTitleDisplay() {
641
+ const permanent = winEl.dataset.perm === "true";
642
+ titleEl.textContent = permanent ? pageTitle : TRANSIENT_TITLE_HINT;
643
+ titleEl.title = permanent ? pageTitle : TRANSIENT_TITLE_HINT;
644
+ }
645
+
646
+ updateTitleDisplay();
647
+
648
+ const actions = document.createElement("div");
649
+ actions.className = "jhp-hwin__actions";
650
+
651
+ const followLink = document.createElement("a");
652
+ followLink.className = "jhp-hwin__btn jhp-hwin__btn--link";
653
+ followLink.href = pageUrl;
654
+ followLink.textContent = "Follow link";
655
+ followLink.title = "Follow link";
656
+
657
+ const closeBtn = document.createElement("button");
658
+ closeBtn.type = "button";
659
+ closeBtn.className = "jhp-hwin__btn";
660
+ closeBtn.textContent = "Close";
661
+ closeBtn.title = "Close (CTRL click to close all)";
662
+
663
+ actions.appendChild(followLink);
664
+ actions.appendChild(closeBtn);
665
+
666
+ header.appendChild(titleEl);
667
+ header.appendChild(actions);
668
+ topBorder.appendChild(header);
669
+
670
+ const contentEl = document.createElement("div");
671
+ contentEl.className = "jhp-hwin__content main-content";
672
+
673
+ const bottomBorder = document.createElement("div");
674
+ bottomBorder.className = "jhp-border jhp-border--bottom";
675
+
676
+ winEl.appendChild(topBorder);
677
+ winEl.appendChild(contentEl);
678
+ winEl.appendChild(bottomBorder);
679
+ document.body.appendChild(winEl);
680
+
681
+ const windowMeta = {
682
+ id,
683
+ el: winEl,
684
+ contentEl,
685
+ endInteraction: null,
686
+ setContent(node) {
687
+ contentEl.innerHTML = "";
688
+ contentEl.appendChild(node);
689
+ fitWindowToContent(winEl, contentEl);
690
+ },
691
+ setLoading() {
692
+ contentEl.innerHTML = '<div class="jhp-hwin__loading">Loading…</div>';
693
+ },
694
+ setError(message) {
695
+ contentEl.innerHTML = `<div class="jhp-hwin__error">${message}</div>`;
696
+ },
697
+ setPosition(position) {
698
+ setWindowPosition(winEl, contentEl, position);
699
+ },
700
+ setPermanent(value) {
701
+ winEl.dataset.perm = value ? "true" : "false";
702
+ updateTitleDisplay();
703
+ },
704
+ setPageTitle(value) {
705
+ pageTitle = value;
706
+ updateTitleDisplay();
707
+ },
708
+ close() {
709
+ if (windowMeta.endInteraction) windowMeta.endInteraction();
710
+ if (winEl.parentNode) winEl.parentNode.removeChild(winEl);
711
+ openWindows.delete(id);
712
+ if (typeof onClose === "function") onClose();
713
+ },
714
+ };
715
+
716
+ setupWindowInteraction(winEl, topBorder, contentEl, windowMeta);
717
+
718
+ closeBtn.addEventListener("click", (evt) => {
719
+ evt.stopPropagation();
720
+ if (evt.ctrlKey || evt.metaKey) {
721
+ closeAllWindows();
722
+ return;
723
+ }
724
+ windowMeta.close();
725
+ });
726
+
727
+ openWindows.set(id, windowMeta);
728
+
729
+ const onResize = () => adjustWindowToViewport(winEl, contentEl, null);
730
+ window.addEventListener("resize", onResize);
731
+ const originalClose = windowMeta.close.bind(windowMeta);
732
+ windowMeta.close = () => {
733
+ window.removeEventListener("resize", onResize);
734
+ originalClose();
735
+ };
736
+
737
+ return windowMeta;
738
+ }
739
+
740
+ function getMeta(anchor) {
741
+ if (!linkMeta.has(anchor)) {
742
+ linkMeta.set(anchor, {
743
+ isHovered: false,
744
+ isLoading: false,
745
+ isPermanent: false,
746
+ windowMeta: null,
747
+ hoverTimer: null,
748
+ linkKey: null,
749
+ });
750
+ }
751
+ return linkMeta.get(anchor);
752
+ }
753
+
754
+ function promoteToPermanent(meta) {
755
+ meta.isPermanent = true;
756
+ if (meta.windowMeta) meta.windowMeta.setPermanent(true);
757
+ if (meta.linkKey) persistentUrls.add(meta.linkKey);
758
+ }
759
+
760
+ async function handleMouseOver(evt, anchor) {
761
+ if (!isInternalPageLink(anchor)) return;
762
+
763
+ const linkKey = normalizeLinkKey(resolveUrl(anchor.href));
764
+ if (persistentUrls.has(linkKey)) return;
765
+
766
+ cleanTempWindows(anchor);
767
+
768
+ const meta = getMeta(anchor);
769
+ tempAnchors.add(anchor);
770
+ if (meta.isHovered || meta.isLoading) return;
771
+ if (meta.isPermanent) return;
772
+
773
+ meta.linkKey = linkKey;
774
+ meta.isHovered = true;
775
+ meta.isPermanent = evt.shiftKey;
776
+ anchor.style.cursor = "progress";
777
+
778
+ clearTimeout(meta.hoverTimer);
779
+
780
+ const showPreview = async () => {
781
+ if (!meta.isHovered && !meta.isPermanent) return;
782
+
783
+ meta.isLoading = true;
784
+
785
+ if (meta.windowMeta && !meta.isPermanent) {
786
+ meta.windowMeta.close();
787
+ meta.windowMeta = null;
788
+ }
789
+
790
+ const win = createWindow({
791
+ title: anchor.textContent.trim() || linkKey,
792
+ pageUrl: resolveUrl(anchor.href).pathname + resolveUrl(anchor.href).search + resolveUrl(anchor.href).hash,
793
+ isPermanent: meta.isPermanent,
794
+ onClose: () => {
795
+ meta.isHovered = false;
796
+ meta.isLoading = false;
797
+ if (meta.linkKey) persistentUrls.delete(meta.linkKey);
798
+ meta.isPermanent = false;
799
+ meta.windowMeta = null;
800
+ tempAnchors.delete(anchor);
801
+ anchor.style.cursor = "";
802
+ },
803
+ });
804
+
805
+ meta.windowMeta = win;
806
+ win.setLoading();
807
+ win.setPosition(getPositionFromEvent(evt, anchor));
808
+ win.el.style.visibility = "visible";
809
+
810
+ try {
811
+ const loaded = await loadLinkContent(anchor);
812
+ if (!meta.isHovered && !meta.isPermanent) {
813
+ win.close();
814
+ return;
815
+ }
816
+
817
+ win.setContent(loaded.content);
818
+ win.setPageTitle(loaded.title);
819
+ win.el.querySelector(".jhp-hwin__btn--link").href = loaded.pageUrl;
820
+ win.setPosition(getPositionFromEvent(evt, anchor));
821
+
822
+ if (meta.isPermanent) {
823
+ promoteToPermanent(meta);
824
+ }
825
+ } catch (err) {
826
+ if (meta.isHovered || meta.isPermanent) {
827
+ win.setError("Could not load preview.");
828
+ } else {
829
+ win.close();
830
+ }
831
+ } finally {
832
+ meta.isLoading = false;
833
+ anchor.style.cursor = "";
834
+ }
835
+ };
836
+
837
+ if (HOVER_DELAY_MS > 0) {
838
+ meta.hoverTimer = setTimeout(showPreview, HOVER_DELAY_MS);
839
+ } else {
840
+ showPreview();
841
+ }
842
+ }
843
+
844
+ function handleMouseLeave(evt, anchor) {
845
+ const meta = linkMeta.get(anchor);
846
+ if (!meta) return;
847
+
848
+ clearTimeout(meta.hoverTimer);
849
+ anchor.style.cursor = "";
850
+
851
+ if (meta.isPermanent) return;
852
+
853
+ if (evt.shiftKey) {
854
+ promoteToPermanent(meta);
855
+ if (meta.windowMeta) meta.windowMeta.setPermanent(true);
856
+ return;
857
+ }
858
+
859
+ meta.isHovered = false;
860
+
861
+ if (meta.isLoading) return;
862
+
863
+ if (meta.windowMeta) {
864
+ meta.windowMeta.close();
865
+ meta.windowMeta = null;
866
+ }
867
+ }
868
+
869
+ function handleMouseMove(evt, anchor) {
870
+ const meta = linkMeta.get(anchor);
871
+ if (!meta || meta.isPermanent) return;
872
+
873
+ if (evt.shiftKey && !meta.isPermanent) {
874
+ promoteToPermanent(meta);
875
+ if (meta.windowMeta) meta.windowMeta.setPermanent(true);
876
+ return;
877
+ }
878
+
879
+ if (!meta.windowMeta || meta.isLoading) return;
880
+
881
+ meta.windowMeta.setPosition(getPositionFromEvent(evt, anchor));
882
+ }
883
+
884
+ function handleClick(evt, anchor) {
885
+ const meta = linkMeta.get(anchor);
886
+ if (!meta || !meta.windowMeta || meta.isPermanent) return;
887
+
888
+ clearTimeout(meta.hoverTimer);
889
+ meta.isHovered = false;
890
+ meta.windowMeta.close();
891
+ meta.windowMeta = null;
892
+ anchor.style.cursor = "";
893
+ }
894
+
895
+ function onPointerOver(evt) {
896
+ const anchor = evt.target.closest("a[href]");
897
+ if (!anchor) return;
898
+ handleMouseOver(evt, anchor);
899
+ }
900
+
901
+ function onPointerOut(evt) {
902
+ const anchor = evt.target.closest("a[href]");
903
+ if (!anchor) return;
904
+ if (anchor.contains(evt.relatedTarget)) return;
905
+ handleMouseLeave(evt, anchor);
906
+ }
907
+
908
+ function onPointerMove(evt) {
909
+ const anchor = evt.target.closest("a[href]");
910
+ if (!anchor) return;
911
+ handleMouseMove(evt, anchor);
912
+ }
913
+
914
+ function onClick(evt) {
915
+ const anchor = evt.target.closest("a[href]");
916
+ if (!anchor) return;
917
+ handleClick(evt, anchor);
918
+ }
919
+
920
+ async function openPreviewLink({ href, title, clientX, clientY, isPermanent = true }) {
921
+ const anchor = document.createElement("a");
922
+ anchor.href = href;
923
+ anchor.textContent = title || href;
924
+
925
+ if (!isInternalPageLink(anchor)) {
926
+ window.location.href = href;
927
+ return null;
928
+ }
929
+
930
+ const url = resolveUrl(href);
931
+ const linkKey = normalizeLinkKey(url);
932
+
933
+ for (const win of openWindows.values()) {
934
+ const follow = win.el.querySelector(".jhp-hwin__btn--link");
935
+ if (!follow) continue;
936
+ try {
937
+ if (normalizeLinkKey(resolveUrl(follow.href)) === linkKey) {
938
+ win.el.style.zIndex = String(++nextZIndex);
939
+ return win;
940
+ }
941
+ } catch {
942
+ /* ignore malformed href */
943
+ }
944
+ }
945
+
946
+ const win = createWindow({
947
+ title: title || href,
948
+ pageUrl: url.pathname + url.search + url.hash,
949
+ isPermanent,
950
+ onClose: () => {
951
+ persistentUrls.delete(linkKey);
952
+ },
953
+ });
954
+
955
+ win.setPermanent(isPermanent);
956
+ const position = getPositionFromPoint(clientX, clientY);
957
+ win.setLoading();
958
+ win.setPosition(position);
959
+ win.el.style.visibility = "visible";
960
+
961
+ try {
962
+ const loaded = await loadLinkContent(anchor);
963
+ win.setContent(loaded.content);
964
+ win.setPageTitle(loaded.title);
965
+ const followLink = win.el.querySelector(".jhp-hwin__btn--link");
966
+ if (followLink) followLink.href = loaded.pageUrl;
967
+ win.setPosition(position);
968
+ if (isPermanent) persistentUrls.add(linkKey);
969
+ } catch {
970
+ win.setError("Could not load preview.");
971
+ }
972
+
973
+ return win;
974
+ }
975
+
976
+ function openPreviewContent({ title, pageUrl, content, clientX, clientY, isPermanent = true, maxWidth = null }) {
977
+ const win = createWindow({
978
+ title: title || "Preview",
979
+ pageUrl: pageUrl || window.location.href,
980
+ isPermanent,
981
+ onClose: () => {},
982
+ });
983
+
984
+ win.setPermanent(isPermanent);
985
+ if (maxWidth) win.el.style.maxWidth = maxWidth;
986
+
987
+ const position = getPositionFromPoint(clientX, clientY);
988
+ rewriteContentLinks(content, pageUrl || window.location.pathname + window.location.search + window.location.hash);
989
+ win.setContent(content);
990
+ if (title) win.setPageTitle(title);
991
+ win.setPosition(position);
992
+ win.el.style.visibility = "visible";
993
+ return win;
994
+ }
995
+
996
+ window.JekyllHoverPopup = {
997
+ isAvailable() {
998
+ return true;
999
+ },
1000
+ openLink: openPreviewLink,
1001
+ openContent: openPreviewContent,
1002
+ };
1003
+
1004
+ document.addEventListener("mouseover", onPointerOver, true);
1005
+ document.addEventListener("mouseout", onPointerOut, true);
1006
+ document.addEventListener("mousemove", onPointerMove, true);
1007
+ document.addEventListener("click", onClick, true);
1008
+
1009
+ syncThemeVars();
1010
+ })();