@veltra/desktop 1.2.23 → 1.2.25
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/dist/components/action/action-group.vue.d.ts +2 -2
- package/dist/components/action/action.vue.d.ts +1 -1
- package/dist/components/batch-edit/batch-edit.vue.d.ts +2 -2
- package/dist/components/cascade/cascade.js +5 -3
- package/dist/components/cascade/cascade.js.map +1 -1
- package/dist/components/cascade/cascade.vue.d.ts +3 -3
- package/dist/components/cascade/use-data-map.js +2 -1
- package/dist/components/cascade/use-data-map.js.map +1 -1
- package/dist/components/checkbox-group/checkbox-group.js +10 -10
- package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
- package/dist/components/code-editor/basic-setup.js +2 -1
- package/dist/components/code-editor/basic-setup.js.map +1 -1
- package/dist/components/code-editor/code-editor.js +136 -79
- package/dist/components/code-editor/code-editor.js.map +1 -1
- package/dist/components/code-editor/code-editor.vue.d.ts +10 -2
- package/dist/components/code-editor/shell-extension.js +139 -0
- package/dist/components/code-editor/shell-extension.js.map +1 -0
- package/dist/components/code-editor/style.js +1 -0
- package/dist/components/code-editor/style2.css +15 -11
- package/dist/components/empty/empty.vue.d.ts +1 -1
- package/dist/components/file-picker/file-picker.vue.d.ts +1 -1
- package/dist/components/input/input.vue.d.ts +2 -2
- package/dist/components/multi-select/multi-select.js +31 -36
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/multi-select/multi-select.vue.d.ts +1 -1
- package/dist/components/multi-tree-select/multi-tree-select.js +8 -6
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +4 -4
- package/dist/components/number/number.vue.d.ts +1 -1
- package/dist/components/number-input/number-input.vue.d.ts +2 -2
- package/dist/components/progress-nodes/progress-nodes.js +6 -4
- package/dist/components/progress-nodes/progress-nodes.js.map +1 -1
- package/dist/components/radio-group/radio-group.js +8 -6
- package/dist/components/radio-group/radio-group.js.map +1 -1
- package/dist/components/select/select.js +15 -13
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select/use-options.js +7 -4
- package/dist/components/select/use-options.js.map +1 -1
- package/dist/components/steps/steps.js +3 -2
- package/dist/components/steps/steps.js.map +1 -1
- package/dist/components/textarea/textarea.vue.d.ts +2 -2
- package/dist/components/tree/use-check.js +2 -1
- package/dist/components/tree/use-check.js.map +1 -1
- package/dist/components/tree/use-select.js +3 -1
- package/dist/components/tree/use-select.js.map +1 -1
- package/dist/components/tree/use-tree-nodes.js +4 -1
- package/dist/components/tree/use-tree-nodes.js.map +1 -1
- package/dist/components/tree-select/tree-select.js +6 -4
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/components/tree-select/tree-select.vue.d.ts +4 -4
- package/dist/node_modules/.bun/@codemirror_commands@6.10.4/node_modules/@codemirror/commands/dist/index.js +12 -1
- package/dist/node_modules/.bun/@codemirror_commands@6.10.4/node_modules/@codemirror/commands/dist/index.js.map +1 -1
- package/dist/style.js +1 -1
- package/dist/types/code-editor.d.ts +13 -2
- package/package.json +6 -6
- package/src/components/cascade/cascade.vue +6 -3
- package/src/components/cascade/use-data-map.ts +3 -2
- package/src/components/checkbox-group/checkbox-group.vue +10 -8
- package/src/components/code-editor/basic-setup.ts +3 -1
- package/src/components/code-editor/code-editor.vue +154 -96
- package/src/components/code-editor/shell-extension.ts +199 -0
- package/src/components/code-editor/style.scss +20 -13
- package/src/components/code-editor/style.ts +1 -0
- package/src/components/multi-select/multi-select.vue +31 -30
- package/src/components/multi-tree-select/multi-tree-select.vue +5 -2
- package/src/components/progress-nodes/progress-nodes.vue +7 -4
- package/src/components/radio-group/radio-group.vue +5 -1
- package/src/components/select/select.vue +15 -11
- package/src/components/select/use-options.ts +7 -4
- package/src/components/steps/steps.vue +3 -1
- package/src/components/tree/use-check.ts +2 -1
- package/src/components/tree/use-select.ts +3 -1
- package/src/components/tree/use-tree-nodes.ts +5 -9
- package/src/components/tree-select/tree-select.vue +7 -4
- package/src/types/code-editor.ts +13 -2
package/dist/style.js
CHANGED
|
@@ -25,9 +25,9 @@ import "./components/card/style.js";
|
|
|
25
25
|
import "./components/cascade/style.js";
|
|
26
26
|
import "./components/check-tag/style.js";
|
|
27
27
|
import "./components/checkbox-group/style.js";
|
|
28
|
+
import "./components/select/style.js";
|
|
28
29
|
import "./components/code-editor/style.js";
|
|
29
30
|
import "./components/collapse/style.js";
|
|
30
|
-
import "./components/select/style.js";
|
|
31
31
|
import "./components/expression-editor/style.js";
|
|
32
32
|
import "./components/condition-editor/style.js";
|
|
33
33
|
import "./components/contextmenu/style.js";
|
|
@@ -5,8 +5,16 @@ type CodeEditorLang = 'js' | 'sql' | 'java' | 'json';
|
|
|
5
5
|
/** 代码编辑器组件属性(不支持 `size`) */
|
|
6
6
|
interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
|
|
7
7
|
modelValue?: string;
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* 可选语言列表。长度大于 1 时在编辑器右上角显示内置语言选择器
|
|
10
|
+
*/
|
|
11
|
+
langs?: CodeEditorLang[];
|
|
12
|
+
/** 当前语言,配合 `v-model:lang` 使用 */
|
|
13
|
+
lang?: CodeEditorLang;
|
|
14
|
+
/** 不可编辑的前缀外壳(展示在编辑器内,不计入 v-model) */
|
|
15
|
+
prefix?: string;
|
|
16
|
+
/** 不可编辑的后缀外壳(展示在编辑器内,不计入 v-model) */
|
|
17
|
+
suffix?: string;
|
|
10
18
|
/** 是否使用暗色主题 */
|
|
11
19
|
dark?: boolean;
|
|
12
20
|
/**
|
|
@@ -18,6 +26,9 @@ interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
|
|
|
18
26
|
/** 代码编辑器组件定义的事件 */
|
|
19
27
|
interface CodeEditorEmits {
|
|
20
28
|
(e: 'update:modelValue', value: string): void;
|
|
29
|
+
(e: 'update:lang', value: CodeEditorLang | undefined): void;
|
|
30
|
+
/** 在编辑器失焦且内容有变更时触发 */
|
|
31
|
+
(e: 'change', value: string): void;
|
|
21
32
|
}
|
|
22
33
|
/** 代码编辑器组件暴露的属性和方法(组件内部使用) */
|
|
23
34
|
interface _CodeEditorExposed {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/desktop",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.25",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"src"
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"@cat-kit/core": ">=1.1.8",
|
|
58
58
|
"@cat-kit/excel": ">=1.0.0",
|
|
59
59
|
"@cat-kit/fe": "^1.1.8",
|
|
60
|
-
"@veltra/compositions": "1.2.
|
|
61
|
-
"@veltra/directives": "1.2.
|
|
62
|
-
"@veltra/icons": "1.2.
|
|
63
|
-
"@veltra/styles": "1.2.
|
|
64
|
-
"@veltra/utils": "1.2.
|
|
60
|
+
"@veltra/compositions": "1.2.25",
|
|
61
|
+
"@veltra/directives": "1.2.25",
|
|
62
|
+
"@veltra/icons": "1.2.25",
|
|
63
|
+
"@veltra/styles": "1.2.25",
|
|
64
|
+
"@veltra/utils": "1.2.25",
|
|
65
65
|
"vue": ">=3.5.39"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
import { Forest, o } from '@cat-kit/core'
|
|
109
109
|
import { useFormFallbackProps, useUserAction } from '@veltra/compositions'
|
|
110
110
|
import { ArrowDown, Close, Search } from '@veltra/icons/normal'
|
|
111
|
-
import { bem, FORM_EMPTY_CONTENT } from '@veltra/utils'
|
|
111
|
+
import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
|
|
112
112
|
import { injectFormContext } from '@veltra/utils'
|
|
113
113
|
import { computed, provide, shallowRef, triggerRef, watch } from 'vue'
|
|
114
114
|
|
|
@@ -146,6 +146,9 @@ const emit = defineEmits<CascadeEmits>()
|
|
|
146
146
|
|
|
147
147
|
const cls = bem('cascade')
|
|
148
148
|
|
|
149
|
+
const labelKey = computed(() => fieldKey(props.labelKey, 'label'))
|
|
150
|
+
const valueKey = computed(() => fieldKey(props.valueKey, 'value'))
|
|
151
|
+
|
|
149
152
|
const { formProps } = injectFormContext()
|
|
150
153
|
|
|
151
154
|
const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
|
|
@@ -169,8 +172,8 @@ const forest = computed(() => {
|
|
|
169
172
|
data,
|
|
170
173
|
index,
|
|
171
174
|
depth,
|
|
172
|
-
value: o(data).get(
|
|
173
|
-
label: o(data).get(
|
|
175
|
+
value: o(data).get(valueKey.value) ?? '',
|
|
176
|
+
label: o(data).get(labelKey.value) ?? '',
|
|
174
177
|
parent
|
|
175
178
|
})
|
|
176
179
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Forest, o } from '@cat-kit/core'
|
|
2
|
+
import { fieldKey } from '@veltra/utils'
|
|
2
3
|
import { shallowRef, triggerRef, watchEffect, type ComputedRef, type ShallowRef } from 'vue'
|
|
3
4
|
|
|
4
5
|
import type { CascadeNode, CascadeProps } from '../../types'
|
|
@@ -18,12 +19,12 @@ export function useDataMap(options: DataMapOptions): UseDataMapReturned {
|
|
|
18
19
|
const dataMap = shallowRef(new Map<string, CascadeNode>())
|
|
19
20
|
|
|
20
21
|
watchEffect(() => {
|
|
21
|
-
const
|
|
22
|
+
const valueKey = fieldKey(props.valueKey, 'value')
|
|
22
23
|
|
|
23
24
|
dataMap.value.clear()
|
|
24
25
|
|
|
25
26
|
forest.value.dfs((node) => {
|
|
26
|
-
const value = o(node.data).get(valueKey
|
|
27
|
+
const value = o(node.data).get(valueKey)
|
|
27
28
|
if (value !== null && value !== undefined) {
|
|
28
29
|
dataMap.value.set(value, node)
|
|
29
30
|
}
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
|
|
28
28
|
<script lang="ts" setup>
|
|
29
29
|
import { useFormFallbackProps } from '@veltra/compositions'
|
|
30
|
-
import { bem, FORM_EMPTY_CONTENT } from '@veltra/utils'
|
|
30
|
+
import { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'
|
|
31
31
|
import { injectFormContext } from '@veltra/utils'
|
|
32
|
+
import { computed } from 'vue'
|
|
32
33
|
|
|
33
34
|
import type { CheckboxGroupProps, CheckboxGroupEmits } from '../../types'
|
|
34
35
|
import { UCheckbox } from '../checkbox'
|
|
@@ -49,6 +50,9 @@ const model = defineModel<any[]>()
|
|
|
49
50
|
|
|
50
51
|
const cls = bem('checkbox-group')
|
|
51
52
|
|
|
53
|
+
const labelKey = computed(() => fieldKey(props.labelKey, 'label'))
|
|
54
|
+
const valueKey = computed(() => fieldKey(props.valueKey, 'value'))
|
|
55
|
+
|
|
52
56
|
const { formProps } = injectFormContext()
|
|
53
57
|
|
|
54
58
|
const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
|
|
@@ -63,8 +67,7 @@ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, prop
|
|
|
63
67
|
* @returns 模型值是否存在于model.value中
|
|
64
68
|
*/
|
|
65
69
|
const getCheckStatus = (item: Record<string, string | number>): boolean => {
|
|
66
|
-
const
|
|
67
|
-
const value = item[valueKey]
|
|
70
|
+
const value = item[valueKey.value]
|
|
68
71
|
if (!value || !model.value) return false
|
|
69
72
|
return model.value.includes(value)
|
|
70
73
|
}
|
|
@@ -75,8 +78,7 @@ const getCheckStatus = (item: Record<string, string | number>): boolean => {
|
|
|
75
78
|
* @param item - 更新的项
|
|
76
79
|
*/
|
|
77
80
|
const handleUpdate = (checked: boolean, item: Record<string, string | number>) => {
|
|
78
|
-
const
|
|
79
|
-
const value = item[valueKey]
|
|
81
|
+
const value = item[valueKey.value]
|
|
80
82
|
if (!value) return
|
|
81
83
|
if (checked) {
|
|
82
84
|
model.value = [...(model.value ?? []), value]
|
|
@@ -86,8 +88,8 @@ const handleUpdate = (checked: boolean, item: Record<string, string | number>) =
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
const getLabel = (value: string | number) => {
|
|
89
|
-
const { items
|
|
90
|
-
const item = items.find((item) => item[valueKey] === value)
|
|
91
|
-
return item?.[labelKey]
|
|
91
|
+
const { items } = props
|
|
92
|
+
const item = items.find((item) => item[valueKey.value] === value)
|
|
93
|
+
return item?.[labelKey.value]
|
|
92
94
|
}
|
|
93
95
|
</script>
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
closeBracketsKeymap,
|
|
5
5
|
completionKeymap
|
|
6
6
|
} from '@codemirror/autocomplete'
|
|
7
|
-
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
|
|
7
|
+
import { history, defaultKeymap, historyKeymap, indentWithTab } from '@codemirror/commands'
|
|
8
8
|
import {
|
|
9
9
|
foldGutter,
|
|
10
10
|
indentOnInput,
|
|
@@ -52,6 +52,8 @@ export const basicSetup: Extension = (() => [
|
|
|
52
52
|
highlightActiveLine(),
|
|
53
53
|
highlightSelectionMatches(),
|
|
54
54
|
keymap.of([
|
|
55
|
+
// Tab 默认留给浏览器焦点导航;代码编辑器需显式绑定缩进,否则焦点会跳到语言选择等控件
|
|
56
|
+
indentWithTab,
|
|
55
57
|
...closeBracketsKeymap,
|
|
56
58
|
...defaultKeymap,
|
|
57
59
|
...searchKeymap,
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<u-scroll :class="className" :style="rootStyle" @keyup.enter.stop>
|
|
3
3
|
<div ref="container"></div>
|
|
4
|
+
|
|
5
|
+
<u-select
|
|
6
|
+
v-if="showLangSelect"
|
|
7
|
+
:class="cls.e('lang-select')"
|
|
8
|
+
v-model="lang"
|
|
9
|
+
size="small"
|
|
10
|
+
:options="langOptions"
|
|
11
|
+
:clearable="false"
|
|
12
|
+
:disabled="disabled"
|
|
13
|
+
/>
|
|
4
14
|
</u-scroll>
|
|
5
15
|
</template>
|
|
6
16
|
|
|
7
17
|
<script lang="ts" setup>
|
|
8
|
-
import { Compartment, EditorState, type Extension } from '@codemirror/state'
|
|
18
|
+
import { Compartment, EditorState, type Extension, type Text } from '@codemirror/state'
|
|
9
19
|
import { oneDark } from '@codemirror/theme-one-dark'
|
|
10
|
-
import { EditorView,
|
|
20
|
+
import { EditorView, tooltips } from '@codemirror/view'
|
|
11
21
|
import { useFormFallbackProps } from '@veltra/compositions'
|
|
12
|
-
import { bem, zIndex } from '@veltra/utils'
|
|
13
|
-
import { injectFormContext } from '@veltra/utils'
|
|
22
|
+
import { bem, injectFormContext, zIndex } from '@veltra/utils'
|
|
14
23
|
import {
|
|
15
24
|
computed,
|
|
16
25
|
onBeforeUnmount,
|
|
@@ -20,10 +29,20 @@ import {
|
|
|
20
29
|
type CSSProperties
|
|
21
30
|
} from 'vue'
|
|
22
31
|
|
|
23
|
-
import type { CodeEditorProps } from '../../types'
|
|
32
|
+
import type { CodeEditorEmits, CodeEditorLang, CodeEditorProps } from '../../types'
|
|
24
33
|
import { UScroll } from '../scroll'
|
|
34
|
+
import { USelect } from '../select'
|
|
25
35
|
import { basicSetup } from './basic-setup'
|
|
26
36
|
import { loadLanguage } from './lang-loaders'
|
|
37
|
+
import {
|
|
38
|
+
buildFullDoc,
|
|
39
|
+
extractBodyFromText,
|
|
40
|
+
getShellRanges,
|
|
41
|
+
mapSelectionToBody,
|
|
42
|
+
shellExtension,
|
|
43
|
+
textEqualsShellDoc,
|
|
44
|
+
type ShellConfig
|
|
45
|
+
} from './shell-extension'
|
|
27
46
|
|
|
28
47
|
defineOptions({ name: 'UCodeEditor' })
|
|
29
48
|
|
|
@@ -34,7 +53,10 @@ const props = withDefaults(defineProps<CodeEditorProps>(), {
|
|
|
34
53
|
defaultLines: 8
|
|
35
54
|
})
|
|
36
55
|
|
|
56
|
+
const emit = defineEmits<CodeEditorEmits>()
|
|
57
|
+
|
|
37
58
|
const model = defineModel<string>()
|
|
59
|
+
const lang = defineModel<CodeEditorLang>('lang')
|
|
38
60
|
|
|
39
61
|
const cls = bem('code-editor')
|
|
40
62
|
|
|
@@ -56,6 +78,22 @@ const rootStyle = computed<CSSProperties>(() => ({
|
|
|
56
78
|
'--u-code-editor-default-lines': Math.max(1, props.defaultLines)
|
|
57
79
|
}))
|
|
58
80
|
|
|
81
|
+
const langs = computed(() => props.langs ?? [])
|
|
82
|
+
|
|
83
|
+
const showLangSelect = computed(() => langs.value.length > 1)
|
|
84
|
+
|
|
85
|
+
const langOptions = computed(() =>
|
|
86
|
+
langs.value.map((value) => ({ label: value.toUpperCase(), value }))
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
/** 实际生效的语言:优先 lang model,否则回落 langs[0] */
|
|
90
|
+
const activeLang = computed<CodeEditorLang | undefined>(() => lang.value ?? langs.value[0])
|
|
91
|
+
|
|
92
|
+
const shellConfig = computed<ShellConfig>(() => ({
|
|
93
|
+
prefix: props.prefix ?? '',
|
|
94
|
+
suffix: props.suffix ?? ''
|
|
95
|
+
}))
|
|
96
|
+
|
|
59
97
|
const containerRef = useTemplateRef('container')
|
|
60
98
|
const editor = shallowRef<EditorView | null>(null)
|
|
61
99
|
|
|
@@ -63,77 +101,28 @@ const themeCompartment = new Compartment()
|
|
|
63
101
|
const editableCompartment = new Compartment()
|
|
64
102
|
const readOnlyCompartment = new Compartment()
|
|
65
103
|
const langCompartment = new Compartment()
|
|
104
|
+
const shellCompartment = new Compartment()
|
|
66
105
|
|
|
67
|
-
|
|
68
|
-
let
|
|
69
|
-
let lastPhantomKey = ''
|
|
70
|
-
|
|
71
|
-
function resetPhantomCache() {
|
|
72
|
-
phantomGutter = null
|
|
73
|
-
phantomContainer = null
|
|
74
|
-
lastPhantomKey = ''
|
|
75
|
-
}
|
|
106
|
+
/** 聚焦时的正文快照,用于失焦时判断是否触发 change */
|
|
107
|
+
let focusSnapshot = ''
|
|
76
108
|
|
|
77
109
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
110
|
+
* 编辑器最近一次写出的正文引用。
|
|
111
|
+
* 用于短路 model → doc 同步,避免每次按键对整篇文档做 O(n) 相等比较。
|
|
80
112
|
*/
|
|
81
|
-
|
|
82
|
-
const view = editor.value
|
|
83
|
-
if (!view) return
|
|
84
|
-
|
|
85
|
-
if (!phantomGutter || !phantomGutter.isConnected) {
|
|
86
|
-
phantomGutter = view.dom.querySelector<HTMLElement>('.cm-gutter.cm-lineNumbers')
|
|
87
|
-
}
|
|
88
|
-
if (!phantomGutter) return
|
|
89
|
-
|
|
90
|
-
if (!phantomContainer) {
|
|
91
|
-
phantomContainer = document.createElement('div')
|
|
92
|
-
phantomContainer.className = 'cm-phantom-lines'
|
|
93
|
-
}
|
|
94
|
-
if (
|
|
95
|
-
phantomContainer.parentElement !== phantomGutter ||
|
|
96
|
-
phantomContainer !== phantomGutter.lastElementChild
|
|
97
|
-
) {
|
|
98
|
-
phantomGutter.appendChild(phantomContainer)
|
|
99
|
-
}
|
|
113
|
+
let lastEditorBody: string | undefined
|
|
100
114
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const lineHeight = view.defaultLineHeight
|
|
115
|
+
/** 上一帧是否处于 IME composition,用于在 composition 结束时补一次 model 同步 */
|
|
116
|
+
let wasComposing = false
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
while (phantomContainer.children.length < target) {
|
|
113
|
-
const el = document.createElement('div')
|
|
114
|
-
el.className = 'cm-gutterElement cm-gutterElement-phantom'
|
|
115
|
-
phantomContainer.appendChild(el)
|
|
116
|
-
}
|
|
117
|
-
for (let i = 0; i < target; i++) {
|
|
118
|
-
const el = phantomContainer.children[i] as HTMLElement
|
|
119
|
-
const num = String(realLines + i + 1)
|
|
120
|
-
if (el.textContent !== num) el.textContent = num
|
|
121
|
-
if (lineHeight && el.style.height !== `${lineHeight}px`) {
|
|
122
|
-
el.style.height = `${lineHeight}px`
|
|
123
|
-
}
|
|
124
|
-
}
|
|
118
|
+
/** 将编辑器正文写回 v-model(跳过与上次相同的值,避免多余更新) */
|
|
119
|
+
function writeModelFromDoc(doc: Text) {
|
|
120
|
+
const nextBody = extractBodyFromText(doc, shellConfig.value)
|
|
121
|
+
if (nextBody === lastEditorBody && nextBody === model.value) return
|
|
122
|
+
lastEditorBody = nextBody
|
|
123
|
+
model.value = nextBody
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
const phantomLineNumbersPlugin = ViewPlugin.fromClass(
|
|
128
|
-
class {
|
|
129
|
-
update(update: ViewUpdate) {
|
|
130
|
-
if (update.docChanged || update.geometryChanged) {
|
|
131
|
-
syncPhantomLineNumbers()
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
|
|
137
126
|
function buildExtensions(): Extension[] {
|
|
138
127
|
return [
|
|
139
128
|
basicSetup,
|
|
@@ -143,7 +132,28 @@ function buildExtensions(): Extension[] {
|
|
|
143
132
|
editableCompartment.of(EditorView.editable.of(!disabled.value)),
|
|
144
133
|
readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),
|
|
145
134
|
langCompartment.of([]),
|
|
146
|
-
|
|
135
|
+
shellCompartment.of(shellExtension(shellConfig.value)),
|
|
136
|
+
EditorView.updateListener.of((update) => {
|
|
137
|
+
const composing = update.view.composing
|
|
138
|
+
|
|
139
|
+
// IME 进行中不写回,避免中间态触发父级重渲染;结束后再统一同步
|
|
140
|
+
if (update.docChanged && !composing) {
|
|
141
|
+
writeModelFromDoc(update.state.doc)
|
|
142
|
+
} else if (wasComposing && !composing) {
|
|
143
|
+
writeModelFromDoc(update.state.doc)
|
|
144
|
+
}
|
|
145
|
+
wasComposing = composing
|
|
146
|
+
|
|
147
|
+
if (!update.focusChanged) return
|
|
148
|
+
if (update.view.hasFocus) {
|
|
149
|
+
focusSnapshot = extractBodyFromText(update.state.doc, shellConfig.value)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
const current = extractBodyFromText(update.state.doc, shellConfig.value)
|
|
153
|
+
if (current !== focusSnapshot) {
|
|
154
|
+
emit('change', current)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
147
157
|
]
|
|
148
158
|
}
|
|
149
159
|
|
|
@@ -154,7 +164,7 @@ async function applyLanguage() {
|
|
|
154
164
|
const view = editor.value
|
|
155
165
|
if (!view) return
|
|
156
166
|
const myToken = ++langToken
|
|
157
|
-
const
|
|
167
|
+
const language = activeLang.value
|
|
158
168
|
|
|
159
169
|
if (!language) {
|
|
160
170
|
if (myToken !== langToken || editor.value !== view) return
|
|
@@ -172,7 +182,8 @@ function destroyEditor() {
|
|
|
172
182
|
++langToken
|
|
173
183
|
editor.value?.destroy()
|
|
174
184
|
editor.value = null
|
|
175
|
-
|
|
185
|
+
lastEditorBody = undefined
|
|
186
|
+
wasComposing = false
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
function createEditor() {
|
|
@@ -181,16 +192,17 @@ function createEditor() {
|
|
|
181
192
|
const parent = containerRef.value
|
|
182
193
|
if (!parent) return
|
|
183
194
|
|
|
195
|
+
const config = shellConfig.value
|
|
196
|
+
const body = model.value ?? ''
|
|
197
|
+
const doc = buildFullDoc(body, config)
|
|
198
|
+
const { bodyStart } = getShellRanges(doc.length, config)
|
|
199
|
+
lastEditorBody = body
|
|
200
|
+
|
|
184
201
|
const view = new EditorView({
|
|
185
|
-
doc
|
|
202
|
+
doc,
|
|
203
|
+
selection: { anchor: bodyStart, head: bodyStart },
|
|
186
204
|
extensions: buildExtensions(),
|
|
187
|
-
parent
|
|
188
|
-
dispatch(tr) {
|
|
189
|
-
view.update([tr])
|
|
190
|
-
if (tr.docChanged) {
|
|
191
|
-
model.value = tr.state.doc.toString()
|
|
192
|
-
}
|
|
193
|
-
}
|
|
205
|
+
parent
|
|
194
206
|
})
|
|
195
207
|
|
|
196
208
|
if (myToken !== editorToken) {
|
|
@@ -199,10 +211,46 @@ function createEditor() {
|
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
editor.value = view
|
|
202
|
-
syncPhantomLineNumbers()
|
|
203
214
|
applyLanguage()
|
|
204
215
|
}
|
|
205
216
|
|
|
217
|
+
/**
|
|
218
|
+
* 将 model 正文同步为完整文档;可选同时 reconfigure shell,
|
|
219
|
+
* 避免 prefix/suffix 变更时「先改扩展、后改文档」导致选区越界。
|
|
220
|
+
*/
|
|
221
|
+
function syncDocFromModel(
|
|
222
|
+
view: EditorView,
|
|
223
|
+
body: string,
|
|
224
|
+
config: ShellConfig,
|
|
225
|
+
options?: { reconfigureShell?: boolean; prevShell?: ShellConfig }
|
|
226
|
+
) {
|
|
227
|
+
const current = view.state.doc
|
|
228
|
+
const docChanged = !textEqualsShellDoc(current, body, config)
|
|
229
|
+
const reconfigureShell = options?.reconfigureShell ?? false
|
|
230
|
+
|
|
231
|
+
if (!docChanged && !reconfigureShell) return
|
|
232
|
+
|
|
233
|
+
const nextDoc = buildFullDoc(body, config)
|
|
234
|
+
const prevShell = options?.prevShell ?? config
|
|
235
|
+
const prevRanges = getShellRanges(current.length, prevShell)
|
|
236
|
+
const nextRanges = getShellRanges(nextDoc.length, config)
|
|
237
|
+
const selection = mapSelectionToBody(view.state.selection, prevRanges, nextRanges, nextDoc.length)
|
|
238
|
+
|
|
239
|
+
view.dispatch({
|
|
240
|
+
...(docChanged
|
|
241
|
+
? {
|
|
242
|
+
changes: { from: 0, to: current.length, insert: nextDoc },
|
|
243
|
+
selection,
|
|
244
|
+
// 仅外壳变更时滚入视口;外部 model 回写不应打断用户滚动位置
|
|
245
|
+
...(reconfigureShell ? { scrollIntoView: true } : {})
|
|
246
|
+
}
|
|
247
|
+
: { selection }),
|
|
248
|
+
...(reconfigureShell ? { effects: shellCompartment.reconfigure(shellExtension(config)) } : {}),
|
|
249
|
+
// 绕过 shell 的 change/transactionFilter,避免全量替换被拦截或选区被错误钳制
|
|
250
|
+
filter: false
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
206
254
|
watch(
|
|
207
255
|
containerRef,
|
|
208
256
|
(el) => {
|
|
@@ -212,7 +260,19 @@ watch(
|
|
|
212
260
|
{ immediate: true, flush: 'post' }
|
|
213
261
|
)
|
|
214
262
|
|
|
215
|
-
watch(
|
|
263
|
+
watch(activeLang, applyLanguage)
|
|
264
|
+
|
|
265
|
+
/** langs 变化时,若当前 lang 未设或不在列表中则纠正为 langs[0] */
|
|
266
|
+
watch(
|
|
267
|
+
langs,
|
|
268
|
+
(list) => {
|
|
269
|
+
if (!list.length) return
|
|
270
|
+
if (!lang.value || !list.includes(lang.value)) {
|
|
271
|
+
lang.value = list[0]
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{ immediate: true }
|
|
275
|
+
)
|
|
216
276
|
|
|
217
277
|
watch(
|
|
218
278
|
() => props.dark,
|
|
@@ -229,25 +289,23 @@ watch(readonly, (v) => {
|
|
|
229
289
|
editor.value?.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(v)) })
|
|
230
290
|
})
|
|
231
291
|
|
|
232
|
-
watch(() => props.defaultLines, syncPhantomLineNumbers)
|
|
233
|
-
|
|
234
292
|
watch(
|
|
235
|
-
[
|
|
236
|
-
([v, e]) => {
|
|
293
|
+
[model, editor, shellConfig],
|
|
294
|
+
([v, e], [, , prevShell]) => {
|
|
237
295
|
if (!e) return
|
|
296
|
+
// IME 组字中不接受外部回写,避免打断输入
|
|
238
297
|
if (e.composing) return
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
selection: inRange ? e.state.selection : { anchor: 0, head: 0 },
|
|
249
|
-
scrollIntoView: true
|
|
298
|
+
const config = shellConfig.value
|
|
299
|
+
const shellChanged =
|
|
300
|
+
!prevShell || prevShell.prefix !== config.prefix || prevShell.suffix !== config.suffix
|
|
301
|
+
// 编辑器自身写出的 model 无需回写文档
|
|
302
|
+
if (!shellChanged && v === lastEditorBody) return
|
|
303
|
+
const body = v ?? ''
|
|
304
|
+
syncDocFromModel(e, body, config, {
|
|
305
|
+
reconfigureShell: shellChanged,
|
|
306
|
+
prevShell: prevShell ?? config
|
|
250
307
|
})
|
|
308
|
+
lastEditorBody = body
|
|
251
309
|
},
|
|
252
310
|
{ immediate: true }
|
|
253
311
|
)
|