@pgcorp/ui-kit 0.8.1 → 0.10.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 +332 -9
- package/docs/getting-started.md +135 -0
- package/docs/public-api.md +53 -4
- package/docs/rich-document.md +216 -0
- package/docs/scroll-viewport.md +167 -0
- package/docs/theming.md +17 -0
- package/package.json +108 -50
- package/src/components/layout/SAppShell.vue +3 -1
- package/src/components/layout/SDockRegion.vue +5 -3
- package/src/components/layout/SResizeHandle.vue +45 -5
- package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
- package/src/components/shared/_internal/SDialogSurface.css +23 -0
- package/src/components/shared/_internal/SDialogSurface.vue +9 -2
- package/src/components/shared/_internal/SToastItem.css +4 -0
- package/src/components/shared/_internal/SToastItem.vue +44 -8
- package/src/components/shared/_internal/STreeNode.vue +24 -11
- package/src/components/shared/_internal/dialogSurface.ts +7 -1
- package/src/components/shared/complex/SKanbanLane.vue +4 -2
- package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
- package/src/components/shared/containers/SDrawer.css +11 -2
- package/src/components/shared/containers/SDrawer.vue +48 -9
- package/src/components/shared/containers/SModal.vue +5 -2
- package/src/components/shared/containers/SPanel.vue +4 -2
- package/src/components/shared/containers/SPopover.vue +7 -3
- package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
- package/src/components/shared/controls/SButton.css +14 -10
- package/src/components/shared/controls/SCombobox.vue +8 -6
- package/src/components/shared/controls/SComboboxTrigger.css +7 -4
- package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
- package/src/components/shared/controls/SContextToggleButton.vue +6 -4
- package/src/components/shared/controls/SDragHandle.vue +8 -4
- package/src/components/shared/controls/SDropdownMenu.vue +7 -4
- package/src/components/shared/controls/SInputText.css +5 -4
- package/src/components/shared/controls/SListbox.vue +4 -2
- package/src/components/shared/controls/SSelect.vue +4 -2
- package/src/components/shared/controls/STextarea.ts +13 -0
- package/src/components/shared/controls/STextarea.vue +391 -37
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
- package/src/components/shared/data-display/SChart.vue +7 -5
- package/src/components/shared/data-display/SChip.vue +3 -1
- package/src/components/shared/data-display/SCodeBlock.css +4 -0
- package/src/components/shared/data-display/SCodeBlock.vue +46 -22
- package/src/components/shared/data-display/SCodeEditor.css +17 -0
- package/src/components/shared/data-display/SCodeEditor.vue +173 -8
- package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
- package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
- package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
- package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
- package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
- package/src/components/shared/data-display/SCopyField.vue +12 -10
- package/src/components/shared/data-display/SDiffViewer.css +122 -0
- package/src/components/shared/data-display/SDiffViewer.vue +499 -0
- package/src/components/shared/data-display/SDocBlock.vue +99 -71
- package/src/components/shared/data-display/SExpandableText.vue +10 -8
- package/src/components/shared/data-display/SJsonTree.vue +4 -2
- package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
- package/src/components/shared/data-display/SMessage.vue +6 -6
- package/src/components/shared/data-display/SRichDocument.css +8 -0
- package/src/components/shared/data-display/SRichDocument.vue +65 -0
- package/src/components/shared/data-display/SScrollViewport.css +53 -0
- package/src/components/shared/data-display/SScrollViewport.vue +470 -0
- package/src/components/shared/data-display/STable.vue +38 -19
- package/src/components/shared/data-display/SToastContainer.vue +22 -17
- package/src/components/shared/data-display/SVirtualList.ts +33 -0
- package/src/components/shared/data-display/SVirtualList.vue +321 -36
- package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
- package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
- package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
- package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
- package/src/components/shared/data-display/codeEditor.ts +200 -0
- package/src/components/shared/data-display/codeWorkspace.ts +233 -0
- package/src/components/shared/data-display/diff.ts +541 -0
- package/src/components/shared/data-display/richDocument.ts +876 -0
- package/src/components/shared/data-display/scrollViewport.ts +47 -0
- package/src/components/shared/data-display/toast.ts +194 -0
- package/src/components/shared/database/SDataGrid.vue +7 -4
- package/src/components/shared/database/SSqlEditor.vue +39 -39
- package/src/components/shared/feedback/SAsyncState.vue +3 -1
- package/src/components/shared/graph/SGraphViewport.vue +11 -9
- package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
- package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
- package/src/components/shared/navigation/STabList.vue +10 -6
- package/src/components/shared/navigation/STabs.vue +15 -11
- package/src/components/shared/navigation/SWizardSteps.vue +4 -2
- package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
- package/src/components/shared/persona/_internal/labels.ts +66 -57
- package/src/components/shared/persona/presentation.ts +140 -110
- package/src/composables/useFloatingPosition.ts +39 -2
- package/src/composables/useUiKitLocale.ts +17 -0
- package/src/internal/codeHighlight.ts +110 -0
- package/src/internal/diffEngine.ts +464 -0
- package/src/internal/diffRuntime.ts +189 -0
- package/src/internal/feedbackPresentation.ts +33 -14
- package/src/internal/localeContext.ts +22 -0
- package/src/internal/ownedAttrs.ts +8 -0
- package/src/internal/runtimeContract.ts +11 -9
- package/src/internal/semanticSizing.ts +13 -0
- package/src/locales/catalog.ts +634 -0
- package/src/locales/locale.ts +472 -0
- package/src/locales/messages.generated.ts +1826 -0
- package/src/locales/messages.ts +150 -0
- package/src/stores/useNotifierStore.ts +61 -44
- package/src/styles/tokens.css +23 -2
- package/src/theme.ts +59 -32
- package/src/workers/diff.worker.ts +73 -0
- package/src/workers/diffProtocol.ts +155 -0
- package/src/components/shared/data-display/SToastContainer.css +0 -53
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
<script setup lang="ts">
|
|
42
42
|
import { computed, type Component } from 'vue'
|
|
43
43
|
import { X } from '../../icons/sputnigUiIcons'
|
|
44
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale'
|
|
44
45
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
45
46
|
import {
|
|
46
47
|
PILL_ICON_CLASS,
|
|
@@ -89,6 +90,7 @@ const emit = defineEmits<{
|
|
|
89
90
|
activate: [event: MouseEvent]
|
|
90
91
|
}>()
|
|
91
92
|
const ownedAttrs = useOwnedAttrs({ component: 'SChip', owner: 'chip root' })
|
|
93
|
+
const { t } = useUiKitLocale()
|
|
92
94
|
|
|
93
95
|
const validatedLabel = computed(() => validateNonEmptyString('SChip', 'label', props.label))
|
|
94
96
|
const validatedSeverity = computed(() => validateExactString(
|
|
@@ -120,7 +122,7 @@ const resolvedCloseLabel = computed(() => {
|
|
|
120
122
|
validateBoolean('SChip', 'closable', props.closable)
|
|
121
123
|
validateBoolean('SChip', 'disabled', props.disabled)
|
|
122
124
|
return props.closeLabel === undefined
|
|
123
|
-
?
|
|
125
|
+
? t('actions.removeChip', { label: validatedLabel.value })
|
|
124
126
|
: validateNonEmptyString('SChip', 'closeLabel', props.closeLabel)
|
|
125
127
|
})
|
|
126
128
|
</script>
|
|
@@ -111,6 +111,10 @@
|
|
|
111
111
|
grid-template-columns: max-content minmax(0, 1fr);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
.s-code-block-plain__line[data-line-numbers='hidden'] {
|
|
115
|
+
display: block;
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
.s-code-block-plain__line-number {
|
|
115
119
|
min-width: 2.5rem;
|
|
116
120
|
padding-inline-end: 0.75rem;
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
size="xs"
|
|
23
23
|
:leading-icon="Copy"
|
|
24
24
|
icon-only
|
|
25
|
-
title="
|
|
26
|
-
aria-label="
|
|
25
|
+
:title="t('clipboard.copyCode')"
|
|
26
|
+
:aria-label="t('clipboard.copyCode')"
|
|
27
27
|
label=""
|
|
28
28
|
@click="copyCode"
|
|
29
29
|
/>
|
|
@@ -38,8 +38,7 @@
|
|
|
38
38
|
:capabilities="editorCapabilities"
|
|
39
39
|
:read-only="validatedReadOnly"
|
|
40
40
|
:viewport="validatedViewport"
|
|
41
|
-
|
|
42
|
-
:aria-labelledby="props.ariaLabelledby"
|
|
41
|
+
v-bind="editorAccessibleName"
|
|
43
42
|
:placeholder="props.placeholder"
|
|
44
43
|
:next-selection-pos="props.nextSelectionPos"
|
|
45
44
|
@update:model-value="emit('update:code', $event)"
|
|
@@ -48,7 +47,7 @@
|
|
|
48
47
|
@ready="onReady"
|
|
49
48
|
/>
|
|
50
49
|
<pre
|
|
51
|
-
v-else
|
|
50
|
+
v-else-if="validatedPresentation === 'plain'"
|
|
52
51
|
:ref="setPlainSurface"
|
|
53
52
|
class="s-code-block-plain"
|
|
54
53
|
:data-viewport="validatedViewport"
|
|
@@ -62,11 +61,30 @@
|
|
|
62
61
|
:key="index"
|
|
63
62
|
class="s-code-block-plain__line"
|
|
64
63
|
><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>
|
|
64
|
+
<pre
|
|
65
|
+
v-else
|
|
66
|
+
:ref="setPlainSurface"
|
|
67
|
+
class="s-code-block-plain"
|
|
68
|
+
:data-viewport="validatedViewport"
|
|
69
|
+
:aria-label="props.ariaLabel"
|
|
70
|
+
:aria-labelledby="props.ariaLabelledby"
|
|
71
|
+
:style="{ tabSize: validatedTabSize }"
|
|
72
|
+
tabindex="0"
|
|
73
|
+
><span class="s-code-block-plain__lines"><span
|
|
74
|
+
v-for="(line, index) in plainLines"
|
|
75
|
+
:key="index"
|
|
76
|
+
class="s-code-block-plain__line"
|
|
77
|
+
:data-line-numbers="validatedShowLineNumbers ? 'visible' : 'hidden'"
|
|
78
|
+
><span v-if="validatedShowLineNumbers" class="s-code-block-plain__line-number" aria-hidden="true">{{ index + 1 }}</span><span class="s-code-block-plain__line-content"><SCodeTokenLine
|
|
79
|
+
:text="line || '\u200b'"
|
|
80
|
+
:language="validatedLanguage"
|
|
81
|
+
variant="code-block"
|
|
82
|
+
/></span></span></span></pre>
|
|
65
83
|
</div>
|
|
66
84
|
<SAsyncState
|
|
67
85
|
v-if="validatedErrorMessage !== undefined || (validatedPresentation === 'editor' && !isReady)"
|
|
68
86
|
:state="validatedErrorMessage === undefined
|
|
69
|
-
? { status: 'loading', message: '
|
|
87
|
+
? { status: 'loading', message: t('editor.loading') }
|
|
70
88
|
: { status: 'error', message: validatedErrorMessage }"
|
|
71
89
|
presentation="overlay"
|
|
72
90
|
/>
|
|
@@ -81,6 +99,7 @@ import { validateCodeEditorLanguage, validateCodeEditorViewport } from '../../..
|
|
|
81
99
|
import { resolveCodeLanguageKey } from '../../../internal/codeLanguageIdentity';
|
|
82
100
|
import { useNotifier } from '../../../composables/useNotifier';
|
|
83
101
|
import { useClipboard } from '../../../composables/useClipboard';
|
|
102
|
+
import { useUiKitLocale } from '../../../composables/useUiKitLocale';
|
|
84
103
|
import SButton from '../controls/SButton.vue';
|
|
85
104
|
import type {
|
|
86
105
|
SCodeEditorCapabilities,
|
|
@@ -92,6 +111,7 @@ import type {
|
|
|
92
111
|
} from './SCodeEditor.vue';
|
|
93
112
|
import { Copy } from '../../icons/sputnigUiIcons';
|
|
94
113
|
import SAsyncState from '../feedback/SAsyncState.vue';
|
|
114
|
+
import SCodeTokenLine from './_internal/SCodeTokenLine.vue';
|
|
95
115
|
|
|
96
116
|
defineOptions({ inheritAttrs: false });
|
|
97
117
|
|
|
@@ -103,7 +123,7 @@ type AccessibleName =
|
|
|
103
123
|
export type Props = AccessibleName & {
|
|
104
124
|
code: string;
|
|
105
125
|
language: SCodeEditorLanguage;
|
|
106
|
-
presentation?: 'editor' | 'plain';
|
|
126
|
+
presentation?: 'editor' | 'plain' | 'highlighted';
|
|
107
127
|
showHeader?: boolean;
|
|
108
128
|
showLineNumbers?: boolean;
|
|
109
129
|
readOnly?: boolean;
|
|
@@ -137,6 +157,9 @@ const codeEditor = ref<InstanceType<typeof import('./SCodeEditor.vue')['default'
|
|
|
137
157
|
const plainSurface = ref<HTMLElement | null>(null);
|
|
138
158
|
const AsyncSCodeEditor = defineAsyncComponent(() => import('../../../internal/lazyCodeEditor'));
|
|
139
159
|
const ownedAttrs = useOwnedAttrs({ component: 'SCodeBlock', owner: 'code block root' });
|
|
160
|
+
const editorAccessibleName = computed<AccessibleName>(() => props.ariaLabel !== undefined
|
|
161
|
+
? { ariaLabel: props.ariaLabel }
|
|
162
|
+
: { ariaLabelledby: props.ariaLabelledby });
|
|
140
163
|
const emit = defineEmits<{
|
|
141
164
|
(e: 'cursor-activity', payload: { pos: number; lineFrom: number; lineTo: number; lineText: string; cursorCol: number }): void
|
|
142
165
|
(e: 'update:code', value: string): void
|
|
@@ -148,7 +171,7 @@ const validatedShowHeader = computed(() => validateBoolean('SCodeBlock', 'showHe
|
|
|
148
171
|
const validatedShowLineNumbers = computed(() => validateBoolean('SCodeBlock', 'showLineNumbers', props.showLineNumbers));
|
|
149
172
|
const validatedReadOnly = computed(() => validateBoolean('SCodeBlock', 'readOnly', props.readOnly));
|
|
150
173
|
const validatedPresentation = computed(() => validateExactString(
|
|
151
|
-
'SCodeBlock', 'presentation', props.presentation, ['editor', 'plain'] as const,
|
|
174
|
+
'SCodeBlock', 'presentation', props.presentation, ['editor', 'plain', 'highlighted'] as const,
|
|
152
175
|
));
|
|
153
176
|
const validatedSurfaceRadius = computed(() => validateExactString(
|
|
154
177
|
'SCodeBlock', 'surfaceRadius', props.surfaceRadius, ['default', 'none', 'xl'] as const,
|
|
@@ -198,7 +221,7 @@ const emitPlainReady = (): void => {
|
|
|
198
221
|
|
|
199
222
|
const syncPresentation = (): void => {
|
|
200
223
|
if (!mounted) return;
|
|
201
|
-
if (validatedPresentation.value
|
|
224
|
+
if (validatedPresentation.value !== 'editor') emitPlainReady();
|
|
202
225
|
else isReady.value = false;
|
|
203
226
|
};
|
|
204
227
|
|
|
@@ -211,14 +234,14 @@ watch(validatedPresentation, syncPresentation);
|
|
|
211
234
|
watch(
|
|
212
235
|
[validatedPresentation, validatedReadOnly],
|
|
213
236
|
([presentation, readOnly]) => {
|
|
214
|
-
if (presentation
|
|
215
|
-
throw new TypeError(
|
|
237
|
+
if (presentation !== 'editor' && !readOnly) {
|
|
238
|
+
throw new TypeError(`SCodeBlock: presentation=${JSON.stringify(presentation)} поддерживает только readOnly=true. / SCodeBlock: presentation=${JSON.stringify(presentation)} supports readOnly=true only.`);
|
|
216
239
|
}
|
|
217
|
-
if (presentation
|
|
218
|
-
throw new TypeError(
|
|
240
|
+
if (presentation !== 'editor' && props.interceptKeys !== undefined) {
|
|
241
|
+
throw new TypeError(`SCodeBlock: presentation=${JSON.stringify(presentation)} не принимает interceptKeys. / SCodeBlock: presentation=${JSON.stringify(presentation)} does not accept interceptKeys.`);
|
|
219
242
|
}
|
|
220
|
-
if (presentation
|
|
221
|
-
throw new TypeError(
|
|
243
|
+
if (presentation !== 'editor' && props.nextSelectionPos !== null) {
|
|
244
|
+
throw new TypeError(`SCodeBlock: presentation=${JSON.stringify(presentation)} не принимает nextSelectionPos. / SCodeBlock: presentation=${JSON.stringify(presentation)} does not accept nextSelectionPos.`);
|
|
222
245
|
}
|
|
223
246
|
},
|
|
224
247
|
{ immediate: true },
|
|
@@ -232,8 +255,8 @@ const setPlainSurface = (element: Element | ComponentPublicInstance | null): voi
|
|
|
232
255
|
};
|
|
233
256
|
|
|
234
257
|
const applySelection = (position: number): void => {
|
|
235
|
-
if (validatedPresentation.value
|
|
236
|
-
throw new Error(
|
|
258
|
+
if (validatedPresentation.value !== 'editor') {
|
|
259
|
+
throw new Error(`SCodeBlock: applySelection недоступен в presentation=${JSON.stringify(validatedPresentation.value)}. / SCodeBlock: applySelection is unavailable in presentation=${JSON.stringify(validatedPresentation.value)}.`);
|
|
237
260
|
}
|
|
238
261
|
const editor = codeEditor.value;
|
|
239
262
|
if (!editor) throw new Error('SCodeBlock: editor не готов; дождитесь ready. / SCodeBlock: editor is not ready; wait for ready.');
|
|
@@ -241,7 +264,7 @@ const applySelection = (position: number): void => {
|
|
|
241
264
|
};
|
|
242
265
|
|
|
243
266
|
const focus = (): void => {
|
|
244
|
-
if (validatedPresentation.value
|
|
267
|
+
if (validatedPresentation.value !== 'editor') {
|
|
245
268
|
const surface = plainSurface.value;
|
|
246
269
|
if (!surface) throw new Error('SCodeBlock: plain surface не готов; дождитесь ready. / SCodeBlock: plain surface is not ready; wait for ready.');
|
|
247
270
|
surface.focus();
|
|
@@ -254,19 +277,20 @@ const focus = (): void => {
|
|
|
254
277
|
|
|
255
278
|
defineExpose({ applySelection, focus });
|
|
256
279
|
const notifier = useNotifier();
|
|
280
|
+
const { t } = useUiKitLocale();
|
|
257
281
|
const { copyText } = useClipboard();
|
|
258
282
|
const copyCode = async () => {
|
|
259
283
|
const result = await copyText(props.code);
|
|
260
284
|
if (result.status === 'copied') {
|
|
261
|
-
notifier.success({ message: '
|
|
285
|
+
notifier.success({ message: t('clipboard.codeCopied') });
|
|
262
286
|
return;
|
|
263
287
|
}
|
|
264
288
|
notifier.danger({
|
|
265
289
|
message: result.status === 'unavailable'
|
|
266
|
-
? '
|
|
290
|
+
? t('clipboard.codeCopyUnavailable')
|
|
267
291
|
: result.status === 'empty'
|
|
268
|
-
? '
|
|
269
|
-
:
|
|
292
|
+
? t('clipboard.emptyCode')
|
|
293
|
+
: `${t('clipboard.codeCopyFailed')} ${result.error.message}`,
|
|
270
294
|
});
|
|
271
295
|
};
|
|
272
296
|
</script>
|
|
@@ -87,6 +87,23 @@
|
|
|
87
87
|
@apply text-surface-400 dark:text-surface-500;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
.s-code-editor :deep(.cm-lintRange-error) {
|
|
91
|
+
text-decoration-color: var(--s-code-editor-diagnostic-error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.s-code-editor :deep(.cm-lintRange-warning) {
|
|
95
|
+
text-decoration-color: var(--s-code-editor-diagnostic-warning);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.s-code-editor :deep(.cm-lintRange-info),
|
|
99
|
+
.s-code-editor :deep(.cm-lintRange-hint) {
|
|
100
|
+
text-decoration-color: var(--s-code-editor-diagnostic-info);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.s-code-editor__diagnostics {
|
|
104
|
+
@apply sr-only;
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
.s-code-editor :deep(.cm-tooltip) {
|
|
91
108
|
@apply rounded-md border border-surface-200 bg-surface-0 text-surface-900 shadow-lg;
|
|
92
109
|
@apply dark:border-surface-700 dark:bg-surface-800 dark:text-surface-100;
|
|
@@ -13,13 +13,28 @@
|
|
|
13
13
|
@ready="onReady"
|
|
14
14
|
@update:model-value="onUpdate"
|
|
15
15
|
/>
|
|
16
|
+
<div
|
|
17
|
+
v-if="validatedDiagnostics.length > 0"
|
|
18
|
+
:id="diagnosticsOwnerId"
|
|
19
|
+
class="s-code-editor__diagnostics"
|
|
20
|
+
role="status"
|
|
21
|
+
aria-live="polite"
|
|
22
|
+
aria-atomic="true"
|
|
23
|
+
>
|
|
24
|
+
<ul>
|
|
25
|
+
<li v-for="diagnostic in validatedDiagnostics" :key="diagnostic.id">
|
|
26
|
+
{{ diagnostic.message }}
|
|
27
|
+
</li>
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
16
30
|
</template>
|
|
17
31
|
|
|
18
32
|
<script setup lang="ts">
|
|
19
|
-
import { computed, ref, watch } from 'vue'
|
|
33
|
+
import { computed, onBeforeUnmount, ref, shallowRef, useId, watch } from 'vue'
|
|
20
34
|
import { Codemirror } from 'vue-codemirror'
|
|
21
35
|
import { oneDark } from '@codemirror/theme-one-dark'
|
|
22
36
|
import { EditorSelection, EditorState, type Extension } from '@codemirror/state'
|
|
37
|
+
import { lintGutter, setDiagnostics, type Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
|
|
23
38
|
import { EditorView, lineNumbers, placeholder as codeMirrorPlaceholder } from '@codemirror/view'
|
|
24
39
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
25
40
|
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
@@ -42,6 +57,18 @@ import {
|
|
|
42
57
|
validateCodeEditorViewport,
|
|
43
58
|
} from '../../../internal/codeEditorContract'
|
|
44
59
|
import { getCodeMirrorLanguageExtension, resolveCodeLanguageKey } from '../codeLanguages'
|
|
60
|
+
import {
|
|
61
|
+
type SCodeEditorDiagnostic,
|
|
62
|
+
type SCodeEditorDiagnosticId,
|
|
63
|
+
type SCodeEditorApi,
|
|
64
|
+
type SCodeEditorRange,
|
|
65
|
+
type SCodeEditorSelectionRange,
|
|
66
|
+
type SCodeEditorSelectionSnapshot,
|
|
67
|
+
type SCodeEditorViewState,
|
|
68
|
+
validateCodeEditorDiagnostics,
|
|
69
|
+
validateCodeEditorRange,
|
|
70
|
+
validateCodeEditorViewState,
|
|
71
|
+
} from './codeEditor'
|
|
45
72
|
|
|
46
73
|
defineOptions({ inheritAttrs: false })
|
|
47
74
|
|
|
@@ -92,6 +119,8 @@ export type Props = AccessibleName & {
|
|
|
92
119
|
viewport?: SCodeEditorViewport
|
|
93
120
|
variant?: SCodeEditorVariant
|
|
94
121
|
nextSelectionPos?: number | null
|
|
122
|
+
/** Controlled diagnostics expressed only through serializable UI-kit ranges. */
|
|
123
|
+
diagnostics?: readonly SCodeEditorDiagnostic<SCodeEditorDiagnosticId>[]
|
|
95
124
|
/** DOM id supplemental status/diagnostic owner. / DOM id of a supplemental status/diagnostic owner. */
|
|
96
125
|
ariaDescribedby?: string
|
|
97
126
|
/** Помечает editor content невалидным без владения presentation диагностики. / Marks the editor content as invalid without owning diagnostic presentation. */
|
|
@@ -106,6 +135,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
106
135
|
viewport: 'content',
|
|
107
136
|
variant: 'code',
|
|
108
137
|
nextSelectionPos: null,
|
|
138
|
+
diagnostics: () => [],
|
|
109
139
|
ariaDescribedby: undefined,
|
|
110
140
|
invalid: false,
|
|
111
141
|
})
|
|
@@ -113,6 +143,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
113
143
|
const emit = defineEmits<{
|
|
114
144
|
'update:modelValue': [value: string]
|
|
115
145
|
'cursor-activity': [payload: SCodeEditorCursorActivity]
|
|
146
|
+
'selection-change': [payload: SCodeEditorSelectionRange]
|
|
147
|
+
'view-state-change': [payload: SCodeEditorViewState]
|
|
116
148
|
'selection-applied': [payload: SCodeEditorSelectionAppliedPayload]
|
|
117
149
|
ready: [payload: SCodeEditorReadyPayload]
|
|
118
150
|
}>()
|
|
@@ -122,8 +154,12 @@ useInteractiveLeafRegistration({ owner: 'SCodeEditor' })
|
|
|
122
154
|
|
|
123
155
|
const runtimeTheme = useRuntimeTheme()
|
|
124
156
|
const trustedChannel = useTrustedCodeEditorExtensions()
|
|
125
|
-
const editorView =
|
|
157
|
+
const editorView = shallowRef<EditorView | null>(null)
|
|
126
158
|
const pendingControlledSelection = ref<number | null>(null)
|
|
159
|
+
const diagnosticsOwnerId = `s-code-editor-diagnostics-${useId()}`
|
|
160
|
+
let scrollOwner: HTMLElement | null = null
|
|
161
|
+
let lastSelection: SCodeEditorSelectionRange | null = null
|
|
162
|
+
let lastViewState: SCodeEditorViewState | null = null
|
|
127
163
|
|
|
128
164
|
const validatedModelValue = computed(() => {
|
|
129
165
|
if (typeof props.modelValue === 'string') return props.modelValue
|
|
@@ -137,6 +173,11 @@ const validatedReadOnly = computed(() => validateBoolean('SCodeEditor', 'readOnl
|
|
|
137
173
|
const validatedDisabled = computed(() => validateBoolean('SCodeEditor', 'disabled', props.disabled))
|
|
138
174
|
const validatedInvalid = computed(() => validateBoolean('SCodeEditor', 'invalid', props.invalid))
|
|
139
175
|
const isLocked = computed(() => validatedReadOnly.value || validatedDisabled.value)
|
|
176
|
+
const validatedDiagnostics = computed(() => validateCodeEditorDiagnostics(
|
|
177
|
+
'SCodeEditor',
|
|
178
|
+
props.diagnostics,
|
|
179
|
+
validatedModelValue.value.length,
|
|
180
|
+
))
|
|
140
181
|
|
|
141
182
|
const resolvedLanguage = computed((): SCodeEditorLanguage => {
|
|
142
183
|
const language = validateCodeEditorLanguage('SCodeEditor', props.language)
|
|
@@ -166,9 +207,12 @@ const contentAttributes = computed((): Record<string, string> => {
|
|
|
166
207
|
} else {
|
|
167
208
|
throw new TypeError('SCodeEditor: требуется ariaLabel или ariaLabelledby. / SCodeEditor: ariaLabel or ariaLabelledby is required.')
|
|
168
209
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
210
|
+
attributes.tabindex = validatedDisabled.value ? '-1' : '0'
|
|
211
|
+
const describedBy = props.ariaDescribedby === undefined
|
|
212
|
+
? []
|
|
213
|
+
: [validateDomId('SCodeEditor', 'ariaDescribedby', props.ariaDescribedby)]
|
|
214
|
+
if (validatedDiagnostics.value.length > 0) describedBy.push(diagnosticsOwnerId)
|
|
215
|
+
if (describedBy.length > 0) attributes['aria-describedby'] = describedBy.join(' ')
|
|
172
216
|
if (validatedInvalid.value) attributes['aria-invalid'] = 'true'
|
|
173
217
|
if (validatedReadOnly.value) attributes['aria-readonly'] = 'true'
|
|
174
218
|
if (validatedDisabled.value) attributes['aria-disabled'] = 'true'
|
|
@@ -197,6 +241,7 @@ const resolvedExtensions = computed<Extension[]>(() => {
|
|
|
197
241
|
if (languageExtension) result.push(languageExtension)
|
|
198
242
|
if (runtimeTheme.value.mode === 'dark') result.push(oneDark)
|
|
199
243
|
if (capabilities.lineNumbers) result.push(lineNumbers())
|
|
244
|
+
if (validatedDiagnostics.value.length > 0) result.push(lintGutter())
|
|
200
245
|
if (props.placeholder !== undefined) result.push(codeMirrorPlaceholder(validateNonEmptyString('SCodeEditor', 'placeholder', props.placeholder)))
|
|
201
246
|
result.push(EditorState.readOnly.of(isLocked.value))
|
|
202
247
|
result.push(EditorView.editable.of(!isLocked.value))
|
|
@@ -211,6 +256,7 @@ const resolvedExtensions = computed<Extension[]>(() => {
|
|
|
211
256
|
}
|
|
212
257
|
result.push(EditorView.updateListener.of((update) => {
|
|
213
258
|
editorView.value = update.view
|
|
259
|
+
if (update.selectionSet || update.docChanged) emitSelectionAndViewState(update.view)
|
|
214
260
|
if ((isLocked.value && capabilities.cursorActivity !== 'always') || (!update.selectionSet && !update.docChanged)) return
|
|
215
261
|
const pos = update.state.selection.main.head
|
|
216
262
|
const line = update.state.doc.lineAt(pos)
|
|
@@ -238,6 +284,93 @@ const dispatchSelection = (position: number, source: SCodeEditorSelectionApplied
|
|
|
238
284
|
emit('selection-applied', { position: validated, source })
|
|
239
285
|
}
|
|
240
286
|
|
|
287
|
+
const requireEditorView = (): EditorView => {
|
|
288
|
+
const view = editorView.value
|
|
289
|
+
if (!view) throw new Error('SCodeEditor: editor не готов; дождитесь ready. / SCodeEditor: editor is not ready; wait for ready.')
|
|
290
|
+
return view
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const freezeSelection = (view: EditorView): SCodeEditorSelectionRange => {
|
|
294
|
+
const range = view.state.selection.main
|
|
295
|
+
return Object.freeze({
|
|
296
|
+
anchor: range.anchor,
|
|
297
|
+
head: range.head,
|
|
298
|
+
from: range.from,
|
|
299
|
+
to: range.to,
|
|
300
|
+
empty: range.empty,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const captureViewStateFrom = (view: EditorView): SCodeEditorViewState => Object.freeze({
|
|
305
|
+
selection: freezeSelection(view),
|
|
306
|
+
scrollTop: view.scrollDOM.scrollTop,
|
|
307
|
+
scrollLeft: view.scrollDOM.scrollLeft,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const selectionsEqual = (
|
|
311
|
+
left: SCodeEditorSelectionRange | null,
|
|
312
|
+
right: SCodeEditorSelectionRange,
|
|
313
|
+
): boolean => left !== null
|
|
314
|
+
&& left.anchor === right.anchor
|
|
315
|
+
&& left.head === right.head
|
|
316
|
+
&& left.from === right.from
|
|
317
|
+
&& left.to === right.to
|
|
318
|
+
&& left.empty === right.empty
|
|
319
|
+
|
|
320
|
+
const viewStatesEqual = (left: SCodeEditorViewState | null, right: SCodeEditorViewState): boolean =>
|
|
321
|
+
left !== null
|
|
322
|
+
&& selectionsEqual(left.selection, right.selection)
|
|
323
|
+
&& left.scrollTop === right.scrollTop
|
|
324
|
+
&& left.scrollLeft === right.scrollLeft
|
|
325
|
+
|
|
326
|
+
const emitSelectionAndViewState = (view: EditorView): void => {
|
|
327
|
+
const selection = freezeSelection(view)
|
|
328
|
+
if (!selectionsEqual(lastSelection, selection)) {
|
|
329
|
+
lastSelection = selection
|
|
330
|
+
emit('selection-change', selection)
|
|
331
|
+
}
|
|
332
|
+
const viewState = captureViewStateFrom(view)
|
|
333
|
+
if (!viewStatesEqual(lastViewState, viewState)) {
|
|
334
|
+
lastViewState = viewState
|
|
335
|
+
emit('view-state-change', viewState)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const emitScrollViewState = (): void => {
|
|
340
|
+
const view = editorView.value
|
|
341
|
+
if (!view) return
|
|
342
|
+
const viewState = captureViewStateFrom(view)
|
|
343
|
+
if (viewStatesEqual(lastViewState, viewState)) return
|
|
344
|
+
lastViewState = viewState
|
|
345
|
+
emit('view-state-change', viewState)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const detachScrollOwner = (): void => {
|
|
349
|
+
scrollOwner?.removeEventListener('scroll', emitScrollViewState)
|
|
350
|
+
scrollOwner = null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const attachScrollOwner = (view: EditorView): void => {
|
|
354
|
+
detachScrollOwner()
|
|
355
|
+
scrollOwner = view.scrollDOM
|
|
356
|
+
scrollOwner.addEventListener('scroll', emitScrollViewState, { passive: true })
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const toCodeMirrorDiagnostics = (
|
|
360
|
+
diagnostics: readonly SCodeEditorDiagnostic<SCodeEditorDiagnosticId>[],
|
|
361
|
+
): readonly CodeMirrorDiagnostic[] => diagnostics.map((diagnostic) => ({
|
|
362
|
+
from: diagnostic.from,
|
|
363
|
+
to: diagnostic.to,
|
|
364
|
+
severity: diagnostic.severity,
|
|
365
|
+
message: diagnostic.message,
|
|
366
|
+
...(diagnostic.source === undefined ? {} : { source: diagnostic.source }),
|
|
367
|
+
}))
|
|
368
|
+
|
|
369
|
+
const syncDiagnostics = (view: EditorView): void => {
|
|
370
|
+
const diagnostics = validateCodeEditorDiagnostics('SCodeEditor', props.diagnostics, view.state.doc.length)
|
|
371
|
+
view.dispatch(setDiagnostics(view.state, toCodeMirrorDiagnostics(diagnostics)))
|
|
372
|
+
}
|
|
373
|
+
|
|
241
374
|
const onUpdate = (value: string): void => {
|
|
242
375
|
if (typeof value !== 'string') {
|
|
243
376
|
throw new TypeError('SCodeEditor: CodeMirror update payload должен быть строкой. / SCodeEditor: CodeMirror update payload must be a string.')
|
|
@@ -250,6 +383,8 @@ const onReady = (payload: unknown): void => {
|
|
|
250
383
|
throw new TypeError('SCodeEditor: CodeMirror ready payload должен содержать EditorView. / SCodeEditor: CodeMirror ready payload must contain EditorView.')
|
|
251
384
|
}
|
|
252
385
|
editorView.value = (payload as { view: EditorView }).view
|
|
386
|
+
attachScrollOwner(editorView.value)
|
|
387
|
+
syncDiagnostics(editorView.value)
|
|
253
388
|
const readyPayload: SCodeEditorReadyPayload = {
|
|
254
389
|
documentLength: editorView.value.state.doc.length,
|
|
255
390
|
viewport: validatedViewport.value,
|
|
@@ -276,11 +411,41 @@ watch(() => props.nextSelectionPos, (position) => {
|
|
|
276
411
|
dispatchSelection(position, 'controlled')
|
|
277
412
|
}, { immediate: true })
|
|
278
413
|
|
|
279
|
-
|
|
414
|
+
watch(validatedDiagnostics, () => {
|
|
415
|
+
if (editorView.value) syncDiagnostics(editorView.value)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
onBeforeUnmount(() => {
|
|
419
|
+
detachScrollOwner()
|
|
420
|
+
editorView.value = null
|
|
421
|
+
lastSelection = null
|
|
422
|
+
lastViewState = null
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
defineExpose<SCodeEditorApi>({
|
|
280
426
|
applySelection: (position: number) => dispatchSelection(position, 'imperative'),
|
|
427
|
+
revealRange: (range: SCodeEditorRange) => {
|
|
428
|
+
const view = requireEditorView()
|
|
429
|
+
const validated = validateCodeEditorRange('SCodeEditor', 'revealRange', range, view.state.doc.length)
|
|
430
|
+
const effect = EditorView.scrollIntoView(EditorSelection.range(validated.from, validated.to), { y: 'nearest' })
|
|
431
|
+
view.dispatch({ effects: effect })
|
|
432
|
+
},
|
|
433
|
+
getSelection: (): SCodeEditorSelectionSnapshot => {
|
|
434
|
+
const view = requireEditorView()
|
|
435
|
+
const selection = freezeSelection(view)
|
|
436
|
+
return Object.freeze({ ...selection, text: view.state.doc.sliceString(selection.from, selection.to) })
|
|
437
|
+
},
|
|
438
|
+
captureViewState: (): SCodeEditorViewState => captureViewStateFrom(requireEditorView()),
|
|
439
|
+
applyViewState: (viewState: SCodeEditorViewState) => {
|
|
440
|
+
const view = requireEditorView()
|
|
441
|
+
const validated = validateCodeEditorViewState('SCodeEditor', viewState, view.state.doc.length)
|
|
442
|
+
view.dispatch({ selection: EditorSelection.range(validated.selection.anchor, validated.selection.head) })
|
|
443
|
+
view.scrollDOM.scrollTop = validated.scrollTop
|
|
444
|
+
view.scrollDOM.scrollLeft = validated.scrollLeft
|
|
445
|
+
emitSelectionAndViewState(view)
|
|
446
|
+
},
|
|
281
447
|
focus: () => {
|
|
282
|
-
|
|
283
|
-
editorView.value.focus()
|
|
448
|
+
requireEditorView().focus()
|
|
284
449
|
},
|
|
285
450
|
})
|
|
286
451
|
</script>
|