ajo-ui 0.1.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/LICENSE +15 -0
- package/README.md +126 -0
- package/dist/accordion.js +130 -0
- package/dist/avatar.js +54 -0
- package/dist/calendar.js +2 -0
- package/dist/carousel.js +239 -0
- package/dist/chart.js +790 -0
- package/dist/checkbox-group.js +70 -0
- package/dist/checkbox.js +77 -0
- package/dist/chunks/bar-CVafh6C1.js +99 -0
- package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
- package/dist/chunks/collection-DtRB63U4.js +111 -0
- package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
- package/dist/chunks/menu-B45IyHHC.js +704 -0
- package/dist/chunks/native-BJdhd9XJ.js +20 -0
- package/dist/chunks/popup-C8Bb3l_g.js +459 -0
- package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
- package/dist/chunks/position-D6_i_SRn.js +434 -0
- package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
- package/dist/collapsible.js +106 -0
- package/dist/command.js +263 -0
- package/dist/context-menu.js +112 -0
- package/dist/data-table.js +5 -0
- package/dist/dialog.js +207 -0
- package/dist/direction.js +22 -0
- package/dist/drawer.js +139 -0
- package/dist/field.js +26 -0
- package/dist/index.js +38 -0
- package/dist/input-date.js +994 -0
- package/dist/input-group.js +52 -0
- package/dist/input-otp.js +179 -0
- package/dist/menu.js +2 -0
- package/dist/menubar.js +236 -0
- package/dist/message-scroller.js +446 -0
- package/dist/navigation-menu.js +330 -0
- package/dist/popover.js +307 -0
- package/dist/progress.js +39 -0
- package/dist/radio-group.js +107 -0
- package/dist/resizable.js +172 -0
- package/dist/select.js +961 -0
- package/dist/sidebar.js +343 -0
- package/dist/slider.js +259 -0
- package/dist/switch.js +53 -0
- package/dist/tabs.js +182 -0
- package/dist/toast.js +492 -0
- package/dist/toggle-group.js +111 -0
- package/dist/toggle.js +52 -0
- package/dist/toolbar.js +127 -0
- package/dist/tooltip.js +196 -0
- package/dist/utils.js +104 -0
- package/dist/virtual-list.js +2 -0
- package/package.json +250 -0
- package/src/accordion.tsx +261 -0
- package/src/availability.ts +261 -0
- package/src/avatar.tsx +99 -0
- package/src/bar.ts +156 -0
- package/src/calendar.tsx +1441 -0
- package/src/carousel.tsx +424 -0
- package/src/chart.tsx +1194 -0
- package/src/checkbox-group.tsx +132 -0
- package/src/checkbox.tsx +130 -0
- package/src/collapsible.tsx +188 -0
- package/src/collection.ts +154 -0
- package/src/command.tsx +511 -0
- package/src/context-menu.tsx +233 -0
- package/src/data-table-contract.ts +143 -0
- package/src/data-table-model.ts +760 -0
- package/src/data-table.tsx +475 -0
- package/src/dialog.tsx +393 -0
- package/src/direction.tsx +45 -0
- package/src/drawer.tsx +251 -0
- package/src/field.tsx +61 -0
- package/src/index.ts +37 -0
- package/src/input-date.tsx +1539 -0
- package/src/input-group.tsx +142 -0
- package/src/input-otp.tsx +324 -0
- package/src/menu-cluster.ts +124 -0
- package/src/menu.tsx +1095 -0
- package/src/menubar.tsx +459 -0
- package/src/message-scroller.tsx +732 -0
- package/src/native.ts +26 -0
- package/src/navigation-menu.tsx +578 -0
- package/src/popover.tsx +519 -0
- package/src/popup-surface.tsx +31 -0
- package/src/popup.ts +569 -0
- package/src/position.ts +523 -0
- package/src/progress.tsx +70 -0
- package/src/radio-group.tsx +186 -0
- package/src/resizable.tsx +310 -0
- package/src/segments.ts +922 -0
- package/src/select.tsx +1501 -0
- package/src/sidebar.tsx +683 -0
- package/src/slider.tsx +424 -0
- package/src/switch.tsx +104 -0
- package/src/tabs.tsx +314 -0
- package/src/toast.tsx +923 -0
- package/src/toggle-group.tsx +249 -0
- package/src/toggle.tsx +91 -0
- package/src/toolbar.tsx +212 -0
- package/src/tooltip.tsx +359 -0
- package/src/utils.ts +204 -0
- package/src/virtual-list.tsx +205 -0
- package/src/virtual.ts +385 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import {
|
|
2
|
+
columnFilteringFeature,
|
|
3
|
+
columnVisibilityFeature,
|
|
4
|
+
constructTable,
|
|
5
|
+
createFilteredRowModel,
|
|
6
|
+
createPaginatedRowModel,
|
|
7
|
+
createSortedRowModel,
|
|
8
|
+
globalFilteringFeature,
|
|
9
|
+
rowPaginationFeature,
|
|
10
|
+
rowSelectionFeature,
|
|
11
|
+
rowSortingFeature,
|
|
12
|
+
sortFn_alphanumeric,
|
|
13
|
+
tableFeatures,
|
|
14
|
+
type ColumnDef,
|
|
15
|
+
type RowSelectionState,
|
|
16
|
+
type SortingState,
|
|
17
|
+
} from '@tanstack/table-core'
|
|
18
|
+
import type { TableReactivityBindings } from '@tanstack/table-core/reactivity'
|
|
19
|
+
import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings'
|
|
20
|
+
import type { Children } from 'ajo'
|
|
21
|
+
import type { Host } from 'ajo-cloves'
|
|
22
|
+
import type {
|
|
23
|
+
DataTableArgs,
|
|
24
|
+
DataTableColumn,
|
|
25
|
+
DataTableData,
|
|
26
|
+
DataTableKey,
|
|
27
|
+
DataTableLabels,
|
|
28
|
+
DataTablePagination,
|
|
29
|
+
} from './data-table-contract'
|
|
30
|
+
|
|
31
|
+
export const dataTableStrategy = tableFeatures({
|
|
32
|
+
columnFilteringFeature,
|
|
33
|
+
columnVisibilityFeature,
|
|
34
|
+
globalFilteringFeature,
|
|
35
|
+
rowPaginationFeature,
|
|
36
|
+
rowSelectionFeature,
|
|
37
|
+
rowSortingFeature,
|
|
38
|
+
filteredRowModel: createFilteredRowModel(),
|
|
39
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
40
|
+
sortedRowModel: createSortedRowModel(),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const DEFAULT_SIZES = [10, 25, 50] as const
|
|
44
|
+
|
|
45
|
+
export const dataTableDefaultLabels: DataTableLabels = {
|
|
46
|
+
columns: 'Columns',
|
|
47
|
+
deselectPage: 'Deselect page',
|
|
48
|
+
deselectResults: 'Deselect filtered results',
|
|
49
|
+
deselectRow: row => `Deselect ${row}`,
|
|
50
|
+
firstPage: 'First page',
|
|
51
|
+
lastPage: 'Last page',
|
|
52
|
+
nextPage: 'Next page',
|
|
53
|
+
page: (page, pages) => `Page ${page} of ${pages}`,
|
|
54
|
+
pagination: table => `${table} pagination`,
|
|
55
|
+
previousPage: 'Previous page',
|
|
56
|
+
reset: 'Reset',
|
|
57
|
+
results: count => `${count} result${count === 1 ? '' : 's'}`,
|
|
58
|
+
rowsPerPage: 'Rows per page',
|
|
59
|
+
search: 'Search',
|
|
60
|
+
selectPage: 'Select page',
|
|
61
|
+
selectResults: 'Select filtered results',
|
|
62
|
+
selectRow: row => `Select ${row}`,
|
|
63
|
+
selected: (selected, total) => `${selected} of ${total} ${total === 1 ? 'row' : 'rows'} selected.`,
|
|
64
|
+
sort: (column, next) => `Sort ${column} ${next}`,
|
|
65
|
+
toolbar: table => `${table} controls`,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type Subscription = { unsubscribe(): void }
|
|
69
|
+
|
|
70
|
+
type ColumnModel<T extends DataTableData> = {
|
|
71
|
+
column: DataTableColumn<T>
|
|
72
|
+
id: string
|
|
73
|
+
read?: (row: T, sourceIndex: number) => unknown
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type DataTableColumnView<T extends DataTableData> = ColumnModel<T> & {
|
|
77
|
+
active: readonly string[]
|
|
78
|
+
sorted: false | 'asc' | 'desc'
|
|
79
|
+
visible: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type DataTableCellView<T extends DataTableData> = {
|
|
83
|
+
column: ColumnModel<T>
|
|
84
|
+
value: unknown
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type DataTableRowView<T extends DataTableData, Key extends DataTableKey> = {
|
|
88
|
+
cells: readonly DataTableCellView<T>[]
|
|
89
|
+
id: string
|
|
90
|
+
key: Key
|
|
91
|
+
original: T
|
|
92
|
+
selected: boolean
|
|
93
|
+
sourceIndex: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type DataTableView<T extends DataTableData, Key extends DataTableKey> = {
|
|
97
|
+
columns: readonly DataTableColumnView<T>[]
|
|
98
|
+
filteredCount: number
|
|
99
|
+
hasFilters: boolean
|
|
100
|
+
page: {
|
|
101
|
+
count: number
|
|
102
|
+
enabled: boolean
|
|
103
|
+
index: number
|
|
104
|
+
size: number
|
|
105
|
+
sizes: readonly number[]
|
|
106
|
+
}
|
|
107
|
+
query: string
|
|
108
|
+
rows: readonly DataTableRowView<T, Key>[]
|
|
109
|
+
selectedCount: number
|
|
110
|
+
selection: {
|
|
111
|
+
enabled: boolean
|
|
112
|
+
all: boolean
|
|
113
|
+
some: boolean
|
|
114
|
+
}
|
|
115
|
+
sourceCount: number
|
|
116
|
+
visibility: boolean
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type DataTableModel<T extends DataTableData, Key extends DataTableKey> = {
|
|
120
|
+
cell(cell: DataTableCellView<T>, row: DataTableRowView<T, Key>): Children
|
|
121
|
+
firstPage(): void
|
|
122
|
+
lastPage(): void
|
|
123
|
+
nextPage(): void
|
|
124
|
+
previousPage(): void
|
|
125
|
+
reset(event?: Event): void
|
|
126
|
+
selected(rowId: string): boolean
|
|
127
|
+
selection(): { all: boolean; some: boolean }
|
|
128
|
+
setFacet(columnId: string, value: string, checked: boolean, event?: Event): void
|
|
129
|
+
setPageSize(size: number): void
|
|
130
|
+
setQuery(query: string, event?: Event): void
|
|
131
|
+
sort(columnId: string, event?: Event): void
|
|
132
|
+
sync(args: DataTableArgs<T, Key>): DataTableView<T, Key>
|
|
133
|
+
toggleColumn(columnId: string, visible: boolean): void
|
|
134
|
+
togglePage(checked: boolean, event?: Event): void
|
|
135
|
+
toggleRow(rowId: string, checked: boolean, event?: Event): void
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const assertText = (value: unknown, name: string) => {
|
|
139
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`DataTable invalid ${name}`)
|
|
140
|
+
return value
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const encodeKey = (key: DataTableKey, index?: number) => {
|
|
144
|
+
if (typeof key === 'string') {
|
|
145
|
+
if (!key) throw new TypeError(`DataTable invalid row key${index === undefined ? '' : ` at ${index}`}`)
|
|
146
|
+
return `s:${key}`
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isFinite(key)) throw new TypeError(`DataTable invalid row key${index === undefined ? '' : ` at ${index}`}`)
|
|
149
|
+
return `n:${Object.is(key, -0) ? 0 : key}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const columnId = <T extends DataTableData,>(column: DataTableColumn<T>) => {
|
|
153
|
+
const id = column.id ?? (typeof column.value === 'string' ? column.value : undefined)
|
|
154
|
+
return assertText(id, 'column id')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const scalarText = (value: unknown, column: string, index: number) => {
|
|
158
|
+
if (value == null) return ''
|
|
159
|
+
if (typeof value === 'string' || typeof value === 'boolean') return String(value)
|
|
160
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
|
161
|
+
throw new TypeError(`DataTable invalid searchable value ${column} at ${index}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const facetValues = <T extends DataTableData,>(model: ColumnModel<T>, row: T, index: number) => {
|
|
165
|
+
const facet = model.column.facet
|
|
166
|
+
const raw = facet?.values?.(row, index)
|
|
167
|
+
if (raw !== undefined) return (Array.isArray(raw) ? raw : [raw]).map(value => assertText(value, `facet value ${model.id}`))
|
|
168
|
+
return [scalarText(model.read?.(row, index), model.id, index)]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const compareValues = (
|
|
172
|
+
left: unknown,
|
|
173
|
+
right: unknown,
|
|
174
|
+
leftRow: { index: number },
|
|
175
|
+
rightRow: { index: number },
|
|
176
|
+
columnId: string,
|
|
177
|
+
alphanumeric: () => number,
|
|
178
|
+
) => {
|
|
179
|
+
const leftMissing = left == null
|
|
180
|
+
const rightMissing = right == null
|
|
181
|
+
if (leftMissing || rightMissing) return leftMissing === rightMissing ? 0 : leftMissing ? 1 : -1
|
|
182
|
+
if (typeof left !== typeof right) throw new TypeError(`DataTable mixed sort values ${columnId}`)
|
|
183
|
+
if (typeof left === 'string') return alphanumeric()
|
|
184
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
185
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) {
|
|
186
|
+
throw new TypeError(`DataTable invalid sort value ${columnId} at ${!Number.isFinite(left) ? leftRow.index : rightRow.index}`)
|
|
187
|
+
}
|
|
188
|
+
return left === right ? 0 : left < right ? -1 : 1
|
|
189
|
+
}
|
|
190
|
+
if (typeof left === 'boolean' && typeof right === 'boolean') return left === right ? 0 : left ? 1 : -1
|
|
191
|
+
throw new TypeError(`DataTable invalid sort value ${columnId}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const paginationConfig = (pagination: false | DataTablePagination | undefined) => {
|
|
195
|
+
const sizes = pagination === false ? DEFAULT_SIZES : pagination?.sizes ?? DEFAULT_SIZES
|
|
196
|
+
if (!sizes.length) throw new RangeError('DataTable empty page sizes')
|
|
197
|
+
const seen = new Set<number>()
|
|
198
|
+
for (const size of sizes) {
|
|
199
|
+
if (!Number.isInteger(size) || size <= 0 || seen.has(size)) throw new RangeError('DataTable invalid page sizes')
|
|
200
|
+
seen.add(size)
|
|
201
|
+
}
|
|
202
|
+
const size = pagination === false ? sizes[0]! : pagination?.defaultSize ?? sizes[0]!
|
|
203
|
+
if (!seen.has(size)) throw new RangeError('DataTable default page size is unavailable')
|
|
204
|
+
return { enabled: pagination !== false, size, sizes }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const dataTableReactivity = (host: Host) => {
|
|
208
|
+
const subscriptions = new Set<Subscription>()
|
|
209
|
+
let disposed = false
|
|
210
|
+
const dispose = () => {
|
|
211
|
+
if (disposed) return
|
|
212
|
+
disposed = true
|
|
213
|
+
for (const subscription of subscriptions) subscription.unsubscribe()
|
|
214
|
+
subscriptions.clear()
|
|
215
|
+
}
|
|
216
|
+
const add = (subscription: Subscription) => {
|
|
217
|
+
if (disposed) subscription.unsubscribe()
|
|
218
|
+
else subscriptions.add(subscription)
|
|
219
|
+
return subscription
|
|
220
|
+
}
|
|
221
|
+
const bindings: TableReactivityBindings = {
|
|
222
|
+
...storeReactivityBindings(),
|
|
223
|
+
addSubscription: subscription => { add(subscription) },
|
|
224
|
+
createOptionsStore: false,
|
|
225
|
+
schedule: fn => queueMicrotask(() => {
|
|
226
|
+
if (disposed || host.signal.aborted) return
|
|
227
|
+
try { fn() } catch (error) { host.throw(error) }
|
|
228
|
+
}),
|
|
229
|
+
unmount: dispose,
|
|
230
|
+
wrapExternalAtoms: false,
|
|
231
|
+
}
|
|
232
|
+
host.signal.addEventListener('abort', dispose, { once: true })
|
|
233
|
+
return { add, bindings }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const createDataTableModel = <T extends DataTableData, Key extends DataTableKey>(
|
|
237
|
+
host: Host,
|
|
238
|
+
initialArgs: DataTableArgs<T, Key>,
|
|
239
|
+
): DataTableModel<T, Key> => {
|
|
240
|
+
const reactive = dataTableReactivity(host)
|
|
241
|
+
const features = tableFeatures({ ...dataTableStrategy, coreReactivityFeature: reactive.bindings })
|
|
242
|
+
let args = initialArgs
|
|
243
|
+
let actionEvent: Event | undefined
|
|
244
|
+
let syncing = false
|
|
245
|
+
let stateVersion = 0
|
|
246
|
+
let renderedVersion = 0
|
|
247
|
+
let requestedVersion = 0
|
|
248
|
+
let queued = false
|
|
249
|
+
let optionsInitialized = false
|
|
250
|
+
|
|
251
|
+
let rowsRef: readonly T[] | undefined
|
|
252
|
+
let rowsLength = 0
|
|
253
|
+
let keyGetter: DataTableArgs<T, Key>['getRowKey'] | undefined
|
|
254
|
+
let tableData: readonly T[] = initialArgs.rows
|
|
255
|
+
let rowIds: string[] = []
|
|
256
|
+
let rowKeys: Key[] = []
|
|
257
|
+
let rowById = new Map<string, number>()
|
|
258
|
+
|
|
259
|
+
let columnsRef: readonly DataTableColumn<T>[] | undefined
|
|
260
|
+
let columnsLength = 0
|
|
261
|
+
let models: ColumnModel<T>[] = []
|
|
262
|
+
let modelById = new Map<string, ColumnModel<T>>()
|
|
263
|
+
let definitions: ReadonlyArray<ColumnDef<typeof features, T, unknown>> = []
|
|
264
|
+
let knownColumns = new Map<string, ColumnModel<T>>()
|
|
265
|
+
|
|
266
|
+
let selectionEnabled = Boolean(initialArgs.selection)
|
|
267
|
+
let selectionControlled = initialArgs.selection?.value !== undefined
|
|
268
|
+
let searchEnabled = Boolean(initialArgs.search)
|
|
269
|
+
let searchValue = ''
|
|
270
|
+
let pageConfig = paginationConfig(initialArgs.pagination)
|
|
271
|
+
|
|
272
|
+
const snapshotRows = (current: DataTableArgs<T, Key>) => {
|
|
273
|
+
if (current.rows === rowsRef && current.getRowKey === keyGetter) {
|
|
274
|
+
if (current.rows.length !== rowsLength) throw new TypeError('DataTable mutated rows')
|
|
275
|
+
if (rowsLength) {
|
|
276
|
+
const last = rowsLength - 1
|
|
277
|
+
if (
|
|
278
|
+
encodeKey(current.getRowKey(current.rows[0]!, 0)) !== rowIds[0]
|
|
279
|
+
|| encodeKey(current.getRowKey(current.rows[last]!, last)) !== rowIds[last]
|
|
280
|
+
) throw new TypeError('DataTable mutated rows')
|
|
281
|
+
}
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
const replaced = current.rows !== rowsRef
|
|
285
|
+
rowsRef = current.rows
|
|
286
|
+
rowsLength = current.rows.length
|
|
287
|
+
keyGetter = current.getRowKey
|
|
288
|
+
// TanStack memoizes core rows by data identity. A new key function needs a
|
|
289
|
+
// fresh identity even when the logical collection itself is unchanged.
|
|
290
|
+
tableData = replaced ? current.rows : [...current.rows]
|
|
291
|
+
rowIds = []
|
|
292
|
+
rowKeys = []
|
|
293
|
+
rowById = new Map()
|
|
294
|
+
current.rows.forEach((row, index) => {
|
|
295
|
+
const key = current.getRowKey(row, index)
|
|
296
|
+
const id = encodeKey(key, index)
|
|
297
|
+
const previous = rowById.get(id)
|
|
298
|
+
if (previous !== undefined) throw new TypeError(`DataTable duplicate row key ${JSON.stringify(key)} ${previous}/${index}`)
|
|
299
|
+
rowById.set(id, index)
|
|
300
|
+
rowIds.push(id)
|
|
301
|
+
rowKeys.push(key)
|
|
302
|
+
})
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const snapshotColumns = (current: DataTableArgs<T, Key>) => {
|
|
307
|
+
if (current.columns === columnsRef) {
|
|
308
|
+
if (current.columns.length !== columnsLength) throw new TypeError('DataTable mutated columns')
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
if (!current.columns.length) throw new TypeError('DataTable needs columns')
|
|
312
|
+
columnsRef = current.columns
|
|
313
|
+
columnsLength = current.columns.length
|
|
314
|
+
models = []
|
|
315
|
+
modelById = new Map()
|
|
316
|
+
for (const column of current.columns) {
|
|
317
|
+
assertText(column.label, 'column label')
|
|
318
|
+
const id = columnId(column)
|
|
319
|
+
if (modelById.has(id)) throw new TypeError(`DataTable duplicate column ${JSON.stringify(id)}`)
|
|
320
|
+
const read = column.value === undefined
|
|
321
|
+
? undefined
|
|
322
|
+
: typeof column.value === 'function'
|
|
323
|
+
? column.value
|
|
324
|
+
: (row: T) => row[column.value as keyof T]
|
|
325
|
+
if (!read && !column.cell) throw new TypeError(`DataTable display column ${JSON.stringify(id)} needs a cell`)
|
|
326
|
+
if (!read && column.facet) throw new TypeError(`DataTable display column ${JSON.stringify(id)} cannot define a facet`)
|
|
327
|
+
if (column.facet) {
|
|
328
|
+
assertText(column.facet.label, `facet label ${id}`)
|
|
329
|
+
const options = new Set<string>()
|
|
330
|
+
for (const option of column.facet.options) {
|
|
331
|
+
assertText(option.label, `facet option label ${id}`)
|
|
332
|
+
assertText(option.value, `facet option value ${id}`)
|
|
333
|
+
if (options.has(option.value)) throw new TypeError(`DataTable duplicate facet option ${id}/${option.value}`)
|
|
334
|
+
options.add(option.value)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const model = { column, id, read }
|
|
338
|
+
models.push(model)
|
|
339
|
+
modelById.set(id, model)
|
|
340
|
+
}
|
|
341
|
+
if (models.every(({ column }) => column.defaultHidden)) throw new TypeError('DataTable needs a visible column')
|
|
342
|
+
|
|
343
|
+
definitions = models.map(model => {
|
|
344
|
+
const { column, id, read } = model
|
|
345
|
+
const definition = {
|
|
346
|
+
enableGlobalFilter: Boolean(read && column.search !== false),
|
|
347
|
+
enableHiding: column.hideable !== false,
|
|
348
|
+
enableSorting: Boolean(read && column.sort !== false),
|
|
349
|
+
filterFn: (row, _columnId, active) => {
|
|
350
|
+
const selected = active as readonly string[]
|
|
351
|
+
return !selected.length || facetValues(model, row.original, row.index).some(value => selected.includes(value))
|
|
352
|
+
},
|
|
353
|
+
header: column.label,
|
|
354
|
+
id,
|
|
355
|
+
sortFn: (left, right, columnId) => {
|
|
356
|
+
if (typeof column.sort === 'function') {
|
|
357
|
+
const result = column.sort(left.original, right.original)
|
|
358
|
+
if (!Number.isFinite(result)) throw new TypeError(`DataTable invalid comparator ${columnId}`)
|
|
359
|
+
return result
|
|
360
|
+
}
|
|
361
|
+
return compareValues(
|
|
362
|
+
left.getValue(columnId),
|
|
363
|
+
right.getValue(columnId),
|
|
364
|
+
left,
|
|
365
|
+
right,
|
|
366
|
+
columnId,
|
|
367
|
+
() => sortFn_alphanumeric(left, right, columnId),
|
|
368
|
+
)
|
|
369
|
+
},
|
|
370
|
+
sortUndefined: false,
|
|
371
|
+
} satisfies ColumnDef<typeof features, T, unknown>
|
|
372
|
+
return read
|
|
373
|
+
? { ...definition, accessorFn: read } satisfies ColumnDef<typeof features, T, unknown>
|
|
374
|
+
: definition
|
|
375
|
+
})
|
|
376
|
+
return true
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const selectionState = (keys: readonly Key[] | undefined) => {
|
|
380
|
+
const state: RowSelectionState = Object.create(null)
|
|
381
|
+
const seen = new Set<string>()
|
|
382
|
+
for (const key of keys ?? []) {
|
|
383
|
+
const id = encodeKey(key)
|
|
384
|
+
if (seen.has(id)) throw new TypeError(`DataTable duplicate selection key ${JSON.stringify(key)}`)
|
|
385
|
+
seen.add(id)
|
|
386
|
+
if (rowById.has(id)) state[id] = true
|
|
387
|
+
}
|
|
388
|
+
return state
|
|
389
|
+
}
|
|
390
|
+
const selectionInput = (selection: DataTableArgs<T, Key>['selection']) => {
|
|
391
|
+
const fallback = selectionState(selection?.defaultValue)
|
|
392
|
+
return selection?.value === undefined ? fallback : selectionState(selection.value)
|
|
393
|
+
}
|
|
394
|
+
const sameSelection = (left: RowSelectionState, right: RowSelectionState) => {
|
|
395
|
+
for (const id of Object.keys(left)) if (Boolean(left[id]) !== Boolean(right[id])) return false
|
|
396
|
+
for (const id of Object.keys(right)) if (Boolean(left[id]) !== Boolean(right[id])) return false
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
snapshotRows(initialArgs)
|
|
401
|
+
snapshotColumns(initialArgs)
|
|
402
|
+
knownColumns = new Map(modelById)
|
|
403
|
+
assertText(initialArgs.label, 'label')
|
|
404
|
+
|
|
405
|
+
const initialSelection = selectionInput(initialArgs.selection)
|
|
406
|
+
const initialVisibility = Object.fromEntries(models.map(({ column, id }) => [
|
|
407
|
+
id,
|
|
408
|
+
column.hideable === false || !column.defaultHidden,
|
|
409
|
+
]))
|
|
410
|
+
|
|
411
|
+
const table = constructTable<typeof features, T>({
|
|
412
|
+
autoResetPageIndex: false,
|
|
413
|
+
columns: definitions,
|
|
414
|
+
data: tableData,
|
|
415
|
+
enableMultiSort: false,
|
|
416
|
+
enableRowRangeSelection: false,
|
|
417
|
+
enableRowSelection: selectionEnabled,
|
|
418
|
+
features,
|
|
419
|
+
getColumnCanGlobalFilter: column => (column.columnDef as { enableGlobalFilter?: boolean }).enableGlobalFilter === true,
|
|
420
|
+
getRowId: (_row, index) => rowIds[index]!,
|
|
421
|
+
globalFilterFn: (row, columnId) => {
|
|
422
|
+
const model = modelById.get(columnId)
|
|
423
|
+
if (!model?.read || model.column.search === false) return false
|
|
424
|
+
const raw = typeof model.column.search === 'function'
|
|
425
|
+
? model.column.search(row.original, row.index)
|
|
426
|
+
: row.getValue(columnId)
|
|
427
|
+
return scalarText(raw, columnId, row.index).toLowerCase().includes(searchValue)
|
|
428
|
+
},
|
|
429
|
+
initialState: {
|
|
430
|
+
columnFilters: [],
|
|
431
|
+
columnVisibility: initialVisibility,
|
|
432
|
+
globalFilter: '',
|
|
433
|
+
pagination: { pageIndex: 0, pageSize: pageConfig.size },
|
|
434
|
+
rowSelection: initialSelection,
|
|
435
|
+
sorting: [],
|
|
436
|
+
},
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
const defaultSelectionUpdater = table.options.onRowSelectionChange!
|
|
440
|
+
|
|
441
|
+
const selectedKeys = (state = table.atoms.rowSelection.get()) => {
|
|
442
|
+
const indexes: number[] = []
|
|
443
|
+
for (const id of Object.keys(state)) {
|
|
444
|
+
const index = rowById.get(id)
|
|
445
|
+
if (state[id] && index !== undefined) indexes.push(index)
|
|
446
|
+
}
|
|
447
|
+
indexes.sort((left, right) => left - right)
|
|
448
|
+
return indexes.map(index => rowKeys[index]!)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const countSelected = (state = table.atoms.rowSelection.get()) => {
|
|
452
|
+
let count = 0
|
|
453
|
+
for (const id of Object.keys(state)) if (state[id] && rowById.has(id)) count++
|
|
454
|
+
return count
|
|
455
|
+
}
|
|
456
|
+
const pageSelection = () => {
|
|
457
|
+
const rows = pageConfig.enabled ? table.getRowModel().rows : table.getPrePaginatedRowModel().rows
|
|
458
|
+
let selected = 0
|
|
459
|
+
for (const row of rows) if (row.getIsSelected()) selected++
|
|
460
|
+
const all = rows.length > 0 && selected === rows.length
|
|
461
|
+
return { all, some: selected > 0 && !all }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const invalidate = (version: number) => {
|
|
465
|
+
requestedVersion = Math.max(requestedVersion, version)
|
|
466
|
+
if (queued || host.signal.aborted) return
|
|
467
|
+
queued = true
|
|
468
|
+
queueMicrotask(() => {
|
|
469
|
+
queued = false
|
|
470
|
+
if (host.signal.aborted || renderedVersion >= requestedVersion) return
|
|
471
|
+
try { host.next() } catch (error) { if (!host.signal.aborted) host.throw(error) }
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const selectionUpdater = (updater: RowSelectionState | ((state: RowSelectionState) => RowSelectionState)) => {
|
|
476
|
+
const selection = args.selection
|
|
477
|
+
if (!selection) return
|
|
478
|
+
if (selection.value !== undefined) {
|
|
479
|
+
const current = selectionState(selection.value)
|
|
480
|
+
const next = typeof updater === 'function' ? updater(current) : updater
|
|
481
|
+
selection.onValueChange(selectedKeys(next), actionEvent)
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
defaultSelectionUpdater(updater)
|
|
485
|
+
selection.onValueChange?.(selectedKeys(), actionEvent)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
reactive.add(table.store.subscribe(() => {
|
|
489
|
+
const version = ++stateVersion
|
|
490
|
+
if (!syncing) invalidate(version)
|
|
491
|
+
}))
|
|
492
|
+
|
|
493
|
+
const withAction = (event: Event | undefined, action: () => void) => {
|
|
494
|
+
if (host.signal.aborted) return
|
|
495
|
+
const previous = actionEvent
|
|
496
|
+
actionEvent = event
|
|
497
|
+
try { action() } finally { actionEvent = previous }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const firstPage = () => { if (!host.signal.aborted && pageConfig.enabled) table.firstPage() }
|
|
501
|
+
const resetPage = () => { if (pageConfig.enabled) table.setPageIndex(0) }
|
|
502
|
+
|
|
503
|
+
const reconcileColumns = (changed: boolean) => {
|
|
504
|
+
if (!changed) return
|
|
505
|
+
const visibility = table.atoms.columnVisibility.get()
|
|
506
|
+
const nextVisibility: Record<string, boolean> = {}
|
|
507
|
+
for (const model of models) {
|
|
508
|
+
const previous = knownColumns.get(model.id)
|
|
509
|
+
nextVisibility[model.id] = model.column.hideable === false
|
|
510
|
+
? true
|
|
511
|
+
: previous ? visibility[model.id] !== false : !model.column.defaultHidden
|
|
512
|
+
}
|
|
513
|
+
if (!Object.values(nextVisibility).some(Boolean)) nextVisibility[models[0]!.id] = true
|
|
514
|
+
table.baseAtoms.columnVisibility.set(nextVisibility)
|
|
515
|
+
|
|
516
|
+
let reset = false
|
|
517
|
+
const sorting = table.atoms.sorting.get()
|
|
518
|
+
if (sorting.some(sort => {
|
|
519
|
+
const model = modelById.get(sort.id)
|
|
520
|
+
return !model?.read || model.column.sort === false
|
|
521
|
+
})) {
|
|
522
|
+
table.baseAtoms.sorting.set([])
|
|
523
|
+
reset = true
|
|
524
|
+
}
|
|
525
|
+
const currentFilters = table.atoms.columnFilters.get()
|
|
526
|
+
const filters = currentFilters.flatMap(filter => {
|
|
527
|
+
const facet = modelById.get(filter.id)?.column.facet
|
|
528
|
+
if (!facet) return []
|
|
529
|
+
const allowed = new Set(facet.options.map(option => option.value))
|
|
530
|
+
const values = (filter.value as readonly string[]).filter(value => allowed.has(value))
|
|
531
|
+
return values.length ? [{ id: filter.id, value: values }] : []
|
|
532
|
+
})
|
|
533
|
+
const filtersChanged = filters.length !== currentFilters.length || filters.some((filter, index) => {
|
|
534
|
+
const current = currentFilters[index]
|
|
535
|
+
const values = filter.value as readonly string[]
|
|
536
|
+
const currentValues = current?.value as readonly string[] | undefined
|
|
537
|
+
return !current || current.id !== filter.id || values.length !== currentValues?.length
|
|
538
|
+
|| values.some((value, valueIndex) => value !== currentValues[valueIndex])
|
|
539
|
+
})
|
|
540
|
+
if (filtersChanged) {
|
|
541
|
+
table.baseAtoms.columnFilters.set(filters)
|
|
542
|
+
reset = true
|
|
543
|
+
}
|
|
544
|
+
knownColumns = new Map(modelById)
|
|
545
|
+
if (reset) resetPage()
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const sync = (nextArgs: DataTableArgs<T, Key>): DataTableView<T, Key> => {
|
|
549
|
+
args = nextArgs
|
|
550
|
+
assertText(args.label, 'label')
|
|
551
|
+
const rowsChanged = snapshotRows(args)
|
|
552
|
+
const columnsChanged = snapshotColumns(args)
|
|
553
|
+
// Rows cache accessor results by column ID. Rebuild them when a schema is
|
|
554
|
+
// replaced over the same collection; stable renders keep the same data ref.
|
|
555
|
+
if (columnsChanged && !rowsChanged) tableData = [...args.rows]
|
|
556
|
+
const nextPage = paginationConfig(args.pagination)
|
|
557
|
+
const nextSelection = Boolean(args.selection)
|
|
558
|
+
const nextSelectionState = selectionInput(args.selection)
|
|
559
|
+
const nextSelectionControlled = args.selection?.value !== undefined
|
|
560
|
+
const nextSearch = Boolean(args.search)
|
|
561
|
+
const optionsChanged = !optionsInitialized
|
|
562
|
+
|| rowsChanged
|
|
563
|
+
|| columnsChanged
|
|
564
|
+
|| selectionEnabled !== nextSelection
|
|
565
|
+
|| searchEnabled !== nextSearch
|
|
566
|
+
|| selectionControlled !== nextSelectionControlled
|
|
567
|
+
|| (nextSelectionControlled && !sameSelection(table.atoms.rowSelection.get(), nextSelectionState))
|
|
568
|
+
|
|
569
|
+
syncing = true
|
|
570
|
+
try {
|
|
571
|
+
if (selectionEnabled !== nextSelection) {
|
|
572
|
+
table.baseAtoms.rowSelection.set(nextSelection
|
|
573
|
+
? nextSelectionState
|
|
574
|
+
: {})
|
|
575
|
+
}
|
|
576
|
+
if (searchEnabled !== nextSearch) {
|
|
577
|
+
searchValue = ''
|
|
578
|
+
table.baseAtoms.globalFilter.set('')
|
|
579
|
+
}
|
|
580
|
+
if (pageConfig.enabled !== nextPage.enabled) {
|
|
581
|
+
table.baseAtoms.pagination.set({ pageIndex: 0, pageSize: nextPage.size })
|
|
582
|
+
} else if (!nextPage.sizes.includes(table.atoms.pagination.get().pageSize)) {
|
|
583
|
+
table.baseAtoms.pagination.set({ pageIndex: 0, pageSize: nextPage.size })
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
selectionEnabled = nextSelection
|
|
587
|
+
selectionControlled = nextSelectionControlled
|
|
588
|
+
searchEnabled = nextSearch
|
|
589
|
+
pageConfig = nextPage
|
|
590
|
+
|
|
591
|
+
if (optionsChanged) {
|
|
592
|
+
table.setOptions(previous => ({
|
|
593
|
+
...previous,
|
|
594
|
+
columns: definitions,
|
|
595
|
+
data: tableData,
|
|
596
|
+
enableGlobalFilter: searchEnabled,
|
|
597
|
+
enableRowSelection: selectionEnabled,
|
|
598
|
+
onRowSelectionChange: selectionUpdater,
|
|
599
|
+
state: selectionControlled ? { rowSelection: nextSelectionState } : undefined,
|
|
600
|
+
}))
|
|
601
|
+
optionsInitialized = true
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
reconcileColumns(columnsChanged)
|
|
605
|
+
if (rowsChanged && args.selection?.value === undefined) {
|
|
606
|
+
const current = table.atoms.rowSelection.get()
|
|
607
|
+
const pruned: RowSelectionState = Object.create(null)
|
|
608
|
+
for (const id of Object.keys(current)) if (rowById.has(id)) pruned[id] = true
|
|
609
|
+
if (Object.keys(pruned).length !== Object.keys(current).length) table.baseAtoms.rowSelection.set(pruned)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (pageConfig.enabled) {
|
|
613
|
+
const pageCount = Math.max(1, table.getPageCount())
|
|
614
|
+
const current = table.atoms.pagination.get()
|
|
615
|
+
if (current.pageIndex >= pageCount) table.baseAtoms.pagination.set({ ...current, pageIndex: pageCount - 1 })
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
syncing = false
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const pagination = table.atoms.pagination.get()
|
|
622
|
+
const pageCount = pageConfig.enabled ? Math.max(1, table.getPageCount()) : 1
|
|
623
|
+
const sourceRows = pageConfig.enabled ? table.getRowModel().rows : table.getPrePaginatedRowModel().rows
|
|
624
|
+
const allColumns = models.map(model => ({
|
|
625
|
+
...model,
|
|
626
|
+
active: (table.getColumn(model.id)!.getFilterValue() as readonly string[] | undefined) ?? [],
|
|
627
|
+
sorted: table.getColumn(model.id)!.getIsSorted(),
|
|
628
|
+
visible: table.getColumn(model.id)!.getIsVisible(),
|
|
629
|
+
}))
|
|
630
|
+
const visibleRows = sourceRows.map(row => ({
|
|
631
|
+
cells: row.getVisibleCells().map(cell => ({ column: modelById.get(cell.column.id)!, value: cell.getValue() })),
|
|
632
|
+
id: row.id,
|
|
633
|
+
key: rowKeys[row.index]!,
|
|
634
|
+
original: row.original,
|
|
635
|
+
selected: row.getIsSelected(),
|
|
636
|
+
sourceIndex: row.index,
|
|
637
|
+
}))
|
|
638
|
+
const selected = countSelected()
|
|
639
|
+
const pageSelected = pageSelection()
|
|
640
|
+
const filters = table.atoms.columnFilters.get()
|
|
641
|
+
const query = searchEnabled ? String(table.atoms.globalFilter.get() ?? '') : ''
|
|
642
|
+
|
|
643
|
+
renderedVersion = stateVersion
|
|
644
|
+
return {
|
|
645
|
+
columns: allColumns,
|
|
646
|
+
filteredCount: table.getFilteredRowModel().rows.length,
|
|
647
|
+
hasFilters: Boolean(query || filters.length),
|
|
648
|
+
page: {
|
|
649
|
+
count: pageCount,
|
|
650
|
+
enabled: pageConfig.enabled,
|
|
651
|
+
index: pageConfig.enabled ? pagination.pageIndex : 0,
|
|
652
|
+
size: pagination.pageSize,
|
|
653
|
+
sizes: pageConfig.sizes,
|
|
654
|
+
},
|
|
655
|
+
query,
|
|
656
|
+
rows: visibleRows,
|
|
657
|
+
selectedCount: selected,
|
|
658
|
+
selection: {
|
|
659
|
+
enabled: selectionEnabled,
|
|
660
|
+
...pageSelected,
|
|
661
|
+
},
|
|
662
|
+
sourceCount: args.rows.length,
|
|
663
|
+
visibility: models.length > 1 && models.some(model => model.column.hideable !== false),
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
cell(cell, row) {
|
|
669
|
+
if (cell.column.column.cell) return cell.column.column.cell(row.original, {
|
|
670
|
+
columnId: cell.column.id,
|
|
671
|
+
sourceIndex: row.sourceIndex,
|
|
672
|
+
value: cell.value,
|
|
673
|
+
})
|
|
674
|
+
if (cell.value == null) return ''
|
|
675
|
+
if (['bigint', 'boolean', 'number', 'string'].includes(typeof cell.value)) return String(cell.value)
|
|
676
|
+
throw new TypeError(`DataTable cannot render ${cell.column.id} at ${row.sourceIndex}`)
|
|
677
|
+
},
|
|
678
|
+
firstPage,
|
|
679
|
+
lastPage: () => { if (!host.signal.aborted && pageConfig.enabled) table.lastPage() },
|
|
680
|
+
nextPage: () => { if (!host.signal.aborted && pageConfig.enabled) table.nextPage() },
|
|
681
|
+
previousPage: () => { if (!host.signal.aborted && pageConfig.enabled) table.previousPage() },
|
|
682
|
+
reset(event) {
|
|
683
|
+
withAction(event, () => {
|
|
684
|
+
searchValue = ''
|
|
685
|
+
table.setGlobalFilter('')
|
|
686
|
+
table.setColumnFilters([])
|
|
687
|
+
resetPage()
|
|
688
|
+
})
|
|
689
|
+
},
|
|
690
|
+
selected: id => table.atoms.rowSelection.get()[id] === true,
|
|
691
|
+
selection: pageSelection,
|
|
692
|
+
setFacet(id, value, checked, event) {
|
|
693
|
+
const facet = modelById.get(id)?.column.facet
|
|
694
|
+
if (!facet?.options.some(option => option.value === value)) return
|
|
695
|
+
withAction(event, () => {
|
|
696
|
+
const column = table.getColumn(id)
|
|
697
|
+
const active = new Set(column?.getFilterValue() as readonly string[] | undefined)
|
|
698
|
+
if (active.has(value) === checked) return
|
|
699
|
+
if (checked) active.add(value)
|
|
700
|
+
else active.delete(value)
|
|
701
|
+
column?.setFilterValue(active.size ? [...active] : undefined)
|
|
702
|
+
resetPage()
|
|
703
|
+
})
|
|
704
|
+
},
|
|
705
|
+
setPageSize(size) {
|
|
706
|
+
if (host.signal.aborted || !pageConfig.enabled || !pageConfig.sizes.includes(size)) return
|
|
707
|
+
table.setPagination({ pageIndex: 0, pageSize: size })
|
|
708
|
+
},
|
|
709
|
+
setQuery(query, event) {
|
|
710
|
+
if (!searchEnabled) return
|
|
711
|
+
withAction(event, () => {
|
|
712
|
+
const value = query.trim()
|
|
713
|
+
searchValue = value.toLowerCase()
|
|
714
|
+
table.setGlobalFilter(value)
|
|
715
|
+
resetPage()
|
|
716
|
+
})
|
|
717
|
+
},
|
|
718
|
+
sort(id, event) {
|
|
719
|
+
const model = modelById.get(id)
|
|
720
|
+
if (!model?.read || model.column.sort === false) return
|
|
721
|
+
withAction(event, () => {
|
|
722
|
+
const current = table.atoms.sorting.get()[0]
|
|
723
|
+
const next: SortingState = current?.id !== id
|
|
724
|
+
? [{ id, desc: false }]
|
|
725
|
+
: !current.desc ? [{ id, desc: true }] : []
|
|
726
|
+
table.setSorting(next)
|
|
727
|
+
resetPage()
|
|
728
|
+
})
|
|
729
|
+
},
|
|
730
|
+
sync,
|
|
731
|
+
toggleColumn(id, visible) {
|
|
732
|
+
if (host.signal.aborted) return
|
|
733
|
+
const column = table.getColumn(id)
|
|
734
|
+
if (!column?.getCanHide()) return
|
|
735
|
+
if (!visible && table.getVisibleLeafColumns().length <= 1) return
|
|
736
|
+
column.toggleVisibility(visible)
|
|
737
|
+
},
|
|
738
|
+
togglePage(checked, event) {
|
|
739
|
+
if (!selectionEnabled) return
|
|
740
|
+
withAction(event, () => {
|
|
741
|
+
const state = { ...table.atoms.rowSelection.get() }
|
|
742
|
+
const rows = pageConfig.enabled ? table.getRowModel().rows : table.getPrePaginatedRowModel().rows
|
|
743
|
+
for (const row of rows) {
|
|
744
|
+
if (checked) state[row.id] = true
|
|
745
|
+
else delete state[row.id]
|
|
746
|
+
}
|
|
747
|
+
table.setRowSelection(state)
|
|
748
|
+
})
|
|
749
|
+
},
|
|
750
|
+
toggleRow(id, checked, event) {
|
|
751
|
+
if (!selectionEnabled || !rowById.has(id)) return
|
|
752
|
+
withAction(event, () => {
|
|
753
|
+
const state = { ...table.atoms.rowSelection.get() }
|
|
754
|
+
if (checked) state[id] = true
|
|
755
|
+
else delete state[id]
|
|
756
|
+
table.setRowSelection(state)
|
|
757
|
+
})
|
|
758
|
+
},
|
|
759
|
+
}
|
|
760
|
+
}
|