intable 0.0.5 → 0.0.7

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.
Files changed (169) hide show
  1. package/README.md +1 -2
  2. package/docs/index-BaMALNy6.css +1 -0
  3. package/docs/index-CDN48t9E.js +3 -0
  4. package/docs/index-Cc4RNkLY.css +1 -0
  5. package/docs/index-MRnbkYmU.js +3 -0
  6. package/docs/index.html +15 -0
  7. package/docs/vite.svg +1 -0
  8. package/index.html +14 -0
  9. package/package.json +30 -37
  10. package/packages/intable/README.md +379 -0
  11. package/packages/intable/package.json +51 -0
  12. package/packages/intable/src/assets/ClearFormat.svg +3 -0
  13. package/packages/intable/src/assets/Forms.svg +4 -0
  14. package/packages/intable/src/assets/MergeCell.svg +4 -0
  15. package/packages/intable/src/assets/SplitCell.svg +4 -0
  16. package/packages/intable/src/assets/gap.svg +3 -0
  17. package/packages/intable/src/assets/loading.svg +12 -0
  18. package/packages/intable/src/assets/paint.svg +9 -0
  19. package/packages/intable/src/assets/solid.svg +1 -0
  20. package/packages/intable/src/components/Columns.tsx +86 -0
  21. package/packages/intable/src/components/DocTree.tsx +36 -0
  22. package/packages/intable/src/components/Menu.tsx +109 -0
  23. package/packages/intable/src/components/Popover.tsx +55 -0
  24. package/packages/intable/src/components/RecycleList.tsx +99 -0
  25. package/packages/intable/src/components/Render.tsx +26 -0
  26. package/packages/intable/src/components/Split.tsx +56 -0
  27. package/packages/intable/src/components/Tree.tsx +115 -0
  28. package/packages/intable/src/components/utils.tsx +12 -0
  29. package/packages/intable/src/hooks/index.ts +200 -0
  30. package/packages/intable/src/hooks/useDir.ts +78 -0
  31. package/packages/intable/src/hooks/useSelector.ts +91 -0
  32. package/packages/intable/src/hooks/useSort.tsx +118 -0
  33. package/packages/intable/src/hooks/useVirtualizer.ts +180 -0
  34. package/packages/intable/src/index.tsx +481 -0
  35. package/packages/intable/src/plugins/CellChangeHighlightPlugin.tsx +5 -0
  36. package/packages/intable/src/plugins/CellMergePlugin.tsx +153 -0
  37. package/packages/intable/src/plugins/CellSelectionPlugin.tsx +175 -0
  38. package/packages/intable/src/plugins/CommandPlugin.tsx +74 -0
  39. package/packages/intable/src/plugins/CopyPastePlugin.tsx +63 -0
  40. package/packages/intable/src/plugins/DiffPlugin.tsx +107 -0
  41. package/packages/intable/src/plugins/DragPlugin.tsx +81 -0
  42. package/packages/intable/src/plugins/EditablePlugin.tsx +252 -0
  43. package/packages/intable/src/plugins/ExpandPlugin.tsx +80 -0
  44. package/packages/intable/src/plugins/HeaderGroup.tsx +289 -0
  45. package/packages/intable/src/plugins/HistoryPlugin.tsx +49 -0
  46. package/packages/intable/src/plugins/MenuPlugin.tsx +195 -0
  47. package/packages/intable/src/plugins/RenderPlugin/components.tsx +51 -0
  48. package/packages/intable/src/plugins/RenderPlugin/index.tsx +81 -0
  49. package/packages/intable/src/plugins/ResizePlugin.tsx +122 -0
  50. package/packages/intable/src/plugins/RowGroupPlugin.tsx +122 -0
  51. package/packages/intable/src/plugins/RowSelectionPlugin.tsx +65 -0
  52. package/packages/intable/src/plugins/TreePlugin.tsx +212 -0
  53. package/packages/intable/src/plugins/VirtualScrollPlugin.tsx +190 -0
  54. package/packages/intable/src/plugins/ZodValidatorPlugin.tsx +61 -0
  55. package/packages/intable/src/style.scss +244 -0
  56. package/{dist → packages/intable/src}/theme/antd.scss +14 -5
  57. package/{dist → packages/intable/src}/theme/element-plus.scss +6 -5
  58. package/packages/intable/src/tree.ts +13 -0
  59. package/packages/intable/src/types/auto-imports.d.ts +13 -0
  60. package/packages/intable/src/utils.ts +122 -0
  61. package/packages/intable/src/wc.tsx +35 -0
  62. package/packages/intable/src/web-component.ts +1 -0
  63. package/packages/react/package.json +31 -0
  64. package/packages/react/src/index.ts +44 -0
  65. package/packages/react/src/plugins/antd.ts +94 -0
  66. package/packages/react/src/style.scss +12 -0
  67. package/packages/react/src/types/auto-imports.d.ts +10 -0
  68. package/packages/vue/package.json +34 -0
  69. package/packages/vue/src/index.ts +63 -0
  70. package/packages/vue/src/plugins/element-plus.ts +69 -0
  71. package/packages/vue/src/style.scss +12 -0
  72. package/packages/vue/src/types/auto-imports.d.ts +10 -0
  73. package/pnpm-workspace.yaml +2 -0
  74. package/public/vite.svg +1 -0
  75. package/scripts/build.js +184 -0
  76. package/scripts/publish.js +95 -0
  77. package/src/assets/ClearFormat.svg +3 -0
  78. package/src/assets/Forms.svg +4 -0
  79. package/src/assets/MergeCell.svg +4 -0
  80. package/src/assets/SplitCell.svg +4 -0
  81. package/src/assets/gap.svg +3 -0
  82. package/src/assets/loading.svg +12 -0
  83. package/src/assets/paint.svg +9 -0
  84. package/src/assets/solid.svg +1 -0
  85. package/src/demo-vue.ts +54 -0
  86. package/src/demo.tsx +107 -0
  87. package/src/index.scss +105 -0
  88. package/src/styles/index.scss +172 -0
  89. package/src/types/auto-imports.d.ts +13 -0
  90. package/stats.html +4949 -0
  91. package/tsconfig.app.json +34 -0
  92. package/tsconfig.json +7 -0
  93. package/tsconfig.node.json +26 -0
  94. package/vite.config.ts +63 -0
  95. package/dist/__uno.css +0 -1
  96. package/dist/chevron-right.js +0 -6
  97. package/dist/components/Columns.d.ts +0 -3
  98. package/dist/components/Columns.js +0 -71
  99. package/dist/components/DocTree.d.ts +0 -4
  100. package/dist/components/DocTree.js +0 -32
  101. package/dist/components/Menu.d.ts +0 -1
  102. package/dist/components/Menu.js +0 -107
  103. package/dist/components/Popover.d.ts +0 -14
  104. package/dist/components/Popover.js +0 -41
  105. package/dist/components/Render.d.ts +0 -4
  106. package/dist/components/Render.js +0 -20
  107. package/dist/components/Split.d.ts +0 -15
  108. package/dist/components/Split.js +0 -76
  109. package/dist/components/Tree.d.ts +0 -37
  110. package/dist/components/Tree.js +0 -82
  111. package/dist/components/utils.d.ts +0 -3
  112. package/dist/components/utils.js +0 -8
  113. package/dist/hooks/index.d.ts +0 -40
  114. package/dist/hooks/index.js +0 -157
  115. package/dist/hooks/useDir.d.ts +0 -11
  116. package/dist/hooks/useDir.js +0 -42
  117. package/dist/hooks/useSelector.d.ts +0 -16
  118. package/dist/hooks/useSelector.js +0 -35
  119. package/dist/hooks/useSort.d.ts +0 -18
  120. package/dist/hooks/useSort.js +0 -83
  121. package/dist/hooks/useVirtualizer.d.ts +0 -25
  122. package/dist/hooks/useVirtualizer.js +0 -67
  123. package/dist/index.d.ts +0 -130
  124. package/dist/index.js +0 -347
  125. package/dist/loading.js +0 -6
  126. package/dist/plugins/CellChangeHighlightPlugin.d.ts +0 -2
  127. package/dist/plugins/CellChangeHighlightPlugin.js +0 -4
  128. package/dist/plugins/CellMergePlugin.d.ts +0 -12
  129. package/dist/plugins/CellMergePlugin.js +0 -2
  130. package/dist/plugins/CellSelectionPlugin.d.ts +0 -15
  131. package/dist/plugins/CellSelectionPlugin.js +0 -115
  132. package/dist/plugins/CommandPlugin.d.ts +0 -14
  133. package/dist/plugins/CommandPlugin.js +0 -12
  134. package/dist/plugins/CopyPastePlugin.d.ts +0 -14
  135. package/dist/plugins/CopyPastePlugin.js +0 -42
  136. package/dist/plugins/DiffPlugin.d.ts +0 -23
  137. package/dist/plugins/DiffPlugin.js +0 -56
  138. package/dist/plugins/DragPlugin.d.ts +0 -14
  139. package/dist/plugins/DragPlugin.js +0 -47
  140. package/dist/plugins/EditablePlugin.d.ts +0 -48
  141. package/dist/plugins/EditablePlugin.js +0 -141
  142. package/dist/plugins/ExpandPlugin.d.ts +0 -18
  143. package/dist/plugins/ExpandPlugin.js +0 -50
  144. package/dist/plugins/HistoryPlugin.d.ts +0 -10
  145. package/dist/plugins/HistoryPlugin.js +0 -30
  146. package/dist/plugins/MenuPlugin.d.ts +0 -18
  147. package/dist/plugins/MenuPlugin.js +0 -107
  148. package/dist/plugins/RenderPlugin/components.d.ts +0 -5
  149. package/dist/plugins/RenderPlugin/components.js +0 -87
  150. package/dist/plugins/RenderPlugin/index.d.ts +0 -30
  151. package/dist/plugins/RenderPlugin/index.js +0 -49
  152. package/dist/plugins/ResizePlugin.d.ts +0 -27
  153. package/dist/plugins/ResizePlugin.js +0 -81
  154. package/dist/plugins/RowGroupPlugin.d.ts +0 -17
  155. package/dist/plugins/RowGroupPlugin.js +0 -83
  156. package/dist/plugins/RowSelectionPlugin.d.ts +0 -20
  157. package/dist/plugins/RowSelectionPlugin.js +0 -42
  158. package/dist/plugins/VirtualScrollPlugin.d.ts +0 -15
  159. package/dist/plugins/VirtualScrollPlugin.js +0 -96
  160. package/dist/plus.js +0 -6
  161. package/dist/style.css +0 -3
  162. package/dist/types/auto-imports.d.js +0 -0
  163. package/dist/utils.d.ts +0 -30
  164. package/dist/utils.js +0 -70
  165. package/dist/wc.d.ts +0 -1
  166. package/dist/wc.js +0 -21
  167. package/dist/web-component.d.ts +0 -1
  168. package/dist/web-component.js +0 -2
  169. package/dist/x.js +0 -6
