@veltra/desktop 1.7.5 → 1.7.6

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.
@@ -1,18 +1,21 @@
1
- import _sfc_main$1 from "../scroll/scroll.js";
2
- import _sfc_main$2 from "../select/select.js";
1
+ import _sfc_main$1 from "../icon/icon.js";
2
+ import _sfc_main$2 from "../scroll/scroll.js";
3
+ import _sfc_main$3 from "../select/select.js";
3
4
  import { Compartment, EditorState } from "../../node_modules/.bun/@codemirror_state@6.7.1/node_modules/@codemirror/state/dist/index.js";
4
5
  import { EditorView, tooltips } from "../../node_modules/.bun/@codemirror_view@6.43.9/node_modules/@codemirror/view/dist/index.js";
5
6
  import { oneDark } from "../../node_modules/.bun/@codemirror_theme-one-dark@6.1.3/node_modules/@codemirror/theme-one-dark/dist/index.js";
6
7
  import { basicSetup } from "./basic-setup.js";
7
8
  import { loadLanguage } from "./lang-loaders.js";
8
9
  import { buildFullDoc, extractBodyFromText, getShellRanges, mapSelectionToBody, shellExtension, textEqualsShellDoc } from "./shell-extension.js";
9
- import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, mergeModels, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, shallowRef, toDisplayString, unref, useModel, useTemplateRef, watch, withCtx, withKeys, withModifiers } from "vue";
10
+ import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, mergeModels, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, shallowRef, toDisplayString, unref, useModel, useTemplateRef, watch, withCtx, withKeys, withModifiers } from "vue";
10
11
  import { useFormFallbackProps } from "@veltra/compositions";
11
12
  import { bem, injectFormContext, zIndex } from "@veltra/utils";
13
+ import { Close, ZoomIn } from "@veltra/icons/normal";
12
14
  //#region src/components/code-editor/code-editor.vue
13
15
  const _hoisted_1 = { ref: "container" };
