@aiao/code-editor-vue 0.0.22 → 0.0.24

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 CHANGED
@@ -14,7 +14,6 @@ pnpm add @aiao/code-editor-vue
14
14
  <script lang="ts" setup>
15
15
  import { ref } from 'vue';
16
16
  import { CodeEditor } from '@aiao/code-editor-vue';
17
- import '@aiao/code-editor-vue/style.css';
18
17
 
19
18
  const code = ref('const answer = 42;');
20
19
  </script>
@@ -51,7 +50,8 @@ const code = ref('const answer = 42;');
51
50
  | `autoFocus` | `boolean` | `false` | 仅初始化时读取 |
52
51
  | `root` | `Document \| ShadowRoot` | `undefined` | 仅初始化时传给编辑器 |
53
52
 
54
- 空字符串或 `plaintext` 会清空语言扩展。未知语言和加载失败会报告错误并保持纯文本状态。快速切换语言时,只有最后一次请求可以更新当前编辑器。
53
+ 空字符串或 `plaintext` 会清空语言扩展。未知语言和加载失败会**降级为纯文本**并发出 `language-error`
54
+ (见下),编辑器本身继续可用。快速切换语言时,只有最后一次请求可以更新当前编辑器。
55
55
 
56
56
  自定义语言列表使用核心包的公开类型:
57
57
 
@@ -68,15 +68,77 @@ const languages: readonly CodeEditorLanguageDescription[] = [myLanguage];
68
68
 
69
69
  ## Events
70
70
 
71
- | 事件 | 参数 | 触发时机 |
72
- | -------------- | ----------------- | ---------------------- |
73
- | `update:value` | `(value: string)` | 用户修改文档 |
74
- | `change` | `(value: string)` | 用户修改文档 |
75
- | `focus` | `()` | 编辑器内容区域获得焦点 |
76
- | `blur` | `()` | 编辑器内容区域失去焦点 |
71
+ | 事件 | 参数 | 触发时机 |
72
+ | ---------------- | ---------------------------------- | ---------------------------------------------------------- |
73
+ | `update:value` | `(value: string)` | 用户修改文档 |
74
+ | `change` | `(value: string)` | 用户修改文档 |
75
+ | `focus` | `()` | 编辑器内容区域获得焦点 |
76
+ | `blur` | `()` | 编辑器内容区域失去焦点 |
77
+ | `language-error` | `(error: CodeEditorLanguageError)` | 语言查不到(`not-found`)或 `load()` 失败(`load-failed`) |
78
+
79
+ ## 语言错误
80
+
81
+ 降级为纯文本时编辑器仍然可用,但宿主需要能观测到这件事 —— `language-error` 就是这个通道:
82
+
83
+ ```vue
84
+ <script lang="ts" setup>
85
+ import type { CodeEditorLanguageError } from '@aiao/code-editor';
86
+ import { CodeEditor } from '@aiao/code-editor-vue';
87
+
88
+ function onLanguageError(error: CodeEditorLanguageError) {
89
+ // 'not-found' 是配置错误,重试没有意义;'load-failed' 通常是网络或分包问题,可以重试。
90
+ if (error.kind === 'load-failed') retry(error.language);
91
+ }
92
+ </script>
93
+
94
+ <template>
95
+ <CodeEditor :language="lang" @language-error="onLanguageError" />
96
+ </template>
97
+ ```
98
+
99
+ 载荷形状(`kind` / `language` / `message` / `cause`)由核心包定义,三端一致:
100
+ Angular 是 `(aoLanguageError)` 输出,React 是 `onLanguageError` 回调。
101
+
102
+ ## 模板 ref 用法
103
+
104
+ 组件通过 `defineExpose` 暴露命令式 API,供模板 ref 访问底层 `EditorView`:
105
+
106
+ ```vue
107
+ <script lang="ts" setup>
108
+ import { ref } from 'vue';
109
+ import { CodeEditor } from '@aiao/code-editor-vue';
110
+ import type { CodeEditorExpose } from '@aiao/code-editor-vue';
111
+
112
+ const editorRef = ref<CodeEditorExpose | null>(null);
113
+ const code = ref('const answer = 42;');
114
+
115
+ function focusEditor() {
116
+ editorRef.value?.focus();
117
+ }
118
+ </script>
119
+
120
+ <template>
121
+ <CodeEditor v-model:value="code" ref="editorRef" />
122
+ </template>
123
+ ```
124
+
125
+ 四个成员与 React 的 `CodeEditorHandle`、Angular 组件实例上的同名成员逐字对齐,跨端迁移时命令式面不需要改写:
126
+
127
+ | 成员 | 类型 | 说明 |
128
+ | ------- | ------------------------ | ----------------------------------------------- |
129
+ | `view` | `EditorView \| null` | 底层 CodeMirror 实例,未挂载或已卸载时为 `null` |
130
+ | `host` | `HTMLDivElement \| null` | 宿主 `div` 元素,未挂载或已卸载时为 `null` |
131
+ | `focus` | `() => void` | 聚焦编辑器内容区域 |
132
+ | `blur` | `() => void` | 使编辑器内容区域失焦 |
77
133
 
78
134
  ## 公开类型
79
135
 
80
136
  ```ts
81
- import type { CodeEditorEmits, CodeEditorProps, CodeEditorSetup, CodeEditorTheme } from '@aiao/code-editor-vue';
137
+ import type {
138
+ CodeEditorEmits,
139
+ CodeEditorExpose,
140
+ CodeEditorProps,
141
+ CodeEditorSetup,
142
+ CodeEditorTheme
143
+ } from '@aiao/code-editor-vue';
82
144
  ```
@@ -1,22 +1,27 @@
1
- import { CodeEditorProps, CodeEditorSetup } from './code-editor.types.js';
2
- declare const __VLS_export: import('vue').DefineComponent<CodeEditorProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
1
+ import { CodeEditorExpose, CodeEditorProps, CodeEditorSetup } from './code-editor.types.js';
2
+ declare const __VLS_export: import('vue').DefineComponent<CodeEditorProps, CodeEditorExpose, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
3
3
  blur: () => any;
4
4
  change: (value: string) => any;
5
5
  focus: () => any;
6
+ "language-error": (error: import('@aiao/code-editor').CodeEditorLanguageError) => any;
6
7
  "update:value": (value: string) => any;
