mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 15:33:17 +00:00
273 lines
8.5 KiB
TypeScript
273 lines
8.5 KiB
TypeScript
type OverlayApi = {
|
|
destroy: () => void;
|
|
refresh: () => void;
|
|
};
|
|
|
|
const SELECTOR = ".desk-detail-scroll, .desk-tree, .p-datatable-table-container";
|
|
const BOUND = "__overlayScrollBound";
|
|
|
|
function attachOverlayScrollbar(el: HTMLElement): OverlayApi {
|
|
if ((el as HTMLElement & { [BOUND]?: boolean })[BOUND]) {
|
|
return { destroy: () => undefined, refresh: () => undefined };
|
|
}
|
|
(el as HTMLElement & { [BOUND]?: boolean })[BOUND] = true;
|
|
|
|
const parent = el.parentElement;
|
|
if (!parent) {
|
|
delete (el as HTMLElement & { [BOUND]?: boolean })[BOUND];
|
|
return { destroy: () => undefined, refresh: () => undefined };
|
|
}
|
|
|
|
const host = document.createElement("div");
|
|
host.className = "overlay-scroll-host";
|
|
parent.insertBefore(host, el);
|
|
host.appendChild(el);
|
|
|
|
const railY = document.createElement("div");
|
|
railY.className = "overlay-scroll-rail overlay-scroll-rail-y";
|
|
railY.setAttribute("aria-hidden", "true");
|
|
const thumbY = document.createElement("div");
|
|
thumbY.className = "overlay-scroll-thumb";
|
|
railY.appendChild(thumbY);
|
|
|
|
const railX = document.createElement("div");
|
|
railX.className = "overlay-scroll-rail overlay-scroll-rail-x";
|
|
railX.setAttribute("aria-hidden", "true");
|
|
const thumbX = document.createElement("div");
|
|
thumbX.className = "overlay-scroll-thumb";
|
|
railX.appendChild(thumbX);
|
|
|
|
host.appendChild(railY);
|
|
host.appendChild(railX);
|
|
|
|
let dragging: "x" | "y" | null = null;
|
|
let dragStart = 0;
|
|
let dragStartScroll = 0;
|
|
let hideTimer = 0;
|
|
let frame = 0;
|
|
|
|
function show() {
|
|
window.clearTimeout(hideTimer);
|
|
host.classList.add("is-overlay-scroll-visible");
|
|
}
|
|
|
|
function scheduleHide() {
|
|
if (dragging) return;
|
|
window.clearTimeout(hideTimer);
|
|
hideTimer = window.setTimeout(() => {
|
|
host.classList.remove("is-overlay-scroll-visible");
|
|
host.classList.remove("is-overlay-scroll-dragging");
|
|
}, 280);
|
|
}
|
|
|
|
function sync() {
|
|
frame = 0;
|
|
const viewH = el.clientHeight;
|
|
const viewW = el.clientWidth;
|
|
const totalH = el.scrollHeight;
|
|
const totalW = el.scrollWidth;
|
|
const maxY = totalH - viewH;
|
|
const maxX = totalW - viewW;
|
|
const both = maxY > 1 && maxX > 1;
|
|
host.classList.toggle("has-overlay-scroll-both", both);
|
|
|
|
if (maxY <= 1) {
|
|
railY.style.visibility = "hidden";
|
|
} else {
|
|
railY.style.visibility = "";
|
|
const railH = Math.max(0, railY.clientHeight);
|
|
const thumbH = Math.max(24, Math.round((viewH / totalH) * railH));
|
|
const track = Math.max(1, railH - thumbH);
|
|
const top = (el.scrollTop / Math.max(1, maxY)) * track;
|
|
thumbY.style.height = `${thumbH}px`;
|
|
thumbY.style.width = "";
|
|
thumbY.style.transform = `translateY(${top}px)`;
|
|
}
|
|
|
|
if (maxX <= 1) {
|
|
railX.style.visibility = "hidden";
|
|
} else {
|
|
railX.style.visibility = "";
|
|
const railW = Math.max(0, railX.clientWidth);
|
|
const thumbW = Math.max(24, Math.round((viewW / totalW) * railW));
|
|
const track = Math.max(1, railW - thumbW);
|
|
const left = (el.scrollLeft / Math.max(1, maxX)) * track;
|
|
thumbX.style.width = `${thumbW}px`;
|
|
thumbX.style.height = "";
|
|
thumbX.style.transform = `translateX(${left}px)`;
|
|
}
|
|
}
|
|
|
|
function requestSync() {
|
|
if (frame) return;
|
|
frame = window.requestAnimationFrame(sync);
|
|
}
|
|
|
|
function onScroll() {
|
|
show();
|
|
requestSync();
|
|
scheduleHide();
|
|
}
|
|
|
|
function onPointerEnter() {
|
|
show();
|
|
requestSync();
|
|
}
|
|
|
|
function onPointerLeave() {
|
|
scheduleHide();
|
|
}
|
|
|
|
function bindThumb(
|
|
axis: "x" | "y",
|
|
thumb: HTMLElement,
|
|
rail: HTMLElement,
|
|
) {
|
|
function onThumbDown(event: PointerEvent) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
dragging = axis;
|
|
dragStart = axis === "y" ? event.clientY : event.clientX;
|
|
dragStartScroll = axis === "y" ? el.scrollTop : el.scrollLeft;
|
|
host.classList.add("is-overlay-scroll-dragging");
|
|
thumb.setPointerCapture(event.pointerId);
|
|
show();
|
|
}
|
|
|
|
function onThumbMove(event: PointerEvent) {
|
|
if (dragging !== axis) return;
|
|
if (axis === "y") {
|
|
const view = el.clientHeight;
|
|
const total = el.scrollHeight;
|
|
const maxScroll = Math.max(1, total - view);
|
|
const railSize = Math.max(1, rail.clientHeight);
|
|
const thumbSize = thumb.offsetHeight;
|
|
const track = Math.max(1, railSize - thumbSize);
|
|
const delta = event.clientY - dragStart;
|
|
el.scrollTop = dragStartScroll + (delta / track) * maxScroll;
|
|
} else {
|
|
const view = el.clientWidth;
|
|
const total = el.scrollWidth;
|
|
const maxScroll = Math.max(1, total - view);
|
|
const railSize = Math.max(1, rail.clientWidth);
|
|
const thumbSize = thumb.offsetWidth;
|
|
const track = Math.max(1, railSize - thumbSize);
|
|
const delta = event.clientX - dragStart;
|
|
el.scrollLeft = dragStartScroll + (delta / track) * maxScroll;
|
|
}
|
|
}
|
|
|
|
function onThumbUp(event: PointerEvent) {
|
|
if (dragging !== axis) return;
|
|
dragging = null;
|
|
host.classList.remove("is-overlay-scroll-dragging");
|
|
try {
|
|
thumb.releasePointerCapture(event.pointerId);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
scheduleHide();
|
|
}
|
|
|
|
function onRailDown(event: PointerEvent) {
|
|
if (event.target !== rail) return;
|
|
const rect = rail.getBoundingClientRect();
|
|
if (axis === "y") {
|
|
const thumbSize = thumb.offsetHeight;
|
|
const view = el.clientHeight;
|
|
const total = el.scrollHeight;
|
|
const maxScroll = Math.max(1, total - view);
|
|
const railSize = Math.max(1, rail.clientHeight);
|
|
const track = Math.max(1, railSize - thumbSize);
|
|
const y = event.clientY - rect.top - thumbSize / 2;
|
|
el.scrollTop = (Math.min(Math.max(y, 0), track) / track) * maxScroll;
|
|
} else {
|
|
const thumbSize = thumb.offsetWidth;
|
|
const view = el.clientWidth;
|
|
const total = el.scrollWidth;
|
|
const maxScroll = Math.max(1, total - view);
|
|
const railSize = Math.max(1, rail.clientWidth);
|
|
const track = Math.max(1, railSize - thumbSize);
|
|
const x = event.clientX - rect.left - thumbSize / 2;
|
|
el.scrollLeft = (Math.min(Math.max(x, 0), track) / track) * maxScroll;
|
|
}
|
|
show();
|
|
}
|
|
|
|
thumb.addEventListener("pointerdown", onThumbDown);
|
|
thumb.addEventListener("pointermove", onThumbMove);
|
|
thumb.addEventListener("pointerup", onThumbUp);
|
|
thumb.addEventListener("pointercancel", onThumbUp);
|
|
rail.addEventListener("pointerdown", onRailDown);
|
|
}
|
|
|
|
bindThumb("y", thumbY, railY);
|
|
bindThumb("x", thumbX, railX);
|
|
|
|
const resizeObserver = new ResizeObserver(() => requestSync());
|
|
resizeObserver.observe(el);
|
|
resizeObserver.observe(host);
|
|
|
|
el.addEventListener("scroll", onScroll, { passive: true });
|
|
host.addEventListener("pointerenter", onPointerEnter);
|
|
host.addEventListener("pointerleave", onPointerLeave);
|
|
|
|
requestSync();
|
|
|
|
return {
|
|
refresh: requestSync,
|
|
destroy() {
|
|
window.clearTimeout(hideTimer);
|
|
if (frame) window.cancelAnimationFrame(frame);
|
|
resizeObserver.disconnect();
|
|
el.removeEventListener("scroll", onScroll);
|
|
host.removeEventListener("pointerenter", onPointerEnter);
|
|
host.removeEventListener("pointerleave", onPointerLeave);
|
|
parent.insertBefore(el, host);
|
|
host.remove();
|
|
delete (el as HTMLElement & { [BOUND]?: boolean })[BOUND];
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineNuxtPlugin(() => {
|
|
if (!import.meta.client) return;
|
|
|
|
const apis = new Map<HTMLElement, OverlayApi>();
|
|
|
|
function bindAll(root: ParentNode = document) {
|
|
root.querySelectorAll<HTMLElement>(SELECTOR).forEach((el) => {
|
|
if (apis.has(el)) {
|
|
apis.get(el)?.refresh();
|
|
return;
|
|
}
|
|
apis.set(el, attachOverlayScrollbar(el));
|
|
});
|
|
}
|
|
|
|
const mo = new MutationObserver((mutations) => {
|
|
let needsBind = false;
|
|
for (const mutation of mutations) {
|
|
mutation.addedNodes.forEach((node) => {
|
|
if (!(node instanceof HTMLElement)) return;
|
|
if (
|
|
node.classList?.contains("overlay-scroll-host")
|
|
|| node.classList?.contains("overlay-scroll-rail")
|
|
) return;
|
|
if (node.matches?.(SELECTOR) || node.querySelector?.(SELECTOR)) needsBind = true;
|
|
});
|
|
}
|
|
if (needsBind) bindAll();
|
|
});
|
|
|
|
const start = () => {
|
|
bindAll();
|
|
mo.observe(document.body, { childList: true, subtree: true });
|
|
};
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
} else {
|
|
start();
|
|
}
|
|
});
|