viur-ui 0.0.1 → 0.0.3
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/README.md +67 -41
- package/index.d.ts +18 -9
- package/index.js +6 -3
- package/package.json +6 -6
- package/src/components/Browser/Browser.vue +58 -58
- package/src/components/Browser/TreeNav.vue +32 -32
- package/src/components/Browser/ViurBrowser.d.vue.ts +21 -21
- package/src/components/Browser/ViurBrowser.vue +47 -44
- package/src/components/Browser/dragSession.ts +27 -27
- package/src/components/Browser/index.ts +3 -3
- package/src/components/Browser/types.ts +9 -9
- package/src/components/DataTable/ColumnFilterPanel.vue +7 -7
- package/src/components/DataTable/DataTable.vue +359 -351
- package/src/components/DataTable/ViurDataTable.d.vue.ts +26 -26
- package/src/components/DataTable/ViurDataTable.vue +51 -50
- package/src/components/DataTable/columns.ts +30 -30
- package/src/components/DataTable/index.ts +12 -12
- package/src/components/DataTable/rows.ts +71 -72
- package/src/components/DataTable/types.ts +55 -55
- package/src/components/DataTable/useColumnLayout.ts +28 -28
- package/src/components/DataTable/useGridNavigation.ts +74 -15
- package/src/components/DataTable/useVirtualRows.ts +10 -10
- package/src/components/DataTable/viurColumns.ts +69 -69
- package/src/forms/ViForm.d.vue.ts +32 -32
- package/src/forms/ViForm.vue +17 -16
- package/src/forms/ViFormCategory.vue +3 -1
- package/src/forms/bones/Bone.vue +125 -108
- package/src/forms/bones/BoneActions.vue +21 -21
- package/src/forms/bones/BoneLabel.vue +1 -1
- package/src/forms/bones/BoneMultipleWrapper.vue +3 -3
- package/src/forms/bones/BoneNestedWrapper.vue +3 -3
- package/src/forms/bones/BoneSingleEntry.vue +2 -2
- package/src/forms/bones/FilterBone.vue +3 -3
- package/src/forms/bones/actions/ActionDialog.vue +2 -2
- package/src/forms/bones/actions/Debug.vue +6 -6
- package/src/forms/bones/actions/Delete.vue +2 -2
- package/src/forms/bones/actions/DescribeImage.vue +4 -3
- package/src/forms/bones/actions/Download.vue +4 -4
- package/src/forms/bones/actions/Placeholder.vue +2 -2
- package/src/forms/bones/actions/index.js +72 -74
- package/src/forms/bones/bone.d.ts +19 -19
- package/src/forms/bones/uploadAdapter.js +3 -3
- package/src/forms/bones/utils.js +40 -41
- package/src/forms/bones/view/base_item.vue +1 -1
- package/src/forms/bones/view/boneLogic.js +1 -1
- package/src/forms/bones/widgets/booleanBone.vue +19 -19
- package/src/forms/bones/widgets/booleanBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/booleanBoneSelect.vue +3 -3
- package/src/forms/bones/widgets/codeBone.vue +22 -22
- package/src/forms/bones/widgets/colorBone.vue +18 -18
- package/src/forms/bones/widgets/dateBone.vue +22 -22
- package/src/forms/bones/widgets/emailBone.vue +20 -20
- package/src/forms/bones/widgets/fileBone.vue +32 -31
- package/src/forms/bones/widgets/keyBone.vue +21 -21
- package/src/forms/bones/widgets/numericBone.vue +21 -21
- package/src/forms/bones/widgets/passwordBone.vue +34 -31
- package/src/forms/bones/widgets/phoneBone.vue +20 -20
- package/src/forms/bones/widgets/rawBone.vue +19 -19
- package/src/forms/bones/widgets/recordBone.vue +16 -16
- package/src/forms/bones/widgets/relationalBone.vue +26 -26
- package/src/forms/bones/widgets/selectBone.vue +23 -23
- package/src/forms/bones/widgets/selectBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/spatialBone.vue +21 -21
- package/src/forms/bones/widgets/stringBone.vue +27 -27
- package/src/forms/bones/widgets/textBone.vue +28 -26
- package/src/forms/install.d.ts +1 -1
- package/src/forms/install.js +7 -0
- package/src/forms/layouts/LayloutList2Columns.vue +1 -1
- package/src/forms/layouts/LayoutCategory.vue +1 -1
- package/src/forms/layouts/LayoutList.vue +1 -1
- package/src/forms/utils.js +22 -22
- package/src/request/configStore.js +4 -4
- package/src/request/envelope.js +6 -6
- package/src/request/request.js +211 -121
- package/src/translations/de.js +31 -0
- package/src/translations/en.js +31 -0
- package/src/user/LoginScreen.d.vue.ts +24 -24
- package/src/user/LoginScreen.vue +6 -7
- package/src/user/store.d.ts +16 -16
- package/src/vite/safeHtml.js +73 -0
- package/src/vite/sanitizeVHtml.d.ts +4 -0
- package/src/vite/sanitizeVHtml.js +15 -0
- package/src/webawesomeCompat.css +0 -40
- package/src/public/assets/icons/fontawesome-manifest.json +0 -8
- package/src/public/assets/icons/regular/file.svg +0 -1
- package/src/public/assets/icons/solid/arrows-left-right.svg +0 -1
- package/src/public/assets/icons/solid/bug.svg +0 -1
- package/src/public/assets/icons/solid/check.svg +0 -1
- package/src/public/assets/icons/solid/circle-info.svg +0 -1
- package/src/public/assets/icons/solid/compress.svg +0 -1
- package/src/public/assets/icons/solid/download.svg +0 -1
- package/src/public/assets/icons/solid/expand.svg +0 -1
- package/src/public/assets/icons/solid/grip-vertical.svg +0 -1
- package/src/public/assets/icons/solid/plus.svg +0 -1
- package/src/public/assets/icons/solid/question.svg +0 -1
- package/src/public/assets/icons/solid/triangle-exclamation.svg +0 -1
- package/src/public/assets/icons/solid/upload.svg +0 -1
- package/src/public/assets/icons/solid/xmark.svg +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<!-- Vue
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<!-- Vue reimplementation of <wa-data-grid> (WebAwesome Pro). Columns are data,
|
|
3
|
+
cell contents come from slots (`cell:<id>`), mutable state
|
|
4
|
+
flows through v-model. Everything else lives in the composables. -->
|
|
5
5
|
<div
|
|
6
6
|
class="vi-dt"
|
|
7
7
|
:class="[
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
]"
|
|
16
16
|
:style="state.hostStyle"
|
|
17
17
|
>
|
|
18
|
-
<!-- ── Toolbar:
|
|
18
|
+
<!-- ── Toolbar: search + columns menu ───────────────────────────── -->
|
|
19
19
|
<div
|
|
20
20
|
v-if="state.showToolbar"
|
|
21
21
|
class="vi-dt-toolbar wa-split wa-gap-s wa-flex-nowrap"
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
</div>
|
|
87
87
|
</div>
|
|
88
88
|
|
|
89
|
-
<!-- ── Scroll
|
|
89
|
+
<!-- ── Scroll area: header, body, column footers ────────────────── -->
|
|
90
90
|
<div
|
|
91
91
|
ref="scrollerEl"
|
|
92
92
|
class="vi-dt-scroller"
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
@dragend="layout.endDrag()"
|
|
139
139
|
@drop.prevent="layout.endDrag()"
|
|
140
140
|
>
|
|
141
|
-
<!--
|
|
141
|
+
<!-- Selection header cell: everything on this page -->
|
|
142
142
|
<input
|
|
143
143
|
v-if="cell.kind === 'select' && selectable === 'multiple'"
|
|
144
144
|
class="vi-dt-checkbox vi-dt-select-all"
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
v-for="displayRow in virtual.state.windowRows"
|
|
272
272
|
:key="displayRow.key"
|
|
273
273
|
>
|
|
274
|
-
<!-- Detail
|
|
274
|
+
<!-- Detail panel: its own full-width row -->
|
|
275
275
|
<div
|
|
276
276
|
v-if="displayRow.kind === 'detail'"
|
|
277
277
|
class="vi-dt-detailrow"
|
|
@@ -310,12 +310,12 @@
|
|
|
310
310
|
role="row"
|
|
311
311
|
:style="state.gridStyle"
|
|
312
312
|
:data-key="displayRow.key"
|
|
313
|
-
@dblclick="onRowDoubleClick(displayRow, $event)"
|
|
314
313
|
:aria-selected="
|
|
315
314
|
selectable !== false && displayRow.kind === 'data' ? state.selectedSet.has(displayRow.nodeKey) : undefined
|
|
316
315
|
"
|
|
317
316
|
:aria-level="state.isTree ? displayRow.depth + 1 : undefined"
|
|
318
317
|
:aria-expanded="displayRow.expandable ? displayRow.expanded : undefined"
|
|
318
|
+
@dblclick="onRowDoubleClick(displayRow, $event)"
|
|
319
319
|
>
|
|
320
320
|
<div
|
|
321
321
|
v-for="cell in state.cellColumns"
|
|
@@ -344,7 +344,7 @@
|
|
|
344
344
|
@click="onCellClick(displayRow, cell, $event)"
|
|
345
345
|
@contextmenu="onCellContextMenu(displayRow, cell, $event)"
|
|
346
346
|
>
|
|
347
|
-
<!--
|
|
347
|
+
<!-- Drag handle for the fixed order -->
|
|
348
348
|
<span
|
|
349
349
|
v-if="cell.kind === 'order' && displayRow.kind === 'data'"
|
|
350
350
|
class="vi-dt-grip"
|
|
@@ -356,7 +356,7 @@
|
|
|
356
356
|
<wa-icon name="grip-vertical"></wa-icon>
|
|
357
357
|
</span>
|
|
358
358
|
|
|
359
|
-
<!--
|
|
359
|
+
<!-- Selection -->
|
|
360
360
|
<input
|
|
361
361
|
v-if="cell.kind === 'select' && selectable !== false"
|
|
362
362
|
class="vi-dt-checkbox"
|
|
@@ -368,7 +368,7 @@
|
|
|
368
368
|
@click.stop="onSelectRow(displayRow, $event as MouseEvent)"
|
|
369
369
|
/>
|
|
370
370
|
|
|
371
|
-
<!--
|
|
371
|
+
<!-- Expand column (tree, groups, detail panels) -->
|
|
372
372
|
<template v-if="cell.kind === 'expand'">
|
|
373
373
|
<wa-button
|
|
374
374
|
v-if="displayRow.expandable"
|
|
@@ -376,14 +376,14 @@
|
|
|
376
376
|
appearance="plain"
|
|
377
377
|
size="small"
|
|
378
378
|
:aria-expanded="displayRow.expanded"
|
|
379
|
-
:aria-label="displayRow.expanded ? '
|
|
379
|
+
:aria-label="displayRow.expanded ? t('dataTable.collapseRow') : t('dataTable.expandRow')"
|
|
380
380
|
@click.stop="toggleExpand(displayRow)"
|
|
381
381
|
>
|
|
382
382
|
<wa-icon :name="displayRow.expanded ? 'chevron-down' : 'chevron-right'"></wa-icon>
|
|
383
383
|
</wa-button>
|
|
384
384
|
</template>
|
|
385
385
|
|
|
386
|
-
<!--
|
|
386
|
+
<!-- Group row: value + count in the first data column, aggregates elsewhere -->
|
|
387
387
|
<template v-else-if="displayRow.kind === 'group' && cell.kind === 'data'">
|
|
388
388
|
<template v-if="cell.index === state.firstDataCellIndex">
|
|
389
389
|
<slot
|
|
@@ -402,13 +402,13 @@
|
|
|
402
402
|
</span>
|
|
403
403
|
</template>
|
|
404
404
|
|
|
405
|
-
<!--
|
|
405
|
+
<!-- Data cell -->
|
|
406
406
|
<template v-else-if="cell.kind === 'data'">
|
|
407
|
-
<!--
|
|
408
|
-
|
|
409
|
-
|
|
407
|
+
<!-- Tree data: guide lines, then arrow (or placeholder). One
|
|
408
|
+
slot per level — a line is only drawn where another
|
|
409
|
+
sibling still follows below the ancestor. -->
|
|
410
410
|
<template v-if="state.isTree && cell.index === state.firstDataCellIndex">
|
|
411
|
-
<!--
|
|
411
|
+
<!-- Pass-through columns … -->
|
|
412
412
|
<span
|
|
413
413
|
v-for="(line, level) in displayRow.ancestorLines"
|
|
414
414
|
:key="level"
|
|
@@ -416,7 +416,7 @@
|
|
|
416
416
|
:class="{ 'is-line': line }"
|
|
417
417
|
aria-hidden="true"
|
|
418
418
|
></span>
|
|
419
|
-
<!-- …
|
|
419
|
+
<!-- … and the row's own corner. -->
|
|
420
420
|
<span
|
|
421
421
|
v-if="displayRow.depth > 0"
|
|
422
422
|
class="vi-dt-guide"
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
:name="displayRow.expanded ? 'chevron-down' : 'chevron-right'"
|
|
451
451
|
></wa-icon>
|
|
452
452
|
</wa-button>
|
|
453
|
-
<!--
|
|
453
|
+
<!-- Leaf: same spacing as between arrow and lines. -->
|
|
454
454
|
<span
|
|
455
455
|
v-else
|
|
456
456
|
class="vi-dt-leaf-gap"
|
|
@@ -491,7 +491,7 @@
|
|
|
491
491
|
</template>
|
|
492
492
|
</div>
|
|
493
493
|
|
|
494
|
-
<!--
|
|
494
|
+
<!-- Column footers, at the bottom edge of the scroll area -->
|
|
495
495
|
<div
|
|
496
496
|
v-if="state.hasColumnFooters"
|
|
497
497
|
class="vi-dt-foot"
|
|
@@ -531,7 +531,7 @@
|
|
|
531
531
|
</div>
|
|
532
532
|
</div>
|
|
533
533
|
|
|
534
|
-
<!--
|
|
534
|
+
<!-- Empty / no results -->
|
|
535
535
|
<div
|
|
536
536
|
v-if="state.showEmpty"
|
|
537
537
|
class="vi-dt-state"
|
|
@@ -555,7 +555,7 @@
|
|
|
555
555
|
</slot>
|
|
556
556
|
</div>
|
|
557
557
|
|
|
558
|
-
<!--
|
|
558
|
+
<!-- Loading overlay (server mode) -->
|
|
559
559
|
<div
|
|
560
560
|
v-if="state.isLoading"
|
|
561
561
|
class="vi-dt-loading"
|
|
@@ -564,14 +564,14 @@
|
|
|
564
564
|
</div>
|
|
565
565
|
</div>
|
|
566
566
|
|
|
567
|
-
<!-- ──
|
|
567
|
+
<!-- ── Load more (cursor sources) ───────────────────────────────── -->
|
|
568
568
|
<div
|
|
569
569
|
v-if="state.cursorMode"
|
|
570
570
|
class="vi-dt-more wa-split wa-gap-s wa-flex-nowrap"
|
|
571
571
|
>
|
|
572
572
|
<span class="vi-dt-moreinfo">{{ state.loadedText }}</span>
|
|
573
573
|
<div class="wa-cluster wa-gap-2xs wa-flex-nowrap">
|
|
574
|
-
<!--
|
|
574
|
+
<!-- Slot for caller content in the bar (e.g. reload action). -->
|
|
575
575
|
<slot name="cursor-start" />
|
|
576
576
|
<select
|
|
577
577
|
class="vi-dt-pagesize"
|
|
@@ -693,7 +693,7 @@
|
|
|
693
693
|
</div>
|
|
694
694
|
</div>
|
|
695
695
|
|
|
696
|
-
<!-- Filter
|
|
696
|
+
<!-- Filter popover per visible filterable column (after their anchors!) -->
|
|
697
697
|
<wa-popover
|
|
698
698
|
v-for="column in state.filterColumns"
|
|
699
699
|
:key="column.id"
|
|
@@ -722,11 +722,11 @@
|
|
|
722
722
|
import { useI18n } from "vue-i18n"
|
|
723
723
|
import { computed, inject, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch, shallowRef } from "vue"
|
|
724
724
|
import ColumnFilterPanel from "./ColumnFilterPanel.vue"
|
|
725
|
-
import { useColumnLayout, type LayoutColumn } from "./useColumnLayout"
|
|
726
|
-
import { useGridNavigation } from "./useGridNavigation"
|
|
727
|
-
import { useVirtualRows } from "./useVirtualRows"
|
|
728
|
-
import { DRAG_SESSION } from "../Browser/dragSession"
|
|
729
|
-
import { aggregateColumn, defaultText, findColumn, isEmptyFilter, nextSortState, resolveColumns } from "./columns"
|
|
725
|
+
import { useColumnLayout, type LayoutColumn } from "./useColumnLayout.ts"
|
|
726
|
+
import { useGridNavigation } from "./useGridNavigation.ts"
|
|
727
|
+
import { useVirtualRows } from "./useVirtualRows.ts"
|
|
728
|
+
import { DRAG_SESSION } from "../Browser/dragSession.ts"
|
|
729
|
+
import { aggregateColumn, defaultText, findColumn, isEmptyFilter, nextSortState, resolveColumns } from "./columns.ts"
|
|
730
730
|
import {
|
|
731
731
|
buildRows,
|
|
732
732
|
collectKeys,
|
|
@@ -742,7 +742,7 @@ import {
|
|
|
742
742
|
type NestedTree,
|
|
743
743
|
type SelectionState,
|
|
744
744
|
type NodeLocation,
|
|
745
|
-
} from "./rows"
|
|
745
|
+
} from "./rows.ts"
|
|
746
746
|
import type {
|
|
747
747
|
ColumnFacets,
|
|
748
748
|
DataTableColumn,
|
|
@@ -758,27 +758,27 @@ import type {
|
|
|
758
758
|
RowNode,
|
|
759
759
|
SortEntry,
|
|
760
760
|
CellBadge,
|
|
761
|
-
} from "./types"
|
|
761
|
+
} from "./types.ts"
|
|
762
762
|
|
|
763
763
|
const props = withDefaults(
|
|
764
764
|
defineProps<{
|
|
765
765
|
columns: DataTableColumn<R>[]
|
|
766
766
|
data?: R[]
|
|
767
|
-
/**
|
|
767
|
+
/** Field with a stable row id — for selection across sorting/pages. */
|
|
768
768
|
rowKey?: string | null
|
|
769
769
|
label?: string | null
|
|
770
770
|
selectable?: false | "single" | "multiple"
|
|
771
771
|
selectableRows?: ((row: R) => boolean) | null
|
|
772
|
-
/**
|
|
772
|
+
/** With selection enabled, a left click on the row selects the entry (Shift: range). */
|
|
773
773
|
selectOnRowClick?: boolean
|
|
774
774
|
/**
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
775
|
+
* Reset the lazy-loaded tree (`loadChildren`) on every external data
|
|
776
|
+
* change (arrows collapsed, children cache empty). Default false: a drop
|
|
777
|
+
* stays a purely local move and nothing collapses unasked —
|
|
778
|
+
* resetting only happens via `reload()` / `resetTree()`.
|
|
779
779
|
*/
|
|
780
780
|
resetTreeOnDataChange?: boolean
|
|
781
|
-
/** Container
|
|
781
|
+
/** Container id of this table in an enclosing drag session (Browser). */
|
|
782
782
|
dragSource?: string
|
|
783
783
|
striped?: boolean
|
|
784
784
|
size?: "xs" | "s" | "m" | "l" | "xl"
|
|
@@ -794,69 +794,69 @@ const props = withDefaults(
|
|
|
794
794
|
groupBy?: string | string[] | null
|
|
795
795
|
childRows?: string | ((row: R) => R[] | undefined | null) | null
|
|
796
796
|
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
* `childRows`,
|
|
797
|
+
* Field referencing the parent row (ViUR: `parententry`). With it the
|
|
798
|
+
* component builds the tree itself from a FLAT list — `rowKey`
|
|
799
|
+
* is required for this. Already nested data still goes through
|
|
800
|
+
* `childRows`, which then takes precedence.
|
|
801
801
|
*/
|
|
802
802
|
parentField?: string | null
|
|
803
803
|
/**
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
804
|
+
* Load the children of a level on demand (ViUR: `list` with `parententry`).
|
|
805
|
+
* When set, EVERY data row gets an expand arrow, because it is unknown
|
|
806
|
+
* beforehand whether there are children. After loading the arrow stays
|
|
807
|
+
* and is disabled if nothing came back — so the layout doesn't jump.
|
|
808
808
|
*/
|
|
809
809
|
loadChildren?: ((row: R) => Promise<R[]>) | null
|
|
810
810
|
/**
|
|
811
|
-
*
|
|
812
|
-
* `node` vs. `leaf`),
|
|
813
|
-
* `true`/`undefined` →
|
|
811
|
+
* If the application knows in advance whether a row CAN have children (in ViUR e.g.
|
|
812
|
+
* `node` vs. `leaf`), report it here: `false` → no arrow at all,
|
|
813
|
+
* `true`/`undefined` → arrow, loading happens on expand.
|
|
814
814
|
*/
|
|
815
815
|
rowHasChildren?: ((row: R) => boolean | undefined) | null
|
|
816
816
|
filterFromLeafRows?: boolean
|
|
817
|
-
/**
|
|
817
|
+
/** Rows get a detail panel (content: slot `row-detail`). */
|
|
818
818
|
rowDetail?: boolean
|
|
819
819
|
rowClass?: ((row: R) => string | null | undefined) | null
|
|
820
820
|
/**
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
* `row-reorder`,
|
|
821
|
+
* Field holding the numeric order (ViUR: "sortindex"). When set,
|
|
822
|
+
* the table is fixed-sorted by it and rows can be reordered by
|
|
823
|
+
* dragging — the component reports the new value via
|
|
824
|
+
* `row-reorder`, saving happens outside.
|
|
825
825
|
*/
|
|
826
826
|
orderField?: string | null
|
|
827
|
-
/**
|
|
827
|
+
/** Step for new values at the start/end of the list. */
|
|
828
828
|
orderStep?: number
|
|
829
829
|
sortDescFirst?: boolean
|
|
830
830
|
withoutSortRemoval?: boolean
|
|
831
831
|
maxMultiSort?: number
|
|
832
832
|
searchFn?: ((value: unknown, term: string, row: R) => boolean) | null
|
|
833
|
-
/** Server
|
|
833
|
+
/** Server mode without callback: the component emits `data-request`. */
|
|
834
834
|
/**
|
|
835
|
-
* Cursor
|
|
836
|
-
*
|
|
837
|
-
*
|
|
835
|
+
* Cursor sources (e.g. ViUR lists) load forward instead of jumping between
|
|
836
|
+
* pages. If `hasMore` is set (even `false`), a bar with "Load more" appears
|
|
837
|
+
* instead of the pager; the caller does the loading on `load-more`.
|
|
838
838
|
*/
|
|
839
839
|
hasMore?: boolean | null
|
|
840
840
|
loadMoreLabel?: string | null
|
|
841
841
|
loadAllLabel?: string | null
|
|
842
|
-
/**
|
|
842
|
+
/** Quick pick "load the next N pages" in the cursor bar. */
|
|
843
843
|
loadMorePages?: number[]
|
|
844
|
-
/**
|
|
844
|
+
/** Page sizes of the cursor bar (ViUR: limit, max 99). */
|
|
845
845
|
cursorPageSizes?: number[]
|
|
846
846
|
server?: boolean
|
|
847
847
|
dataSource?: ((request: DataTableRequest) => Promise<DataTableResponse<R>>) | null
|
|
848
848
|
total?: number
|
|
849
849
|
loading?: boolean
|
|
850
850
|
filterDebounce?: number
|
|
851
|
-
/** true/false
|
|
851
|
+
/** Force true/false, "auto" from `virtualizeThreshold` rows on. */
|
|
852
852
|
virtualize?: boolean | "auto"
|
|
853
853
|
virtualizeThreshold?: number
|
|
854
854
|
rowHeight?: number
|
|
855
855
|
/**
|
|
856
|
-
*
|
|
857
|
-
* `--dt-max-height: 30rem` —
|
|
858
|
-
*
|
|
859
|
-
*
|
|
856
|
+
* Cap for the scroll area. WITHOUT a value the CSS default
|
|
857
|
+
* `--dt-max-height: 30rem` applies — so the variable stays overridable
|
|
858
|
+
* from outside (e.g. `--dt-max-height: none`, so the table fills an
|
|
859
|
+
* area). A set value is applied inline and wins.
|
|
860
860
|
*/
|
|
861
861
|
maxHeight?: string
|
|
862
862
|
}>(),
|
|
@@ -907,12 +907,12 @@ const props = withDefaults(
|
|
|
907
907
|
virtualize: "auto",
|
|
908
908
|
virtualizeThreshold: 60,
|
|
909
909
|
rowHeight: 44,
|
|
910
|
-
//
|
|
910
|
+
// No default: see maxHeight above — the cap comes from the CSS.
|
|
911
911
|
maxHeight: undefined,
|
|
912
912
|
}
|
|
913
913
|
)
|
|
914
914
|
|
|
915
|
-
//
|
|
915
|
+
// State that can be tracked from outside — one v-model each.
|
|
916
916
|
const sort = defineModel<SortEntry[]>("sort", { default: () => [] })
|
|
917
917
|
const filters = defineModel<FilterEntry[]>("filters", { default: () => [] })
|
|
918
918
|
const search = defineModel<string>("search", { default: "" })
|
|
@@ -923,7 +923,7 @@ const pageSize = defineModel<number>("pageSize", { default: 20 })
|
|
|
923
923
|
|
|
924
924
|
const emit = defineEmits<{
|
|
925
925
|
"cell-click": [payload: { column: string; value: unknown; row: R; index: number }]
|
|
926
|
-
/**
|
|
926
|
+
/** Double click on a data row — typically to open the record. */
|
|
927
927
|
"row-dblclick": [payload: { key: RowKey; row: R; index: number; originalEvent: MouseEvent }]
|
|
928
928
|
"cell-contextmenu": [payload: { column: string; value: unknown; row: R; index: number; originalEvent: MouseEvent }]
|
|
929
929
|
"sort-change": [sort: SortEntry[]]
|
|
@@ -940,15 +940,15 @@ const emit = defineEmits<{
|
|
|
940
940
|
payload: {
|
|
941
941
|
row: R
|
|
942
942
|
key: RowKey
|
|
943
|
-
/**
|
|
943
|
+
/** New value for the `orderField` field. */
|
|
944
944
|
order: number
|
|
945
|
-
/**
|
|
945
|
+
/** New parent row (tree); null = top level. */
|
|
946
946
|
parent: RowKey | null
|
|
947
|
-
/** true
|
|
947
|
+
/** true if the parent changed (then `move` instead of `edit`). */
|
|
948
948
|
parentChanged: boolean
|
|
949
|
-
/** Position
|
|
949
|
+
/** Position among the new siblings. */
|
|
950
950
|
toIndex: number
|
|
951
|
-
/**
|
|
951
|
+
/** Neighbours share the same value — the list should be renumbered. */
|
|
952
952
|
needsReindex: boolean
|
|
953
953
|
},
|
|
954
954
|
]
|
|
@@ -960,47 +960,47 @@ const emit = defineEmits<{
|
|
|
960
960
|
|
|
961
961
|
const uid = useId()
|
|
962
962
|
|
|
963
|
-
// DOM
|
|
963
|
+
// DOM refs (allowed exception).
|
|
964
964
|
const scrollerEl = ref<HTMLElement>()
|
|
965
965
|
const bodyEl = ref<HTMLElement>()
|
|
966
966
|
|
|
967
|
-
//
|
|
967
|
+
// Non-reactive internals.
|
|
968
968
|
let headerObserver: ResizeObserver | undefined
|
|
969
969
|
let detailObserver: ResizeObserver | undefined
|
|
970
970
|
let debounceTimer = 0
|
|
971
971
|
let abortController: AbortController | undefined
|
|
972
972
|
let lastSelectedIndex: number | null = null
|
|
973
973
|
|
|
974
|
-
//
|
|
975
|
-
//
|
|
974
|
+
// Enclosing drag session (Browser): allows dropping rows OUTSIDE the table
|
|
975
|
+
// (navigation tree, folder rows). Without Browser: null.
|
|
976
976
|
const dragSession = inject(DRAG_SESSION, null)
|
|
977
977
|
|
|
978
978
|
const { t } = useI18n()
|
|
979
979
|
|
|
980
980
|
/**
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
981
|
+
* Optimistic moves (drag & drop / Alt+arrow): rowKey → new
|
|
982
|
+
* order value and, if applicable, new parent. This makes the row vanish immediately
|
|
983
|
+
* from its old position and appear at the target (also INSIDE a folder) without
|
|
984
|
+
* waiting for saving and reloading. As soon as the caller delivers new data
|
|
985
|
+
* (props.data / server response), the overrides are discarded —
|
|
986
|
+
* then the data alone is authoritative again.
|
|
987
987
|
*/
|
|
988
988
|
const moveOverrides = shallowRef(new Map<RowKey, { order: number; parent?: RowKey | null }>())
|
|
989
989
|
|
|
990
990
|
/**
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
991
|
+
* Optimistic moves in LAZY-LOAD mode (`loadChildren`): there the children
|
|
992
|
+
* don't live in `props.data` but in the cache `state.loadedChildren` —
|
|
993
|
+
* so a move takes the row directly out of the source bucket and
|
|
994
|
+
* puts a copy into the target bucket. For the top level (props.data must
|
|
995
|
+
* not be mutated) there is a hide list and injected copies.
|
|
996
|
+
* Both are — like moveOverrides — discarded as soon as new data arrives.
|
|
997
997
|
*/
|
|
998
998
|
const lazyMoves = shallowRef({ hidden: new Set<RowKey>(), injected: [] as R[] })
|
|
999
999
|
|
|
1000
1000
|
const LEAD_WIDTH = 44
|
|
1001
1001
|
const ORDER_WIDTH = 36
|
|
1002
1002
|
|
|
1003
|
-
/**
|
|
1003
|
+
/** Drop: before, after or inside (like admin4). */
|
|
1004
1004
|
type DropMode = "before" | "after" | "inside"
|
|
1005
1005
|
|
|
1006
1006
|
interface CellColumn {
|
|
@@ -1027,7 +1027,7 @@ const layout = useColumnLayout<R>({
|
|
|
1027
1027
|
})
|
|
1028
1028
|
|
|
1029
1029
|
const state = reactive({
|
|
1030
|
-
/** Server
|
|
1030
|
+
/** Server mode: rows and total count from the last response. */
|
|
1031
1031
|
serverRows: [] as R[],
|
|
1032
1032
|
serverTotal: -1,
|
|
1033
1033
|
fetching: false,
|
|
@@ -1044,8 +1044,8 @@ const state = reactive({
|
|
|
1044
1044
|
isServer: computed((): boolean => props.server || Boolean(props.dataSource)),
|
|
1045
1045
|
|
|
1046
1046
|
/**
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1047
|
+
* Hierarchy from a flat list (parentField). Null once the data is
|
|
1048
|
+
* already nested or the prerequisites are missing.
|
|
1049
1049
|
*/
|
|
1050
1050
|
nested: computed((): NestedTree<R> | null => {
|
|
1051
1051
|
if (props.childRows || !props.parentField) return null
|
|
@@ -1057,9 +1057,9 @@ const state = reactive({
|
|
|
1057
1057
|
}),
|
|
1058
1058
|
|
|
1059
1059
|
/**
|
|
1060
|
-
*
|
|
1061
|
-
* (`parentField`)
|
|
1062
|
-
*
|
|
1060
|
+
* Access to the children: from `childRows`, from the built tree
|
|
1061
|
+
* (`parentField`) and/or from the lazy-loaded levels — merged in this
|
|
1062
|
+
* order.
|
|
1063
1063
|
*/
|
|
1064
1064
|
childAccessor: computed((): ChildRowsAccessor<R> | null => {
|
|
1065
1065
|
const nested = state.nested as NestedTree<R> | null
|
|
@@ -1083,16 +1083,16 @@ const state = reactive({
|
|
|
1083
1083
|
isTree: computed((): boolean => Boolean(state.childAccessor)),
|
|
1084
1084
|
isLoading: computed((): boolean => props.loading ?? state.fetching),
|
|
1085
1085
|
|
|
1086
|
-
/**
|
|
1086
|
+
/** Lazy-loaded children per row key (empty array = there are none). */
|
|
1087
1087
|
loadedChildren: {} as Record<string, R[]>,
|
|
1088
|
-
/**
|
|
1088
|
+
/** Rows whose children are currently loading. */
|
|
1089
1089
|
loadingChildren: {} as Record<string, boolean>,
|
|
1090
1090
|
|
|
1091
|
-
/**
|
|
1091
|
+
/** Ongoing row drag gesture (fixed order). */
|
|
1092
1092
|
rowDrag: null as {
|
|
1093
1093
|
key: RowKey
|
|
1094
1094
|
overKey: RowKey | null
|
|
1095
|
-
/**
|
|
1095
|
+
/** Like admin4: before, after or INSIDE (new parent). */
|
|
1096
1096
|
mode: DropMode
|
|
1097
1097
|
} | null,
|
|
1098
1098
|
|
|
@@ -1116,24 +1116,24 @@ const state = reactive({
|
|
|
1116
1116
|
return patched
|
|
1117
1117
|
})
|
|
1118
1118
|
}
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1119
|
+
// Fixed order: numerically ascending — also in SERVER mode. Only this way
|
|
1120
|
+
// does a row moved via drag & drop IMMEDIATELY take its place without
|
|
1121
|
+
// reloading the level (the new value comes from moveOverrides above).
|
|
1122
|
+
// An active column sort takes precedence: then the pipeline orders in
|
|
1123
|
+
// client mode and the server in server mode.
|
|
1124
1124
|
if (!props.orderField || sort.value.length > 0) return base
|
|
1125
1125
|
const field = props.orderField
|
|
1126
1126
|
return [...base].sort((a, b) => Number(a[field] ?? 0) - Number(b[field] ?? 0))
|
|
1127
1127
|
}),
|
|
1128
1128
|
|
|
1129
1129
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1130
|
+
* Reordering only makes sense in the fixed order — not with
|
|
1131
|
+
* grouping and not while a column sort is active.
|
|
1132
1132
|
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1133
|
+
* In server mode it is explicitly allowed: the order comes from the
|
|
1134
|
+
* server there (`orderby` on the field), the new value is computed from the
|
|
1135
|
+
* neighbours and reported outward via `row-reorder` — saving happens
|
|
1136
|
+
* outside anyway.
|
|
1137
1137
|
*/
|
|
1138
1138
|
orderActive: computed(
|
|
1139
1139
|
(): boolean => Boolean(props.orderField) && !state.groupByList.length && sort.value.length === 0
|
|
@@ -1141,7 +1141,7 @@ const state = reactive({
|
|
|
1141
1141
|
|
|
1142
1142
|
sort: computed((): SortEntry[] => sort.value),
|
|
1143
1143
|
|
|
1144
|
-
/** Filter/
|
|
1144
|
+
/** Filter/search/sort/grouping — in server mode the server does this. */
|
|
1145
1145
|
built: computed(
|
|
1146
1146
|
(): BuiltRows<R> =>
|
|
1147
1147
|
buildRows<R>({
|
|
@@ -1158,19 +1158,19 @@ const state = reactive({
|
|
|
1158
1158
|
})
|
|
1159
1159
|
),
|
|
1160
1160
|
|
|
1161
|
-
/** Cursor
|
|
1161
|
+
/** Cursor mode: the caller controls loading more (see hasMore). */
|
|
1162
1162
|
cursorMode: computed((): boolean => props.hasMore !== null && props.hasMore !== undefined),
|
|
1163
|
-
/**
|
|
1163
|
+
/** Page sizes of the cursor bar — the current size is always selectable. */
|
|
1164
1164
|
cursorPageSizes: computed((): number[] => {
|
|
1165
1165
|
const options = new Set(props.cursorPageSizes)
|
|
1166
1166
|
options.add(pageSize.value)
|
|
1167
1167
|
return [...options].sort((a, b) => a - b)
|
|
1168
1168
|
}),
|
|
1169
|
-
/**
|
|
1169
|
+
/** "N entries" or "N of M" — M only if the server knows a total count. */
|
|
1170
1170
|
loadedText: computed((): string => {
|
|
1171
1171
|
const loaded = state.rows.length
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
1172
|
+
// Local search/filter active: show the number of MATCHES, not the
|
|
1173
|
+
// load counter — otherwise the bar looks as if nothing were filtered.
|
|
1174
1174
|
if (!state.isServer && state.filteredCount !== loaded) {
|
|
1175
1175
|
return t("dataTable.filteredOf", { filtered: state.filteredCount, loaded })
|
|
1176
1176
|
}
|
|
@@ -1203,15 +1203,15 @@ const state = reactive({
|
|
|
1203
1203
|
flattenNodes(state.pagedNodes as RowNode<R>[], {
|
|
1204
1204
|
isExpanded: (key) => state.expandedSet.has(key),
|
|
1205
1205
|
withDetail: props.rowDetail,
|
|
1206
|
-
//
|
|
1207
|
-
//
|
|
1206
|
+
// With lazy loading it's unknown beforehand whether a row has children: show
|
|
1207
|
+
// the arrow as long as the opposite isn't established.
|
|
1208
1208
|
mayHaveChildren: props.loadChildren
|
|
1209
1209
|
? (node) => node.kind === "data" && Boolean(node.row) && childState(node.row!) !== "none"
|
|
1210
1210
|
: undefined,
|
|
1211
1211
|
})
|
|
1212
1212
|
),
|
|
1213
1213
|
|
|
1214
|
-
/** Index in
|
|
1214
|
+
/** Index in the render list per row key — for keyboard and focus. */
|
|
1215
1215
|
rowIndexOf: computed((): Record<string, number> => {
|
|
1216
1216
|
const map: Record<string, number> = {}
|
|
1217
1217
|
state.displayRows.forEach((row, index) => {
|
|
@@ -1256,7 +1256,7 @@ const state = reactive({
|
|
|
1256
1256
|
return map
|
|
1257
1257
|
}),
|
|
1258
1258
|
|
|
1259
|
-
/**
|
|
1259
|
+
/** Leading columns (selection, expand) plus the visible data columns. */
|
|
1260
1260
|
cellColumns: computed((): CellColumn[] => {
|
|
1261
1261
|
const cells: CellColumn[] = []
|
|
1262
1262
|
let lead = 0
|
|
@@ -1318,9 +1318,9 @@ const state = reactive({
|
|
|
1318
1318
|
firstDataCellIndex: computed((): number => state.cellColumns.find((cell) => cell.kind === "data")?.index ?? 0),
|
|
1319
1319
|
|
|
1320
1320
|
/**
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1321
|
+
* Dedicated expand column only for groups and detail panels. With tree data
|
|
1322
|
+
* the arrow sits IN the first data column — only there does it indent with the
|
|
1323
|
+
* level, and that is exactly what makes the hierarchy visible.
|
|
1324
1324
|
*/
|
|
1325
1325
|
hasExpandColumn: computed((): boolean => Boolean(props.rowDetail || state.groupByList.length)),
|
|
1326
1326
|
|
|
@@ -1335,15 +1335,15 @@ const state = reactive({
|
|
|
1335
1335
|
|
|
1336
1336
|
hostStyle: computed((): Record<string, string> => {
|
|
1337
1337
|
const style: Record<string, string> = { "--dt-row-height": `${props.rowHeight}px` }
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1338
|
+
// Only set when given — otherwise the inline value would override every
|
|
1339
|
+
// outside rule and `--dt-max-height` would no longer be a variable.
|
|
1340
1340
|
if (props.maxHeight) style["--dt-max-height"] = props.maxHeight
|
|
1341
1341
|
return style
|
|
1342
1342
|
}),
|
|
1343
1343
|
|
|
1344
1344
|
gridRole: computed((): string => (state.isTree ? "treegrid" : "grid")),
|
|
1345
1345
|
|
|
1346
|
-
// Panel
|
|
1346
|
+
// Panel props aren't generic — cast to the base form here.
|
|
1347
1347
|
filterColumns: computed((): ResolvedColumn[] =>
|
|
1348
1348
|
(layout.state.visibleColumns as LayoutColumn<R>[])
|
|
1349
1349
|
.map((col) => col.column as unknown as ResolvedColumn)
|
|
@@ -1379,8 +1379,8 @@ const state = reactive({
|
|
|
1379
1379
|
showToolbar: computed((): boolean => props.withSearch || props.withColumnsMenu),
|
|
1380
1380
|
})
|
|
1381
1381
|
|
|
1382
|
-
// reactive()
|
|
1383
|
-
//
|
|
1382
|
+
// reactive() unwraps the generics to `Row`; these helpers give the accessors
|
|
1383
|
+
// their concrete type back.
|
|
1384
1384
|
const cols = (): ResolvedColumn<R>[] => state.columns as ResolvedColumn<R>[]
|
|
1385
1385
|
const displayed = (): DisplayRow<R>[] => state.displayRows as DisplayRow<R>[]
|
|
1386
1386
|
const dataRowsOf = (): R[] => state.built.dataRows as R[]
|
|
@@ -1458,7 +1458,7 @@ const nav = useGridNavigation(scrollerEl, {
|
|
|
1458
1458
|
},
|
|
1459
1459
|
})
|
|
1460
1460
|
|
|
1461
|
-
// ──
|
|
1461
|
+
// ── Rendering ────────────────────────────────────────────────────────
|
|
1462
1462
|
|
|
1463
1463
|
function cellStyle(cell: CellColumn): Record<string, string> {
|
|
1464
1464
|
if (!cell.sticky) return {}
|
|
@@ -1468,13 +1468,13 @@ function cellStyle(cell: CellColumn): Record<string, string> {
|
|
|
1468
1468
|
}
|
|
1469
1469
|
|
|
1470
1470
|
/**
|
|
1471
|
-
*
|
|
1472
|
-
* — in
|
|
1471
|
+
* Indentation of tree and group levels. It sits on the FIRST data column
|
|
1472
|
+
* — in the narrow expand column (fixed width) it would be invisible.
|
|
1473
1473
|
*/
|
|
1474
1474
|
/**
|
|
1475
|
-
*
|
|
1476
|
-
* (`.vi-dt-guide`) —
|
|
1477
|
-
*
|
|
1475
|
+
* Indentation of the levels. In the tree the guide slots handle this
|
|
1476
|
+
* (`.vi-dt-guide`) — there the normal padding stays. Groups without a tree
|
|
1477
|
+
* have no guide lines and are still indented via padding.
|
|
1478
1478
|
*/
|
|
1479
1479
|
function indentStyle(cell: CellColumn, displayRow: DisplayRow<R>): Record<string, string> {
|
|
1480
1480
|
if (cell.index !== state.firstDataCellIndex || displayRow.depth === 0) return {}
|
|
@@ -1519,13 +1519,13 @@ function aggregateText(cell: CellColumn, displayRow: DisplayRow<R>): string {
|
|
|
1519
1519
|
return column.def.formatter && first ? column.def.formatter(value, first) : defaultText(value)
|
|
1520
1520
|
}
|
|
1521
1521
|
|
|
1522
|
-
// ──
|
|
1522
|
+
// ── Sorting ──────────────────────────────────────────────────────────
|
|
1523
1523
|
|
|
1524
1524
|
function toggleSort(column: ResolvedColumn<R>, multi: boolean): void {
|
|
1525
1525
|
if (!column.sortable) return
|
|
1526
|
-
//
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
1526
|
+
// The NEW value is kept locally and passed on — not read back from the model
|
|
1527
|
+
// (see scheduleFetch: with a bound v-model the update only arrives
|
|
1528
|
+
// with the parent's prop).
|
|
1529
1529
|
const next = nextSortState(sort.value, column as ResolvedColumn, {
|
|
1530
1530
|
multi,
|
|
1531
1531
|
descFirst: props.sortDescFirst,
|
|
@@ -1550,12 +1550,12 @@ function onHeaderClick(cell: CellColumn, event: MouseEvent): void {
|
|
|
1550
1550
|
nav.setActive(-1, cell.index, false)
|
|
1551
1551
|
if (cell.kind !== "data") return
|
|
1552
1552
|
const target = event.target as HTMLElement
|
|
1553
|
-
//
|
|
1553
|
+
// Don't sort on clicks on header controls (filter, menu, pin, resize).
|
|
1554
1554
|
if (target.closest(".vi-dt-filter-button, .vi-dt-column-menu, .vi-dt-pin, .vi-dt-resize")) return
|
|
1555
1555
|
toggleSort(cell.col!.column, event.shiftKey)
|
|
1556
1556
|
}
|
|
1557
1557
|
|
|
1558
|
-
// ──
|
|
1558
|
+
// ── Search & filter ──────────────────────────────────────────────────
|
|
1559
1559
|
|
|
1560
1560
|
function onSearchInput(value: string): void {
|
|
1561
1561
|
search.value = value
|
|
@@ -1573,9 +1573,9 @@ function setFilter(columnId: string, value: unknown): void {
|
|
|
1573
1573
|
if (state.isServer) scheduleFetch(true, { filters: next })
|
|
1574
1574
|
}
|
|
1575
1575
|
|
|
1576
|
-
// ──
|
|
1576
|
+
// ── Selection ────────────────────────────────────────────────────────
|
|
1577
1577
|
|
|
1578
|
-
/**
|
|
1578
|
+
/** Badges of a cell (columns with a `badge` function, e.g. select/bool). */
|
|
1579
1579
|
function badgesFor(column: ResolvedColumn<R>, row: R): CellBadge[] | null {
|
|
1580
1580
|
const badges = column.def.badge?.(column.get(row), row)
|
|
1581
1581
|
return badges?.length ? badges : null
|
|
@@ -1586,10 +1586,10 @@ function canSelect(row: R): boolean {
|
|
|
1586
1586
|
}
|
|
1587
1587
|
|
|
1588
1588
|
/**
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
* (`selectableRows`).
|
|
1592
|
-
*
|
|
1589
|
+
* Selectable keys of the current page — including collapsed
|
|
1590
|
+
* subtrees and group members, but without locked rows
|
|
1591
|
+
* (`selectableRows`). Only visit the page's root nodes, otherwise
|
|
1592
|
+
* expanded children count twice.
|
|
1593
1593
|
*/
|
|
1594
1594
|
function selectableKeysOnPage(): RowKey[] {
|
|
1595
1595
|
const keys: RowKey[] = []
|
|
@@ -1649,7 +1649,7 @@ function toggleRowSelection(displayRow: DisplayRow<R>, range: boolean): void {
|
|
|
1649
1649
|
return
|
|
1650
1650
|
}
|
|
1651
1651
|
|
|
1652
|
-
//
|
|
1652
|
+
// Tree/group: include children.
|
|
1653
1653
|
const keys = displayRow.node ? collectKeys(displayRow.node) : [displayRow.nodeKey]
|
|
1654
1654
|
const selectable = keys.filter((key) => {
|
|
1655
1655
|
const row = findRowByKey(key)
|
|
@@ -1670,19 +1670,19 @@ function findRowByKey(key: RowKey): R | undefined {
|
|
|
1670
1670
|
}
|
|
1671
1671
|
|
|
1672
1672
|
/**
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1673
|
+
* Selection via checkbox. Deliberately on `click` rather than `change`: only the
|
|
1674
|
+
* click event carries `shiftKey`, and range selection relies on it.
|
|
1675
1675
|
*/
|
|
1676
1676
|
function onSelectRow(displayRow: DisplayRow<R>, event: MouseEvent): void {
|
|
1677
1677
|
toggleRowSelection(displayRow, event.shiftKey)
|
|
1678
1678
|
}
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
// mousedown
|
|
1679
|
+
// Click on the ROW (not the checkbox): behaves like a file manager.
|
|
1680
|
+
// A plain click replaces the selection with this row; Ctrl/Cmd adds
|
|
1681
|
+
// or removes it; Shift selects the range. The checkbox
|
|
1682
|
+
// itself (onSelectRow) still toggles additively.
|
|
1683
|
+
// With Ctrl/Cmd or Shift held, the click serves multi-selection —
|
|
1684
|
+
// the browser's text selection is suppressed for this (preventDefault on
|
|
1685
|
+
// mousedown prevents the selection from starting, the click still arrives).
|
|
1686
1686
|
function onCellMouseDown(event: MouseEvent): void {
|
|
1687
1687
|
if (props.selectable === false || !props.selectOnRowClick) return
|
|
1688
1688
|
if (event.ctrlKey || event.metaKey || event.shiftKey) event.preventDefault()
|
|
@@ -1694,7 +1694,7 @@ function selectRowFromClick(displayRow: DisplayRow<R>, event: MouseEvent): void
|
|
|
1694
1694
|
}
|
|
1695
1695
|
if (displayRow.kind === "detail") return
|
|
1696
1696
|
if (displayRow.kind === "data" && displayRow.row && !canSelect(displayRow.row)) return
|
|
1697
|
-
//
|
|
1697
|
+
// Tree/group: include children (like the checkbox).
|
|
1698
1698
|
const keys = (displayRow.node ? collectKeys(displayRow.node) : [displayRow.nodeKey]).filter((key) => {
|
|
1699
1699
|
const row = findRowByKey(key)
|
|
1700
1700
|
return !row || canSelect(row)
|
|
@@ -1722,18 +1722,18 @@ function toggleSelectAll(): void {
|
|
|
1722
1722
|
selectAllOnPage()
|
|
1723
1723
|
}
|
|
1724
1724
|
|
|
1725
|
-
// ──
|
|
1725
|
+
// ── Lazy-load children ───────────────────────────────────────────────
|
|
1726
1726
|
|
|
1727
1727
|
function rowKeyOf(row: R): RowKey {
|
|
1728
1728
|
return props.rowKey ? (row[props.rowKey] as RowKey) : ""
|
|
1729
1729
|
}
|
|
1730
1730
|
|
|
1731
1731
|
/**
|
|
1732
|
-
*
|
|
1733
|
-
* "none" →
|
|
1734
|
-
* "empty" →
|
|
1735
|
-
* "loaded" →
|
|
1736
|
-
* "unknown" →
|
|
1732
|
+
* What do we know about this row's children?
|
|
1733
|
+
* "none" → definitely none (the application says so via `rowHasChildren`)
|
|
1734
|
+
* "empty" → loaded, there were none → arrow stays, but disabled
|
|
1735
|
+
* "loaded" → loaded, there are children
|
|
1736
|
+
* "unknown" → not loaded yet → arrow active
|
|
1737
1737
|
*/
|
|
1738
1738
|
function childState(row: R): "none" | "empty" | "loaded" | "unknown" {
|
|
1739
1739
|
if (props.rowHasChildren?.(row) === false) return "none"
|
|
@@ -1746,13 +1746,13 @@ function isLoadingChildren(row?: R): boolean {
|
|
|
1746
1746
|
return row ? Boolean(state.loadingChildren[String(rowKeyOf(row))]) : false
|
|
1747
1747
|
}
|
|
1748
1748
|
|
|
1749
|
-
/**
|
|
1749
|
+
/** Arrow without effect: loaded and there is nothing. */
|
|
1750
1750
|
function isEmptyBranch(displayRow: DisplayRow<R>): boolean {
|
|
1751
1751
|
if (!props.loadChildren || displayRow.kind !== "data" || !displayRow.row) return false
|
|
1752
1752
|
return childState(displayRow.row) === "empty" && !displayRow.node?.children.length
|
|
1753
1753
|
}
|
|
1754
1754
|
|
|
1755
|
-
/**
|
|
1755
|
+
/** Fetch a row's children (once; `force` reloads). */
|
|
1756
1756
|
async function fetchChildren(row: R, force = false): Promise<void> {
|
|
1757
1757
|
if (!props.loadChildren) return
|
|
1758
1758
|
const key = String(rowKeyOf(row))
|
|
@@ -1771,20 +1771,22 @@ async function fetchChildren(row: R, force = false): Promise<void> {
|
|
|
1771
1771
|
}
|
|
1772
1772
|
}
|
|
1773
1773
|
|
|
1774
|
-
// ──
|
|
1774
|
+
// ── Expanding ────────────────────────────────────────────────────────
|
|
1775
1775
|
|
|
1776
1776
|
function toggleExpand(displayRow: DisplayRow<R>): void {
|
|
1777
1777
|
if (!displayRow.expandable) return
|
|
1778
1778
|
if (isEmptyBranch(displayRow) || isLoadingChildren(displayRow.row)) return
|
|
1779
1779
|
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1780
|
+
// Load first, then expand — otherwise an empty level expands and
|
|
1781
|
+
// collapses again as soon as the response arrives.
|
|
1782
1782
|
if (props.loadChildren && displayRow.kind === "data" && displayRow.row) {
|
|
1783
1783
|
const row = displayRow.row
|
|
1784
1784
|
if (childState(row) === "unknown" && !displayRow.node?.children.length) {
|
|
1785
|
-
void fetchChildren(row)
|
|
1786
|
-
|
|
1787
|
-
|
|
1785
|
+
void fetchChildren(row)
|
|
1786
|
+
.then(() => {
|
|
1787
|
+
if (childState(row) !== "empty") expand(displayRow.nodeKey, row)
|
|
1788
|
+
})
|
|
1789
|
+
.catch((error) => console.warn("[DataTable] Ebene konnte nicht aufgeklappt werden", error))
|
|
1788
1790
|
return
|
|
1789
1791
|
}
|
|
1790
1792
|
}
|
|
@@ -1801,7 +1803,7 @@ function toggleExpand(displayRow: DisplayRow<R>): void {
|
|
|
1801
1803
|
}
|
|
1802
1804
|
}
|
|
1803
1805
|
|
|
1804
|
-
/**
|
|
1806
|
+
/** Expand a row and emit the event. */
|
|
1805
1807
|
function expand(key: RowKey, row?: R): void {
|
|
1806
1808
|
if (expandedKeys.value.includes(key)) return
|
|
1807
1809
|
expandedKeys.value = [...expandedKeys.value, key]
|
|
@@ -1816,19 +1818,19 @@ function allExpandableKeys(nodes: RowNode<R>[] = state.built.nodes as RowNode<R>
|
|
|
1816
1818
|
return into
|
|
1817
1819
|
}
|
|
1818
1820
|
|
|
1819
|
-
// ──
|
|
1821
|
+
// ── Cell clicks ──────────────────────────────────────────────────────
|
|
1820
1822
|
|
|
1821
1823
|
/**
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1824
|
+
* Double click on a data row. Controls in the row (selection,
|
|
1825
|
+
* expand, actions) don't trigger it — double-clicking there doesn't mean
|
|
1826
|
+
* "open record".
|
|
1825
1827
|
*/
|
|
1826
1828
|
function onRowDoubleClick(displayRow: DisplayRow<R>, event: MouseEvent): void {
|
|
1827
1829
|
if (displayRow.kind !== "data" || !displayRow.row) return
|
|
1828
1830
|
const target = event.target as HTMLElement
|
|
1829
1831
|
if (target.closest("wa-checkbox, wa-button, a, input, select, .vi-dt-expand")) return
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
+
// A double click fires two clicks, which would undo the click selection
|
|
1833
|
+
// again — the row stays/becomes selected.
|
|
1832
1834
|
if (props.selectable !== false && props.selectOnRowClick && canSelect(displayRow.row)) {
|
|
1833
1835
|
const key = displayRow.nodeKey
|
|
1834
1836
|
if (!state.selectedSet.has(key)) {
|
|
@@ -1852,8 +1854,8 @@ function onCellClick(displayRow: DisplayRow<R>, cell: CellColumn, event: MouseEv
|
|
|
1852
1854
|
if (cell.kind !== "data" || !displayRow.row) return
|
|
1853
1855
|
const target = event.target as HTMLElement
|
|
1854
1856
|
if (target.closest(".vi-dt-expand, .vi-dt-checkbox")) return
|
|
1855
|
-
//
|
|
1856
|
-
// in
|
|
1857
|
+
// Select the row on click — unless the click hits an interactive element
|
|
1858
|
+
// in the cell (link, button, input field) that handles the click itself.
|
|
1857
1859
|
if (
|
|
1858
1860
|
props.selectable !== false &&
|
|
1859
1861
|
props.selectOnRowClick &&
|
|
@@ -1881,14 +1883,14 @@ function onCellContextMenu(displayRow: DisplayRow<R>, cell: CellColumn, event: M
|
|
|
1881
1883
|
})
|
|
1882
1884
|
}
|
|
1883
1885
|
|
|
1884
|
-
// ──
|
|
1886
|
+
// ── Fixed order: reordering rows ─────────────────────────────────────
|
|
1885
1887
|
|
|
1886
1888
|
/**
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1889
|
+
* Rows are dragged with POINTER events, not with HTML5 drag and drop.
|
|
1890
|
+
* Reason: `dragover` requires an accepting target and fires unreliably
|
|
1891
|
+
* depending on the environment (not at all on touch) — the insertion marker would then
|
|
1892
|
+
* be missing and the target position would be random. Pointer events behave the same
|
|
1893
|
+
* everywhere, work with touch and are testable.
|
|
1892
1894
|
*/
|
|
1893
1895
|
function beginRowDrag(displayRow: DisplayRow<R>, event: PointerEvent): void {
|
|
1894
1896
|
if (!state.orderActive || displayRow.kind !== "data") return
|
|
@@ -1916,15 +1918,15 @@ function beginRowDrag(displayRow: DisplayRow<R>, event: PointerEvent): void {
|
|
|
1916
1918
|
}
|
|
1917
1919
|
|
|
1918
1920
|
/**
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
* (
|
|
1921
|
+
* Determine the row under the pointer and derive the zone. As in admin4 there are
|
|
1922
|
+
* three: top quarter = before, bottom quarter = after, the middle = INSIDE
|
|
1923
|
+
* (the row becomes a child of the target). Without a tree only before/after remain.
|
|
1922
1924
|
*/
|
|
1923
1925
|
function updateRowDrag(clientX: number, clientY: number): void {
|
|
1924
1926
|
const drag = state.rowDrag
|
|
1925
1927
|
if (!drag) return
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
1928
|
+
// Ask the enclosing session first: if it provides a target (tree node or
|
|
1929
|
+
// folder row), it takes over display and drop — the internal indicator rests.
|
|
1928
1930
|
if (dragSession?.state.active) {
|
|
1929
1931
|
const external = dragSession.update(clientX, clientY)
|
|
1930
1932
|
if (external) {
|
|
@@ -1937,7 +1939,7 @@ function updateRowDrag(clientX: number, clientY: number): void {
|
|
|
1937
1939
|
const target = displayed().find((row) => row.key === element.dataset.key)
|
|
1938
1940
|
if (!target || target.nodeKey === drag.key) return
|
|
1939
1941
|
|
|
1940
|
-
//
|
|
1942
|
+
// Dragging into its own subtree is impossible — it would hang from itself.
|
|
1941
1943
|
if (isDescendant(state.built.nodes as RowNode<R>[], drag.key, target.nodeKey)) {
|
|
1942
1944
|
drag.overKey = null
|
|
1943
1945
|
return
|
|
@@ -1952,7 +1954,7 @@ function updateRowDrag(clientX: number, clientY: number): void {
|
|
|
1952
1954
|
else drag.mode = "inside"
|
|
1953
1955
|
}
|
|
1954
1956
|
|
|
1955
|
-
/**
|
|
1957
|
+
/** Lazy-load mode: take the row out of its old bucket and insert it at the target. */
|
|
1956
1958
|
function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, insertAt: number, order: number): void {
|
|
1957
1959
|
const row = source.node.row
|
|
1958
1960
|
if (!row) return
|
|
@@ -1964,9 +1966,9 @@ function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, ins
|
|
|
1964
1966
|
const hidden = new Set(lazyMoves.value.hidden)
|
|
1965
1967
|
let injected = lazyMoves.value.injected.filter((candidate) => (candidate[props.rowKey!] as RowKey) !== key)
|
|
1966
1968
|
|
|
1967
|
-
//
|
|
1969
|
+
// Remove from the source …
|
|
1968
1970
|
if (source.parentKey === null) {
|
|
1969
|
-
hidden.add(key) //
|
|
1971
|
+
hidden.add(key) // top level: props.data stays untouched, just hide
|
|
1970
1972
|
} else {
|
|
1971
1973
|
const bucket = state.loadedChildren[String(source.parentKey)] as R[] | undefined
|
|
1972
1974
|
if (bucket) {
|
|
@@ -1976,10 +1978,10 @@ function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, ins
|
|
|
1976
1978
|
}
|
|
1977
1979
|
}
|
|
1978
1980
|
|
|
1979
|
-
// …
|
|
1981
|
+
// … and insert at the target.
|
|
1980
1982
|
if (targetParent === null) {
|
|
1981
|
-
//
|
|
1982
|
-
//
|
|
1983
|
+
// The original (if it is in props.data) stays hidden; the copy with the
|
|
1984
|
+
// new order value is visible.
|
|
1983
1985
|
hidden.add(key)
|
|
1984
1986
|
injected = [...injected, moved]
|
|
1985
1987
|
} else {
|
|
@@ -1989,20 +1991,20 @@ function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, ins
|
|
|
1989
1991
|
next.splice(Math.max(0, Math.min(insertAt, next.length)), 0, moved)
|
|
1990
1992
|
state.loadedChildren[String(targetParent)] = next as (typeof state.loadedChildren)[string]
|
|
1991
1993
|
}
|
|
1992
|
-
// Bucket
|
|
1993
|
-
//
|
|
1994
|
+
// Bucket never loaded: on expand fetchChildren gets the current state
|
|
1995
|
+
// from the server — which contains the row after saving anyway.
|
|
1994
1996
|
}
|
|
1995
1997
|
|
|
1996
1998
|
lazyMoves.value = { hidden, injected }
|
|
1997
1999
|
}
|
|
1998
2000
|
|
|
1999
2001
|
/**
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2002
|
+
* Reset lazy-load mode: discard the children cache and collapse all
|
|
2003
|
+
* branches — the arrows are back to "collapsed" afterwards, and the next
|
|
2004
|
+
* expand fetches the fresh state from the server. Runs on `reload()` and
|
|
2005
|
+
* when the caller delivers new data; without `loadChildren` (parentField
|
|
2006
|
+
* trees) the expand state is kept, since there the children are always
|
|
2007
|
+
* derived from the current data anyway.
|
|
2006
2008
|
*/
|
|
2007
2009
|
function resetLazyTree(): void {
|
|
2008
2010
|
if (!props.loadChildren) return
|
|
@@ -2011,15 +2013,15 @@ function resetLazyTree(): void {
|
|
|
2011
2013
|
if (expandedKeys.value.length) expandedKeys.value = []
|
|
2012
2014
|
}
|
|
2013
2015
|
|
|
2014
|
-
// Drag &
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2019
|
-
//
|
|
2020
|
-
//
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2016
|
+
// Drag & drop is purely CLIENT-SIDE: the row is moved locally (buckets/
|
|
2017
|
+
// overrides above), `row-reorder` reports the move for saving in the
|
|
2018
|
+
// background — the table reloads NOTHING afterwards, and the app doesn't
|
|
2019
|
+
// have to either. When new data comes in from outside, it is authoritative: the
|
|
2020
|
+
// optimistic overrides are discarded (fresh data already contains
|
|
2021
|
+
// a saved move). The lazy-load tree stays as it is —
|
|
2022
|
+
// it is only collapsed and cleared via `reload()`/`resetTree()` or, via the
|
|
2023
|
+
// opt-in `resetTreeOnDataChange`, on every data change not preceded by a
|
|
2024
|
+
// move.
|
|
2023
2025
|
watch(
|
|
2024
2026
|
() => (props.dataSource ? (state.serverRows as R[]) : props.data),
|
|
2025
2027
|
() => {
|
|
@@ -2036,8 +2038,8 @@ watch(
|
|
|
2036
2038
|
function commitRowDrag(): void {
|
|
2037
2039
|
const drag = state.rowDrag
|
|
2038
2040
|
state.rowDrag = null
|
|
2039
|
-
//
|
|
2040
|
-
//
|
|
2041
|
+
// A session target (tree/folder outside the internal logic) takes precedence:
|
|
2042
|
+
// the Browser takes over the move, nothing else happens here.
|
|
2041
2043
|
if (dragSession?.state.active) {
|
|
2042
2044
|
if (dragSession.state.over) {
|
|
2043
2045
|
dragSession.drop()
|
|
@@ -2051,11 +2053,11 @@ function commitRowDrag(): void {
|
|
|
2051
2053
|
}
|
|
2052
2054
|
|
|
2053
2055
|
/**
|
|
2054
|
-
*
|
|
2056
|
+
* Move a row to a new position and report the new order value.
|
|
2055
2057
|
*
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2058
|
+
* The computation is always among the future SIBLINGS — for "inside" that is
|
|
2059
|
+
* among the target's children, otherwise among the target's siblings. Only then
|
|
2060
|
+
* does the value make sense in the tree: `sortindex` applies per parent.
|
|
2059
2061
|
*/
|
|
2060
2062
|
function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
2061
2063
|
if (!props.orderField || key === targetKey) return
|
|
@@ -2065,9 +2067,9 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2065
2067
|
const target = locateNode(nodes, targetKey)
|
|
2066
2068
|
if (!target) return
|
|
2067
2069
|
|
|
2068
|
-
//
|
|
2069
|
-
//
|
|
2070
|
-
// (
|
|
2070
|
+
// Multi-selection: if the dragged row is part of the selection, the whole
|
|
2071
|
+
// selection moves along — in display order. Rows that may not go to the target
|
|
2072
|
+
// (the target itself, ancestors of the target) are left out.
|
|
2071
2073
|
const moving = movingKeysFor(key).filter(
|
|
2072
2074
|
(candidate) => candidate !== targetKey && !isDescendant(nodes, candidate, targetKey)
|
|
2073
2075
|
)
|
|
@@ -2081,7 +2083,7 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2081
2083
|
let values: number[]
|
|
2082
2084
|
|
|
2083
2085
|
if (mode === "inside") {
|
|
2084
|
-
//
|
|
2086
|
+
// New children: after the target's existing children.
|
|
2085
2087
|
parentKey = targetKey
|
|
2086
2088
|
const children = target.node.children.filter((child) => !movingSet.has(child.key))
|
|
2087
2089
|
values = children.map(orderOf)
|
|
@@ -2103,12 +2105,12 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2103
2105
|
if (!source?.node.row) continue
|
|
2104
2106
|
|
|
2105
2107
|
const { order, needsReindex } = orderValueFor(values, insertAt, props.orderStep)
|
|
2106
|
-
//
|
|
2107
|
-
//
|
|
2108
|
+
// Add the assigned value to the neighbour list so the next
|
|
2109
|
+
// row of the selection is sorted in AFTER it (order is preserved).
|
|
2108
2110
|
values.splice(Math.max(0, Math.min(values.length, insertAt)), 0, order)
|
|
2109
2111
|
|
|
2110
|
-
//
|
|
2111
|
-
//
|
|
2112
|
+
// Apply optimistically: the row leaves its old position immediately and
|
|
2113
|
+
// appears at the target — on a drop INTO a folder, among its children.
|
|
2112
2114
|
if (props.rowKey) {
|
|
2113
2115
|
if (props.loadChildren) {
|
|
2114
2116
|
applyLazyMove(source, parentKey, insertAt, order)
|
|
@@ -2146,10 +2148,10 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2146
2148
|
}
|
|
2147
2149
|
|
|
2148
2150
|
/**
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
*
|
|
2152
|
-
*
|
|
2151
|
+
* Which rows a move affects: only the dragged one — or, if it is part of the
|
|
2152
|
+
* selection, all selected rows in display order. Selected
|
|
2153
|
+
* descendants of an also-selected row move implicitly with their
|
|
2154
|
+
* subtree and are not moved twice.
|
|
2153
2155
|
*/
|
|
2154
2156
|
function movingKeysFor(key: RowKey): RowKey[] {
|
|
2155
2157
|
if (props.selectable === false || !state.selectedSet.has(key) || state.selectedSet.size < 2) return [key]
|
|
@@ -2159,8 +2161,8 @@ function movingKeysFor(key: RowKey): RowKey[] {
|
|
|
2159
2161
|
for (const row of state.dataDisplayRows as DisplayRow<R>[]) {
|
|
2160
2162
|
if (selected.has(row.nodeKey)) ordered.push(row.nodeKey)
|
|
2161
2163
|
}
|
|
2162
|
-
// dataDisplayRows
|
|
2163
|
-
//
|
|
2164
|
+
// dataDisplayRows only contains VISIBLE rows — selected rows in
|
|
2165
|
+
// collapsed branches are appended so they aren't left behind.
|
|
2164
2166
|
const seen = new Set(ordered)
|
|
2165
2167
|
for (const candidate of selected) {
|
|
2166
2168
|
if (!seen.has(candidate)) ordered.push(candidate)
|
|
@@ -2172,15 +2174,15 @@ function movingKeysFor(key: RowKey): RowKey[] {
|
|
|
2172
2174
|
return result.length ? result : [key]
|
|
2173
2175
|
}
|
|
2174
2176
|
|
|
2175
|
-
/**
|
|
2177
|
+
/** Row key as in the pipeline (rowKey or index path). */
|
|
2176
2178
|
function keyOfRow(row: R, index: number): RowKey {
|
|
2177
2179
|
return props.rowKey ? (row[props.rowKey] as RowKey) : String(index)
|
|
2178
2180
|
}
|
|
2179
2181
|
|
|
2180
2182
|
/**
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2183
|
+
* Keyboard: move a row by one position (Alt + ↑/↓) — within its
|
|
2184
|
+
* SIBLINGS. Changing the parent is reserved for the mouse; via keyboard
|
|
2185
|
+
* it wouldn't be clear which level is being targeted.
|
|
2184
2186
|
*/
|
|
2185
2187
|
function moveRowByKeyboard(rowIndex: number, delta: number): void {
|
|
2186
2188
|
if (!state.orderActive) return
|
|
@@ -2194,7 +2196,7 @@ function moveRowByKeyboard(rowIndex: number, delta: number): void {
|
|
|
2194
2196
|
nav.setActive(rowIndex + delta, nav.state.col)
|
|
2195
2197
|
}
|
|
2196
2198
|
|
|
2197
|
-
// ──
|
|
2199
|
+
// ── Columns: resize, reorder, pin ────────────────────────────────────
|
|
2198
2200
|
|
|
2199
2201
|
function onResizeStart(columnId: string, event: PointerEvent): void {
|
|
2200
2202
|
layout.startResize(columnId, event.clientX)
|
|
@@ -2225,13 +2227,13 @@ function pinColumn(columnId: string, side: PinSide | false): void {
|
|
|
2225
2227
|
}
|
|
2226
2228
|
|
|
2227
2229
|
/**
|
|
2228
|
-
*
|
|
2230
|
+
* Size a column to the width of its widest content.
|
|
2229
2231
|
*
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2234
|
-
* (
|
|
2232
|
+
* The TEXT is measured, not the box: the content sits in a
|
|
2233
|
+
* block element that fills the cell — its scrollWidth would therefore always be
|
|
2234
|
+
* the current column width, and autosize could never shrink. A Range
|
|
2235
|
+
* over the node contents yields the real text width; for component content
|
|
2236
|
+
* (badges, tags) the measurement falls back to scrollWidth.
|
|
2235
2237
|
*/
|
|
2236
2238
|
function autoSizeColumn(columnId: string): void {
|
|
2237
2239
|
const cells = scrollerEl.value?.querySelectorAll<HTMLElement>(`[data-column="${columnId}"]`)
|
|
@@ -2247,7 +2249,7 @@ function autoSizeColumn(columnId: string): void {
|
|
|
2247
2249
|
}
|
|
2248
2250
|
widest = Math.max(widest, Math.max(measured, content.scrollWidth === content.clientWidth ? 0 : content.scrollWidth))
|
|
2249
2251
|
}
|
|
2250
|
-
const width = Math.ceil(widest) + 40 //
|
|
2252
|
+
const width = Math.ceil(widest) + 40 // padding + header tools
|
|
2251
2253
|
layout.setWidth(columnId, width)
|
|
2252
2254
|
emit("column-resize", { column: columnId, width, finished: true })
|
|
2253
2255
|
}
|
|
@@ -2272,7 +2274,7 @@ function resetColumns(): void {
|
|
|
2272
2274
|
layout.resetColumns()
|
|
2273
2275
|
}
|
|
2274
2276
|
|
|
2275
|
-
// ──
|
|
2277
|
+
// ── Pagination ───────────────────────────────────────────────────────
|
|
2276
2278
|
|
|
2277
2279
|
function setPage(next: number, notify = true): void {
|
|
2278
2280
|
const clamped = Math.max(0, Math.min(state.pageCount - 1, next))
|
|
@@ -2283,7 +2285,7 @@ function setPage(next: number, notify = true): void {
|
|
|
2283
2285
|
if (state.isServer) scheduleFetch(false, { page: clamped })
|
|
2284
2286
|
}
|
|
2285
2287
|
|
|
2286
|
-
/** Cursor
|
|
2288
|
+
/** Cursor bar: new page size — the caller reloads with it (page-change). */
|
|
2287
2289
|
function onCursorPageSize(next: number): void {
|
|
2288
2290
|
pageSize.value = next
|
|
2289
2291
|
emit("page-change", { page: 0, pageSize: next })
|
|
@@ -2296,16 +2298,16 @@ function setPageSize(next: number): void {
|
|
|
2296
2298
|
if (state.isServer) scheduleFetch(false, { page: 0, pageSize: next })
|
|
2297
2299
|
}
|
|
2298
2300
|
|
|
2299
|
-
// ── Server
|
|
2301
|
+
// ── Server mode ──────────────────────────────────────────────────────
|
|
2300
2302
|
|
|
2301
2303
|
/**
|
|
2302
|
-
*
|
|
2304
|
+
* What the next server fetch should use instead of the model values.
|
|
2303
2305
|
*
|
|
2304
|
-
*
|
|
2305
|
-
* `defineModel`
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2306
|
+
* Needed because with a BOUND `v-model`, Vue doesn't set the local value of a
|
|
2307
|
+
* `defineModel` immediately: the update only arrives via the parent's
|
|
2308
|
+
* prop. Reading right after writing therefore yields the old value —
|
|
2309
|
+
* unnoticed as long as nobody binds `v-model:sort` & co., and visibly wrong
|
|
2310
|
+
* on the first sort click as soon as someone does.
|
|
2309
2311
|
*/
|
|
2310
2312
|
type RequestOverrides = Partial<Pick<DataTableRequest, "sort" | "filters" | "search" | "page" | "pageSize">>
|
|
2311
2313
|
let pendingRequest: RequestOverrides = {}
|
|
@@ -2364,12 +2366,12 @@ async function fetchRows(): Promise<void> {
|
|
|
2364
2366
|
|
|
2365
2367
|
function reload(): void {
|
|
2366
2368
|
scheduleFetch(false)
|
|
2367
|
-
//
|
|
2368
|
-
//
|
|
2369
|
+
// Without dataSource the caller replaces props.data itself (the watch above
|
|
2370
|
+
// kicks in then); we reset the lazy-load tree in any case.
|
|
2369
2371
|
if (!props.dataSource) resetLazyTree()
|
|
2370
2372
|
}
|
|
2371
2373
|
|
|
2372
|
-
// ──
|
|
2374
|
+
// ── Copy & CSV ───────────────────────────────────────────────────────
|
|
2373
2375
|
|
|
2374
2376
|
interface ExportOptions {
|
|
2375
2377
|
columnIds?: string[]
|
|
@@ -2437,13 +2439,13 @@ async function copySelectedRows(options: ExportOptions & { format?: "tsv" | "csv
|
|
|
2437
2439
|
try {
|
|
2438
2440
|
await navigator.clipboard?.writeText(text)
|
|
2439
2441
|
} catch {
|
|
2440
|
-
// Clipboard
|
|
2442
|
+
// Clipboard may be denied (permission, insecure context).
|
|
2441
2443
|
}
|
|
2442
2444
|
state.announcement = t("dataTable.rowsCopied", { count: rows.length })
|
|
2443
2445
|
return rows.length
|
|
2444
2446
|
}
|
|
2445
2447
|
|
|
2446
|
-
// ──
|
|
2448
|
+
// ── Save / load state ────────────────────────────────────────────────
|
|
2447
2449
|
|
|
2448
2450
|
function getState(): DataTableState {
|
|
2449
2451
|
return {
|
|
@@ -2498,7 +2500,7 @@ function resetState(): void {
|
|
|
2498
2500
|
}
|
|
2499
2501
|
|
|
2500
2502
|
defineExpose({
|
|
2501
|
-
//
|
|
2503
|
+
// Rows
|
|
2502
2504
|
getVisibleRows: (): R[] =>
|
|
2503
2505
|
displayed()
|
|
2504
2506
|
.filter((row) => row.kind === "data")
|
|
@@ -2506,7 +2508,7 @@ defineExpose({
|
|
|
2506
2508
|
getProcessedRows: (): R[] => dataRowsOf(),
|
|
2507
2509
|
getSelectedRows: (): R[] => resolveSelectedRows(selectedKeys.value),
|
|
2508
2510
|
scrollToIndex: virtual.scrollToIndex,
|
|
2509
|
-
//
|
|
2511
|
+
// Expanding
|
|
2510
2512
|
expandRow: (key: RowKey) => {
|
|
2511
2513
|
if (!expandedKeys.value.includes(key)) expandedKeys.value = [...expandedKeys.value, key]
|
|
2512
2514
|
},
|
|
@@ -2519,7 +2521,7 @@ defineExpose({
|
|
|
2519
2521
|
collapseAllRows: () => {
|
|
2520
2522
|
expandedKeys.value = []
|
|
2521
2523
|
},
|
|
2522
|
-
//
|
|
2524
|
+
// Columns
|
|
2523
2525
|
toggleColumn: (id: string, visible: boolean) => layout.toggleColumn(id, visible, true),
|
|
2524
2526
|
pinColumn: (id: string, side: PinSide | false) => layout.pinColumn(id, side, true),
|
|
2525
2527
|
getColumnPin: layout.getColumnPin,
|
|
@@ -2528,32 +2530,32 @@ defineExpose({
|
|
|
2528
2530
|
sizeColumnsToFit,
|
|
2529
2531
|
resetColumns,
|
|
2530
2532
|
getColumnFacets: (id: string): ColumnFacets => (state.facets as Record<string, ColumnFacets>)[id] ?? { values: [] },
|
|
2531
|
-
//
|
|
2533
|
+
// Data
|
|
2532
2534
|
reload,
|
|
2533
|
-
/**
|
|
2535
|
+
/** Reset the lazy-load tree: arrows collapsed, children cache empty (fresh on expand). */
|
|
2534
2536
|
resetTree: resetLazyTree,
|
|
2535
|
-
/**
|
|
2537
|
+
/** Discard a row's lazy-loaded children and fetch them again. */
|
|
2536
2538
|
reloadChildren: async (key: RowKey): Promise<void> => {
|
|
2537
2539
|
const row = findRowByKey(key)
|
|
2538
2540
|
if (!row) return
|
|
2539
2541
|
delete state.loadedChildren[String(key)]
|
|
2540
|
-
await fetchChildren(row, true)
|
|
2542
|
+
return await fetchChildren(row, true)
|
|
2541
2543
|
},
|
|
2542
|
-
/**
|
|
2544
|
+
/** A row's lazy-loaded children (undefined = not loaded yet). */
|
|
2543
2545
|
getLoadedChildren: (key: RowKey): R[] | undefined => state.loadedChildren[String(key)] as R[] | undefined,
|
|
2544
2546
|
getDataAsCsv,
|
|
2545
2547
|
exportDataAsCsv,
|
|
2546
2548
|
copySelectedRows,
|
|
2547
|
-
//
|
|
2549
|
+
// State
|
|
2548
2550
|
getState,
|
|
2549
2551
|
setState,
|
|
2550
2552
|
resetState,
|
|
2551
2553
|
focus: () => void nav.focusActive(),
|
|
2552
2554
|
})
|
|
2553
2555
|
|
|
2554
|
-
// ──
|
|
2556
|
+
// ── Measure, observe, react ──────────────────────────────────────────
|
|
2555
2557
|
|
|
2556
|
-
/**
|
|
2558
|
+
/** Measure header cells — the widths are the basis for the sticky offsets. */
|
|
2557
2559
|
function observeHeaderCells(): void {
|
|
2558
2560
|
const scroller = scrollerEl.value
|
|
2559
2561
|
if (!scroller || !headerObserver) return
|
|
@@ -2563,7 +2565,7 @@ function observeHeaderCells(): void {
|
|
|
2563
2565
|
}
|
|
2564
2566
|
}
|
|
2565
2567
|
|
|
2566
|
-
/** Detail
|
|
2568
|
+
/** Detail panels have free height → measure them for virtualization. */
|
|
2567
2569
|
function measureDetailRows(): void {
|
|
2568
2570
|
const body = bodyEl.value
|
|
2569
2571
|
if (!body || !detailObserver) return
|
|
@@ -2628,7 +2630,7 @@ watch(
|
|
|
2628
2630
|
() => void nextTick(measureDetailRows)
|
|
2629
2631
|
)
|
|
2630
2632
|
|
|
2631
|
-
//
|
|
2633
|
+
// Data change: clamp the page into the valid range.
|
|
2632
2634
|
watch(
|
|
2633
2635
|
() => state.filteredCount,
|
|
2634
2636
|
() => {
|
|
@@ -2638,7 +2640,7 @@ watch(
|
|
|
2638
2640
|
</script>
|
|
2639
2641
|
|
|
2640
2642
|
<style scoped>
|
|
2641
|
-
/*
|
|
2643
|
+
/* Sizes and colors mirror the CSS variables of <wa-data-grid>. */
|
|
2642
2644
|
.vi-dt {
|
|
2643
2645
|
--dt-border-color: var(--wa-color-surface-border);
|
|
2644
2646
|
--dt-border-width: var(--wa-border-width-s);
|
|
@@ -2653,11 +2655,11 @@ watch(
|
|
|
2653
2655
|
display: flex;
|
|
2654
2656
|
flex-direction: column;
|
|
2655
2657
|
min-inline-size: 0;
|
|
2656
|
-
/*
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2658
|
+
/* Containing block for absolutely positioned children — above all .vi-dt-live.
|
|
2659
|
+
Without it the live region hangs off the initial container (body) and escapes
|
|
2660
|
+
EVERY `overflow: hidden` of its ancestors: it then sits at the
|
|
2661
|
+
document position of its grid and stretches the scroll area of the whole
|
|
2662
|
+
page (several thousand pixels with eight grids stacked). */
|
|
2661
2663
|
position: relative;
|
|
2662
2664
|
color: var(--wa-color-text-normal);
|
|
2663
2665
|
font-size: var(--wa-font-size-s);
|
|
@@ -2669,7 +2671,7 @@ watch(
|
|
|
2669
2671
|
overflow: hidden;
|
|
2670
2672
|
}
|
|
2671
2673
|
|
|
2672
|
-
/*
|
|
2674
|
+
/* Sizes: row height, padding and text scale together. */
|
|
2673
2675
|
.vi-dt.is-size-xs {
|
|
2674
2676
|
--dt-row-height: 2rem;
|
|
2675
2677
|
--dt-cell-padding: var(--wa-space-xs);
|
|
@@ -2699,15 +2701,15 @@ watch(
|
|
|
2699
2701
|
min-inline-size: 14rem;
|
|
2700
2702
|
}
|
|
2701
2703
|
|
|
2702
|
-
/*
|
|
2703
|
-
|
|
2704
|
+
/* One scroll area for header, body and footer — only this way do pinned
|
|
2705
|
+
columns and the header row stay aligned when scrolling in both directions. */
|
|
2704
2706
|
.vi-dt-scroller {
|
|
2705
2707
|
position: relative;
|
|
2706
2708
|
overflow: auto;
|
|
2707
|
-
/*
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
`--dt-max-height: none
|
|
2709
|
+
/* Stretches when the table has a fixed height (e.g. in a
|
|
2710
|
+
filling view) — otherwise it stays content-sized. The cap still
|
|
2711
|
+
applies to content-sized tables; to fill, set
|
|
2712
|
+
`--dt-max-height: none` from outside. */
|
|
2711
2713
|
flex: 1 1 auto;
|
|
2712
2714
|
min-block-size: 0;
|
|
2713
2715
|
max-block-size: var(--dt-max-height, 30rem);
|
|
@@ -2717,17 +2719,18 @@ watch(
|
|
|
2717
2719
|
inset-block-start: 0;
|
|
2718
2720
|
z-index: 3;
|
|
2719
2721
|
}
|
|
2720
|
-
/*
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2722
|
+
/* The row only provides the tint as a variable and lays out the cells —
|
|
2723
|
+
painting happens in the CELLS, which always cover the full width of the grid.
|
|
2724
|
+
The row box itself grows to the full width of its column tracks
|
|
2725
|
+
(`min-inline-size: max-content`). Without that it is only as wide as the
|
|
2726
|
+
visible area while the tracks extend beyond it — and cells pinned to the
|
|
2727
|
+
start, being `position: sticky` inside the row, scroll away with the row
|
|
2728
|
+
once the table is scrolled further than one visible width. */
|
|
2727
2729
|
.vi-dt-row {
|
|
2728
2730
|
--dt-row-bg: var(--wa-color-surface-default);
|
|
2729
2731
|
display: grid;
|
|
2730
2732
|
align-items: stretch;
|
|
2733
|
+
min-inline-size: max-content;
|
|
2731
2734
|
min-block-size: var(--dt-row-height, 2.75rem);
|
|
2732
2735
|
background-color: transparent;
|
|
2733
2736
|
}
|
|
@@ -2739,14 +2742,14 @@ watch(
|
|
|
2739
2742
|
.vi-dt.is-striped .vi-dt-row.is-odd {
|
|
2740
2743
|
--dt-row-bg: var(--dt-stripe);
|
|
2741
2744
|
}
|
|
2742
|
-
/*
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2745
|
+
/* Precedence of the row color: selection > hover > stripe > base surface.
|
|
2746
|
+
The stripe rule needs `.is-striped` on the root element and thus weighs
|
|
2747
|
+
more than a plain `.vi-dt-row.is-data:hover` — without the following
|
|
2748
|
+
prefixes neither hover nor selection would be visible on odd rows. */
|
|
2746
2749
|
.vi-dt .vi-dt-row.is-data:hover {
|
|
2747
2750
|
--dt-row-bg: var(--dt-row-hover);
|
|
2748
2751
|
}
|
|
2749
|
-
/*
|
|
2752
|
+
/* Selection via row click: the row signals that it is clickable. */
|
|
2750
2753
|
.vi-dt.is-selectable .vi-dt-row.is-data {
|
|
2751
2754
|
cursor: pointer;
|
|
2752
2755
|
}
|
|
@@ -2789,10 +2792,15 @@ watch(
|
|
|
2789
2792
|
gap: var(--wa-space-2xs);
|
|
2790
2793
|
min-inline-size: 0;
|
|
2791
2794
|
padding-inline: var(--dt-cell-padding);
|
|
2792
|
-
/*
|
|
2795
|
+
/* Each cell carries the tint and divider itself (see .vi-dt-row). */
|
|
2793
2796
|
background-color: var(--dt-row-bg);
|
|
2794
2797
|
border-block-end: var(--dt-border-width) solid var(--dt-border-color);
|
|
2795
2798
|
overflow: hidden;
|
|
2799
|
+
/* Every row is its own grid, sized by `max-content` (see .vi-dt-row). If
|
|
2800
|
+
cell content counted towards that size, a row with a long text would
|
|
2801
|
+
size its flex columns differently from the others and the columns would
|
|
2802
|
+
no longer line up. Content still overflows and ellipsizes as before. */
|
|
2803
|
+
contain: inline-size;
|
|
2796
2804
|
}
|
|
2797
2805
|
.vi-dt-cell.is-lead {
|
|
2798
2806
|
padding-inline: var(--wa-space-2xs);
|
|
@@ -2808,12 +2816,12 @@ watch(
|
|
|
2808
2816
|
outline: var(--wa-focus-ring);
|
|
2809
2817
|
outline-offset: -2px;
|
|
2810
2818
|
}
|
|
2811
|
-
/*
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
`background-color: var(--dt-row-bg)`
|
|
2815
|
-
|
|
2816
|
-
|
|
2819
|
+
/* Pinned cells lie ABOVE the horizontally scrolling content and must hide it
|
|
2820
|
+
opaquely. The row tints (stripe, hover, selection) are
|
|
2821
|
+
semi-transparent (color-mix with transparent), so a plain
|
|
2822
|
+
`background-color: var(--dt-row-bg)` would let the content show through.
|
|
2823
|
+
Hence: an opaque base surface as the color, the row tint as an overlay — the
|
|
2824
|
+
result is the same color value as for the freely scrolling cells. */
|
|
2817
2825
|
.vi-dt-cell.is-sticky {
|
|
2818
2826
|
background-color: var(--wa-color-surface-default);
|
|
2819
2827
|
background-image: linear-gradient(var(--dt-row-bg), var(--dt-row-bg));
|
|
@@ -2838,8 +2846,8 @@ watch(
|
|
|
2838
2846
|
.vi-dt-hcell {
|
|
2839
2847
|
padding-inline: var(--wa-space-s);
|
|
2840
2848
|
}
|
|
2841
|
-
/*
|
|
2842
|
-
|
|
2849
|
+
/* The label fills the free space — otherwise the filter and menu buttons
|
|
2850
|
+
slide into the middle of the cell and intercept the sort click. */
|
|
2843
2851
|
.vi-dt-hcell .vi-dt-hlabel {
|
|
2844
2852
|
flex: 1 1 auto;
|
|
2845
2853
|
min-inline-size: 0;
|
|
@@ -2851,16 +2859,16 @@ watch(
|
|
|
2851
2859
|
.vi-dt-hcell.is-align-center .vi-dt-hlabel {
|
|
2852
2860
|
text-align: center;
|
|
2853
2861
|
}
|
|
2854
|
-
/*
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
/* visibility
|
|
2858
|
-
(
|
|
2859
|
-
|
|
2860
|
-
-
|
|
2861
|
-
|
|
2862
|
-
-
|
|
2863
|
-
|
|
2862
|
+
/* Only show the column menu when the column is focused/hovered — otherwise it eats
|
|
2863
|
+
the label. (wa-dropdown itself is display:contents, so style the trigger.)
|
|
2864
|
+
Active filters always stay visible. */
|
|
2865
|
+
/* visibility instead of display: the button is invisible and NOT clickable
|
|
2866
|
+
(so it doesn't intercept the sort click in the middle of the cell), but keeps its
|
|
2867
|
+
place in the layout. Both matter:
|
|
2868
|
+
- With display:none the button leaves the layout. If its menu is open, the
|
|
2869
|
+
popup suddenly has no anchor anymore and jumps to the top left corner.
|
|
2870
|
+
- Also, the header row would reflow on every hover change and shift open
|
|
2871
|
+
filter popovers by the button width. */
|
|
2864
2872
|
.vi-dt-column-menu-button {
|
|
2865
2873
|
visibility: hidden;
|
|
2866
2874
|
}
|
|
@@ -2925,24 +2933,24 @@ watch(
|
|
|
2925
2933
|
position: relative;
|
|
2926
2934
|
}
|
|
2927
2935
|
|
|
2928
|
-
/* ──
|
|
2929
|
-
/*
|
|
2930
|
-
|
|
2931
|
-
|
|
2936
|
+
/* ── Making the tree structure visible ─────────────────────────────── */
|
|
2937
|
+
/* Indentation alone reads poorly. Hence one vertical guide line per level
|
|
2938
|
+
(as a background pattern over the indented area) and a
|
|
2939
|
+
short corner to the row itself — like in a file tree. */
|
|
2932
2940
|
.vi-dt {
|
|
2933
2941
|
--dt-guide: var(--wa-color-surface-border);
|
|
2934
2942
|
}
|
|
2935
2943
|
.vi-dt-cell.is-tree-cell {
|
|
2936
2944
|
position: relative;
|
|
2937
2945
|
}
|
|
2938
|
-
/*
|
|
2939
|
-
|
|
2940
|
-
is-line →
|
|
2941
|
-
is-tee →
|
|
2942
|
-
is-corner →
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
+
/* One slot per level. The vertical line sits in the MIDDLE of the slot so
|
|
2947
|
+
it runs through the expand arrows of the respective level.
|
|
2948
|
+
is-line → continuous line (ancestor still has siblings)
|
|
2949
|
+
is-tee → "├" (this row still has siblings)
|
|
2950
|
+
is-corner → "└" (last child: empty below)
|
|
2951
|
+
none → empty slot (the subtree ends there)
|
|
2952
|
+
The arm ends at the edge of the slot — it does NOT run past the arrow or the
|
|
2953
|
+
leaf placeholder up to the text. */
|
|
2946
2954
|
.vi-dt-guide {
|
|
2947
2955
|
position: relative;
|
|
2948
2956
|
flex: 0 0 auto;
|
|
@@ -2959,11 +2967,11 @@ watch(
|
|
|
2959
2967
|
inline-size: 1px;
|
|
2960
2968
|
background-color: var(--dt-guide);
|
|
2961
2969
|
}
|
|
2962
|
-
/*
|
|
2970
|
+
/* Corner: the vertical line ends at half height. */
|
|
2963
2971
|
.vi-dt-guide.is-corner::before {
|
|
2964
2972
|
inset-block: 0 50%;
|
|
2965
2973
|
}
|
|
2966
|
-
/*
|
|
2974
|
+
/* Short arm from the middle to the edge of the slot. */
|
|
2967
2975
|
.vi-dt-guide.is-tee::after,
|
|
2968
2976
|
.vi-dt-guide.is-corner::after {
|
|
2969
2977
|
content: "";
|
|
@@ -2974,15 +2982,15 @@ watch(
|
|
|
2974
2982
|
background-color: var(--dt-guide);
|
|
2975
2983
|
}
|
|
2976
2984
|
|
|
2977
|
-
/*
|
|
2978
|
-
|
|
2979
|
-
|
|
2985
|
+
/* Arrow and leaf placeholder MUST be equally wide — otherwise the labels of a
|
|
2986
|
+
level don't line up and the indentation gets
|
|
2987
|
+
partially eaten up (the wa-button is wider by itself). */
|
|
2980
2988
|
.vi-dt-expand {
|
|
2981
2989
|
flex: 0 0 auto;
|
|
2982
2990
|
inline-size: var(--dt-toggle-size);
|
|
2983
2991
|
}
|
|
2984
|
-
/*
|
|
2985
|
-
|
|
2992
|
+
/* Loaded and empty: the arrow stays in place (no layout jump),
|
|
2993
|
+
but is visibly out of service. */
|
|
2986
2994
|
.vi-dt-expand.is-empty {
|
|
2987
2995
|
opacity: 0.3;
|
|
2988
2996
|
cursor: default;
|
|
@@ -2991,13 +2999,13 @@ watch(
|
|
|
2991
2999
|
font-size: 0.75em;
|
|
2992
3000
|
--track-width: 2px;
|
|
2993
3001
|
}
|
|
2994
|
-
/*
|
|
3002
|
+
/* Leaves: same spacing as between arrow and guide lines. */
|
|
2995
3003
|
.vi-dt-leaf-gap {
|
|
2996
3004
|
flex: 0 0 auto;
|
|
2997
3005
|
inline-size: var(--wa-space-2xs);
|
|
2998
3006
|
}
|
|
2999
3007
|
|
|
3000
|
-
/* ──
|
|
3008
|
+
/* ── Fixed order: handle and insertion marker ──────────────────────── */
|
|
3001
3009
|
.vi-dt-grip {
|
|
3002
3010
|
display: inline-flex;
|
|
3003
3011
|
align-items: center;
|
|
@@ -3018,16 +3026,16 @@ watch(
|
|
|
3018
3026
|
.vi-dt-row.is-dragging {
|
|
3019
3027
|
opacity: 0.5;
|
|
3020
3028
|
}
|
|
3021
|
-
/*
|
|
3022
|
-
|
|
3029
|
+
/* Marker where the row would land. As an inset shadow so it costs no
|
|
3030
|
+
layout height and rows don't jump while dragging. */
|
|
3023
3031
|
.vi-dt-row.is-drop-before .vi-dt-cell {
|
|
3024
3032
|
box-shadow: inset 0 2px 0 var(--dt-accent);
|
|
3025
3033
|
}
|
|
3026
3034
|
.vi-dt-row.is-drop-after .vi-dt-cell {
|
|
3027
3035
|
box-shadow: inset 0 -2px 0 var(--dt-accent);
|
|
3028
3036
|
}
|
|
3029
|
-
/*
|
|
3030
|
-
|
|
3037
|
+
/* "Inside": the row becomes the parent — hence a frame around the whole row
|
|
3038
|
+
instead of a line at the edge. */
|
|
3031
3039
|
.vi-dt-row.is-drop-inside .vi-dt-cell {
|
|
3032
3040
|
box-shadow:
|
|
3033
3041
|
inset 0 2px 0 var(--dt-accent),
|
|
@@ -3037,8 +3045,8 @@ watch(
|
|
|
3037
3045
|
--dt-row-bg: color-mix(in oklab, var(--dt-selected) 60%, transparent);
|
|
3038
3046
|
}
|
|
3039
3047
|
|
|
3040
|
-
/*
|
|
3041
|
-
|
|
3048
|
+
/* The expand column sits left of the tree cell — its line continues
|
|
3049
|
+
so arrow and structure connect. */
|
|
3042
3050
|
.vi-dt-cell.is-lead.is-expand-cell {
|
|
3043
3051
|
position: relative;
|
|
3044
3052
|
}
|
|
@@ -3141,8 +3149,8 @@ watch(
|
|
|
3141
3149
|
font-size: var(--wa-font-size-s);
|
|
3142
3150
|
cursor: pointer;
|
|
3143
3151
|
}
|
|
3144
|
-
/*
|
|
3145
|
-
(position: relative)
|
|
3152
|
+
/* Screen readers only: invisible but present. The containing block is .vi-dt
|
|
3153
|
+
(position: relative) so the region stays clipped within the grid. */
|
|
3146
3154
|
.vi-dt-live {
|
|
3147
3155
|
position: absolute;
|
|
3148
3156
|
inset-block-start: 0;
|