@alfakit/core 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +2 -1
- package/dist/actions.js.map +1 -1
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +3 -0
- package/dist/calendar.js.map +1 -1
- package/dist/form.d.ts +6 -0
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +27 -3
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +7 -7
- package/dist/page-actions.d.ts.map +1 -1
- package/dist/page-actions.js +30 -1
- package/dist/page-actions.js.map +1 -1
- package/dist/prefetch.d.ts +3 -1
- package/dist/prefetch.d.ts.map +1 -1
- package/dist/prefetch.js +37 -3
- package/dist/prefetch.js.map +1 -1
- package/dist/pwa.d.ts +12 -0
- package/dist/pwa.d.ts.map +1 -1
- package/dist/pwa.js +28 -1
- package/dist/pwa.js.map +1 -1
- package/dist/rich-editor.d.ts +11 -0
- package/dist/rich-editor.d.ts.map +1 -1
- package/dist/rich-editor.js +29 -1
- package/dist/rich-editor.js.map +1 -1
- package/dist/schema/panel.d.ts +9 -0
- package/dist/schema/panel.d.ts.map +1 -1
- package/dist/schema/panel.js.map +1 -1
- package/dist/shortcuts.d.ts +27 -0
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +53 -2
- package/dist/shortcuts.js.map +1 -1
- package/dist/starter-kit.d.ts +27 -2
- package/dist/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit.js +32 -2
- package/dist/starter-kit.js.map +1 -1
- package/dist/table-layout.d.ts +6 -2
- package/dist/table-layout.d.ts.map +1 -1
- package/dist/table-layout.js +5 -3
- package/dist/table-layout.js.map +1 -1
- package/dist/table.d.ts.map +1 -1
- package/dist/table.js +5 -2
- package/dist/table.js.map +1 -1
- package/dist/undo.d.ts +2 -0
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +21 -1
- package/dist/undo.js.map +1 -1
- package/package.json +1 -1
- package/themes.css +4 -0
package/dist/table-layout.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-layout.js","names":[],"sources":["../src/table-layout.ts"],"sourcesContent":["import { inertiaReload, requestJson } from './http'\nimport type { Alignment } from './schema/common'\nimport type {\n AnyPaginationSchema,\n BaseColumnSchema,\n Breakpoint,\n ColumnGroupSchema,\n ColumnLayoutNode,\n ColumnSchema,\n SplitLayoutSchema,\n StackLayoutSchema,\n TableSchema,\n} from './schema/table'\nimport type { TableQuery } from './table'\n\n/**\n * One cell of the column-group header row: a group spanning `span` adjacent visible\n * columns, or (without `group`) an ungrouped column whose header spans both rows.\n */\nexport interface ColumnGroupHeaderCell {\n key: string\n span: number\n group?: ColumnGroupSchema\n column?: ColumnSchema\n}\n\n/**\n * The extra header row for `ColumnGroup`s. Adjacent visible columns of the same group\n * share one header; a group split by a reordered column gets one header per run.\n * Returns null when the table has no groups.\n */\nexport function columnGroupHeaderRow(\n columns: ColumnSchema[],\n groups: ColumnGroupSchema[] | undefined,\n): ColumnGroupHeaderCell[] | null {\n if (!groups || groups.length === 0) {\n return null\n }\n\n const groupOf = new Map<string, ColumnGroupSchema>()\n\n for (const group of groups) {\n for (const name of group.columns) {\n groupOf.set(name, group)\n }\n }\n\n const cells: ColumnGroupHeaderCell[] = []\n\n for (const column of columns) {\n const group = groupOf.get(column.name)\n const previous = cells[cells.length - 1]\n\n if (group && previous?.group === group) {\n previous.span += 1\n } else if (group) {\n cells.push({ key: `group:${group.label}:${column.name}`, span: 1, group })\n } else {\n cells.push({ key: `column:${column.name}`, span: 1, column })\n }\n }\n\n return cells\n}\n\n/** Whether a column sits under a group header (so its own header is in the second row). */\nexport function isGroupedColumn(name: string, groups: ColumnGroupSchema[] | undefined): boolean {\n return Boolean(groups?.some((group) => group.columns.includes(name)))\n}\n\n/** Column names referenced anywhere in a content layout, in order. */\nexport function layoutColumnNames(nodes: ColumnLayoutNode[] | undefined): string[] {\n const names: string[] = []\n\n for (const node of nodes ?? []) {\n if (node.type === 'Column') {\n names.push(node.name)\n } else {\n names.push(...layoutColumnNames(node.schema))\n }\n }\n\n return names\n}\n\nconst SPLIT_FROM: Record<Breakpoint | 'default', string> = {\n default: 'flex-row items-center',\n sm: 'flex-col sm:flex-row sm:items-center',\n md: 'flex-col md:flex-row md:items-center',\n lg: 'flex-col lg:flex-row lg:items-center',\n xl: 'flex-col xl:flex-row xl:items-center',\n '2xl': 'flex-col 2xl:flex-row 2xl:items-center',\n}\n\n/** Classes of a `Split`: side by side from its breakpoint, stacked below it. */\nexport function splitClasses(layout: Pick<SplitLayoutSchema, 'from'>): string {\n return `flex gap-3 ${SPLIT_FROM[layout.from ?? 'default']}`\n}\n\nconst VISIBLE_FROM: Record<Breakpoint, string> = {\n sm: 'hidden sm:flex',\n md: 'hidden md:flex',\n lg: 'hidden lg:flex',\n xl: 'hidden xl:flex',\n '2xl': 'hidden 2xl:flex',\n}\n\nconst HIDDEN_FROM: Record<Breakpoint, string> = {\n sm: 'sm:hidden',\n md: 'md:hidden',\n lg: 'lg:hidden',\n xl: 'xl:hidden',\n '2xl': '2xl:hidden',\n}\n\n/** Responsive visibility classes for `visibleFrom()` / `hiddenFrom()`. */\nexport function layoutVisibilityClasses(layout: {\n visibleFrom?: Breakpoint\n hiddenFrom?: Breakpoint\n}): string {\n return [\n layout.visibleFrom ? VISIBLE_FROM[layout.visibleFrom] : '',\n layout.hiddenFrom ? HIDDEN_FROM[layout.hiddenFrom] : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nconst VISIBLE_FROM_CELL: Record<Breakpoint, string> = {\n sm: 'hidden sm:table-cell',\n md: 'hidden md:table-cell',\n lg: 'hidden lg:table-cell',\n xl: 'hidden xl:table-cell',\n '2xl': 'hidden 2xl:table-cell',\n}\n\nconst VISIBLE_FROM_BLOCK: Record<Breakpoint, string> = {\n sm: 'hidden sm:block',\n md: 'hidden md:block',\n lg: 'hidden lg:block',\n xl: 'hidden xl:block',\n '2xl': 'hidden 2xl:block',\n}\n\n/**\n * Responsive visibility classes for a column's `visibleFrom()` / `hiddenFrom()`: for its\n * header and cells in a table (`table-cell`), or its block in a content layout.\n */\nexport function columnVisibilityClasses(\n column: Pick<BaseColumnSchema, 'visibleFrom' | 'hiddenFrom'>,\n display: 'table-cell' | 'block' = 'table-cell',\n): string {\n const visible = display === 'block' ? VISIBLE_FROM_BLOCK : VISIBLE_FROM_CELL\n\n return [\n column.visibleFrom ? visible[column.visibleFrom] : '',\n column.hiddenFrom ? HIDDEN_FROM[column.hiddenFrom] : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nconst STACK_SPACE: Record<number, string> = { 1: 'gap-1', 2: 'gap-2', 3: 'gap-3' }\n\nconst STACK_ALIGN: Partial<Record<Alignment, string>> = {\n start: 'items-start text-start',\n center: 'items-center text-center',\n end: 'items-end text-end',\n}\n\n/** Classes of a `Stack`: a vertical flex column with its gap and alignment. */\nexport function stackClasses(layout: Pick<StackLayoutSchema, 'space' | 'alignment'>): string {\n return [\n 'flex flex-col',\n STACK_SPACE[layout.space ?? 0] ?? 'gap-0.5',\n layout.alignment ? STACK_ALIGN[layout.alignment] : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\n/** Inside a `Split`, whether a node takes the remaining width. */\nexport function layoutNodeGrows(node: ColumnLayoutNode, columns: ColumnSchema[]): boolean {\n if (node.type === 'Column') {\n return columns.find((column) => column.name === node.name)?.grow !== false\n }\n\n return node.grow !== false\n}\n\n/** Record actions placed at the start of the row (before or after the checkbox). */\nexport function recordActionsAtStart(position: TableSchema['recordActionsPosition']): boolean {\n return position === 'before-cells' || position === 'before-columns'\n}\n\n/** The total record count, when the pagination mode knows it. */\nexport function paginationTotal(pagination: AnyPaginationSchema | null | undefined): number | null {\n if (!pagination || ('mode' in pagination && pagination.mode && pagination.mode !== 'default')) {\n return null\n }\n\n return 'total' in pagination ? pagination.total : null\n}\n\n/** Load more and infinite scroll keep earlier pages and append the next one. */\nexport function isAppendingPagination(\n pagination: AnyPaginationSchema | null | undefined,\n): pagination is Extract<AnyPaginationSchema, { mode: 'load-more' | 'infinite' }> {\n return pagination?.mode === 'load-more' || pagination?.mode === 'infinite'\n}\n\n/**\n * The table query for the next page of a simple, cursor or appending paginator, or null\n * on the last page.\n */\nexport function nextPageQuery(\n pagination: AnyPaginationSchema | null | undefined,\n): TableQuery | null {\n if (!pagination) return null\n\n switch (pagination.mode) {\n case 'cursor':\n return pagination.hasMorePages && pagination.nextCursor\n ? { cursor: pagination.nextCursor }\n : null\n case 'simple':\n case 'load-more':\n case 'infinite':\n return pagination.hasMorePages ? { page: pagination.currentPage + 1 } : null\n default:\n return pagination.currentPage < pagination.lastPage\n ? { page: pagination.currentPage + 1 }\n : null\n }\n}\n\n/** The table query for the previous page, or null on the first page. */\nexport function previousPageQuery(\n pagination: AnyPaginationSchema | null | undefined,\n): TableQuery | null {\n if (!pagination || isAppendingPagination(pagination)) return null\n\n if (pagination.mode === 'cursor') {\n return pagination.previousCursor ? { cursor: pagination.previousCursor } : null\n }\n\n return pagination.currentPage > 1 ? { page: pagination.currentPage - 1 } : null\n}\n\n/** Move a column one step in the user's column order. */\nexport function moveColumn(names: readonly string[], name: string, offset: -1 | 1): string[] {\n const from = names.indexOf(name)\n const to = from + offset\n\n if (from < 0 || to < 0 || to >= names.length) {\n return [...names]\n }\n\n const next = [...names]\n const [moved] = next.splice(from, 1)\n\n if (moved !== undefined) next.splice(to, 0, moved)\n\n return next\n}\n\n/** Drop a dragged column onto another: it takes the target's position in the order. */\nexport function moveColumnTo(names: readonly string[], name: string, target: string): string[] {\n const from = names.indexOf(name)\n const to = names.indexOf(target)\n\n if (from < 0 || to < 0 || from === to) {\n return [...names]\n }\n\n const next = [...names]\n const [moved] = next.splice(from, 1)\n\n if (moved !== undefined) next.splice(to, 0, moved)\n\n return next\n}\n\n/**\n * Where a column sits in the column manager, for screen readers: inside a column group\n * it counts among the group's columns (`group` is its label); otherwise among the\n * top-level items, where a column group counts as one item.\n */\nexport interface ColumnManagerPosition {\n position: number\n count: number\n group?: string\n}\n\n/** The column group (of `columnGroups`) a column belongs to. */\nfunction columnGroupOf(\n name: string,\n groups: readonly ColumnGroupSchema[] | undefined,\n): ColumnGroupSchema | undefined {\n return groups?.find((group) => group.columns.includes(name))\n}\n\n/**\n * The top-level items of a column order: an ungrouped column, or the contiguous columns\n * of one column group.\n */\nfunction columnManagerItems(\n order: readonly string[],\n groups: readonly ColumnGroupSchema[] | undefined,\n): string[][] {\n const items: string[][] = []\n let current: ColumnGroupSchema | undefined\n\n for (const name of order) {\n const group = columnGroupOf(name, groups)\n const last = items[items.length - 1]\n\n if (group && group === current && last) {\n last.push(name)\n } else {\n items.push([name])\n }\n\n current = group\n }\n\n return items\n}\n\nexport function columnManagerPosition(\n order: readonly string[],\n name: string,\n groups?: readonly ColumnGroupSchema[],\n): ColumnManagerPosition {\n const group = columnGroupOf(name, groups)\n\n if (group) {\n const siblings = order.filter((column) => group.columns.includes(column))\n\n return { position: siblings.indexOf(name) + 1, count: siblings.length, group: group.label }\n }\n\n const items = columnManagerItems(order, groups)\n\n return {\n position: items.findIndex((item) => item.includes(name)) + 1,\n count: items.length,\n }\n}\n\n/**\n * Move a column one step in the column manager: a grouped column swaps with its\n * neighbour in the same group; an ungrouped column moves past the neighbouring item\n * (a whole column group), so groups stay together. Returns the order unchanged at an edge.\n */\nexport function moveColumnInManager(\n order: readonly string[],\n name: string,\n offset: -1 | 1,\n groups?: readonly ColumnGroupSchema[],\n): string[] {\n const group = columnGroupOf(name, groups)\n\n if (group) {\n const siblings = order.filter((column) => group.columns.includes(column))\n const neighbour = siblings[siblings.indexOf(name) + offset]\n\n if (neighbour === undefined) return [...order]\n\n const next = [...order]\n next[order.indexOf(name)] = neighbour\n next[order.indexOf(neighbour)] = name\n\n return next\n }\n\n const items = columnManagerItems(order, groups)\n const index = items.findIndex((item) => item.includes(name))\n const target = index + offset\n\n if (index < 0 || target < 0 || target >= items.length) return [...order]\n\n const next = [...items]\n const [moved] = next.splice(index, 1)\n\n if (moved !== undefined) next.splice(target, 0, moved)\n\n return next.flat()\n}\n\n/** The columns in a (draft) column order; columns missing from it keep their place at the end. */\nexport function orderColumnsBy<T extends { name: string }>(\n columns: readonly T[],\n order: readonly string[] | null | undefined,\n): T[] {\n if (!order) return [...columns]\n\n const rank = (name: string) => {\n const index = order.indexOf(name)\n\n return index < 0 ? order.length : index\n }\n\n return [...columns].sort((a, b) => rank(a.name) - rank(b.name))\n}\n\n/** Whether a draft column order differs from the applied one. */\nexport function columnOrderChanged(applied: readonly string[], draft: readonly string[]): boolean {\n return applied.length !== draft.length || applied.some((name, index) => draft[index] !== name)\n}\n\n/**\n * Keep keyboard focus on a column manager move button after a move: once the list has\n * re-rendered, refocus it, or the other move button when it became disabled at an edge.\n */\nexport function keepMoveButtonFocus(button: HTMLElement | null | undefined): void {\n if (!button || typeof document === 'undefined' || document.activeElement !== button) return\n\n const controls = button.parentElement\n const schedule =\n typeof requestAnimationFrame === 'function'\n ? requestAnimationFrame\n : (callback: () => void) => setTimeout(callback, 0)\n\n schedule(() => {\n const active = document.activeElement\n\n if (active !== button && active !== document.body && active !== null) return\n\n if (button.isConnected && !(button as HTMLButtonElement).disabled) {\n button.focus()\n\n return\n }\n\n controls?.querySelector<HTMLElement>('button:not(:disabled)')?.focus()\n })\n}\n\n/**\n * Store the user's column order on the server (per user, in the session), then reload the\n * table so it renders in the new order.\n */\nexport async function saveColumnOrder(\n endpoint: string,\n token: string,\n component: string,\n columns: string[],\n): Promise<string[]> {\n const response = await requestJson<{ columns: string[] }>(endpoint, {\n method: 'POST',\n body: { token, component, columns },\n })\n\n inertiaReload({ only: ['page'] })\n\n return response.columns\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"table-layout.js","names":[],"sources":["../src/table-layout.ts"],"sourcesContent":["import { inertiaReload, requestJson } from './http'\nimport type { Alignment } from './schema/common'\nimport type {\n AnyPaginationSchema,\n BaseColumnSchema,\n Breakpoint,\n ColumnGroupSchema,\n ColumnLayoutNode,\n ColumnSchema,\n SplitLayoutSchema,\n StackLayoutSchema,\n TableSchema,\n} from './schema/table'\nimport type { TableQuery } from './table'\n\n/**\n * One cell of the column-group header row: a group spanning `span` adjacent visible\n * columns, or (without `group`) an ungrouped column whose header spans both rows.\n */\nexport interface ColumnGroupHeaderCell {\n key: string\n span: number\n group?: ColumnGroupSchema\n column?: ColumnSchema\n}\n\n/**\n * The extra header row for `ColumnGroup`s. Adjacent visible columns of the same group\n * share one header; a group split by a reordered column gets one header per run.\n * Returns null when the table has no groups.\n */\nexport function columnGroupHeaderRow(\n columns: ColumnSchema[],\n groups: ColumnGroupSchema[] | undefined,\n): ColumnGroupHeaderCell[] | null {\n if (!groups || groups.length === 0) {\n return null\n }\n\n const groupOf = new Map<string, ColumnGroupSchema>()\n\n for (const group of groups) {\n for (const name of group.columns) {\n groupOf.set(name, group)\n }\n }\n\n const cells: ColumnGroupHeaderCell[] = []\n\n for (const column of columns) {\n const group = groupOf.get(column.name)\n const previous = cells[cells.length - 1]\n\n if (group && previous?.group === group) {\n previous.span += 1\n } else if (group) {\n cells.push({ key: `group:${group.label}:${column.name}`, span: 1, group })\n } else {\n cells.push({ key: `column:${column.name}`, span: 1, column })\n }\n }\n\n return cells\n}\n\n/** Whether a column sits under a group header (so its own header is in the second row). */\nexport function isGroupedColumn(name: string, groups: ColumnGroupSchema[] | undefined): boolean {\n return Boolean(groups?.some((group) => group.columns.includes(name)))\n}\n\n/** Column names referenced anywhere in a content layout, in order. */\nexport function layoutColumnNames(nodes: ColumnLayoutNode[] | undefined): string[] {\n const names: string[] = []\n\n for (const node of nodes ?? []) {\n if (node.type === 'Column') {\n names.push(node.name)\n } else {\n names.push(...layoutColumnNames(node.schema))\n }\n }\n\n return names\n}\n\nconst SPLIT_FROM: Record<Breakpoint | 'default', string> = {\n default: 'flex-row items-center',\n sm: 'flex-col sm:flex-row sm:items-center',\n md: 'flex-col md:flex-row md:items-center',\n lg: 'flex-col lg:flex-row lg:items-center',\n xl: 'flex-col xl:flex-row xl:items-center',\n '2xl': 'flex-col 2xl:flex-row 2xl:items-center',\n}\n\n/** Classes of a `Split`: side by side from its breakpoint, stacked below it. */\nexport function splitClasses(layout: Pick<SplitLayoutSchema, 'from'>): string {\n return `flex gap-3 ${SPLIT_FROM[layout.from ?? 'default']}`\n}\n\nconst VISIBLE_FROM: Record<Breakpoint, string> = {\n sm: 'hidden sm:flex',\n md: 'hidden md:flex',\n lg: 'hidden lg:flex',\n xl: 'hidden xl:flex',\n '2xl': 'hidden 2xl:flex',\n}\n\nconst HIDDEN_FROM: Record<Breakpoint, string> = {\n sm: 'sm:hidden',\n md: 'md:hidden',\n lg: 'lg:hidden',\n xl: 'xl:hidden',\n '2xl': '2xl:hidden',\n}\n\n/** Responsive visibility classes for `visibleFrom()` / `hiddenFrom()`. */\nexport function layoutVisibilityClasses(layout: {\n visibleFrom?: Breakpoint\n hiddenFrom?: Breakpoint\n}): string {\n return [\n layout.visibleFrom ? VISIBLE_FROM[layout.visibleFrom] : '',\n layout.hiddenFrom ? HIDDEN_FROM[layout.hiddenFrom] : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nconst VISIBLE_FROM_CELL: Record<Breakpoint, string> = {\n sm: 'hidden sm:table-cell',\n md: 'hidden md:table-cell',\n lg: 'hidden lg:table-cell',\n xl: 'hidden xl:table-cell',\n '2xl': 'hidden 2xl:table-cell',\n}\n\nconst VISIBLE_FROM_BLOCK: Record<Breakpoint, string> = {\n sm: 'hidden sm:block',\n md: 'hidden md:block',\n lg: 'hidden lg:block',\n xl: 'hidden xl:block',\n '2xl': 'hidden 2xl:block',\n}\n\n/**\n * Responsive visibility classes for a column's `visibleFrom()` / `hiddenFrom()`: for its\n * header and cells in a table (`table-cell`), or its block in a content layout.\n */\nexport function columnVisibilityClasses(\n column: Pick<BaseColumnSchema, 'visibleFrom' | 'hiddenFrom'>,\n display: 'table-cell' | 'block' = 'table-cell',\n): string {\n const visible = display === 'block' ? VISIBLE_FROM_BLOCK : VISIBLE_FROM_CELL\n\n return [\n column.visibleFrom ? visible[column.visibleFrom] : '',\n column.hiddenFrom ? HIDDEN_FROM[column.hiddenFrom] : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nconst STACK_SPACE: Record<number, string> = { 1: 'gap-1', 2: 'gap-2', 3: 'gap-3' }\n\nconst STACK_ALIGN: Partial<Record<Alignment, string>> = {\n start: 'items-start text-start',\n center: 'items-center text-center',\n end: 'items-end text-end',\n}\n\n/** Classes of a `Stack`: a vertical flex column with its gap and alignment. */\nexport function stackClasses(layout: Pick<StackLayoutSchema, 'space' | 'alignment'>): string {\n return [\n 'flex flex-col',\n STACK_SPACE[layout.space ?? 0] ?? 'gap-0.5',\n layout.alignment ? STACK_ALIGN[layout.alignment] : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\n/** Inside a `Split`, whether a node takes the remaining width. */\nexport function layoutNodeGrows(node: ColumnLayoutNode, columns: ColumnSchema[]): boolean {\n if (node.type === 'Column') {\n return columns.find((column) => column.name === node.name)?.grow !== false\n }\n\n return node.grow !== false\n}\n\n/** Record actions placed at the start of the row (before or after the checkbox). */\nexport function recordActionsAtStart(position: TableSchema['recordActionsPosition']): boolean {\n return position === 'before-cells' || position === 'before-columns'\n}\n\n/** The total record count, when the pagination mode knows it. */\nexport function paginationTotal(pagination: AnyPaginationSchema | null | undefined): number | null {\n if (!pagination || ('mode' in pagination && pagination.mode && pagination.mode !== 'default')) {\n return null\n }\n\n return 'total' in pagination ? pagination.total : null\n}\n\n/** Load more and infinite scroll keep earlier pages and append the next one. */\nexport function isAppendingPagination(\n pagination: AnyPaginationSchema | null | undefined,\n): pagination is Extract<AnyPaginationSchema, { mode: 'load-more' | 'infinite' }> {\n return pagination?.mode === 'load-more' || pagination?.mode === 'infinite'\n}\n\n/**\n * The table query for the next page of a simple, cursor or appending paginator, or null\n * on the last page.\n */\nexport function nextPageQuery(\n pagination: AnyPaginationSchema | null | undefined,\n): TableQuery | null {\n if (!pagination) return null\n\n switch (pagination.mode) {\n case 'cursor':\n return pagination.hasMorePages && pagination.nextCursor\n ? { cursor: pagination.nextCursor }\n : null\n case 'simple':\n case 'load-more':\n case 'infinite':\n return pagination.hasMorePages ? { page: pagination.currentPage + 1 } : null\n default:\n return pagination.currentPage < pagination.lastPage\n ? { page: pagination.currentPage + 1 }\n : null\n }\n}\n\n/** The table query for the previous page, or null on the first page. */\nexport function previousPageQuery(\n pagination: AnyPaginationSchema | null | undefined,\n): TableQuery | null {\n if (!pagination || isAppendingPagination(pagination)) return null\n\n if (pagination.mode === 'cursor') {\n return pagination.previousCursor ? { cursor: pagination.previousCursor } : null\n }\n\n return pagination.currentPage > 1 ? { page: pagination.currentPage - 1 } : null\n}\n\n/** Move a column one step in the user's column order. */\nexport function moveColumn(names: readonly string[], name: string, offset: -1 | 1): string[] {\n const from = names.indexOf(name)\n const to = from + offset\n\n if (from < 0 || to < 0 || to >= names.length) {\n return [...names]\n }\n\n const next = [...names]\n const [moved] = next.splice(from, 1)\n\n if (moved !== undefined) next.splice(to, 0, moved)\n\n return next\n}\n\n/** Drop a dragged column onto another: it takes the target's position in the order. */\nexport function moveColumnTo(names: readonly string[], name: string, target: string): string[] {\n const from = names.indexOf(name)\n const to = names.indexOf(target)\n\n if (from < 0 || to < 0 || from === to) {\n return [...names]\n }\n\n const next = [...names]\n const [moved] = next.splice(from, 1)\n\n if (moved !== undefined) next.splice(to, 0, moved)\n\n return next\n}\n\n/**\n * Where a column sits in the column manager, for screen readers: inside a column group\n * it counts among the group's columns (`group` is its label); otherwise among the\n * top-level items, where a column group counts as one item.\n */\nexport interface ColumnManagerPosition {\n position: number\n count: number\n group?: string\n}\n\n/** The column group (of `columnGroups`) a column belongs to. */\nfunction columnGroupOf(\n name: string,\n groups: readonly ColumnGroupSchema[] | undefined,\n): ColumnGroupSchema | undefined {\n return groups?.find((group) => group.columns.includes(name))\n}\n\n/**\n * The top-level items of a column order: an ungrouped column, or the contiguous columns\n * of one column group.\n */\nfunction columnManagerItems(\n order: readonly string[],\n groups: readonly ColumnGroupSchema[] | undefined,\n): string[][] {\n const items: string[][] = []\n let current: ColumnGroupSchema | undefined\n\n for (const name of order) {\n const group = columnGroupOf(name, groups)\n const last = items[items.length - 1]\n\n if (group && group === current && last) {\n last.push(name)\n } else {\n items.push([name])\n }\n\n current = group\n }\n\n return items\n}\n\nexport function columnManagerPosition(\n order: readonly string[],\n name: string,\n groups?: readonly ColumnGroupSchema[],\n): ColumnManagerPosition {\n const group = columnGroupOf(name, groups)\n\n if (group) {\n const siblings = order.filter((column) => group.columns.includes(column))\n\n return { position: siblings.indexOf(name) + 1, count: siblings.length, group: group.label }\n }\n\n const items = columnManagerItems(order, groups)\n\n return {\n position: items.findIndex((item) => item.includes(name)) + 1,\n count: items.length,\n }\n}\n\n/**\n * Move a column one step in the column manager: a grouped column swaps with its\n * neighbour in the same group; an ungrouped column moves past the neighbouring item\n * (a whole column group), so groups stay together. Returns the order unchanged at an edge.\n */\nexport function moveColumnInManager(\n order: readonly string[],\n name: string,\n offset: -1 | 1,\n groups?: readonly ColumnGroupSchema[],\n): string[] {\n const group = columnGroupOf(name, groups)\n\n if (group) {\n const siblings = order.filter((column) => group.columns.includes(column))\n const neighbour = siblings[siblings.indexOf(name) + offset]\n\n if (neighbour === undefined) return [...order]\n\n const next = [...order]\n next[order.indexOf(name)] = neighbour\n next[order.indexOf(neighbour)] = name\n\n return next\n }\n\n const items = columnManagerItems(order, groups)\n const index = items.findIndex((item) => item.includes(name))\n const target = index + offset\n\n if (index < 0 || target < 0 || target >= items.length) return [...order]\n\n const next = [...items]\n const [moved] = next.splice(index, 1)\n\n if (moved !== undefined) next.splice(target, 0, moved)\n\n return next.flat()\n}\n\n/** The columns in a (draft) column order; columns missing from it keep their place at the end. */\nexport function orderColumnsBy<T extends { name: string }>(\n columns: readonly T[],\n order: readonly string[] | null | undefined,\n): T[] {\n if (!order) return [...columns]\n\n const rank = (name: string) => {\n const index = order.indexOf(name)\n\n return index < 0 ? order.length : index\n }\n\n return [...columns].sort((a, b) => rank(a.name) - rank(b.name))\n}\n\n/** Whether a draft column order differs from the applied one. */\nexport function columnOrderChanged(applied: readonly string[], draft: readonly string[]): boolean {\n return applied.length !== draft.length || applied.some((name, index) => draft[index] !== name)\n}\n\n/**\n * Keep keyboard focus on a column manager move button after a move: once the list has\n * re-rendered, refocus it, or the other move button when it became disabled at an edge.\n */\nexport function keepMoveButtonFocus(button: HTMLElement | null | undefined): void {\n if (!button || typeof document === 'undefined' || document.activeElement !== button) return\n\n const controls = button.parentElement\n const schedule =\n typeof requestAnimationFrame === 'function'\n ? requestAnimationFrame\n : (callback: () => void) => setTimeout(callback, 0)\n\n schedule(() => {\n const active = document.activeElement\n\n if (active !== button && active !== document.body && active !== null) return\n\n if (button.isConnected && !(button as HTMLButtonElement).disabled) {\n button.focus()\n\n return\n }\n\n controls?.querySelector<HTMLElement>('button:not(:disabled)')?.focus()\n })\n}\n\n/**\n * Store the user's column order on the server (per user, in the session), then reload the\n * table so it renders in the new order. `reload: false` when a table visit follows (Reset,\n * Apply with visibility changes): that visit renders the new order, and a reload of the\n * current URL running alongside it could finish last and bring the old columns back.\n */\nexport async function saveColumnOrder(\n endpoint: string,\n token: string,\n component: string,\n columns: string[],\n options: { reload?: boolean } = {},\n): Promise<string[]> {\n const response = await requestJson<{ columns: string[] }>(endpoint, {\n method: 'POST',\n body: { token, component, columns },\n })\n\n if (options.reload ?? true) inertiaReload({ only: ['page'] })\n\n return response.columns\n}\n"],"mappings":"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"}
|
package/dist/table.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.d.ts","names":[],"sources":["../src/table.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"table.d.ts","names":[],"sources":["../src/table.ts"],"mappings":";;;;;;iBAeiB;EACf;EACA;EACA;EACA,UAAU,eAAe;EACzB,eAAe;EACf;;EAEA;EACA;EACA;;EAEA;EACA;;EAEA;;;wBAWc,yBACd,gCACA;;wBAmDc,cAAc,gCAAgC;;;;;wBAQ9C,cACd,oBACA,gCACA,SAAS,YACT;;YAsGU,sBAAsB,KAChC;;wBAKc,WACd,gCACA,SAAS,YACT,iBACA,YAAW;iBAwBI;;EAEf,UAAU;;EAEV;;EAEA;;iBAGe;;EAEf;;EAEA;;;;;qBAMW;WACK,OAAO,MAAM;UAErB;UAEA;;;EAOR,UAAU,OAAO,KAAK;;MAQlB;;EAOJ,UAAU,KAAK;;;;;;EASf,UAAU;MAWN,SAAS;EAIb,WAAW,KAAK;;;;;EAQhB,OAAO,KAAK,WAAW,iBAAiB;;EAkCxC,WAAW,SAAS;;;;;EAqBpB,YAAY,eAAe,SAAS;EAiBpC,cAAc,eAAe,SAAS;;EAatC,WAAW,eAAe,SAAS;EAWnC,aAAa,SAAS;;EAatB;EAMA;MAII;MAIA;;wBAKU,oBACd,WAAW,gBACX,SAAS;iBAOM;EACf;EACA;;;wBAIoB,gBACpB,WAAW,gBACX,eACA,mBACA,QAAQ,WACR,gBACA,iBACC;wBAUmB,aACpB,WAAW,gBACX,eACA,mBACA,OAAO,aACP;EAAW;IACV;;;;;;wBAca,0BACd,OAAO,KAAK;;wBAME,qBACd,OAAO,KAAK;;wBAQE,oBAAoB,OAAO,KAAK;wBAMhC,eAAe,OAAO,cAAc;;;;;wBAQpC,oBACd,OAAO,KAAK;;qBAMD;qBACA;;wBAGG,6BAA6B,SAAS;;;;;wBAYtC,yBACd,WAAW,gBACX,SAAS,gCACR;;;;;wBAYmB,yBACpB,aACA,eACA,mBACA,QAAQ,KAAK,0CACb,iBACC,QAAQ;;;;;wBAcK,0BACd,SAAS,UACT,QAAQ,KAAK,sDACb,gBACA,YAAY,YACX;;;;;;wBAgBa,yBACd,QAAQ,KACN,4GAGF;EAAS;EAAkB;EAAgB;;EACxC;EAAkB;EAAa;;;wBAiBpB,WAAW;;qBAQd,6BAA6B;;wBAQ1B,uBAAuB;;qBAK1B,4BAA4B;;qBA8B5B,6BAA6B;;qBA8B7B,+BAA+B;YA6BhC;;;;;wBAMI,iBAAiB,QAAQ,+BAA+B;;wBAUxD,mBAAmB,QAAQ;;wBAK3B,sBACd,OAAO,KAAK;;;;;qBAmBD;;;;;;;;;;;;;;wBAeG,mBACd,QAAQ,KAAK"}
|
package/dist/table.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createStore } from "./store.js";
|
|
2
|
+
import { currentPageQuery } from "./page-query.js";
|
|
2
3
|
import { beginTableLoading, endTableLoading } from "./table-toolbar.js";
|
|
3
4
|
import { inertiaReload, inertiaVisit, requestJson } from "./http.js";
|
|
4
5
|
//#region src/table.ts
|
|
@@ -283,7 +284,8 @@ async function updateTableCell(endpoints, token, component, record, column, stat
|
|
|
283
284
|
component,
|
|
284
285
|
record,
|
|
285
286
|
column,
|
|
286
|
-
state
|
|
287
|
+
state,
|
|
288
|
+
tableQuery: currentPageQuery()
|
|
287
289
|
}
|
|
288
290
|
})).state;
|
|
289
291
|
}
|
|
@@ -293,7 +295,8 @@ async function reorderTable(endpoints, token, component, order, options = {}) {
|
|
|
293
295
|
body: {
|
|
294
296
|
token,
|
|
295
297
|
component,
|
|
296
|
-
order
|
|
298
|
+
order,
|
|
299
|
+
tableQuery: currentPageQuery()
|
|
297
300
|
}
|
|
298
301
|
});
|
|
299
302
|
if (options.reload ?? true) inertiaReload();
|
package/dist/table.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.js","names":[],"sources":["../src/table.ts"],"sourcesContent":["import type { VisitOptions } from '@inertiajs/core'\nimport { inertiaReload, inertiaVisit, requestJson } from './http'\nimport type { AnyActionSchema } from './schema/actions'\nimport type { Option, RecordKey } from './schema/common'\nimport type {\n ColumnSchema,\n FiltersFormWidth,\n SelectColumnSchema,\n TableRecordSchema,\n TableSchema,\n} from './schema/table'\nimport { createStore, type Store } from './store'\nimport { beginTableLoading, endTableLoading } from './table-toolbar'\n\nexport interface TableQuery {\n sort?: string | null\n direction?: 'asc' | 'desc' | null\n search?: string | null\n filters?: Record<string, Record<string, unknown>> | null\n columnSearch?: Record<string, string> | null\n page?: number | null\n /** Opaque cursor of a cursor-paginated table. */\n cursor?: string | null\n perPage?: number | 'all' | null\n columns?: string[] | null\n /** Group id; `''` turns a default group off, `null` returns to the default. */\n group?: string | null\n groupDirection?: 'asc' | 'desc' | null\n /** Reorder mode of a `reorderable()` table; the server lists every record while it is on. */\n reordering?: boolean | null\n}\n\n/**\n * Query-string keys of each table (by identifier, `''` for none) that the server keeps\n * in the session. Cleared values of these keys are sent as empty keys (`?search=`) so the\n * server stores them instead of restoring the previous value.\n */\nconst persistedTableState = new Map<string, readonly string[]>()\n\n/** Register a table's persisted keys (from `TableSchema.persistedState`); returns a cleanup. */\nexport function registerTablePersistence(\n identifier: string | undefined,\n keys: readonly string[] | undefined,\n): () => void {\n const id = identifier ?? ''\n\n if (!keys || keys.length === 0) {\n persistedTableState.delete(id)\n\n return () => {}\n }\n\n persistedTableState.set(id, keys)\n\n return () => {\n if (persistedTableState.get(id) === keys) persistedTableState.delete(id)\n }\n}\n\n/**\n * Append a nested value as PHP-style bracket parameters (`key[a][0][b]=1`). Empty values\n * (`null`, `''`, `false`) are skipped; `true` becomes `1`. Returns whether anything was set.\n */\nfunction appendNested(params: URLSearchParams, key: string, value: unknown): boolean {\n if (value === null || value === undefined || value === '' || value === false) {\n return false\n }\n\n if (Array.isArray(value)) {\n let appended = false\n value.forEach((item, index) => {\n appended = appendNested(params, `${key}[${index}]`, item) || appended\n })\n\n return appended\n }\n\n if (typeof value === 'object') {\n let appended = false\n\n for (const [name, item] of Object.entries(value as Record<string, unknown>)) {\n appended = appendNested(params, `${key}[${name}]`, item) || appended\n }\n\n return appended\n }\n\n params.set(key, typeof value === 'boolean' ? '1' : String(value as string | number))\n\n return true\n}\n\n/** Query-string key for a table parameter, honouring the identifier prefix. */\nexport function tableQueryKey(identifier: string | undefined, name: string): string {\n return identifier ? `${identifier}_${name}` : name\n}\n\n/**\n * Build the URL for a table state change by merging into the current location's query.\n * Only the keys present in `changes` are touched; `null` removes a key.\n */\nexport function buildTableUrl(\n currentUrl: string,\n identifier: string | undefined,\n changes: TableQuery,\n persisted: readonly string[] = persistedTableState.get(identifier ?? '') ?? [],\n): string {\n const url = new URL(\n currentUrl,\n typeof window !== 'undefined' ? window.location.origin : 'http://localhost',\n )\n const params = url.searchParams\n\n const removePrefix = (prefix: string) => {\n for (const key of [...params.keys()]) {\n if (key === prefix || key.startsWith(`${prefix}[`)) {\n params.delete(key)\n }\n }\n }\n\n const setScalar = (name: string, value: string | number | null | undefined) => {\n const key = tableQueryKey(identifier, name)\n\n if (value === null || value === undefined || value === '') {\n // A persisted key is cleared explicitly, or the server would restore it.\n if (persisted.includes(name)) {\n params.set(key, '')\n } else {\n params.delete(key)\n }\n } else {\n params.set(key, String(value))\n }\n }\n\n if ('sort' in changes) setScalar('sort', changes.sort)\n if ('direction' in changes) setScalar('direction', changes.direction)\n if ('search' in changes) setScalar('search', changes.search)\n if ('perPage' in changes) setScalar('perPage', changes.perPage)\n if ('page' in changes) setScalar('page', changes.page && changes.page > 1 ? changes.page : null)\n if ('cursor' in changes) setScalar('cursor', changes.cursor)\n if ('groupDirection' in changes) setScalar('groupDirection', changes.groupDirection)\n if ('reordering' in changes) setScalar('reordering', changes.reordering ? 1 : null)\n\n if ('group' in changes) {\n const key = tableQueryKey(identifier, 'group')\n\n if (changes.group === null || changes.group === undefined) {\n params.delete(key)\n } else {\n params.set(key, changes.group)\n }\n }\n\n if ('filters' in changes) {\n const key = tableQueryKey(identifier, 'filters')\n removePrefix(key)\n\n // Filter state may nest (the query builder's rule tree).\n const appended = appendNested(params, key, changes.filters ?? {})\n\n if (!appended && persisted.includes('filters')) {\n params.set(key, '')\n }\n }\n\n if ('columnSearch' in changes) {\n const key = tableQueryKey(identifier, 'columnSearch')\n removePrefix(key)\n\n let appended = false\n\n for (const [column, value] of Object.entries(changes.columnSearch ?? {})) {\n if (value) {\n params.set(`${key}[${column}]`, value)\n appended = true\n }\n }\n\n if (!appended && persisted.includes('columnSearch')) {\n params.set(key, '')\n }\n }\n\n if ('columns' in changes) {\n const key = tableQueryKey(identifier, 'columns')\n removePrefix(key)\n\n changes.columns?.forEach((column, index) => params.set(`${key}[${index}]`, column))\n\n // Every toggleable column hidden: an empty key, or the server restores the stored set.\n if (!changes.columns?.length && persisted.includes('columns')) {\n params.set(key, '')\n }\n }\n\n // Any change other than paging resets to the first page.\n if (!('page' in changes) && !('cursor' in changes) && Object.keys(changes).length > 0) {\n params.delete(tableQueryKey(identifier, 'page'))\n params.delete(tableQueryKey(identifier, 'cursor'))\n }\n\n return url.pathname + (params.toString() ? `?${params.toString()}` : '') + url.hash\n}\n\n/** Visit lifecycle callbacks a table state change may observe (load more, retry). */\nexport type TableVisitCallbacks = Pick<\n VisitOptions,\n 'onSuccess' | 'onError' | 'onFinish' | 'onCancel'\n>\n\n/** Navigate (Inertia visit, preserving state/scroll) to a new table state. */\nexport function visitTable(\n identifier: string | undefined,\n changes: TableQuery,\n only?: string[],\n callbacks: TableVisitCallbacks = {},\n): void {\n const url = buildTableUrl(\n typeof window !== 'undefined' ? window.location.href : '/',\n identifier,\n changes,\n )\n\n // `loadingSkeleton()`: the table knows a reload of its records is in flight.\n beginTableLoading(identifier)\n\n inertiaVisit(url, {\n preserveState: true,\n preserveScroll: true,\n replace: true,\n only,\n ...callbacks,\n onFinish: (visit) => {\n endTableLoading(identifier)\n callbacks.onFinish?.(visit)\n },\n })\n}\n\nexport interface SelectionState {\n /** Selected record keys, always stored as strings. */\n selected: Set<string>\n /** True when \"select all N records\" (beyond the current page) is active. */\n allSelected: boolean\n /** Group of the selected records under `selectGroupsOnly()`. */\n group?: string\n}\n\nexport interface SelectionLimits {\n /** `maxSelectableRecords()`: further records can't be selected. */\n max?: number | null\n /** `selectGroupsOnly()`: selecting a record of another group starts a new selection. */\n groupsOnly?: boolean\n}\n\n/**\n * Record selection for bulk actions. Keys are compared as strings.\n */\nexport class TableSelection {\n public readonly store: Store<SelectionState>\n\n private scope: string | undefined\n\n private limits: SelectionLimits = {}\n\n constructor() {\n this.store = createStore<SelectionState>({ selected: new Set(), allSelected: false })\n }\n\n /** Apply the table's selection limits (call with every new table schema). */\n configure(table: Pick<TableSchema, 'maxSelectableRecords' | 'selectGroupsOnly'>): void {\n this.limits = {\n max: table.maxSelectableRecords ?? null,\n groupsOnly: Boolean(table.selectGroupsOnly),\n }\n }\n\n /** Whether `maxSelectableRecords()` is reached. */\n get isFull(): boolean {\n const max = this.limits.max\n\n return typeof max === 'number' && this.state.selected.size >= max\n }\n\n /** Whether the record's checkbox can be ticked now (always true for selected rows). */\n canSelect(key: RecordKey): boolean {\n return this.isSelected(key) || !this.isFull\n }\n\n /**\n * Follow the table's `selectionScope`: when the filters, search or tab change the\n * selection is cleared, so a bulk action never hits records that are no longer listed.\n * Call it with every new table schema; an absent scope keeps the selection.\n */\n syncScope(scope: string | null | undefined): void {\n const next = scope ?? undefined\n const changed = next !== undefined && this.scope !== undefined && next !== this.scope\n\n this.scope = next\n\n if (changed && (this.state.selected.size > 0 || this.state.allSelected)) {\n this.clear()\n }\n }\n\n get state(): SelectionState {\n return this.store.get()\n }\n\n isSelected(key: RecordKey): boolean {\n return this.state.allSelected || this.state.selected.has(String(key))\n }\n\n /**\n * Select or deselect a record. `group` is the record's group (`record.group`): under\n * `selectGroupsOnly()` a record of another group replaces the selection.\n */\n toggle(key: RecordKey, force?: boolean, group?: string): void {\n this.store.set((state) => {\n const id = String(key)\n const shouldSelect = force ?? !state.selected.has(id)\n const otherGroup =\n shouldSelect && this.limits.groupsOnly && state.selected.size > 0 && state.group !== group\n const selected = otherGroup ? new Set<string>() : new Set(state.selected)\n\n if (shouldSelect) {\n const max = this.limits.max\n\n if (typeof max === 'number' && !selected.has(id) && selected.size >= max) {\n return state\n }\n\n selected.add(id)\n } else {\n selected.delete(id)\n }\n\n return {\n selected,\n allSelected: false,\n group:\n this.limits.groupsOnly && selected.size > 0\n ? shouldSelect\n ? group\n : state.group\n : undefined,\n }\n })\n }\n\n /** Select the page's selectable records, up to `maxSelectableRecords()`. */\n selectPage(records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = new Set(state.selected)\n const max = this.limits.max\n\n for (const record of records) {\n if (typeof max === 'number' && selected.size >= max) break\n\n if (record.selectable !== false) {\n selected.add(String(record.key))\n }\n }\n\n return { selected, allSelected: false, group: state.group }\n })\n }\n\n /**\n * Select every selectable record of one group on the page (`selectGroupsOnly()`), in place\n * of the current selection when it holds another group.\n */\n selectGroup(group: string, records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = state.group === group ? new Set(state.selected) : new Set<string>()\n const max = this.limits.max\n\n for (const record of records) {\n if (typeof max === 'number' && selected.size >= max) break\n\n if (record.selectable !== false && (record.group ?? '') === group) {\n selected.add(String(record.key))\n }\n }\n\n return { selected, allSelected: false, group: selected.size > 0 ? group : undefined }\n })\n }\n\n deselectGroup(group: string, records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = new Set(state.selected)\n\n for (const record of records) {\n if ((record.group ?? '') === group) selected.delete(String(record.key))\n }\n\n return { selected, allSelected: false, group: selected.size > 0 ? state.group : undefined }\n })\n }\n\n /** Checkbox state of a group heading: all, some or none of its selectable records. */\n groupState(group: string, records: TableRecordSchema[]): boolean | 'indeterminate' {\n const members = records.filter(\n (record) => record.selectable !== false && (record.group ?? '') === group,\n )\n const count = members.filter((record) => this.isSelected(record.key)).length\n\n if (count === 0 || members.length === 0) return false\n\n return count === members.length ? true : 'indeterminate'\n }\n\n deselectPage(records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = new Set(state.selected)\n\n for (const record of records) {\n selected.delete(String(record.key))\n }\n\n return { selected, allSelected: false, group: selected.size > 0 ? state.group : undefined }\n })\n }\n\n /** \"Select all N records\"; ignored under `maxSelectableRecords()` / `selectGroupsOnly()`. */\n selectAll(): void {\n if (typeof this.limits.max === 'number' || this.limits.groupsOnly) return\n\n this.store.set({ selected: new Set(), allSelected: true })\n }\n\n clear(): void {\n this.store.set({ selected: new Set(), allSelected: false })\n }\n\n get count(): number {\n return this.state.selected.size\n }\n\n get keys(): string[] {\n return [...this.state.selected]\n }\n}\n\nexport function isPageFullySelected(\n selection: TableSelection,\n records: TableRecordSchema[],\n): boolean {\n const selectable = records.filter((record) => record.selectable !== false)\n\n return selectable.length > 0 && selectable.every((record) => selection.isSelected(record.key))\n}\n\nexport interface TableEndpoints {\n cell: string\n reorder: string\n}\n\n/** Persist an inline cell edit, then reload page props. */\nexport async function updateTableCell(\n endpoints: TableEndpoints,\n token: string,\n component: string,\n record: RecordKey,\n column: string,\n state: unknown,\n): Promise<unknown> {\n const response = await requestJson<{ state: unknown }>(endpoints.cell, {\n method: 'POST',\n body: { token, component, record, column, state },\n })\n\n return response.state\n}\n\nexport async function reorderTable(\n endpoints: TableEndpoints,\n token: string,\n component: string,\n order: RecordKey[],\n options: { reload?: boolean } = {},\n): Promise<void> {\n await requestJson(endpoints.reorder, { method: 'POST', body: { token, component, order } })\n\n if (options.reload ?? true) inertiaReload()\n}\n\n/**\n * Whether entering reorder mode reloads the table with `?reordering=1`: a paginated table\n * lists every record while reordering (so records can move between pages), unless it has\n * `paginatedWhileReordering()`.\n */\nexport function reorderingListsAllRecords(\n table: Pick<TableSchema, 'pagination' | 'paginatedWhileReordering'>,\n): boolean {\n return Boolean(table.pagination) && !table.paginatedWhileReordering\n}\n\n/** Enter reorder mode: reloads the table without pages when `reorderingListsAllRecords()`. */\nexport function startTableReordering(\n table: Pick<TableSchema, 'identifier' | 'pagination' | 'paginatedWhileReordering'>,\n): void {\n if (reorderingListsAllRecords(table)) {\n visitTable(table.identifier, { reordering: true, page: null, cursor: null }, ['page'])\n }\n}\n\n/** Leave reorder mode: a table the server lists in reorder mode reloads with its pages. */\nexport function stopTableReordering(table: Pick<TableSchema, 'identifier' | 'reordering'>): void {\n if (table.reordering) {\n visitTable(table.identifier, { reordering: null, page: null }, ['page'])\n }\n}\n\nexport function visibleColumns(table: TableSchema): TableSchema['columns'] {\n return table.columns.filter((column) => table.visibleColumns.includes(column.name))\n}\n\n/**\n * Whether the bulk bar offers \"select all N records\": not under `selectCurrentPageOnly()`,\n * `maxSelectableRecords()` or `selectGroupsOnly()`.\n */\nexport function canSelectAllRecords(\n table: Pick<TableSchema, 'selectCurrentPageOnly' | 'maxSelectableRecords' | 'selectGroupsOnly'>,\n): boolean {\n return !table.selectCurrentPageOnly && !table.maxSelectableRecords && !table.selectGroupsOnly\n}\n\n/** Reserved action arguments carrying the selection (`accessSelectedRecords()`). */\nexport const SELECTED_RECORDS_ARGUMENT = 'alfakitSelectedRecords'\nexport const ALL_SELECTED_RECORDS_ARGUMENT = 'alfakitAllSelectedRecords'\n\n/** Whether one of the actions (or an action in a group) `accessSelectedRecords()`. */\nexport function actionsAccessSelectedRecords(actions: AnyActionSchema[] | undefined): boolean {\n return (actions ?? []).some((action) =>\n 'actions' in action\n ? actionsAccessSelectedRecords(action.actions as AnyActionSchema[])\n : Boolean((action as { accessSelectedRecords?: boolean }).accessSelectedRecords),\n )\n}\n\n/**\n * Arguments that hand the table's selection to record actions that\n * `accessSelectedRecords()`; undefined when none of `actions` does.\n */\nexport function selectedRecordsArguments(\n selection: SelectionState,\n actions: AnyActionSchema[] | undefined,\n): Record<string, unknown> | undefined {\n if (!actionsAccessSelectedRecords(actions)) return undefined\n\n return selection.allSelected\n ? { [ALL_SELECTED_RECORDS_ARGUMENT]: true }\n : { [SELECTED_RECORDS_ARGUMENT]: [...selection.selected] }\n}\n\n/**\n * Search the options of a `SelectColumn` whose options live on the server\n * (`optionsSearchPath`), through the schema options endpoint and the table's filters schema.\n */\nexport async function searchTableColumnOptions(\n url: string,\n token: string,\n component: string,\n column: Pick<SelectColumnSchema, 'optionsSearchPath'>,\n search: string,\n): Promise<Option[]> {\n if (!column.optionsSearchPath) return []\n\n const response = await requestJson<{ options?: Option[] }>(url, {\n query: { token, component: `${component}.filters`, path: column.optionsSearchPath, search },\n })\n\n return response.options ?? []\n}\n\n/**\n * Options of a combobox `SelectColumn` matching `search` (case-insensitive, on the label's\n * text), without the record itself under `ignoreRecord`, capped at `optionsLimit`.\n */\nexport function filterSelectColumnOptions(\n options: Option[],\n column: Pick<SelectColumnSchema, 'optionsLimit' | 'ignoreRecord'>,\n search: string,\n recordKey?: RecordKey,\n): Option[] {\n const needle = search.trim().toLocaleLowerCase()\n const matches = options.filter(\n (option) =>\n !(column.ignoreRecord && recordKey !== undefined && option.value === String(recordKey)) &&\n (needle === '' || optionText(option.label).toLocaleLowerCase().includes(needle)),\n )\n\n return column.optionsLimit ? matches.slice(0, column.optionsLimit) : matches\n}\n\n/**\n * What an empty combobox `SelectColumn` list says: the column's own message\n * (`loadingMessage()`, `searchingMessage()`, `noOptionsMessage()`, `noSearchResultsMessage()`)\n * or the translation key and English fallback to use instead.\n */\nexport function selectColumnEmptyMessage(\n column: Pick<\n SelectColumnSchema,\n 'loadingMessage' | 'searchingMessage' | 'noOptionsMessage' | 'noSearchResultsMessage'\n >,\n state: { loading: boolean; search: string; optionCount: number },\n): { message?: string; key: string; fallback: string } {\n if (state.loading) {\n return state.search !== ''\n ? { message: column.searchingMessage, key: 'form.searching', fallback: 'Searching…' }\n : { message: column.loadingMessage, key: 'loading', fallback: 'Loading…' }\n }\n\n return state.search === '' && state.optionCount === 0\n ? {\n message: column.noOptionsMessage,\n key: 'form.no_options',\n fallback: 'No options available.',\n }\n : { message: column.noSearchResultsMessage, key: 'no_results', fallback: 'No results.' }\n}\n\n/** Plain text of an option label that may be (sanitised) HTML. */\nexport function optionText(label: string): string {\n return label\n .replace(/<[^>]*>/g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** Text sizes of an image column / entry's \"+N\" (`limitedRemainingText(size:)`, default `sm`). */\nexport const REMAINING_TEXT_SIZE_CLASSES: Record<string, string> = {\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-base',\n lg: 'text-lg',\n}\n\n/** The class of the \"+N\" after limited images. */\nexport function remainingTextSizeClass(size: string | undefined): string {\n return REMAINING_TEXT_SIZE_CLASSES[size ?? 'sm'] ?? 'text-sm'\n}\n\n/** Width classes of the filters dropdown / sidebar (`filtersFormWidth()`). */\nexport const FILTERS_FORM_WIDTH_CLASSES: Record<string, string> = {\n '3xs': 'sm:w-3xs',\n '2xs': 'sm:w-2xs',\n xs: 'sm:w-xs',\n sm: 'sm:w-sm',\n md: 'sm:w-md',\n lg: 'sm:w-lg',\n xl: 'sm:w-xl',\n '2xl': 'sm:w-2xl',\n '3xl': 'sm:w-3xl',\n '4xl': 'sm:w-4xl',\n '5xl': 'sm:w-5xl',\n '6xl': 'sm:w-6xl',\n '7xl': 'sm:w-7xl',\n none: 'sm:w-auto',\n min: 'sm:w-min',\n max: 'sm:w-max',\n fit: 'sm:w-fit',\n prose: 'sm:w-[65ch]',\n container: 'sm:container',\n 'screen-sm': 'sm:w-(--breakpoint-sm)',\n 'screen-md': 'sm:w-(--breakpoint-md)',\n 'screen-lg': 'sm:w-(--breakpoint-lg)',\n 'screen-xl': 'sm:w-(--breakpoint-xl)',\n 'screen-2xl': 'sm:w-(--breakpoint-2xl)',\n full: 'sm:w-full',\n screen: 'sm:w-screen',\n}\n\n/** Width classes of the filters dialog (`FiltersLayout::Modal`). */\nexport const FILTERS_MODAL_WIDTH_CLASSES: Record<string, string> = {\n '3xs': 'sm:max-w-3xs',\n '2xs': 'sm:max-w-2xs',\n xs: 'sm:max-w-xs',\n sm: 'sm:max-w-sm',\n md: 'sm:max-w-md',\n lg: 'sm:max-w-lg',\n xl: 'sm:max-w-xl',\n '2xl': 'sm:max-w-2xl',\n '3xl': 'sm:max-w-3xl',\n '4xl': 'sm:max-w-4xl',\n '5xl': 'sm:max-w-5xl',\n '6xl': 'sm:max-w-6xl',\n '7xl': 'sm:max-w-7xl',\n none: 'sm:max-w-none',\n min: 'sm:max-w-min',\n max: 'sm:max-w-max',\n fit: 'sm:max-w-fit',\n prose: 'sm:max-w-prose',\n container: 'sm:container',\n 'screen-sm': 'sm:max-w-(--breakpoint-sm)',\n 'screen-md': 'sm:max-w-(--breakpoint-md)',\n 'screen-lg': 'sm:max-w-(--breakpoint-lg)',\n 'screen-xl': 'sm:max-w-(--breakpoint-xl)',\n 'screen-2xl': 'sm:max-w-(--breakpoint-2xl)',\n full: 'sm:max-w-full',\n screen: 'sm:max-w-screen',\n}\n\n/** Width classes of a sidebar filters layout (`BeforeContent` / `AfterContent`). */\nexport const FILTERS_SIDEBAR_WIDTH_CLASSES: Record<string, string> = {\n '3xs': 'lg:w-3xs',\n '2xs': 'lg:w-2xs',\n xs: 'lg:w-xs',\n sm: 'lg:w-sm',\n md: 'lg:w-md',\n lg: 'lg:w-lg',\n xl: 'lg:w-xl',\n '2xl': 'lg:w-2xl',\n '3xl': 'lg:w-3xl',\n '4xl': 'lg:w-4xl',\n '5xl': 'lg:w-5xl',\n '6xl': 'lg:w-6xl',\n '7xl': 'lg:w-7xl',\n none: 'lg:w-auto',\n min: 'lg:w-min',\n max: 'lg:w-max',\n fit: 'lg:w-fit',\n prose: 'lg:w-[65ch]',\n container: 'lg:container',\n 'screen-sm': 'lg:w-(--breakpoint-sm)',\n 'screen-md': 'lg:w-(--breakpoint-md)',\n 'screen-lg': 'lg:w-(--breakpoint-lg)',\n 'screen-xl': 'lg:w-(--breakpoint-xl)',\n 'screen-2xl': 'lg:w-(--breakpoint-2xl)',\n full: 'lg:w-full',\n screen: 'lg:w-screen',\n}\n\nexport type FiltersPlacement = 'toolbar' | 'above' | 'below' | 'before' | 'after'\n\n/**\n * Where a filters layout renders: in the toolbar (dropdown, modal), above or below the\n * table, or in a sidebar before / after it.\n */\nexport function filtersPlacement(layout: TableSchema['filtersLayout']): FiltersPlacement {\n if (layout.startsWith('above-content')) return 'above'\n if (layout === 'below-content') return 'below'\n if (layout.startsWith('before-content')) return 'before'\n if (layout.startsWith('after-content')) return 'after'\n\n return 'toolbar'\n}\n\n/** Whether the filters panel of this layout can be collapsed. */\nexport function filtersCollapsible(layout: TableSchema['filtersLayout']): boolean {\n return layout.endsWith('-collapsible')\n}\n\n/** The width class of a filters form for its layout, or undefined for the default. */\nexport function filtersFormWidthClass(\n table: Pick<TableSchema, 'filtersLayout' | 'filtersFormWidth'>,\n): string | undefined {\n const width = table.filtersFormWidth as FiltersFormWidth | undefined\n\n if (!width) return undefined\n\n const placement = filtersPlacement(table.filtersLayout)\n\n if (placement === 'before' || placement === 'after') return FILTERS_SIDEBAR_WIDTH_CLASSES[width]\n if (table.filtersLayout === 'modal') return FILTERS_MODAL_WIDTH_CLASSES[width]\n if (placement === 'toolbar') return FILTERS_FORM_WIDTH_CLASSES[width]\n\n return undefined\n}\n\n/**\n * Classes of a `stackedOnMobile()` table: below `md` the table, its body and rows are\n * blocks and each row is a card; every cell shows its column label (`stackedCellLabel`).\n */\nexport const STACKED_TABLE_CLASSES = {\n table: 'max-md:block',\n header: 'max-md:hidden',\n body: 'max-md:block',\n row: 'max-md:flex max-md:flex-col max-md:gap-2 max-md:border-b max-md:px-4 max-md:py-3',\n cell: 'max-md:flex max-md:min-w-0 max-md:items-start max-md:justify-between max-md:gap-3 max-md:p-0 max-md:text-start max-md:whitespace-normal',\n /** Cells with their own responsive visibility keep it (no flex override). */\n responsiveCell: 'max-md:p-0 max-md:text-start max-md:whitespace-normal',\n controlCell: 'max-md:flex max-md:items-center max-md:justify-start max-md:p-0',\n actionsCell: 'max-md:flex max-md:items-center max-md:justify-end max-md:p-0',\n label: 'text-muted-foreground shrink-0 text-xs font-medium md:hidden',\n footer: 'max-md:hidden',\n} as const\n\n/** Classes of a data cell in a stacked-on-mobile table. */\nexport function stackedCellClasses(\n column: Pick<ColumnSchema, 'visibleFrom' | 'hiddenFrom'>,\n): string {\n return column.visibleFrom || column.hiddenFrom\n ? STACKED_TABLE_CLASSES.responsiveCell\n : STACKED_TABLE_CLASSES.cell\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"table.js","names":[],"sources":["../src/table.ts"],"sourcesContent":["import type { VisitOptions } from '@inertiajs/core'\nimport { inertiaReload, inertiaVisit, requestJson } from './http'\nimport { currentPageQuery } from './page-query'\nimport type { AnyActionSchema } from './schema/actions'\nimport type { Option, RecordKey } from './schema/common'\nimport type {\n ColumnSchema,\n FiltersFormWidth,\n SelectColumnSchema,\n TableRecordSchema,\n TableSchema,\n} from './schema/table'\nimport { createStore, type Store } from './store'\nimport { beginTableLoading, endTableLoading } from './table-toolbar'\n\nexport interface TableQuery {\n sort?: string | null\n direction?: 'asc' | 'desc' | null\n search?: string | null\n filters?: Record<string, Record<string, unknown>> | null\n columnSearch?: Record<string, string> | null\n page?: number | null\n /** Opaque cursor of a cursor-paginated table. */\n cursor?: string | null\n perPage?: number | 'all' | null\n columns?: string[] | null\n /** Group id; `''` turns a default group off, `null` returns to the default. */\n group?: string | null\n groupDirection?: 'asc' | 'desc' | null\n /** Reorder mode of a `reorderable()` table; the server lists every record while it is on. */\n reordering?: boolean | null\n}\n\n/**\n * Query-string keys of each table (by identifier, `''` for none) that the server keeps\n * in the session. Cleared values of these keys are sent as empty keys (`?search=`) so the\n * server stores them instead of restoring the previous value.\n */\nconst persistedTableState = new Map<string, readonly string[]>()\n\n/** Register a table's persisted keys (from `TableSchema.persistedState`); returns a cleanup. */\nexport function registerTablePersistence(\n identifier: string | undefined,\n keys: readonly string[] | undefined,\n): () => void {\n const id = identifier ?? ''\n\n if (!keys || keys.length === 0) {\n persistedTableState.delete(id)\n\n return () => {}\n }\n\n persistedTableState.set(id, keys)\n\n return () => {\n if (persistedTableState.get(id) === keys) persistedTableState.delete(id)\n }\n}\n\n/**\n * Append a nested value as PHP-style bracket parameters (`key[a][0][b]=1`). Empty values\n * (`null`, `''`, `false`) are skipped; `true` becomes `1`. Returns whether anything was set.\n */\nfunction appendNested(params: URLSearchParams, key: string, value: unknown): boolean {\n if (value === null || value === undefined || value === '' || value === false) {\n return false\n }\n\n if (Array.isArray(value)) {\n let appended = false\n value.forEach((item, index) => {\n appended = appendNested(params, `${key}[${index}]`, item) || appended\n })\n\n return appended\n }\n\n if (typeof value === 'object') {\n let appended = false\n\n for (const [name, item] of Object.entries(value as Record<string, unknown>)) {\n appended = appendNested(params, `${key}[${name}]`, item) || appended\n }\n\n return appended\n }\n\n params.set(key, typeof value === 'boolean' ? '1' : String(value as string | number))\n\n return true\n}\n\n/** Query-string key for a table parameter, honouring the identifier prefix. */\nexport function tableQueryKey(identifier: string | undefined, name: string): string {\n return identifier ? `${identifier}_${name}` : name\n}\n\n/**\n * Build the URL for a table state change by merging into the current location's query.\n * Only the keys present in `changes` are touched; `null` removes a key.\n */\nexport function buildTableUrl(\n currentUrl: string,\n identifier: string | undefined,\n changes: TableQuery,\n persisted: readonly string[] = persistedTableState.get(identifier ?? '') ?? [],\n): string {\n const url = new URL(\n currentUrl,\n typeof window !== 'undefined' ? window.location.origin : 'http://localhost',\n )\n const params = url.searchParams\n\n const removePrefix = (prefix: string) => {\n for (const key of [...params.keys()]) {\n if (key === prefix || key.startsWith(`${prefix}[`)) {\n params.delete(key)\n }\n }\n }\n\n const setScalar = (name: string, value: string | number | null | undefined) => {\n const key = tableQueryKey(identifier, name)\n\n if (value === null || value === undefined || value === '') {\n // A persisted key is cleared explicitly, or the server would restore it.\n if (persisted.includes(name)) {\n params.set(key, '')\n } else {\n params.delete(key)\n }\n } else {\n params.set(key, String(value))\n }\n }\n\n if ('sort' in changes) setScalar('sort', changes.sort)\n if ('direction' in changes) setScalar('direction', changes.direction)\n if ('search' in changes) setScalar('search', changes.search)\n if ('perPage' in changes) setScalar('perPage', changes.perPage)\n if ('page' in changes) setScalar('page', changes.page && changes.page > 1 ? changes.page : null)\n if ('cursor' in changes) setScalar('cursor', changes.cursor)\n if ('groupDirection' in changes) setScalar('groupDirection', changes.groupDirection)\n if ('reordering' in changes) setScalar('reordering', changes.reordering ? 1 : null)\n\n if ('group' in changes) {\n const key = tableQueryKey(identifier, 'group')\n\n if (changes.group === null || changes.group === undefined) {\n params.delete(key)\n } else {\n params.set(key, changes.group)\n }\n }\n\n if ('filters' in changes) {\n const key = tableQueryKey(identifier, 'filters')\n removePrefix(key)\n\n // Filter state may nest (the query builder's rule tree).\n const appended = appendNested(params, key, changes.filters ?? {})\n\n if (!appended && persisted.includes('filters')) {\n params.set(key, '')\n }\n }\n\n if ('columnSearch' in changes) {\n const key = tableQueryKey(identifier, 'columnSearch')\n removePrefix(key)\n\n let appended = false\n\n for (const [column, value] of Object.entries(changes.columnSearch ?? {})) {\n if (value) {\n params.set(`${key}[${column}]`, value)\n appended = true\n }\n }\n\n if (!appended && persisted.includes('columnSearch')) {\n params.set(key, '')\n }\n }\n\n if ('columns' in changes) {\n const key = tableQueryKey(identifier, 'columns')\n removePrefix(key)\n\n changes.columns?.forEach((column, index) => params.set(`${key}[${index}]`, column))\n\n // Every toggleable column hidden: an empty key, or the server restores the stored set.\n if (!changes.columns?.length && persisted.includes('columns')) {\n params.set(key, '')\n }\n }\n\n // Any change other than paging resets to the first page.\n if (!('page' in changes) && !('cursor' in changes) && Object.keys(changes).length > 0) {\n params.delete(tableQueryKey(identifier, 'page'))\n params.delete(tableQueryKey(identifier, 'cursor'))\n }\n\n return url.pathname + (params.toString() ? `?${params.toString()}` : '') + url.hash\n}\n\n/** Visit lifecycle callbacks a table state change may observe (load more, retry). */\nexport type TableVisitCallbacks = Pick<\n VisitOptions,\n 'onSuccess' | 'onError' | 'onFinish' | 'onCancel'\n>\n\n/** Navigate (Inertia visit, preserving state/scroll) to a new table state. */\nexport function visitTable(\n identifier: string | undefined,\n changes: TableQuery,\n only?: string[],\n callbacks: TableVisitCallbacks = {},\n): void {\n const url = buildTableUrl(\n typeof window !== 'undefined' ? window.location.href : '/',\n identifier,\n changes,\n )\n\n // `loadingSkeleton()`: the table knows a reload of its records is in flight.\n beginTableLoading(identifier)\n\n inertiaVisit(url, {\n preserveState: true,\n preserveScroll: true,\n replace: true,\n only,\n ...callbacks,\n onFinish: (visit) => {\n endTableLoading(identifier)\n callbacks.onFinish?.(visit)\n },\n })\n}\n\nexport interface SelectionState {\n /** Selected record keys, always stored as strings. */\n selected: Set<string>\n /** True when \"select all N records\" (beyond the current page) is active. */\n allSelected: boolean\n /** Group of the selected records under `selectGroupsOnly()`. */\n group?: string\n}\n\nexport interface SelectionLimits {\n /** `maxSelectableRecords()`: further records can't be selected. */\n max?: number | null\n /** `selectGroupsOnly()`: selecting a record of another group starts a new selection. */\n groupsOnly?: boolean\n}\n\n/**\n * Record selection for bulk actions. Keys are compared as strings.\n */\nexport class TableSelection {\n public readonly store: Store<SelectionState>\n\n private scope: string | undefined\n\n private limits: SelectionLimits = {}\n\n constructor() {\n this.store = createStore<SelectionState>({ selected: new Set(), allSelected: false })\n }\n\n /** Apply the table's selection limits (call with every new table schema). */\n configure(table: Pick<TableSchema, 'maxSelectableRecords' | 'selectGroupsOnly'>): void {\n this.limits = {\n max: table.maxSelectableRecords ?? null,\n groupsOnly: Boolean(table.selectGroupsOnly),\n }\n }\n\n /** Whether `maxSelectableRecords()` is reached. */\n get isFull(): boolean {\n const max = this.limits.max\n\n return typeof max === 'number' && this.state.selected.size >= max\n }\n\n /** Whether the record's checkbox can be ticked now (always true for selected rows). */\n canSelect(key: RecordKey): boolean {\n return this.isSelected(key) || !this.isFull\n }\n\n /**\n * Follow the table's `selectionScope`: when the filters, search or tab change the\n * selection is cleared, so a bulk action never hits records that are no longer listed.\n * Call it with every new table schema; an absent scope keeps the selection.\n */\n syncScope(scope: string | null | undefined): void {\n const next = scope ?? undefined\n const changed = next !== undefined && this.scope !== undefined && next !== this.scope\n\n this.scope = next\n\n if (changed && (this.state.selected.size > 0 || this.state.allSelected)) {\n this.clear()\n }\n }\n\n get state(): SelectionState {\n return this.store.get()\n }\n\n isSelected(key: RecordKey): boolean {\n return this.state.allSelected || this.state.selected.has(String(key))\n }\n\n /**\n * Select or deselect a record. `group` is the record's group (`record.group`): under\n * `selectGroupsOnly()` a record of another group replaces the selection.\n */\n toggle(key: RecordKey, force?: boolean, group?: string): void {\n this.store.set((state) => {\n const id = String(key)\n const shouldSelect = force ?? !state.selected.has(id)\n const otherGroup =\n shouldSelect && this.limits.groupsOnly && state.selected.size > 0 && state.group !== group\n const selected = otherGroup ? new Set<string>() : new Set(state.selected)\n\n if (shouldSelect) {\n const max = this.limits.max\n\n if (typeof max === 'number' && !selected.has(id) && selected.size >= max) {\n return state\n }\n\n selected.add(id)\n } else {\n selected.delete(id)\n }\n\n return {\n selected,\n allSelected: false,\n group:\n this.limits.groupsOnly && selected.size > 0\n ? shouldSelect\n ? group\n : state.group\n : undefined,\n }\n })\n }\n\n /** Select the page's selectable records, up to `maxSelectableRecords()`. */\n selectPage(records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = new Set(state.selected)\n const max = this.limits.max\n\n for (const record of records) {\n if (typeof max === 'number' && selected.size >= max) break\n\n if (record.selectable !== false) {\n selected.add(String(record.key))\n }\n }\n\n return { selected, allSelected: false, group: state.group }\n })\n }\n\n /**\n * Select every selectable record of one group on the page (`selectGroupsOnly()`), in place\n * of the current selection when it holds another group.\n */\n selectGroup(group: string, records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = state.group === group ? new Set(state.selected) : new Set<string>()\n const max = this.limits.max\n\n for (const record of records) {\n if (typeof max === 'number' && selected.size >= max) break\n\n if (record.selectable !== false && (record.group ?? '') === group) {\n selected.add(String(record.key))\n }\n }\n\n return { selected, allSelected: false, group: selected.size > 0 ? group : undefined }\n })\n }\n\n deselectGroup(group: string, records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = new Set(state.selected)\n\n for (const record of records) {\n if ((record.group ?? '') === group) selected.delete(String(record.key))\n }\n\n return { selected, allSelected: false, group: selected.size > 0 ? state.group : undefined }\n })\n }\n\n /** Checkbox state of a group heading: all, some or none of its selectable records. */\n groupState(group: string, records: TableRecordSchema[]): boolean | 'indeterminate' {\n const members = records.filter(\n (record) => record.selectable !== false && (record.group ?? '') === group,\n )\n const count = members.filter((record) => this.isSelected(record.key)).length\n\n if (count === 0 || members.length === 0) return false\n\n return count === members.length ? true : 'indeterminate'\n }\n\n deselectPage(records: TableRecordSchema[]): void {\n this.store.set((state) => {\n const selected = new Set(state.selected)\n\n for (const record of records) {\n selected.delete(String(record.key))\n }\n\n return { selected, allSelected: false, group: selected.size > 0 ? state.group : undefined }\n })\n }\n\n /** \"Select all N records\"; ignored under `maxSelectableRecords()` / `selectGroupsOnly()`. */\n selectAll(): void {\n if (typeof this.limits.max === 'number' || this.limits.groupsOnly) return\n\n this.store.set({ selected: new Set(), allSelected: true })\n }\n\n clear(): void {\n this.store.set({ selected: new Set(), allSelected: false })\n }\n\n get count(): number {\n return this.state.selected.size\n }\n\n get keys(): string[] {\n return [...this.state.selected]\n }\n}\n\nexport function isPageFullySelected(\n selection: TableSelection,\n records: TableRecordSchema[],\n): boolean {\n const selectable = records.filter((record) => record.selectable !== false)\n\n return selectable.length > 0 && selectable.every((record) => selection.isSelected(record.key))\n}\n\nexport interface TableEndpoints {\n cell: string\n reorder: string\n}\n\n/** Persist an inline cell edit, then reload page props. */\nexport async function updateTableCell(\n endpoints: TableEndpoints,\n token: string,\n component: string,\n record: RecordKey,\n column: string,\n state: unknown,\n): Promise<unknown> {\n const response = await requestJson<{ state: unknown }>(endpoints.cell, {\n method: 'POST',\n // The page's tab, filters and view: the record is looked up in the table as shown.\n body: { token, component, record, column, state, tableQuery: currentPageQuery() },\n })\n\n return response.state\n}\n\nexport async function reorderTable(\n endpoints: TableEndpoints,\n token: string,\n component: string,\n order: RecordKey[],\n options: { reload?: boolean } = {},\n): Promise<void> {\n await requestJson(endpoints.reorder, {\n method: 'POST',\n body: { token, component, order, tableQuery: currentPageQuery() },\n })\n\n if (options.reload ?? true) inertiaReload()\n}\n\n/**\n * Whether entering reorder mode reloads the table with `?reordering=1`: a paginated table\n * lists every record while reordering (so records can move between pages), unless it has\n * `paginatedWhileReordering()`.\n */\nexport function reorderingListsAllRecords(\n table: Pick<TableSchema, 'pagination' | 'paginatedWhileReordering'>,\n): boolean {\n return Boolean(table.pagination) && !table.paginatedWhileReordering\n}\n\n/** Enter reorder mode: reloads the table without pages when `reorderingListsAllRecords()`. */\nexport function startTableReordering(\n table: Pick<TableSchema, 'identifier' | 'pagination' | 'paginatedWhileReordering'>,\n): void {\n if (reorderingListsAllRecords(table)) {\n visitTable(table.identifier, { reordering: true, page: null, cursor: null }, ['page'])\n }\n}\n\n/** Leave reorder mode: a table the server lists in reorder mode reloads with its pages. */\nexport function stopTableReordering(table: Pick<TableSchema, 'identifier' | 'reordering'>): void {\n if (table.reordering) {\n visitTable(table.identifier, { reordering: null, page: null }, ['page'])\n }\n}\n\nexport function visibleColumns(table: TableSchema): TableSchema['columns'] {\n return table.columns.filter((column) => table.visibleColumns.includes(column.name))\n}\n\n/**\n * Whether the bulk bar offers \"select all N records\": not under `selectCurrentPageOnly()`,\n * `maxSelectableRecords()` or `selectGroupsOnly()`.\n */\nexport function canSelectAllRecords(\n table: Pick<TableSchema, 'selectCurrentPageOnly' | 'maxSelectableRecords' | 'selectGroupsOnly'>,\n): boolean {\n return !table.selectCurrentPageOnly && !table.maxSelectableRecords && !table.selectGroupsOnly\n}\n\n/** Reserved action arguments carrying the selection (`accessSelectedRecords()`). */\nexport const SELECTED_RECORDS_ARGUMENT = 'alfakitSelectedRecords'\nexport const ALL_SELECTED_RECORDS_ARGUMENT = 'alfakitAllSelectedRecords'\n\n/** Whether one of the actions (or an action in a group) `accessSelectedRecords()`. */\nexport function actionsAccessSelectedRecords(actions: AnyActionSchema[] | undefined): boolean {\n return (actions ?? []).some((action) =>\n 'actions' in action\n ? actionsAccessSelectedRecords(action.actions as AnyActionSchema[])\n : Boolean((action as { accessSelectedRecords?: boolean }).accessSelectedRecords),\n )\n}\n\n/**\n * Arguments that hand the table's selection to record actions that\n * `accessSelectedRecords()`; undefined when none of `actions` does.\n */\nexport function selectedRecordsArguments(\n selection: SelectionState,\n actions: AnyActionSchema[] | undefined,\n): Record<string, unknown> | undefined {\n if (!actionsAccessSelectedRecords(actions)) return undefined\n\n return selection.allSelected\n ? { [ALL_SELECTED_RECORDS_ARGUMENT]: true }\n : { [SELECTED_RECORDS_ARGUMENT]: [...selection.selected] }\n}\n\n/**\n * Search the options of a `SelectColumn` whose options live on the server\n * (`optionsSearchPath`), through the schema options endpoint and the table's filters schema.\n */\nexport async function searchTableColumnOptions(\n url: string,\n token: string,\n component: string,\n column: Pick<SelectColumnSchema, 'optionsSearchPath'>,\n search: string,\n): Promise<Option[]> {\n if (!column.optionsSearchPath) return []\n\n const response = await requestJson<{ options?: Option[] }>(url, {\n query: { token, component: `${component}.filters`, path: column.optionsSearchPath, search },\n })\n\n return response.options ?? []\n}\n\n/**\n * Options of a combobox `SelectColumn` matching `search` (case-insensitive, on the label's\n * text), without the record itself under `ignoreRecord`, capped at `optionsLimit`.\n */\nexport function filterSelectColumnOptions(\n options: Option[],\n column: Pick<SelectColumnSchema, 'optionsLimit' | 'ignoreRecord'>,\n search: string,\n recordKey?: RecordKey,\n): Option[] {\n const needle = search.trim().toLocaleLowerCase()\n const matches = options.filter(\n (option) =>\n !(column.ignoreRecord && recordKey !== undefined && option.value === String(recordKey)) &&\n (needle === '' || optionText(option.label).toLocaleLowerCase().includes(needle)),\n )\n\n return column.optionsLimit ? matches.slice(0, column.optionsLimit) : matches\n}\n\n/**\n * What an empty combobox `SelectColumn` list says: the column's own message\n * (`loadingMessage()`, `searchingMessage()`, `noOptionsMessage()`, `noSearchResultsMessage()`)\n * or the translation key and English fallback to use instead.\n */\nexport function selectColumnEmptyMessage(\n column: Pick<\n SelectColumnSchema,\n 'loadingMessage' | 'searchingMessage' | 'noOptionsMessage' | 'noSearchResultsMessage'\n >,\n state: { loading: boolean; search: string; optionCount: number },\n): { message?: string; key: string; fallback: string } {\n if (state.loading) {\n return state.search !== ''\n ? { message: column.searchingMessage, key: 'form.searching', fallback: 'Searching…' }\n : { message: column.loadingMessage, key: 'loading', fallback: 'Loading…' }\n }\n\n return state.search === '' && state.optionCount === 0\n ? {\n message: column.noOptionsMessage,\n key: 'form.no_options',\n fallback: 'No options available.',\n }\n : { message: column.noSearchResultsMessage, key: 'no_results', fallback: 'No results.' }\n}\n\n/** Plain text of an option label that may be (sanitised) HTML. */\nexport function optionText(label: string): string {\n return label\n .replace(/<[^>]*>/g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** Text sizes of an image column / entry's \"+N\" (`limitedRemainingText(size:)`, default `sm`). */\nexport const REMAINING_TEXT_SIZE_CLASSES: Record<string, string> = {\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-base',\n lg: 'text-lg',\n}\n\n/** The class of the \"+N\" after limited images. */\nexport function remainingTextSizeClass(size: string | undefined): string {\n return REMAINING_TEXT_SIZE_CLASSES[size ?? 'sm'] ?? 'text-sm'\n}\n\n/** Width classes of the filters dropdown / sidebar (`filtersFormWidth()`). */\nexport const FILTERS_FORM_WIDTH_CLASSES: Record<string, string> = {\n '3xs': 'sm:w-3xs',\n '2xs': 'sm:w-2xs',\n xs: 'sm:w-xs',\n sm: 'sm:w-sm',\n md: 'sm:w-md',\n lg: 'sm:w-lg',\n xl: 'sm:w-xl',\n '2xl': 'sm:w-2xl',\n '3xl': 'sm:w-3xl',\n '4xl': 'sm:w-4xl',\n '5xl': 'sm:w-5xl',\n '6xl': 'sm:w-6xl',\n '7xl': 'sm:w-7xl',\n none: 'sm:w-auto',\n min: 'sm:w-min',\n max: 'sm:w-max',\n fit: 'sm:w-fit',\n prose: 'sm:w-[65ch]',\n container: 'sm:container',\n 'screen-sm': 'sm:w-(--breakpoint-sm)',\n 'screen-md': 'sm:w-(--breakpoint-md)',\n 'screen-lg': 'sm:w-(--breakpoint-lg)',\n 'screen-xl': 'sm:w-(--breakpoint-xl)',\n 'screen-2xl': 'sm:w-(--breakpoint-2xl)',\n full: 'sm:w-full',\n screen: 'sm:w-screen',\n}\n\n/** Width classes of the filters dialog (`FiltersLayout::Modal`). */\nexport const FILTERS_MODAL_WIDTH_CLASSES: Record<string, string> = {\n '3xs': 'sm:max-w-3xs',\n '2xs': 'sm:max-w-2xs',\n xs: 'sm:max-w-xs',\n sm: 'sm:max-w-sm',\n md: 'sm:max-w-md',\n lg: 'sm:max-w-lg',\n xl: 'sm:max-w-xl',\n '2xl': 'sm:max-w-2xl',\n '3xl': 'sm:max-w-3xl',\n '4xl': 'sm:max-w-4xl',\n '5xl': 'sm:max-w-5xl',\n '6xl': 'sm:max-w-6xl',\n '7xl': 'sm:max-w-7xl',\n none: 'sm:max-w-none',\n min: 'sm:max-w-min',\n max: 'sm:max-w-max',\n fit: 'sm:max-w-fit',\n prose: 'sm:max-w-prose',\n container: 'sm:container',\n 'screen-sm': 'sm:max-w-(--breakpoint-sm)',\n 'screen-md': 'sm:max-w-(--breakpoint-md)',\n 'screen-lg': 'sm:max-w-(--breakpoint-lg)',\n 'screen-xl': 'sm:max-w-(--breakpoint-xl)',\n 'screen-2xl': 'sm:max-w-(--breakpoint-2xl)',\n full: 'sm:max-w-full',\n screen: 'sm:max-w-screen',\n}\n\n/** Width classes of a sidebar filters layout (`BeforeContent` / `AfterContent`). */\nexport const FILTERS_SIDEBAR_WIDTH_CLASSES: Record<string, string> = {\n '3xs': 'lg:w-3xs',\n '2xs': 'lg:w-2xs',\n xs: 'lg:w-xs',\n sm: 'lg:w-sm',\n md: 'lg:w-md',\n lg: 'lg:w-lg',\n xl: 'lg:w-xl',\n '2xl': 'lg:w-2xl',\n '3xl': 'lg:w-3xl',\n '4xl': 'lg:w-4xl',\n '5xl': 'lg:w-5xl',\n '6xl': 'lg:w-6xl',\n '7xl': 'lg:w-7xl',\n none: 'lg:w-auto',\n min: 'lg:w-min',\n max: 'lg:w-max',\n fit: 'lg:w-fit',\n prose: 'lg:w-[65ch]',\n container: 'lg:container',\n 'screen-sm': 'lg:w-(--breakpoint-sm)',\n 'screen-md': 'lg:w-(--breakpoint-md)',\n 'screen-lg': 'lg:w-(--breakpoint-lg)',\n 'screen-xl': 'lg:w-(--breakpoint-xl)',\n 'screen-2xl': 'lg:w-(--breakpoint-2xl)',\n full: 'lg:w-full',\n screen: 'lg:w-screen',\n}\n\nexport type FiltersPlacement = 'toolbar' | 'above' | 'below' | 'before' | 'after'\n\n/**\n * Where a filters layout renders: in the toolbar (dropdown, modal), above or below the\n * table, or in a sidebar before / after it.\n */\nexport function filtersPlacement(layout: TableSchema['filtersLayout']): FiltersPlacement {\n if (layout.startsWith('above-content')) return 'above'\n if (layout === 'below-content') return 'below'\n if (layout.startsWith('before-content')) return 'before'\n if (layout.startsWith('after-content')) return 'after'\n\n return 'toolbar'\n}\n\n/** Whether the filters panel of this layout can be collapsed. */\nexport function filtersCollapsible(layout: TableSchema['filtersLayout']): boolean {\n return layout.endsWith('-collapsible')\n}\n\n/** The width class of a filters form for its layout, or undefined for the default. */\nexport function filtersFormWidthClass(\n table: Pick<TableSchema, 'filtersLayout' | 'filtersFormWidth'>,\n): string | undefined {\n const width = table.filtersFormWidth as FiltersFormWidth | undefined\n\n if (!width) return undefined\n\n const placement = filtersPlacement(table.filtersLayout)\n\n if (placement === 'before' || placement === 'after') return FILTERS_SIDEBAR_WIDTH_CLASSES[width]\n if (table.filtersLayout === 'modal') return FILTERS_MODAL_WIDTH_CLASSES[width]\n if (placement === 'toolbar') return FILTERS_FORM_WIDTH_CLASSES[width]\n\n return undefined\n}\n\n/**\n * Classes of a `stackedOnMobile()` table: below `md` the table, its body and rows are\n * blocks and each row is a card; every cell shows its column label (`stackedCellLabel`).\n */\nexport const STACKED_TABLE_CLASSES = {\n table: 'max-md:block',\n header: 'max-md:hidden',\n body: 'max-md:block',\n row: 'max-md:flex max-md:flex-col max-md:gap-2 max-md:border-b max-md:px-4 max-md:py-3',\n cell: 'max-md:flex max-md:min-w-0 max-md:items-start max-md:justify-between max-md:gap-3 max-md:p-0 max-md:text-start max-md:whitespace-normal',\n /** Cells with their own responsive visibility keep it (no flex override). */\n responsiveCell: 'max-md:p-0 max-md:text-start max-md:whitespace-normal',\n controlCell: 'max-md:flex max-md:items-center max-md:justify-start max-md:p-0',\n actionsCell: 'max-md:flex max-md:items-center max-md:justify-end max-md:p-0',\n label: 'text-muted-foreground shrink-0 text-xs font-medium md:hidden',\n footer: 'max-md:hidden',\n} as const\n\n/** Classes of a data cell in a stacked-on-mobile table. */\nexport function stackedCellClasses(\n column: Pick<ColumnSchema, 'visibleFrom' | 'hiddenFrom'>,\n): string {\n return column.visibleFrom || column.hiddenFrom\n ? STACKED_TABLE_CLASSES.responsiveCell\n : STACKED_TABLE_CLASSES.cell\n}\n"],"mappings":"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"}
|
package/dist/undo.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare class UndoShortcuts {
|
|
|
30
30
|
}
|
|
31
31
|
/** The browser-wide registry the renderers' undo buttons share. */
|
|
32
32
|
export declare const undoShortcuts: UndoShortcuts;
|
|
33
|
+
/** Whether the offer with this token was used or expired. */
|
|
34
|
+
export declare function isUndoFinished(undo: Pick<NotificationUndoSchema, 'token'>): boolean;
|
|
33
35
|
/**
|
|
34
36
|
* Wire one toast's undo button: the countdown, the shortcut and the request. `onTick` gets
|
|
35
37
|
* the seconds left; `onClose` runs when the offer ends (undone or expired) so the toast can
|
package/dist/undo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"undo.d.ts","names":[],"sources":["../src/undo.ts"],"mappings":";;;;;;;wBAUgB,YAAY,MAAM,yBAAyB;;;;;wBAoB3C,mBACd,iBACA,SAAS,4BACT;;wBAiCc,eAAe,OAAO;;;;;qBAazB;mBACM;UAET;mBAES;;EAiBjB,SAAS,YAAY;EAQrB,WAAW;MAYP;UAII;UASA;;;qBAWG,eAAa;;;;;;
|
|
1
|
+
{"version":3,"file":"undo.d.ts","names":[],"sources":["../src/undo.ts"],"mappings":";;;;;;;wBAUgB,YAAY,MAAM,yBAAyB;;;;;wBAoB3C,mBACd,iBACA,SAAS,4BACT;;wBAiCc,eAAe,OAAO;;;;;qBAazB;mBACM;UAET;mBAES;;EAiBjB,SAAS,YAAY;EAQrB,WAAW;MAYP;UAII;UASA;;;qBAWG,eAAa;;wBAUV,eAAe,MAAM,KAAK;;;;;;wBAS1B,SACd,YACA,MAAM,wBACN;EACE,SAAS;EACT;EACA,UAAU;;EAET,YAAY;EAAe"}
|
package/dist/undo.js
CHANGED
|
@@ -95,11 +95,29 @@ var UndoShortcuts = class {
|
|
|
95
95
|
/** The browser-wide registry the renderers' undo buttons share. */
|
|
96
96
|
const undoShortcuts = new UndoShortcuts();
|
|
97
97
|
/**
|
|
98
|
+
* Undo tokens whose offer ended (used or expired) in this browser tab. A toast whose button
|
|
99
|
+
* mounts again (the toaster re-rendering it while it closes, another toast arriving) must
|
|
100
|
+
* not offer a used token to Ctrl/Cmd+Z again.
|
|
101
|
+
*/
|
|
102
|
+
const finishedUndoTokens = /* @__PURE__ */ new Set();
|
|
103
|
+
/** Whether the offer with this token was used or expired. */
|
|
104
|
+
function isUndoFinished(undo) {
|
|
105
|
+
return finishedUndoTokens.has(undo.token);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
98
108
|
* Wire one toast's undo button: the countdown, the shortcut and the request. `onTick` gets
|
|
99
109
|
* the seconds left; `onClose` runs when the offer ends (undone or expired) so the toast can
|
|
100
110
|
* close. Returns `{ undo, stop }`.
|
|
101
111
|
*/
|
|
102
112
|
function bindUndo(id, undo, handlers) {
|
|
113
|
+
if (isUndoFinished(undo)) {
|
|
114
|
+
handlers.onTick(0);
|
|
115
|
+
handlers.onClose();
|
|
116
|
+
return {
|
|
117
|
+
undo: async () => {},
|
|
118
|
+
stop: () => {}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
103
121
|
let done = false;
|
|
104
122
|
let stopCountdown = () => {};
|
|
105
123
|
let withdraw = () => {};
|
|
@@ -110,6 +128,7 @@ function bindUndo(id, undo, handlers) {
|
|
|
110
128
|
const run = async () => {
|
|
111
129
|
if (done) return;
|
|
112
130
|
done = true;
|
|
131
|
+
finishedUndoTokens.add(undo.token);
|
|
113
132
|
stop();
|
|
114
133
|
handlers.onBusy?.(true);
|
|
115
134
|
try {
|
|
@@ -122,6 +141,7 @@ function bindUndo(id, undo, handlers) {
|
|
|
122
141
|
stopCountdown = startUndoCountdown(undo.seconds, handlers.onTick, () => {
|
|
123
142
|
if (!done) {
|
|
124
143
|
done = true;
|
|
144
|
+
finishedUndoTokens.add(undo.token);
|
|
125
145
|
withdraw();
|
|
126
146
|
handlers.onClose();
|
|
127
147
|
}
|
|
@@ -133,6 +153,6 @@ function bindUndo(id, undo, handlers) {
|
|
|
133
153
|
};
|
|
134
154
|
}
|
|
135
155
|
//#endregion
|
|
136
|
-
export { UndoShortcuts, bindUndo, isUndoShortcut, performUndo, startUndoCountdown, undoShortcuts };
|
|
156
|
+
export { UndoShortcuts, bindUndo, isUndoFinished, isUndoShortcut, performUndo, startUndoCountdown, undoShortcuts };
|
|
137
157
|
|
|
138
158
|
//# sourceMappingURL=undo.js.map
|
package/dist/undo.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"undo.js","names":[],"sources":["../src/undo.ts"],"sourcesContent":["import { router } from './http'\nimport type { NotificationUndoSchema } from './schema/notifications'\nimport { isEditableTarget } from './shortcuts'\n\n/**\n * The \"Undo\" button of `Action::undoable()` toasts: a countdown, the undo request and the\n * Ctrl/Cmd+Z shortcut (while a toast offers undo and focus is not in a text field).\n */\n\n/** Send the undo request; the answer (an Inertia redirect back) flashes its notification. */\nexport function performUndo(undo: NotificationUndoSchema): Promise<void> {\n return new Promise((resolve) => {\n if (!undo.url) {\n resolve()\n\n return\n }\n\n router.post(\n undo.url,\n { undo: undo.token },\n { preserveScroll: true, preserveState: true, onFinish: () => resolve() },\n )\n })\n}\n\n/**\n * Count down from `seconds`, calling `onTick` with the seconds left (right away, then once\n * a second) and `onDone` at zero. Returns a function that stops it.\n */\nexport function startUndoCountdown(\n seconds: number,\n onTick: (remaining: number) => void,\n onDone?: () => void,\n): () => void {\n if (typeof window === 'undefined') {\n return () => {}\n }\n\n const ends = Date.now() + seconds * 1000\n let stopped = false\n\n onTick(Math.max(0, seconds))\n\n const timer = setInterval(() => {\n const remaining = Math.max(0, Math.ceil((ends - Date.now()) / 1000))\n\n onTick(remaining)\n\n if (remaining === 0) {\n stop()\n onDone?.()\n }\n }, 1000)\n\n function stop(): void {\n if (!stopped) {\n stopped = true\n clearInterval(timer)\n }\n }\n\n return stop\n}\n\n/** Ctrl+Z (Cmd+Z on macOS), without Shift or Alt. */\nexport function isUndoShortcut(event: KeyboardEvent): boolean {\n return (\n (event.ctrlKey || event.metaKey) &&\n !event.shiftKey &&\n !event.altKey &&\n event.key.toLowerCase() === 'z'\n )\n}\n\n/**\n * The toasts that offer undo right now. Ctrl/Cmd+Z undoes the newest one, unless focus is\n * in a field (where it undoes typing).\n */\nexport class UndoShortcuts {\n private readonly entries: Array<{ id: string; run: () => void }> = []\n\n private listening = false\n\n private readonly onKeyDown = (event: KeyboardEvent): void => {\n if (event.defaultPrevented || !isUndoShortcut(event) || isEditableTarget(event.target)) {\n return\n }\n\n const entry = this.entries.at(-1)\n\n if (!entry) {\n return\n }\n\n event.preventDefault()\n this.unregister(entry.id)\n entry.run()\n }\n\n /** Offer `run` for Ctrl/Cmd+Z; returns a function that withdraws it. */\n register(id: string, run: () => void): () => void {\n this.unregister(id)\n this.entries.push({ id, run })\n this.listen()\n\n return () => this.unregister(id)\n }\n\n unregister(id: string): void {\n const index = this.entries.findIndex((entry) => entry.id === id)\n\n if (index !== -1) {\n this.entries.splice(index, 1)\n }\n\n if (this.entries.length === 0) {\n this.stopListening()\n }\n }\n\n get size(): number {\n return this.entries.length\n }\n\n private listen(): void {\n if (this.listening || typeof window === 'undefined') {\n return\n }\n\n this.listening = true\n window.addEventListener('keydown', this.onKeyDown)\n }\n\n private stopListening(): void {\n if (!this.listening || typeof window === 'undefined') {\n return\n }\n\n this.listening = false\n window.removeEventListener('keydown', this.onKeyDown)\n }\n}\n\n/** The browser-wide registry the renderers' undo buttons share. */\nexport const undoShortcuts = new UndoShortcuts()\n\n/**\n * Wire one toast's undo button: the countdown, the shortcut and the request. `onTick` gets\n * the seconds left; `onClose` runs when the offer ends (undone or expired) so the toast can\n * close. Returns `{ undo, stop }`.\n */\nexport function bindUndo(\n id: string,\n undo: NotificationUndoSchema,\n handlers: {\n onTick: (remaining: number) => void\n onClose: () => void\n onBusy?: (busy: boolean) => void\n },\n): { undo: () => Promise<void>; stop: () => void } {\n let done = false\n let stopCountdown: () => void = () => {}\n let withdraw: () => void = () => {}\n\n const stop = (): void => {\n stopCountdown()\n withdraw()\n }\n\n const run = async (): Promise<void> => {\n if (done) {\n return\n }\n\n done = true\n stop()\n handlers.onBusy?.(true)\n\n try {\n await performUndo(undo)\n } finally {\n handlers.onBusy?.(false)\n handlers.onClose()\n }\n }\n\n stopCountdown = startUndoCountdown(undo.seconds, handlers.onTick, () => {\n if (!done) {\n done = true\n withdraw()\n handlers.onClose()\n }\n })\n withdraw = undoShortcuts.register(id, () => void run())\n\n return { undo: run, stop }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"undo.js","names":[],"sources":["../src/undo.ts"],"sourcesContent":["import { router } from './http'\nimport type { NotificationUndoSchema } from './schema/notifications'\nimport { isEditableTarget } from './shortcuts'\n\n/**\n * The \"Undo\" button of `Action::undoable()` toasts: a countdown, the undo request and the\n * Ctrl/Cmd+Z shortcut (while a toast offers undo and focus is not in a text field).\n */\n\n/** Send the undo request; the answer (an Inertia redirect back) flashes its notification. */\nexport function performUndo(undo: NotificationUndoSchema): Promise<void> {\n return new Promise((resolve) => {\n if (!undo.url) {\n resolve()\n\n return\n }\n\n router.post(\n undo.url,\n { undo: undo.token },\n { preserveScroll: true, preserveState: true, onFinish: () => resolve() },\n )\n })\n}\n\n/**\n * Count down from `seconds`, calling `onTick` with the seconds left (right away, then once\n * a second) and `onDone` at zero. Returns a function that stops it.\n */\nexport function startUndoCountdown(\n seconds: number,\n onTick: (remaining: number) => void,\n onDone?: () => void,\n): () => void {\n if (typeof window === 'undefined') {\n return () => {}\n }\n\n const ends = Date.now() + seconds * 1000\n let stopped = false\n\n onTick(Math.max(0, seconds))\n\n const timer = setInterval(() => {\n const remaining = Math.max(0, Math.ceil((ends - Date.now()) / 1000))\n\n onTick(remaining)\n\n if (remaining === 0) {\n stop()\n onDone?.()\n }\n }, 1000)\n\n function stop(): void {\n if (!stopped) {\n stopped = true\n clearInterval(timer)\n }\n }\n\n return stop\n}\n\n/** Ctrl+Z (Cmd+Z on macOS), without Shift or Alt. */\nexport function isUndoShortcut(event: KeyboardEvent): boolean {\n return (\n (event.ctrlKey || event.metaKey) &&\n !event.shiftKey &&\n !event.altKey &&\n event.key.toLowerCase() === 'z'\n )\n}\n\n/**\n * The toasts that offer undo right now. Ctrl/Cmd+Z undoes the newest one, unless focus is\n * in a field (where it undoes typing).\n */\nexport class UndoShortcuts {\n private readonly entries: Array<{ id: string; run: () => void }> = []\n\n private listening = false\n\n private readonly onKeyDown = (event: KeyboardEvent): void => {\n if (event.defaultPrevented || !isUndoShortcut(event) || isEditableTarget(event.target)) {\n return\n }\n\n const entry = this.entries.at(-1)\n\n if (!entry) {\n return\n }\n\n event.preventDefault()\n this.unregister(entry.id)\n entry.run()\n }\n\n /** Offer `run` for Ctrl/Cmd+Z; returns a function that withdraws it. */\n register(id: string, run: () => void): () => void {\n this.unregister(id)\n this.entries.push({ id, run })\n this.listen()\n\n return () => this.unregister(id)\n }\n\n unregister(id: string): void {\n const index = this.entries.findIndex((entry) => entry.id === id)\n\n if (index !== -1) {\n this.entries.splice(index, 1)\n }\n\n if (this.entries.length === 0) {\n this.stopListening()\n }\n }\n\n get size(): number {\n return this.entries.length\n }\n\n private listen(): void {\n if (this.listening || typeof window === 'undefined') {\n return\n }\n\n this.listening = true\n window.addEventListener('keydown', this.onKeyDown)\n }\n\n private stopListening(): void {\n if (!this.listening || typeof window === 'undefined') {\n return\n }\n\n this.listening = false\n window.removeEventListener('keydown', this.onKeyDown)\n }\n}\n\n/** The browser-wide registry the renderers' undo buttons share. */\nexport const undoShortcuts = new UndoShortcuts()\n\n/**\n * Undo tokens whose offer ended (used or expired) in this browser tab. A toast whose button\n * mounts again (the toaster re-rendering it while it closes, another toast arriving) must\n * not offer a used token to Ctrl/Cmd+Z again.\n */\nconst finishedUndoTokens = new Set<string>()\n\n/** Whether the offer with this token was used or expired. */\nexport function isUndoFinished(undo: Pick<NotificationUndoSchema, 'token'>): boolean {\n return finishedUndoTokens.has(undo.token)\n}\n\n/**\n * Wire one toast's undo button: the countdown, the shortcut and the request. `onTick` gets\n * the seconds left; `onClose` runs when the offer ends (undone or expired) so the toast can\n * close. Returns `{ undo, stop }`.\n */\nexport function bindUndo(\n id: string,\n undo: NotificationUndoSchema,\n handlers: {\n onTick: (remaining: number) => void\n onClose: () => void\n onBusy?: (busy: boolean) => void\n },\n): { undo: () => Promise<void>; stop: () => void } {\n if (isUndoFinished(undo)) {\n handlers.onTick(0)\n handlers.onClose()\n\n return { undo: async () => {}, stop: () => {} }\n }\n\n let done = false\n let stopCountdown: () => void = () => {}\n let withdraw: () => void = () => {}\n\n const stop = (): void => {\n stopCountdown()\n withdraw()\n }\n\n const run = async (): Promise<void> => {\n if (done) {\n return\n }\n\n done = true\n finishedUndoTokens.add(undo.token)\n stop()\n handlers.onBusy?.(true)\n\n try {\n await performUndo(undo)\n } finally {\n handlers.onBusy?.(false)\n handlers.onClose()\n }\n }\n\n stopCountdown = startUndoCountdown(undo.seconds, handlers.onTick, () => {\n if (!done) {\n done = true\n finishedUndoTokens.add(undo.token)\n withdraw()\n handlers.onClose()\n }\n })\n withdraw = undoShortcuts.register(id, () => void run())\n\n return { undo: run, stop }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfakit/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Framework-agnostic schema types and state management for Alfakit, the Laravel admin panel builder by Alfa Six.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alfa Six (https://alfasixbh.com)",
|
package/themes.css
CHANGED
|
@@ -742,6 +742,10 @@
|
|
|
742
742
|
max-height: none;
|
|
743
743
|
width: 100%;
|
|
744
744
|
max-width: 100%;
|
|
745
|
+
/* The shadcn centring utilities read these variables. Resetting them survives a
|
|
746
|
+
minifier that drops `translate: none` in favour of the utilities' `translate`. */
|
|
747
|
+
--tw-translate-x: 0;
|
|
748
|
+
--tw-translate-y: 0;
|
|
745
749
|
translate: none;
|
|
746
750
|
transform: none;
|
|
747
751
|
border-radius: 0;
|