Files
djk/internal/webui/card-viewport.js
T

75 lines
3.7 KiB
JavaScript

import { element, reconcileChildren, withFocus } from './ui-core.js';
export function createCardViewport({container, scroller, create, update, key, rendered}) {
let items = [], emptyNode = null, frame = 0, revision = 0, drawnRevision = -1;
let columns = 1, rowHeight = 0, measuredWidth = 0, first = -1, last = -1;
let mounted = new Map();
const before = element('div', 'cards-spacer'), after = element('div', 'cards-spacer');
before.setAttribute('aria-hidden', 'true');
after.setAttribute('aria-hidden', 'true');
container.setAttribute('role', 'list');
function schedule() {
if (!frame) frame = requestAnimationFrame(() => {frame = 0; draw();});
}
function draw() {
const width = container.clientWidth;
if (!width || !scroller.clientHeight) return;
const style = getComputedStyle(container);
const tracks = style.gridTemplateColumns.split(/\s+/).filter(value => /^\d/.test(value));
const nextColumns = Math.max(1, tracks.length);
if (width !== measuredWidth || columns !== nextColumns) {
measuredWidth = width;
columns = nextColumns;
rowHeight = 0;
first = -1;
}
const gap = parseFloat(style.rowGap) || 0;
const step = (rowHeight || (parseFloat(tracks[0]) || width) * 4 / 3 + 60) + gap;
const top = Math.max(0, scroller.getBoundingClientRect().top - container.getBoundingClientRect().top);
const totalRows = Math.ceil(items.length / columns);
const startRow = Math.max(0, Math.min(Math.max(0, totalRows - 1), Math.floor(top / step) - 1));
const endRow = Math.min(totalRows, Math.ceil((top + scroller.clientHeight) / step) + 1);
const start = Math.min(items.length, startRow * columns), end = Math.min(items.length, endRow * columns);
if (first === start && last === end && drawnRevision === revision) return;
const nextMounted = new Map(), nodes = [], changed = [];
if (startRow > 0 && items.length) {before.style.height = Math.max(0, startRow * step - gap) + 'px'; nodes.push(before);}
for (let index = start; index < end; index++) {
const item = items[index], renderKey = key(item);
let card = mounted.get(item.id);
if (!card || card.renderKey !== renderKey) {card = create(item); changed.push(card);}
else if (drawnRevision !== revision) {update(card, item); changed.push(card);}
card.setAttribute('role', 'listitem');
card.setAttribute('aria-posinset', String(index + 1));
card.setAttribute('aria-setsize', String(items.length));
nextMounted.set(item.id, card);
nodes.push(card);
}
if (endRow < totalRows) {after.style.height = Math.max(0, (totalRows - endRow) * step - gap) + 'px'; nodes.push(after);}
if (!items.length && emptyNode) nodes.push(emptyNode);
const active = document.activeElement?.closest?.('.card');
if (active && container.contains(active) && !nextMounted.has(active.dataset.dramaId)) scroller.focus({preventScroll: true});
withFocus(container, () => reconcileChildren(container, nodes));
mounted = nextMounted;
first = start;
last = end;
drawnRevision = revision;
container.setAttribute('aria-busy', 'false');
if (!rowHeight && mounted.size) {
rowHeight = mounted.values().next().value.getBoundingClientRect().height;
first = -1;
schedule();
}
if (changed.length) rendered(changed);
}
scroller.addEventListener('scroll', schedule, {passive: true});
window.addEventListener('resize', schedule);
if (typeof ResizeObserver === 'function') new ResizeObserver(schedule).observe(container);
return {
setItems(next, placeholder = null) {items = next; emptyNode = placeholder; revision++; container.setAttribute('aria-busy', 'true'); schedule();},
refresh() {first = -1; schedule();}
};
}