892dfe600d
clicking the already-locked row navigated (the old second-click-to-open flow), which meant there was no way back to the unlocked hover-browsing state once something got locked. row clicks now toggle lock/unlock uniformly everywhere; the preview image is the one dedicated way to actually open a project, on the index same as project pages.
186 lines
8.5 KiB
JavaScript
186 lines
8.5 KiB
JavaScript
/* tui file browser — preview pane (image OR text panes), hover + lock +
|
|
keyboard. re-binds itself after pjax navigation (document listeners are added
|
|
once; rows/preview are re-queried per page). */
|
|
(function () {
|
|
"use strict";
|
|
var rows = [], img = null, texts = [], cap = null, cur = 0, locked = -1, container = null, player = null, pv = null;
|
|
|
|
function hideTexts() { texts.forEach(function (t) { t.hidden = true; }); }
|
|
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 showPlayer() { if (player) player.hidden = false; }
|
|
|
|
// tell the shell to "type" the matching command. navigable rows (those with an
|
|
// href: ".." and the portfolio project folders) are directories → `cd`; the
|
|
// preview-only leaves inside a project (images, info.md, video) are files → `cat`.
|
|
function ghostCmd(a) {
|
|
if (!a) return;
|
|
var name = a.getAttribute("data-name") || "";
|
|
var isDir = a.hasAttribute("href");
|
|
document.dispatchEvent(new CustomEvent("shell:ghost", { detail: (isDir ? "cd " : "cat ") + name }));
|
|
}
|
|
function ghostClear() { document.dispatchEvent(new CustomEvent("shell:ghost-clear")); }
|
|
function showImg(src) {
|
|
if (!img) return;
|
|
img.hidden = false;
|
|
if (img.getAttribute("src") === src) { img.style.opacity = "1"; return; }
|
|
img.style.opacity = "0";
|
|
img.onload = img.onerror = function () { img.style.opacity = "1"; };
|
|
img.src = src;
|
|
if (img.complete) img.style.opacity = "1"; // already cached → no fade
|
|
}
|
|
|
|
// renders row i into the MAIN preview pane. driven by lock (click) and
|
|
// keyboard nav only — hovering never touches this, it drives the small
|
|
// peek column instead (see bind()), so a locked file stays put while you
|
|
// browse the others.
|
|
function render(i, silent, scroll) {
|
|
if (!rows.length) return;
|
|
if (i < 0) i = 0;
|
|
if (i >= rows.length) i = rows.length - 1;
|
|
cur = i;
|
|
var a = rows[i];
|
|
if (scroll && a.scrollIntoView) a.scrollIntoView({ block: "nearest" });
|
|
|
|
var src = a.getAttribute("data-img");
|
|
var tkey = a.getAttribute("data-text");
|
|
var vid = a.getAttribute("data-video");
|
|
if (src) { showImg(src); hideTexts(); hidePlayer(); }
|
|
else if (vid) { if (img) img.hidden = true; hideTexts(); showPlayer(); }
|
|
else if (tkey) { if (img) img.hidden = true; hidePlayer(); showText(tkey); }
|
|
// rows with no preview of their own (".." above all) leave whatever was
|
|
// last shown in place instead of blanking the pane out
|
|
|
|
if (cap) {
|
|
var n = a.querySelector(".tui__name");
|
|
var y = a.querySelector(".tui__date");
|
|
cap.textContent = (n ? n.textContent.trim() : a.getAttribute("data-name")) + (y ? " " + y.textContent.trim() : "");
|
|
}
|
|
|
|
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() {
|
|
var list = document.querySelector(".filebrowser .tui__ls");
|
|
container = list ? list.closest(".filebrowser") : null;
|
|
rows = list ? Array.prototype.slice.call(list.querySelectorAll("a[data-name]")) : [];
|
|
img = document.querySelector(".tui__preview img");
|
|
texts = Array.prototype.slice.call(document.querySelectorAll(".tui__preview .tui__text"));
|
|
player = document.querySelector(".tui__preview .player");
|
|
pv = player ? player.querySelector("video") : null;
|
|
cap = document.querySelector(".tui__status");
|
|
cur = 0; locked = -1;
|
|
if (!rows.length) return;
|
|
|
|
// small side-by-side peek — sits to the left of the file list (mirroring
|
|
// the info text's spot on the right, same 18rem width) and shows
|
|
// whichever OTHER image is under the mouse while something is locked,
|
|
// without disturbing the main preview.
|
|
var peek = null, peekImg = null;
|
|
if (container) {
|
|
peek = document.createElement("div");
|
|
peek.className = "proj-peek";
|
|
peekImg = document.createElement("img");
|
|
peekImg.alt = "";
|
|
peek.appendChild(peekImg);
|
|
container.insertBefore(peek, container.firstChild);
|
|
}
|
|
function showPeek(a) {
|
|
var src = a.getAttribute("data-img");
|
|
if (!peek || !src) { if (peek) peek.classList.remove("is-visible"); return; }
|
|
peekImg.src = src;
|
|
peek.classList.add("is-visible");
|
|
}
|
|
function hidePeek() { if (peek) peek.classList.remove("is-visible"); }
|
|
|
|
rows.forEach(function (a, i) {
|
|
// portfolio-index rows are real links (data-img + href together) — a
|
|
// single click there used to jump straight into the project, which
|
|
// meant hovering was the only way to preview a different one first.
|
|
// now a click just locks/unlocks it like any other row; the preview
|
|
// image is the dedicated way to actually navigate in.
|
|
var isProjectLink = a.hasAttribute("href") && a.hasAttribute("data-img");
|
|
a.addEventListener("mouseenter", function () {
|
|
if (locked < 0) { hidePeek(); render(i); } // nothing locked — hover drives the main preview, like before locking existed
|
|
else if (i !== locked) showPeek(a);
|
|
else hidePeek();
|
|
});
|
|
a.addEventListener("click", function (e) {
|
|
if (isProjectLink) e.preventDefault(); // never navigate from the row itself
|
|
else if (a.getAttribute("href")) return; // ".." navigates normally
|
|
else e.preventDefault();
|
|
if (locked === i) { unlock(); ghostClear(); }
|
|
else { lock(i); render(i, true); }
|
|
hidePeek();
|
|
});
|
|
});
|
|
|
|
if (list) list.addEventListener("mouseleave", hidePeek);
|
|
|
|
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) {
|
|
img.onclick = function () {
|
|
var a = rows[cur];
|
|
if (a && a.hasAttribute("href") && a.hasAttribute("data-img")) {
|
|
var href = a.getAttribute("href");
|
|
if (window.pjax) window.pjax(href); else window.location.href = href;
|
|
}
|
|
};
|
|
}
|
|
|
|
// warm the cache so switching between gallery images is instant
|
|
var srcs = rows.map(function (a) { return a.getAttribute("data-img"); }).filter(Boolean);
|
|
if (srcs.length) {
|
|
var preload = function () { srcs.forEach(function (s) { var im = new Image(); im.src = s; }); };
|
|
if (window.requestIdleCallback) requestIdleCallback(preload); else setTimeout(preload, 300);
|
|
}
|
|
}
|
|
|
|
// one-time document listeners (reference the current bound state)
|
|
// clicking elsewhere IN THE WIDGET (the preview image, blank space in the
|
|
// list) releases the lock; clicks out in the text/page don't — a click to
|
|
// read or select a word shouldn't unlock the currently selected file.
|
|
document.addEventListener("click", function (e) {
|
|
if (e.target.closest && e.target.closest(".tui__ls a")) return;
|
|
if (!(e.target.closest && e.target.closest(".filebrowser"))) return;
|
|
if (locked >= 0) { unlock(); ghostClear(); }
|
|
});
|
|
document.addEventListener("keydown", function (e) {
|
|
var t = e.target;
|
|
if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return;
|
|
if (!rows.length) return;
|
|
if (e.key === "ArrowDown") { e.preventDefault(); render(cur + 1, false, true); }
|
|
else if (e.key === "ArrowUp") { e.preventDefault(); render(cur - 1, false, true); }
|
|
else if (e.key === "Enter") {
|
|
var href = rows[cur] && rows[cur].getAttribute("href");
|
|
if (href) { if (window.pjax) window.pjax(href); else window.location.href = href; }
|
|
}
|
|
});
|
|
|
|
// the always-open home menu lives in the persistent header (outside the
|
|
// filebrowser and untouched by pjax). hovering a folder ghost-types `cd <name>`
|
|
// so the shell echoes where a click would take you, until you type for real.
|
|
Array.prototype.forEach.call(document.querySelectorAll(".menu .tui__ls a[data-name]"), function (a) {
|
|
a.addEventListener("mouseenter", function () {
|
|
document.dispatchEvent(new CustomEvent("shell:ghost", { detail: "cd " + (a.getAttribute("data-name") || "") }));
|
|
});
|
|
});
|
|
|
|
bind();
|
|
document.addEventListener("pjax:load", bind);
|
|
})();
|