@ai-matrx/design-system 0.25.0 → 0.27.0
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 +94 -0
- package/dist/data-table/copy-helpers.cjs.map +1 -1
- package/dist/data-table/copy-helpers.d.cts +3 -1
- package/dist/data-table/copy-helpers.d.ts +3 -1
- package/dist/data-table/copy-helpers.js.map +1 -1
- package/dist/data-table/data-row-inspector.cjs.map +1 -1
- package/dist/data-table/data-row-inspector.js.map +1 -1
- package/dist/data-table/data-row-window.cjs.map +1 -1
- package/dist/data-table/data-row-window.js.map +1 -1
- package/dist/data-table/date-filter.cjs +145 -0
- package/dist/data-table/date-filter.cjs.map +1 -0
- package/dist/data-table/date-filter.d.cts +31 -0
- package/dist/data-table/date-filter.d.ts +31 -0
- package/dist/data-table/date-filter.js +125 -0
- package/dist/data-table/date-filter.js.map +1 -0
- package/dist/data-table/facets.cjs +118 -0
- package/dist/data-table/facets.cjs.map +1 -0
- package/dist/data-table/facets.d.cts +100 -0
- package/dist/data-table/facets.d.ts +100 -0
- package/dist/data-table/facets.js +98 -0
- package/dist/data-table/facets.js.map +1 -0
- package/dist/data-table/filter-engine.cjs +88 -0
- package/dist/data-table/filter-engine.cjs.map +1 -1
- package/dist/data-table/filter-engine.d.cts +3 -1
- package/dist/data-table/filter-engine.d.ts +3 -1
- package/dist/data-table/filter-engine.js +88 -0
- package/dist/data-table/filter-engine.js.map +1 -1
- package/dist/data-table/host.cjs.map +1 -1
- package/dist/data-table/host.d.cts +14 -3
- package/dist/data-table/host.d.ts +14 -3
- package/dist/data-table/host.js.map +1 -1
- package/dist/data-table/index.cjs +918 -152
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +5 -2
- package/dist/data-table/index.d.ts +5 -2
- package/dist/data-table/index.js +956 -189
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.cjs +2 -0
- package/dist/data-table/infer-filter.cjs.map +1 -1
- package/dist/data-table/infer-filter.d.cts +4 -2
- package/dist/data-table/infer-filter.d.ts +4 -2
- package/dist/data-table/infer-filter.js +2 -0
- package/dist/data-table/infer-filter.js.map +1 -1
- package/dist/data-table/layered-filters.d.cts +3 -1
- package/dist/data-table/layered-filters.d.ts +3 -1
- package/dist/data-table/menu-targets.cjs +77 -0
- package/dist/data-table/menu-targets.cjs.map +1 -0
- package/dist/data-table/menu-targets.d.cts +108 -0
- package/dist/data-table/menu-targets.d.ts +108 -0
- package/dist/data-table/menu-targets.js +57 -0
- package/dist/data-table/menu-targets.js.map +1 -0
- package/dist/data-table/pagination.cjs.map +1 -1
- package/dist/data-table/pagination.js.map +1 -1
- package/dist/data-table/query-control.cjs.map +1 -1
- package/dist/data-table/query-control.d.cts +3 -1
- package/dist/data-table/query-control.d.ts +3 -1
- package/dist/data-table/query-control.js.map +1 -1
- package/dist/data-table/spreadsheet.cjs +61 -13
- package/dist/data-table/spreadsheet.cjs.map +1 -1
- package/dist/data-table/spreadsheet.d.cts +36 -1
- package/dist/data-table/spreadsheet.d.ts +36 -1
- package/dist/data-table/spreadsheet.js +61 -13
- package/dist/data-table/spreadsheet.js.map +1 -1
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +3 -1
- package/dist/data-table/types.d.ts +3 -1
- package/dist/data-table/url-state.d.cts +3 -1
- package/dist/data-table/url-state.d.ts +3 -1
- package/dist/data-table/uuid-cell.cjs.map +1 -1
- package/dist/data-table/uuid-cell.js.map +1 -1
- package/dist/data-table/xlsx.cjs.map +1 -1
- package/dist/data-table/xlsx.d.cts +3 -1
- package/dist/data-table/xlsx.d.ts +3 -1
- package/dist/data-table/xlsx.js.map +1 -1
- package/dist/field-formats/index.cjs +15 -0
- package/dist/field-formats/index.cjs.map +1 -1
- package/dist/field-formats/index.js +15 -0
- package/dist/field-formats/index.js.map +1 -1
- package/dist/{types-nlLCMiqd.d.ts → types-D9JBR6ZU.d.ts} +118 -5
- package/dist/{types-CmQrOz-T.d.cts → types-jQddSUnj.d.cts} +118 -5
- package/package.json +31 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts","../../src/data-table/query-control.ts"],"sourcesContent":["import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n/** Exact SQL-style NULL selection, distinct from the empty-cell option. */\nexport const SELECT_NULL_VALUE = \"__null__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n // `values: []` is an explicit multi-select state: the user deselected\n // every option, so it must produce an empty result. Legacy single-value\n // filters and an absent `values` field retain their clear/all behavior.\n if (filter.values !== undefined && filter.values.length === 0) {\n return false;\n }\n const selected = filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_NULL_VALUE\n ? value == null\n : v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : v === \"\"\n ? value === \"\" || (Array.isArray(value) && value.includes(\"\"))\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n // A missing cell is not zero. Without this guard, Number(\"\") made a\n // blank numeric value pass filters whose range included zero.\n const raw = stringifyCellValue(value).trim();\n if (raw === \"\") return false;\n const normalized = raw.replace(/[^0-9.-]/g, \"\");\n if (normalized === \"\") return false;\n const n = typeof value === \"number\"\n ? value\n : Number(normalized);\n if (!Number.isFinite(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values !== undefined || (f.value && f.value !== \"__all__\"))\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values !== undefined\n ? true\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n","import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n SELECT_NULL_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n let hasEmptyCollection = false;\n for (const row of rows) {\n const cell = getCellValue(row, col);\n const values = selectCellValues(cell);\n if (values.length === 0) {\n hasEmpty = true;\n if (Array.isArray(cell)) hasEmptyCollection = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? col.filterOptions.map((option) => ({\n ...option,\n label: option.label.trim() ? option.label : option.value === \"\" ? \"(empty string)\" : option.value.trim() ? option.value : \"(whitespace)\",\n }))\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n // An explicit literal-empty choice must remain independently deselectable.\n // The combined empty sentinel would overlap it after All -> uncheck.\n // Empty collections still require that broader option: neither null nor\n // a literal empty string represents an empty array.\n const emptyOption = !hasEmptyCollection && base.some((option) => option.value === \"\")\n ? { value: SELECT_NULL_VALUE, label: \"(null)\" }\n : { value: SELECT_EMPTY_VALUE, label: \"(empty)\" };\n const additions = [];\n if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);\n if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {\n additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" });\n }\n base.unshift(...additions);\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n","import {\n collectSelectOptions,\n resolveFilterKind,\n type ResolvedFilterKind,\n} from \"./infer-filter\";\nimport type { MatrxColumnDef, MatrxDataTableQueryState } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\n\nexport interface QueryFilterMeta {\n kind: ResolvedFilterKind | null;\n options: Array<{ value: string; label: string }>;\n}\n\n/**\n * Resolve header-filter metadata without inferring remote cardinality from the\n * current page. Controlled callers must declare select options explicitly;\n * local tables preserve the original inference behavior.\n */\nexport function resolveQueryFilterMeta<T>(\n column: MatrxColumnDef<T>,\n rows: T[],\n controlled: boolean,\n): QueryFilterMeta {\n if (!controlled) {\n const kind = resolveFilterKind(column, rows);\n return {\n kind,\n options: kind === \"select\" ? collectSelectOptions(column, rows) : [],\n };\n }\n if (column.filter === false) return { kind: null, options: [] };\n if (resolveColumnMarker(column) && (column.filter === undefined || column.filter === \"auto\")) {\n return { kind: \"boolean\", options: [] };\n }\n const declared = column.filter;\n const kind: ResolvedFilterKind =\n declared && declared !== \"auto\"\n ? declared\n : column.filterOptions?.length\n ? \"select\"\n : \"text\";\n return {\n kind,\n options: kind === \"select\" ? (column.filterOptions ?? []) : [],\n };\n}\n\nexport interface QueryStateChangeOptions {\n /** Reset to the first page after a query-shape change. */\n resetPage?: boolean;\n}\n\n/**\n * Build the next controlled query state without mutating the caller's object.\n * Search, filters, sort, facets, and page-size changes pass `resetPage: true`;\n * direct pagination passes a page patch without resetting it.\n */\nexport function nextQueryState(\n current: MatrxDataTableQueryState,\n patch: Partial<MatrxDataTableQueryState>,\n options: QueryStateChangeOptions = {},\n): MatrxDataTableQueryState {\n return {\n ...current,\n ...patch,\n page: options.resetPage ? 1 : (patch.page ?? current.page),\n };\n}\n\n/** Resolve a safe one-based page for the latest remote total. */\nexport function safeQueryPage(\n requestedPage: number,\n totalItems: number,\n pageSize: number,\n): number {\n const safeTotal = Math.max(0, totalItems);\n const safeSize = Math.max(1, pageSize);\n const pageCount = Math.max(1, Math.ceil(safeTotal / safeSize));\n return Math.min(Math.max(1, requestedPage), pageCount);\n}\n"],"mappings":"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","names":["kind"]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts","../../src/data-table/query-control.ts"],"sourcesContent":["import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\nimport { dateFilterBounds } from \"./date-filter\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n/** Exact SQL-style NULL selection, distinct from the empty-cell option. */\nexport const SELECT_NULL_VALUE = \"__null__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n // `values: []` is an explicit multi-select state: the user deselected\n // every option, so it must produce an empty result. Legacy single-value\n // filters and an absent `values` field retain their clear/all behavior.\n if (filter.values !== undefined && filter.values.length === 0) {\n return false;\n }\n const selected = filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_NULL_VALUE\n ? value == null\n : v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : v === \"\"\n ? value === \"\" || (Array.isArray(value) && value.includes(\"\"))\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n // A missing cell is not zero. Without this guard, Number(\"\") made a\n // blank numeric value pass filters whose range included zero.\n const raw = stringifyCellValue(value).trim();\n if (raw === \"\") return false;\n const normalized = raw.replace(/[^0-9.-]/g, \"\");\n if (normalized === \"\") return false;\n const n = typeof value === \"number\"\n ? value\n : Number(normalized);\n if (!Number.isFinite(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n case \"date\": {\n const raw = value instanceof Date ? value : new Date(stringifyCellValue(value));\n if (Number.isNaN(raw.valueOf())) return false;\n const { since, until } = dateFilterBounds(filter);\n const timestamp = raw.valueOf();\n if (since !== undefined) {\n const sinceTimestamp = new Date(since).valueOf();\n if (Number.isNaN(sinceTimestamp) || timestamp < sinceTimestamp) return false;\n }\n if (until !== undefined) {\n const untilTimestamp = new Date(until).valueOf();\n if (Number.isNaN(untilTimestamp) || timestamp >= untilTimestamp) return false;\n }\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values !== undefined || (f.value && f.value !== \"__all__\"))\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n case \"date\":\n if (isColumnFilterActive(f)) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values !== undefined\n ? true\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n case \"date\": {\n const bounds = dateFilterBounds(filter);\n return bounds.since !== undefined || bounds.until !== undefined;\n }\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n","import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n SELECT_NULL_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\" | \"date\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n let hasEmptyCollection = false;\n for (const row of rows) {\n const cell = getCellValue(row, col);\n const values = selectCellValues(cell);\n if (values.length === 0) {\n hasEmpty = true;\n if (Array.isArray(cell)) hasEmptyCollection = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? col.filterOptions.map((option) => ({\n ...option,\n label: option.label.trim() ? option.label : option.value === \"\" ? \"(empty string)\" : option.value.trim() ? option.value : \"(whitespace)\",\n }))\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n // An explicit literal-empty choice must remain independently deselectable.\n // The combined empty sentinel would overlap it after All -> uncheck.\n // Empty collections still require that broader option: neither null nor\n // a literal empty string represents an empty array.\n const emptyOption = !hasEmptyCollection && base.some((option) => option.value === \"\")\n ? { value: SELECT_NULL_VALUE, label: \"(null)\" }\n : { value: SELECT_EMPTY_VALUE, label: \"(empty)\" };\n const additions = [];\n if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);\n if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {\n additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" });\n }\n base.unshift(...additions);\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"date\":\n return \"Date\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n","import {\n collectSelectOptions,\n resolveFilterKind,\n type ResolvedFilterKind,\n} from \"./infer-filter\";\nimport type { MatrxColumnDef, MatrxDataTableQueryState } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\n\nexport interface QueryFilterMeta {\n kind: ResolvedFilterKind | null;\n options: Array<{ value: string; label: string }>;\n}\n\n/**\n * Resolve header-filter metadata without inferring remote cardinality from the\n * current page. Controlled callers must declare select options explicitly;\n * local tables preserve the original inference behavior.\n */\nexport function resolveQueryFilterMeta<T>(\n column: MatrxColumnDef<T>,\n rows: T[],\n controlled: boolean,\n): QueryFilterMeta {\n if (!controlled) {\n const kind = resolveFilterKind(column, rows);\n return {\n kind,\n options: kind === \"select\" ? collectSelectOptions(column, rows) : [],\n };\n }\n if (column.filter === false) return { kind: null, options: [] };\n if (resolveColumnMarker(column) && (column.filter === undefined || column.filter === \"auto\")) {\n return { kind: \"boolean\", options: [] };\n }\n const declared = column.filter;\n const kind: ResolvedFilterKind =\n declared && declared !== \"auto\"\n ? declared\n : column.filterOptions?.length\n ? \"select\"\n : \"text\";\n return {\n kind,\n options: kind === \"select\" ? (column.filterOptions ?? []) : [],\n };\n}\n\nexport interface QueryStateChangeOptions {\n /** Reset to the first page after a query-shape change. */\n resetPage?: boolean;\n}\n\n/**\n * Build the next controlled query state without mutating the caller's object.\n * Search, filters, sort, facets, and page-size changes pass `resetPage: true`;\n * direct pagination passes a page patch without resetting it.\n */\nexport function nextQueryState(\n current: MatrxDataTableQueryState,\n patch: Partial<MatrxDataTableQueryState>,\n options: QueryStateChangeOptions = {},\n): MatrxDataTableQueryState {\n return {\n ...current,\n ...patch,\n page: options.resetPage ? 1 : (patch.page ?? current.page),\n };\n}\n\n/** Resolve a safe one-based page for the latest remote total. */\nexport function safeQueryPage(\n requestedPage: number,\n totalItems: number,\n pageSize: number,\n): number {\n const safeTotal = Math.max(0, totalItems);\n const safeSize = Math.max(1, pageSize);\n const pageCount = Math.max(1, Math.ceil(safeTotal / safeSize));\n return Math.min(Math.max(1, requestedPage), pageCount);\n}\n"],"mappings":"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","names":["kind"]}
|
|
@@ -668,6 +668,26 @@ function useSpreadsheetGrid(options) {
|
|
|
668
668
|
for (let i = 0; i < columnIds.length; i += 1) map.set(columnIds[i], i);
|
|
669
669
|
return map;
|
|
670
670
|
}, [columnIds]);
|
|
671
|
+
const screenPatches = (0, import_react.useCallback)(
|
|
672
|
+
(patches) => {
|
|
673
|
+
const validate = optionsRef.current?.validateCell;
|
|
674
|
+
if (!validate) return { accepted: patches, refusals: [] };
|
|
675
|
+
const accepted = [];
|
|
676
|
+
const refusals = [];
|
|
677
|
+
for (const patch of patches) {
|
|
678
|
+
let reason;
|
|
679
|
+
try {
|
|
680
|
+
reason = validate({ rowId: patch.rowId, columnId: patch.columnId, value: patch.value });
|
|
681
|
+
} catch {
|
|
682
|
+
reason = void 0;
|
|
683
|
+
}
|
|
684
|
+
if (reason) refusals.push({ columnId: patch.columnId, reason });
|
|
685
|
+
else accepted.push(patch);
|
|
686
|
+
}
|
|
687
|
+
return { accepted, refusals };
|
|
688
|
+
},
|
|
689
|
+
[]
|
|
690
|
+
);
|
|
671
691
|
const applyPatches = (0, import_react.useCallback)(
|
|
672
692
|
async (patches, intent, label) => {
|
|
673
693
|
const current = optionsRef.current;
|
|
@@ -724,9 +744,10 @@ function useSpreadsheetGrid(options) {
|
|
|
724
744
|
...base,
|
|
725
745
|
range: { top: bounds.r0, left: bounds.c0, bottom: bounds.r1, right: bounds.c1 }
|
|
726
746
|
});
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
747
|
+
const screened = screenPatches(result.patches);
|
|
748
|
+
await applyPatches(screened.accepted, "fill-down", "Filled down");
|
|
749
|
+
announceSkips([...result.skipped, ...screened.refusals], result.summary);
|
|
750
|
+
}, [fillRequestBase, bounds, applyPatches, announceSkips, screenPatches]);
|
|
730
751
|
const doClear = (0, import_react.useCallback)(async () => {
|
|
731
752
|
const current = optionsRef.current;
|
|
732
753
|
if (!current || !bounds) return;
|
|
@@ -747,9 +768,13 @@ function useSpreadsheetGrid(options) {
|
|
|
747
768
|
patches.push({ rowId, columnId, value: null });
|
|
748
769
|
}
|
|
749
770
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
771
|
+
const screened = screenPatches(patches);
|
|
772
|
+
await applyPatches(screened.accepted, "clear", "Cleared");
|
|
773
|
+
announceSkips(
|
|
774
|
+
[...skipped, ...screened.refusals],
|
|
775
|
+
`Cleared ${screened.accepted.length} cells.`
|
|
776
|
+
);
|
|
777
|
+
}, [bounds, applyPatches, announceSkips, screenPatches]);
|
|
753
778
|
const doUndo = (0, import_react.useCallback)(async () => {
|
|
754
779
|
const taken = popUndo(stack);
|
|
755
780
|
if (!taken.entry) return;
|
|
@@ -834,6 +859,14 @@ function useSpreadsheetGrid(options) {
|
|
|
834
859
|
case "escape":
|
|
835
860
|
setRange(null);
|
|
836
861
|
return;
|
|
862
|
+
case "edit":
|
|
863
|
+
case "editSeeded": {
|
|
864
|
+
const focused = range?.focus;
|
|
865
|
+
if (!focused || !current.onEditRequest) return;
|
|
866
|
+
const seed = action.kind === "editSeeded" ? action.seed : null;
|
|
867
|
+
if (current.onEditRequest(focused, seed)) event.preventDefault();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
837
870
|
default:
|
|
838
871
|
return;
|
|
839
872
|
}
|
|
@@ -848,6 +881,13 @@ function useSpreadsheetGrid(options) {
|
|
|
848
881
|
);
|
|
849
882
|
return gridToTsv(grid);
|
|
850
883
|
}, [range, rowIds, columnIds]);
|
|
884
|
+
const selectionAddresses = (0, import_react.useCallback)(() => {
|
|
885
|
+
if (!range || !bounds) return { rowIds: [], columnIds: [] };
|
|
886
|
+
return {
|
|
887
|
+
rowIds: rowIds.slice(bounds.r0, bounds.r1 + 1),
|
|
888
|
+
columnIds: columnIds.slice(bounds.c0, bounds.c1 + 1)
|
|
889
|
+
};
|
|
890
|
+
}, [range, bounds, rowIds, columnIds]);
|
|
851
891
|
const onCopy = (0, import_react.useCallback)(
|
|
852
892
|
(event) => {
|
|
853
893
|
if (!optionsRef.current || !range || targetsAnEditor(event.target)) return;
|
|
@@ -903,11 +943,12 @@ function useSpreadsheetGrid(options) {
|
|
|
903
943
|
patches.push({ rowId, columnId, value: line[c] ?? null });
|
|
904
944
|
}
|
|
905
945
|
}
|
|
906
|
-
|
|
946
|
+
const screened = screenPatches(patches);
|
|
947
|
+
void applyPatches(screened.accepted, "paste", "Pasted");
|
|
907
948
|
const tail = outside > 0 ? ` ${outside} landed past the edge of the table and were ignored.` : "";
|
|
908
|
-
announceSkips(skipped, `Pasted ${
|
|
949
|
+
announceSkips([...skipped, ...screened.refusals], `Pasted ${screened.accepted.length} cells.${tail}`);
|
|
909
950
|
},
|
|
910
|
-
[range, rowIds, columnIds, applyPatches, announceSkips]
|
|
951
|
+
[range, rowIds, columnIds, applyPatches, announceSkips, screenPatches]
|
|
911
952
|
);
|
|
912
953
|
const cellProps = (0, import_react.useCallback)(
|
|
913
954
|
(rowId, columnId) => {
|
|
@@ -926,6 +967,10 @@ function useSpreadsheetGrid(options) {
|
|
|
926
967
|
onMouseDown: (event) => {
|
|
927
968
|
if (event.button !== 0) return;
|
|
928
969
|
event.preventDefault();
|
|
970
|
+
const origin = event.currentTarget;
|
|
971
|
+
if (typeof HTMLElement !== "undefined" && origin instanceof HTMLElement) {
|
|
972
|
+
origin.closest('[data-matrx-table-spreadsheet="true"]')?.focus({ preventScroll: true });
|
|
973
|
+
}
|
|
929
974
|
setRange(
|
|
930
975
|
(previous) => event.shiftKey && previous ? { anchor: previous.anchor, focus: address } : { anchor: address, focus: address }
|
|
931
976
|
);
|
|
@@ -987,12 +1032,13 @@ function useSpreadsheetGrid(options) {
|
|
|
987
1032
|
source: { top: source.r0, left: source.c0, bottom: source.r1, right: source.c1 },
|
|
988
1033
|
target
|
|
989
1034
|
});
|
|
990
|
-
|
|
991
|
-
|
|
1035
|
+
const screened = screenPatches(result.patches);
|
|
1036
|
+
void applyPatches(screened.accepted, "fill", "Filled");
|
|
1037
|
+
announceSkips([...result.skipped, ...screened.refusals], result.summary);
|
|
992
1038
|
};
|
|
993
1039
|
window.addEventListener("mouseup", commit);
|
|
994
1040
|
return () => window.removeEventListener("mouseup", commit);
|
|
995
|
-
}, [options, fillRequestBase, applyPatches, announceSkips]);
|
|
1041
|
+
}, [options, fillRequestBase, applyPatches, announceSkips, screenPatches]);
|
|
996
1042
|
return {
|
|
997
1043
|
range,
|
|
998
1044
|
bounds,
|
|
@@ -1008,7 +1054,9 @@ function useSpreadsheetGrid(options) {
|
|
|
1008
1054
|
isFillHandleCell,
|
|
1009
1055
|
startFillDrag,
|
|
1010
1056
|
canUndo: stack.past.length > 0,
|
|
1011
|
-
canRedo: stack.future.length > 0
|
|
1057
|
+
canRedo: stack.future.length > 0,
|
|
1058
|
+
selectionTsv: selectionAsTsv,
|
|
1059
|
+
selectionAddresses
|
|
1012
1060
|
};
|
|
1013
1061
|
}
|
|
1014
1062
|
//# sourceMappingURL=spreadsheet.cjs.map
|