intable 0.0.6 → 0.0.8
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/.github/copilot-instructions.md +102 -0
- package/README.md +16 -263
- package/docs/index-BaMALNy6.css +1 -0
- package/docs/index-CDN48t9E.js +3 -0
- package/docs/index-Cc4RNkLY.css +1 -0
- package/docs/index-MRnbkYmU.js +3 -0
- package/docs/index.html +15 -0
- package/docs/vite.svg +1 -0
- package/index.html +13 -0
- package/package.json +35 -38
- package/packages/intable/README.md +379 -0
- package/packages/intable/package.json +51 -0
- package/packages/intable/src/assets/ClearFormat.svg +3 -0
- package/packages/intable/src/assets/Forms.svg +4 -0
- package/packages/intable/src/assets/MergeCell.svg +4 -0
- package/packages/intable/src/assets/SplitCell.svg +4 -0
- package/packages/intable/src/assets/gap.svg +3 -0
- package/packages/intable/src/assets/loading.svg +12 -0
- package/packages/intable/src/assets/paint.svg +9 -0
- package/packages/intable/src/assets/solid.svg +1 -0
- package/packages/intable/src/components/Columns.tsx +86 -0
- package/packages/intable/src/components/DocTree.tsx +36 -0
- package/packages/intable/src/components/Menu.tsx +109 -0
- package/packages/intable/src/components/Popover.tsx +55 -0
- package/packages/intable/src/components/RecycleList.tsx +99 -0
- package/packages/intable/src/components/Render.tsx +26 -0
- package/packages/intable/src/components/Split.tsx +56 -0
- package/packages/intable/src/components/Tree.tsx +115 -0
- package/packages/intable/src/components/utils.tsx +12 -0
- package/packages/intable/src/hooks/index.ts +200 -0
- package/packages/intable/src/hooks/useDir.ts +78 -0
- package/packages/intable/src/hooks/useSelector.ts +91 -0
- package/packages/intable/src/hooks/useSort.tsx +118 -0
- package/packages/intable/src/hooks/useVirtualizer.ts +180 -0
- package/packages/intable/src/index.tsx +489 -0
- package/packages/intable/src/plugins/CellChangeHighlightPlugin.tsx +5 -0
- package/packages/intable/src/plugins/CellMergePlugin.tsx +153 -0
- package/packages/intable/src/plugins/CellSelectionPlugin.tsx +175 -0
- package/packages/intable/src/plugins/CommandPlugin.tsx +74 -0
- package/packages/intable/src/plugins/CopyPastePlugin.tsx +99 -0
- package/packages/intable/src/plugins/DiffPlugin.tsx +120 -0
- package/packages/intable/src/plugins/DragPlugin.tsx +81 -0
- package/packages/intable/src/plugins/EditablePlugin.tsx +252 -0
- package/packages/intable/src/plugins/ExpandPlugin.tsx +80 -0
- package/packages/intable/src/plugins/HeaderGroup.tsx +289 -0
- package/packages/intable/src/plugins/HistoryPlugin.tsx +49 -0
- package/packages/intable/src/plugins/MenuPlugin.tsx +195 -0
- package/packages/intable/src/plugins/RenderPlugin/components.tsx +51 -0
- package/packages/intable/src/plugins/RenderPlugin/index.tsx +81 -0
- package/packages/intable/src/plugins/ResizePlugin.tsx +122 -0
- package/packages/intable/src/plugins/RowGroupPlugin.tsx +122 -0
- package/packages/intable/src/plugins/RowSelectionPlugin.tsx +65 -0
- package/packages/intable/src/plugins/TreePlugin.tsx +212 -0
- package/packages/intable/src/plugins/VirtualScrollPlugin.tsx +190 -0
- package/packages/intable/src/plugins/ZodValidatorPlugin.tsx +61 -0
- package/packages/intable/src/style.scss +244 -0
- package/{dist → packages/intable/src}/theme/antd.scss +14 -5
- package/packages/intable/src/theme/dark.scss +46 -0
- package/{dist → packages/intable/src}/theme/element-plus.scss +6 -5
- package/packages/intable/src/theme/github.scss +80 -0
- package/packages/intable/src/theme/material.scss +73 -0
- package/packages/intable/src/theme/shadcn.scss +66 -0
- package/packages/intable/src/theme/stripe.scss +57 -0
- package/packages/intable/src/tree.ts +13 -0
- package/packages/intable/src/types/auto-imports.d.ts +13 -0
- package/packages/intable/src/utils.ts +122 -0
- package/packages/intable/src/wc.tsx +35 -0
- package/packages/intable/src/web-component.ts +1 -0
- package/packages/react/package.json +36 -0
- package/packages/react/src/index.ts +44 -0
- package/packages/react/src/plugins/antd.ts +94 -0
- package/packages/react/src/style.scss +12 -0
- package/packages/react/src/types/auto-imports.d.ts +10 -0
- package/packages/vue/package.json +34 -0
- package/packages/vue/src/index.ts +63 -0
- package/packages/vue/src/plugins/element-plus.ts +69 -0
- package/packages/vue/src/style.scss +12 -0
- package/packages/vue/src/types/auto-imports.d.ts +10 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/vite.svg +1 -0
- package/scripts/build.js +184 -0
- package/scripts/publish.js +95 -0
- package/src/assets/ClearFormat.svg +3 -0
- package/src/assets/Forms.svg +4 -0
- package/src/assets/MergeCell.svg +4 -0
- package/src/assets/SplitCell.svg +4 -0
- package/src/assets/gap.svg +3 -0
- package/src/assets/loading.svg +12 -0
- package/src/assets/paint.svg +9 -0
- package/src/assets/solid.svg +1 -0
- package/src/demo-vue.ts +54 -0
- package/src/index.scss +105 -0
- package/src/index.tsx +20 -0
- package/src/pages/demo/BasicDemo.tsx +19 -0
- package/src/pages/demo/CellMergeDemo.tsx +41 -0
- package/src/pages/demo/CellSelectionDemo.tsx +24 -0
- package/src/pages/demo/CompositeDemo.tsx +60 -0
- package/src/pages/demo/CopyPasteDemo.tsx +26 -0
- package/src/pages/demo/DiffDemo.tsx +33 -0
- package/src/pages/demo/DragDemo.tsx +25 -0
- package/src/pages/demo/EditableDemo.tsx +58 -0
- package/src/pages/demo/ExpandDemo.tsx +32 -0
- package/src/pages/demo/HeaderGroupDemo.tsx +36 -0
- package/src/pages/demo/HistoryDemo.tsx +28 -0
- package/src/pages/demo/ReactDemo.tsx +59 -0
- package/src/pages/demo/ResizeDemo.tsx +24 -0
- package/src/pages/demo/RowGroupDemo.tsx +43 -0
- package/src/pages/demo/RowSelectionDemo.tsx +27 -0
- package/src/pages/demo/TreeDemo.tsx +45 -0
- package/src/pages/demo/VirtualScrollDemo.tsx +21 -0
- package/src/pages/demo/helpers.tsx +39 -0
- package/src/pages/demo/index.tsx +180 -0
- package/src/pages/index.tsx +2 -0
- package/src/pages/website.scss +37 -0
- package/src/pages/website.tsx +651 -0
- package/src/styles/index.scss +172 -0
- package/src/types/auto-imports.d.ts +13 -0
- package/stats.html +4949 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +70 -0
- package/dist/__uno.css +0 -1
- package/dist/chevron-right.js +0 -6
- package/dist/components/Columns.d.ts +0 -3
- package/dist/components/Columns.js +0 -71
- package/dist/components/DocTree.d.ts +0 -4
- package/dist/components/DocTree.js +0 -32
- package/dist/components/Menu.d.ts +0 -1
- package/dist/components/Menu.js +0 -107
- package/dist/components/Popover.d.ts +0 -14
- package/dist/components/Popover.js +0 -41
- package/dist/components/Render.d.ts +0 -4
- package/dist/components/Render.js +0 -20
- package/dist/components/Split.d.ts +0 -15
- package/dist/components/Split.js +0 -76
- package/dist/components/Tree.d.ts +0 -37
- package/dist/components/Tree.js +0 -82
- package/dist/components/utils.d.ts +0 -3
- package/dist/components/utils.js +0 -8
- package/dist/hooks/index.d.ts +0 -40
- package/dist/hooks/index.js +0 -157
- package/dist/hooks/useDir.d.ts +0 -11
- package/dist/hooks/useDir.js +0 -42
- package/dist/hooks/useSelector.d.ts +0 -16
- package/dist/hooks/useSelector.js +0 -35
- package/dist/hooks/useSort.d.ts +0 -18
- package/dist/hooks/useSort.js +0 -83
- package/dist/hooks/useVirtualizer.d.ts +0 -25
- package/dist/hooks/useVirtualizer.js +0 -67
- package/dist/index.d.ts +0 -130
- package/dist/index.js +0 -347
- package/dist/loading.js +0 -6
- package/dist/plugins/CellChangeHighlightPlugin.d.ts +0 -2
- package/dist/plugins/CellChangeHighlightPlugin.js +0 -4
- package/dist/plugins/CellMergePlugin.d.ts +0 -12
- package/dist/plugins/CellMergePlugin.js +0 -2
- package/dist/plugins/CellSelectionPlugin.d.ts +0 -15
- package/dist/plugins/CellSelectionPlugin.js +0 -115
- package/dist/plugins/CommandPlugin.d.ts +0 -14
- package/dist/plugins/CommandPlugin.js +0 -12
- package/dist/plugins/CopyPastePlugin.d.ts +0 -14
- package/dist/plugins/CopyPastePlugin.js +0 -42
- package/dist/plugins/DiffPlugin.d.ts +0 -23
- package/dist/plugins/DiffPlugin.js +0 -56
- package/dist/plugins/DragPlugin.d.ts +0 -14
- package/dist/plugins/DragPlugin.js +0 -47
- package/dist/plugins/EditablePlugin.d.ts +0 -48
- package/dist/plugins/EditablePlugin.js +0 -141
- package/dist/plugins/ExpandPlugin.d.ts +0 -18
- package/dist/plugins/ExpandPlugin.js +0 -50
- package/dist/plugins/HistoryPlugin.d.ts +0 -10
- package/dist/plugins/HistoryPlugin.js +0 -30
- package/dist/plugins/MenuPlugin.d.ts +0 -18
- package/dist/plugins/MenuPlugin.js +0 -107
- package/dist/plugins/RenderPlugin/components.d.ts +0 -5
- package/dist/plugins/RenderPlugin/components.js +0 -87
- package/dist/plugins/RenderPlugin/index.d.ts +0 -30
- package/dist/plugins/RenderPlugin/index.js +0 -49
- package/dist/plugins/ResizePlugin.d.ts +0 -27
- package/dist/plugins/ResizePlugin.js +0 -81
- package/dist/plugins/RowGroupPlugin.d.ts +0 -17
- package/dist/plugins/RowGroupPlugin.js +0 -83
- package/dist/plugins/RowSelectionPlugin.d.ts +0 -20
- package/dist/plugins/RowSelectionPlugin.js +0 -42
- package/dist/plugins/VirtualScrollPlugin.d.ts +0 -15
- package/dist/plugins/VirtualScrollPlugin.js +0 -96
- package/dist/plus.js +0 -6
- package/dist/style.css +0 -3
- package/dist/types/auto-imports.d.js +0 -0
- package/dist/utils.d.ts +0 -30
- package/dist/utils.js +0 -70
- package/dist/wc.d.ts +0 -1
- package/dist/wc.js +0 -21
- package/dist/web-component.d.ts +0 -1
- package/dist/web-component.js +0 -2
- 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
|
+
}
|