INIT
This commit is contained in:
+264
@@ -0,0 +1,264 @@
|
||||
import { attachRef } from "svelte-toolbelt";
|
||||
import { Context } from "runed";
|
||||
import { createBitsAttrs } from "../../internal/attrs.js";
|
||||
import { getElemDirection } from "../../internal/locale.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
import { getDirectionalKeys } from "../../internal/get-directional-keys.js";
|
||||
import { useId } from "../../shared/index.js";
|
||||
const paginationAttrs = createBitsAttrs({
|
||||
component: "pagination",
|
||||
parts: ["root", "page", "prev", "next"],
|
||||
});
|
||||
const PaginationRootContext = new Context("Pagination.Root");
|
||||
export class PaginationRootState {
|
||||
static create(opts) {
|
||||
return PaginationRootContext.set(new PaginationRootState(opts));
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
totalPages = $derived.by(() => {
|
||||
if (this.opts.count.current === 0)
|
||||
return 1;
|
||||
return Math.ceil(this.opts.count.current / this.opts.perPage.current);
|
||||
});
|
||||
range = $derived.by(() => {
|
||||
const start = (this.opts.page.current - 1) * this.opts.perPage.current;
|
||||
const end = Math.min(start + this.opts.perPage.current, this.opts.count.current);
|
||||
return { start: start + 1, end };
|
||||
});
|
||||
pages = $derived.by(() => getPageItems({
|
||||
page: this.opts.page.current,
|
||||
totalPages: this.totalPages,
|
||||
siblingCount: this.opts.siblingCount.current,
|
||||
}));
|
||||
hasPrevPage = $derived.by(() => this.opts.page.current > 1);
|
||||
hasNextPage = $derived.by(() => this.opts.page.current < this.totalPages);
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
setPage(page) {
|
||||
this.opts.page.current = page;
|
||||
}
|
||||
getPageTriggerNodes() {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return [];
|
||||
return Array.from(node.querySelectorAll("[data-pagination-page]"));
|
||||
}
|
||||
getButtonNode(type) {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return;
|
||||
return node.querySelector(paginationAttrs.selector(type));
|
||||
}
|
||||
prevPage() {
|
||||
this.opts.page.current = Math.max(this.opts.page.current - 1, 1);
|
||||
}
|
||||
nextPage() {
|
||||
this.opts.page.current = Math.min(this.opts.page.current + 1, this.totalPages);
|
||||
}
|
||||
snippetProps = $derived.by(() => ({
|
||||
pages: this.pages,
|
||||
range: this.range,
|
||||
currentPage: this.opts.page.current,
|
||||
}));
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
[paginationAttrs.root]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class PaginationPageState {
|
||||
static create(opts) {
|
||||
return new PaginationPageState(opts, PaginationRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
#isSelected = $derived.by(() => this.opts.page.current.value === this.root.opts.page.current);
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
}
|
||||
onclick(e) {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
this.root.setPage(this.opts.page.current.value);
|
||||
}
|
||||
onkeydown(e) {
|
||||
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
||||
e.preventDefault();
|
||||
this.root.setPage(this.opts.page.current.value);
|
||||
}
|
||||
else {
|
||||
handleTriggerKeydown(e, this.opts.ref.current, this.root);
|
||||
}
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"aria-label": `Page ${this.opts.page.current.value}`,
|
||||
"data-value": `${this.opts.page.current.value}`,
|
||||
"data-selected": this.#isSelected ? "" : undefined,
|
||||
[paginationAttrs.page]: "",
|
||||
//
|
||||
onclick: this.onclick,
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class PaginationButtonState {
|
||||
static create(opts) {
|
||||
return new PaginationButtonState(opts, PaginationRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
}
|
||||
#action() {
|
||||
this.opts.type === "prev" ? this.root.prevPage() : this.root.nextPage();
|
||||
}
|
||||
#isDisabled = $derived.by(() => {
|
||||
if (this.opts.disabled.current)
|
||||
return true;
|
||||
if (this.opts.type === "prev")
|
||||
return !this.root.hasPrevPage;
|
||||
if (this.opts.type === "next")
|
||||
return !this.root.hasNextPage;
|
||||
return false;
|
||||
});
|
||||
onclick(e) {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
this.#action();
|
||||
}
|
||||
onkeydown(e) {
|
||||
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
||||
e.preventDefault();
|
||||
this.#action();
|
||||
}
|
||||
else {
|
||||
handleTriggerKeydown(e, this.opts.ref.current, this.root);
|
||||
}
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
[paginationAttrs[this.opts.type]]: "",
|
||||
disabled: this.#isDisabled,
|
||||
//
|
||||
onclick: this.onclick,
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
//
|
||||
// HELPERS
|
||||
//
|
||||
/**
|
||||
* Shared logic for handling keyboard navigation on
|
||||
* pagination page triggers and prev/next buttons.
|
||||
*
|
||||
*
|
||||
* @param e - KeyboardEvent
|
||||
* @param node - The HTMLElement that triggered the event.
|
||||
* @param root - The root pagination state instance
|
||||
*/
|
||||
function handleTriggerKeydown(e, node, root) {
|
||||
if (!node || !root.opts.ref.current)
|
||||
return;
|
||||
const items = root.getPageTriggerNodes();
|
||||
const nextButton = root.getButtonNode("next");
|
||||
const prevButton = root.getButtonNode("prev");
|
||||
if (prevButton) {
|
||||
items.unshift(prevButton);
|
||||
}
|
||||
if (nextButton) {
|
||||
items.push(nextButton);
|
||||
}
|
||||
const currentIndex = items.indexOf(node);
|
||||
const dir = getElemDirection(root.opts.ref.current);
|
||||
const { nextKey, prevKey } = getDirectionalKeys(dir, root.opts.orientation.current);
|
||||
const loop = root.opts.loop.current;
|
||||
const keyToIndex = {
|
||||
[nextKey]: currentIndex + 1,
|
||||
[prevKey]: currentIndex - 1,
|
||||
[kbd.HOME]: 0,
|
||||
[kbd.END]: items.length - 1,
|
||||
};
|
||||
let itemIndex = keyToIndex[e.key];
|
||||
if (itemIndex === undefined)
|
||||
return;
|
||||
e.preventDefault();
|
||||
if (itemIndex < 0 && loop) {
|
||||
itemIndex = items.length - 1;
|
||||
}
|
||||
else if (itemIndex === items.length && loop) {
|
||||
itemIndex = 0;
|
||||
}
|
||||
const itemToFocus = items[itemIndex];
|
||||
if (!itemToFocus)
|
||||
return;
|
||||
itemToFocus.focus();
|
||||
}
|
||||
/**
|
||||
* Returns an array of page items used to render out the
|
||||
* pagination page triggers.
|
||||
*
|
||||
* Credit: https://github.com/melt-ui/melt-ui
|
||||
*/
|
||||
function getPageItems({ page = 1, totalPages, siblingCount = 1 }) {
|
||||
const pageItems = [];
|
||||
const pagesToShow = new Set([1, totalPages]);
|
||||
const firstItemWithSiblings = 3 + siblingCount;
|
||||
const lastItemWithSiblings = totalPages - 2 - siblingCount;
|
||||
if (firstItemWithSiblings > lastItemWithSiblings) {
|
||||
for (let i = 2; i <= totalPages - 1; i++) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
else if (page < firstItemWithSiblings) {
|
||||
for (let i = 2; i <= Math.min(firstItemWithSiblings, totalPages); i++) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
else if (page > lastItemWithSiblings) {
|
||||
for (let i = totalPages - 1; i >= Math.max(lastItemWithSiblings, 2); i--) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = Math.max(page - siblingCount, 2); i <= Math.min(page + siblingCount, totalPages); i++) {
|
||||
pagesToShow.add(i);
|
||||
}
|
||||
}
|
||||
function addPage(value) {
|
||||
pageItems.push({ type: "page", value, key: `page-${value}` });
|
||||
}
|
||||
function addEllipsis() {
|
||||
const id = useId();
|
||||
pageItems.push({ type: "ellipsis", key: `ellipsis-${id}` });
|
||||
}
|
||||
let lastNumber = 0;
|
||||
for (const p of Array.from(pagesToShow).sort((a, b) => a - b)) {
|
||||
if (p - lastNumber > 1) {
|
||||
addEllipsis();
|
||||
}
|
||||
addPage(p);
|
||||
lastNumber = p;
|
||||
}
|
||||
return pageItems;
|
||||
}
|
||||
Reference in New Issue
Block a user