@stonecrop/atable 0.30.0 → 0.32.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.
Files changed (79) hide show
  1. package/dist/assets/index.css +804 -1
  2. package/dist/atable.js +2809 -1944
  3. package/dist/atable.js.map +1 -1
  4. package/dist/src/components/ACell.vue.d.ts +17 -0
  5. package/dist/src/components/ACell.vue.d.ts.map +1 -0
  6. package/dist/src/components/AGanttCell.vue.d.ts +34 -0
  7. package/dist/src/components/AGanttCell.vue.d.ts.map +1 -0
  8. package/dist/src/components/AGanttConnection.vue.d.ts +13 -0
  9. package/dist/src/components/AGanttConnection.vue.d.ts.map +1 -0
  10. package/dist/src/components/ARow.vue.d.ts +2693 -0
  11. package/dist/src/components/ARow.vue.d.ts.map +1 -0
  12. package/dist/src/components/ARowActions.vue.d.ts +16 -0
  13. package/dist/src/components/ARowActions.vue.d.ts.map +1 -0
  14. package/dist/src/components/ATable.vue.d.ts +13358 -0
  15. package/dist/src/components/ATable.vue.d.ts.map +1 -0
  16. package/dist/src/components/ATableColumnFilter.vue.d.ts +11 -0
  17. package/dist/src/components/ATableColumnFilter.vue.d.ts.map +1 -0
  18. package/dist/src/components/ATableHeader.vue.d.ts +20 -0
  19. package/dist/src/components/ATableHeader.vue.d.ts.map +1 -0
  20. package/dist/src/components/ATableLoading.vue.d.ts +14 -0
  21. package/dist/src/components/ATableLoading.vue.d.ts.map +1 -0
  22. package/dist/src/components/ATableLoadingBar.vue.d.ts +14 -0
  23. package/dist/src/components/ATableLoadingBar.vue.d.ts.map +1 -0
  24. package/dist/src/components/ATableModal.vue.d.ts +18 -0
  25. package/dist/src/components/ATableModal.vue.d.ts.map +1 -0
  26. package/dist/src/components/ATablePaginationFooter.vue.d.ts +17 -0
  27. package/dist/src/components/ATablePaginationFooter.vue.d.ts.map +1 -0
  28. package/dist/src/icons/index.d.ts +14 -7
  29. package/dist/src/icons/index.d.ts.map +1 -1
  30. package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
  31. package/dist/src/types/index.d.ts.map +1 -1
  32. package/package.json +35 -29
  33. package/dist/atable.d.ts +0 -3560
  34. package/dist/atable.tsbuildinfo +0 -1
  35. package/dist/icons/stonecrop-ui-icon-add.svg +0 -5
  36. package/dist/icons/stonecrop-ui-icon-delete.svg +0 -5
  37. package/dist/icons/stonecrop-ui-icon-duplicate.svg +0 -5
  38. package/dist/icons/stonecrop-ui-icon-insert-above.svg +0 -15
  39. package/dist/icons/stonecrop-ui-icon-insert-below.svg +0 -15
  40. package/dist/icons/stonecrop-ui-icon-move.svg +0 -4
  41. package/dist/icons/stonecrop-ui-icon-open.svg +0 -5
  42. package/dist/src/composables/table-pagination.js +0 -108
  43. package/dist/src/icons/index.js +0 -32
  44. package/dist/src/index.js +0 -33
  45. package/dist/src/linkSearchableText.js +0 -44
  46. package/dist/src/resolveFilterType.js +0 -27
  47. package/dist/src/schemaToColumns.js +0 -59
  48. package/dist/src/stores/table.js +0 -833
  49. package/dist/src/types/index.js +0 -0
  50. package/dist/src/utils.js +0 -39
  51. package/src/components/ACell.vue +0 -410
  52. package/src/components/AGanttCell.vue +0 -638
  53. package/src/components/AGanttConnection.vue +0 -164
  54. package/src/components/ARow.vue +0 -251
  55. package/src/components/ARowActions.vue +0 -407
  56. package/src/components/ATable.vue +0 -538
  57. package/src/components/ATableColumnFilter.vue +0 -216
  58. package/src/components/ATableHeader.vue +0 -141
  59. package/src/components/ATableLoading.vue +0 -86
  60. package/src/components/ATableLoadingBar.vue +0 -81
  61. package/src/components/ATableModal.vue +0 -52
  62. package/src/components/ATablePaginationFooter.vue +0 -100
  63. package/src/composables/table-pagination.ts +0 -172
  64. package/src/icons/index.ts +0 -38
  65. package/src/icons/stonecrop-ui-icon-add.svg +0 -5
  66. package/src/icons/stonecrop-ui-icon-delete.svg +0 -5
  67. package/src/icons/stonecrop-ui-icon-duplicate.svg +0 -5
  68. package/src/icons/stonecrop-ui-icon-insert-above.svg +0 -15
  69. package/src/icons/stonecrop-ui-icon-insert-below.svg +0 -15
  70. package/src/icons/stonecrop-ui-icon-move.svg +0 -4
  71. package/src/icons/stonecrop-ui-icon-open.svg +0 -5
  72. package/src/index.ts +0 -62
  73. package/src/linkSearchableText.ts +0 -42
  74. package/src/resolveFilterType.ts +0 -32
  75. package/src/schemaToColumns.ts +0 -67
  76. package/src/shims-vue.d.ts +0 -10
  77. package/src/stores/table.ts +0 -978
  78. package/src/types/index.ts +0 -794
  79. package/src/utils.ts +0 -40