14
16
  const _sfc_main = /*@__PURE__*/ defineComponent({
15
17
  name: "UCodeEditor",
18
+ inheritAttrs: false,
16
19
  __name: "code-editor",
17
20
  props: /*@__PURE__*/ mergeModels({
18
21
  modelValue: {},
@@ -24,6 +27,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
24
27
  type: Boolean,
25
28
  default: false
26
29
  },
30
+ zoomable: {
31
+ type: Boolean,
32
+ default: true
33
+ },
27
34
  defaultLines: { default: 8 },
28
35
  tips: {},
29
36
  span: {},
@@ -60,11 +67,20 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
60
67
  disabled: false,
61
68
  readonly: false
62
69
  });
70
+ /** 放大状态:复用同一组件根 DOM / 同一 EditorView 实例,Teleport 移入 body 全屏遮罩 */
71
+ const zoomed = shallowRef(false);
72
+ /** 放大期间原位置占位的高度(取放大前组件根高度) */
73
+ const placeholderHeight = shallowRef("");
74
+ /** 放大遮罩的 z 轴层级,进入放大时取一次,避免渲染期反复自增 */
75
+ const zoomZIndex = shallowRef();
76
+ const rootRef = useTemplateRef("root");
77
+ const zoomWrapRef = useTemplateRef("zoomWrap");
63
78
  const className = computed(() => [
64
79
  cls.b,
65
80
  bem.is("disabled", disabled.value),
66
81
  bem.is("readonly", readonly.value),
67
- bem.is("dark", props.dark)
82
+ bem.is("dark", props.dark),
83
+ bem.is("zoomed", zoomed.value)
68
84
  ]);
69
85
  const rootStyle = computed(() => ({ "--u-code-editor-default-lines": Math.max(1, props.defaultLines) }));
70
86
  const langs = computed(() => props.langs ?? []);
@@ -77,6 +93,29 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
77
93
  const activeLang = computed(() => lang.value ?? langs.value[0]);
78
94
  /** 仅一种语言时,在选择器位置展示名称 */
79
95
  const langLabel = computed(() => langs.value.length === 1 && activeLang.value ? activeLang.value.toUpperCase() : void 0);
96
+ /** 语言标识或放大/关闭按钮任一存在时才渲染工具栏 */
97
+ const hasToolbar = computed(() => showLangSelect.value || !!langLabel.value || props.zoomable || zoomed.value);
98
+ function enterZoom() {
99
+ if (disabled.value || zoomed.value) return;
100
+ const root = rootRef.value;
101
+ if (root) placeholderHeight.value = `${root.offsetHeight}px`;
102
+ zoomZIndex.value = zIndex();
103
+ zoomed.value = true;
104
+ editor.value?.dispatch({ effects: tooltipThemeCompartment.reconfigure(EditorView.theme({ ".cm-tooltip": { zIndex: zIndex() } })) });
105
+ nextTick(() => zoomWrapRef.value?.focus());
106
+ }
107
+ function exitZoom() {
108
+ if (!zoomed.value) return;
109
+ zoomed.value = false;
110
+ }
111
+ /** 放大期间锁定 body 滚动,退出后恢复原值 */
112
+ let prevBodyOverflow = "";
113
+ watch(zoomed, (v) => {
114
+ if (v) {
115
+ prevBodyOverflow = document.body.style.overflow;
116
+ document.body.style.overflow = "hidden";
117
+ } else document.body.style.overflow = prevBodyOverflow;
118
+ });
80
119
  const shellConfig = computed(() => ({
81
120
  prefix: props.prefix ?? "",
82
121
  suffix: props.suffix ?? ""
@@ -88,6 +127,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
88
127
  const readOnlyCompartment = new Compartment();
89
128
  const langCompartment = new Compartment();
90
129
  const shellCompartment = new Compartment();
130
+ const tooltipThemeCompartment = new Compartment();
91
131
  /** 聚焦时的正文快照,用于失焦时判断是否触发 change */
92
132
  let focusSnapshot = "";
93
133
  /**
@@ -108,7 +148,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
108
148
  return [
109
149
  basicSetup,
110
150
  tooltips({ parent: document.body }),
111
- EditorView.theme({ ".cm-tooltip": { zIndex: zIndex() } }),
151
+ tooltipThemeCompartment.of(EditorView.theme({ ".cm-tooltip": { zIndex: zIndex() } })),
112
152
  themeCompartment.of(props.dark ? oneDark : []),
113
153
  editableCompartment.of(EditorView.editable.of(!disabled.value)),
114
154
  readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),
@@ -246,14 +286,35 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
246
286
  });
247
287
  lastEditorBody = body;
248
288
  }, { immediate: true });
249
- onBeforeUnmount(destroyEditor);
289
+ onBeforeUnmount(() => {
290
+ destroyEditor();
291
+ if (zoomed.value) document.body.style.overflow = prevBodyOverflow;
292
+ });
250
293
  return (_ctx, _cache) => {
251
- return openBlock(), createBlock(unref(_sfc_main$1), {
252
- class: normalizeClass(className.value),
253
- style: normalizeStyle(rootStyle.value),
254
- onKeyup: _cache[1] || (_cache[1] = withKeys(withModifiers(() => {}, ["stop"]), ["enter"]))
255
- }, {
256
- default: withCtx(() => [createElementVNode("div", _hoisted_1, null, 512), showLangSelect.value ? (openBlock(), createBlock(unref(_sfc_main$2), {
294
+ return openBlock(), createElementBlock(Fragment, null, [
295
+ createCommentVNode(" 放大时原位置的等高占位,避免页面布局位移 "),
296
+ zoomed.value ? (openBlock(), createElementBlock("div", {
297
+ key: 0,
298
+ class: normalizeClass(unref(cls).e("placeholder")),
299
+ style: normalizeStyle({ height: placeholderHeight.value })
300
+ }, null, 6)) : createCommentVNode("v-if", true),
301
+ (openBlock(), createBlock(Teleport, {
302
+ to: "body",
303
+ disabled: !zoomed.value
304
+ }, [createElementVNode("div", {
305
+ ref: "zoomWrap",
306
+ class: normalizeClass([unref(cls).e("zoom-wrap"), unref(bem).is("zoomed", zoomed.value)]),
307
+ style: normalizeStyle(zoomed.value ? { zIndex: zoomZIndex.value } : void 0),
308
+ tabindex: "-1",
309
+ onKeyup: withKeys(exitZoom, ["esc"])
310
+ }, [createElementVNode("div", mergeProps({ ref: "root" }, _ctx.$attrs, {
311
+ class: className.value,
312
+ style: rootStyle.value,
313
+ onKeyup: _cache[1] || (_cache[1] = withKeys(withModifiers(() => {}, ["stop"]), ["enter"]))
314
+ }), [hasToolbar.value ? (openBlock(), createElementBlock("div", {
315
+ key: 0,
316
+ class: normalizeClass(unref(cls).e("toolbar"))
317
+ }, [showLangSelect.value ? (openBlock(), createBlock(unref(_sfc_main$3), {
257
318
  key: 0,
258
319
  class: normalizeClass(unref(cls).e("lang-select")),
259
320
  modelValue: lang.value,
@@ -270,9 +331,27 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
270
331
  ])) : langLabel.value ? (openBlock(), createElementBlock("span", {
271
332
  key: 1,
272
333
  class: normalizeClass(unref(cls).e("lang-label"))
273
- }, toDisplayString(langLabel.value), 3)) : createCommentVNode("v-if", true)]),
274
- _: 1
275
- }, 8, ["class", "style"]);
334
+ }, toDisplayString(langLabel.value), 3)) : createCommentVNode("v-if", true), createElementVNode("div", { class: normalizeClass(unref(cls).e("toolbar-actions")) }, [zoomed.value ? (openBlock(), createBlock(unref(_sfc_main$1), {
335
+ key: 0,
336
+ class: normalizeClass(unref(cls).e("btn-close")),
337
+ title: "关闭",
338
+ onClick: exitZoom
339
+ }, {
340
+ default: withCtx(() => [createVNode(unref(Close))]),
341
+ _: 1
342
+ }, 8, ["class"])) : __props.zoomable ? (openBlock(), createBlock(unref(_sfc_main$1), {
343
+ key: 1,
344
+ class: normalizeClass([unref(cls).e("btn-zoom"), unref(bem).is("disabled", unref(disabled))]),
345
+ title: "放大",
346
+ onClick: enterZoom
347
+ }, {
348
+ default: withCtx(() => [createVNode(unref(ZoomIn))]),
349
+ _: 1
350
+ }, 8, ["class"])) : createCommentVNode("v-if", true)], 2)], 2)) : createCommentVNode("v-if", true), createVNode(unref(_sfc_main$2), { class: normalizeClass(unref(cls).e("scroll")) }, {
351
+ default: withCtx(() => [createElementVNode("div", _hoisted_1, null, 512)]),
352
+ _: 1
353
+ }, 8, ["class"])], 16)], 38)], 8, ["disabled"]))
354
+ ], 64);
276
355
  };
277
356
  }
278
357
  });
@@ -1 +1 @@
1
- {"version":3,"file":"code-editor.js","names":[],"sources":["../../../src/components/code-editor/code-editor.vue"],"sourcesContent":["<template>\n <u-scroll :class=\"className\" :style=\"rootStyle\" @keyup.enter.stop>\n <div ref=\"container\"></div>\n\n <u-select\n v-if=\"showLangSelect\"\n :class=\"cls.e('lang-select')\"\n v-model=\"lang\"\n size=\"small\"\n :options=\"langOptions\"\n :clearable=\"false\"\n :disabled=\"disabled\"\n />\n <span v-else-if=\"langLabel\" :class=\"cls.e('lang-label')\">{{ langLabel }}</span>\n </u-scroll>\n</template>\n\n<script lang=\"ts\" setup>\nimport { Compartment, EditorState, type Extension, type Text } from '@codemirror/state'\nimport { oneDark } from '@codemirror/theme-one-dark'\nimport { EditorView, tooltips } from '@codemirror/view'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { bem, injectFormContext, zIndex } from '@veltra/utils'\nimport {\n computed,\n onBeforeUnmount,\n shallowRef,\n useTemplateRef,\n watch,\n type CSSProperties\n} from 'vue'\n\nimport type { CodeEditorEmits, CodeEditorLang, CodeEditorProps } from '../../types'\nimport { UScroll } from '../scroll'\nimport { USelect } from '../select'\nimport { basicSetup } from './basic-setup'\nimport { loadLanguage } from './lang-loaders'\nimport {\n buildFullDoc,\n extractBodyFromText,\n getShellRanges,\n mapSelectionToBody,\n shellExtension,\n textEqualsShellDoc,\n type ShellConfig\n} from './shell-extension'\n\ndefineOptions({ name: 'UCodeEditor' })\n\nconst props = withDefaults(defineProps<CodeEditorProps>(), {\n disabled: undefined,\n readonly: undefined,\n dark: false,\n defaultLines: 8\n})\n\nconst emit = defineEmits<CodeEditorEmits>()\n\nconst model = defineModel<string>()\nconst lang = defineModel<CodeEditorLang>('lang')\n\nconst cls = bem('code-editor')\n\nconst { formProps } = injectFormContext()\n\nconst { disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n disabled: false,\n readonly: false\n})\n\nconst className = computed<string[]>(() => [\n cls.b,\n bem.is('disabled', disabled.value),\n bem.is('readonly', readonly.value),\n bem.is('dark', props.dark)\n])\n\nconst rootStyle = computed<CSSProperties>(() => ({\n '--u-code-editor-default-lines': Math.max(1, props.defaultLines)\n}))\n\nconst langs = computed(() => props.langs ?? [])\n\nconst showLangSelect = computed(() => langs.value.length > 1)\n\nconst langOptions = computed(() =>\n langs.value.map((value) => ({ label: value.toUpperCase(), value }))\n)\n\n/** 实际生效的语言:优先 lang model,否则回落 langs[0] */\nconst activeLang = computed<CodeEditorLang | undefined>(() => lang.value ?? langs.value[0])\n\n/** 仅一种语言时,在选择器位置展示名称 */\nconst langLabel = computed(() =>\n langs.value.length === 1 && activeLang.value ? activeLang.value.toUpperCase() : undefined\n)\n\nconst shellConfig = computed<ShellConfig>(() => ({\n prefix: props.prefix ?? '',\n suffix: props.suffix ?? ''\n}))\n\nconst containerRef = useTemplateRef('container')\nconst editor = shallowRef<EditorView | null>(null)\n\nconst themeCompartment = new Compartment()\nconst editableCompartment = new Compartment()\nconst readOnlyCompartment = new Compartment()\nconst langCompartment = new Compartment()\nconst shellCompartment = new Compartment()\n\n/** 聚焦时的正文快照,用于失焦时判断是否触发 change */\nlet focusSnapshot = ''\n\n/**\n * 编辑器最近一次写出的正文引用。\n * 用于短路 model → doc 同步,避免每次按键对整篇文档做 O(n) 相等比较。\n */\nlet lastEditorBody: string | undefined\n\n/** 上一帧是否处于 IME composition,用于在 composition 结束时补一次 model 同步 */\nlet wasComposing = false\n\n/** 将编辑器正文写回 v-model(跳过与上次相同的值,避免多余更新) */\nfunction writeModelFromDoc(doc: Text) {\n const nextBody = extractBodyFromText(doc, shellConfig.value)\n if (nextBody === lastEditorBody && nextBody === model.value) return\n lastEditorBody = nextBody\n model.value = nextBody\n}\n\nfunction buildExtensions(): Extension[] {\n return [\n basicSetup,\n tooltips({ parent: document.body }),\n EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } }),\n themeCompartment.of(props.dark ? oneDark : []),\n editableCompartment.of(EditorView.editable.of(!disabled.value)),\n readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),\n langCompartment.of([]),\n shellCompartment.of(shellExtension(shellConfig.value)),\n EditorView.updateListener.of((update) => {\n const composing = update.view.composing\n\n // IME 进行中不写回,避免中间态触发父级重渲染;结束后再统一同步\n if (update.docChanged && !composing) {\n writeModelFromDoc(update.state.doc)\n } else if (wasComposing && !composing) {\n writeModelFromDoc(update.state.doc)\n }\n wasComposing = composing\n\n if (!update.focusChanged) return\n if (update.view.hasFocus) {\n focusSnapshot = extractBodyFromText(update.state.doc, shellConfig.value)\n return\n }\n const current = extractBodyFromText(update.state.doc, shellConfig.value)\n if (current !== focusSnapshot) {\n emit('change', current)\n }\n })\n ]\n}\n\nlet editorToken = 0\nlet langToken = 0\n\nasync function applyLanguage() {\n const view = editor.value\n if (!view) return\n const myToken = ++langToken\n const language = activeLang.value\n\n if (!language) {\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure([]) })\n return\n }\n\n const ext = await loadLanguage(language)\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure(ext) })\n}\n\nfunction destroyEditor() {\n ++editorToken\n ++langToken\n editor.value?.destroy()\n editor.value = null\n lastEditorBody = undefined\n wasComposing = false\n}\n\nfunction createEditor() {\n destroyEditor()\n const myToken = ++editorToken\n const parent = containerRef.value\n if (!parent) return\n\n const config = shellConfig.value\n const body = model.value ?? ''\n const doc = buildFullDoc(body, config)\n const { bodyStart } = getShellRanges(doc.length, config)\n lastEditorBody = body\n\n const view = new EditorView({\n doc,\n selection: { anchor: bodyStart, head: bodyStart },\n extensions: buildExtensions(),\n parent\n })\n\n if (myToken !== editorToken) {\n view.destroy()\n return\n }\n\n editor.value = view\n applyLanguage()\n}\n\n/**\n * 将 model 正文同步为完整文档;可选同时 reconfigure shell,\n * 避免 prefix/suffix 变更时「先改扩展、后改文档」导致选区越界。\n */\nfunction syncDocFromModel(\n view: EditorView,\n body: string,\n config: ShellConfig,\n options?: { reconfigureShell?: boolean; prevShell?: ShellConfig }\n) {\n const current = view.state.doc\n const docChanged = !textEqualsShellDoc(current, body, config)\n const reconfigureShell = options?.reconfigureShell ?? false\n\n if (!docChanged && !reconfigureShell) return\n\n const nextDoc = buildFullDoc(body, config)\n const prevShell = options?.prevShell ?? config\n const prevRanges = getShellRanges(current.length, prevShell)\n const nextRanges = getShellRanges(nextDoc.length, config)\n const selection = mapSelectionToBody(view.state.selection, prevRanges, nextRanges, nextDoc.length)\n\n view.dispatch({\n ...(docChanged\n ? {\n changes: { from: 0, to: current.length, insert: nextDoc },\n selection,\n // 仅外壳变更时滚入视口;外部 model 回写不应打断用户滚动位置\n ...(reconfigureShell ? { scrollIntoView: true } : {})\n }\n : { selection }),\n ...(reconfigureShell ? { effects: shellCompartment.reconfigure(shellExtension(config)) } : {}),\n // 绕过 shell 的 change/transactionFilter,避免全量替换被拦截或选区被错误钳制\n filter: false\n })\n}\n\nwatch(\n containerRef,\n (el) => {\n if (el) createEditor()\n else destroyEditor()\n },\n { immediate: true, flush: 'post' }\n)\n\nwatch(activeLang, applyLanguage)\n\n/** langs 变化时,若当前 lang 未设或不在列表中则纠正为 langs[0] */\nwatch(\n langs,\n (list) => {\n if (!list.length) return\n if (!lang.value || !list.includes(lang.value)) {\n lang.value = list[0]\n }\n },\n { immediate: true }\n)\n\nwatch(\n () => props.dark,\n (dark) => {\n editor.value?.dispatch({ effects: themeCompartment.reconfigure(dark ? oneDark : []) })\n }\n)\n\nwatch(disabled, (v) => {\n editor.value?.dispatch({ effects: editableCompartment.reconfigure(EditorView.editable.of(!v)) })\n})\n\nwatch(readonly, (v) => {\n editor.value?.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(v)) })\n})\n\nwatch(\n [model, editor, shellConfig],\n ([v, e], [, , prevShell]) => {\n if (!e) return\n // IME 组字中不接受外部回写,避免打断输入\n if (e.composing) return\n const config = shellConfig.value\n const shellChanged =\n !prevShell || prevShell.prefix !== config.prefix || prevShell.suffix !== config.suffix\n // 编辑器自身写出的 model 无需回写文档\n if (!shellChanged && v === lastEditorBody) return\n const body = v ?? ''\n syncDocFromModel(e, body, config, {\n reconfigureShell: shellChanged,\n prevShell: prevShell ?? config\n })\n lastEditorBody = body\n },\n { immediate: true }\n)\n\nonBeforeUnmount(destroyEditor)\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"code-editor.js","names":["$attrs"],"sources":["../../../src/components/code-editor/code-editor.vue"],"sourcesContent":["<template>\n <!-- 放大时原位置的等高占位,避免页面布局位移 -->\n <div v-if=\"zoomed\" :class=\"cls.e('placeholder')\" :style=\"{ height: placeholderHeight }\" />\n\n <Teleport to=\"body\" :disabled=\"!zoomed\">\n <div\n ref=\"zoomWrap\"\n :class=\"[cls.e('zoom-wrap'), bem.is('zoomed', zoomed)]\"\n :style=\"zoomed ? { zIndex: zoomZIndex } : undefined\"\n tabindex=\"-1\"\n @keyup.esc=\"exitZoom\"\n >\n <div ref=\"root\" v-bind=\"$attrs\" :class=\"className\" :style=\"rootStyle\" @keyup.enter.stop>\n <div v-if=\"hasToolbar\" :class=\"cls.e('toolbar')\">\n <u-select\n v-if=\"showLangSelect\"\n :class=\"cls.e('lang-select')\"\n v-model=\"lang\"\n size=\"small\"\n :options=\"langOptions\"\n :clearable=\"false\"\n :disabled=\"disabled\"\n />\n <span v-else-if=\"langLabel\" :class=\"cls.e('lang-label')\">{{ langLabel }}</span>\n\n <div :class=\"cls.e('toolbar-actions')\">\n <u-icon v-if=\"zoomed\" :class=\"cls.e('btn-close')\" title=\"关闭\" @click=\"exitZoom\">\n <Close />\n </u-icon>\n <u-icon\n v-else-if=\"zoomable\"\n :class=\"[cls.e('btn-zoom'), bem.is('disabled', disabled)]\"\n title=\"放大\"\n @click=\"enterZoom\"\n >\n <ZoomIn />\n </u-icon>\n </div>\n </div>\n\n <u-scroll :class=\"cls.e('scroll')\">\n <div ref=\"container\"></div>\n </u-scroll>\n </div>\n </div>\n </Teleport>\n</template>\n\n<script lang=\"ts\" setup>\nimport { Compartment, EditorState, type Extension, type Text } from '@codemirror/state'\nimport { oneDark } from '@codemirror/theme-one-dark'\nimport { EditorView, tooltips } from '@codemirror/view'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { ZoomIn, Close } from '@veltra/icons/normal'\nimport { bem, injectFormContext, zIndex } from '@veltra/utils'\nimport {\n computed,\n nextTick,\n onBeforeUnmount,\n shallowRef,\n useTemplateRef,\n watch,\n type CSSProperties\n} from 'vue'\n\nimport type { CodeEditorEmits, CodeEditorLang, CodeEditorProps } from '../../types'\nimport { UIcon } from '../icon'\nimport { UScroll } from '../scroll'\nimport { USelect } from '../select'\nimport { basicSetup } from './basic-setup'\nimport { loadLanguage } from './lang-loaders'\nimport {\n buildFullDoc,\n extractBodyFromText,\n getShellRanges,\n mapSelectionToBody,\n shellExtension,\n textEqualsShellDoc,\n type ShellConfig\n} from './shell-extension'\n\ndefineOptions({ name: 'UCodeEditor', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<CodeEditorProps>(), {\n disabled: undefined,\n readonly: undefined,\n dark: false,\n zoomable: true,\n defaultLines: 8\n})\n\nconst emit = defineEmits<CodeEditorEmits>()\n\nconst model = defineModel<string>()\nconst lang = defineModel<CodeEditorLang>('lang')\n\nconst cls = bem('code-editor')\n\nconst { formProps } = injectFormContext()\n\nconst { disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n disabled: false,\n readonly: false\n})\n\n/** 放大状态:复用同一组件根 DOM / 同一 EditorView 实例,Teleport 移入 body 全屏遮罩 */\nconst zoomed = shallowRef(false)\n\n/** 放大期间原位置占位的高度(取放大前组件根高度) */\nconst placeholderHeight = shallowRef('')\n\n/** 放大遮罩的 z 轴层级,进入放大时取一次,避免渲染期反复自增 */\nconst zoomZIndex = shallowRef<number>()\n\nconst rootRef = useTemplateRef<HTMLElement>('root')\nconst zoomWrapRef = useTemplateRef<HTMLElement>('zoomWrap')\n\nconst className = computed<string[]>(() => [\n cls.b,\n bem.is('disabled', disabled.value),\n bem.is('readonly', readonly.value),\n bem.is('dark', props.dark),\n bem.is('zoomed', zoomed.value)\n])\n\nconst rootStyle = computed<CSSProperties>(() => ({\n '--u-code-editor-default-lines': Math.max(1, props.defaultLines)\n}))\n\nconst langs = computed(() => props.langs ?? [])\n\nconst showLangSelect = computed(() => langs.value.length > 1)\n\nconst langOptions = computed(() =>\n langs.value.map((value) => ({ label: value.toUpperCase(), value }))\n)\n\n/** 实际生效的语言:优先 lang model,否则回落 langs[0] */\nconst activeLang = computed<CodeEditorLang | undefined>(() => lang.value ?? langs.value[0])\n\n/** 仅一种语言时,在选择器位置展示名称 */\nconst langLabel = computed(() =>\n langs.value.length === 1 && activeLang.value ? activeLang.value.toUpperCase() : undefined\n)\n\n/** 语言标识或放大/关闭按钮任一存在时才渲染工具栏 */\nconst hasToolbar = computed(\n () => showLangSelect.value || !!langLabel.value || props.zoomable || zoomed.value\n)\n\nfunction enterZoom() {\n if (disabled.value || zoomed.value) return\n const root = rootRef.value\n if (root) placeholderHeight.value = `${root.offsetHeight}px`\n zoomZIndex.value = zIndex()\n zoomed.value = true\n // 遮罩层级高于创建编辑器时写入的 tooltip 层级,同步抬升避免补全提示被遮罩盖住\n editor.value?.dispatch({\n effects: tooltipThemeCompartment.reconfigure(\n EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } })\n )\n })\n // 对齐 dialog 先例:放大按钮(u-icon)不可聚焦,点击后焦点在 body 上,\n // keyup 不会向下经过遮罩,需主动聚焦遮罩,Esc 才能冒泡到 keyup.esc\n nextTick(() => zoomWrapRef.value?.focus())\n}\n\nfunction exitZoom() {\n if (!zoomed.value) return\n zoomed.value = false\n}\n\n/** 放大期间锁定 body 滚动,退出后恢复原值 */\nlet prevBodyOverflow = ''\n\nwatch(zoomed, (v) => {\n if (v) {\n prevBodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n } else {\n document.body.style.overflow = prevBodyOverflow\n }\n})\n\nconst shellConfig = computed<ShellConfig>(() => ({\n prefix: props.prefix ?? '',\n suffix: props.suffix ?? ''\n}))\n\nconst containerRef = useTemplateRef('container')\nconst editor = shallowRef<EditorView | null>(null)\n\nconst themeCompartment = new Compartment()\nconst editableCompartment = new Compartment()\nconst readOnlyCompartment = new Compartment()\nconst langCompartment = new Compartment()\nconst shellCompartment = new Compartment()\nconst tooltipThemeCompartment = new Compartment()\n\n/** 聚焦时的正文快照,用于失焦时判断是否触发 change */\nlet focusSnapshot = ''\n\n/**\n * 编辑器最近一次写出的正文引用。\n * 用于短路 model → doc 同步,避免每次按键对整篇文档做 O(n) 相等比较。\n */\nlet lastEditorBody: string | undefined\n\n/** 上一帧是否处于 IME composition,用于在 composition 结束时补一次 model 同步 */\nlet wasComposing = false\n\n/** 将编辑器正文写回 v-model(跳过与上次相同的值,避免多余更新) */\nfunction writeModelFromDoc(doc: Text) {\n const nextBody = extractBodyFromText(doc, shellConfig.value)\n if (nextBody === lastEditorBody && nextBody === model.value) return\n lastEditorBody = nextBody\n model.value = nextBody\n}\n\nfunction buildExtensions(): Extension[] {\n return [\n basicSetup,\n tooltips({ parent: document.body }),\n tooltipThemeCompartment.of(EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } })),\n themeCompartment.of(props.dark ? oneDark : []),\n editableCompartment.of(EditorView.editable.of(!disabled.value)),\n readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),\n langCompartment.of([]),\n shellCompartment.of(shellExtension(shellConfig.value)),\n EditorView.updateListener.of((update) => {\n const composing = update.view.composing\n\n // IME 进行中不写回,避免中间态触发父级重渲染;结束后再统一同步\n if (update.docChanged && !composing) {\n writeModelFromDoc(update.state.doc)\n } else if (wasComposing && !composing) {\n writeModelFromDoc(update.state.doc)\n }\n wasComposing = composing\n\n if (!update.focusChanged) return\n if (update.view.hasFocus) {\n focusSnapshot = extractBodyFromText(update.state.doc, shellConfig.value)\n return\n }\n const current = extractBodyFromText(update.state.doc, shellConfig.value)\n if (current !== focusSnapshot) {\n emit('change', current)\n }\n })\n ]\n}\n\nlet editorToken = 0\nlet langToken = 0\n\nasync function applyLanguage() {\n const view = editor.value\n if (!view) return\n const myToken = ++langToken\n const language = activeLang.value\n\n if (!language) {\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure([]) })\n return\n }\n\n const ext = await loadLanguage(language)\n if (myToken !== langToken || editor.value !== view) return\n view.dispatch({ effects: langCompartment.reconfigure(ext) })\n}\n\nfunction destroyEditor() {\n ++editorToken\n ++langToken\n editor.value?.destroy()\n editor.value = null\n lastEditorBody = undefined\n wasComposing = false\n}\n\nfunction createEditor() {\n destroyEditor()\n const myToken = ++editorToken\n const parent = containerRef.value\n if (!parent) return\n\n const config = shellConfig.value\n const body = model.value ?? ''\n const doc = buildFullDoc(body, config)\n const { bodyStart } = getShellRanges(doc.length, config)\n lastEditorBody = body\n\n const view = new EditorView({\n doc,\n selection: { anchor: bodyStart, head: bodyStart },\n extensions: buildExtensions(),\n parent\n })\n\n if (myToken !== editorToken) {\n view.destroy()\n return\n }\n\n editor.value = view\n applyLanguage()\n}\n\n/**\n * 将 model 正文同步为完整文档;可选同时 reconfigure shell,\n * 避免 prefix/suffix 变更时「先改扩展、后改文档」导致选区越界。\n */\nfunction syncDocFromModel(\n view: EditorView,\n body: string,\n config: ShellConfig,\n options?: { reconfigureShell?: boolean; prevShell?: ShellConfig }\n) {\n const current = view.state.doc\n const docChanged = !textEqualsShellDoc(current, body, config)\n const reconfigureShell = options?.reconfigureShell ?? false\n\n if (!docChanged && !reconfigureShell) return\n\n const nextDoc = buildFullDoc(body, config)\n const prevShell = options?.prevShell ?? config\n const prevRanges = getShellRanges(current.length, prevShell)\n const nextRanges = getShellRanges(nextDoc.length, config)\n const selection = mapSelectionToBody(view.state.selection, prevRanges, nextRanges, nextDoc.length)\n\n view.dispatch({\n ...(docChanged\n ? {\n changes: { from: 0, to: current.length, insert: nextDoc },\n selection,\n // 仅外壳变更时滚入视口;外部 model 回写不应打断用户滚动位置\n ...(reconfigureShell ? { scrollIntoView: true } : {})\n }\n : { selection }),\n ...(reconfigureShell ? { effects: shellCompartment.reconfigure(shellExtension(config)) } : {}),\n // 绕过 shell 的 change/transactionFilter,避免全量替换被拦截或选区被错误钳制\n filter: false\n })\n}\n\nwatch(\n containerRef,\n (el) => {\n if (el) createEditor()\n else destroyEditor()\n },\n { immediate: true, flush: 'post' }\n)\n\nwatch(activeLang, applyLanguage)\n\n/** langs 变化时,若当前 lang 未设或不在列表中则纠正为 langs[0] */\nwatch(\n langs,\n (list) => {\n if (!list.length) return\n if (!lang.value || !list.includes(lang.value)) {\n lang.value = list[0]\n }\n },\n { immediate: true }\n)\n\nwatch(\n () => props.dark,\n (dark) => {\n editor.value?.dispatch({ effects: themeCompartment.reconfigure(dark ? oneDark : []) })\n }\n)\n\nwatch(disabled, (v) => {\n editor.value?.dispatch({ effects: editableCompartment.reconfigure(EditorView.editable.of(!v)) })\n})\n\nwatch(readonly, (v) => {\n editor.value?.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(v)) })\n})\n\nwatch(\n [model, editor, shellConfig],\n ([v, e], [, , prevShell]) => {\n if (!e) return\n // IME 组字中不接受外部回写,避免打断输入\n if (e.composing) return\n const config = shellConfig.value\n const shellChanged =\n !prevShell || prevShell.prefix !== config.prefix || prevShell.suffix !== config.suffix\n // 编辑器自身写出的 model 无需回写文档\n if (!shellChanged && v === lastEditorBody) return\n const body = v ?? ''\n syncDocFromModel(e, body, config, {\n reconfigureShell: shellChanged,\n prevShell: prevShell ?? config\n })\n lastEditorBody = body\n },\n { immediate: true }\n)\n\nonBeforeUnmount(() => {\n destroyEditor()\n if (zoomed.value) document.body.style.overflow = prevBodyOverflow\n})\n</script>\n"],"mappings":"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"}
@@ -1,2 +1,3 @@
1
+ import "../icon/style.js";
1
2
  import "../select/style.js";
2
3
  import "./style2.css";
@@ -27,6 +27,8 @@
27
27
  --u-code-editor-active-bg: var(--u-color-primary-a-6);
28
28
  --u-code-editor-selection-bg: var(--u-color-primary-a-16);
29
29
  position: relative;
30
+ display: flex;
31
+ flex-direction: column;
30
32
  width: 100%;
31
33
  max-height: 480px;
32
34
  border-radius: var(--u-radius-default);
@@ -39,28 +41,67 @@
39
41
  .u-code-editor:hover, .u-code-editor:focus-within {
40
42
  border-color: var(--u-color-primary);
41
43
  }
44
+ .u-code-editor__toolbar {
45
+ flex-shrink: 0;
46
+ padding: 4px var(--u-gap-small);
47
+ border-bottom: 1px solid var(--u-code-editor-border);
48
+ display: flex;
49
+ justify-content: space-between;
50
+ align-items: center;
51
+ flex-wrap: nowrap;
52
+ }
53
+ .u-code-editor__toolbar-actions {
54
+ margin-left: auto;
55
+ gap: var(--u-gap-small);
56
+ display: flex;
57
+ justify-content: flex-start;
58
+ align-items: center;
59
+ flex-wrap: nowrap;
60
+ }
42
61
  .u-code-editor__lang-select {
43
- position: absolute;
44
- top: 2px;
45
- right: 2px;
46
- z-index: 2;
47
62
  width: 88px;
48
63
  border: none;
49
64
  }
50
- .u-code-editor__lang-select input {
51
- text-align: right;
52
- }
53
65
  .u-code-editor__lang-label {
54
- position: absolute;
55
- top: 2px;
56
- right: 8px;
57
- z-index: 2;
58
66
  font-size: 12px;
59
- line-height: 24px;
60
67
  color: var(--u-code-editor-gutter-fg);
61
- pointer-events: none;
62
68
  user-select: none;
63
69
  }
70
+ .u-code-editor__btn-zoom, .u-code-editor__btn-close {
71
+ cursor: pointer;
72
+ font-size: 14px;
73
+ color: var(--u-code-editor-gutter-fg);
74
+ transition: color var(--u-transition-fast) var(--u-transition-ease);
75
+ }
76
+ .u-code-editor__btn-zoom:hover, .u-code-editor__btn-close:hover {
77
+ color: var(--u-color-primary);
78
+ }
79
+ .u-code-editor__btn-zoom.is-disabled, .u-code-editor__btn-close.is-disabled {
80
+ cursor: not-allowed;
81
+ }
82
+ .u-code-editor__btn-zoom.is-disabled:hover, .u-code-editor__btn-close.is-disabled:hover {
83
+ color: var(--u-code-editor-gutter-fg);
84
+ }
85
+ .u-code-editor__scroll {
86
+ flex: 1;
87
+ min-height: 0;
88
+ }
89
+ .u-code-editor__placeholder {
90
+ box-sizing: border-box;
91
+ width: 100%;
92
+ }
93
+ .u-code-editor__zoom-wrap {
94
+ display: contents;
95
+ }
96
+ .u-code-editor__zoom-wrap.is-zoomed {
97
+ position: fixed;
98
+ inset: 0;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ background-color: rgba(0, 0, 0, 0.32);
103
+ outline: none;
104
+ }
64
105
  .u-code-editor .cm-editor {
65
106
  background: transparent;
66
107
  color: var(--u-code-editor-fg);
@@ -113,6 +154,13 @@
113
154
  --u-code-editor-active-bg: rgba(255, 255, 255, 0.04);
114
155
  --u-code-editor-selection-bg: rgba(99, 162, 255, 0.25);
115
156
  }
157
+ .u-code-editor.is-zoomed {
158
+ width: 80vw;
159
+ height: 80vh;
160
+ max-height: none;
161
+ border-radius: var(--u-radius-large);
162
+ box-shadow: var(--u-shadow-lg);
163
+ }
116
164
  .u-code-editor.is-disabled {
117
165
  background-color: var(--u-color-disabled);
118
166
  box-shadow: none;
@@ -5,7 +5,7 @@ type CodeEditorLang = 'js' | 'sql' | 'java' | 'json' | 'markdown' | 'spel' | 'ba
5
5
  interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
6
6
  modelValue?: string;
7
7
  /**
8
- * 可选语言列表。长度大于 1 时在编辑器右上角显示内置语言选择器;
8
+ * 可选语言列表。长度大于 1 时在顶部工具栏显示内置语言选择器;
9
9
  * 仅一种语言时显示语言名称标签
10
10
  */
11
11
  langs?: CodeEditorLang[];
@@ -17,6 +17,8 @@ interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
17
17
  suffix?: string;
18
18
  /** 是否使用暗色主题 */
19
19
  dark?: boolean;
20
+ /** 是否显示放大按钮,默认 true;false 时不渲染放大按钮 */
21
+ zoomable?: boolean;
20
22
  /**
21
23
  * 默认显示的行数,用于撑起编辑器最小高度,超出后滚动
22
24
  * @default 8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.7.5",
3
+ "version": "1.7.6",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -80,12 +80,12 @@
80
80
  "peerDependencies": {
81
81
  "@cat-kit/core": ">=1.2.1",
82
82
  "@cat-kit/fe": ">=1.2.1",
83
- "@veltra/compositions": "^1.7.5",
84
- "@veltra/directives": "^1.7.5",
83
+ "@veltra/compositions": "^1.7.6",
84
+ "@veltra/directives": "^1.7.6",
85
85
  "@veltra/icons": "^1.5.0",
86
86
  "@veltra/sheet-core": "^2.5.4",
87
- "@veltra/styles": "^1.7.5",
88
- "@veltra/utils": "^1.7.5",
87
+ "@veltra/styles": "^1.7.6",
88
+ "@veltra/utils": "^1.7.6",
89
89
  "vue": ">=3.5.42"
90
90
  },
91
91
  "peerDependenciesMeta": {
@@ -1,18 +1,49 @@
1
1
  <template>
2
- <u-scroll :class="className" :style="rootStyle" @keyup.enter.stop>
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
- />
14
- <span v-else-if="langLabel" :class="cls.e('lang-label')">{{ langLabel }}</span>
15
- </u-scroll>
2
+ <!-- 放大时原位置的等高占位,避免页面布局位移 -->
3
+ <div v-if="zoomed" :class="cls.e('placeholder')" :style="{ height: placeholderHeight }" />
4
+
5
+ <Teleport to="body" :disabled="!zoomed">
6
+ <div
7
+ ref="zoomWrap"
8
+ :class="[cls.e('zoom-wrap'), bem.is('zoomed', zoomed)]"
9
+ :style="zoomed ? { zIndex: zoomZIndex } : undefined"
10
+ tabindex="-1"
11
+ @keyup.esc="exitZoom"
12
+ >
13
+ <div ref="root" v-bind="$attrs" :class="className" :style="rootStyle" @keyup.enter.stop>
14
+ <div v-if="hasToolbar" :class="cls.e('toolbar')">
15
+ <u-select
16
+ v-if="showLangSelect"
17
+ :class="cls.e('lang-select')"
18
+ v-model="lang"
19
+ size="small"
20
+ :options="langOptions"
21
+ :clearable="false"
22
+ :disabled="disabled"
23
+ />
24
+ <span v-else-if="langLabel" :class="cls.e('lang-label')">{{ langLabel }}</span>
25
+
26
+ <div :class="cls.e('toolbar-actions')">
27
+ <u-icon v-if="zoomed" :class="cls.e('btn-close')" title="关闭" @click="exitZoom">
28
+ <Close />
29
+ </u-icon>
30
+ <u-icon
31
+ v-else-if="zoomable"
32
+ :class="[cls.e('btn-zoom'), bem.is('disabled', disabled)]"
33
+ title="放大"
34
+ @click="enterZoom"
35
+ >
36
+ <ZoomIn />
37
+ </u-icon>
38
+ </div>
39
+ </div>
40
+
41
+ <u-scroll :class="cls.e('scroll')">
42
+ <div ref="container"></div>
43
+ </u-scroll>
44
+ </div>
45
+ </div>
46
+ </Teleport>
16
47
  </template>
17
48
 
18
49
  <script lang="ts" setup>
@@ -20,9 +51,11 @@ import { Compartment, EditorState, type Extension, type Text } from '@codemirror
20
51
  import { oneDark } from '@codemirror/theme-one-dark'
21
52
  import { EditorView, tooltips } from '@codemirror/view'
22
53
  import { useFormFallbackProps } from '@veltra/compositions'
54
+ import { ZoomIn, Close } from '@veltra/icons/normal'
23
55
  import { bem, injectFormContext, zIndex } from '@veltra/utils'
24
56
  import {
25
57
  computed,
58
+ nextTick,
26
59
  onBeforeUnmount,
27
60
  shallowRef,
28
61
  useTemplateRef,
@@ -31,6 +64,7 @@ import {
31
64
  } from 'vue'
32
65
 
33
66
  import type { CodeEditorEmits, CodeEditorLang, CodeEditorProps } from '../../types'
67
+ import { UIcon } from '../icon'
34
68
  import { UScroll } from '../scroll'
35
69
  import { USelect } from '../select'
36
70
  import { basicSetup } from './basic-setup'
@@ -45,12 +79,13 @@ import {
45
79
  type ShellConfig
46
80
  } from './shell-extension'
47
81
 
48
- defineOptions({ name: 'UCodeEditor' })
82
+ defineOptions({ name: 'UCodeEditor', inheritAttrs: false })
49
83
 
50
84
  const props = withDefaults(defineProps<CodeEditorProps>(), {
51
85
  disabled: undefined,
52
86
  readonly: undefined,
53
87
  dark: false,
88
+ zoomable: true,
54
89
  defaultLines: 8
55
90
  })
56
91
 
@@ -68,11 +103,24 @@ const { disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
68
103
  readonly: false
69
104
  })
70
105
 
106
+ /** 放大状态:复用同一组件根 DOM / 同一 EditorView 实例,Teleport 移入 body 全屏遮罩 */
107
+ const zoomed = shallowRef(false)
108
+
109
+ /** 放大期间原位置占位的高度(取放大前组件根高度) */
110
+ const placeholderHeight = shallowRef('')
111
+
112
+ /** 放大遮罩的 z 轴层级,进入放大时取一次,避免渲染期反复自增 */
113
+ const zoomZIndex = shallowRef<number>()
114
+
115
+ const rootRef = useTemplateRef<HTMLElement>('root')
116
+ const zoomWrapRef = useTemplateRef<HTMLElement>('zoomWrap')
117
+
71
118
  const className = computed<string[]>(() => [
72
119
  cls.b,
73
120
  bem.is('disabled', disabled.value),
74
121
  bem.is('readonly', readonly.value),
75
- bem.is('dark', props.dark)
122
+ bem.is('dark', props.dark),
123
+ bem.is('zoomed', zoomed.value)
76
124
  ])
77
125
 
78
126
  const rootStyle = computed<CSSProperties>(() => ({
@@ -95,6 +143,45 @@ const langLabel = computed(() =>
95
143
  langs.value.length === 1 && activeLang.value ? activeLang.value.toUpperCase() : undefined
96
144
  )
97
145
 
146
+ /** 语言标识或放大/关闭按钮任一存在时才渲染工具栏 */
147
+ const hasToolbar = computed(
148
+ () => showLangSelect.value || !!langLabel.value || props.zoomable || zoomed.value
149
+ )
150
+
151
+ function enterZoom() {
152
+ if (disabled.value || zoomed.value) return
153
+ const root = rootRef.value
154
+ if (root) placeholderHeight.value = `${root.offsetHeight}px`
155
+ zoomZIndex.value = zIndex()
156
+ zoomed.value = true
157
+ // 遮罩层级高于创建编辑器时写入的 tooltip 层级,同步抬升避免补全提示被遮罩盖住
158
+ editor.value?.dispatch({
159
+ effects: tooltipThemeCompartment.reconfigure(
160
+ EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } })
161
+ )
162
+ })
163
+ // 对齐 dialog 先例:放大按钮(u-icon)不可聚焦,点击后焦点在 body 上,
164
+ // keyup 不会向下经过遮罩,需主动聚焦遮罩,Esc 才能冒泡到 keyup.esc
165
+ nextTick(() => zoomWrapRef.value?.focus())
166
+ }
167
+
168
+ function exitZoom() {
169
+ if (!zoomed.value) return
170
+ zoomed.value = false
171
+ }
172
+
173
+ /** 放大期间锁定 body 滚动,退出后恢复原值 */
174
+ let prevBodyOverflow = ''
175
+
176
+ watch(zoomed, (v) => {
177
+ if (v) {
178
+ prevBodyOverflow = document.body.style.overflow
179
+ document.body.style.overflow = 'hidden'
180
+ } else {
181
+ document.body.style.overflow = prevBodyOverflow
182
+ }
183
+ })
184
+
98
185
  const shellConfig = computed<ShellConfig>(() => ({
99
186
  prefix: props.prefix ?? '',
100
187
  suffix: props.suffix ?? ''
@@ -108,6 +195,7 @@ const editableCompartment = new Compartment()
108
195
  const readOnlyCompartment = new Compartment()
109
196
  const langCompartment = new Compartment()
110
197
  const shellCompartment = new Compartment()
198
+ const tooltipThemeCompartment = new Compartment()
111
199
 
112
200
  /** 聚焦时的正文快照,用于失焦时判断是否触发 change */
113
201
  let focusSnapshot = ''
@@ -133,7 +221,7 @@ function buildExtensions(): Extension[] {
133
221
  return [
134
222
  basicSetup,
135
223
  tooltips({ parent: document.body }),
136
- EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } }),
224
+ tooltipThemeCompartment.of(EditorView.theme({ '.cm-tooltip': { zIndex: zIndex() } })),
137
225
  themeCompartment.of(props.dark ? oneDark : []),
138
226
  editableCompartment.of(EditorView.editable.of(!disabled.value)),
139
227
  readOnlyCompartment.of(EditorState.readOnly.of(readonly.value)),
@@ -316,5 +404,8 @@ watch(
316
404
  { immediate: true }
317
405
  )
318
406
 
319
- onBeforeUnmount(destroyEditor)
407
+ onBeforeUnmount(() => {
408
+ destroyEditor()
409
+ if (zoomed.value) document.body.style.overflow = prevBodyOverflow
410
+ })
320
411
  </script>
@@ -16,6 +16,8 @@ $root-name: code-editor;
16
16
  --u-code-editor-selection-bg: #{fn.color-a(color, 16, primary)};
17
17
 
18
18
  position: relative;
19
+ display: flex;
20
+ flex-direction: column;
19
21
  width: 100%;
20
22
  max-height: 480px;
21
23
  border-radius: fn.use-var(radius, default);
@@ -32,31 +34,77 @@ $root-name: code-editor;
32
34
  border-color: fn.use-var(color, primary);
33
35
  }
34
36
 
37
+ @include m.e(toolbar) {
38
+ flex-shrink: 0;
39
+ padding: 4px fn.use-var(gap, small);
40
+ border-bottom: 1px solid var(--u-code-editor-border);
41
+ @include m.flex($justify: space-between);
42
+ }
43
+
44
+ @include m.e(toolbar-actions) {
45
+ margin-left: auto;
46
+ gap: fn.use-var(gap, small);
47
+ @include m.flex;
48
+ }
49
+
35
50
  @include m.e(lang-select) {
36
- position: absolute;
37
- top: 2px;
38
- right: 2px;
39
- z-index: 2;
40
51
  width: 88px;
41
52
  border: none;
42
-
43
- input {
44
- text-align: right;
45
- }
46
53
  }
47
54
 
48
55
  @include m.e(lang-label) {
49
- position: absolute;
50
- top: 2px;
51
- right: 8px;
52
- z-index: 2;
53
56
  font-size: 12px;
54
- line-height: 24px;
55
57
  color: var(--u-code-editor-gutter-fg);
56
- pointer-events: none;
57
58
  user-select: none;
58
59
  }
59
60
 
61
+ @include m.e(btn-zoom, btn-close) {
62
+ cursor: pointer;
63
+ font-size: 14px;
64
+ color: var(--u-code-editor-gutter-fg);
65
+ transition: color fn.use-var(transition, fast) fn.use-var(transition, ease);
66
+
67
+ &:hover {
68
+ color: fn.use-var(color, primary);
69
+ }
70
+
71
+ @include m.is(disabled) {
72
+ cursor: not-allowed;
73
+
74
+ &:hover {
75
+ color: var(--u-code-editor-gutter-fg);
76
+ }
77
+ }
78
+ }
79
+
80
+ // 编辑器滚动区:工具栏在其之外,任何滚动位置都不遮挡代码
81
+ @include m.e(scroll) {
82
+ flex: 1;
83
+ min-height: 0;
84
+ }
85
+
86
+ // 放大时原位置的等高占位,撑起布局避免位移
87
+ @include m.e(placeholder) {
88
+ box-sizing: border-box;
89
+ width: 100%;
90
+ }
91
+
92
+ // 放大遮罩:平时 display: contents 不参与布局,放大时成为 body 下的全屏遮罩
93
+ @include m.e(zoom-wrap) {
94
+ display: contents;
95
+
96
+ @include m.is(zoomed) {
97
+ position: fixed;
98
+ inset: 0;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ background-color: rgba(0, 0, 0, 0.32);
103
+ // 遮罩主动聚焦承接 Esc(对齐 dialog 遮罩),不显示焦点框
104
+ outline: none;
105
+ }
106
+ }
107
+
60
108
  .cm-editor {
61
109
  background: transparent;
62
110
  color: var(--u-code-editor-fg);
@@ -127,6 +175,15 @@ $root-name: code-editor;
127
175
  --u-code-editor-selection-bg: rgba(99, 162, 255, 0.25);
128
176
  }
129
177
 
178
+ // 放大态:遮罩内视觉居中,脱离 480px 最大高度限制
179
+ @include m.is(zoomed) {
180
+ width: 80vw;
181
+ height: 80vh;
182
+ max-height: none;
183
+ border-radius: fn.use-var(radius, large);
184
+ box-shadow: fn.use-var(shadow, lg);
185
+ }
186
+
130
187
  @include m.is(disabled) {
131
188
  background-color: fn.use-var(color, disabled);
132
189
  box-shadow: none;
@@ -1,2 +1,3 @@
1
+ import '../icon/style'
1
2
  import '../select/style'
2
3
  import './style.scss'
@@ -14,7 +14,7 @@ export type CodeEditorLang =
14
14
  export interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
15
15
  modelValue?: string
16
16
  /**
17
- * 可选语言列表。长度大于 1 时在编辑器右上角显示内置语言选择器;
17
+ * 可选语言列表。长度大于 1 时在顶部工具栏显示内置语言选择器;
18
18
  * 仅一种语言时显示语言名称标签
19
19
  */
20
20
  langs?: CodeEditorLang[]
@@ -26,6 +26,8 @@ export interface CodeEditorProps extends Omit<FormComponentProps, 'size'> {
26
26
  suffix?: string
27
27
  /** 是否使用暗色主题 */
28
28
  dark?: boolean
29
+ /** 是否显示放大按钮,默认 true;false 时不渲染放大按钮 */
30
+ zoomable?: boolean
29
31
  /**
30
32
  * 默认显示的行数,用于撑起编辑器最小高度,超出后滚动
31
33
  * @default 8