@pgcorp/ui-kit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/docs/public-api.md +8 -0
- package/package.json +1 -1
- package/src/components/layout/SAppShell.vue +3 -2
- package/src/components/shared/complex/STree.vue +29 -4
- package/src/components/shared/complex/treeAdapter.ts +2 -1
- package/src/components/shared/complex/types.ts +1 -12
- package/src/components/shared/containers/SLeftSidebar.vue +7 -2
- package/src/components/shared/containers/SPageHeader.css +5 -0
- package/src/components/shared/containers/SPageHeader.vue +1 -1
- package/src/components/shared/containers/SPopover.vue +2 -1
- package/src/components/shared/containers/SSidebarGroup.vue +10 -5
- package/src/components/shared/containers/sidebar.ts +2 -1
- package/src/components/shared/controls/SButton.vue +10 -5
- package/src/components/shared/controls/SCombobox.vue +47 -5
- package/src/components/shared/controls/SInteractiveSurface.vue +2 -1
- package/src/components/shared/controls/SLink.ts +1 -17
- package/src/components/shared/controls/SLink.vue +26 -2
- package/src/components/shared/data-display/SDocBlock.vue +10 -10
- package/src/components/shared/data-display/SLinkedSystemsList.vue +3 -3
- package/src/components/shared/data-display/STable.css +39 -0
- package/src/components/shared/data-display/STable.vue +454 -67
- package/src/components/shared/data-display/STooltip.vue +2 -1
- package/src/components/shared/data-display/SVirtualList.ts +1 -16
- package/src/components/shared/data-display/SVirtualList.vue +18 -2
- package/src/components/shared/data-display/json.ts +1 -1
- package/src/components/shared/data-display/table.ts +4 -29
- package/src/components/shared/database/SDataGrid.vue +29 -8
- package/src/components/shared/database/SSqlEditor.vue +3 -2
- package/src/components/shared/database/dataGrid.ts +1 -17
- package/src/components/shared/navigation/STabs.vue +2 -1
- package/src/composables/uiPreferencesReset.ts +2 -1
- package/src/composables/useClipboard.ts +2 -1
- package/src/composables/useSidebarPanelState.ts +3 -2
- package/src/internal/es2020.ts +50 -0
- package/src/internal/inlineTokenEditorContract.ts +4 -3
- package/src/internal/pointerInteractionLease.ts +2 -1
- package/src/internal/semanticSizing.ts +2 -2
- package/src/styles/tokens.css +1 -0
|
@@ -16,6 +16,28 @@
|
|
|
16
16
|
>
|
|
17
17
|
<thead class="s-table-header">
|
|
18
18
|
<tr>
|
|
19
|
+
<th
|
|
20
|
+
v-if="validatedSelectionMode === 'multiple'"
|
|
21
|
+
class="s-table__control-cell"
|
|
22
|
+
scope="col"
|
|
23
|
+
data-width="2xs"
|
|
24
|
+
>
|
|
25
|
+
<SCheckbox
|
|
26
|
+
:model-value="allCurrentRowsSelected"
|
|
27
|
+
:indeterminate="someCurrentRowsSelected && !allCurrentRowsSelected"
|
|
28
|
+
:disabled="selectableRows.length === 0"
|
|
29
|
+
:aria-label="allCurrentRowsSelected ? 'Снять выбор со всех строк' : 'Выбрать все строки'"
|
|
30
|
+
@update:model-value="updateAllRowsSelection"
|
|
31
|
+
/>
|
|
32
|
+
</th>
|
|
33
|
+
<th
|
|
34
|
+
v-if="hasRowDetails"
|
|
35
|
+
class="s-table__control-cell"
|
|
36
|
+
scope="col"
|
|
37
|
+
data-width="2xs"
|
|
38
|
+
>
|
|
39
|
+
<span class="s-table__assistive">Детали строки</span>
|
|
40
|
+
</th>
|
|
19
41
|
<th
|
|
20
42
|
v-for="column in validatedColumns"
|
|
21
43
|
:key="column.field"
|
|
@@ -38,15 +60,24 @@
|
|
|
38
60
|
:trailing-icon="sortIcon(column)"
|
|
39
61
|
@click="requestSort(column)"
|
|
40
62
|
>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
<span
|
|
64
|
+
class="s-table__header-content"
|
|
65
|
+
:data-header-presentation="column.headerPresentation ?? 'visible'"
|
|
66
|
+
>
|
|
67
|
+
<slot name="column" :column="column">
|
|
68
|
+
<slot :name="`header-${column.field}`" :column="column">{{ column.header }}</slot>
|
|
69
|
+
</slot>
|
|
70
|
+
</span>
|
|
44
71
|
</SButton>
|
|
45
|
-
<
|
|
72
|
+
<span
|
|
73
|
+
v-else
|
|
74
|
+
class="s-table__header-content"
|
|
75
|
+
:data-header-presentation="column.headerPresentation ?? 'visible'"
|
|
76
|
+
>
|
|
46
77
|
<slot name="column" :column="column">
|
|
47
78
|
<slot :name="`header-${column.field}`" :column="column">{{ column.header }}</slot>
|
|
48
79
|
</slot>
|
|
49
|
-
</
|
|
80
|
+
</span>
|
|
50
81
|
</th>
|
|
51
82
|
</tr>
|
|
52
83
|
</thead>
|
|
@@ -59,44 +90,90 @@
|
|
|
59
90
|
? { status: 'error', message: errorMessage }
|
|
60
91
|
: { status: 'empty', message: emptyMessage }"
|
|
61
92
|
presentation="table-row"
|
|
62
|
-
:colspan="
|
|
93
|
+
:colspan="renderedColumnCount"
|
|
63
94
|
/>
|
|
64
95
|
<template v-else>
|
|
65
|
-
<
|
|
96
|
+
<template
|
|
66
97
|
v-for="(row, rowIndex) in validatedRows"
|
|
67
|
-
:key="
|
|
68
|
-
:data-state="resolveRowState(row)"
|
|
69
|
-
:aria-selected="resolvedRowKeys.get(row) === validatedSelectedRowKey ? 'true' : undefined"
|
|
70
|
-
tabindex="0"
|
|
71
|
-
@click="onRowClick($event, row, rowIndex)"
|
|
72
|
-
@keydown.enter="onRowKeydown($event, row, rowIndex)"
|
|
73
|
-
@keydown.space.prevent="onRowKeydown($event, row, rowIndex)"
|
|
98
|
+
:key="rowDomToken(row)"
|
|
74
99
|
>
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
:
|
|
78
|
-
:
|
|
79
|
-
|
|
100
|
+
<tr
|
|
101
|
+
:data-state="resolveRowState(row)"
|
|
102
|
+
:data-selected="rowIsSelected(row) || undefined"
|
|
103
|
+
:aria-selected="rowAriaSelected(row)"
|
|
104
|
+
tabindex="0"
|
|
105
|
+
@click="onRowClick($event, row, rowIndex)"
|
|
106
|
+
@contextmenu="onRowContextMenu($event, row, rowIndex)"
|
|
107
|
+
@keydown.enter="onRowKeydown($event, row, rowIndex)"
|
|
108
|
+
@keydown.space="onRowKeydown($event, row, rowIndex)"
|
|
80
109
|
>
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
<td
|
|
111
|
+
v-if="validatedSelectionMode === 'multiple'"
|
|
112
|
+
class="s-table__control-cell"
|
|
113
|
+
>
|
|
114
|
+
<SCheckbox
|
|
115
|
+
:model-value="rowIsSelected(row)"
|
|
116
|
+
:disabled="!rowIsSelectable(row)"
|
|
117
|
+
:aria-label="rowSelectionLabel(row, rowIndex)"
|
|
118
|
+
@update:model-value="updateRowSelection(row, rowIndex, $event)"
|
|
119
|
+
/>
|
|
120
|
+
</td>
|
|
121
|
+
<td v-if="hasRowDetails" class="s-table__control-cell">
|
|
122
|
+
<SButton
|
|
123
|
+
v-if="rowIsExpandable(row)"
|
|
124
|
+
:aria-label="rowExpansionLabel(row, rowIndex)"
|
|
125
|
+
appearance="text"
|
|
126
|
+
severity="secondary"
|
|
127
|
+
size="xs"
|
|
128
|
+
density="compact"
|
|
129
|
+
shape="square"
|
|
130
|
+
icon-only
|
|
131
|
+
:disclosure="{
|
|
132
|
+
expanded: rowIsExpanded(row),
|
|
133
|
+
controls: rowDetailsId(row),
|
|
134
|
+
iconPlacement: 'leading',
|
|
135
|
+
}"
|
|
136
|
+
@click="toggleRowExpansion(row, rowIndex)"
|
|
137
|
+
/>
|
|
138
|
+
</td>
|
|
139
|
+
<td
|
|
140
|
+
v-for="column in validatedColumns"
|
|
141
|
+
:key="column.field"
|
|
142
|
+
:data-align="column.align ?? 'left'"
|
|
143
|
+
:data-state="resolveCellState(row, column)"
|
|
87
144
|
>
|
|
88
145
|
<slot
|
|
89
|
-
name="cell"
|
|
146
|
+
:name="`cell-${column.field}`"
|
|
90
147
|
:value="resolveCellValue(row, column)"
|
|
91
148
|
:row="row"
|
|
92
149
|
:column="column"
|
|
93
150
|
:row-index="rowIndex"
|
|
94
151
|
>
|
|
95
|
-
|
|
152
|
+
<slot
|
|
153
|
+
name="cell"
|
|
154
|
+
:value="resolveCellValue(row, column)"
|
|
155
|
+
:row="row"
|
|
156
|
+
:column="column"
|
|
157
|
+
:row-index="rowIndex"
|
|
158
|
+
>
|
|
159
|
+
{{ resolveCellValue(row, column) }}
|
|
160
|
+
</slot>
|
|
96
161
|
</slot>
|
|
97
|
-
</
|
|
98
|
-
</
|
|
99
|
-
|
|
162
|
+
</td>
|
|
163
|
+
</tr>
|
|
164
|
+
<tr v-if="rowIsExpandable(row) && rowIsExpanded(row)" data-row-details>
|
|
165
|
+
<td :colspan="renderedColumnCount">
|
|
166
|
+
<div
|
|
167
|
+
:id="rowDetailsId(row)"
|
|
168
|
+
class="s-table__row-details"
|
|
169
|
+
role="region"
|
|
170
|
+
:aria-label="`Детали: ${resolveRowLabel(row, rowIndex)}`"
|
|
171
|
+
>
|
|
172
|
+
<slot name="row-details" :row="row" :row-index="rowIndex" />
|
|
173
|
+
</div>
|
|
174
|
+
</td>
|
|
175
|
+
</tr>
|
|
176
|
+
</template>
|
|
100
177
|
</template>
|
|
101
178
|
</tbody>
|
|
102
179
|
</table>
|
|
@@ -104,25 +181,77 @@
|
|
|
104
181
|
</template>
|
|
105
182
|
|
|
106
183
|
<script lang="ts">
|
|
184
|
+
import type { ScrollableViewportMaxBlockSize } from '../../../internal/semanticSizing'
|
|
185
|
+
import type {
|
|
186
|
+
STableCellState,
|
|
187
|
+
STableColumn,
|
|
188
|
+
STableColumnContent,
|
|
189
|
+
STableColumnHeaderPresentation,
|
|
190
|
+
STableDensity,
|
|
191
|
+
STableMinWidth,
|
|
192
|
+
STableRowKey,
|
|
193
|
+
STableRowState,
|
|
194
|
+
STableSelectionMode,
|
|
195
|
+
STableSort,
|
|
196
|
+
STableSurface,
|
|
197
|
+
} from './table'
|
|
198
|
+
|
|
107
199
|
export type {
|
|
108
200
|
STableAlignment,
|
|
109
201
|
STableCellState,
|
|
110
202
|
STableColumn,
|
|
111
203
|
STableColumnContent,
|
|
204
|
+
STableColumnHeaderPresentation,
|
|
112
205
|
STableDensity,
|
|
113
206
|
STableMinWidth,
|
|
114
|
-
STableProps,
|
|
115
207
|
STableRowKey,
|
|
116
208
|
STableRowState,
|
|
209
|
+
STableSelectionMode,
|
|
117
210
|
STableSort,
|
|
118
211
|
STableSortDirection,
|
|
119
212
|
STableSurface,
|
|
120
213
|
} from './table'
|
|
214
|
+
|
|
215
|
+
/** Публичный generic-контракт таблицы принадлежит source-SFC runtime. / The public generic table contract is owned by the source-SFC runtime. */
|
|
216
|
+
export interface STableProps<
|
|
217
|
+
Row extends object,
|
|
218
|
+
RowKey extends STableRowKey,
|
|
219
|
+
Field extends string,
|
|
220
|
+
> {
|
|
221
|
+
columns: readonly STableColumn<Row, Field>[]
|
|
222
|
+
data: readonly Row[]
|
|
223
|
+
getRowKey: (row: Row) => RowKey
|
|
224
|
+
loading?: boolean
|
|
225
|
+
loadingMessage?: string
|
|
226
|
+
errorMessage?: string
|
|
227
|
+
emptyMessage?: string
|
|
228
|
+
selectedRowKey?: RowKey
|
|
229
|
+
selectionMode?: STableSelectionMode
|
|
230
|
+
selectedRowKeys?: readonly RowKey[]
|
|
231
|
+
isRowSelectable?: (row: Row, index: number) => boolean
|
|
232
|
+
expandedRowKeys?: readonly RowKey[]
|
|
233
|
+
isRowExpandable?: (row: Row, index: number) => boolean
|
|
234
|
+
getRowLabel?: (row: Row, index: number) => string
|
|
235
|
+
sort?: STableSort<Field> | null
|
|
236
|
+
maxHeight?: ScrollableViewportMaxBlockSize
|
|
237
|
+
density?: STableDensity
|
|
238
|
+
surface?: STableSurface
|
|
239
|
+
minWidth?: STableMinWidth
|
|
240
|
+
rowState?: (row: Row) => STableRowState
|
|
241
|
+
cellState?: (row: Row, column: STableColumn<Row, Field>) => STableCellState
|
|
242
|
+
/** Accessible name без visible owner. / Accessible name when there is no visible owner. */
|
|
243
|
+
ariaLabel?: string
|
|
244
|
+
/** DOM id visible owner, mutually exclusive with ariaLabel. / DOM id of the visible owner, mutually exclusive with ariaLabel. */
|
|
245
|
+
ariaLabelledby?: string
|
|
246
|
+
/** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
|
|
247
|
+
ariaDescribedby?: string
|
|
248
|
+
}
|
|
121
249
|
</script>
|
|
122
250
|
|
|
123
251
|
<script setup lang="ts" generic="Row extends object = Record<string, unknown>, RowKey extends STableRowKey = STableRowKey, Field extends string = Extract<keyof Row, string>">
|
|
124
|
-
import { computed, useSlots } from 'vue'
|
|
252
|
+
import { computed, useId, useSlots } from 'vue'
|
|
125
253
|
import { ChevronDown, ChevronsUpDown, ChevronUp } from '../../icons/sputnigUiIcons'
|
|
254
|
+
import { hasOwn } from '../../../internal/es2020'
|
|
126
255
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
127
256
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
128
257
|
import {
|
|
@@ -138,16 +267,7 @@ import {
|
|
|
138
267
|
} from '../../../internal/runtimeContract'
|
|
139
268
|
import SAsyncState from '../feedback/SAsyncState.vue'
|
|
140
269
|
import SButton from '../controls/SButton.vue'
|
|
141
|
-
import
|
|
142
|
-
STableCellState,
|
|
143
|
-
STableColumn,
|
|
144
|
-
STableColumnContent,
|
|
145
|
-
STableProps,
|
|
146
|
-
STableRowKey,
|
|
147
|
-
STableRowState,
|
|
148
|
-
STableSort,
|
|
149
|
-
} from './table'
|
|
150
|
-
|
|
270
|
+
import SCheckbox from '../controls/SCheckbox.vue'
|
|
151
271
|
defineOptions({ inheritAttrs: false })
|
|
152
272
|
|
|
153
273
|
const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
@@ -156,6 +276,12 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
|
156
276
|
errorMessage: '',
|
|
157
277
|
emptyMessage: 'Нет данных для отображения',
|
|
158
278
|
selectedRowKey: undefined,
|
|
279
|
+
selectionMode: 'none',
|
|
280
|
+
selectedRowKeys: () => [],
|
|
281
|
+
isRowSelectable: undefined,
|
|
282
|
+
expandedRowKeys: () => [],
|
|
283
|
+
isRowExpandable: undefined,
|
|
284
|
+
getRowLabel: undefined,
|
|
159
285
|
sort: null,
|
|
160
286
|
maxHeight: 'none',
|
|
161
287
|
density: 'default',
|
|
@@ -169,6 +295,10 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
|
169
295
|
})
|
|
170
296
|
const emit = defineEmits<{
|
|
171
297
|
'row-click': [row: Row, index: number]
|
|
298
|
+
'row-contextmenu': [row: Row, index: number, event: MouseEvent]
|
|
299
|
+
'update:selectedRowKeys': [value: readonly RowKey[]]
|
|
300
|
+
'update:expandedRowKeys': [value: readonly RowKey[]]
|
|
301
|
+
'row-expand': [row: Row, index: number, expanded: boolean]
|
|
172
302
|
'update:sort': [value: STableSort<Field> | null]
|
|
173
303
|
sort: [value: STableSort<Field> | null]
|
|
174
304
|
}>()
|
|
@@ -181,9 +311,13 @@ const TABLE_ALIGNMENTS = ['left', 'center', 'right'] as const
|
|
|
181
311
|
const TABLE_ROW_STATES = ['default', 'inserted', 'updated', 'deleted'] as const
|
|
182
312
|
const TABLE_CELL_STATES = ['default', 'pending'] as const
|
|
183
313
|
const TABLE_COLUMN_CONTENT = ['field', 'value', 'slot'] as const
|
|
314
|
+
const TABLE_HEADER_PRESENTATIONS = ['visible', 'assistive'] as const
|
|
315
|
+
const TABLE_SELECTION_MODES = ['none', 'multiple'] as const
|
|
184
316
|
|
|
185
317
|
const ownedAttrs = useOwnedAttrs({ component: 'STable', owner: 'table scroll surface' })
|
|
186
318
|
useInteractiveLeafRegistration({ owner: 'STable' })
|
|
319
|
+
const tableInstanceId = `s-table-${useId()}`
|
|
320
|
+
const hasRowDetails = typeof slots['row-details'] === 'function'
|
|
187
321
|
|
|
188
322
|
function eventBelongsToNestedControl(event: Event): boolean {
|
|
189
323
|
const owner = event.currentTarget
|
|
@@ -196,8 +330,14 @@ function onRowClick(event: MouseEvent, row: Row, rowIndex: number): void {
|
|
|
196
330
|
if (!eventBelongsToNestedControl(event)) emit('row-click', row, rowIndex)
|
|
197
331
|
}
|
|
198
332
|
|
|
333
|
+
function onRowContextMenu(event: MouseEvent, row: Row, rowIndex: number): void {
|
|
334
|
+
if (!eventBelongsToNestedControl(event)) emit('row-contextmenu', row, rowIndex, event)
|
|
335
|
+
}
|
|
336
|
+
|
|
199
337
|
function onRowKeydown(event: KeyboardEvent, row: Row, rowIndex: number): void {
|
|
200
|
-
if (event.target
|
|
338
|
+
if (event.target !== event.currentTarget) return
|
|
339
|
+
event.preventDefault()
|
|
340
|
+
emit('row-click', row, rowIndex)
|
|
201
341
|
}
|
|
202
342
|
const validatedLoading = computed(() => validateBoolean('STable', 'loading', props.loading))
|
|
203
343
|
const resolvedAccessibleName = computed(() => {
|
|
@@ -243,12 +383,24 @@ const validatedMaxHeight = computed(() => validateScrollableViewportMaxBlockSize
|
|
|
243
383
|
'maxHeight',
|
|
244
384
|
props.maxHeight,
|
|
245
385
|
))
|
|
386
|
+
const validatedSelectionMode = computed(() => validateExactString(
|
|
387
|
+
'STable',
|
|
388
|
+
'selectionMode',
|
|
389
|
+
props.selectionMode,
|
|
390
|
+
TABLE_SELECTION_MODES,
|
|
391
|
+
))
|
|
246
392
|
|
|
247
393
|
const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
248
394
|
const fields = new Set<string>()
|
|
249
395
|
for (const column of props.columns) {
|
|
250
396
|
const field = validateNonEmptyString('STable', 'columns[].field', column.field)
|
|
251
397
|
validateNonEmptyString('STable', `column ${JSON.stringify(field)}.header`, column.header)
|
|
398
|
+
const headerPresentation: STableColumnHeaderPresentation = validateExactString(
|
|
399
|
+
'STable',
|
|
400
|
+
`column ${JSON.stringify(field)}.headerPresentation`,
|
|
401
|
+
column.headerPresentation ?? 'visible',
|
|
402
|
+
TABLE_HEADER_PRESENTATIONS,
|
|
403
|
+
)
|
|
252
404
|
if (fields.has(field)) {
|
|
253
405
|
throw new TypeError(
|
|
254
406
|
`STable: columns содержит duplicate field ${JSON.stringify(field)}. `
|
|
@@ -262,6 +414,12 @@ const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
|
262
414
|
if (column.sortable !== undefined) {
|
|
263
415
|
validateBoolean('STable', `column ${JSON.stringify(field)}.sortable`, column.sortable)
|
|
264
416
|
}
|
|
417
|
+
if (headerPresentation === 'assistive' && column.sortable) {
|
|
418
|
+
throw new TypeError(
|
|
419
|
+
`STable: assistive header column ${JSON.stringify(field)} не может быть sortable без visible action. `
|
|
420
|
+
+ `/ STable: assistive header column ${JSON.stringify(field)} cannot be sortable without a visible action.`,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
265
423
|
if (column.width !== undefined) {
|
|
266
424
|
validateTableColumnInlineSize('STable', `column ${JSON.stringify(field)}.width`, column.width)
|
|
267
425
|
}
|
|
@@ -348,51 +506,182 @@ function describeRowKey(key: STableRowKey): string {
|
|
|
348
506
|
return typeof key === 'string' ? JSON.stringify(key) : String(key)
|
|
349
507
|
}
|
|
350
508
|
|
|
351
|
-
function
|
|
509
|
+
function diagnosticOwner(): string {
|
|
510
|
+
return resolvedAccessibleName.value.ariaLabel === undefined
|
|
511
|
+
? `STable [aria-labelledby=${JSON.stringify(resolvedAccessibleName.value.ariaLabelledby)}]`
|
|
512
|
+
: `STable ${JSON.stringify(resolvedAccessibleName.value.ariaLabel)}`
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function describeThrownError(error: unknown): string {
|
|
516
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function validateRowKey(value: unknown, coordinate: string): RowKey {
|
|
352
520
|
if (
|
|
353
521
|
(typeof value === 'string' && value.trim().length > 0)
|
|
354
522
|
|| (typeof value === 'number' && Number.isFinite(value))
|
|
355
523
|
) return value as RowKey
|
|
356
524
|
|
|
357
525
|
throw new TypeError(
|
|
358
|
-
|
|
359
|
-
+
|
|
526
|
+
`${diagnosticOwner()}: ${coordinate} должен вернуть non-empty string | finite number. `
|
|
527
|
+
+ `/ ${diagnosticOwner()}: ${coordinate} must return a non-empty string | finite number.`,
|
|
360
528
|
)
|
|
361
529
|
}
|
|
362
530
|
|
|
363
531
|
const resolvedRowKeys = computed<ReadonlyMap<Row, RowKey>>(() => {
|
|
364
532
|
if (typeof props.getRowKey !== 'function') {
|
|
365
533
|
throw new TypeError(
|
|
366
|
-
|
|
367
|
-
+
|
|
534
|
+
`${diagnosticOwner()}: getRowKey должен быть обязательным function resolver. `
|
|
535
|
+
+ `/ ${diagnosticOwner()}: getRowKey must be a required function resolver.`,
|
|
368
536
|
)
|
|
369
537
|
}
|
|
370
538
|
|
|
371
|
-
const
|
|
539
|
+
const firstIndexByKey = new Map<STableRowKey, number>()
|
|
372
540
|
const rowKeys = new Map<Row, RowKey>()
|
|
373
|
-
for (const row of props.data) {
|
|
374
|
-
|
|
375
|
-
|
|
541
|
+
for (const [rowIndex, row] of props.data.entries()) {
|
|
542
|
+
let value: unknown
|
|
543
|
+
try {
|
|
544
|
+
value = props.getRowKey(row)
|
|
545
|
+
} catch (error: unknown) {
|
|
376
546
|
throw new TypeError(
|
|
377
|
-
|
|
378
|
-
+ `/ STable: getRowKey returned duplicate key ${describeRowKey(key)}.`,
|
|
547
|
+
`${diagnosticOwner()}: getRowKey failed at data[${rowIndex}]: ${describeThrownError(error)}.`,
|
|
379
548
|
)
|
|
380
549
|
}
|
|
381
|
-
|
|
550
|
+
const key = validateRowKey(value, `getRowKey at data[${rowIndex}]`)
|
|
551
|
+
const firstIndex = firstIndexByKey.get(key)
|
|
552
|
+
if (firstIndex !== undefined) {
|
|
553
|
+
throw new TypeError(
|
|
554
|
+
`${diagnosticOwner()}: getRowKey вернул duplicate key ${describeRowKey(key)} at data[${rowIndex}]; `
|
|
555
|
+
+ `first returned at data[${firstIndex}]. / ${diagnosticOwner()}: getRowKey returned duplicate key `
|
|
556
|
+
+ `${describeRowKey(key)} at data[${rowIndex}]; first returned at data[${firstIndex}].`,
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
firstIndexByKey.set(key, rowIndex)
|
|
382
560
|
rowKeys.set(row, key)
|
|
383
561
|
}
|
|
384
562
|
return rowKeys
|
|
385
563
|
})
|
|
386
564
|
|
|
565
|
+
function validateControlledKeys(coordinate: string, value: unknown): readonly RowKey[] {
|
|
566
|
+
if (!Array.isArray(value)) {
|
|
567
|
+
throw new TypeError(
|
|
568
|
+
`${diagnosticOwner()}: ${coordinate} должен быть readonly array. `
|
|
569
|
+
+ `/ ${diagnosticOwner()}: ${coordinate} must be a readonly array.`,
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
const validated: RowKey[] = []
|
|
573
|
+
const seen = new Set<STableRowKey>()
|
|
574
|
+
for (const [index, candidate] of value.entries()) {
|
|
575
|
+
const key = validateRowKey(candidate, `${coordinate}[${index}]`)
|
|
576
|
+
if (seen.has(key)) {
|
|
577
|
+
throw new TypeError(
|
|
578
|
+
`${diagnosticOwner()}: ${coordinate} содержит duplicate key ${describeRowKey(key)}. `
|
|
579
|
+
+ `/ ${diagnosticOwner()}: ${coordinate} contains duplicate key ${describeRowKey(key)}.`,
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
seen.add(key)
|
|
583
|
+
validated.push(key)
|
|
584
|
+
}
|
|
585
|
+
return validated
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const validatedSelectedRowKey = computed<RowKey | undefined>(() => {
|
|
589
|
+
if (props.selectedRowKey === undefined) return undefined
|
|
590
|
+
if (validatedSelectionMode.value === 'multiple') {
|
|
591
|
+
throw new TypeError(
|
|
592
|
+
`${diagnosticOwner()}: selectedRowKey несовместим с selectionMode="multiple"; используйте selectedRowKeys. `
|
|
593
|
+
+ `/ ${diagnosticOwner()}: selectedRowKey is incompatible with selectionMode="multiple"; use selectedRowKeys.`,
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
const selectedKey = validateRowKey(props.selectedRowKey, 'selectedRowKey')
|
|
597
|
+
const rowKeys = new Set(resolvedRowKeys.value.values())
|
|
598
|
+
if (!rowKeys.has(selectedKey)) {
|
|
599
|
+
throw new TypeError(
|
|
600
|
+
`${diagnosticOwner()}: selectedRowKey ${describeRowKey(selectedKey)} отсутствует в resolved row keys. `
|
|
601
|
+
+ `/ ${diagnosticOwner()}: selectedRowKey ${describeRowKey(selectedKey)} is absent from resolved row keys.`,
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
return selectedKey
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const validatedSelectedRowKeys = computed<readonly RowKey[]>(() => {
|
|
608
|
+
const keys = validateControlledKeys('selectedRowKeys', props.selectedRowKeys)
|
|
609
|
+
if (validatedSelectionMode.value === 'none' && keys.length > 0) {
|
|
610
|
+
throw new TypeError(
|
|
611
|
+
`${diagnosticOwner()}: selectedRowKeys требует selectionMode="multiple". `
|
|
612
|
+
+ `/ ${diagnosticOwner()}: selectedRowKeys requires selectionMode="multiple".`,
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
if (props.isRowSelectable !== undefined && validatedSelectionMode.value !== 'multiple') {
|
|
616
|
+
throw new TypeError(
|
|
617
|
+
`${diagnosticOwner()}: isRowSelectable требует selectionMode="multiple". `
|
|
618
|
+
+ `/ ${diagnosticOwner()}: isRowSelectable requires selectionMode="multiple".`,
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
if (props.isRowSelectable !== undefined && typeof props.isRowSelectable !== 'function') {
|
|
622
|
+
throw new TypeError(`${diagnosticOwner()}: isRowSelectable must be a function.`)
|
|
623
|
+
}
|
|
624
|
+
return keys
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
const rowSelectableByRow = computed<ReadonlyMap<Row, boolean>>(() => {
|
|
628
|
+
const result = new Map<Row, boolean>()
|
|
629
|
+
for (const [index, row] of props.data.entries()) {
|
|
630
|
+
const selectable = validatedSelectionMode.value === 'multiple'
|
|
631
|
+
? validateBoolean('STable', `isRowSelectable at data[${index}]`, props.isRowSelectable?.(row, index) ?? true)
|
|
632
|
+
: false
|
|
633
|
+
result.set(row, selectable)
|
|
634
|
+
}
|
|
635
|
+
return result
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
const validatedExpandedRowKeys = computed<readonly RowKey[]>(() => {
|
|
639
|
+
const keys = validateControlledKeys('expandedRowKeys', props.expandedRowKeys)
|
|
640
|
+
if (!hasRowDetails && (keys.length > 0 || props.isRowExpandable !== undefined)) {
|
|
641
|
+
throw new TypeError(
|
|
642
|
+
`${diagnosticOwner()}: expandedRowKeys/isRowExpandable требует slot row-details. `
|
|
643
|
+
+ `/ ${diagnosticOwner()}: expandedRowKeys/isRowExpandable requires the row-details slot.`,
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
if (props.isRowExpandable !== undefined && typeof props.isRowExpandable !== 'function') {
|
|
647
|
+
throw new TypeError(`${diagnosticOwner()}: isRowExpandable must be a function.`)
|
|
648
|
+
}
|
|
649
|
+
return keys
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
const rowExpandableByRow = computed<ReadonlyMap<Row, boolean>>(() => {
|
|
653
|
+
const result = new Map<Row, boolean>()
|
|
654
|
+
for (const [index, row] of props.data.entries()) {
|
|
655
|
+
const expandable = hasRowDetails
|
|
656
|
+
? validateBoolean('STable', `isRowExpandable at data[${index}]`, props.isRowExpandable?.(row, index) ?? true)
|
|
657
|
+
: false
|
|
658
|
+
result.set(row, expandable)
|
|
659
|
+
}
|
|
660
|
+
const expanded = new Set(validatedExpandedRowKeys.value)
|
|
661
|
+
for (const row of props.data) {
|
|
662
|
+
if (expanded.has(rowKey(row)) && !result.get(row)) {
|
|
663
|
+
throw new TypeError(
|
|
664
|
+
`${diagnosticOwner()}: expandedRowKeys содержит current row, для которой isRowExpandable вернул false. `
|
|
665
|
+
+ `/ ${diagnosticOwner()}: expandedRowKeys contains a current row for which isRowExpandable returned false.`,
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return result
|
|
670
|
+
})
|
|
671
|
+
|
|
387
672
|
const validatedRows = computed<readonly Row[]>(() => {
|
|
388
673
|
const columns = validatedColumns.value
|
|
389
674
|
void validatedSort.value
|
|
390
675
|
void resolvedRowKeys.value
|
|
391
676
|
void validatedSelectedRowKey.value
|
|
677
|
+
void validatedSelectedRowKeys.value
|
|
678
|
+
void rowSelectableByRow.value
|
|
679
|
+
void validatedExpandedRowKeys.value
|
|
680
|
+
void rowExpandableByRow.value
|
|
392
681
|
for (const row of props.data) {
|
|
393
682
|
for (const column of columns) {
|
|
394
683
|
const content = column.content ?? (column.value === undefined ? 'field' : 'value')
|
|
395
|
-
if (content === 'field' && !
|
|
684
|
+
if (content === 'field' && !hasOwn(row, column.field)) {
|
|
396
685
|
throw new TypeError(
|
|
397
686
|
`STable: column ${JSON.stringify(column.field)} не известен row; добавьте typed field или value resolver. `
|
|
398
687
|
+ `/ STable: column ${JSON.stringify(column.field)} is unknown to row; add a typed field or value resolver.`,
|
|
@@ -403,18 +692,116 @@ const validatedRows = computed<readonly Row[]>(() => {
|
|
|
403
692
|
return props.data
|
|
404
693
|
})
|
|
405
694
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
695
|
+
function rowKey(row: Row): RowKey {
|
|
696
|
+
const key = resolvedRowKeys.value.get(row)
|
|
697
|
+
if (key === undefined) throw new Error(`${diagnosticOwner()}: row identity is unavailable`)
|
|
698
|
+
return key
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function rowKeyToken(key: RowKey): string {
|
|
702
|
+
return `${typeof key === 'number' ? 'number' : 'string'}:${String(key)}`
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function rowDomToken(row: Row): string {
|
|
706
|
+
return rowKeyToken(rowKey(row))
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function rowDetailsId(row: Row): string {
|
|
710
|
+
return `${tableInstanceId}-details-${encodeURIComponent(rowDomToken(row))}`
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function resolveRowLabel(row: Row, index: number): string {
|
|
714
|
+
if (props.getRowLabel === undefined) return `Строка ${index + 1}`
|
|
715
|
+
if (typeof props.getRowLabel !== 'function') {
|
|
716
|
+
throw new TypeError(`${diagnosticOwner()}: getRowLabel must be a function.`)
|
|
415
717
|
}
|
|
416
|
-
return
|
|
417
|
-
}
|
|
718
|
+
return validateNonEmptyString('STable', `getRowLabel at data[${index}]`, props.getRowLabel(row, index))
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const selectedKeySet = computed(() => new Set(validatedSelectedRowKeys.value))
|
|
722
|
+
const expandedKeySet = computed(() => new Set(validatedExpandedRowKeys.value))
|
|
723
|
+
const selectableRows = computed(() => props.data.filter((row) => rowSelectableByRow.value.get(row) === true))
|
|
724
|
+
const selectedCurrentRowCount = computed(() => selectableRows.value.filter(
|
|
725
|
+
(row) => selectedKeySet.value.has(rowKey(row)),
|
|
726
|
+
).length)
|
|
727
|
+
const someCurrentRowsSelected = computed(() => selectedCurrentRowCount.value > 0)
|
|
728
|
+
const allCurrentRowsSelected = computed(() => (
|
|
729
|
+
selectableRows.value.length > 0 && selectedCurrentRowCount.value === selectableRows.value.length
|
|
730
|
+
))
|
|
731
|
+
const renderedColumnCount = computed(() => Math.max(
|
|
732
|
+
validatedColumns.value.length
|
|
733
|
+
+ (validatedSelectionMode.value === 'multiple' ? 1 : 0)
|
|
734
|
+
+ (hasRowDetails ? 1 : 0),
|
|
735
|
+
1,
|
|
736
|
+
))
|
|
737
|
+
|
|
738
|
+
function rowIsSelectable(row: Row): boolean {
|
|
739
|
+
return rowSelectableByRow.value.get(row) === true
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function rowIsSelected(row: Row): boolean {
|
|
743
|
+
return validatedSelectionMode.value === 'multiple' && selectedKeySet.value.has(rowKey(row))
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function rowAriaSelected(row: Row): 'true' | 'false' | undefined {
|
|
747
|
+
if (validatedSelectionMode.value === 'multiple') return rowIsSelected(row) ? 'true' : 'false'
|
|
748
|
+
return rowKey(row) === validatedSelectedRowKey.value ? 'true' : undefined
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function rowSelectionLabel(row: Row, index: number): string {
|
|
752
|
+
const label = resolveRowLabel(row, index)
|
|
753
|
+
if (!rowIsSelectable(row)) return `Выбор недоступен: ${label}`
|
|
754
|
+
return rowIsSelected(row) ? `Снять выбор: ${label}` : `Выбрать: ${label}`
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function updateRowSelection(row: Row, index: number, selected: boolean): void {
|
|
758
|
+
void index
|
|
759
|
+
if (!rowIsSelectable(row)) return
|
|
760
|
+
const key = rowKey(row)
|
|
761
|
+
const current = validatedSelectedRowKeys.value
|
|
762
|
+
if (selected && !selectedKeySet.value.has(key)) emit('update:selectedRowKeys', [...current, key])
|
|
763
|
+
if (!selected && selectedKeySet.value.has(key)) {
|
|
764
|
+
emit('update:selectedRowKeys', current.filter((candidate) => candidate !== key))
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function updateAllRowsSelection(selected: boolean): void {
|
|
769
|
+
const current = validatedSelectedRowKeys.value
|
|
770
|
+
const currentSelectableKeys = new Set(selectableRows.value.map((row) => rowKey(row)))
|
|
771
|
+
if (!selected) {
|
|
772
|
+
emit('update:selectedRowKeys', current.filter((key) => !currentSelectableKeys.has(key)))
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
const next = [...current]
|
|
776
|
+
const nextKeys = new Set(current)
|
|
777
|
+
for (const key of currentSelectableKeys) {
|
|
778
|
+
if (!nextKeys.has(key)) next.push(key)
|
|
779
|
+
}
|
|
780
|
+
emit('update:selectedRowKeys', next)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function rowIsExpandable(row: Row): boolean {
|
|
784
|
+
return rowExpandableByRow.value.get(row) === true
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function rowIsExpanded(row: Row): boolean {
|
|
788
|
+
return expandedKeySet.value.has(rowKey(row))
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function rowExpansionLabel(row: Row, index: number): string {
|
|
792
|
+
return `${rowIsExpanded(row) ? 'Свернуть' : 'Раскрыть'}: ${resolveRowLabel(row, index)}`
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function toggleRowExpansion(row: Row, index: number): void {
|
|
796
|
+
if (!rowIsExpandable(row)) return
|
|
797
|
+
const key = rowKey(row)
|
|
798
|
+
const expanded = !expandedKeySet.value.has(key)
|
|
799
|
+
const next = expanded
|
|
800
|
+
? [...validatedExpandedRowKeys.value, key]
|
|
801
|
+
: validatedExpandedRowKeys.value.filter((candidate) => candidate !== key)
|
|
802
|
+
emit('update:expandedRowKeys', next)
|
|
803
|
+
emit('row-expand', row, index, expanded)
|
|
804
|
+
}
|
|
418
805
|
|
|
419
806
|
const resolveCellValue = (row: Row, column: STableColumn<Row, Field>): unknown => column.value
|
|
420
807
|
? column.value(row)
|