@pgcorp/ui-kit 0.7.3 → 0.8.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 +161 -8
- package/docs/accessibility.md +12 -0
- package/docs/getting-started.md +32 -2
- package/docs/public-api.md +27 -2
- package/package.json +18 -3
- package/src/components/shared/codeLanguages.ts +2 -107
- package/src/components/shared/containers/SPanel.css +32 -1
- package/src/components/shared/containers/SPanel.vue +4 -0
- package/src/components/shared/controls/SCheckbox.vue +2 -2
- package/src/components/shared/controls/SInteractiveSurface.css +54 -4
- package/src/components/shared/controls/SInteractiveSurface.vue +45 -31
- package/src/components/shared/controls/SListbox.vue +4 -4
- package/src/components/shared/controls/SSwitch.vue +2 -2
- package/src/components/shared/data-display/SActionCard.css +6 -2
- package/src/components/shared/data-display/SActionCard.vue +13 -3
- package/src/components/shared/data-display/SActionListItem.css +7 -8
- package/src/components/shared/data-display/SActionListItem.vue +21 -18
- package/src/components/shared/data-display/SChart.css +149 -0
- package/src/components/shared/data-display/SChart.vue +493 -0
- package/src/components/shared/data-display/SChip.css +24 -0
- package/src/components/shared/data-display/SChip.vue +30 -6
- package/src/components/shared/data-display/SCodeBlock.css +68 -0
- package/src/components/shared/data-display/SCodeBlock.vue +102 -16
- package/src/components/shared/data-display/SMetricCard.css +1 -0
- package/src/components/shared/data-display/SMetricCard.vue +1 -0
- package/src/components/shared/data-display/STable.vue +120 -145
- package/src/components/shared/data-display/chart.ts +54 -0
- package/src/components/shared/navigation/STabList.vue +0 -3
- package/src/internal/chartGeometry.ts +852 -0
- package/src/internal/codeLanguageIdentity.ts +89 -0
- package/src/internal/lazyCodeEditor.ts +1 -0
- package/src/internal/linkTarget.ts +1 -3
- package/src/internal/ownedAttrs.ts +1 -0
- package/src/internal/useBinaryInput.ts +9 -2
- package/src/styles/tailwind.css +3 -0
- package/src/styles/tokens.css +33 -0
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
:class="{
|
|
6
6
|
's-code-block--radius-none': validatedSurfaceRadius === 'none',
|
|
7
7
|
's-code-block--radius-xl': validatedSurfaceRadius === 'xl',
|
|
8
|
-
's-code-block--interactive': !validatedReadOnly,
|
|
8
|
+
's-code-block--interactive': validatedPresentation === 'editor' && !validatedReadOnly,
|
|
9
9
|
's-code-block--viewport': !['content', 'document'].includes(validatedViewport),
|
|
10
10
|
}"
|
|
11
|
+
:data-presentation="validatedPresentation"
|
|
11
12
|
>
|
|
12
13
|
<div v-if="validatedShowHeader" class="s-code-block-header">
|
|
13
14
|
<div class="s-code-block-info">
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
</div>
|
|
30
31
|
</div>
|
|
31
32
|
<div class="s-code-block-editor-region">
|
|
32
|
-
<
|
|
33
|
+
<AsyncSCodeEditor
|
|
34
|
+
v-if="validatedPresentation === 'editor'"
|
|
33
35
|
ref="codeEditor"
|
|
34
36
|
:model-value="props.code"
|
|
35
37
|
:language="validatedLanguage"
|
|
@@ -45,9 +47,24 @@
|
|
|
45
47
|
@selection-applied="emit('selection-applied', $event)"
|
|
46
48
|
@ready="onReady"
|
|
47
49
|
/>
|
|
50
|
+
<pre
|
|
51
|
+
v-else
|
|
52
|
+
:ref="setPlainSurface"
|
|
53
|
+
class="s-code-block-plain"
|
|
54
|
+
:data-viewport="validatedViewport"
|
|
55
|
+
:data-line-numbers="validatedShowLineNumbers ? 'visible' : 'hidden'"
|
|
56
|
+
:aria-label="props.ariaLabel"
|
|
57
|
+
:aria-labelledby="props.ariaLabelledby"
|
|
58
|
+
:style="{ tabSize: validatedTabSize }"
|
|
59
|
+
tabindex="0"
|
|
60
|
+
><code v-if="!validatedShowLineNumbers">{{ validatedCode }}</code><code v-else class="s-code-block-plain__lines"><span
|
|
61
|
+
v-for="(line, index) in plainLines"
|
|
62
|
+
:key="index"
|
|
63
|
+
class="s-code-block-plain__line"
|
|
64
|
+
><span class="s-code-block-plain__line-number" aria-hidden="true">{{ index + 1 }}</span><span class="s-code-block-plain__line-content">{{ line || '\u200b' }}</span></span></code></pre>
|
|
48
65
|
</div>
|
|
49
66
|
<SAsyncState
|
|
50
|
-
v-if="validatedErrorMessage !== undefined || !isReady"
|
|
67
|
+
v-if="validatedErrorMessage !== undefined || (validatedPresentation === 'editor' && !isReady)"
|
|
51
68
|
:state="validatedErrorMessage === undefined
|
|
52
69
|
? { status: 'loading', message: 'Загрузка кода...' }
|
|
53
70
|
: { status: 'error', message: validatedErrorMessage }"
|
|
@@ -57,21 +74,21 @@
|
|
|
57
74
|
</template>
|
|
58
75
|
|
|
59
76
|
<script setup lang="ts">
|
|
60
|
-
import { computed, ref } from 'vue';
|
|
77
|
+
import { computed, defineAsyncComponent, onMounted, ref, watch, type ComponentPublicInstance } from 'vue';
|
|
61
78
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
62
|
-
import { validateBoolean, validateExactString, validateNonEmptyString } from '../../../internal/runtimeContract';
|
|
79
|
+
import { validateBoolean, validateBoundedInteger, validateExactString, validateNonEmptyString } from '../../../internal/runtimeContract';
|
|
63
80
|
import { validateCodeEditorLanguage, validateCodeEditorViewport } from '../../../internal/codeEditorContract';
|
|
64
|
-
import { resolveCodeLanguageKey } from '
|
|
81
|
+
import { resolveCodeLanguageKey } from '../../../internal/codeLanguageIdentity';
|
|
65
82
|
import { useNotifier } from '../../../composables/useNotifier';
|
|
66
83
|
import { useClipboard } from '../../../composables/useClipboard';
|
|
67
84
|
import SButton from '../controls/SButton.vue';
|
|
68
|
-
import
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
import type {
|
|
86
|
+
SCodeEditorCapabilities,
|
|
87
|
+
SCodeEditorCursorActivity,
|
|
88
|
+
SCodeEditorLanguage,
|
|
89
|
+
SCodeEditorReadyPayload,
|
|
90
|
+
SCodeEditorSelectionAppliedPayload,
|
|
91
|
+
SCodeEditorViewport,
|
|
75
92
|
} from './SCodeEditor.vue';
|
|
76
93
|
import { Copy } from '../../icons/sputnigUiIcons';
|
|
77
94
|
import SAsyncState from '../feedback/SAsyncState.vue';
|
|
@@ -86,6 +103,7 @@ type AccessibleName =
|
|
|
86
103
|
export type Props = AccessibleName & {
|
|
87
104
|
code: string;
|
|
88
105
|
language: SCodeEditorLanguage;
|
|
106
|
+
presentation?: 'editor' | 'plain';
|
|
89
107
|
showHeader?: boolean;
|
|
90
108
|
showLineNumbers?: boolean;
|
|
91
109
|
readOnly?: boolean;
|
|
@@ -100,6 +118,7 @@ export type Props = AccessibleName & {
|
|
|
100
118
|
};
|
|
101
119
|
|
|
102
120
|
const props = withDefaults(defineProps<Props>(), {
|
|
121
|
+
presentation: 'editor',
|
|
103
122
|
showHeader: true,
|
|
104
123
|
showLineNumbers: true,
|
|
105
124
|
readOnly: true,
|
|
@@ -114,7 +133,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
114
133
|
});
|
|
115
134
|
|
|
116
135
|
const isReady = ref(false);
|
|
117
|
-
const codeEditor = ref<InstanceType<typeof SCodeEditor> | null>(null);
|
|
136
|
+
const codeEditor = ref<InstanceType<typeof import('./SCodeEditor.vue')['default']> | null>(null);
|
|
137
|
+
const plainSurface = ref<HTMLElement | null>(null);
|
|
138
|
+
const AsyncSCodeEditor = defineAsyncComponent(() => import('../../../internal/lazyCodeEditor'));
|
|
118
139
|
const ownedAttrs = useOwnedAttrs({ component: 'SCodeBlock', owner: 'code block root' });
|
|
119
140
|
const emit = defineEmits<{
|
|
120
141
|
(e: 'cursor-activity', payload: { pos: number; lineFrom: number; lineTo: number; lineText: string; cursorCol: number }): void
|
|
@@ -124,19 +145,29 @@ const emit = defineEmits<{
|
|
|
124
145
|
}>();
|
|
125
146
|
|
|
126
147
|
const validatedShowHeader = computed(() => validateBoolean('SCodeBlock', 'showHeader', props.showHeader));
|
|
148
|
+
const validatedShowLineNumbers = computed(() => validateBoolean('SCodeBlock', 'showLineNumbers', props.showLineNumbers));
|
|
127
149
|
const validatedReadOnly = computed(() => validateBoolean('SCodeBlock', 'readOnly', props.readOnly));
|
|
150
|
+
const validatedPresentation = computed(() => validateExactString(
|
|
151
|
+
'SCodeBlock', 'presentation', props.presentation, ['editor', 'plain'] as const,
|
|
152
|
+
));
|
|
128
153
|
const validatedSurfaceRadius = computed(() => validateExactString(
|
|
129
154
|
'SCodeBlock', 'surfaceRadius', props.surfaceRadius, ['default', 'none', 'xl'] as const,
|
|
130
155
|
));
|
|
131
156
|
const validatedViewport = computed(() => validateCodeEditorViewport('SCodeBlock', props.viewport));
|
|
132
157
|
const validatedLanguage = computed(() => validateCodeEditorLanguage('SCodeBlock', props.language));
|
|
158
|
+
const validatedCode = computed(() => {
|
|
159
|
+
if (typeof props.code !== 'string') throw new TypeError('SCodeBlock: code должен быть string. / SCodeBlock: code must be a string.');
|
|
160
|
+
return props.code;
|
|
161
|
+
});
|
|
162
|
+
const validatedTabSize = computed(() => validateBoundedInteger('SCodeBlock', 'tabSize', props.tabSize, { minimum: 1, maximum: 16 }));
|
|
133
163
|
const resolvedLanguageId = computed(() => resolveCodeLanguageKey(validatedLanguage.value));
|
|
164
|
+
const plainLines = computed(() => validatedCode.value.split('\n'));
|
|
134
165
|
const validatedErrorMessage = computed(() => props.errorMessage === undefined
|
|
135
166
|
? undefined
|
|
136
167
|
: validateNonEmptyString('SCodeBlock', 'errorMessage', props.errorMessage));
|
|
137
168
|
const editorCapabilities = computed<SCodeEditorCapabilities>(() => ({
|
|
138
|
-
lineNumbers:
|
|
139
|
-
indentation: { tabSize:
|
|
169
|
+
lineNumbers: validatedShowLineNumbers.value,
|
|
170
|
+
indentation: { tabSize: validatedTabSize.value, keyBehavior: props.tabKeyBehavior },
|
|
140
171
|
...(props.interceptKeys === undefined ? {} : { keyInterceptor: props.interceptKeys }),
|
|
141
172
|
cursorActivity: 'always',
|
|
142
173
|
}));
|
|
@@ -154,13 +185,68 @@ const onReady = (payload: SCodeEditorReadyPayload): void => {
|
|
|
154
185
|
emit('ready', payload);
|
|
155
186
|
};
|
|
156
187
|
|
|
188
|
+
let mounted = false;
|
|
189
|
+
const emitPlainReady = (): void => {
|
|
190
|
+
isReady.value = true;
|
|
191
|
+
emit('ready', {
|
|
192
|
+
documentLength: validatedCode.value.length,
|
|
193
|
+
viewport: validatedViewport.value,
|
|
194
|
+
variant: 'code',
|
|
195
|
+
locked: true,
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const syncPresentation = (): void => {
|
|
200
|
+
if (!mounted) return;
|
|
201
|
+
if (validatedPresentation.value === 'plain') emitPlainReady();
|
|
202
|
+
else isReady.value = false;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
onMounted(() => {
|
|
206
|
+
mounted = true;
|
|
207
|
+
syncPresentation();
|
|
208
|
+
});
|
|
209
|
+
watch(validatedPresentation, syncPresentation);
|
|
210
|
+
|
|
211
|
+
watch(
|
|
212
|
+
[validatedPresentation, validatedReadOnly],
|
|
213
|
+
([presentation, readOnly]) => {
|
|
214
|
+
if (presentation === 'plain' && !readOnly) {
|
|
215
|
+
throw new TypeError('SCodeBlock: presentation="plain" поддерживает только readOnly=true. / SCodeBlock: presentation="plain" supports readOnly=true only.');
|
|
216
|
+
}
|
|
217
|
+
if (presentation === 'plain' && props.interceptKeys !== undefined) {
|
|
218
|
+
throw new TypeError('SCodeBlock: presentation="plain" не принимает interceptKeys. / SCodeBlock: presentation="plain" does not accept interceptKeys.');
|
|
219
|
+
}
|
|
220
|
+
if (presentation === 'plain' && props.nextSelectionPos !== null) {
|
|
221
|
+
throw new TypeError('SCodeBlock: presentation="plain" не принимает nextSelectionPos. / SCodeBlock: presentation="plain" does not accept nextSelectionPos.');
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{ immediate: true },
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const setPlainSurface = (element: Element | ComponentPublicInstance | null): void => {
|
|
228
|
+
if (element !== null && !(element instanceof HTMLElement)) {
|
|
229
|
+
throw new TypeError('SCodeBlock: plain surface ref должен быть HTMLElement. / SCodeBlock: plain surface ref must be an HTMLElement.');
|
|
230
|
+
}
|
|
231
|
+
plainSurface.value = element;
|
|
232
|
+
};
|
|
233
|
+
|
|
157
234
|
const applySelection = (position: number): void => {
|
|
235
|
+
if (validatedPresentation.value === 'plain') {
|
|
236
|
+
throw new Error('SCodeBlock: applySelection недоступен в presentation="plain". / SCodeBlock: applySelection is unavailable in presentation="plain".');
|
|
237
|
+
}
|
|
158
238
|
const editor = codeEditor.value;
|
|
159
239
|
if (!editor) throw new Error('SCodeBlock: editor не готов; дождитесь ready. / SCodeBlock: editor is not ready; wait for ready.');
|
|
160
240
|
editor.applySelection(position);
|
|
161
241
|
};
|
|
162
242
|
|
|
163
243
|
const focus = (): void => {
|
|
244
|
+
if (validatedPresentation.value === 'plain') {
|
|
245
|
+
const surface = plainSurface.value;
|
|
246
|
+
if (!surface) throw new Error('SCodeBlock: plain surface не готов; дождитесь ready. / SCodeBlock: plain surface is not ready; wait for ready.');
|
|
247
|
+
surface.focus();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
164
250
|
const editor = codeEditor.value;
|
|
165
251
|
if (!editor) throw new Error('SCodeBlock: editor не готов; дождитесь ready. / SCodeBlock: editor is not ready; wait for ready.');
|
|
166
252
|
editor.focus();
|
|
@@ -108,6 +108,7 @@ watchEffect(() => {
|
|
|
108
108
|
<SProgressBar :accessibility="progressAccessibility" :value="resolvedProgressValue" :severity="progressSeverity" />
|
|
109
109
|
<div class="s-metric-card__progress-label">{{ resolvedProgressLabel }}</div>
|
|
110
110
|
</div>
|
|
111
|
+
<div v-if="$slots.chart" class="s-metric-card__chart"><slot name="chart" /></div>
|
|
111
112
|
<slot />
|
|
112
113
|
</div>
|
|
113
114
|
<template v-if="$slots.actions" #actions><slot name="actions" /></template>
|
|
@@ -96,34 +96,34 @@
|
|
|
96
96
|
/>
|
|
97
97
|
<template v-else>
|
|
98
98
|
<template
|
|
99
|
-
v-for="
|
|
100
|
-
:key="
|
|
99
|
+
v-for="entry in validatedRows"
|
|
100
|
+
:key="rowKeyToken(entry.key)"
|
|
101
101
|
>
|
|
102
102
|
<tr
|
|
103
|
-
:data-state="resolveRowState(row)"
|
|
104
|
-
:data-selected="rowIsSelected(
|
|
105
|
-
:aria-selected="rowAriaSelected(
|
|
103
|
+
:data-state="resolveRowState(entry.row)"
|
|
104
|
+
:data-selected="rowIsSelected(entry) || undefined"
|
|
105
|
+
:aria-selected="rowAriaSelected(entry)"
|
|
106
106
|
tabindex="0"
|
|
107
|
-
@click="onRowClick($event, row,
|
|
108
|
-
@contextmenu="onRowContextMenu($event, row,
|
|
109
|
-
@keydown.enter="onRowKeydown($event, row,
|
|
110
|
-
@keydown.space="onRowKeydown($event, row,
|
|
107
|
+
@click="onRowClick($event, entry.row, entry.index)"
|
|
108
|
+
@contextmenu="onRowContextMenu($event, entry.row, entry.index)"
|
|
109
|
+
@keydown.enter="onRowKeydown($event, entry.row, entry.index)"
|
|
110
|
+
@keydown.space="onRowKeydown($event, entry.row, entry.index)"
|
|
111
111
|
>
|
|
112
112
|
<td
|
|
113
113
|
v-if="validatedSelectionMode === 'multiple'"
|
|
114
114
|
class="s-table__control-cell"
|
|
115
115
|
>
|
|
116
116
|
<SCheckbox
|
|
117
|
-
:model-value="rowIsSelected(
|
|
118
|
-
:disabled="!
|
|
119
|
-
:aria-label="rowSelectionLabel(
|
|
120
|
-
@update:model-value="updateRowSelection(
|
|
117
|
+
:model-value="rowIsSelected(entry)"
|
|
118
|
+
:disabled="!entry.selectable"
|
|
119
|
+
:aria-label="rowSelectionLabel(entry)"
|
|
120
|
+
@update:model-value="updateRowSelection(entry, $event)"
|
|
121
121
|
/>
|
|
122
122
|
</td>
|
|
123
123
|
<td v-if="hasRowDetails" class="s-table__control-cell">
|
|
124
124
|
<SButton
|
|
125
|
-
v-if="
|
|
126
|
-
:aria-label="rowExpansionLabel(
|
|
125
|
+
v-if="entry.expandable"
|
|
126
|
+
:aria-label="rowExpansionLabel(entry)"
|
|
127
127
|
appearance="text"
|
|
128
128
|
severity="secondary"
|
|
129
129
|
size="xs"
|
|
@@ -131,47 +131,47 @@
|
|
|
131
131
|
shape="square"
|
|
132
132
|
icon-only
|
|
133
133
|
:disclosure="{
|
|
134
|
-
expanded: rowIsExpanded(
|
|
135
|
-
controls: rowDetailsId(
|
|
134
|
+
expanded: rowIsExpanded(entry),
|
|
135
|
+
controls: rowDetailsId(entry.key),
|
|
136
136
|
iconPlacement: 'leading',
|
|
137
137
|
}"
|
|
138
|
-
@click="toggleRowExpansion(
|
|
138
|
+
@click="toggleRowExpansion(entry)"
|
|
139
139
|
/>
|
|
140
140
|
</td>
|
|
141
141
|
<td
|
|
142
142
|
v-for="column in validatedColumns"
|
|
143
143
|
:key="column.field"
|
|
144
144
|
:data-align="column.align ?? 'left'"
|
|
145
|
-
:data-state="resolveCellState(row, column)"
|
|
145
|
+
:data-state="resolveCellState(entry.row, column)"
|
|
146
146
|
>
|
|
147
147
|
<slot
|
|
148
148
|
:name="`cell-${column.field}`"
|
|
149
|
-
:value="resolveCellValue(row, column)"
|
|
150
|
-
:row="row"
|
|
149
|
+
:value="resolveCellValue(entry.row, column)"
|
|
150
|
+
:row="entry.row"
|
|
151
151
|
:column="column"
|
|
152
|
-
:row-index="
|
|
152
|
+
:row-index="entry.index"
|
|
153
153
|
>
|
|
154
154
|
<slot
|
|
155
155
|
name="cell"
|
|
156
|
-
:value="resolveCellValue(row, column)"
|
|
157
|
-
:row="row"
|
|
156
|
+
:value="resolveCellValue(entry.row, column)"
|
|
157
|
+
:row="entry.row"
|
|
158
158
|
:column="column"
|
|
159
|
-
:row-index="
|
|
159
|
+
:row-index="entry.index"
|
|
160
160
|
>
|
|
161
|
-
{{ resolveCellValue(row, column) }}
|
|
161
|
+
{{ resolveCellValue(entry.row, column) }}
|
|
162
162
|
</slot>
|
|
163
163
|
</slot>
|
|
164
164
|
</td>
|
|
165
165
|
</tr>
|
|
166
|
-
<tr v-if="
|
|
166
|
+
<tr v-if="entry.expandable && rowIsExpanded(entry)" data-row-details>
|
|
167
167
|
<td :colspan="renderedColumnCount">
|
|
168
168
|
<div
|
|
169
|
-
:id="rowDetailsId(
|
|
169
|
+
:id="rowDetailsId(entry.key)"
|
|
170
170
|
class="s-table__row-details"
|
|
171
171
|
role="region"
|
|
172
|
-
:aria-label="`Детали: ${resolveRowLabel(row,
|
|
172
|
+
:aria-label="`Детали: ${resolveRowLabel(entry.row, entry.index)}`"
|
|
173
173
|
>
|
|
174
|
-
<slot name="row-details" :row="row" :row-index="
|
|
174
|
+
<slot name="row-details" :row="entry.row" :row-index="entry.index" />
|
|
175
175
|
</div>
|
|
176
176
|
</td>
|
|
177
177
|
</tr>
|
|
@@ -664,7 +664,7 @@ function validateRowKey(value: unknown, coordinate: string): RowKey {
|
|
|
664
664
|
)
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
-
|
|
667
|
+
function resolveRowKey(row: Row, rowIndex: number): RowKey {
|
|
668
668
|
if (typeof props.getRowKey !== 'function') {
|
|
669
669
|
throw new TypeError(
|
|
670
670
|
`${diagnosticOwner()}: getRowKey должен быть обязательным function resolver. `
|
|
@@ -672,31 +672,16 @@ const resolvedRowKeys = computed<ReadonlyMap<Row, RowKey>>(() => {
|
|
|
672
672
|
)
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
throw new TypeError(
|
|
683
|
-
`${diagnosticOwner()}: getRowKey failed at data[${rowIndex}]: ${describeThrownError(error)}.`,
|
|
684
|
-
)
|
|
685
|
-
}
|
|
686
|
-
const key = validateRowKey(value, `getRowKey at data[${rowIndex}]`)
|
|
687
|
-
const firstIndex = firstIndexByKey.get(key)
|
|
688
|
-
if (firstIndex !== undefined) {
|
|
689
|
-
throw new TypeError(
|
|
690
|
-
`${diagnosticOwner()}: getRowKey вернул duplicate key ${describeRowKey(key)} at data[${rowIndex}]; `
|
|
691
|
-
+ `first returned at data[${firstIndex}]. / ${diagnosticOwner()}: getRowKey returned duplicate key `
|
|
692
|
-
+ `${describeRowKey(key)} at data[${rowIndex}]; first returned at data[${firstIndex}].`,
|
|
693
|
-
)
|
|
694
|
-
}
|
|
695
|
-
firstIndexByKey.set(key, rowIndex)
|
|
696
|
-
rowKeys.set(row, key)
|
|
675
|
+
let value: unknown
|
|
676
|
+
try {
|
|
677
|
+
value = props.getRowKey(row)
|
|
678
|
+
} catch (error: unknown) {
|
|
679
|
+
throw new TypeError(
|
|
680
|
+
`${diagnosticOwner()}: getRowKey failed at data[${rowIndex}]: ${describeThrownError(error)}.`,
|
|
681
|
+
)
|
|
697
682
|
}
|
|
698
|
-
return
|
|
699
|
-
}
|
|
683
|
+
return validateRowKey(value, `getRowKey at data[${rowIndex}]`)
|
|
684
|
+
}
|
|
700
685
|
|
|
701
686
|
function validateControlledKeys(coordinate: string, value: unknown): readonly RowKey[] {
|
|
702
687
|
if (!Array.isArray(value)) {
|
|
@@ -729,15 +714,7 @@ const validatedSelectedRowKey = computed<RowKey | undefined>(() => {
|
|
|
729
714
|
+ `/ ${diagnosticOwner()}: selectedRowKey is incompatible with selectionMode="multiple"; use selectedRowKeys.`,
|
|
730
715
|
)
|
|
731
716
|
}
|
|
732
|
-
|
|
733
|
-
const rowKeys = new Set(resolvedRowKeys.value.values())
|
|
734
|
-
if (!rowKeys.has(selectedKey)) {
|
|
735
|
-
throw new TypeError(
|
|
736
|
-
`${diagnosticOwner()}: selectedRowKey ${describeRowKey(selectedKey)} отсутствует в resolved row keys. `
|
|
737
|
-
+ `/ ${diagnosticOwner()}: selectedRowKey ${describeRowKey(selectedKey)} is absent from resolved row keys.`,
|
|
738
|
-
)
|
|
739
|
-
}
|
|
740
|
-
return selectedKey
|
|
717
|
+
return validateRowKey(props.selectedRowKey, 'selectedRowKey')
|
|
741
718
|
})
|
|
742
719
|
|
|
743
720
|
const validatedSelectedRowKeys = computed<readonly RowKey[]>(() => {
|
|
@@ -760,17 +737,6 @@ const validatedSelectedRowKeys = computed<readonly RowKey[]>(() => {
|
|
|
760
737
|
return keys
|
|
761
738
|
})
|
|
762
739
|
|
|
763
|
-
const rowSelectableByRow = computed<ReadonlyMap<Row, boolean>>(() => {
|
|
764
|
-
const result = new Map<Row, boolean>()
|
|
765
|
-
for (const [index, row] of props.data.entries()) {
|
|
766
|
-
const selectable = validatedSelectionMode.value === 'multiple'
|
|
767
|
-
? validateBoolean('STable', `isRowSelectable at data[${index}]`, props.isRowSelectable?.(row, index) ?? true)
|
|
768
|
-
: false
|
|
769
|
-
result.set(row, selectable)
|
|
770
|
-
}
|
|
771
|
-
return result
|
|
772
|
-
})
|
|
773
|
-
|
|
774
740
|
const validatedExpandedRowKeys = computed<readonly RowKey[]>(() => {
|
|
775
741
|
const keys = validateControlledKeys('expandedRowKeys', props.expandedRowKeys)
|
|
776
742
|
if (!hasRowDetails && (keys.length > 0 || props.isRowExpandable !== undefined)) {
|
|
@@ -785,36 +751,32 @@ const validatedExpandedRowKeys = computed<readonly RowKey[]>(() => {
|
|
|
785
751
|
return keys
|
|
786
752
|
})
|
|
787
753
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
const expanded = new Set(validatedExpandedRowKeys.value)
|
|
797
|
-
for (const row of props.data) {
|
|
798
|
-
if (expanded.has(rowKey(row)) && !result.get(row)) {
|
|
799
|
-
throw new TypeError(
|
|
800
|
-
`${diagnosticOwner()}: expandedRowKeys содержит current row, для которой isRowExpandable вернул false. `
|
|
801
|
-
+ `/ ${diagnosticOwner()}: expandedRowKeys contains a current row for which isRowExpandable returned false.`,
|
|
802
|
-
)
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
return result
|
|
806
|
-
})
|
|
754
|
+
interface ResolvedRowEntry {
|
|
755
|
+
readonly row: Row
|
|
756
|
+
readonly key: RowKey
|
|
757
|
+
readonly index: number
|
|
758
|
+
readonly selectable: boolean
|
|
759
|
+
readonly expandable: boolean
|
|
760
|
+
}
|
|
807
761
|
|
|
808
|
-
const
|
|
762
|
+
const resolvedRowEntries = computed<readonly ResolvedRowEntry[]>(() => {
|
|
809
763
|
const columns = validatedColumns.value
|
|
810
764
|
void activeSorts.value
|
|
811
|
-
void resolvedRowKeys.value
|
|
812
|
-
void validatedSelectedRowKey.value
|
|
813
765
|
void validatedSelectedRowKeys.value
|
|
814
|
-
void rowSelectableByRow.value
|
|
815
766
|
void validatedExpandedRowKeys.value
|
|
816
|
-
|
|
817
|
-
|
|
767
|
+
const firstIndexByKey = new Map<STableRowKey, number>()
|
|
768
|
+
const entries: ResolvedRowEntry[] = []
|
|
769
|
+
for (const [index, row] of props.data.entries()) {
|
|
770
|
+
const key = resolveRowKey(row, index)
|
|
771
|
+
const firstIndex = firstIndexByKey.get(key)
|
|
772
|
+
if (firstIndex !== undefined) {
|
|
773
|
+
throw new TypeError(
|
|
774
|
+
`${diagnosticOwner()}: getRowKey вернул duplicate key ${describeRowKey(key)} at data[${index}]; `
|
|
775
|
+
+ `first returned at data[${firstIndex}]. / ${diagnosticOwner()}: getRowKey returned duplicate key `
|
|
776
|
+
+ `${describeRowKey(key)} at data[${index}]; first returned at data[${firstIndex}].`,
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
firstIndexByKey.set(key, index)
|
|
818
780
|
for (const column of columns) {
|
|
819
781
|
const content = column.content ?? (column.value === undefined ? 'field' : 'value')
|
|
820
782
|
if (content === 'field' && !hasOwn(row, column.field)) {
|
|
@@ -824,26 +786,48 @@ const validatedRows = computed<readonly Row[]>(() => {
|
|
|
824
786
|
)
|
|
825
787
|
}
|
|
826
788
|
}
|
|
827
|
-
|
|
828
|
-
|
|
789
|
+
entries.push({
|
|
790
|
+
row,
|
|
791
|
+
key,
|
|
792
|
+
index,
|
|
793
|
+
selectable: validatedSelectionMode.value === 'multiple'
|
|
794
|
+
? validateBoolean('STable', `isRowSelectable at data[${index}]`, props.isRowSelectable?.(row, index) ?? true)
|
|
795
|
+
: false,
|
|
796
|
+
expandable: hasRowDetails
|
|
797
|
+
? validateBoolean('STable', `isRowExpandable at data[${index}]`, props.isRowExpandable?.(row, index) ?? true)
|
|
798
|
+
: false,
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
return entries
|
|
829
802
|
})
|
|
830
803
|
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
804
|
+
const validatedRows = computed<readonly ResolvedRowEntry[]>(() => {
|
|
805
|
+
const entries = resolvedRowEntries.value
|
|
806
|
+
const selectedKey = validatedSelectedRowKey.value
|
|
807
|
+
if (selectedKey !== undefined && !entries.some((entry) => entry.key === selectedKey)) {
|
|
808
|
+
throw new TypeError(
|
|
809
|
+
`${diagnosticOwner()}: selectedRowKey ${describeRowKey(selectedKey)} отсутствует в resolved row keys. `
|
|
810
|
+
+ `/ ${diagnosticOwner()}: selectedRowKey ${describeRowKey(selectedKey)} is absent from resolved row keys.`,
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
const expanded = new Set(validatedExpandedRowKeys.value)
|
|
814
|
+
for (const entry of entries) {
|
|
815
|
+
if (expanded.has(entry.key) && !entry.expandable) {
|
|
816
|
+
throw new TypeError(
|
|
817
|
+
`${diagnosticOwner()}: expandedRowKeys содержит current row, для которой isRowExpandable вернул false. `
|
|
818
|
+
+ `/ ${diagnosticOwner()}: expandedRowKeys contains a current row for which isRowExpandable returned false.`,
|
|
819
|
+
)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return entries
|
|
823
|
+
})
|
|
836
824
|
|
|
837
825
|
function rowKeyToken(key: RowKey): string {
|
|
838
826
|
return `${typeof key === 'number' ? 'number' : 'string'}:${String(key)}`
|
|
839
827
|
}
|
|
840
828
|
|
|
841
|
-
function
|
|
842
|
-
return rowKeyToken(
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function rowDetailsId(row: Row): string {
|
|
846
|
-
return `${tableInstanceId}-details-${encodeURIComponent(rowDomToken(row))}`
|
|
829
|
+
function rowDetailsId(key: RowKey): string {
|
|
830
|
+
return `${tableInstanceId}-details-${encodeURIComponent(rowKeyToken(key))}`
|
|
847
831
|
}
|
|
848
832
|
|
|
849
833
|
function resolveRowLabel(row: Row, index: number): string {
|
|
@@ -856,9 +840,9 @@ function resolveRowLabel(row: Row, index: number): string {
|
|
|
856
840
|
|
|
857
841
|
const selectedKeySet = computed(() => new Set(validatedSelectedRowKeys.value))
|
|
858
842
|
const expandedKeySet = computed(() => new Set(validatedExpandedRowKeys.value))
|
|
859
|
-
const selectableRows = computed(() =>
|
|
843
|
+
const selectableRows = computed(() => validatedRows.value.filter((entry) => entry.selectable))
|
|
860
844
|
const selectedCurrentRowCount = computed(() => selectableRows.value.filter(
|
|
861
|
-
(
|
|
845
|
+
(entry) => selectedKeySet.value.has(entry.key),
|
|
862
846
|
).length)
|
|
863
847
|
const someCurrentRowsSelected = computed(() => selectedCurrentRowCount.value > 0)
|
|
864
848
|
const allCurrentRowsSelected = computed(() => (
|
|
@@ -871,29 +855,24 @@ const renderedColumnCount = computed(() => Math.max(
|
|
|
871
855
|
1,
|
|
872
856
|
))
|
|
873
857
|
|
|
874
|
-
function
|
|
875
|
-
return
|
|
858
|
+
function rowIsSelected(entry: ResolvedRowEntry): boolean {
|
|
859
|
+
return validatedSelectionMode.value === 'multiple' && selectedKeySet.value.has(entry.key)
|
|
876
860
|
}
|
|
877
861
|
|
|
878
|
-
function
|
|
879
|
-
|
|
862
|
+
function rowAriaSelected(entry: ResolvedRowEntry): 'true' | 'false' | undefined {
|
|
863
|
+
if (validatedSelectionMode.value === 'multiple') return rowIsSelected(entry) ? 'true' : 'false'
|
|
864
|
+
return entry.key === validatedSelectedRowKey.value ? 'true' : undefined
|
|
880
865
|
}
|
|
881
866
|
|
|
882
|
-
function
|
|
883
|
-
|
|
884
|
-
|
|
867
|
+
function rowSelectionLabel(entry: ResolvedRowEntry): string {
|
|
868
|
+
const label = resolveRowLabel(entry.row, entry.index)
|
|
869
|
+
if (!entry.selectable) return `Выбор недоступен: ${label}`
|
|
870
|
+
return rowIsSelected(entry) ? `Снять выбор: ${label}` : `Выбрать: ${label}`
|
|
885
871
|
}
|
|
886
872
|
|
|
887
|
-
function
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
return rowIsSelected(row) ? `Снять выбор: ${label}` : `Выбрать: ${label}`
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function updateRowSelection(row: Row, index: number, selected: boolean): void {
|
|
894
|
-
void index
|
|
895
|
-
if (!rowIsSelectable(row)) return
|
|
896
|
-
const key = rowKey(row)
|
|
873
|
+
function updateRowSelection(entry: ResolvedRowEntry, selected: boolean): void {
|
|
874
|
+
if (!entry.selectable) return
|
|
875
|
+
const key = entry.key
|
|
897
876
|
const current = validatedSelectedRowKeys.value
|
|
898
877
|
if (selected && !selectedKeySet.value.has(key)) emit('update:selectedRowKeys', [...current, key])
|
|
899
878
|
if (!selected && selectedKeySet.value.has(key)) {
|
|
@@ -903,7 +882,7 @@ function updateRowSelection(row: Row, index: number, selected: boolean): void {
|
|
|
903
882
|
|
|
904
883
|
function updateAllRowsSelection(selected: boolean): void {
|
|
905
884
|
const current = validatedSelectedRowKeys.value
|
|
906
|
-
const currentSelectableKeys = new Set(selectableRows.value.map((
|
|
885
|
+
const currentSelectableKeys = new Set(selectableRows.value.map((entry) => entry.key))
|
|
907
886
|
if (!selected) {
|
|
908
887
|
emit('update:selectedRowKeys', current.filter((key) => !currentSelectableKeys.has(key)))
|
|
909
888
|
return
|
|
@@ -916,27 +895,23 @@ function updateAllRowsSelection(selected: boolean): void {
|
|
|
916
895
|
emit('update:selectedRowKeys', next)
|
|
917
896
|
}
|
|
918
897
|
|
|
919
|
-
function
|
|
920
|
-
return
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function rowIsExpanded(row: Row): boolean {
|
|
924
|
-
return expandedKeySet.value.has(rowKey(row))
|
|
898
|
+
function rowIsExpanded(entry: ResolvedRowEntry): boolean {
|
|
899
|
+
return expandedKeySet.value.has(entry.key)
|
|
925
900
|
}
|
|
926
901
|
|
|
927
|
-
function rowExpansionLabel(
|
|
928
|
-
return `${rowIsExpanded(
|
|
902
|
+
function rowExpansionLabel(entry: ResolvedRowEntry): string {
|
|
903
|
+
return `${rowIsExpanded(entry) ? 'Свернуть' : 'Раскрыть'}: ${resolveRowLabel(entry.row, entry.index)}`
|
|
929
904
|
}
|
|
930
905
|
|
|
931
|
-
function toggleRowExpansion(
|
|
932
|
-
if (!
|
|
933
|
-
const key =
|
|
906
|
+
function toggleRowExpansion(entry: ResolvedRowEntry): void {
|
|
907
|
+
if (!entry.expandable) return
|
|
908
|
+
const key = entry.key
|
|
934
909
|
const expanded = !expandedKeySet.value.has(key)
|
|
935
910
|
const next = expanded
|
|
936
911
|
? [...validatedExpandedRowKeys.value, key]
|
|
937
912
|
: validatedExpandedRowKeys.value.filter((candidate) => candidate !== key)
|
|
938
913
|
emit('update:expandedRowKeys', next)
|
|
939
|
-
emit('row-expand', row, index, expanded)
|
|
914
|
+
emit('row-expand', entry.row, entry.index, expanded)
|
|
940
915
|
}
|
|
941
916
|
|
|
942
917
|
const resolveCellValue = (row: Row, column: STableColumn<Row, Field>): unknown => column.value
|