File without changes
package/dist/src/utils.js DELETED
@@ -1,39 +0,0 @@
1
- export const isHtmlString = (htmlString) => {
2
- const $document = new DOMParser().parseFromString(htmlString, 'text/html');
3
- return Array.from($document.body.childNodes).some(node => node.nodeType === 1);
4
- };
5
- export const generateHash = (length = 8) => {
6
- return Array.from({ length }, () => Math.floor(Math.random() * 16).toString(16)).join('');
7
- };
8
- /**
9
- * Render a composite quantity value (`{ qty, uom }`, see `QuantityValue` in `@stonecrop/aform`)
10
- * as `"<qty> <uom>"`. Shared by the two paths a quantity column can reach formatting through:
11
- * the `format` that `schemaToColumns` attaches, and the table store's category-based default.
12
- */
13
- export const formatQuantity = (value) => {
14
- if (value === null || value === undefined)
15
- return '';
16
- if (typeof value === 'object') {
17
- const qty = value.qty ?? '';
18
- const uom = value.uom ?? '';
19
- return uom ? `${qty} ${uom}`.trim() : String(qty);
20
- }
21
- return String(value);
22
- };
23
- /**
24
- * Render a composite currency value (`{ amount, currency }`, see `CurrencyValue` in
25
- * `@stonecrop/aform`) as `"<amount> <currency>"`. `currency` is an AFormLinkValue FK reference —
26
- * its `displayText` is preferred, falling back to `id`. Shared by the two paths a currency column
27
- * can reach formatting through: the `format` that `schemaToColumns` attaches, and the table
28
- * store's category-based default.
29
- */
30
- export const formatCurrency = (value) => {
31
- if (value === null || value === undefined)
32
- return '';
33
- if (typeof value === 'object') {
34
- const amount = value.amount ?? '';
35
- const currency = value.currency?.displayText ?? value.currency?.id ?? '';
36
- return currency ? `${amount} ${currency}`.trim() : String(amount);
37
- }
38
- return String(value);
39
- };
@@ -1,410 +0,0 @@
1
- <template>
2
- <td
3
- ref="cell"
4
- :data-colindex="colIndex"
5
- :data-rowindex="rowIndex"
6
- :data-editable="column.edit"
7
- :contenteditable="isContentEditable"
8
- :tabindex="tabIndex"
9
- :spellcheck="false"
10
- :style="cellStyle"
11
- class="atable-cell"
12
- :class="cellClasses"
13
- @focus="onFocus"
14
- @paste="updateCellData"
15
- @input="debouncedUpdateCellData"
16
- @click="onCellClick">
17
- <component :is="column.cellComponent" v-if="column.cellComponent" v-bind="cellComponentBindings" />
18
- <component :is="'ABadge'" v-else-if="badgeFromFormat" v-bind="badgeFromFormat" presentation="cell-fill" />
19
- <component :is="'ABadge'" v-else-if="badgeFromOptions" v-bind="badgeFromOptions" />
20
- <span v-else-if="isHtmlValue" v-html="renderedValue" />
21
- <span v-else>{{ renderedValue }}</span>
22
- </td>
23
- </template>
24
-
25
- <script setup lang="ts">
26
- import { KeypressHandlers, defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
27
- import { isBadgeDescriptor, hasBadgeOptions } from '@stonecrop/schema'
28
- import { useDebounceFn, useElementBounding } from '@vueuse/core'
29
- import { computed, type CSSProperties, onMounted, ref, useTemplateRef, nextTick } from 'vue'
30
-
31
- import { createTableStore, getIndent } from '../stores/table'
32
- import { isHtmlString } from '../utils'
33
-
34
- const {
35
- colIndex,
36
- rowIndex,
37
- store,
38
- addNavigation = true,
39
- tabIndex = 0,
40
- pinned = false,
41
- debounce = 300,
42
- } = defineProps<{
43
- colIndex: number
44
- rowIndex: number
45
- store: ReturnType<typeof createTableStore>
46
- addNavigation?: boolean | KeypressHandlers
47
- tabIndex?: number
48
- pinned?: boolean
49
- debounce?: number
50
- }>()
51
-
52
- const cellRef = useTemplateRef<HTMLTableCellElement>('cell')
53
-
54
- // keep a shallow copy of the original cell value for comparison
55
- const originalData = store.getCellData(colIndex, rowIndex)
56
- const currentData = ref('')
57
- const cellModified = ref(false)
58
-
59
- const column = store.columns[colIndex]
60
- const row = store.rows[rowIndex]
61
-
62
- const textAlign = column.align || 'center'
63
- const cellWidth = column.width || '40ch'
64
-
65
- const displayValue = computed(() => store.getCellDisplayValue(colIndex, rowIndex))
66
-
67
- // Resolved display text for Link columns with bare string IDs.
68
- // null = not yet resolved or not applicable; non-null overrides displayValue.
69
- const resolvedText = ref<string | null>(null)
70
-
71
- onMounted(() => {
72
- if (!column.linkDoctype) return
73
- const raw = store.getCellData(colIndex, rowIndex)
74
-
75
- // Server returns link fields as { id, displayText } objects.
76
- if (raw !== null && raw !== undefined && typeof raw === 'object') {
77
- const obj = raw as Record<string, unknown>
78
- const display = obj.displayText ?? obj.id
79
- if (typeof display === 'string' || typeof display === 'number') {
80
- resolvedText.value = String(display)
81
- }
82
- return
83
- }
84
-
85
- // Bare string ID — call the resolver if available.
86
- const resolver = store.linkResolver
87
- if (!resolver || typeof raw !== 'string' || raw === '') return
88
- void resolver(column.linkDoctype, raw).then(text => {
89
- if (text != null) resolvedText.value = text
90
- return text
91
- })
92
- })
93
-
94
- const renderedValue = computed(() => resolvedText.value ?? displayValue.value)
95
-
96
- const badgeFromFormat = computed(() => {
97
- if (column.cellComponent) return undefined
98
- const value = renderedValue.value
99
- return isBadgeDescriptor(value) ? value : undefined
100
- })
101
-
102
- const badgeFromOptions = computed(() => {
103
- if (column.cellComponent || badgeFromFormat.value) return undefined
104
- if (!hasBadgeOptions(column.options)) return undefined
105
- return {
106
- value: store.getCellData(colIndex, rowIndex),
107
- options: column.options,
108
- presentation: 'cell-fill' as const,
109
- }
110
- })
111
-
112
- const usesBadgeDisplay = computed(
113
- () => !!column.cellComponent || badgeFromFormat.value !== undefined || hasBadgeOptions(column.options)
114
- )
115
-
116
- const isContentEditable = computed(() => column.edit && !usesBadgeDisplay.value)
117
-
118
- const cellComponentBindings = computed(() => {
119
- const props = { ...column.cellComponentProps }
120
- if (column.cellComponent !== 'ABadge') {
121
- return { ...props, value: renderedValue.value }
122
- }
123
- if (isBadgeDescriptor(renderedValue.value)) {
124
- return { presentation: 'cell-fill' as const, ...props, ...renderedValue.value }
125
- }
126
- // ABadge resolves its own label, so it needs the badge map as well as the value — and the value
127
- // has to be the stored one, because badge maps are keyed on that rather than on formatted text.
128
- return {
129
- presentation: 'cell-fill' as const,
130
- options: column.options,
131
- ...props,
132
- value: store.getCellData(colIndex, rowIndex),
133
- }
134
- })
135
-
136
- const isHtmlValue = computed(() => {
137
- // TODO: check if display value is a native DOM element
138
- return typeof renderedValue.value === 'string' ? isHtmlString(renderedValue.value) : false
139
- })
140
-
141
- const cellStyle = computed((): CSSProperties => {
142
- return {
143
- textAlign,
144
- width: cellWidth,
145
- fontWeight: !cellModified.value ? 'inherit' : 'bold',
146
- paddingLeft: getIndent(colIndex, store.display[rowIndex]?.indent),
147
- }
148
- })
149
-
150
- const cellClasses = computed(() => {
151
- return {
152
- 'sticky-column': pinned,
153
- 'cell-modified': cellModified.value,
154
- 'atable-cell--badge-fill': usesBadgeDisplay.value,
155
- }
156
- })
157
-
158
- const onCellClick = () => {
159
- // First, select all text if the cell is editable
160
- selectAllText()
161
-
162
- // Then handle modal display (original showModal behavior)
163
- showModal()
164
- }
165
-
166
- const showModal = () => {
167
- const { left, bottom, width, height } = useElementBounding(cellRef)
168
-
169
- if (column.mask) {
170
- // TODO: add masking to cell values
171
- // column.mask(event)
172
- }
173
-
174
- if (column.modalComponent) {
175
- store.$patch(state => {
176
- state.modal.visible = true
177
- state.modal.colIndex = colIndex
178
- state.modal.rowIndex = rowIndex
179
- // Snapshot the cell's box as it was when the modal opened. These are refs, and assigning
180
- // them raw happened to work only because reactive reads unwrap refs — which also left the
181
- // stored values live, tracking the cell for as long as the modal stayed open.
182
- state.modal.left = left.value
183
- state.modal.bottom = bottom.value
184
- state.modal.width = width.value
185
- state.modal.height = height.value
186
- state.modal.cell = cellRef.value
187
-
188
- if (typeof column.modalComponent === 'function') {
189
- // `table` is a computed, so Pinia exposes it as a getter — it is not on `$state`, which
190
- // is what $patch hands back. Reading it off the store is what passes a real table.
191
- state.modal.component = column.modalComponent({ table: store.table, row, column })
192
- } else {
193
- state.modal.component = column.modalComponent
194
- }
195
-
196
- state.modal.componentProps = column.modalComponentExtraProps
197
- })
198
- }
199
- }
200
-
201
- if (addNavigation) {
202
- let handlers = {
203
- ...defaultKeypressHandlers,
204
- 'keydown.f2': showModal,
205
- 'keydown.alt.up': showModal,
206
- 'keydown.alt.down': showModal,
207
- 'keydown.alt.left': showModal,
208
- 'keydown.alt.right': showModal,
209
- }
210
-
211
- if (typeof addNavigation === 'object') {
212
- handlers = {
213
- ...handlers,
214
- ...addNavigation,
215
- }
216
- }
217
-
218
- useKeyboardNav([
219
- {
220
- selectors: cellRef,
221
- handlers: handlers,
222
- },
223
- ])
224
- }
225
-
226
- // const updateData = (event: Event) => {
227
- // if (event) {
228
- // // custom components need to handle their own updateData, this is the default
229
- // if (!column.component) {
230
- // store.setCellData(colIndex, rowIndex, cell.value.innerHTML)
231
- // }
232
- // cellModified.value = true
233
- // }
234
- // }
235
-
236
- const selectAllText = () => {
237
- if (cellRef.value && column.edit) {
238
- // Use the Selection API to select all text content in the contenteditable cell
239
- const selection = window.getSelection()
240
- if (selection) {
241
- try {
242
- const range = document.createRange()
243
- if (range.selectNodeContents) {
244
- range.selectNodeContents(cellRef.value)
245
- selection.removeAllRanges()
246
- selection.addRange(range)
247
- }
248
- } catch {
249
- // Fallback for environments where Range API is not fully supported
250
- // This is expected in some test environments
251
- }
252
- }
253
- }
254
- }
255
-
256
- const onFocus = () => {
257
- if (cellRef.value) {
258
- currentData.value = cellRef.value.textContent!
259
- // Select all text when the cell receives focus
260
- selectAllText()
261
- }
262
- }
263
-
264
- const saveCursorPosition = () => {
265
- try {
266
- const selection = window.getSelection()
267
- if (selection && selection.rangeCount > 0 && cellRef.value) {
268
- const range = selection.getRangeAt(0)
269
- // Save the offset from the start of the cell
270
- const preCaretRange = range.cloneRange()
271
- if (preCaretRange.selectNodeContents && preCaretRange.setEnd) {
272
- preCaretRange.selectNodeContents(cellRef.value)
273
- preCaretRange.setEnd(range.endContainer, range.endOffset)
274
- return preCaretRange.toString().length
275
- }
276
- }
277
- } catch {
278
- // Fallback for environments where Selection API is not fully supported
279
- }
280
- return 0
281
- }
282
-
283
- const restoreCursorPosition = (position: number) => {
284
- if (!cellRef.value) return
285
-
286
- try {
287
- const selection = window.getSelection()
288
- if (!selection) return
289
-
290
- let charIndex = 0
291
- const walker = document.createTreeWalker
292
- ? document.createTreeWalker(cellRef.value, NodeFilter.SHOW_TEXT, null)
293
- : null
294
-
295
- if (!walker) return
296
-
297
- let node: Node | null
298
- let range: Range | null = null
299
-
300
- while ((node = walker.nextNode())) {
301
- const textNode = node as Text
302
- const nextCharIndex = charIndex + textNode.textContent.length
303
-
304
- if (position <= nextCharIndex) {
305
- range = document.createRange()
306
- if (range.setStart && range.setEnd) {
307
- range.setStart(textNode, position - charIndex)
308
- range.setEnd(textNode, position - charIndex)
309
- break
310
- }
311
- }
312
- charIndex = nextCharIndex
313
- }
314
-
315
- if (range && selection.removeAllRanges && selection.addRange) {
316
- selection.removeAllRanges()
317
- selection.addRange(range)
318
- }
319
- } catch {
320
- // Fallback for environments where DOM APIs are not fully supported
321
- }
322
- }
323
-
324
- const updateCellData = (payload: Event) => {
325
- if (!column.edit) return
326
-
327
- const target = payload.target as HTMLTableCellElement
328
- if (target.textContent === currentData.value) {
329
- return
330
- }
331
-
332
- // Save cursor position before updating
333
- const cursorPosition = saveCursorPosition()
334
-
335
- currentData.value = target.textContent!
336
-
337
- // only apply changes if the cell value has changed after being mounted
338
- if (column.format) {
339
- cellModified.value = target.textContent !== store.getFormattedValue(colIndex, rowIndex, originalData)
340
- // TODO: need to setup reverse format function?
341
- store.setCellText(colIndex, rowIndex, target.textContent)
342
- } else {
343
- cellModified.value = target.textContent !== originalData
344
- store.setCellData(colIndex, rowIndex, target.textContent)
345
- }
346
-
347
- // Use nextTick to restore cursor position after Vue's reactive updates but before browser repaint
348
- void nextTick().then(() => {
349
- return restoreCursorPosition(cursorPosition)
350
- })
351
- }
352
-
353
- const debouncedUpdateCellData = useDebounceFn(updateCellData, debounce)
354
-
355
- defineExpose({
356
- currentData,
357
- })
358
- </script>
359
-
360
- <style>
361
- .atable-cell {
362
- border-radius: 0px;
363
- box-sizing: border-box;
364
- margin: 0px;
365
- outline: none;
366
- box-shadow: none;
367
- color: var(--sc-cell-text-color);
368
- overflow: hidden;
369
- padding-left: 0.5ch !important;
370
- padding-right: 0.5ch;
371
- padding-top: var(--sc-atable-row-padding);
372
- padding-bottom: var(--sc-atable-row-padding);
373
- border-spacing: 0px;
374
- border-collapse: collapse;
375
- overflow: hidden;
376
- text-overflow: ellipsis;
377
- order: 1;
378
- white-space: nowrap;
379
- max-width: 40ch;
380
- border-top: 1px solid var(--sc-row-border-color);
381
- margin-left: 1px;
382
- }
383
- .atable-cell a {
384
- color: var(--sc-cell-text-color);
385
- text-decoration: none;
386
- }
387
- .atable-cell:focus,
388
- .atable-cell:focus-within {
389
- background-color: var(--sc-focus-cell-background);
390
- outline-width: var(--sc-atable-cell-border-width);
391
- outline-style: solid;
392
- outline-offset: calc(var(--sc-atable-cell-border-width) * -1);
393
- outline-color: var(--sc-focus-cell-outline);
394
- box-shadow: none;
395
- overflow: hidden;
396
- text-wrap: nowrap;
397
- box-sizing: border-box;
398
- }
399
- .cell-modified {
400
- font-weight: bold;
401
- font-style: italic;
402
- }
403
- .cell-modified-highlight {
404
- background-color: var(--sc-cell-changed-color);
405
- }
406
- .atable-cell--badge-fill {
407
- padding: 0 !important;
408
- margin-left: 0;
409
- }
410
- </style>