web-ui-pack
Version:
Web package with UI elements
243 lines (242 loc) • 8.96 kB
JavaScript
import onEvent from "./onEvent";
import onScroll from "./onScroll";
import onScrollStop from "./onScrollStop";
export default class WUPScrolled {
el;
options;
state;
pages = [];
constructor(el, options) {
this.el = el;
this.options = options;
this.init();
this.disposeLst.push(onScroll(el, (v) => this.goTo(v === 1), options));
this.disposeLst.push(onEvent(el, "keydown", (e) => this.gotKeyDown(e), { passive: false }));
options.scrollByClick && this.disposeLst.push(onEvent(el, "click", (e) => this.gotClick(e), { passive: false }));
}
init() {
this.pages.forEach((p) => p.items.forEach((a) => a.remove()));
this.state = {
index: this.options.pages?.current || 0,
items: [],
};
this.pages = [];
const p = this.options.pages;
if (p) {
const from = -1 * (p.before ?? 0);
const to = p.after ?? 0;
let curItems;
for (let i = from; i <= to; ++i) {
const index = this.incrementPage(p.current, i);
const nextState = i === 1 ? { index: this.state.index, items: curItems } : this.state;
const items = this.options.onRender(1, index, this.state, nextState);
this.el.append(...items);
this.pages.push({ index, items });
if (index === p.current) {
curItems = items;
}
}
this.state.items = curItems;
}
else {
this.state.items = Array.prototype.slice.call(this.el.children);
this.pages.push(this.state);
}
window.requestAnimationFrame(() => this.scrollToRange(false, this.state.items));
}
gotKeyDown(e) {
if (!e.altKey && !e.shiftKey && !e.ctrlKey && !e.defaultPrevented) {
let inc = 0;
switch (e.key) {
case "PageUp":
inc = -1;
break;
case "PageDown":
inc = 1;
break;
case "ArrowUp":
inc = this.options.isXScroll ? 0 : -1;
break;
case "ArrowDown":
inc = this.options.isXScroll ? 0 : 1;
break;
case "ArrowLeft":
inc = !this.options.isXScroll ? 0 : -1;
break;
case "ArrowRight":
inc = !this.options.isXScroll ? 0 : 1;
break;
default:
break;
}
if (inc) {
e.preventDefault();
this.goTo(inc === 1);
}
}
}
gotClick(e) {
if (!e.defaultPrevented && !e.button) {
const { target } = e;
const iNext = this.pages.findIndex((pg) => pg.items.some((a) => a === target || a.contains(target)));
if (iNext > -1) {
e.preventDefault();
const next = this.pages[iNext].index;
next !== this.state.index ? this.goTo(next) : this.options.onClickSkip?.call(this, e);
}
}
}
incrementPage(pIndex, inc, allowOverflow) {
pIndex += inc;
const p = this.options.pages;
if (p) {
if (p.cycled) {
if (!p.total) {
throw new Error("option [pages.cycled] doesn't work without [pages.total]");
}
pIndex = (p.total + pIndex) % p.total;
}
else if (!allowOverflow) {
pIndex = Math.min(Math.max(pIndex, 0), p.total ? p.total - 1 : Number.MAX_SAFE_INTEGER);
}
}
return pIndex;
}
tryFixSize() {
const { el } = this;
const { isXScroll } = this.options;
if ((!isXScroll && el.style.maxHeight) || (isXScroll && el.style.maxWidth)) {
return;
}
const { maxHeight, maxWidth } = getComputedStyle(el);
const { width, height } = el.getBoundingClientRect();
if (el.offsetHeight && !isXScroll && !maxHeight?.endsWith("px"))
el.style.maxHeight = `${height}px`;
if (el.offsetWidth && isXScroll && !maxWidth?.endsWith("px"))
el.style.maxWidth = `${width}px`;
}
getNearest(prev, next, total) {
const diff1 = next - prev;
const cnt1 = Math.abs(diff1);
const diff2 = total - cnt1;
const cnt2 = Math.abs(diff2);
if (cnt1 <= cnt2) {
return { isForward: diff1 > 0, count: cnt1 };
}
return { isForward: diff1 < diff2, count: cnt2 };
}
#scrollNum = 0;
goTo(pi, smooth = true) {
let isForward;
let count = 1;
if (pi === true) {
isForward = true;
}
else if (pi === false) {
isForward = false;
}
else {
const p = this.options.pages;
pi = p ? Math.max(0, pi) : pi;
pi = p?.total ? Math.min(pi, p.total) : pi;
if (p?.cycled) {
const r = this.getNearest(this.state.index, pi, p.total);
isForward = r.isForward;
count = r.count;
}
else {
const diff = pi - this.state.index;
isForward = diff > 0;
count = Math.abs(diff);
}
}
if (pi === this.state.index) {
return Promise.resolve();
}
this.tryFixSize();
const rstMaxSize = () => {
if (this.#scrollNum === 0) {
this.el.style.maxHeight = "";
this.el.style.maxWidth = "";
}
};
const restoreScroll = this.saveScroll();
const pagesRemove = [];
let wasAdded = false;
for (let i = 1; i <= count; ++i) {
const inc = isForward ? 1 : -1;
pi = this.incrementPage(this.state.index, inc);
if (this.state.index === pi) {
break;
}
const renderIndex = this.incrementPage(pi, isForward ? this.options.pages?.after || 0 : (this.options.pages?.before || 0) * -1, true);
const nextState = {
index: pi,
items: this.pages.find((p) => p.index === pi)?.items || [],
};
const itemsAdd = this.options.onRender(inc, renderIndex, this.state, nextState);
if (!itemsAdd?.length) {
rstMaxSize();
break;
}
wasAdded = true;
const pageRemove = isForward ? this.pages.shift() : this.pages.pop();
pageRemove.items.forEach((a) => (a.__scrollRemove = true));
pagesRemove.push(pageRemove);
const pageAdd = {
index: renderIndex,
items: itemsAdd,
};
pageAdd.items.forEach((a) => delete a.__scrollRemove);
if (isForward) {
this.el.append(...pageAdd.items);
this.pages.push(pageAdd);
}
else {
this.el.prepend(...pageAdd.items);
this.pages.unshift(pageAdd);
}
nextState.items = nextState.items.length ? nextState.items : pageAdd.items;
this.state = nextState;
}
if (!wasAdded) {
return Promise.resolve();
}
restoreScroll();
let r = this.scrollToRange(smooth, this.state.items);
++this.#scrollNum;
if (!smooth) {
r = Promise.resolve();
}
return r.then(() => {
pagesRemove.forEach((p) => p.items.forEach((a) => a.__scrollRemove && a.remove()));
--this.#scrollNum;
rstMaxSize();
});
}
saveScroll() {
const savedItems = [this.state.items[0], this.state.items[this.state.items.length - 1]];
return () => this.scrollToRange(false, savedItems);
}
scrollToRange(isSmooth, items) {
const y1 = items[0].offsetTop - this.el.offsetTop;
const x1 = items[0].offsetLeft - this.el.offsetLeft;
const $2 = items[items.length - 1];
const y2 = $2.offsetTop + $2.offsetHeight - this.el.offsetTop;
const x2 = $2.offsetLeft + $2.offsetWidth - this.el.offsetLeft;
const top = y1 + (y2 - y1 - this.el.offsetHeight) / 2;
const left = x1 + (x2 - x1 - this.el.offsetWidth) / 2;
this.el.scroll({ top, left, behavior: isSmooth ? "smooth" : "auto" });
if (isSmooth) {
return new Promise((resolve) => {
onScrollStop(this.el, () => resolve(), { onceNotStarted: true });
});
}
return undefined;
}
disposeLst = [];
dispose() {
this.disposeLst.forEach((f) => f());
this.disposeLst.length = 0;
}
}