viur-ui 0.0.1 → 0.0.2
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 +3 -5
- 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 +346 -346
- 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 +15 -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 +22 -22
- 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
|
@@ -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"
|
|
@@ -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,14 +1771,14 @@ 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) {
|
|
@@ -1801,7 +1801,7 @@ function toggleExpand(displayRow: DisplayRow<R>): void {
|
|
|
1801
1801
|
}
|
|
1802
1802
|
}
|
|
1803
1803
|
|
|
1804
|
-
/**
|
|
1804
|
+
/** Expand a row and emit the event. */
|
|
1805
1805
|
function expand(key: RowKey, row?: R): void {
|
|
1806
1806
|
if (expandedKeys.value.includes(key)) return
|
|
1807
1807
|
expandedKeys.value = [...expandedKeys.value, key]
|
|
@@ -1816,19 +1816,19 @@ function allExpandableKeys(nodes: RowNode<R>[] = state.built.nodes as RowNode<R>
|
|
|
1816
1816
|
return into
|
|
1817
1817
|
}
|
|
1818
1818
|
|
|
1819
|
-
// ──
|
|
1819
|
+
// ── Cell clicks ──────────────────────────────────────────────────────
|
|
1820
1820
|
|
|
1821
1821
|
/**
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1822
|
+
* Double click on a data row. Controls in the row (selection,
|
|
1823
|
+
* expand, actions) don't trigger it — double-clicking there doesn't mean
|
|
1824
|
+
* "open record".
|
|
1825
1825
|
*/
|
|
1826
1826
|
function onRowDoubleClick(displayRow: DisplayRow<R>, event: MouseEvent): void {
|
|
1827
1827
|
if (displayRow.kind !== "data" || !displayRow.row) return
|
|
1828
1828
|
const target = event.target as HTMLElement
|
|
1829
1829
|
if (target.closest("wa-checkbox, wa-button, a, input, select, .vi-dt-expand")) return
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1830
|
+
// A double click fires two clicks, which would undo the click selection
|
|
1831
|
+
// again — the row stays/becomes selected.
|
|
1832
1832
|
if (props.selectable !== false && props.selectOnRowClick && canSelect(displayRow.row)) {
|
|
1833
1833
|
const key = displayRow.nodeKey
|
|
1834
1834
|
if (!state.selectedSet.has(key)) {
|
|
@@ -1852,8 +1852,8 @@ function onCellClick(displayRow: DisplayRow<R>, cell: CellColumn, event: MouseEv
|
|
|
1852
1852
|
if (cell.kind !== "data" || !displayRow.row) return
|
|
1853
1853
|
const target = event.target as HTMLElement
|
|
1854
1854
|
if (target.closest(".vi-dt-expand, .vi-dt-checkbox")) return
|
|
1855
|
-
//
|
|
1856
|
-
// in
|
|
1855
|
+
// Select the row on click — unless the click hits an interactive element
|
|
1856
|
+
// in the cell (link, button, input field) that handles the click itself.
|
|
1857
1857
|
if (
|
|
1858
1858
|
props.selectable !== false &&
|
|
1859
1859
|
props.selectOnRowClick &&
|
|
@@ -1881,14 +1881,14 @@ function onCellContextMenu(displayRow: DisplayRow<R>, cell: CellColumn, event: M
|
|
|
1881
1881
|
})
|
|
1882
1882
|
}
|
|
1883
1883
|
|
|
1884
|
-
// ──
|
|
1884
|
+
// ── Fixed order: reordering rows ─────────────────────────────────────
|
|
1885
1885
|
|
|
1886
1886
|
/**
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1887
|
+
* Rows are dragged with POINTER events, not with HTML5 drag and drop.
|
|
1888
|
+
* Reason: `dragover` requires an accepting target and fires unreliably
|
|
1889
|
+
* depending on the environment (not at all on touch) — the insertion marker would then
|
|
1890
|
+
* be missing and the target position would be random. Pointer events behave the same
|
|
1891
|
+
* everywhere, work with touch and are testable.
|
|
1892
1892
|
*/
|
|
1893
1893
|
function beginRowDrag(displayRow: DisplayRow<R>, event: PointerEvent): void {
|
|
1894
1894
|
if (!state.orderActive || displayRow.kind !== "data") return
|
|
@@ -1916,15 +1916,15 @@ function beginRowDrag(displayRow: DisplayRow<R>, event: PointerEvent): void {
|
|
|
1916
1916
|
}
|
|
1917
1917
|
|
|
1918
1918
|
/**
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
* (
|
|
1919
|
+
* Determine the row under the pointer and derive the zone. As in admin4 there are
|
|
1920
|
+
* three: top quarter = before, bottom quarter = after, the middle = INSIDE
|
|
1921
|
+
* (the row becomes a child of the target). Without a tree only before/after remain.
|
|
1922
1922
|
*/
|
|
1923
1923
|
function updateRowDrag(clientX: number, clientY: number): void {
|
|
1924
1924
|
const drag = state.rowDrag
|
|
1925
1925
|
if (!drag) return
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
1926
|
+
// Ask the enclosing session first: if it provides a target (tree node or
|
|
1927
|
+
// folder row), it takes over display and drop — the internal indicator rests.
|
|
1928
1928
|
if (dragSession?.state.active) {
|
|
1929
1929
|
const external = dragSession.update(clientX, clientY)
|
|
1930
1930
|
if (external) {
|
|
@@ -1937,7 +1937,7 @@ function updateRowDrag(clientX: number, clientY: number): void {
|
|
|
1937
1937
|
const target = displayed().find((row) => row.key === element.dataset.key)
|
|
1938
1938
|
if (!target || target.nodeKey === drag.key) return
|
|
1939
1939
|
|
|
1940
|
-
//
|
|
1940
|
+
// Dragging into its own subtree is impossible — it would hang from itself.
|
|
1941
1941
|
if (isDescendant(state.built.nodes as RowNode<R>[], drag.key, target.nodeKey)) {
|
|
1942
1942
|
drag.overKey = null
|
|
1943
1943
|
return
|
|
@@ -1952,7 +1952,7 @@ function updateRowDrag(clientX: number, clientY: number): void {
|
|
|
1952
1952
|
else drag.mode = "inside"
|
|
1953
1953
|
}
|
|
1954
1954
|
|
|
1955
|
-
/**
|
|
1955
|
+
/** Lazy-load mode: take the row out of its old bucket and insert it at the target. */
|
|
1956
1956
|
function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, insertAt: number, order: number): void {
|
|
1957
1957
|
const row = source.node.row
|
|
1958
1958
|
if (!row) return
|
|
@@ -1964,9 +1964,9 @@ function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, ins
|
|
|
1964
1964
|
const hidden = new Set(lazyMoves.value.hidden)
|
|
1965
1965
|
let injected = lazyMoves.value.injected.filter((candidate) => (candidate[props.rowKey!] as RowKey) !== key)
|
|
1966
1966
|
|
|
1967
|
-
//
|
|
1967
|
+
// Remove from the source …
|
|
1968
1968
|
if (source.parentKey === null) {
|
|
1969
|
-
hidden.add(key) //
|
|
1969
|
+
hidden.add(key) // top level: props.data stays untouched, just hide
|
|
1970
1970
|
} else {
|
|
1971
1971
|
const bucket = state.loadedChildren[String(source.parentKey)] as R[] | undefined
|
|
1972
1972
|
if (bucket) {
|
|
@@ -1976,10 +1976,10 @@ function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, ins
|
|
|
1976
1976
|
}
|
|
1977
1977
|
}
|
|
1978
1978
|
|
|
1979
|
-
// …
|
|
1979
|
+
// … and insert at the target.
|
|
1980
1980
|
if (targetParent === null) {
|
|
1981
|
-
//
|
|
1982
|
-
//
|
|
1981
|
+
// The original (if it is in props.data) stays hidden; the copy with the
|
|
1982
|
+
// new order value is visible.
|
|
1983
1983
|
hidden.add(key)
|
|
1984
1984
|
injected = [...injected, moved]
|
|
1985
1985
|
} else {
|
|
@@ -1989,20 +1989,20 @@ function applyLazyMove(source: NodeLocation<R>, targetParent: RowKey | null, ins
|
|
|
1989
1989
|
next.splice(Math.max(0, Math.min(insertAt, next.length)), 0, moved)
|
|
1990
1990
|
state.loadedChildren[String(targetParent)] = next as (typeof state.loadedChildren)[string]
|
|
1991
1991
|
}
|
|
1992
|
-
// Bucket
|
|
1993
|
-
//
|
|
1992
|
+
// Bucket never loaded: on expand fetchChildren gets the current state
|
|
1993
|
+
// from the server — which contains the row after saving anyway.
|
|
1994
1994
|
}
|
|
1995
1995
|
|
|
1996
1996
|
lazyMoves.value = { hidden, injected }
|
|
1997
1997
|
}
|
|
1998
1998
|
|
|
1999
1999
|
/**
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2000
|
+
* Reset lazy-load mode: discard the children cache and collapse all
|
|
2001
|
+
* branches — the arrows are back to "collapsed" afterwards, and the next
|
|
2002
|
+
* expand fetches the fresh state from the server. Runs on `reload()` and
|
|
2003
|
+
* when the caller delivers new data; without `loadChildren` (parentField
|
|
2004
|
+
* trees) the expand state is kept, since there the children are always
|
|
2005
|
+
* derived from the current data anyway.
|
|
2006
2006
|
*/
|
|
2007
2007
|
function resetLazyTree(): void {
|
|
2008
2008
|
if (!props.loadChildren) return
|
|
@@ -2011,15 +2011,15 @@ function resetLazyTree(): void {
|
|
|
2011
2011
|
if (expandedKeys.value.length) expandedKeys.value = []
|
|
2012
2012
|
}
|
|
2013
2013
|
|
|
2014
|
-
// Drag &
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2019
|
-
//
|
|
2020
|
-
//
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2014
|
+
// Drag & drop is purely CLIENT-SIDE: the row is moved locally (buckets/
|
|
2015
|
+
// overrides above), `row-reorder` reports the move for saving in the
|
|
2016
|
+
// background — the table reloads NOTHING afterwards, and the app doesn't
|
|
2017
|
+
// have to either. When new data comes in from outside, it is authoritative: the
|
|
2018
|
+
// optimistic overrides are discarded (fresh data already contains
|
|
2019
|
+
// a saved move). The lazy-load tree stays as it is —
|
|
2020
|
+
// it is only collapsed and cleared via `reload()`/`resetTree()` or, via the
|
|
2021
|
+
// opt-in `resetTreeOnDataChange`, on every data change not preceded by a
|
|
2022
|
+
// move.
|
|
2023
2023
|
watch(
|
|
2024
2024
|
() => (props.dataSource ? (state.serverRows as R[]) : props.data),
|
|
2025
2025
|
() => {
|
|
@@ -2036,8 +2036,8 @@ watch(
|
|
|
2036
2036
|
function commitRowDrag(): void {
|
|
2037
2037
|
const drag = state.rowDrag
|
|
2038
2038
|
state.rowDrag = null
|
|
2039
|
-
//
|
|
2040
|
-
//
|
|
2039
|
+
// A session target (tree/folder outside the internal logic) takes precedence:
|
|
2040
|
+
// the Browser takes over the move, nothing else happens here.
|
|
2041
2041
|
if (dragSession?.state.active) {
|
|
2042
2042
|
if (dragSession.state.over) {
|
|
2043
2043
|
dragSession.drop()
|
|
@@ -2051,11 +2051,11 @@ function commitRowDrag(): void {
|
|
|
2051
2051
|
}
|
|
2052
2052
|
|
|
2053
2053
|
/**
|
|
2054
|
-
*
|
|
2054
|
+
* Move a row to a new position and report the new order value.
|
|
2055
2055
|
*
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2056
|
+
* The computation is always among the future SIBLINGS — for "inside" that is
|
|
2057
|
+
* among the target's children, otherwise among the target's siblings. Only then
|
|
2058
|
+
* does the value make sense in the tree: `sortindex` applies per parent.
|
|
2059
2059
|
*/
|
|
2060
2060
|
function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
2061
2061
|
if (!props.orderField || key === targetKey) return
|
|
@@ -2065,9 +2065,9 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2065
2065
|
const target = locateNode(nodes, targetKey)
|
|
2066
2066
|
if (!target) return
|
|
2067
2067
|
|
|
2068
|
-
//
|
|
2069
|
-
//
|
|
2070
|
-
// (
|
|
2068
|
+
// Multi-selection: if the dragged row is part of the selection, the whole
|
|
2069
|
+
// selection moves along — in display order. Rows that may not go to the target
|
|
2070
|
+
// (the target itself, ancestors of the target) are left out.
|
|
2071
2071
|
const moving = movingKeysFor(key).filter(
|
|
2072
2072
|
(candidate) => candidate !== targetKey && !isDescendant(nodes, candidate, targetKey)
|
|
2073
2073
|
)
|
|
@@ -2081,7 +2081,7 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2081
2081
|
let values: number[]
|
|
2082
2082
|
|
|
2083
2083
|
if (mode === "inside") {
|
|
2084
|
-
//
|
|
2084
|
+
// New children: after the target's existing children.
|
|
2085
2085
|
parentKey = targetKey
|
|
2086
2086
|
const children = target.node.children.filter((child) => !movingSet.has(child.key))
|
|
2087
2087
|
values = children.map(orderOf)
|
|
@@ -2103,12 +2103,12 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2103
2103
|
if (!source?.node.row) continue
|
|
2104
2104
|
|
|
2105
2105
|
const { order, needsReindex } = orderValueFor(values, insertAt, props.orderStep)
|
|
2106
|
-
//
|
|
2107
|
-
//
|
|
2106
|
+
// Add the assigned value to the neighbour list so the next
|
|
2107
|
+
// row of the selection is sorted in AFTER it (order is preserved).
|
|
2108
2108
|
values.splice(Math.max(0, Math.min(values.length, insertAt)), 0, order)
|
|
2109
2109
|
|
|
2110
|
-
//
|
|
2111
|
-
//
|
|
2110
|
+
// Apply optimistically: the row leaves its old position immediately and
|
|
2111
|
+
// appears at the target — on a drop INTO a folder, among its children.
|
|
2112
2112
|
if (props.rowKey) {
|
|
2113
2113
|
if (props.loadChildren) {
|
|
2114
2114
|
applyLazyMove(source, parentKey, insertAt, order)
|
|
@@ -2146,10 +2146,10 @@ function moveRowTo(key: RowKey, targetKey: RowKey, mode: DropMode): void {
|
|
|
2146
2146
|
}
|
|
2147
2147
|
|
|
2148
2148
|
/**
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
*
|
|
2152
|
-
*
|
|
2149
|
+
* Which rows a move affects: only the dragged one — or, if it is part of the
|
|
2150
|
+
* selection, all selected rows in display order. Selected
|
|
2151
|
+
* descendants of an also-selected row move implicitly with their
|
|
2152
|
+
* subtree and are not moved twice.
|
|
2153
2153
|
*/
|
|
2154
2154
|
function movingKeysFor(key: RowKey): RowKey[] {
|
|
2155
2155
|
if (props.selectable === false || !state.selectedSet.has(key) || state.selectedSet.size < 2) return [key]
|
|
@@ -2159,8 +2159,8 @@ function movingKeysFor(key: RowKey): RowKey[] {
|
|
|
2159
2159
|
for (const row of state.dataDisplayRows as DisplayRow<R>[]) {
|
|
2160
2160
|
if (selected.has(row.nodeKey)) ordered.push(row.nodeKey)
|
|
2161
2161
|
}
|
|
2162
|
-
// dataDisplayRows
|
|
2163
|
-
//
|
|
2162
|
+
// dataDisplayRows only contains VISIBLE rows — selected rows in
|
|
2163
|
+
// collapsed branches are appended so they aren't left behind.
|
|
2164
2164
|
const seen = new Set(ordered)
|
|
2165
2165
|
for (const candidate of selected) {
|
|
2166
2166
|
if (!seen.has(candidate)) ordered.push(candidate)
|
|
@@ -2172,15 +2172,15 @@ function movingKeysFor(key: RowKey): RowKey[] {
|
|
|
2172
2172
|
return result.length ? result : [key]
|
|
2173
2173
|
}
|
|
2174
2174
|
|
|
2175
|
-
/**
|
|
2175
|
+
/** Row key as in the pipeline (rowKey or index path). */
|
|
2176
2176
|
function keyOfRow(row: R, index: number): RowKey {
|
|
2177
2177
|
return props.rowKey ? (row[props.rowKey] as RowKey) : String(index)
|
|
2178
2178
|
}
|
|
2179
2179
|
|
|
2180
2180
|
/**
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2181
|
+
* Keyboard: move a row by one position (Alt + ↑/↓) — within its
|
|
2182
|
+
* SIBLINGS. Changing the parent is reserved for the mouse; via keyboard
|
|
2183
|
+
* it wouldn't be clear which level is being targeted.
|
|
2184
2184
|
*/
|
|
2185
2185
|
function moveRowByKeyboard(rowIndex: number, delta: number): void {
|
|
2186
2186
|
if (!state.orderActive) return
|
|
@@ -2194,7 +2194,7 @@ function moveRowByKeyboard(rowIndex: number, delta: number): void {
|
|
|
2194
2194
|
nav.setActive(rowIndex + delta, nav.state.col)
|
|
2195
2195
|
}
|
|
2196
2196
|
|
|
2197
|
-
// ──
|
|
2197
|
+
// ── Columns: resize, reorder, pin ────────────────────────────────────
|
|
2198
2198
|
|
|
2199
2199
|
function onResizeStart(columnId: string, event: PointerEvent): void {
|
|
2200
2200
|
layout.startResize(columnId, event.clientX)
|
|
@@ -2225,13 +2225,13 @@ function pinColumn(columnId: string, side: PinSide | false): void {
|
|
|
2225
2225
|
}
|
|
2226
2226
|
|
|
2227
2227
|
/**
|
|
2228
|
-
*
|
|
2228
|
+
* Size a column to the width of its widest content.
|
|
2229
2229
|
*
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2234
|
-
* (
|
|
2230
|
+
* The TEXT is measured, not the box: the content sits in a
|
|
2231
|
+
* block element that fills the cell — its scrollWidth would therefore always be
|
|
2232
|
+
* the current column width, and autosize could never shrink. A Range
|
|
2233
|
+
* over the node contents yields the real text width; for component content
|
|
2234
|
+
* (badges, tags) the measurement falls back to scrollWidth.
|
|
2235
2235
|
*/
|
|
2236
2236
|
function autoSizeColumn(columnId: string): void {
|
|
2237
2237
|
const cells = scrollerEl.value?.querySelectorAll<HTMLElement>(`[data-column="${columnId}"]`)
|
|
@@ -2247,7 +2247,7 @@ function autoSizeColumn(columnId: string): void {
|
|
|
2247
2247
|
}
|
|
2248
2248
|
widest = Math.max(widest, Math.max(measured, content.scrollWidth === content.clientWidth ? 0 : content.scrollWidth))
|
|
2249
2249
|
}
|
|
2250
|
-
const width = Math.ceil(widest) + 40 //
|
|
2250
|
+
const width = Math.ceil(widest) + 40 // padding + header tools
|
|
2251
2251
|
layout.setWidth(columnId, width)
|
|
2252
2252
|
emit("column-resize", { column: columnId, width, finished: true })
|
|
2253
2253
|
}
|
|
@@ -2272,7 +2272,7 @@ function resetColumns(): void {
|
|
|
2272
2272
|
layout.resetColumns()
|
|
2273
2273
|
}
|
|
2274
2274
|
|
|
2275
|
-
// ──
|
|
2275
|
+
// ── Pagination ───────────────────────────────────────────────────────
|
|
2276
2276
|
|
|
2277
2277
|
function setPage(next: number, notify = true): void {
|
|
2278
2278
|
const clamped = Math.max(0, Math.min(state.pageCount - 1, next))
|
|
@@ -2283,7 +2283,7 @@ function setPage(next: number, notify = true): void {
|
|
|
2283
2283
|
if (state.isServer) scheduleFetch(false, { page: clamped })
|
|
2284
2284
|
}
|
|
2285
2285
|
|
|
2286
|
-
/** Cursor
|
|
2286
|
+
/** Cursor bar: new page size — the caller reloads with it (page-change). */
|
|
2287
2287
|
function onCursorPageSize(next: number): void {
|
|
2288
2288
|
pageSize.value = next
|
|
2289
2289
|
emit("page-change", { page: 0, pageSize: next })
|
|
@@ -2296,16 +2296,16 @@ function setPageSize(next: number): void {
|
|
|
2296
2296
|
if (state.isServer) scheduleFetch(false, { page: 0, pageSize: next })
|
|
2297
2297
|
}
|
|
2298
2298
|
|
|
2299
|
-
// ── Server
|
|
2299
|
+
// ── Server mode ──────────────────────────────────────────────────────
|
|
2300
2300
|
|
|
2301
2301
|
/**
|
|
2302
|
-
*
|
|
2302
|
+
* What the next server fetch should use instead of the model values.
|
|
2303
2303
|
*
|
|
2304
|
-
*
|
|
2305
|
-
* `defineModel`
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2304
|
+
* Needed because with a BOUND `v-model`, Vue doesn't set the local value of a
|
|
2305
|
+
* `defineModel` immediately: the update only arrives via the parent's
|
|
2306
|
+
* prop. Reading right after writing therefore yields the old value —
|
|
2307
|
+
* unnoticed as long as nobody binds `v-model:sort` & co., and visibly wrong
|
|
2308
|
+
* on the first sort click as soon as someone does.
|
|
2309
2309
|
*/
|
|
2310
2310
|
type RequestOverrides = Partial<Pick<DataTableRequest, "sort" | "filters" | "search" | "page" | "pageSize">>
|
|
2311
2311
|
let pendingRequest: RequestOverrides = {}
|
|
@@ -2364,12 +2364,12 @@ async function fetchRows(): Promise<void> {
|
|
|
2364
2364
|
|
|
2365
2365
|
function reload(): void {
|
|
2366
2366
|
scheduleFetch(false)
|
|
2367
|
-
//
|
|
2368
|
-
//
|
|
2367
|
+
// Without dataSource the caller replaces props.data itself (the watch above
|
|
2368
|
+
// kicks in then); we reset the lazy-load tree in any case.
|
|
2369
2369
|
if (!props.dataSource) resetLazyTree()
|
|
2370
2370
|
}
|
|
2371
2371
|
|
|
2372
|
-
// ──
|
|
2372
|
+
// ── Copy & CSV ───────────────────────────────────────────────────────
|
|
2373
2373
|
|
|
2374
2374
|
interface ExportOptions {
|
|
2375
2375
|
columnIds?: string[]
|
|
@@ -2437,13 +2437,13 @@ async function copySelectedRows(options: ExportOptions & { format?: "tsv" | "csv
|
|
|
2437
2437
|
try {
|
|
2438
2438
|
await navigator.clipboard?.writeText(text)
|
|
2439
2439
|
} catch {
|
|
2440
|
-
// Clipboard
|
|
2440
|
+
// Clipboard may be denied (permission, insecure context).
|
|
2441
2441
|
}
|
|
2442
2442
|
state.announcement = t("dataTable.rowsCopied", { count: rows.length })
|
|
2443
2443
|
return rows.length
|
|
2444
2444
|
}
|
|
2445
2445
|
|
|
2446
|
-
// ──
|
|
2446
|
+
// ── Save / load state ────────────────────────────────────────────────
|
|
2447
2447
|
|
|
2448
2448
|
function getState(): DataTableState {
|
|
2449
2449
|
return {
|
|
@@ -2498,7 +2498,7 @@ function resetState(): void {
|
|
|
2498
2498
|
}
|
|
2499
2499
|
|
|
2500
2500
|
defineExpose({
|
|
2501
|
-
//
|
|
2501
|
+
// Rows
|
|
2502
2502
|
getVisibleRows: (): R[] =>
|
|
2503
2503
|
displayed()
|
|
2504
2504
|
.filter((row) => row.kind === "data")
|
|
@@ -2506,7 +2506,7 @@ defineExpose({
|
|
|
2506
2506
|
getProcessedRows: (): R[] => dataRowsOf(),
|
|
2507
2507
|
getSelectedRows: (): R[] => resolveSelectedRows(selectedKeys.value),
|
|
2508
2508
|
scrollToIndex: virtual.scrollToIndex,
|
|
2509
|
-
//
|
|
2509
|
+
// Expanding
|
|
2510
2510
|
expandRow: (key: RowKey) => {
|
|
2511
2511
|
if (!expandedKeys.value.includes(key)) expandedKeys.value = [...expandedKeys.value, key]
|
|
2512
2512
|
},
|
|
@@ -2519,7 +2519,7 @@ defineExpose({
|
|
|
2519
2519
|
collapseAllRows: () => {
|
|
2520
2520
|
expandedKeys.value = []
|
|
2521
2521
|
},
|
|
2522
|
-
//
|
|
2522
|
+
// Columns
|
|
2523
2523
|
toggleColumn: (id: string, visible: boolean) => layout.toggleColumn(id, visible, true),
|
|
2524
2524
|
pinColumn: (id: string, side: PinSide | false) => layout.pinColumn(id, side, true),
|
|
2525
2525
|
getColumnPin: layout.getColumnPin,
|
|
@@ -2528,32 +2528,32 @@ defineExpose({
|
|
|
2528
2528
|
sizeColumnsToFit,
|
|
2529
2529
|
resetColumns,
|
|
2530
2530
|
getColumnFacets: (id: string): ColumnFacets => (state.facets as Record<string, ColumnFacets>)[id] ?? { values: [] },
|
|
2531
|
-
//
|
|
2531
|
+
// Data
|
|
2532
2532
|
reload,
|
|
2533
|
-
/**
|
|
2533
|
+
/** Reset the lazy-load tree: arrows collapsed, children cache empty (fresh on expand). */
|
|
2534
2534
|
resetTree: resetLazyTree,
|
|
2535
|
-
/**
|
|
2535
|
+
/** Discard a row's lazy-loaded children and fetch them again. */
|
|
2536
2536
|
reloadChildren: async (key: RowKey): Promise<void> => {
|
|
2537
2537
|
const row = findRowByKey(key)
|
|
2538
2538
|
if (!row) return
|
|
2539
2539
|
delete state.loadedChildren[String(key)]
|
|
2540
2540
|
await fetchChildren(row, true)
|
|
2541
2541
|
},
|
|
2542
|
-
/**
|
|
2542
|
+
/** A row's lazy-loaded children (undefined = not loaded yet). */
|
|
2543
2543
|
getLoadedChildren: (key: RowKey): R[] | undefined => state.loadedChildren[String(key)] as R[] | undefined,
|
|
2544
2544
|
getDataAsCsv,
|
|
2545
2545
|
exportDataAsCsv,
|
|
2546
2546
|
copySelectedRows,
|
|
2547
|
-
//
|
|
2547
|
+
// State
|
|
2548
2548
|
getState,
|
|
2549
2549
|
setState,
|
|
2550
2550
|
resetState,
|
|
2551
2551
|
focus: () => void nav.focusActive(),
|
|
2552
2552
|
})
|
|
2553
2553
|
|
|
2554
|
-
// ──
|
|
2554
|
+
// ── Measure, observe, react ──────────────────────────────────────────
|
|
2555
2555
|
|
|
2556
|
-
/**
|
|
2556
|
+
/** Measure header cells — the widths are the basis for the sticky offsets. */
|
|
2557
2557
|
function observeHeaderCells(): void {
|
|
2558
2558
|
const scroller = scrollerEl.value
|
|
2559
2559
|
if (!scroller || !headerObserver) return
|
|
@@ -2563,7 +2563,7 @@ function observeHeaderCells(): void {
|
|
|
2563
2563
|
}
|
|
2564
2564
|
}
|
|
2565
2565
|
|
|
2566
|
-
/** Detail
|
|
2566
|
+
/** Detail panels have free height → measure them for virtualization. */
|
|
2567
2567
|
function measureDetailRows(): void {
|
|
2568
2568
|
const body = bodyEl.value
|
|
2569
2569
|
if (!body || !detailObserver) return
|
|
@@ -2628,7 +2628,7 @@ watch(
|
|
|
2628
2628
|
() => void nextTick(measureDetailRows)
|
|
2629
2629
|
)
|
|
2630
2630
|
|
|
2631
|
-
//
|
|
2631
|
+
// Data change: clamp the page into the valid range.
|
|
2632
2632
|
watch(
|
|
2633
2633
|
() => state.filteredCount,
|
|
2634
2634
|
() => {
|
|
@@ -2638,7 +2638,7 @@ watch(
|
|
|
2638
2638
|
</script>
|
|
2639
2639
|
|
|
2640
2640
|
<style scoped>
|
|
2641
|
-
/*
|
|
2641
|
+
/* Sizes and colors mirror the CSS variables of <wa-data-grid>. */
|
|
2642
2642
|
.vi-dt {
|
|
2643
2643
|
--dt-border-color: var(--wa-color-surface-border);
|
|
2644
2644
|
--dt-border-width: var(--wa-border-width-s);
|
|
@@ -2653,11 +2653,11 @@ watch(
|
|
|
2653
2653
|
display: flex;
|
|
2654
2654
|
flex-direction: column;
|
|
2655
2655
|
min-inline-size: 0;
|
|
2656
|
-
/*
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2656
|
+
/* Containing block for absolutely positioned children — above all .vi-dt-live.
|
|
2657
|
+
Without it the live region hangs off the initial container (body) and escapes
|
|
2658
|
+
EVERY `overflow: hidden` of its ancestors: it then sits at the
|
|
2659
|
+
document position of its grid and stretches the scroll area of the whole
|
|
2660
|
+
page (several thousand pixels with eight grids stacked). */
|
|
2661
2661
|
position: relative;
|
|
2662
2662
|
color: var(--wa-color-text-normal);
|
|
2663
2663
|
font-size: var(--wa-font-size-s);
|
|
@@ -2669,7 +2669,7 @@ watch(
|
|
|
2669
2669
|
overflow: hidden;
|
|
2670
2670
|
}
|
|
2671
2671
|
|
|
2672
|
-
/*
|
|
2672
|
+
/* Sizes: row height, padding and text scale together. */
|
|
2673
2673
|
.vi-dt.is-size-xs {
|
|
2674
2674
|
--dt-row-height: 2rem;
|
|
2675
2675
|
--dt-cell-padding: var(--wa-space-xs);
|
|
@@ -2699,15 +2699,15 @@ watch(
|
|
|
2699
2699
|
min-inline-size: 14rem;
|
|
2700
2700
|
}
|
|
2701
2701
|
|
|
2702
|
-
/*
|
|
2703
|
-
|
|
2702
|
+
/* One scroll area for header, body and footer — only this way do pinned
|
|
2703
|
+
columns and the header row stay aligned when scrolling in both directions. */
|
|
2704
2704
|
.vi-dt-scroller {
|
|
2705
2705
|
position: relative;
|
|
2706
2706
|
overflow: auto;
|
|
2707
|
-
/*
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
`--dt-max-height: none
|
|
2707
|
+
/* Stretches when the table has a fixed height (e.g. in a
|
|
2708
|
+
filling view) — otherwise it stays content-sized. The cap still
|
|
2709
|
+
applies to content-sized tables; to fill, set
|
|
2710
|
+
`--dt-max-height: none` from outside. */
|
|
2711
2711
|
flex: 1 1 auto;
|
|
2712
2712
|
min-block-size: 0;
|
|
2713
2713
|
max-block-size: var(--dt-max-height, 30rem);
|
|
@@ -2717,13 +2717,13 @@ watch(
|
|
|
2717
2717
|
inset-block-start: 0;
|
|
2718
2718
|
z-index: 3;
|
|
2719
2719
|
}
|
|
2720
|
-
/*
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2720
|
+
/* The row only provides the tint as a variable and lays out the cells —
|
|
2721
|
+
painting happens in the CELLS.
|
|
2722
|
+
Reason: the row box is only as wide as the visible area, but its
|
|
2723
|
+
column tracks can extend beyond it (horizontal scrolling).
|
|
2724
|
+
A background or border on the row therefore breaks off in the middle of the grid
|
|
2725
|
+
— visible as a missing divider under pinned columns as soon as you
|
|
2726
|
+
scroll. The cells, in contrast, cover the full width. */
|
|
2727
2727
|
.vi-dt-row {
|
|
2728
2728
|
--dt-row-bg: var(--wa-color-surface-default);
|
|
2729
2729
|
display: grid;
|
|
@@ -2739,14 +2739,14 @@ watch(
|
|
|
2739
2739
|
.vi-dt.is-striped .vi-dt-row.is-odd {
|
|
2740
2740
|
--dt-row-bg: var(--dt-stripe);
|
|
2741
2741
|
}
|
|
2742
|
-
/*
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2742
|
+
/* Precedence of the row color: selection > hover > stripe > base surface.
|
|
2743
|
+
The stripe rule needs `.is-striped` on the root element and thus weighs
|
|
2744
|
+
more than a plain `.vi-dt-row.is-data:hover` — without the following
|
|
2745
|
+
prefixes neither hover nor selection would be visible on odd rows. */
|
|
2746
2746
|
.vi-dt .vi-dt-row.is-data:hover {
|
|
2747
2747
|
--dt-row-bg: var(--dt-row-hover);
|
|
2748
2748
|
}
|
|
2749
|
-
/*
|
|
2749
|
+
/* Selection via row click: the row signals that it is clickable. */
|
|
2750
2750
|
.vi-dt.is-selectable .vi-dt-row.is-data {
|
|
2751
2751
|
cursor: pointer;
|
|
2752
2752
|
}
|
|
@@ -2789,7 +2789,7 @@ watch(
|
|
|
2789
2789
|
gap: var(--wa-space-2xs);
|
|
2790
2790
|
min-inline-size: 0;
|
|
2791
2791
|
padding-inline: var(--dt-cell-padding);
|
|
2792
|
-
/*
|
|
2792
|
+
/* Each cell carries the tint and divider itself (see .vi-dt-row). */
|
|
2793
2793
|
background-color: var(--dt-row-bg);
|
|
2794
2794
|
border-block-end: var(--dt-border-width) solid var(--dt-border-color);
|
|
2795
2795
|
overflow: hidden;
|
|
@@ -2808,12 +2808,12 @@ watch(
|
|
|
2808
2808
|
outline: var(--wa-focus-ring);
|
|
2809
2809
|
outline-offset: -2px;
|
|
2810
2810
|
}
|
|
2811
|
-
/*
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
`background-color: var(--dt-row-bg)`
|
|
2815
|
-
|
|
2816
|
-
|
|
2811
|
+
/* Pinned cells lie ABOVE the horizontally scrolling content and must hide it
|
|
2812
|
+
opaquely. The row tints (stripe, hover, selection) are
|
|
2813
|
+
semi-transparent (color-mix with transparent), so a plain
|
|
2814
|
+
`background-color: var(--dt-row-bg)` would let the content show through.
|
|
2815
|
+
Hence: an opaque base surface as the color, the row tint as an overlay — the
|
|
2816
|
+
result is the same color value as for the freely scrolling cells. */
|
|
2817
2817
|
.vi-dt-cell.is-sticky {
|
|
2818
2818
|
background-color: var(--wa-color-surface-default);
|
|
2819
2819
|
background-image: linear-gradient(var(--dt-row-bg), var(--dt-row-bg));
|
|
@@ -2838,8 +2838,8 @@ watch(
|
|
|
2838
2838
|
.vi-dt-hcell {
|
|
2839
2839
|
padding-inline: var(--wa-space-s);
|
|
2840
2840
|
}
|
|
2841
|
-
/*
|
|
2842
|
-
|
|
2841
|
+
/* The label fills the free space — otherwise the filter and menu buttons
|
|
2842
|
+
slide into the middle of the cell and intercept the sort click. */
|
|
2843
2843
|
.vi-dt-hcell .vi-dt-hlabel {
|
|
2844
2844
|
flex: 1 1 auto;
|
|
2845
2845
|
min-inline-size: 0;
|
|
@@ -2851,16 +2851,16 @@ watch(
|
|
|
2851
2851
|
.vi-dt-hcell.is-align-center .vi-dt-hlabel {
|
|
2852
2852
|
text-align: center;
|
|
2853
2853
|
}
|
|
2854
|
-
/*
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
/* visibility
|
|
2858
|
-
(
|
|
2859
|
-
|
|
2860
|
-
-
|
|
2861
|
-
|
|
2862
|
-
-
|
|
2863
|
-
|
|
2854
|
+
/* Only show the column menu when the column is focused/hovered — otherwise it eats
|
|
2855
|
+
the label. (wa-dropdown itself is display:contents, so style the trigger.)
|
|
2856
|
+
Active filters always stay visible. */
|
|
2857
|
+
/* visibility instead of display: the button is invisible and NOT clickable
|
|
2858
|
+
(so it doesn't intercept the sort click in the middle of the cell), but keeps its
|
|
2859
|
+
place in the layout. Both matter:
|
|
2860
|
+
- With display:none the button leaves the layout. If its menu is open, the
|
|
2861
|
+
popup suddenly has no anchor anymore and jumps to the top left corner.
|
|
2862
|
+
- Also, the header row would reflow on every hover change and shift open
|
|
2863
|
+
filter popovers by the button width. */
|
|
2864
2864
|
.vi-dt-column-menu-button {
|
|
2865
2865
|
visibility: hidden;
|
|
2866
2866
|
}
|
|
@@ -2925,24 +2925,24 @@ watch(
|
|
|
2925
2925
|
position: relative;
|
|
2926
2926
|
}
|
|
2927
2927
|
|
|
2928
|
-
/* ──
|
|
2929
|
-
/*
|
|
2930
|
-
|
|
2931
|
-
|
|
2928
|
+
/* ── Making the tree structure visible ─────────────────────────────── */
|
|
2929
|
+
/* Indentation alone reads poorly. Hence one vertical guide line per level
|
|
2930
|
+
(as a background pattern over the indented area) and a
|
|
2931
|
+
short corner to the row itself — like in a file tree. */
|
|
2932
2932
|
.vi-dt {
|
|
2933
2933
|
--dt-guide: var(--wa-color-surface-border);
|
|
2934
2934
|
}
|
|
2935
2935
|
.vi-dt-cell.is-tree-cell {
|
|
2936
2936
|
position: relative;
|
|
2937
2937
|
}
|
|
2938
|
-
/*
|
|
2939
|
-
|
|
2940
|
-
is-line →
|
|
2941
|
-
is-tee →
|
|
2942
|
-
is-corner →
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2938
|
+
/* One slot per level. The vertical line sits in the MIDDLE of the slot so
|
|
2939
|
+
it runs through the expand arrows of the respective level.
|
|
2940
|
+
is-line → continuous line (ancestor still has siblings)
|
|
2941
|
+
is-tee → "├" (this row still has siblings)
|
|
2942
|
+
is-corner → "└" (last child: empty below)
|
|
2943
|
+
none → empty slot (the subtree ends there)
|
|
2944
|
+
The arm ends at the edge of the slot — it does NOT run past the arrow or the
|
|
2945
|
+
leaf placeholder up to the text. */
|
|
2946
2946
|
.vi-dt-guide {
|
|
2947
2947
|
position: relative;
|
|
2948
2948
|
flex: 0 0 auto;
|
|
@@ -2959,11 +2959,11 @@ watch(
|
|
|
2959
2959
|
inline-size: 1px;
|
|
2960
2960
|
background-color: var(--dt-guide);
|
|
2961
2961
|
}
|
|
2962
|
-
/*
|
|
2962
|
+
/* Corner: the vertical line ends at half height. */
|
|
2963
2963
|
.vi-dt-guide.is-corner::before {
|
|
2964
2964
|
inset-block: 0 50%;
|
|
2965
2965
|
}
|
|
2966
|
-
/*
|
|
2966
|
+
/* Short arm from the middle to the edge of the slot. */
|
|
2967
2967
|
.vi-dt-guide.is-tee::after,
|
|
2968
2968
|
.vi-dt-guide.is-corner::after {
|
|
2969
2969
|
content: "";
|
|
@@ -2974,15 +2974,15 @@ watch(
|
|
|
2974
2974
|
background-color: var(--dt-guide);
|
|
2975
2975
|
}
|
|
2976
2976
|
|
|
2977
|
-
/*
|
|
2978
|
-
|
|
2979
|
-
|
|
2977
|
+
/* Arrow and leaf placeholder MUST be equally wide — otherwise the labels of a
|
|
2978
|
+
level don't line up and the indentation gets
|
|
2979
|
+
partially eaten up (the wa-button is wider by itself). */
|
|
2980
2980
|
.vi-dt-expand {
|
|
2981
2981
|
flex: 0 0 auto;
|
|
2982
2982
|
inline-size: var(--dt-toggle-size);
|
|
2983
2983
|
}
|
|
2984
|
-
/*
|
|
2985
|
-
|
|
2984
|
+
/* Loaded and empty: the arrow stays in place (no layout jump),
|
|
2985
|
+
but is visibly out of service. */
|
|
2986
2986
|
.vi-dt-expand.is-empty {
|
|
2987
2987
|
opacity: 0.3;
|
|
2988
2988
|
cursor: default;
|
|
@@ -2991,13 +2991,13 @@ watch(
|
|
|
2991
2991
|
font-size: 0.75em;
|
|
2992
2992
|
--track-width: 2px;
|
|
2993
2993
|
}
|
|
2994
|
-
/*
|
|
2994
|
+
/* Leaves: same spacing as between arrow and guide lines. */
|
|
2995
2995
|
.vi-dt-leaf-gap {
|
|
2996
2996
|
flex: 0 0 auto;
|
|
2997
2997
|
inline-size: var(--wa-space-2xs);
|
|
2998
2998
|
}
|
|
2999
2999
|
|
|
3000
|
-
/* ──
|
|
3000
|
+
/* ── Fixed order: handle and insertion marker ──────────────────────── */
|
|
3001
3001
|
.vi-dt-grip {
|
|
3002
3002
|
display: inline-flex;
|
|
3003
3003
|
align-items: center;
|
|
@@ -3018,16 +3018,16 @@ watch(
|
|
|
3018
3018
|
.vi-dt-row.is-dragging {
|
|
3019
3019
|
opacity: 0.5;
|
|
3020
3020
|
}
|
|
3021
|
-
/*
|
|
3022
|
-
|
|
3021
|
+
/* Marker where the row would land. As an inset shadow so it costs no
|
|
3022
|
+
layout height and rows don't jump while dragging. */
|
|
3023
3023
|
.vi-dt-row.is-drop-before .vi-dt-cell {
|
|
3024
3024
|
box-shadow: inset 0 2px 0 var(--dt-accent);
|
|
3025
3025
|
}
|
|
3026
3026
|
.vi-dt-row.is-drop-after .vi-dt-cell {
|
|
3027
3027
|
box-shadow: inset 0 -2px 0 var(--dt-accent);
|
|
3028
3028
|
}
|
|
3029
|
-
/*
|
|
3030
|
-
|
|
3029
|
+
/* "Inside": the row becomes the parent — hence a frame around the whole row
|
|
3030
|
+
instead of a line at the edge. */
|
|
3031
3031
|
.vi-dt-row.is-drop-inside .vi-dt-cell {
|
|
3032
3032
|
box-shadow:
|
|
3033
3033
|
inset 0 2px 0 var(--dt-accent),
|
|
@@ -3037,8 +3037,8 @@ watch(
|
|
|
3037
3037
|
--dt-row-bg: color-mix(in oklab, var(--dt-selected) 60%, transparent);
|
|
3038
3038
|
}
|
|
3039
3039
|
|
|
3040
|
-
/*
|
|
3041
|
-
|
|
3040
|
+
/* The expand column sits left of the tree cell — its line continues
|
|
3041
|
+
so arrow and structure connect. */
|
|
3042
3042
|
.vi-dt-cell.is-lead.is-expand-cell {
|
|
3043
3043
|
position: relative;
|
|
3044
3044
|
}
|
|
@@ -3141,8 +3141,8 @@ watch(
|
|
|
3141
3141
|
font-size: var(--wa-font-size-s);
|
|
3142
3142
|
cursor: pointer;
|
|
3143
3143
|
}
|
|
3144
|
-
/*
|
|
3145
|
-
(position: relative)
|
|
3144
|
+
/* Screen readers only: invisible but present. The containing block is .vi-dt
|
|
3145
|
+
(position: relative) so the region stays clipped within the grid. */
|
|
3146
3146
|
.vi-dt-live {
|
|
3147
3147
|
position: absolute;
|
|
3148
3148
|
inset-block-start: 0;
|