UNPKG

web-ui-pack

Version:
243 lines (242 loc) 8.96 kB
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; } }