fix: hover now peeks the preview even while a file is locked; remove the scroll-triggered sticky thumbnail
the scroll-triggered mini-preview from the last commit solved the wrong problem — it never showed in practice (images are generally as tall or taller than the short project texts, so the gap it needed never opened), and wasnt what was actually wanted: the existing left-column preview pane should keep responding to hover even once a file is locked, only reverting to the locked file once the mouse leaves the list. lock now only gates the is-selected styling, the ● locked indicator, and the click-to-navigate semantics on the portfolio index — not what the pane displays.
This commit is contained in:
@@ -251,38 +251,6 @@ body:not(.home) .menu { display: none; }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* small pinned copy of the locked image — appears at the left edge (same
|
|
||||||
spot the real preview normally sits) once that preview has scrolled out
|
|
||||||
of view, and disappears again once the text next to it runs out. width
|
|
||||||
matches .proj-info so the two columns read as a pair. fixed, not sticky:
|
|
||||||
the file list's own box is only as tall as its own content, not the much
|
|
||||||
taller text next to it, so sticky positioning has nothing to be bounded
|
|
||||||
by — a scroll-driven fixed element sidesteps that entirely. */
|
|
||||||
.proj-sticky {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: calc((100vw - var(--container-width)) / 2);
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 18rem;
|
|
||||||
z-index: 5;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.proj-sticky.is-visible { opacity: 1; visibility: visible; }
|
|
||||||
.proj-sticky img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-height: 60vh;
|
|
||||||
object-fit: contain;
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
@media (max-width: 1099.98px) {
|
|
||||||
.proj-sticky { display: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* prev/next as shell "cd" commands — same vocabulary as the rest of the site */
|
/* prev/next as shell "cd" commands — same vocabulary as the rest of the site */
|
||||||
.proj-nav {
|
.proj-nav {
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
|
|||||||
+46
-79
@@ -1,57 +1,14 @@
|
|||||||
/* tui file browser — preview pane (image OR text panes), hover + freeze +
|
/* tui file browser — preview pane (image OR text panes), hover + lock +
|
||||||
keyboard. re-binds itself after pjax navigation (document listeners are added
|
keyboard. re-binds itself after pjax navigation (document listeners are added
|
||||||
once; rows/preview are re-queried per page). */
|
once; rows/preview are re-queried per page). */
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
var rows = [], img = null, texts = [], cap = null, cur = 0, frozen = false, container = null, player = null, pv = null;
|
var rows = [], img = null, texts = [], cap = null, cur = 0, locked = -1, container = null, player = null, pv = null;
|
||||||
var layout = null, mainPreview = null, sticky = null, stickyImg = null;
|
|
||||||
|
|
||||||
function hideTexts() { texts.forEach(function (t) { t.hidden = true; }); }
|
function hideTexts() { texts.forEach(function (t) { t.hidden = true; }); }
|
||||||
function showText(k) { texts.forEach(function (t) { t.hidden = (t.getAttribute("data-text") !== k); }); }
|
function showText(k) { texts.forEach(function (t) { t.hidden = (t.getAttribute("data-text") !== k); }); }
|
||||||
function hidePlayer() { if (player) { player.hidden = true; if (pv && !pv.paused) pv.pause(); } }
|
function hidePlayer() { if (player) { player.hidden = true; if (pv && !pv.paused) pv.pause(); } }
|
||||||
function showPlayer() { if (player) player.hidden = false; }
|
function showPlayer() { if (player) player.hidden = false; }
|
||||||
function setFrozen(v) { frozen = v; if (container) container.classList.toggle("is-frozen", v); syncSticky(); }
|
|
||||||
|
|
||||||
// small pinned copy of the locked image, shown to the left of the text
|
|
||||||
// column once the real preview has scrolled out of view — mirrors the
|
|
||||||
// LOCKED row only (like the main preview already does once frozen), never
|
|
||||||
// the hover state, and only for as long as there's still text alongside it.
|
|
||||||
function ensureSticky() {
|
|
||||||
if (sticky) return;
|
|
||||||
sticky = document.createElement("div");
|
|
||||||
sticky.className = "proj-sticky";
|
|
||||||
stickyImg = document.createElement("img");
|
|
||||||
stickyImg.alt = "";
|
|
||||||
sticky.appendChild(stickyImg);
|
|
||||||
document.body.appendChild(sticky);
|
|
||||||
}
|
|
||||||
function syncSticky() {
|
|
||||||
if (!sticky) return;
|
|
||||||
var a = frozen ? rows[cur] : null;
|
|
||||||
var src = a ? a.getAttribute("data-img") : null;
|
|
||||||
if (src && layout && mainPreview) {
|
|
||||||
if (stickyImg.getAttribute("src") !== src) stickyImg.src = src;
|
|
||||||
updateStickyVisibility();
|
|
||||||
} else {
|
|
||||||
sticky.classList.remove("is-visible");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function updateStickyVisibility() {
|
|
||||||
if (!sticky || !stickyImg.getAttribute("src") || !layout || !mainPreview) return;
|
|
||||||
if (!window.matchMedia("(min-width: 1100px)").matches) { sticky.classList.remove("is-visible"); return; }
|
|
||||||
var pr = mainPreview.getBoundingClientRect();
|
|
||||||
var lr = layout.getBoundingClientRect();
|
|
||||||
var show = pr.bottom < 0 && lr.bottom > 0;
|
|
||||||
sticky.classList.toggle("is-visible", show);
|
|
||||||
}
|
|
||||||
var stickyTick = false;
|
|
||||||
function onScrollOrResize() {
|
|
||||||
if (stickyTick) return;
|
|
||||||
stickyTick = true;
|
|
||||||
requestAnimationFrame(function () { stickyTick = false; updateStickyVisibility(); });
|
|
||||||
}
|
|
||||||
window.addEventListener("scroll", onScrollOrResize, { passive: true });
|
|
||||||
window.addEventListener("resize", onScrollOrResize);
|
|
||||||
|
|
||||||
// tell the shell to "type" the matching command. navigable rows (those with an
|
// tell the shell to "type" the matching command. navigable rows (those with an
|
||||||
// href: ".." and the portfolio project folders) are directories → `cd`; the
|
// href: ".." and the portfolio project folders) are directories → `cd`; the
|
||||||
@@ -73,16 +30,15 @@
|
|||||||
if (img.complete) img.style.opacity = "1"; // already cached → no fade
|
if (img.complete) img.style.opacity = "1"; // already cached → no fade
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(i, silent, scroll) {
|
// renders row i into the preview pane. this runs on hover AND on lock —
|
||||||
|
// hovering always drives what the pane shows, lock only decides what it
|
||||||
|
// falls back to once the mouse leaves the list again.
|
||||||
|
function render(i, silent, scroll) {
|
||||||
if (!rows.length) return;
|
if (!rows.length) return;
|
||||||
if (i < 0) i = 0;
|
if (i < 0) i = 0;
|
||||||
if (i >= rows.length) i = rows.length - 1;
|
if (i >= rows.length) i = rows.length - 1;
|
||||||
cur = i;
|
cur = i;
|
||||||
for (var r = 0; r < rows.length; r++) rows[r].classList.remove("is-selected");
|
|
||||||
var a = rows[i];
|
var a = rows[i];
|
||||||
a.classList.add("is-selected");
|
|
||||||
// only scroll for keyboard nav — scrolling on hover nudges edge rows out from
|
|
||||||
// under the cursor, so the top/bottom row "sometimes" can't be clicked.
|
|
||||||
if (scroll && a.scrollIntoView) a.scrollIntoView({ block: "nearest" });
|
if (scroll && a.scrollIntoView) a.scrollIntoView({ block: "nearest" });
|
||||||
|
|
||||||
var src = a.getAttribute("data-img");
|
var src = a.getAttribute("data-img");
|
||||||
@@ -100,9 +56,17 @@
|
|||||||
cap.textContent = (n ? n.textContent.trim() : a.getAttribute("data-name")) + (y ? " " + y.textContent.trim() : "");
|
cap.textContent = (n ? n.textContent.trim() : a.getAttribute("data-name")) + (y ? " " + y.textContent.trim() : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!silent) ghostCmd(a); // echo into the shell, except on the initial load
|
if (!silent) ghostCmd(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lock(i) {
|
||||||
|
locked = i;
|
||||||
|
for (var r = 0; r < rows.length; r++) rows[r].classList.remove("is-selected");
|
||||||
|
if (rows[i]) rows[i].classList.add("is-selected");
|
||||||
|
if (container) container.classList.toggle("is-frozen", locked >= 0);
|
||||||
|
}
|
||||||
|
function unlock() { lock(-1); }
|
||||||
|
|
||||||
function bind() {
|
function bind() {
|
||||||
var list = document.querySelector(".filebrowser .tui__ls");
|
var list = document.querySelector(".filebrowser .tui__ls");
|
||||||
container = list ? list.closest(".filebrowser") : null;
|
container = list ? list.closest(".filebrowser") : null;
|
||||||
@@ -112,38 +76,48 @@
|
|||||||
player = document.querySelector(".tui__preview .player");
|
player = document.querySelector(".tui__preview .player");
|
||||||
pv = player ? player.querySelector("video") : null;
|
pv = player ? player.querySelector("video") : null;
|
||||||
cap = document.querySelector(".tui__status");
|
cap = document.querySelector(".tui__status");
|
||||||
cur = 0; frozen = false;
|
cur = 0; locked = -1;
|
||||||
|
if (!rows.length) return;
|
||||||
layout = document.querySelector(".proj-layout");
|
|
||||||
mainPreview = document.querySelector(".filebrowser .tui__preview");
|
|
||||||
ensureSticky();
|
|
||||||
sticky.classList.remove("is-visible");
|
|
||||||
if (!rows.length) { layout = null; return; }
|
|
||||||
|
|
||||||
rows.forEach(function (a, i) {
|
rows.forEach(function (a, i) {
|
||||||
// portfolio-index rows are real links (data-img + href together) — a
|
// portfolio-index rows are real links (data-img + href together) — a
|
||||||
// single click there used to jump straight into the project, which
|
// single click there used to jump straight into the project, which
|
||||||
// meant hovering was the only way to preview a different one first.
|
// meant hovering was the only way to preview a different one first.
|
||||||
// now the first click just previews it (same as the old hover did);
|
// now the first click just locks it (no navigation); only a second
|
||||||
// only a second click on the already-previewed row, or the preview
|
// click on the already-locked row, or the preview image itself,
|
||||||
// image itself, actually navigates.
|
// actually navigates.
|
||||||
var isProjectLink = a.hasAttribute("href") && a.hasAttribute("data-img");
|
var isProjectLink = a.hasAttribute("href") && a.hasAttribute("data-img");
|
||||||
a.addEventListener("mouseenter", function () { if (!frozen) select(i); });
|
a.addEventListener("mouseenter", function () { render(i); });
|
||||||
a.addEventListener("click", function (e) {
|
a.addEventListener("click", function (e) {
|
||||||
if (isProjectLink) {
|
if (isProjectLink) {
|
||||||
if (frozen && cur === i) return; // already previewed — let the link through
|
if (locked === i) return; // already locked — let the link through
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
select(i);
|
lock(i);
|
||||||
setFrozen(true);
|
render(i, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (a.getAttribute("href")) return; // ".." navigates normally
|
if (a.getAttribute("href")) return; // ".." navigates normally
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (frozen && cur === i) { setFrozen(false); ghostClear(); }
|
if (locked === i) { unlock(); ghostClear(); }
|
||||||
else { select(i); setFrozen(true); }
|
else { lock(i); render(i, true); }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// leaving the list reverts the preview back to whatever is locked (or
|
||||||
|
// to the initial file, if nothing is) — hover only ever peeks
|
||||||
|
if (list) {
|
||||||
|
list.addEventListener("mouseleave", function () {
|
||||||
|
render(locked >= 0 ? locked : 0, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = rows.findIndex(function (a) { return a.classList.contains("is-selected"); });
|
||||||
|
start = start < 0 ? 0 : start;
|
||||||
|
render(start, true); // initial preview: don't echo into the shell yet
|
||||||
|
// portfolio/about listings start locked on their first file — games stay
|
||||||
|
// unlocked, there's nothing else to browse there
|
||||||
|
if (container && !container.classList.contains("game-embed")) lock(start);
|
||||||
|
|
||||||
if (img) {
|
if (img) {
|
||||||
img.onclick = function () {
|
img.onclick = function () {
|
||||||
var a = rows[cur];
|
var a = rows[cur];
|
||||||
@@ -154,12 +128,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var start = rows.findIndex(function (a) { return a.classList.contains("is-selected"); });
|
|
||||||
select(start < 0 ? 0 : start, true); // initial selection: don't echo into the shell yet
|
|
||||||
// portfolio/about listings start pre-locked on their first file, same as a
|
|
||||||
// manual click — games stay hover-driven, there's nothing else to browse there
|
|
||||||
if (container && !container.classList.contains("game-embed")) setFrozen(true);
|
|
||||||
|
|
||||||
// warm the cache so switching between gallery images is instant
|
// warm the cache so switching between gallery images is instant
|
||||||
var srcs = rows.map(function (a) { return a.getAttribute("data-img"); }).filter(Boolean);
|
var srcs = rows.map(function (a) { return a.getAttribute("data-img"); }).filter(Boolean);
|
||||||
if (srcs.length) {
|
if (srcs.length) {
|
||||||
@@ -170,20 +138,19 @@
|
|||||||
|
|
||||||
// one-time document listeners (reference the current bound state)
|
// one-time document listeners (reference the current bound state)
|
||||||
// clicking elsewhere IN THE WIDGET (the preview image, blank space in the
|
// clicking elsewhere IN THE WIDGET (the preview image, blank space in the
|
||||||
// list) releases the lock; clicks out in the text/page don't — now that
|
// list) releases the lock; clicks out in the text/page don't — a click to
|
||||||
// listings start locked, a click to read or select a word shouldn't kill
|
// read or select a word shouldn't unlock the currently selected file.
|
||||||
// the sticky preview underneath it.
|
|
||||||
document.addEventListener("click", function (e) {
|
document.addEventListener("click", function (e) {
|
||||||
if (e.target.closest && e.target.closest(".tui__ls a")) return;
|
if (e.target.closest && e.target.closest(".tui__ls a")) return;
|
||||||
if (!(e.target.closest && e.target.closest(".filebrowser"))) return;
|
if (!(e.target.closest && e.target.closest(".filebrowser"))) return;
|
||||||
if (frozen) { setFrozen(false); ghostClear(); }
|
if (locked >= 0) { unlock(); ghostClear(); }
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", function (e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
var t = e.target;
|
var t = e.target;
|
||||||
if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return;
|
if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return;
|
||||||
if (!rows.length) return;
|
if (!rows.length) return;
|
||||||
if (e.key === "ArrowDown") { e.preventDefault(); select(cur + 1, false, true); }
|
if (e.key === "ArrowDown") { e.preventDefault(); render(cur + 1, false, true); }
|
||||||
else if (e.key === "ArrowUp") { e.preventDefault(); select(cur - 1, false, true); }
|
else if (e.key === "ArrowUp") { e.preventDefault(); render(cur - 1, false, true); }
|
||||||
else if (e.key === "Enter") {
|
else if (e.key === "Enter") {
|
||||||
var href = rows[cur] && rows[cur].getAttribute("href");
|
var href = rows[cur] && rows[cur].getAttribute("href");
|
||||||
if (href) { if (window.pjax) window.pjax(href); else window.location.href = href; }
|
if (href) { if (window.pjax) window.pjax(href); else window.location.href = href; }
|
||||||
|
|||||||
Reference in New Issue
Block a user