@pgcorp/ui-kit 0.2.0 → 0.3.1
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 +5 -5
- package/docs/public-api.md +8 -0
- package/package.json +15 -3
- package/src/components/layout/SAppShell.vue +3 -2
- package/src/components/layout/SStack.css +9 -3
- package/src/components/layout/SStack.vue +10 -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 +11 -0
- package/src/components/shared/containers/SPageHeader.vue +7 -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.css +13 -0
- package/src/components/shared/controls/SButton.vue +135 -29
- package/src/components/shared/controls/SCombobox.css +9 -0
- package/src/components/shared/controls/SCombobox.vue +322 -0
- package/src/components/shared/controls/SInputText.vue +15 -0
- 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/controls/SListbox.vue +99 -1
- package/src/components/shared/controls/SListboxOption.vue +15 -2
- package/src/components/shared/controls/SSwitch.css +48 -0
- package/src/components/shared/controls/SSwitch.vue +109 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +6 -3
- package/src/components/shared/data-display/SChip.css +59 -0
- package/src/components/shared/data-display/SChip.vue +92 -0
- 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.vue +150 -14
- 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 +9 -28
- 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/SBottomNav.vue +3 -0
- 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/ownedAttrs.ts +24 -2
- package/src/internal/pointerInteractionLease.ts +2 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-chip"
|
|
5
|
+
:data-severity="validatedSeverity"
|
|
6
|
+
:data-size="validatedSize"
|
|
7
|
+
:data-closable="closable || undefined"
|
|
8
|
+
:data-disabled="disabled || undefined"
|
|
9
|
+
>
|
|
10
|
+
<span v-if="leadingIcon" class="s-chip__icon" aria-hidden="true">
|
|
11
|
+
<component :is="leadingIcon" class="s-chip__icon-graphic" />
|
|
12
|
+
</span>
|
|
13
|
+
<span class="s-chip__label">{{ validatedLabel }}</span>
|
|
14
|
+
<SButton
|
|
15
|
+
v-if="closable"
|
|
16
|
+
label=""
|
|
17
|
+
appearance="ghost"
|
|
18
|
+
severity="secondary"
|
|
19
|
+
size="xs"
|
|
20
|
+
density="compact"
|
|
21
|
+
spacing="tight"
|
|
22
|
+
shape="circle"
|
|
23
|
+
icon-only
|
|
24
|
+
:leading-icon="X"
|
|
25
|
+
:disabled="disabled"
|
|
26
|
+
:aria-label="resolvedCloseLabel"
|
|
27
|
+
:title="resolvedCloseLabel"
|
|
28
|
+
@click.stop="emit('close', $event)"
|
|
29
|
+
/>
|
|
30
|
+
</span>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { computed, type Component } from 'vue'
|
|
35
|
+
import { X } from '../../icons/sputnigUiIcons'
|
|
36
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
37
|
+
import {
|
|
38
|
+
validateBoolean,
|
|
39
|
+
validateExactString,
|
|
40
|
+
validateNonEmptyString,
|
|
41
|
+
} from '../../../internal/runtimeContract'
|
|
42
|
+
import SButton from '../controls/SButton.vue'
|
|
43
|
+
import type { BadgeSeverity } from './SBadge.vue'
|
|
44
|
+
|
|
45
|
+
defineOptions({ inheritAttrs: false })
|
|
46
|
+
|
|
47
|
+
export type SChipSeverity = BadgeSeverity
|
|
48
|
+
export type SChipSize = 'sm' | 'md'
|
|
49
|
+
|
|
50
|
+
export interface Props {
|
|
51
|
+
label: string
|
|
52
|
+
severity?: SChipSeverity
|
|
53
|
+
size?: SChipSize
|
|
54
|
+
leadingIcon?: Component
|
|
55
|
+
closable?: boolean
|
|
56
|
+
closeLabel?: string
|
|
57
|
+
disabled?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
61
|
+
severity: 'secondary',
|
|
62
|
+
size: 'md',
|
|
63
|
+
leadingIcon: undefined,
|
|
64
|
+
closable: false,
|
|
65
|
+
closeLabel: undefined,
|
|
66
|
+
disabled: false,
|
|
67
|
+
})
|
|
68
|
+
const emit = defineEmits<{
|
|
69
|
+
close: [event: MouseEvent]
|
|
70
|
+
}>()
|
|
71
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SChip', owner: 'chip root' })
|
|
72
|
+
|
|
73
|
+
const validatedLabel = computed(() => validateNonEmptyString('SChip', 'label', props.label))
|
|
74
|
+
const validatedSeverity = computed(() => validateExactString(
|
|
75
|
+
'SChip',
|
|
76
|
+
'severity',
|
|
77
|
+
props.severity,
|
|
78
|
+
['primary', 'secondary', 'success', 'info', 'warn', 'danger', 'contrast'] as const,
|
|
79
|
+
))
|
|
80
|
+
const validatedSize = computed(() => validateExactString(
|
|
81
|
+
'SChip', 'size', props.size, ['sm', 'md'] as const,
|
|
82
|
+
))
|
|
83
|
+
const resolvedCloseLabel = computed(() => {
|
|
84
|
+
validateBoolean('SChip', 'closable', props.closable)
|
|
85
|
+
validateBoolean('SChip', 'disabled', props.disabled)
|
|
86
|
+
return props.closeLabel === undefined
|
|
87
|
+
? `Удалить «${validatedLabel.value}»`
|
|
88
|
+
: validateNonEmptyString('SChip', 'closeLabel', props.closeLabel)
|
|
89
|
+
})
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<style lang="postcss" src="./SChip.css" scoped></style>
|
|
@@ -88,20 +88,20 @@ useInteractiveLeafRegistration({ owner: 'SDocBlock' });
|
|
|
88
88
|
|
|
89
89
|
function escapeHighlightedCode(code: string): string {
|
|
90
90
|
return code
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
91
|
+
.replace(/&/gu, '&')
|
|
92
|
+
.replace(/</gu, '<')
|
|
93
|
+
.replace(/>/gu, '>')
|
|
94
|
+
.replace(/"/gu, '"')
|
|
95
|
+
.replace(/'/gu, ''');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function escapeHtmlAttribute(value: string): string {
|
|
99
99
|
return value
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.
|
|
100
|
+
.replace(/&/gu, '&')
|
|
101
|
+
.replace(/</gu, '<')
|
|
102
|
+
.replace(/>/gu, '>')
|
|
103
|
+
.replace(/"/gu, '"')
|
|
104
|
+
.replace(/'/gu, ''');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
const markdownRenderer = new marked.Renderer();
|
|
@@ -113,6 +113,7 @@ export interface Slots<T extends LinkedSystemListItem = LinkedSystemListItem> {
|
|
|
113
113
|
|
|
114
114
|
<script setup lang="ts" generic="T extends LinkedSystemListItem = LinkedSystemListItem">
|
|
115
115
|
import { computed, watchEffect } from 'vue'
|
|
116
|
+
import { withErrorCause } from '../../../internal/es2020'
|
|
116
117
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
117
118
|
import {
|
|
118
119
|
validateBoolean,
|
|
@@ -169,11 +170,10 @@ function validateExternalUrl(value: unknown, index: number): void {
|
|
|
169
170
|
try {
|
|
170
171
|
parsedUrl = new URL(externalUrl)
|
|
171
172
|
} catch (error: unknown) {
|
|
172
|
-
throw new TypeError(
|
|
173
|
+
throw withErrorCause(new TypeError(
|
|
173
174
|
`SLinkedSystemsList: ${coordinate} должен быть абсолютным HTTP(S) URL или null; получено ${JSON.stringify(externalUrl)}. `
|
|
174
175
|
+ `/ SLinkedSystemsList: ${coordinate} must be an absolute HTTP(S) URL or null; received ${JSON.stringify(externalUrl)}.`,
|
|
175
|
-
|
|
176
|
-
)
|
|
176
|
+
), error)
|
|
177
177
|
}
|
|
178
178
|
if (
|
|
179
179
|
(parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:')
|
|
@@ -23,10 +23,30 @@
|
|
|
23
23
|
:data-width="column.width"
|
|
24
24
|
:data-min-width="column.minWidth"
|
|
25
25
|
:data-align="column.align ?? 'left'"
|
|
26
|
+
:data-sortable="column.sortable || undefined"
|
|
27
|
+
:aria-sort="columnAriaSort(column)"
|
|
26
28
|
>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
<SButton
|
|
30
|
+
v-if="column.sortable"
|
|
31
|
+
:aria-label="sortActionLabel(column)"
|
|
32
|
+
appearance="text"
|
|
33
|
+
severity="secondary"
|
|
34
|
+
size="xs"
|
|
35
|
+
density="compact"
|
|
36
|
+
width="full"
|
|
37
|
+
:content-align="column.align === 'right' ? 'end' : column.align === 'center' ? 'center' : 'start'"
|
|
38
|
+
:trailing-icon="sortIcon(column)"
|
|
39
|
+
@click="requestSort(column)"
|
|
40
|
+
>
|
|
41
|
+
<slot name="column" :column="column">
|
|
42
|
+
<slot :name="`header-${column.field}`" :column="column">{{ column.header }}</slot>
|
|
43
|
+
</slot>
|
|
44
|
+
</SButton>
|
|
45
|
+
<template v-else>
|
|
46
|
+
<slot name="column" :column="column">
|
|
47
|
+
<slot :name="`header-${column.field}`" :column="column">{{ column.header }}</slot>
|
|
48
|
+
</slot>
|
|
49
|
+
</template>
|
|
30
50
|
</th>
|
|
31
51
|
</tr>
|
|
32
52
|
</thead>
|
|
@@ -47,6 +67,10 @@
|
|
|
47
67
|
:key="resolvedRowKeys.get(row)"
|
|
48
68
|
:data-state="resolveRowState(row)"
|
|
49
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)"
|
|
50
74
|
>
|
|
51
75
|
<td
|
|
52
76
|
v-for="column in validatedColumns"
|
|
@@ -80,6 +104,19 @@
|
|
|
80
104
|
</template>
|
|
81
105
|
|
|
82
106
|
<script lang="ts">
|
|
107
|
+
import type { ScrollableViewportMaxBlockSize } from '../../../internal/semanticSizing'
|
|
108
|
+
import type {
|
|
109
|
+
STableCellState,
|
|
110
|
+
STableColumn,
|
|
111
|
+
STableColumnContent,
|
|
112
|
+
STableDensity,
|
|
113
|
+
STableMinWidth,
|
|
114
|
+
STableRowKey,
|
|
115
|
+
STableRowState,
|
|
116
|
+
STableSort,
|
|
117
|
+
STableSurface,
|
|
118
|
+
} from './table'
|
|
119
|
+
|
|
83
120
|
export type {
|
|
84
121
|
STableAlignment,
|
|
85
122
|
STableCellState,
|
|
@@ -87,16 +124,49 @@ export type {
|
|
|
87
124
|
STableColumnContent,
|
|
88
125
|
STableDensity,
|
|
89
126
|
STableMinWidth,
|
|
90
|
-
STableProps,
|
|
91
127
|
STableRowKey,
|
|
92
128
|
STableRowState,
|
|
129
|
+
STableSort,
|
|
130
|
+
STableSortDirection,
|
|
93
131
|
STableSurface,
|
|
94
132
|
} from './table'
|
|
133
|
+
|
|
134
|
+
/** Публичный generic-контракт таблицы принадлежит source-SFC runtime. / The public generic table contract is owned by the source-SFC runtime. */
|
|
135
|
+
export interface STableProps<
|
|
136
|
+
Row extends object,
|
|
137
|
+
RowKey extends STableRowKey,
|
|
138
|
+
Field extends string,
|
|
139
|
+
> {
|
|
140
|
+
columns: readonly STableColumn<Row, Field>[]
|
|
141
|
+
data: readonly Row[]
|
|
142
|
+
getRowKey: (row: Row) => RowKey
|
|
143
|
+
loading?: boolean
|
|
144
|
+
loadingMessage?: string
|
|
145
|
+
errorMessage?: string
|
|
146
|
+
emptyMessage?: string
|
|
147
|
+
selectedRowKey?: RowKey
|
|
148
|
+
sort?: STableSort<Field> | null
|
|
149
|
+
maxHeight?: ScrollableViewportMaxBlockSize
|
|
150
|
+
density?: STableDensity
|
|
151
|
+
surface?: STableSurface
|
|
152
|
+
minWidth?: STableMinWidth
|
|
153
|
+
rowState?: (row: Row) => STableRowState
|
|
154
|
+
cellState?: (row: Row, column: STableColumn<Row, Field>) => STableCellState
|
|
155
|
+
/** Accessible name без visible owner. / Accessible name when there is no visible owner. */
|
|
156
|
+
ariaLabel?: string
|
|
157
|
+
/** DOM id visible owner, mutually exclusive with ariaLabel. / DOM id of the visible owner, mutually exclusive with ariaLabel. */
|
|
158
|
+
ariaLabelledby?: string
|
|
159
|
+
/** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
|
|
160
|
+
ariaDescribedby?: string
|
|
161
|
+
}
|
|
95
162
|
</script>
|
|
96
163
|
|
|
97
164
|
<script setup lang="ts" generic="Row extends object = Record<string, unknown>, RowKey extends STableRowKey = STableRowKey, Field extends string = Extract<keyof Row, string>">
|
|
98
165
|
import { computed, useSlots } from 'vue'
|
|
166
|
+
import { ChevronDown, ChevronsUpDown, ChevronUp } from '../../icons/sputnigUiIcons'
|
|
167
|
+
import { hasOwn } from '../../../internal/es2020'
|
|
99
168
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
169
|
+
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
100
170
|
import {
|
|
101
171
|
validateScrollableViewportMaxBlockSize,
|
|
102
172
|
validateTableColumnInlineSize,
|
|
@@ -109,15 +179,7 @@ import {
|
|
|
109
179
|
validateNonEmptyString,
|
|
110
180
|
} from '../../../internal/runtimeContract'
|
|
111
181
|
import SAsyncState from '../feedback/SAsyncState.vue'
|
|
112
|
-
import
|
|
113
|
-
STableCellState,
|
|
114
|
-
STableColumn,
|
|
115
|
-
STableColumnContent,
|
|
116
|
-
STableProps,
|
|
117
|
-
STableRowKey,
|
|
118
|
-
STableRowState,
|
|
119
|
-
} from './table'
|
|
120
|
-
|
|
182
|
+
import SButton from '../controls/SButton.vue'
|
|
121
183
|
defineOptions({ inheritAttrs: false })
|
|
122
184
|
|
|
123
185
|
const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
@@ -126,6 +188,7 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
|
126
188
|
errorMessage: '',
|
|
127
189
|
emptyMessage: 'Нет данных для отображения',
|
|
128
190
|
selectedRowKey: undefined,
|
|
191
|
+
sort: null,
|
|
129
192
|
maxHeight: 'none',
|
|
130
193
|
density: 'default',
|
|
131
194
|
surface: 'bordered',
|
|
@@ -136,6 +199,11 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
|
136
199
|
ariaLabelledby: undefined,
|
|
137
200
|
ariaDescribedby: undefined,
|
|
138
201
|
})
|
|
202
|
+
const emit = defineEmits<{
|
|
203
|
+
'row-click': [row: Row, index: number]
|
|
204
|
+
'update:sort': [value: STableSort<Field> | null]
|
|
205
|
+
sort: [value: STableSort<Field> | null]
|
|
206
|
+
}>()
|
|
139
207
|
const slots = useSlots()
|
|
140
208
|
|
|
141
209
|
const TABLE_DENSITIES = ['default', 'compact'] as const
|
|
@@ -147,6 +215,22 @@ const TABLE_CELL_STATES = ['default', 'pending'] as const
|
|
|
147
215
|
const TABLE_COLUMN_CONTENT = ['field', 'value', 'slot'] as const
|
|
148
216
|
|
|
149
217
|
const ownedAttrs = useOwnedAttrs({ component: 'STable', owner: 'table scroll surface' })
|
|
218
|
+
useInteractiveLeafRegistration({ owner: 'STable' })
|
|
219
|
+
|
|
220
|
+
function eventBelongsToNestedControl(event: Event): boolean {
|
|
221
|
+
const owner = event.currentTarget
|
|
222
|
+
return event.composedPath().some((candidate) => candidate !== owner && candidate instanceof HTMLElement && (
|
|
223
|
+
candidate.matches('button, a, input, select, textarea, [role="button"], [role="link"]')
|
|
224
|
+
))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onRowClick(event: MouseEvent, row: Row, rowIndex: number): void {
|
|
228
|
+
if (!eventBelongsToNestedControl(event)) emit('row-click', row, rowIndex)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function onRowKeydown(event: KeyboardEvent, row: Row, rowIndex: number): void {
|
|
232
|
+
if (event.target === event.currentTarget) emit('row-click', row, rowIndex)
|
|
233
|
+
}
|
|
150
234
|
const validatedLoading = computed(() => validateBoolean('STable', 'loading', props.loading))
|
|
151
235
|
const resolvedAccessibleName = computed(() => {
|
|
152
236
|
if (props.ariaLabel !== undefined && props.ariaLabelledby !== undefined) {
|
|
@@ -207,6 +291,9 @@ const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
|
207
291
|
if (column.align !== undefined) {
|
|
208
292
|
validateExactString('STable', `column ${JSON.stringify(field)}.align`, column.align, TABLE_ALIGNMENTS)
|
|
209
293
|
}
|
|
294
|
+
if (column.sortable !== undefined) {
|
|
295
|
+
validateBoolean('STable', `column ${JSON.stringify(field)}.sortable`, column.sortable)
|
|
296
|
+
}
|
|
210
297
|
if (column.width !== undefined) {
|
|
211
298
|
validateTableColumnInlineSize('STable', `column ${JSON.stringify(field)}.width`, column.width)
|
|
212
299
|
}
|
|
@@ -241,6 +328,54 @@ const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
|
241
328
|
return props.columns
|
|
242
329
|
})
|
|
243
330
|
|
|
331
|
+
const validatedSort = computed<STableSort<Field> | null>(() => {
|
|
332
|
+
if (props.sort === null || props.sort === undefined) return null
|
|
333
|
+
const key = validateNonEmptyString('STable', 'sort.key', props.sort.key) as Field
|
|
334
|
+
const column = validatedColumns.value.find((candidate) => candidate.field === key)
|
|
335
|
+
if (!column?.sortable) {
|
|
336
|
+
throw new TypeError(`STable: sort key ${JSON.stringify(key)} должен ссылаться на sortable column. / STable: sort key ${JSON.stringify(key)} must reference a sortable column.`)
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
key,
|
|
340
|
+
direction: validateExactString(
|
|
341
|
+
'STable',
|
|
342
|
+
'sort.direction',
|
|
343
|
+
props.sort.direction,
|
|
344
|
+
['ascending', 'descending'] as const,
|
|
345
|
+
),
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const columnAriaSort = (column: STableColumn<Row, Field>): 'none' | 'ascending' | 'descending' | undefined => {
|
|
350
|
+
if (!column.sortable) return undefined
|
|
351
|
+
return validatedSort.value?.key === column.field ? validatedSort.value.direction : 'none'
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const sortIcon = (column: STableColumn<Row, Field>) => {
|
|
355
|
+
const direction = columnAriaSort(column)
|
|
356
|
+
return direction === 'ascending' ? ChevronUp : direction === 'descending' ? ChevronDown : ChevronsUpDown
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const sortActionLabel = (column: STableColumn<Row, Field>): string => {
|
|
360
|
+
const direction = columnAriaSort(column)
|
|
361
|
+
if (direction === 'ascending') return `${column.header}: сортировать по убыванию`
|
|
362
|
+
if (direction === 'descending') return `${column.header}: сбросить сортировку`
|
|
363
|
+
return `${column.header}: сортировать по возрастанию`
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function requestSort(column: STableColumn<Row, Field>): void {
|
|
367
|
+
if (!column.sortable) return
|
|
368
|
+
const current = validatedSort.value
|
|
369
|
+
const key = column.field as Field
|
|
370
|
+
const next: STableSort<Field> | null = current?.key !== key
|
|
371
|
+
? { key, direction: 'ascending' }
|
|
372
|
+
: current.direction === 'ascending'
|
|
373
|
+
? { key, direction: 'descending' }
|
|
374
|
+
: null
|
|
375
|
+
emit('update:sort', next)
|
|
376
|
+
emit('sort', next)
|
|
377
|
+
}
|
|
378
|
+
|
|
244
379
|
function describeRowKey(key: STableRowKey): string {
|
|
245
380
|
return typeof key === 'string' ? JSON.stringify(key) : String(key)
|
|
246
381
|
}
|
|
@@ -283,12 +418,13 @@ const resolvedRowKeys = computed<ReadonlyMap<Row, RowKey>>(() => {
|
|
|
283
418
|
|
|
284
419
|
const validatedRows = computed<readonly Row[]>(() => {
|
|
285
420
|
const columns = validatedColumns.value
|
|
421
|
+
void validatedSort.value
|
|
286
422
|
void resolvedRowKeys.value
|
|
287
423
|
void validatedSelectedRowKey.value
|
|
288
424
|
for (const row of props.data) {
|
|
289
425
|
for (const column of columns) {
|
|
290
426
|
const content = column.content ?? (column.value === undefined ? 'field' : 'value')
|
|
291
|
-
if (content === 'field' && !
|
|
427
|
+
if (content === 'field' && !hasOwn(row, column.field)) {
|
|
292
428
|
throw new TypeError(
|
|
293
429
|
`STable: column ${JSON.stringify(column.field)} не известен row; добавьте typed field или value resolver. `
|
|
294
430
|
+ `/ STable: column ${JSON.stringify(column.field)} is unknown to row; add a typed field or value resolver.`,
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
type VNodeRef,
|
|
70
70
|
} from 'vue';
|
|
71
71
|
import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
72
|
+
import { hasOwn } from '../../../internal/es2020';
|
|
72
73
|
import {
|
|
73
74
|
resolveInteractiveElement,
|
|
74
75
|
type ElementRefTarget,
|
|
@@ -202,7 +203,7 @@ function assertDenseArray(coordinate: string, value: unknown): readonly unknown[
|
|
|
202
203
|
throw triggerContractError(`${coordinate} должен быть массивом. ${coordinate} must be an array.`);
|
|
203
204
|
}
|
|
204
205
|
for (let index = 0; index < value.length; index += 1) {
|
|
205
|
-
if (!
|
|
206
|
+
if (!hasOwn(value, index)) {
|
|
206
207
|
throw triggerContractError(`${coordinate}[${index}] отсутствует в sparse array. ${coordinate}[${index}] is missing from a sparse array.`);
|
|
207
208
|
}
|
|
208
209
|
}
|
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
/** Ключ элемента виртуального списка. / Virtual-list item key. */
|
|
2
2
|
export type SVirtualListItemKey = string | number
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* Публичный контракт виртуального списка с измеряемыми строками.
|
|
6
|
-
* Public contract for a virtual list with measured rows.
|
|
7
|
-
*/
|
|
8
|
-
export interface SVirtualListProps<Item = unknown> {
|
|
9
|
-
items: readonly Item[]
|
|
10
|
-
/** Оценочная высота строки до фактического измерения ResizeObserver. / Estimated row height before ResizeObserver measurement. */
|
|
11
|
-
itemSize: number
|
|
12
|
-
overscan?: number
|
|
13
|
-
height?: number | null
|
|
14
|
-
/** Устойчивый доменный ключ; индекс массива не является допустимым fallback. / Stable domain key; an array index is not a valid fallback. */
|
|
15
|
-
itemKey: (item: Item) => SVirtualListItemKey
|
|
16
|
-
itemLabel: (item: Item, index: number) => string
|
|
17
|
-
accessibleLabel: string
|
|
18
|
-
externalScrollTarget?: HTMLElement | null
|
|
19
|
-
}
|
|
4
|
+
export type { SVirtualListProps } from './SVirtualList.vue'
|
|
@@ -35,6 +35,24 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
</template>
|
|
37
37
|
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
import type { SVirtualListItemKey } from './SVirtualList'
|
|
40
|
+
|
|
41
|
+
/** Публичный контракт virtual list принадлежит source-SFC runtime. / The public virtual-list contract is owned by the source-SFC runtime. */
|
|
42
|
+
export interface SVirtualListProps<Item = unknown> {
|
|
43
|
+
items: readonly Item[]
|
|
44
|
+
/** Оценочная высота строки до фактического измерения. / Estimated row height before measurement. */
|
|
45
|
+
itemSize: number
|
|
46
|
+
overscan?: number
|
|
47
|
+
height?: number | null
|
|
48
|
+
/** Устойчивый доменный ключ; индекс не является fallback. / Stable domain key; the index is not a fallback. */
|
|
49
|
+
itemKey: (item: Item) => SVirtualListItemKey
|
|
50
|
+
itemLabel: (item: Item, index: number) => string
|
|
51
|
+
accessibleLabel: string
|
|
52
|
+
externalScrollTarget?: HTMLElement | null
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
38
56
|
<script setup lang="ts" generic="T">
|
|
39
57
|
import {
|
|
40
58
|
computed,
|
|
@@ -47,8 +65,6 @@ import {
|
|
|
47
65
|
} from 'vue'
|
|
48
66
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
49
67
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
50
|
-
import type { SVirtualListItemKey, SVirtualListProps } from './SVirtualList'
|
|
51
|
-
|
|
52
68
|
defineOptions({ inheritAttrs: false })
|
|
53
69
|
|
|
54
70
|
type Alignment = 'auto' | 'start' | 'center' | 'end'
|
|
@@ -12,7 +12,7 @@ export type JsonValue = JsonScalar | JsonObject | JsonArray
|
|
|
12
12
|
|
|
13
13
|
/** Escapes one RFC 6901 reference token. / Экранирует один reference token по RFC 6901. */
|
|
14
14
|
export function escapeJsonPointerToken(token: string): string {
|
|
15
|
-
return token.
|
|
15
|
+
return token.replace(/~/gu, '~0').replace(/\//gu, '~1')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Appends a reference token to an RFC 6901 JSON Pointer. / Добавляет reference token к JSON Pointer по RFC 6901. */
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ScrollableViewportMaxBlockSize,
|
|
3
2
|
TableColumnInlineSize,
|
|
4
3
|
TableColumnMinInlineSize,
|
|
5
4
|
} from '../../../internal/semanticSizing'
|
|
@@ -12,6 +11,13 @@ export type STableSurface = 'bordered' | 'plain'
|
|
|
12
11
|
export type STableMinWidth = 'content' | 'md' | 'lg'
|
|
13
12
|
export type STableDensity = 'default' | 'compact'
|
|
14
13
|
export type STableColumnContent = 'field' | 'value' | 'slot'
|
|
14
|
+
export type STableSortDirection = 'ascending' | 'descending'
|
|
15
|
+
|
|
16
|
+
/** Controlled sort state; consumer owns data ordering. / Управляемое состояние сортировки; порядок данных принадлежит consumer. */
|
|
17
|
+
export interface STableSort<Field extends string = string> {
|
|
18
|
+
key: Field
|
|
19
|
+
direction: STableSortDirection
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
interface STableColumnBase<Field extends string> {
|
|
17
23
|
field: Field
|
|
@@ -19,6 +25,7 @@ interface STableColumnBase<Field extends string> {
|
|
|
19
25
|
width?: TableColumnInlineSize
|
|
20
26
|
minWidth?: TableColumnMinInlineSize
|
|
21
27
|
align?: STableAlignment
|
|
28
|
+
sortable?: boolean
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
type STableFieldColumn<Field extends string> = STableColumnBase<Field> & {
|
|
@@ -47,30 +54,4 @@ export type STableColumn<
|
|
|
47
54
|
| STableSlotColumn<Field>
|
|
48
55
|
| (Field extends Extract<keyof Row, string> ? STableFieldColumn<Field> : never)
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
export interface STableProps<
|
|
52
|
-
Row extends object,
|
|
53
|
-
RowKey extends STableRowKey,
|
|
54
|
-
Field extends string,
|
|
55
|
-
> {
|
|
56
|
-
columns: readonly STableColumn<Row, Field>[]
|
|
57
|
-
data: readonly Row[]
|
|
58
|
-
getRowKey: (row: Row) => RowKey
|
|
59
|
-
loading?: boolean
|
|
60
|
-
loadingMessage?: string
|
|
61
|
-
errorMessage?: string
|
|
62
|
-
emptyMessage?: string
|
|
63
|
-
selectedRowKey?: RowKey
|
|
64
|
-
maxHeight?: ScrollableViewportMaxBlockSize
|
|
65
|
-
density?: STableDensity
|
|
66
|
-
surface?: STableSurface
|
|
67
|
-
minWidth?: STableMinWidth
|
|
68
|
-
rowState?: (row: Row) => STableRowState
|
|
69
|
-
cellState?: (row: Row, column: STableColumn<Row, Field>) => STableCellState
|
|
70
|
-
/** Accessible name без visible owner. / Accessible name when there is no visible owner. */
|
|
71
|
-
ariaLabel?: string
|
|
72
|
-
/** DOM id visible owner, mutually exclusive with ariaLabel. / DOM id of the visible owner, mutually exclusive with ariaLabel. */
|
|
73
|
-
ariaLabelledby?: string
|
|
74
|
-
/** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
|
|
75
|
-
ariaDescribedby?: string
|
|
76
|
-
}
|
|
57
|
+
export type { STableProps } from './STable.vue'
|
|
@@ -83,6 +83,13 @@
|
|
|
83
83
|
</template>
|
|
84
84
|
|
|
85
85
|
<script lang="ts">
|
|
86
|
+
import type { ScrollableViewportMaxBlockSize } from '../../../internal/semanticSizing'
|
|
87
|
+
import type {
|
|
88
|
+
SDataGridColumn,
|
|
89
|
+
SDataGridColumnKey,
|
|
90
|
+
SDataGridRow,
|
|
91
|
+
} from './dataGrid'
|
|
92
|
+
|
|
86
93
|
export type {
|
|
87
94
|
SDataGridCell,
|
|
88
95
|
SDataGridCellKind,
|
|
@@ -90,14 +97,32 @@ export type {
|
|
|
90
97
|
SDataGridColumnKey,
|
|
91
98
|
SDataGridCommitInput,
|
|
92
99
|
SDataGridEditPayload,
|
|
93
|
-
SDataGridProps,
|
|
94
100
|
SDataGridRow,
|
|
95
101
|
SDataGridRowState,
|
|
96
102
|
} from './dataGrid'
|
|
103
|
+
|
|
104
|
+
/** Публичный контракт data grid принадлежит source-SFC runtime. / The public data-grid contract is owned by the source-SFC runtime. */
|
|
105
|
+
export interface SDataGridProps<Key extends SDataGridColumnKey = SDataGridColumnKey> {
|
|
106
|
+
columns: readonly SDataGridColumn<Key>[]
|
|
107
|
+
rows: readonly SDataGridRow<Key>[]
|
|
108
|
+
title?: string
|
|
109
|
+
loading?: boolean
|
|
110
|
+
disabled?: boolean
|
|
111
|
+
readOnly?: boolean
|
|
112
|
+
errorMessage?: string
|
|
113
|
+
emptyMessage?: string
|
|
114
|
+
selectedRowId?: string
|
|
115
|
+
maxHeight?: ScrollableViewportMaxBlockSize
|
|
116
|
+
/** Accessible name без visible title. / Accessible name when there is no visible title. */
|
|
117
|
+
ariaLabel?: string
|
|
118
|
+
/** DOM id external visible title. / DOM id of an external visible title. */
|
|
119
|
+
ariaLabelledby?: string
|
|
120
|
+
}
|
|
97
121
|
</script>
|
|
98
122
|
|
|
99
123
|
<script setup lang="ts" generic="ColumnKey extends string = string">
|
|
100
124
|
import { computed, reactive, useId, useSlots, watch } from 'vue'
|
|
125
|
+
import { hasOwn } from '../../../internal/es2020'
|
|
101
126
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
102
127
|
import {
|
|
103
128
|
validateScrollableViewportMaxBlockSize,
|
|
@@ -121,12 +146,8 @@ import type {
|
|
|
121
146
|
} from '../data-display/table'
|
|
122
147
|
import type {
|
|
123
148
|
SDataGridCell,
|
|
124
|
-
SDataGridColumn,
|
|
125
|
-
SDataGridColumnKey,
|
|
126
149
|
SDataGridCommitInput,
|
|
127
150
|
SDataGridEditPayload,
|
|
128
|
-
SDataGridProps,
|
|
129
|
-
SDataGridRow,
|
|
130
151
|
} from './dataGrid'
|
|
131
152
|
|
|
132
153
|
defineOptions({ inheritAttrs: false })
|
|
@@ -266,7 +287,7 @@ const validatedGrid = computed<ValidatedGrid<ColumnKey>>(() => {
|
|
|
266
287
|
}
|
|
267
288
|
}
|
|
268
289
|
for (const [key, column] of columnByKey) {
|
|
269
|
-
if (!
|
|
290
|
+
if (!hasOwn(row.cells, key)) {
|
|
270
291
|
throw new TypeError(
|
|
271
292
|
`SDataGrid: row ${JSON.stringify(rowId)} не содержит cell ${JSON.stringify(key)}. `
|
|
272
293
|
+ `/ SDataGrid: row ${JSON.stringify(rowId)} is missing cell ${JSON.stringify(key)}.`,
|
|
@@ -358,7 +379,7 @@ function resolveColumn(key: string): SDataGridColumn<ColumnKey> {
|
|
|
358
379
|
|
|
359
380
|
function resolveCell(row: SDataGridRow<ColumnKey>, columnKeyValue: string): SDataGridCell {
|
|
360
381
|
const columnKey = resolveColumn(columnKeyValue).key
|
|
361
|
-
if (
|
|
382
|
+
if (hasOwn(row.cells, columnKey)) return row.cells[columnKey]
|
|
362
383
|
throw new TypeError(
|
|
363
384
|
`SDataGrid: row ${JSON.stringify(row.id)} не содержит cell ${JSON.stringify(columnKey)}. `
|
|
364
385
|
+ `/ SDataGrid: row ${JSON.stringify(row.id)} is missing cell ${JSON.stringify(columnKey)}.`,
|
|
@@ -404,7 +425,7 @@ function editCell(row: SDataGridRow<ColumnKey>, column: SDataGridColumn<ColumnKe
|
|
|
404
425
|
function validateCommitInput(input: unknown): SDataGridCommitInput {
|
|
405
426
|
assertRecord(input, 'commit input')
|
|
406
427
|
const source = validateExactString('SDataGrid', 'commit input.source', input.source, COMMIT_SOURCES)
|
|
407
|
-
const hasValue =
|
|
428
|
+
const hasValue = hasOwn(input, 'value')
|
|
408
429
|
if (source === 'draft' && hasValue) {
|
|
409
430
|
throw new TypeError(
|
|
410
431
|
'SDataGrid: draft commit не должен содержать value. / SDataGrid: draft commit must not contain value.',
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
|
|
52
52
|
<script setup lang="ts">
|
|
53
53
|
import { computed, ref, useId, useSlots } from 'vue'
|
|
54
|
+
import { hasOwn } from '../../../internal/es2020'
|
|
54
55
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
55
56
|
import { provideTrustedCodeEditorExtensions } from '../../../internal/codeEditorExtensions'
|
|
56
57
|
import { validateCodeEditorViewport } from '../../../internal/codeEditorContract'
|
|
@@ -291,8 +292,8 @@ const validatedDiagnostics = computed<SSqlEditorDiagnostic[]>(() => {
|
|
|
291
292
|
return props.diagnostics.map((candidate, index) => {
|
|
292
293
|
const runtimeCandidate: unknown = candidate
|
|
293
294
|
const record = assertPlainExactRecord(`SSqlEditor.diagnostics[${index}]`, runtimeCandidate, ['severity', 'message', 'from', 'to', 'line', 'column'])
|
|
294
|
-
const hasOffset =
|
|
295
|
-
const hasLine =
|
|
295
|
+
const hasOffset = hasOwn(record, 'from')
|
|
296
|
+
const hasLine = hasOwn(record, 'line') || hasOwn(record, 'column')
|
|
296
297
|
if (hasOffset === hasLine) throw new TypeError(`SSqlEditor: diagnostics[${index}] должен задавать ровно один range: from/to или line/column. / SSqlEditor: diagnostics[${index}] must provide exactly one range: from/to or line/column.`)
|
|
297
298
|
const allowedRangeKeys = hasOffset
|
|
298
299
|
? ['severity', 'message', 'from', 'to']
|