@@ -0,0 +1,252 @@
1
+ import { createComputed, createEffect, createMemo, createRoot, createSignal, on, onCleanup, useContext, type Component, type JSX } from 'solid-js'
2
+ import { combineProps } from '@solid-primitives/props'
3
+ import { createAsyncMemo } from '@solid-primitives/memo'
4
+ import { delay } from 'es-toolkit'
5
+ import { createMutable } from 'solid-js/store'
6
+ import { Ctx, type Plugin, type TableColumn } from '..'
7
+ import { Checkbox, Files } from './RenderPlugin/components'
8
+ import { chooseFile, resolveOptions } from '../utils'
9
+
10
+ declare module '../index' {
11
+ interface TableProps {
12
+ validator?: (value: any, data: any, col: TableColumn) => string | boolean | Promise<string | boolean>
13
+ }
14
+ interface TableColumn {
15
+ editable?: boolean
16
+ editor?: string | Editor
17
+ editorProps?: any
18
+ editorPopup?: boolean // todo
19
+ validator?: (value: any, rowData: any, col: TableColumn) => boolean | string | Promise<boolean | string>
20
+ }
21
+ interface TableStore {
22
+ editors: { [key: string]: Editor }
23
+ }
24
+ }
25
+
26
+ export type Editor = (props: EditorOpt) => {
27
+ el: JSX.Element
28
+ getValue: () => any
29
+ destroy: () => void
30
+ focus?: () => void
31
+ blur?: () => void
32
+ }
33
+
34
+ export interface EditorOpt {
35
+ col: TableColumn
36
+ data: any
37
+ value: any
38
+ eventKey?: string
39
+ onChange?: (value: any) => void
40
+ ok: () => void
41
+ cancel: () => void
42
+ props?: any
43
+ }
44
+
45
+ export const EditablePlugin: Plugin = {
46
+ name: 'editable',
47
+ store: () => ({
48
+ editors: { ...editors }
49
+ }),
50
+ rewriteProps: {
51
+ Td: ({ Td }, { store }) => o => {
52
+ let el!: HTMLElement
53
+ const { props } = useContext(Ctx)
54
+ const editable = createMemo(() => !!o.col.editable && !o.data[store.internal] && !o.col[store.internal])
55
+ const [editing, setEditing] = createSignal(false)
56
+ let eventKey = ''
57
+
58
+ const selected = createMemo(() => (([x, y]) => o.x == x && o.y == y)(store.selected.start || []))
59
+
60
+ const preEdit = createMemo(() => selected() && editable() && !editing())
61
+
62
+ const [validationError, setValidationError] = createSignal<string | null>(null)
63
+ const [validating, setValidating] = createSignal(false)
64
+ createEffect(() => { if (!editing()) { setValidationError(null); setValidating(false) } })
65
+
66
+ const editorState = createAsyncMemo(async () => {
67
+ if (editing()) {
68
+ let canceled = false
69
+ const editor = (editor => typeof editor == 'string' ? store.editors[editor] : editor)(o.col.editor || 'text')
70
+ const opt = {
71
+ props: o.col.editorProps,
72
+ col: o.col,
73
+ eventKey,
74
+ data: o.data,
75
+ value: o.data[o.col.id],
76
+ ok: async () => {
77
+ await validate(ret.getValue())
78
+ setEditing(false)
79
+ },
80
+ cancel: () => (canceled = true, setValidationError(null), setEditing(false)),
81
+ onChange: v => validate(v).catch(() => {}) // Validate on each change but ignore errors until final submission
82
+ }
83
+ const ret = editor(opt)
84
+ onCleanup(() => {
85
+ if (!canceled && ret.getValue() !== o.data[o.col.id]) {
86
+ const arr = [...props.data!]
87
+ arr[o.y] = { ...arr[o.y], [o.col.id]: ret.getValue() }
88
+ props.onDataChange?.(arr)
89
+ }
90
+ if (!canceled) {
91
+ validate(ret.getValue())
92
+ }
93
+ ret.destroy()
94
+ })
95
+ return [opt, ret] as const
96
+ }
97
+ })
98
+
99
+ async function validate(value) {
100
+ if (props.validator || o.col.validator) {
101
+ try {
102
+ setValidating(true)
103
+ const result = await (async () => {
104
+ for (const v of [props.validator, o.col.validator]) {
105
+ if (!v) continue
106
+ const r = await v(value, o.data, o.col)
107
+ if (r !== true) return r
108
+ }
109
+ return true
110
+ })()
111
+ setValidating(false)
112
+ if (result !== true) {
113
+ setValidationError(typeof result === 'string' ? result : 'Error')
114
+ } else {
115
+ setValidationError(null)
116
+ }
117
+ } catch (e) {
118
+ setValidating(false)
119
+ setValidationError((e as Error).message || 'Error')
120
+ }
121
+ if (validationError() != null) throw new Error(validationError() || 'Error')
122
+ }
123
+ }
124
+
125
+ createEffect(() => {
126
+ editorState()?.[1]?.focus?.()
127
+ })
128
+
129
+ createEffect(() => {
130
+ if (editing()) {
131
+ const sss = createMemo(() => JSON.stringify(store.selected))
132
+ createEffect(on(sss, () => setEditing(false), { defer: true }))
133
+ }
134
+ })
135
+
136
+ let input: HTMLInputElement
137
+ const size = createMutable({ w: 0, h: 0 })
138
+ createComputed(() => editing() && (size.w = el.getBoundingClientRect().width, size.h = el.getBoundingClientRect().height))
139
+
140
+ o = combineProps(o, {
141
+ ref: v => el = v,
142
+ get class() { return [editing() ? 'is-editing' : '', validationError() !== null ? 'is-invalid' : ''].filter(Boolean).join(' ') },
143
+ get style() { return editing() ? `width: ${size.w}px; height: ${size.h}px; padding: 0; ` : '' },
144
+ onClick: () => input?.focus?.(),
145
+ onDblClick: () => setEditing(editable()),
146
+ onKeyDown: e => e.key == 'Escape' && editorState()?.[0].cancel()
147
+ } as JSX.HTMLAttributes<any>)
148
+
149
+ return (
150
+ <Td {...o}>
151
+ {preEdit() &&
152
+ <input
153
+ style='position: absolute; margin-top: 1em; width: 0; height: 0; pointer-events: none; opacity: 0'
154
+ ref={e => { input = e; delay(0).then(() => e.focus({ preventScroll: true })) }}
155
+ onKeyDown={e => {
156
+ e.key == ' ' && e.preventDefault()
157
+ }}
158
+ onInput={e => {
159
+ eventKey = e.target.value
160
+ setEditing(!e.isComposing)
161
+ }}
162
+ onCompositionEnd={() => {
163
+ setEditing(true)
164
+ }}
165
+ />
166
+ }
167
+ {editorState()?.[1]?.el
168
+ ? <div class='in-cell-edit-wrapper'>
169
+ {editorState()?.[1]?.el}
170
+ {validating() && <span class='cell-validating' />}
171
+ </div>
172
+ : o.children
173
+ }
174
+ {validationError() !== null &&
175
+ <div class='cell-validation-error'>
176
+ {validationError()}
177
+ </div>
178
+ }
179
+ </Td>
180
+ )
181
+ }
182
+ }
183
+ }
184
+
185
+ const createEditor = (Comp: Component<any>, extra?, isSelector?): Editor => (
186
+ ({ eventKey, value, col, ok, cancel, props, onChange }) => createRoot(destroy => {
187
+ const [v, setV] = createSignal(eventKey || value)
188
+ let el!: HTMLElement
189
+ ;(<Comp
190
+ ref={e => el = e}
191
+ class='relative block px-2 size-full z-9 box-border resize-none outline-0'
192
+ value={v()}
193
+ onInput={e => (setV(e instanceof Event ? e.target.value : e), onChange?.(v()))}
194
+ onChange={e => (setV(e instanceof Event ? e.target.value : e), onChange?.(v()), isSelector && ok())}
195
+ on:pointerdown={e => e.stopPropagation()}
196
+ on:keydown={e => {
197
+ e.stopPropagation()
198
+ e.key == 'Enter' && ok()
199
+ e.key == 'Escape' && cancel()
200
+ }}
201
+ options={col.enum ? resolveOptions(col.enum ?? []) : undefined}
202
+ {...extra}
203
+ {...props}
204
+ />)
205
+
206
+ return {
207
+ el,
208
+ getValue: v,
209
+ focus: () => el.focus(),
210
+ destroy,
211
+ }
212
+ })
213
+ )
214
+
215
+ const Input = o => <input {...o} />
216
+
217
+ const text = createEditor(Input)
218
+ const number = createEditor(Input, { type: 'number' })
219
+ const range = createEditor(Input, { type: 'range' })
220
+ const color = createEditor(Input, { type: 'color' })
221
+ const tel = createEditor(Input, { type: 'tel' })
222
+ const password = createEditor(Input, { type: 'password' })
223
+ const date = createEditor(Input, { type: 'date' }, true)
224
+ const time = createEditor(Input, { type: 'time' }, true)
225
+ const datetime = createEditor(Input, { type: 'datetime-local' }, true)
226
+ const select = createEditor(o => <select {...o}>{o.options?.map(e => <option value={e.value}>{e.label}</option>)}</select>, {}, true)
227
+
228
+ const file = createEditor(o => {
229
+ const onAdd = () => chooseFile({ multiple: true }).then(files => o.onChange([...o.value || [], ...files.map(e => ({ name: e.name, size: e.size }))]))
230
+ return <Files {...o} class='relative z-9 outline-(2 blue) min-h-a! h-a! p-1 bg-#fff' onAdd={onAdd} />
231
+ })
232
+
233
+ const checkbox = createEditor(o => (
234
+ <label ref={o.ref} class='h-full flex items-center'>
235
+ <Checkbox {...o} ref={() => {}} onInput={() => {}} class='mx-3!' />
236
+ </label>
237
+ ))
238
+
239
+ export const editors = {
240
+ text,
241
+ number,
242
+ range,
243
+ date,
244
+ time,
245
+ datetime,
246
+ color,
247
+ tel,
248
+ password,
249
+ file,
250
+ checkbox,
251
+ select,
252
+ }
@@ -0,0 +1,80 @@
1
+ import { createEffect, createMemo, useContext } from 'solid-js'
2
+ import type { Component, JSX } from 'solid-js'
3
+ import { defaultsDeep } from 'es-toolkit/compat'
4
+ import { Ctx, type Plugin, type TableColumn, type TableProps, type TableStore } from '..'
5
+ import { renderComponent, solidComponent } from '../components/utils'
6
+ import { useSelector } from '../hooks/useSelector'
7
+ import { combineProps } from '@solid-primitives/props'
8
+ import { log } from '../utils'
9
+
10
+ declare module '../index' {
11
+ interface TableProps {
12
+ expand?: {
13
+ enable?: boolean // todo
14
+ render?: (props: { data: any, y: number }) => JSX.Element
15
+ }
16
+ }
17
+ interface Commands {
18
+ expand: ReturnType<typeof useSelector<any[]>>
19
+ }
20
+ }
21
+
22
+ export const ExpandPlugin: Plugin = {
23
+ name: 'expand',
24
+
25
+ store: (store) => ({
26
+ expandCol: {
27
+ id: Symbol('expand'),
28
+ fixed: 'left',
29
+ width: 45,
30
+ render: solidComponent((o) => <ArrowCell store={store} data={o.data} />),
31
+ // todo
32
+ props: o => ({ onClick: () => store.commands.expand.toggle(o.data) }),
33
+ [store.internal]: 1
34
+ } as TableColumn,
35
+ }),
36
+
37
+ commands: (store) => ({
38
+ expand: useSelector({ multiple: true })
39
+ }),
40
+
41
+ rewriteProps: {
42
+ expand: ({ expand }) => defaultsDeep(expand, {
43
+ enable: false,
44
+ } as TableProps['expand']),
45
+
46
+ columns: ({ columns }, { store }) => store.props.expand?.enable
47
+ ? [store.expandCol, ...columns]
48
+ : columns,
49
+
50
+ Tr: ({ Tr }, { store }) => store.props.expand?.enable ? o => {
51
+ return (
52
+ <Tr {...o}>{
53
+ !o.data?.[store.expandCol.id] ? o.children :
54
+ <td colspan={store.props.columns?.length} style='width: 100%'>
55
+ {renderComponent(store.props.expand?.render, { ...o, data: o.data[store.expandCol.id] }, store.props.renderer)}
56
+ </td>
57
+ }</Tr>
58
+ )
59
+ } : Tr,
60
+
61
+ Td: ({ Td }, { store }) => o => {
62
+ o = combineProps(o, { onClick: () => o.col.id == store.expandCol.id && store.commands.expand.toggle(o.data) })
63
+ return <Td {...o} />
64
+ },
65
+
66
+ data: ({ data }, { store }) => (
67
+ store.commands.expand.value.length
68
+ ? data?.flatMap(e => store.commands.expand.has(e) ? [e, { [store.expandCol.id]: e }] : e)
69
+ : data
70
+ )
71
+ }
72
+ }
73
+
74
+ const ArrowCell: Component<{ data: any, store: TableStore }> = ({ data, store }) => {
75
+ return (
76
+ <div style='display: flex; align-items: center; width: 100%; height: 100%; opacity: .4'>
77
+ <ILucideChevronRight style={`transform: rotate(${store.commands.expand.has(data) ? 90 : 0}deg);`} />
78
+ </div>
79
+ )
80
+ }
@@ -0,0 +1,289 @@
1
+ import { For, Show, useContext, createMemo, createSignal, createEffect } from 'solid-js'
2
+ import { Ctx, type Plugin$0, type TableColumn } from '..'
3
+
4
+ declare module '../index' {
5
+ interface TableColumn {
6
+ children?: TableColumn[]
7
+ }
8
+ interface TableStore {
9
+ /** Returns column indices of group anchors whose span overlaps [xStart, xEnd]. */
10
+ _headerGroupAnchors?: (xStart: number, xEnd: number) => number[]
11
+ }
12
+ }
13
+
14
+ // ── Helpers ──────────────────────────────────────────────────────────────
15
+
16
+ /** Count the number of leaf columns under a node (colspan). */
17
+ function leafCount(col: TableColumn): number {
18
+ if (!col.children?.length) return 1
19
+ return col.children.reduce((n, c) => n + leafCount(c), 0)
20
+ }
21
+
22
+ /** Calculate the maximum depth of the column tree. */
23
+ function maxDepth(columns: TableColumn[]): number {
24
+ let d = 0
25
+ for (const col of columns) {
26
+ if (col.children?.length) {
27
+ d = Math.max(d, maxDepth(col.children))
28
+ }
29
+ }
30
+ return d + 1
31
+ }
32
+
33
+ /** Collect all leaf columns in tree order (these are what the body renders). */
34
+ function flatLeaves(columns: TableColumn[], out: TableColumn[] = []): TableColumn[] {
35
+ for (const col of columns) {
36
+ if (col.children?.length) {
37
+ flatLeaves(col.children, out)
38
+ } else {
39
+ out.push(col)
40
+ }
41
+ }
42
+ return out
43
+ }
44
+
45
+ // ── Flat grid for flex-compatible header rows ────────────────────────────
46
+
47
+ type CellKind = 'anchor' | 'colspan-hidden' | 'rowspan-hidden'
48
+
49
+ interface GridCell {
50
+ kind: CellKind
51
+ /** The column object that owns this cell (group col for anchors, leaf col for placeholders) */
52
+ anchorCol: TableColumn
53
+ colspan: number
54
+ rowspan: number
55
+ }
56
+
57
+ /**
58
+ * Build a 2-D grid `[row][colIdx]` where `colIdx` maps 1:1 with `allCols`
59
+ * (i.e. `props.columns` which includes internal + leaf columns).
60
+ *
61
+ * Every row has exactly `allCols.length` entries so flex rows stay aligned.
62
+ */
63
+ function buildFlatGrid(
64
+ rawCols: TableColumn[],
65
+ totalDepth: number,
66
+ allCols: TableColumn[],
67
+ rawToIdx: Map<TableColumn, number>,
68
+ rawLeaves: TableColumn[],
69
+ store: any,
70
+ ): (GridCell | null)[][] {
71
+
72
+ const width = allCols.length
73
+ const grid: (GridCell | null)[][] = Array.from({ length: totalDepth }, () =>
74
+ new Array(width).fill(null),
75
+ )
76
+
77
+ let leafOffset = 0
78
+
79
+ function walk(cols: TableColumn[], depth: number) {
80
+ for (const col of cols) {
81
+ const hasChildren = !!col.children?.length
82
+ const lc = leafCount(col)
83
+ const rs = hasChildren ? 1 : totalDepth - depth
84
+
85
+ // Find starting index in allCols
86
+ const anchorLeaf = hasChildren ? rawLeaves[leafOffset] : col
87
+ const startIdx = rawToIdx.get(anchorLeaf)
88
+ if (startIdx == null) { if (!hasChildren) leafOffset++; continue }
89
+
90
+ // Anchor cell
91
+ grid[depth][startIdx] = { kind: 'anchor', anchorCol: col, colspan: lc, rowspan: rs }
92
+
93
+ // Colspan-covered (same row, to the right)
94
+ for (let dx = 1; dx < lc; dx++) {
95
+ grid[depth][startIdx + dx] = { kind: 'colspan-hidden', anchorCol: col, colspan: lc, rowspan: rs }
96
+ }
97
+
98
+ // Rowspan-covered (subsequent rows)
99
+ for (let dy = 1; dy < rs; dy++) {
100
+ for (let dx = 0; dx < lc; dx++) {
101
+ grid[depth + dy][startIdx + dx] = { kind: 'rowspan-hidden', anchorCol: allCols[startIdx + dx], colspan: 1, rowspan: 1 }
102
+ }
103
+ }
104
+
105
+ if (hasChildren) {
106
+ walk(col.children!, depth + 1)
107
+ } else {
108
+ leafOffset++
109
+ }
110
+ }
111
+ }
112
+
113
+ leafOffset = 0
114
+ walk(rawCols, 0)
115
+
116
+ // Fill positions for internal columns (index, expand, row-selection…)
117
+ for (let i = 0; i < width; i++) {
118
+ if (grid[0][i] != null) continue
119
+ const col = allCols[i]
120
+ if (col[store.internal]) {
121
+ grid[0][i] = { kind: 'anchor', anchorCol: col, colspan: 1, rowspan: totalDepth }
122
+ for (let dy = 1; dy < totalDepth; dy++) {
123
+ grid[dy][i] = { kind: 'rowspan-hidden', anchorCol: col, colspan: 1, rowspan: 1 }
124
+ }
125
+ }
126
+ }
127
+
128
+ return grid
129
+ }
130
+
131
+ // ── Plugin ───────────────────────────────────────────────────────────────
132
+
133
+ export const HeaderGroupPlugin: Plugin$0 = {
134
+ name: 'header-group',
135
+
136
+ store: (store) => ({
137
+ /**
138
+ * Called inside VirtualScrollPlugin's X-virtualizer extras callback.
139
+ * Returns the column indices of every group anchor whose span overlaps [xStart, xEnd]
140
+ * so that group header cells are always rendered even when their anchor column has
141
+ * scrolled out of the visible window.
142
+ */
143
+ _headerGroupAnchors(xStart: number, xEnd: number): number[] {
144
+ const rawCols = store.rawProps.columns || []
145
+ if (!rawCols.some(c => (c as TableColumn).children?.length)) return []
146
+ if (maxDepth(rawCols) <= 1) return []
147
+
148
+ const allCols = store.props.columns || []
149
+ const rawLeaves = flatLeaves(rawCols)
150
+
151
+ const rawToIdx = new Map<TableColumn, number>()
152
+ for (let i = 0; i < allCols.length; i++) {
153
+ rawToIdx.set(allCols[i][store.raw] || allCols[i], i)
154
+ }
155
+
156
+ const anchors: number[] = []
157
+ let leafOffset = 0
158
+
159
+ function walk(cols: TableColumn[]) {
160
+ for (const col of cols) {
161
+ if (col.children?.length) {
162
+ const lc = leafCount(col)
163
+ const anchorLeaf = rawLeaves[leafOffset]
164
+ if (anchorLeaf) {
165
+ const si = rawToIdx.get(anchorLeaf)
166
+ if (si != null) {
167
+ const ei = si + lc - 1
168
+ if (si <= xEnd && ei >= xStart) anchors.push(si)
169
+ }
170
+ }
171
+ walk(col.children!)
172
+ } else {
173
+ leafOffset++
174
+ }
175
+ }
176
+ }
177
+
178
+ walk(rawCols)
179
+ return anchors
180
+ },
181
+ }),
182
+
183
+ rewriteProps: {
184
+ /**
185
+ * Flatten nested column definitions into leaf-only columns for the body.
186
+ * The header rendering is handled entirely by the Thead rewrite.
187
+ */
188
+ columns: ({ columns }) => flatLeaves(columns),
189
+
190
+ /**
191
+ * Replace the default single-row <Thead> with a multi-row header
192
+ * using a flat grid: every row has exactly `props.columns.length` entries
193
+ * so that flex-based virtual scroll renders correct alignment.
194
+ *
195
+ * Cell types:
196
+ * - anchor: visible cell with content, colspan/rowspan props
197
+ * - colspan-hidden: covered by an anchor to the left (display:none)
198
+ * - rowspan-hidden: covered by an anchor above (visibility:hidden in
199
+ * flex mode to preserve width, display:none in table mode)
200
+ */
201
+ Thead: ({ Thead }, { store }) => o => {
202
+ const { props } = useContext(Ctx)
203
+
204
+ const gridData = createMemo(() => {
205
+ const rawCols = store.rawProps.columns || []
206
+ const depth = maxDepth(rawCols)
207
+ if (depth <= 1) return null
208
+
209
+ const allCols = props.columns || []
210
+ const rawLeaves = flatLeaves(rawCols)
211
+
212
+ // Map raw column identity → index in allCols (props.columns)
213
+ const rawToIdx = new Map<TableColumn, number>()
214
+ for (let i = 0; i < allCols.length; i++) {
215
+ rawToIdx.set(allCols[i][store.raw] || allCols[i], i)
216
+ }
217
+
218
+ const grid = buildFlatGrid(rawCols, depth, allCols, rawToIdx, rawLeaves, store)
219
+ return { grid, depth }
220
+ })
221
+
222
+ // Measure header row height for rowspan overflow in flex mode.
223
+ // The last row (all leaf cells) gives the base height.
224
+ const [headerRowH, setHeaderRowH] = createSignal(24)
225
+ createEffect(() => {
226
+ const thead = store.thead
227
+ if (!thead) return
228
+ const trs = thead.querySelectorAll(':scope > tr')
229
+ const lastTr = trs[trs.length - 1] as HTMLElement | undefined
230
+ if (lastTr) setHeaderRowH(lastTr.offsetHeight || 24)
231
+ })
232
+
233
+ const isVirtual = () => !!store.virtualizerX
234
+
235
+ return (
236
+ <Show when={gridData()} fallback={<Thead {...o} />}>
237
+ {data => (
238
+ <Thead {...o}>
239
+ <For each={data().grid}>{(row, rowIdx) => (
240
+ <props.Tr
241
+ style={isVirtual()
242
+ ? `height:${headerRowH()}px;overflow:visible;position:relative;z-index:${data().depth - rowIdx()}`
243
+ : ''}
244
+ >
245
+ <props.EachCells each={props.columns}>
246
+ {(col, colIdx) => {
247
+ const cell = () => row[colIdx()]
248
+ if (!cell()) return null
249
+
250
+ const style = () => {
251
+ const c = cell()!
252
+ if (c.kind === 'anchor') {
253
+ // In flex mode, rowspan cells need explicit height to overflow into rows below
254
+ if (isVirtual() && c.rowspan > 1 && headerRowH()) {
255
+ return `height:${c.rowspan * headerRowH()}px;position:relative;`
256
+ }
257
+ return ''
258
+ }
259
+ if (c.kind === 'colspan-hidden') return 'display:none'
260
+ // rowspan-hidden: in flex mode keep width for alignment; in table mode hide completely
261
+ if (c.kind === 'rowspan-hidden') return isVirtual() ? 'visibility:hidden' : 'display:none'
262
+ return ''
263
+ }
264
+
265
+ const isAnchor = () => cell()!.kind === 'anchor'
266
+
267
+ return (
268
+ <props.Th
269
+ x={colIdx()}
270
+ col={isAnchor() ? cell()!.anchorCol : col()}
271
+ colspan={isAnchor() && cell()!.colspan > 1 ? cell()!.colspan : undefined}
272
+ rowspan={isAnchor() && cell()!.rowspan > 1 ? cell()!.rowspan : undefined}
273
+ style={style()}
274
+ covered={!isAnchor()}
275
+ >
276
+ {isAnchor() ? cell()!.anchorCol.name : ''}
277
+ </props.Th>
278
+ )
279
+ }}
280
+ </props.EachCells>
281
+ </props.Tr>
282
+ )}</For>
283
+ </Thead>
284
+ )}
285
+ </Show>
286
+ )
287
+ },
288
+ },
289
+ }
@@ -0,0 +1,49 @@
1
+ import { createMemo } from 'solid-js'
2
+ import { unwrap } from 'solid-js/store'
3
+ import { captureStoreUpdates } from '@solid-primitives/deep'
4
+ import { combineProps } from '@solid-primitives/props'
5
+ import { useHistory } from '../hooks'
6
+ import { type Plugin } from '..'
7
+
8
+ declare module '../index' {
9
+ interface TableProps {
10
+
11
+ }
12
+ interface TableStore {
13
+ history: ReturnType<typeof useHistory>
14
+ }
15
+ }
16
+
17
+ export const HistoryPlugin: Plugin = {
18
+ store: (store) => {
19
+ const getDelta = createMemo(() => captureStoreUpdates(store.rawProps.data || []))
20
+ let clonedState
21
+ return ({
22
+ history: useHistory([() => {
23
+ const delta = getDelta()()
24
+ if (!delta.length) return
25
+
26
+ for (const { path, value } of delta) {
27
+ if (path.length == 0) {
28
+ clonedState = structuredClone(unwrap(value))
29
+ // clonedState = [...value]
30
+ } else {
31
+ const target = [...clonedState]
32
+ path.reduce((o, k, i) => o[k] = i < path.length -1 ? Array.isArray(o[k]) ? [...o[k]] : { ...o[k] } : structuredClone(unwrap(value)), target)
33
+ clonedState = target
34
+ }
35
+ }
36
+ return clonedState
37
+ }, v => store.props!.onDataChange?.(v)])
38
+ })
39
+ },
40
+ keybindings: (store) => ({
41
+ '$mod+Z': () => store.history.undo(),
42
+ '$mod+Y': () => store.history.redo(),
43
+ }),
44
+ rewriteProps: {
45
+ tdProps: ({ tdProps }, { store }) => o => combineProps(tdProps?.(o) || {}, {
46
+ // get style() { return o.data[o.col.id] != store.unsaveData[o.y]?.[o.col.id] ? `background: #80808030` : `` }
47
+ })
48
+ },
49
+ }