7
8
  }, string, import('vue').PublicProps, Readonly<CodeEditorProps> & Readonly<{
8
9
  onBlur?: (() => any) | undefined;
9
10
  onChange?: ((value: string) => any) | undefined;
10
11
  onFocus?: (() => any) | undefined;
12
+ "onLanguage-error"?: ((error: import('@aiao/code-editor').CodeEditorLanguageError) => any) | undefined;
11
13
  "onUpdate:value"?: ((value: string) => any) | undefined;
12
14
  }>, {
13
15
  readonly autoFocus: boolean;
16
+ readonly describedBy: string;
14
17
  readonly disabled: boolean;
15
18
  readonly highlightWhitespace: boolean;
16
19
  readonly indentUnit: string;
17
20
  readonly indentWithTab: boolean;
18
21
  readonly language: string;
19
22
  readonly languages: readonly import('@aiao/code-editor').CodeEditorLanguageDescription[];
23
+ readonly label: string;
24
+ readonly labelledBy: string;
20
25
  readonly lineWrapping: boolean;
21
26
  readonly placeholder: string;
22
27
  readonly readonly: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"CodeEditor.vue.d.ts","sourceRoot":"","sources":["../src/CodeEditor.vue"],"names":[],"mappings":"AAuQA,OAAO,KAAK,EAAmB,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAkQhG,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;6EAIhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
1
+ {"version":3,"file":"CodeEditor.vue.d.ts","sourceRoot":"","sources":["../src/CodeEditor.vue"],"names":[],"mappings":"AAuVA,OAAO,KAAK,EAAmB,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA8UlH,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6EAKhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -1,29 +1,173 @@
1
- import { CodeEditorLanguageDescription, CodeEditorTheme } from '@aiao/code-editor';
1
+ import { CodeEditorLanguageDescription, CodeEditorLanguageError, CodeEditorTheme } from '@aiao/code-editor';
2
+ import { EditorView } from '@codemirror/view';
2
3
  /** CodeMirror 预置扩展集合。 */
3
4
  export type CodeEditorSetup = 'basic' | 'minimal' | null;
4
- /** Vue CodeEditor 的公开属性。 */
5
+ /**
6
+ * Vue CodeEditor 的公开属性。
7
+ *
8
+ * @remarks 与 Angular 的 inputs、React 的 props 同名同义;仅初始化生效的两项
9
+ * ({@link CodeEditorProps.autoFocus} / {@link CodeEditorProps.root})在各自条目上标注。
10
+ */
5
11
  export interface CodeEditorProps {
12
+ /**
13
+ * 挂载后立即聚焦编辑器。
14
+ *
15
+ * @defaultValue false
16
+ * @remarks **仅挂载时读取一次**,后续变更被忽略。`disabled` 或 `readonly` 为真时
17
+ * 不抢焦点 —— 否则用户得到一个无法输入、也无从得知为什么无法输入的控件。
18
+ */
6
19
  readonly autoFocus?: boolean;
20
+ /**
21
+ * 补充说明元素的 id,写成内部 textbox 的 `aria-describedby`。
22
+ *
23
+ * @remarks 属性落在 CodeMirror 的 `.cm-content` 上 —— 承担 `role="textbox"` 的是它,
24
+ * 不是组件的宿主元素。空串按「未设置」处理。三端同名同义。
25
+ */
26
+ readonly describedBy?: string;
27
+ /**
28
+ * 禁用:只读且不可编辑,并向辅助技术报告 `aria-disabled`。
29
+ *
30
+ * @defaultValue false
31
+ * @remarks 与 `readonly` 是独立的两个属性,任一为真都会让编辑器不可编辑;
32
+ * 区别在于禁用还会写出 `aria-disabled` 并抑制 `autoFocus`。
33
+ */
7
34
  readonly disabled?: boolean;
35
+ /**
36
+ * 显示空白字符。
37
+ *
38
+ * @defaultValue false
39
+ */
8
40
  readonly highlightWhitespace?: boolean;
41
+ /**
42
+ * 一级缩进使用的字符串。
43
+ *
44
+ * @defaultValue ' '(两个空格,三端一致)
45
+ */
9
46
  readonly indentUnit?: string;
47
+ /**
48
+ * 允许 Tab 键缩进。
49
+ *
50
+ * @defaultValue false
51
+ * @remarks 打开后 Tab 不再移动焦点,键盘用户会被困在编辑器里 —— 这是可达性取舍,
52
+ * 三端默认一致地关闭。
53
+ */
10
54
  readonly indentWithTab?: boolean;
55
+ /**
56
+ * 语法高亮语言名或别名;`''` 与 `'plaintext'` 表示不高亮。
57
+ *
58
+ * @defaultValue 'sql'
59
+ * @remarks 查不到或 `load()` 失败时**降级为纯文本**并继续可用,同时发出
60
+ * `language-error` 事件(见 {@link CodeEditorEmits})。快速切换时只有最后一次请求生效。
61
+ */
11
62
  readonly language?: string;
63
+ /**
64
+ * 候选语言列表;空列表表示不高亮。
65
+ *
66
+ * @defaultValue SUPPORT_LANGUAGES
67
+ * @remarks 传内联字面量(`:languages="[myLang]"`)是有成本的:每轮更新都是新数组,
68
+ * 而 `watch` 按引用触发。组件会再判断当前语言解析到的 description 是否真的换了 ——
69
+ * 只有真换了才重新 `load()`,但这层判断本身仍要跑,稳定引用(模块级常量或 `computed`)更省。
70
+ */
12
71
  readonly languages?: readonly CodeEditorLanguageDescription[];
72
+ /**
73
+ * 可访问名称,写成内部 textbox 的 `aria-label`。
74
+ *
75
+ * @remarks 落点与 {@link CodeEditorProps.describedBy} 相同。
76
+ */
77
+ readonly label?: string;
78
+ /**
79
+ * 承担标签的元素 id,写成内部 textbox 的 `aria-labelledby`。
80
+ *
81
+ * @remarks 落点与 {@link CodeEditorProps.describedBy} 相同。
82
+ */
83
+ readonly labelledBy?: string;
84
+ /**
85
+ * 自动换行。
86
+ *
87
+ * @defaultValue false
88
+ */
13
89
  readonly lineWrapping?: boolean;
90
+ /**
91
+ * 空文档时显示的占位文本。
92
+ *
93
+ * @defaultValue ''
94
+ */
14
95
  readonly placeholder?: string;
96
+ /**
97
+ * 只读:仍可聚焦、可选择、可复制,但不能改。
98
+ *
99
+ * @defaultValue false
100
+ * @remarks 只挡编辑**命令**,不挡宿主同步 —— `value` 变化照样写进文档,
101
+ * 否则只读的受控用法直接失效。
102
+ */
15
103
  readonly readonly?: boolean;
104
+ /**
105
+ * CodeMirror 挂载的 DOM 根,用于 Shadow DOM 场景。
106
+ *
107
+ * @remarks **仅挂载时读取一次** —— CodeMirror 无法在构造后改 root。
108
+ * 需要换 root 时请用 `v-if` 或 `:key` 强制重建组件。
109
+ */
16
110
  readonly root?: Document | ShadowRoot;
111
+ /**
112
+ * CodeMirror 预设扩展集;`null` 表示不加载任何预设。
113
+ *
114
+ * @defaultValue 'basic'
115
+ */
17
116
  readonly setup?: CodeEditorSetup;
117
+ /**
118
+ * 主题。
119
+ *
120
+ * @defaultValue 'light'
121
+ */
18
122
  readonly theme?: CodeEditorTheme;
123
+ /**
124
+ * 文档内容,配合 `v-model:value` 使用。
125
+ *
126
+ * @defaultValue ''
127
+ * @remarks **不是严格受控**:组件不会把用户编辑写回本属性,也不会在每次更新时
128
+ * 强行拉平文档。本属性变化时只同步公共前后缀之间的那一段差量,光标、选区与滚动位置都保留,
129
+ * 且不进 undo 栈。父组件忽略 `update:value` 时编辑器会保留用户输入。
130
+ */
19
131
  readonly value?: string;
20
132
  }
21
133
  /** Vue CodeEditor 的公开事件。 */
22
134
  export interface CodeEditorEmits {
135
+ /** 编辑器内容区域失去焦点。 */
23
136
  blur: [];
137
+ /** 用户编辑导致的内容变化;宿主经 `value` 的程序化写入**不**触发。 */
24
138
  change: [value: string];
139
+ /** 编辑器内容区域获得焦点。 */
25
140
  focus: [];
141
+ /**
142
+ * 语言解析或加载失败。
143
+ *
144
+ * @remarks
145
+ * CEV-004:这条路径此前只有一条 `console.error`,宿主完全观测不到 ——
146
+ * 拿不到错误、没有重试或降级的机会,用户只看到高亮突然消失。
147
+ * 降级策略不变(清空语言扩展退回纯文本),日志也保留,本事件是**新增的**观测通道。
148
+ * 载荷由核心包定义,与 Angular 的 `aoLanguageError` 输出、React 的 `onLanguageError`
149
+ * 回调逐字一致:`kind` 为 `'not-found'` 时是配置错误,重试没有意义;
150
+ * 为 `'load-failed'` 时通常是网络或分包问题,可以重试。
151
+ */
152
+ 'language-error': [error: CodeEditorLanguageError];
153
+ /** `v-model:value` 的更新通道,触发时机与 `change` 相同。 */
26
154
  'update:value': [value: string];
27
155
  }
156
+ /**
157
+ * `defineExpose` 暴露给模板 ref 的命令式 API。
158
+ *
159
+ * @remarks 四个成员与 React 的 `CodeEditorHandle`、Angular 组件实例上的同名成员
160
+ * 逐字对齐(CEA-009)—— 跨端迁移时命令式面不需要改写。
161
+ */
162
+ export interface CodeEditorExpose {
163
+ /** 底层 CodeMirror EditorView,未挂载或已卸载时为 null。 */
164
+ readonly view: EditorView | null;
165
+ /** 宿主 `div` 元素(CodeMirror 挂载所在的容器),未挂载或已卸载时为 null。 */
166
+ readonly host: HTMLDivElement | null;
167
+ /** 移开键盘焦点;未挂载时是空操作。 */
168
+ blur(): void;
169
+ /** 把键盘焦点交给编辑器内容区域;未挂载时是空操作。 */
170
+ focus(): void;
171
+ }
28
172
  export type { CodeEditorTheme } from '@aiao/code-editor';
29
173
  //# sourceMappingURL=code-editor.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"code-editor.types.d.ts","sourceRoot":"","sources":["../src/code-editor.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAExF,yBAAyB;AACzB,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;AAEzD,4BAA4B;AAC5B,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,4BAA4B;AAC5B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,EAAE,CAAC;IACT,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACxB,KAAK,EAAE,EAAE,CAAC;IACV,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"code-editor.types.d.ts","sourceRoot":"","sources":["../src/code-editor.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACjH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,yBAAyB;AACzB,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IAC9D;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,4BAA4B;AAC5B,MAAM,WAAW,eAAe;IAC9B,mBAAmB;IACnB,IAAI,EAAE,EAAE,CAAC;IACT,6CAA6C;IAC7C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACxB,mBAAmB;IACnB,KAAK,EAAE,EAAE,CAAC;IACV;;;;;;;;;;OAUG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,uBAAuB,CAAC,CAAC;IACnD,+CAA+C;IAC/C,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IACrC,uBAAuB;IACvB,IAAI,IAAI,IAAI,CAAC;IACb,+BAA+B;IAC/B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * ```vue
6
6
  * <script setup lang="ts">
7
7
  * import { CodeEditor } from '@aiao/code-editor-vue';
8
- * import '@aiao/code-editor-vue/style.css';
9
8
  * </script>
10
9
  *
11
10
  * <template>
@@ -13,6 +12,6 @@
13
12
  * </template>
14
13
  * ```
15
14
  */
16
- export type { CodeEditorEmits, CodeEditorProps, CodeEditorSetup, CodeEditorTheme } from './code-editor.types.js';
15
+ export type { CodeEditorEmits, CodeEditorExpose, CodeEditorProps, CodeEditorSetup, CodeEditorTheme } from './code-editor.types.js';
17
16
  export { default as CodeEditor } from './CodeEditor.vue';
18
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACjH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,eAAe,EAChB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -1,251 +1,208 @@
1
- import { computed as e, createElementBlock as t, defineComponent as n, mergeProps as r, onMounted as i, onUnmounted as a, openBlock as o, ref as s, shallowRef as c, watch as l } from "vue";
2
- import { SUPPORT_LANGUAGES as u, findLanguageByName as d } from "@aiao/code-editor";
1
+ import { computed as e, createElementBlock as t, defineComponent as n, mergeProps as r, onMounted as ee, onUnmounted as te, openBlock as i, ref as ne, shallowRef as re, watch as a } from "vue";
2
+ import { SUPPORT_LANGUAGES as o, buildCodeEditorContentAttributes as s, codeEditorLanguageLoadFailed as ie, codeEditorLanguageNotFound as c, computeMinimalDocumentChange as ae, isSameResolvedLanguage as l, resolveCodeEditorLanguage as u, shouldAutoFocusCodeEditor as d } from "@aiao/code-editor";
3
3
  import { indentWithTab as f } from "@codemirror/commands";
4
- import { indentUnit as p } from "@codemirror/language";
5
- import { Annotation as m, Compartment as h, EditorState as g } from "@codemirror/state";
6
- import { oneDark as _ } from "@codemirror/theme-one-dark";
7
- import { EditorView as v, highlightWhitespace as y, keymap as b, placeholder as x } from "@codemirror/view";
8
- import { basicSetup as S, minimalSetup as C } from "codemirror";
9
- //#region src/CodeEditor.vue?vue&type=script&setup=true&lang.ts
10
- var w = /*@__PURE__*/ n({
4
+ import { indentUnit as oe } from "@codemirror/language";
5
+ import { Annotation as se, Compartment as p, EditorState as m, Transaction as h } from "@codemirror/state";
6
+ import { oneDark as g } from "@codemirror/theme-one-dark";
7
+ import { EditorView as _, highlightWhitespace as v, keymap as y, placeholder as b } from "@codemirror/view";
8
+ import { basicSetup as x, minimalSetup as S } from "codemirror";
9
+ //#endregion
10
+ //#region src/CodeEditor.vue
11
+ var C = /* @__PURE__ */ n({
11
12
  __name: "CodeEditor",
12
13
  props: {
13
14
  autoFocus: {
14
15
  type: Boolean,
15
- required: !1,
16
16
  default: !1
17
17
  },
18
+ describedBy: { default: "" },
18
19
  disabled: {
19
20
  type: Boolean,
20
- required: !1,
21
21
  default: !1
22
22
  },
23
23
  highlightWhitespace: {
24
24
  type: Boolean,
25
- required: !1,
26
25
  default: !1
27
26
  },
28
- indentUnit: {
29
- type: String,
30
- required: !1,
31
- default: " "
32
- },
27
+ indentUnit: { default: " " },
33
28
  indentWithTab: {
34
29
  type: Boolean,
35
- required: !1,
36
30
  default: !1
37
31
  },
38
- language: {
39
- type: String,
40
- required: !1,
41
- default: "sql"
42
- },
43
- languages: {
44
- type: Array,
45
- required: !1,
46
- default: () => u
47
- },
32
+ language: { default: "sql" },
33
+ languages: { default: () => o },
34
+ label: { default: "" },
35
+ labelledBy: { default: "" },
48
36
  lineWrapping: {
49
37
  type: Boolean,
50
- required: !1,
51
38
  default: !1
52
39
  },
53
- placeholder: {
54
- type: String,
55
- required: !1,
56
- default: ""
57
- },
40
+ placeholder: { default: "" },
58
41
  readonly: {
59
42
  type: Boolean,
60
- required: !1,
61
43
  default: !1
62
44
  },
63
- root: {
64
- type: null,
65
- required: !1
66
- },
67
- setup: {
68
- type: [String, null],
69
- required: !1,
70
- default: "basic"
71
- },
72
- theme: {
73
- type: String,
74
- required: !1,
75
- default: "light"
76
- },
77
- value: {
78
- type: String,
79
- required: !1,
80
- default: ""
81
- }
45
+ root: {},
46
+ setup: { default: "basic" },
47
+ theme: { default: "light" },
48
+ value: { default: "" }
82
49
  },
83
50
  emits: [
84
51
  "blur",
85
52
  "change",
86
53
  "focus",
54
+ "language-error",
87
55
  "update:value"
88
56
  ],
89
- setup(t, { expose: n, emit: r }) {
90
- n();
91
- let o = t, u = r, w = s(null), T = c(null), E = e(() => ({
92
- "aria-disabled": o.disabled || void 0,
93
- class: "aiao-code-editor"
94
- })), D = new h(), O = new h(), k = new h(), A = new h(), j = new h(), M = new h(), N = new h(), P = new h(), F = new h(), I = new h(), L = m.define(), R = {
95
- basic: S,
96
- minimal: C
97
- }, z = 0;
98
- i(() => {
99
- let e = w.value;
57
+ setup(n, { expose: o, emit: C }) {
58
+ let w = n, T = C, E = ne(null), D = re(null), ce = e(() => ({
59
+ "aria-disabled": w.disabled || void 0,
60
+ class: "aiao-code-editor",
61
+ style: {
62
+ display: "block",
63
+ fontSize: "0.875rem",
64
+ height: "100%",
65
+ overflow: "hidden",
66
+ width: "100%"
67
+ }
68
+ })), O = new p(), k = new p(), A = new p(), j = new p(), M = new p(), N = new p(), P = new p(), F = new p(), I = new p(), L = new p(), R = new p(), z = se.define(), B = {
69
+ basic: x,
70
+ minimal: S
71
+ }, V = 0, H;
72
+ ee(() => {
73
+ let e = E.value;
100
74
  if (!e) return;
101
- let t = new v({
75
+ let t = m.create({
76
+ doc: w.value,
77
+ extensions: [
78
+ O.of(W()),
79
+ k.of([]),
80
+ A.of([]),
81
+ j.of([]),
82
+ M.of([]),
83
+ N.of([]),
84
+ P.of([]),
85
+ F.of([]),
86
+ I.of([]),
87
+ L.of([]),
88
+ R.of([]),
89
+ _.theme({
90
+ "&": {
91
+ fontSize: "0.875rem",
92
+ height: "100%"
93
+ },
94
+ "&.cm-focused": {
95
+ outline: "2px solid Highlight",
96
+ outlineOffset: "-2px"
97
+ },
98
+ ".cm-gutters": {
99
+ background: "initial",
100
+ border: "none"
101
+ },
102
+ ".cm-scroller": { overflow: "auto" }
103
+ }),
104
+ _.domEventObservers({
105
+ blur: () => T("blur"),
106
+ focus: () => T("focus")
107
+ }),
108
+ _.updateListener.of((e) => {
109
+ let t = e.transactions.some((e) => e.annotation(z));
110
+ if (!e.docChanged || t) return;
111
+ let n = e.state.doc.toString();
112
+ T("update:value", n), T("change", n);
113
+ })
114
+ ]
115
+ }), n = new _({
102
116
  parent: e,
103
- state: g.create({
104
- doc: o.value,
105
- extensions: [
106
- D.of(V()),
107
- O.of([]),
108
- k.of([]),
109
- A.of([]),
110
- j.of([]),
111
- M.of([]),
112
- N.of([]),
113
- P.of([]),
114
- F.of([]),
115
- I.of([]),
116
- v.domEventObservers({
117
- blur: () => u("blur"),
118
- focus: () => u("focus")
119
- }),
120
- v.updateListener.of((e) => {
121
- let t = e.transactions.some((e) => e.annotation(L));
122
- if (!e.docChanged || t) return;
123
- let n = e.state.doc.toString();
124
- u("update:value", n), u("change", n);
125
- })
126
- ]
127
- }),
128
- ...o.root ? { root: o.root } : {}
117
+ state: t,
118
+ ...w.root ? { root: w.root } : {}
129
119
  });
130
- T.value = t, H(), U(), G(), K(), q(), J(), Y(), o.autoFocus && t.focus();
131
- }), a(() => {
132
- z += 1;
133
- let e = T.value;
134
- T.value = null, e?.destroy();
135
- }), l(() => o.value, (e) => {
136
- let t = T.value;
137
- !t || t.state.doc.toString() === e || t.dispatch({
138
- annotations: L.of(!0),
139
- changes: {
140
- from: 0,
141
- insert: e,
142
- to: t.state.doc.length
143
- }
120
+ D.value = n, G(), K(), J(), X(), Z(), Q(), $(), Y(), w.autoFocus && d(w) && n.focus();
121
+ }), te(() => {
122
+ V += 1;
123
+ let e = D.value;
124
+ D.value = null, e?.destroy();
125
+ }), a(() => w.value, (e) => {
126
+ let t = D.value;
127
+ if (!t) return;
128
+ let n = ae(t.state.doc.toString(), e ?? "");
129
+ n && t.dispatch({
130
+ annotations: [z.of(!0), h.addToHistory.of(!1)],
131
+ changes: n,
132
+ scrollIntoView: !1
144
133
  });
145
- }), l(() => o.setup, B), l(() => o.theme, H), l([() => o.language, () => o.languages], U), l([() => o.readonly, () => o.disabled], G), l(() => o.placeholder, K), l([() => o.indentWithTab, () => o.indentUnit], q), l(() => o.lineWrapping, J), l(() => o.highlightWhitespace, Y);
146
- function B() {
147
- T.value?.dispatch({ effects: D.reconfigure(V()) });
134
+ }), a(() => w.setup, U), a(() => w.theme, G), a([() => w.language, () => w.languages], le), a([() => w.readonly, () => w.disabled], J), a(() => w.placeholder, X), a([() => w.indentWithTab, () => w.indentUnit], Z), a(() => w.lineWrapping, Q), a(() => w.highlightWhitespace, $), a([
135
+ () => w.describedBy,
136
+ () => w.disabled,
137
+ () => w.label,
138
+ () => w.labelledBy
139
+ ], Y);
140
+ function U() {
141
+ D.value?.dispatch({ effects: O.reconfigure(W()) });
142
+ }
143
+ function W() {
144
+ return w.setup === null ? [] : B[w.setup];
148
145
  }
149
- function V() {
150
- return o.setup === null ? [] : R[o.setup];
146
+ function G() {
147
+ D.value?.dispatch({ effects: A.reconfigure(w.theme === "dark" ? g : []) });
151
148
  }
152
- function H() {
153
- T.value?.dispatch({ effects: k.reconfigure(o.theme === "dark" ? _ : []) });
149
+ function le() {
150
+ let e = u(w.language, w.languages);
151
+ l(H, e) || K();
154
152
  }
155
- function U() {
156
- let e = ++z, t = T.value;
153
+ function K() {
154
+ let e = ++V, t = D.value;
157
155
  if (!t) return;
158
- let n = o.language;
159
- if (!n || n === "plaintext" || o.languages.length === 0) {
160
- t.dispatch({ effects: O.reconfigure([]) });
156
+ let n = w.language, r = u(n, w.languages);
157
+ if (H = r, r.kind !== "found") {
158
+ t.dispatch({ effects: k.reconfigure([]) }), r.kind === "not-found" && (console.error(`[CodeEditor] Language '${n}' not found.`), T("language-error", c(n)));
161
159
  return;
162
160
  }
163
- let r = d(n, o.languages);
164
- if (!r) {
165
- console.error(`[CodeEditor] Language '${n}' not found.`), t.dispatch({ effects: O.reconfigure([]) });
166
- return;
167
- }
168
- r.load().then((n) => {
169
- W(e, t) && t.dispatch({ effects: O.reconfigure(n.extension) });
161
+ r.description.load().then((n) => {
162
+ q(e, t) && t.dispatch({ effects: k.reconfigure(n.extension) });
170
163
  }, (r) => {
171
- W(e, t) && (console.error(`[CodeEditor] Failed to load language '${n}':`, r), t.dispatch({ effects: O.reconfigure([]) }));
164
+ q(e, t) && (t.dispatch({ effects: k.reconfigure([]) }), console.error(`[CodeEditor] Failed to load language '${n}':`, r), T("language-error", ie(n, r)));
172
165
  });
173
166
  }
174
- function W(e, t) {
175
- return e === z && T.value === t;
167
+ function q(e, t) {
168
+ return e === V && D.value === t;
176
169
  }
177
- function G() {
178
- let e = T.value;
170
+ function J() {
171
+ let e = D.value;
179
172
  if (!e) return;
180
- let t = o.readonly || o.disabled;
181
- e.dispatch({ effects: [A.reconfigure(g.readOnly.of(t)), j.reconfigure(v.editable.of(!t))] });
173
+ let t = w.readonly || w.disabled;
174
+ e.dispatch({ effects: [j.reconfigure(m.readOnly.of(t)), M.reconfigure(_.editable.of(!t))] });
182
175
  }
183
- function K() {
184
- T.value?.dispatch({ effects: M.reconfigure(o.placeholder ? x(o.placeholder) : []) });
176
+ function Y() {
177
+ let e = s(w);
178
+ D.value?.dispatch({ effects: R.reconfigure(_.contentAttributes.of(e)) });
185
179
  }
186
- function q() {
187
- T.value?.dispatch({ effects: [N.reconfigure(o.indentWithTab ? b.of([f]) : []), P.reconfigure(o.indentUnit ? p.of(o.indentUnit) : [])] });
180
+ function X() {
181
+ D.value?.dispatch({ effects: N.reconfigure(w.placeholder ? b(w.placeholder) : []) });
188
182
  }
189
- function J() {
190
- T.value?.dispatch({ effects: F.reconfigure(o.lineWrapping ? v.lineWrapping : []) });
183
+ function Z() {
184
+ D.value?.dispatch({ effects: [P.reconfigure(w.indentWithTab ? y.of([f]) : []), F.reconfigure(w.indentUnit ? oe.of(w.indentUnit) : [])] });
191
185
  }
192
- function Y() {
193
- T.value?.dispatch({ effects: I.reconfigure(o.highlightWhitespace ? y() : []) });
186
+ function Q() {
187
+ D.value?.dispatch({ effects: I.reconfigure(w.lineWrapping ? _.lineWrapping : []) });
194
188
  }
195
- let X = {
196
- props: o,
197
- emit: u,
198
- editorRef: w,
199
- viewRef: T,
200
- hostAttributes: E,
201
- setupConf: D,
202
- languageConf: O,
203
- themeConf: k,
204
- readonlyConf: A,
205
- editableConf: j,
206
- placeholderConf: M,
207
- indentWithTabConf: N,
208
- indentUnitConf: P,
209
- lineWrappingConf: F,
210
- highlightWhitespaceConf: I,
211
- External: L,
212
- setupExtensions: R,
213
- get languageRequest() {
214
- return z;
189
+ function $() {
190
+ D.value?.dispatch({ effects: L.reconfigure(w.highlightWhitespace ? v() : []) });
191
+ }
192
+ return o({
193
+ get view() {
194
+ return D.value;
215
195
  },
216
- set languageRequest(e) {
217
- z = e;
196
+ get host() {
197
+ return D.value ? E.value : null;
218
198
  },
219
- updateSetup: B,
220
- getSetupExtension: V,
221
- updateTheme: H,
222
- updateLanguage: U,
223
- isCurrentLanguageRequest: W,
224
- updateReadonly: G,
225
- updatePlaceholder: K,
226
- updateIndent: q,
227
- updateLineWrapping: J,
228
- updateHighlightWhitespace: Y
229
- };
230
- return Object.defineProperty(X, "__isScriptSetup", {
231
- enumerable: !1,
232
- value: !0
233
- }), X;
199
+ blur: () => D.value?.contentDOM.blur(),
200
+ focus: () => D.value?.focus()
201
+ }), (e, n) => (i(), t("div", r(ce.value, {
202
+ ref_key: "editorRef",
203
+ ref: E
204
+ }), null, 16));
234
205
  }
235
- }), T = (e, t) => {
236
- let n = e.__vccOpts || e;
237
- for (let [e, r] of t) n[e] = r;
238
- return n;
239
- };
240
- //#endregion
241
- //#region src/CodeEditor.vue
242
- function E(e, n, i, a, s, c) {
243
- return o(), t("div", r(a.hostAttributes, { ref: "editorRef" }), null, 16);
244
- }
245
- var D = /*#__PURE__*/ T(w, [
246
- ["render", E],
247
- ["__scopeId", "data-v-9b1829ca"],
248
- ["__file", "/private/tmp/aiao-release-027.kcmOWl/aiao/packages/code-editor-vue/src/CodeEditor.vue"]
249
- ]);
206
+ });
250
207
  //#endregion
251
- export { D as CodeEditor };
208
+ export { C as CodeEditor };
package/package.json CHANGED
@@ -1,16 +1,14 @@
1
1
  {
2
2
  "name": "@aiao/code-editor-vue",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "main": "./dist/index.js",
6
7
  "module": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
8
- "style": "./dist/index.css",
9
9
  "exports": {
10
10
  "./package.json": "./package.json",
11
- "./style.css": "./dist/index.css",
12
11
  ".": {
13
- "@aiao/source": "./src/index.ts",
14
12
  "types": "./dist/index.d.ts",
15
13
  "import": "./dist/index.js",
16
14
  "default": "./dist/index.js"
@@ -18,6 +16,10 @@
18
16
  },
19
17
  "files": [
20
18
  "dist",
19
+ "src",
20
+ "!src/**/*.spec.*",
21
+ "!src/**/*.test.*",
22
+ "!src/**/__tests__/**",
21
23
  "!**/*.tsbuildinfo"
22
24
  ],
23
25
  "publishConfig": {
@@ -30,15 +32,15 @@
30
32
  ]
31
33
  },
32
34
  "peerDependencies": {
33
- "vue": ">=3.5.0"
35
+ "vue": "^3.5.40"
34
36
  },
35
37
  "dependencies": {
36
38
  "@codemirror/commands": "^6.10.4",
37
39
  "@codemirror/language": "^6.12.4",
38
40
  "@codemirror/state": "6.7.1",
39
41
  "@codemirror/theme-one-dark": "^6.1.3",
40
- "@codemirror/view": "^6.43.5",
42
+ "@codemirror/view": "^6.43.8",
41
43
  "codemirror": "^6.0.1",
42
- "@aiao/code-editor": "0.0.22"
44
+ "@aiao/code-editor": "0.0.24"
43
45
  }
44
46
  }
@@ -0,0 +1,316 @@
1
+ <script lang="ts" setup>
2
+ import type { ResolvedCodeEditorLanguage } from '@aiao/code-editor';
3
+ import {
4
+ buildCodeEditorContentAttributes,
5
+ codeEditorLanguageLoadFailed,
6
+ codeEditorLanguageNotFound,
7
+ computeMinimalDocumentChange,
8
+ isSameResolvedLanguage,
9
+ resolveCodeEditorLanguage,
10
+ shouldAutoFocusCodeEditor,
11
+ SUPPORT_LANGUAGES
12
+ } from '@aiao/code-editor';
13
+ import { indentWithTab as indentWithTabCommand } from '@codemirror/commands';
14
+ import { indentUnit as indentUnitExt } from '@codemirror/language';
15
+ import { Annotation, Compartment, EditorState, type Extension, Transaction } from '@codemirror/state';
16
+ import { oneDark } from '@codemirror/theme-one-dark';
17
+ import {
18
+ EditorView,
19
+ highlightWhitespace as highlightWhitespaceExt,
20
+ keymap,
21
+ placeholder as placeholderExt
22
+ } from '@codemirror/view';
23
+ import { basicSetup, minimalSetup } from 'codemirror';
24
+ import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
25
+
26
+ import type { CodeEditorEmits, CodeEditorExpose, CodeEditorProps, CodeEditorSetup } from './code-editor.types.js';
27
+
28
+ const props = withDefaults(defineProps<CodeEditorProps>(), {
29
+ autoFocus: false,
30
+ describedBy: '',
31
+ disabled: false,
32
+ highlightWhitespace: false,
33
+ indentUnit: ' ',
34
+ indentWithTab: false,
35
+ language: 'sql',
36
+ label: '',
37
+ labelledBy: '',
38
+ languages: () => SUPPORT_LANGUAGES,
39
+ lineWrapping: false,
40
+ placeholder: '',
41
+ readonly: false,
42
+ setup: 'basic',
43
+ theme: 'light',
44
+ value: ''
45
+ });
46
+
47
+ const emit = defineEmits<CodeEditorEmits>();
48
+ const editorRef = ref<HTMLDivElement | null>(null);
49
+ const viewRef = shallowRef<EditorView | null>(null);
50
+ const hostAttributes = computed(() => ({
51
+ 'aria-disabled': props.disabled || undefined,
52
+ class: 'aiao-code-editor',
53
+ style: {
54
+ display: 'block',
55
+ fontSize: '0.875rem',
56
+ height: '100%',
57
+ overflow: 'hidden',
58
+ width: '100%'
59
+ }
60
+ }));
61
+ const setupConf = new Compartment();
62
+ const languageConf = new Compartment();
63
+ const themeConf = new Compartment();
64
+ const readonlyConf = new Compartment();
65
+ const editableConf = new Compartment();
66
+ const placeholderConf = new Compartment();
67
+ const indentWithTabConf = new Compartment();
68
+ const indentUnitConf = new Compartment();
69
+ const lineWrappingConf = new Compartment();
70
+ const highlightWhitespaceConf = new Compartment();
71
+ const a11yConf = new Compartment();
72
+ const External = Annotation.define<boolean>();
73
+ const setupExtensions = {
74
+ basic: basicSetup,
75
+ minimal: minimalSetup
76
+ } satisfies Record<Exclude<CodeEditorSetup, null>, Extension>;
77
+ let languageRequest = 0;
78
+ /** 最近一次**已生效**的语言解析结果,用于跳过等价的重复配置(CEV-003)。 */
79
+ let appliedLanguage: ResolvedCodeEditorLanguage | undefined;
80
+
81
+ onMounted(() => {
82
+ const parent = editorRef.value;
83
+ if (!parent) return;
84
+
85
+ const state = EditorState.create({
86
+ doc: props.value,
87
+ extensions: [
88
+ setupConf.of(getSetupExtension()),
89
+ languageConf.of([]),
90
+ themeConf.of([]),
91
+ readonlyConf.of([]),
92
+ editableConf.of([]),
93
+ placeholderConf.of([]),
94
+ indentWithTabConf.of([]),
95
+ indentUnitConf.of([]),
96
+ lineWrappingConf.of([]),
97
+ highlightWhitespaceConf.of([]),
98
+ a11yConf.of([]),
99
+ EditorView.theme({
100
+ '&': { fontSize: '0.875rem', height: '100%' },
101
+ '&.cm-focused': { outline: '2px solid Highlight', outlineOffset: '-2px' },
102
+ '.cm-gutters': { background: 'initial', border: 'none' },
103
+ '.cm-scroller': { overflow: 'auto' }
104
+ }),
105
+ EditorView.domEventObservers({
106
+ blur: () => emit('blur'),
107
+ focus: () => emit('focus')
108
+ }),
109
+ EditorView.updateListener.of(update => {
110
+ const external = update.transactions.some(transaction => transaction.annotation(External));
111
+ if (!update.docChanged || external) return;
112
+ const value = update.state.doc.toString();
113
+ emit('update:value', value);
114
+ emit('change', value);
115
+ })
116
+ ]
117
+ });
118
+ const view = new EditorView({
119
+ parent,
120
+ state,
121
+ ...(props.root ? { root: props.root } : {})
122
+ });
123
+
124
+ viewRef.value = view;
125
+ updateTheme();
126
+ updateLanguage();
127
+ updateReadonly();
128
+ updatePlaceholder();
129
+ updateIndent();
130
+ updateLineWrapping();
131
+ updateHighlightWhitespace();
132
+ updateAccessibility();
133
+ // CEA-008 / CER-006(用户可见的行为修正):`disabled` / `readonly` 都会把编辑器配成
134
+ // 不可编辑,此时仍然抢焦点,用户只会得到一个无法输入、也无从得知为什么无法输入的控件。
135
+ if (props.autoFocus && shouldAutoFocusCodeEditor(props)) view.focus();
136
+ });
137
+
138
+ onUnmounted(() => {
139
+ languageRequest += 1;
140
+ const view = viewRef.value;
141
+ viewRef.value = null;
142
+ view?.destroy();
143
+ });
144
+
145
+ watch(
146
+ () => props.value,
147
+ value => {
148
+ const view = viewRef.value;
149
+ if (!view) return;
150
+ // CEV-001:早先是 `{ from: 0, to: doc.length }` 全文替换 —— CodeMirror 会把落在
151
+ // 替换区内的光标映射到区间起点,宿主保存 / 格式化 / 协同同步一次就把用户送回文档开头。
152
+ // 差量计算走核心包,三端共用同一份实现。
153
+ const changes = computeMinimalDocumentChange(view.state.doc.toString(), value ?? '');
154
+ if (!changes) return;
155
+ view.dispatch({
156
+ // `External` 只被本组件的 update listener 用来阻止回调环路,CodeMirror 的 history
157
+ // **不认识**它;少了 `addToHistory.of(false)`,宿主回写照样进 undo 栈。
158
+ annotations: [External.of(true), Transaction.addToHistory.of(false)],
159
+ changes,
160
+ scrollIntoView: false
161
+ });
162
+ }
163
+ );
164
+ watch(() => props.setup, updateSetup);
165
+ watch(() => props.theme, updateTheme);
166
+ watch([() => props.language, () => props.languages], syncLanguage);
167
+ watch([() => props.readonly, () => props.disabled], updateReadonly);
168
+ watch(() => props.placeholder, updatePlaceholder);
169
+ watch([() => props.indentWithTab, () => props.indentUnit], updateIndent);
170
+ watch(() => props.lineWrapping, updateLineWrapping);
171
+ watch(() => props.highlightWhitespace, updateHighlightWhitespace);
172
+ watch([() => props.describedBy, () => props.disabled, () => props.label, () => props.labelledBy], updateAccessibility);
173
+
174
+ function updateSetup() {
175
+ viewRef.value?.dispatch({
176
+ effects: setupConf.reconfigure(getSetupExtension())
177
+ });
178
+ }
179
+
180
+ function getSetupExtension(): Extension {
181
+ return props.setup === null ? [] : setupExtensions[props.setup];
182
+ }
183
+
184
+ function updateTheme() {
185
+ viewRef.value?.dispatch({
186
+ effects: themeConf.reconfigure(props.theme === 'dark' ? oneDark : [])
187
+ });
188
+ }
189
+
190
+ /**
191
+ * `language` / `languages` 变化后的语言同步。
192
+ *
193
+ * CEV-003:`watch` 按数组**引用**触发。父组件写 `:languages="[myLang]"` 这类内联字面量时,
194
+ * 每轮更新都会重新查找、`load()` 并 reconfigure 语言 compartment ——
195
+ * 受控大文档等于每次输入都整篇重新词法分析。判定基准改成
196
+ * 「当前语言实际解析到的 description identity」,与 Angular / React 同构。
197
+ */
198
+ function syncLanguage() {
199
+ const resolved = resolveCodeEditorLanguage(props.language, props.languages);
200
+ if (isSameResolvedLanguage(appliedLanguage, resolved)) return;
201
+ updateLanguage();
202
+ }
203
+
204
+ /**
205
+ * **无条件**重新应用语言配置。挂载时以及 {@link syncLanguage} 判定确有变化时调用。
206
+ */
207
+ function updateLanguage() {
208
+ const request = ++languageRequest;
209
+ const view = viewRef.value;
210
+ if (!view) return;
211
+
212
+ const language = props.language;
213
+ const resolved = resolveCodeEditorLanguage(language, props.languages);
214
+ appliedLanguage = resolved;
215
+ if (resolved.kind !== 'found') {
216
+ view.dispatch({ effects: languageConf.reconfigure([]) });
217
+ // CEV-004:`none` 是「明确不高亮」,不是错误;只有 `not-found` 才报给宿主。
218
+ if (resolved.kind === 'not-found') {
219
+ console.error(`[CodeEditor] Language '${language}' not found.`);
220
+ emit('language-error', codeEditorLanguageNotFound(language));
221
+ }
222
+ return;
223
+ }
224
+
225
+ // 解析结果里已经带着命中的 description —— 早先这里又 `findLanguageByName` 查了一遍,
226
+ // 同一份查找逻辑跑两次,两处结果分叉时没有任何信号。
227
+ void resolved.description.load().then(
228
+ support => {
229
+ if (!isCurrentLanguageRequest(request, view)) return;
230
+ view.dispatch({ effects: languageConf.reconfigure(support.extension as Extension) });
231
+ },
232
+ (error: unknown) => {
233
+ if (!isCurrentLanguageRequest(request, view)) return;
234
+ view.dispatch({ effects: languageConf.reconfigure([]) });
235
+ console.error(`[CodeEditor] Failed to load language '${language}':`, error);
236
+ emit('language-error', codeEditorLanguageLoadFailed(language, error));
237
+ }
238
+ );
239
+ }
240
+
241
+ function isCurrentLanguageRequest(request: number, view: EditorView) {
242
+ return request === languageRequest && viewRef.value === view;
243
+ }
244
+
245
+ function updateReadonly() {
246
+ const view = viewRef.value;
247
+ if (!view) return;
248
+ const readonly = props.readonly || props.disabled;
249
+ view.dispatch({
250
+ effects: [
251
+ readonlyConf.reconfigure(EditorState.readOnly.of(readonly)),
252
+ editableConf.reconfigure(EditorView.editable.of(!readonly))
253
+ ]
254
+ });
255
+ }
256
+
257
+ /**
258
+ * 把无障碍属性写到真正承担 `role="textbox"` 的 `.cm-content` 上。
259
+ *
260
+ * CEA-008 / CER-006:只在宿主 `div` 上补 `aria-*` 是把缺陷换个位置 —— 屏幕阅读器进到
261
+ * textbox 之后读到的仍然是一个没有可访问名称、只报 `aria-readonly` 的多行文本框。
262
+ * `readonlyConf` 让 CodeMirror 写出 `aria-readonly`,但「只读」与「禁用」不是一回事,
263
+ * 禁用语义必须单独告知。属性字典由核心包计算,三端共用同一份实现。
264
+ */
265
+ function updateAccessibility() {
266
+ const attributes = buildCodeEditorContentAttributes(props);
267
+ viewRef.value?.dispatch({ effects: a11yConf.reconfigure(EditorView.contentAttributes.of(attributes)) });
268
+ }
269
+
270
+ function updatePlaceholder() {
271
+ viewRef.value?.dispatch({
272
+ effects: placeholderConf.reconfigure(props.placeholder ? placeholderExt(props.placeholder) : [])
273
+ });
274
+ }
275
+
276
+ function updateIndent() {
277
+ viewRef.value?.dispatch({
278
+ effects: [
279
+ indentWithTabConf.reconfigure(props.indentWithTab ? keymap.of([indentWithTabCommand]) : []),
280
+ indentUnitConf.reconfigure(props.indentUnit ? indentUnitExt.of(props.indentUnit) : [])
281
+ ]
282
+ });
283
+ }
284
+
285
+ function updateLineWrapping() {
286
+ viewRef.value?.dispatch({
287
+ effects: lineWrappingConf.reconfigure(props.lineWrapping ? EditorView.lineWrapping : [])
288
+ });
289
+ }
290
+
291
+ function updateHighlightWhitespace() {
292
+ viewRef.value?.dispatch({
293
+ effects: highlightWhitespaceConf.reconfigure(props.highlightWhitespace ? highlightWhitespaceExt() : [])
294
+ });
295
+ }
296
+
297
+ defineExpose<CodeEditorExpose>({
298
+ get view() {
299
+ return viewRef.value;
300
+ },
301
+ // 跟着 `view` 一起置空:`editorRef` 在卸载后仍可能残留元素引用,
302
+ // 而 handle 的契约是「未挂载或已卸载时为 null」,三端逐字一致(CEA-009)。
303
+ get host() {
304
+ return viewRef.value ? editorRef.value : null;
305
+ },
306
+ blur: () => viewRef.value?.contentDOM.blur(),
307
+ focus: () => viewRef.value?.focus()
308
+ });
309
+ </script>
310
+
311
+ <template>
312
+ <div
313
+ v-bind="hostAttributes"
314
+ ref="editorRef"
315
+ />
316
+ </template>
@@ -0,0 +1,177 @@
1
+ import type { CodeEditorLanguageDescription, CodeEditorLanguageError, CodeEditorTheme } from '@aiao/code-editor';
2
+ import type { EditorView } from '@codemirror/view';
3
+
4
+ /** CodeMirror 预置扩展集合。 */
5
+ export type CodeEditorSetup = 'basic' | 'minimal' | null;
6
+
7
+ /**
8
+ * Vue CodeEditor 的公开属性。
9
+ *
10
+ * @remarks 与 Angular 的 inputs、React 的 props 同名同义;仅初始化生效的两项
11
+ * ({@link CodeEditorProps.autoFocus} / {@link CodeEditorProps.root})在各自条目上标注。
12
+ */
13
+ export interface CodeEditorProps {
14
+ /**
15
+ * 挂载后立即聚焦编辑器。
16
+ *
17
+ * @defaultValue false
18
+ * @remarks **仅挂载时读取一次**,后续变更被忽略。`disabled` 或 `readonly` 为真时
19
+ * 不抢焦点 —— 否则用户得到一个无法输入、也无从得知为什么无法输入的控件。
20
+ */
21
+ readonly autoFocus?: boolean;
22
+ /**
23
+ * 补充说明元素的 id,写成内部 textbox 的 `aria-describedby`。
24
+ *
25
+ * @remarks 属性落在 CodeMirror 的 `.cm-content` 上 —— 承担 `role="textbox"` 的是它,
26
+ * 不是组件的宿主元素。空串按「未设置」处理。三端同名同义。
27
+ */
28
+ readonly describedBy?: string;
29
+ /**
30
+ * 禁用:只读且不可编辑,并向辅助技术报告 `aria-disabled`。
31
+ *
32
+ * @defaultValue false
33
+ * @remarks 与 `readonly` 是独立的两个属性,任一为真都会让编辑器不可编辑;
34
+ * 区别在于禁用还会写出 `aria-disabled` 并抑制 `autoFocus`。
35
+ */
36
+ readonly disabled?: boolean;
37
+ /**
38
+ * 显示空白字符。
39
+ *
40
+ * @defaultValue false
41
+ */
42
+ readonly highlightWhitespace?: boolean;
43
+ /**
44
+ * 一级缩进使用的字符串。
45
+ *
46
+ * @defaultValue ' '(两个空格,三端一致)
47
+ */
48
+ readonly indentUnit?: string;
49
+ /**
50
+ * 允许 Tab 键缩进。
51
+ *
52
+ * @defaultValue false
53
+ * @remarks 打开后 Tab 不再移动焦点,键盘用户会被困在编辑器里 —— 这是可达性取舍,
54
+ * 三端默认一致地关闭。
55
+ */
56
+ readonly indentWithTab?: boolean;
57
+ /**
58
+ * 语法高亮语言名或别名;`''` 与 `'plaintext'` 表示不高亮。
59
+ *
60
+ * @defaultValue 'sql'
61
+ * @remarks 查不到或 `load()` 失败时**降级为纯文本**并继续可用,同时发出
62
+ * `language-error` 事件(见 {@link CodeEditorEmits})。快速切换时只有最后一次请求生效。
63
+ */
64
+ readonly language?: string;
65
+ /**
66
+ * 候选语言列表;空列表表示不高亮。
67
+ *
68
+ * @defaultValue SUPPORT_LANGUAGES
69
+ * @remarks 传内联字面量(`:languages="[myLang]"`)是有成本的:每轮更新都是新数组,
70
+ * 而 `watch` 按引用触发。组件会再判断当前语言解析到的 description 是否真的换了 ——
71
+ * 只有真换了才重新 `load()`,但这层判断本身仍要跑,稳定引用(模块级常量或 `computed`)更省。
72
+ */
73
+ readonly languages?: readonly CodeEditorLanguageDescription[];
74
+ /**
75
+ * 可访问名称,写成内部 textbox 的 `aria-label`。
76
+ *
77
+ * @remarks 落点与 {@link CodeEditorProps.describedBy} 相同。
78
+ */
79
+ readonly label?: string;
80
+ /**
81
+ * 承担标签的元素 id,写成内部 textbox 的 `aria-labelledby`。
82
+ *
83
+ * @remarks 落点与 {@link CodeEditorProps.describedBy} 相同。
84
+ */
85
+ readonly labelledBy?: string;
86
+ /**
87
+ * 自动换行。
88
+ *
89
+ * @defaultValue false
90
+ */
91
+ readonly lineWrapping?: boolean;
92
+ /**
93
+ * 空文档时显示的占位文本。
94
+ *
95
+ * @defaultValue ''
96
+ */
97
+ readonly placeholder?: string;
98
+ /**
99
+ * 只读:仍可聚焦、可选择、可复制,但不能改。
100
+ *
101
+ * @defaultValue false
102
+ * @remarks 只挡编辑**命令**,不挡宿主同步 —— `value` 变化照样写进文档,
103
+ * 否则只读的受控用法直接失效。
104
+ */
105
+ readonly readonly?: boolean;
106
+ /**
107
+ * CodeMirror 挂载的 DOM 根,用于 Shadow DOM 场景。
108
+ *
109
+ * @remarks **仅挂载时读取一次** —— CodeMirror 无法在构造后改 root。
110
+ * 需要换 root 时请用 `v-if` 或 `:key` 强制重建组件。
111
+ */
112
+ readonly root?: Document | ShadowRoot;
113
+ /**
114
+ * CodeMirror 预设扩展集;`null` 表示不加载任何预设。
115
+ *
116
+ * @defaultValue 'basic'
117
+ */
118
+ readonly setup?: CodeEditorSetup;
119
+ /**
120
+ * 主题。
121
+ *
122
+ * @defaultValue 'light'
123
+ */
124
+ readonly theme?: CodeEditorTheme;
125
+ /**
126
+ * 文档内容,配合 `v-model:value` 使用。
127
+ *
128
+ * @defaultValue ''
129
+ * @remarks **不是严格受控**:组件不会把用户编辑写回本属性,也不会在每次更新时
130
+ * 强行拉平文档。本属性变化时只同步公共前后缀之间的那一段差量,光标、选区与滚动位置都保留,
131
+ * 且不进 undo 栈。父组件忽略 `update:value` 时编辑器会保留用户输入。
132
+ */
133
+ readonly value?: string;
134
+ }
135
+
136
+ /** Vue CodeEditor 的公开事件。 */
137
+ export interface CodeEditorEmits {
138
+ /** 编辑器内容区域失去焦点。 */
139
+ blur: [];
140
+ /** 用户编辑导致的内容变化;宿主经 `value` 的程序化写入**不**触发。 */
141
+ change: [value: string];
142
+ /** 编辑器内容区域获得焦点。 */
143
+ focus: [];
144
+ /**
145
+ * 语言解析或加载失败。
146
+ *
147
+ * @remarks
148
+ * CEV-004:这条路径此前只有一条 `console.error`,宿主完全观测不到 ——
149
+ * 拿不到错误、没有重试或降级的机会,用户只看到高亮突然消失。
150
+ * 降级策略不变(清空语言扩展退回纯文本),日志也保留,本事件是**新增的**观测通道。
151
+ * 载荷由核心包定义,与 Angular 的 `aoLanguageError` 输出、React 的 `onLanguageError`
152
+ * 回调逐字一致:`kind` 为 `'not-found'` 时是配置错误,重试没有意义;
153
+ * 为 `'load-failed'` 时通常是网络或分包问题,可以重试。
154
+ */
155
+ 'language-error': [error: CodeEditorLanguageError];
156
+ /** `v-model:value` 的更新通道,触发时机与 `change` 相同。 */
157
+ 'update:value': [value: string];
158
+ }
159
+
160
+ /**
161
+ * `defineExpose` 暴露给模板 ref 的命令式 API。
162
+ *
163
+ * @remarks 四个成员与 React 的 `CodeEditorHandle`、Angular 组件实例上的同名成员
164
+ * 逐字对齐(CEA-009)—— 跨端迁移时命令式面不需要改写。
165
+ */
166
+ export interface CodeEditorExpose {
167
+ /** 底层 CodeMirror EditorView,未挂载或已卸载时为 null。 */
168
+ readonly view: EditorView | null;
169
+ /** 宿主 `div` 元素(CodeMirror 挂载所在的容器),未挂载或已卸载时为 null。 */
170
+ readonly host: HTMLDivElement | null;
171
+ /** 移开键盘焦点;未挂载时是空操作。 */
172
+ blur(): void;
173
+ /** 把键盘焦点交给编辑器内容区域;未挂载时是空操作。 */
174
+ focus(): void;
175
+ }
176
+
177
+ export type { CodeEditorTheme } from '@aiao/code-editor';
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @aiao/code-editor-vue
3
+ *
4
+ * @example
5
+ * ```vue
6
+ * <script setup lang="ts">
7
+ * import { CodeEditor } from '@aiao/code-editor-vue';
8
+ * </script>
9
+ *
10
+ * <template>
11
+ * <CodeEditor v-model:value="code" language="typescript" />
12
+ * </template>
13
+ * ```
14
+ */
15
+ export type {
16
+ CodeEditorEmits,
17
+ CodeEditorExpose,
18
+ CodeEditorProps,
19
+ CodeEditorSetup,
20
+ CodeEditorTheme
21
+ } from './code-editor.types.js';
22
+ export { default as CodeEditor } from './CodeEditor.vue';
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import { defineComponent } from 'vue';
3
+ const component: ReturnType<typeof defineComponent>;
4
+ export default component;
5
+ }
package/dist/index.css DELETED
@@ -1,2 +0,0 @@
1
- .aiao-code-editor[data-v-9b1829ca]{width:100%;height:100%;font-size:.875rem;display:block;overflow:hidden}.aiao-code-editor[data-v-9b1829ca] .cm-editor{height:100%}.aiao-code-editor[data-v-9b1829ca] .cm-scroller{overflow:auto}
2
- /*$vite$:1*/