@aiao/code-editor-angular 0.0.25 → 0.0.28

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,555 +1,610 @@
1
- import { SUPPORT_LANGUAGES, shouldAutoFocusCodeEditor, computeMinimalDocumentChange, resolveCodeEditorLanguage, codeEditorLanguageNotFound, codeEditorLanguageLoadFailed, isSameResolvedLanguage, buildCodeEditorContentAttributes } from '@aiao/code-editor';
2
- import { isPlatformBrowser } from '@angular/common';
3
- import * as i0 from '@angular/core';
4
- import { inject, ElementRef, PLATFORM_ID, signal, computed, input, booleanAttribute, output, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
5
- import { NG_VALUE_ACCESSOR } from '@angular/forms';
6
- import { indentWithTab } from '@codemirror/commands';
7
- import { indentUnit } from '@codemirror/language';
8
- import { Annotation, Compartment, EditorState, Transaction } from '@codemirror/state';
9
- import { oneDark } from '@codemirror/theme-one-dark';
10
- import { EditorView, placeholder, keymap, highlightWhitespace } from '@codemirror/view';
11
- import { basicSetup, minimalSetup } from 'codemirror';
12
-
13
- /**
14
- * 标记「外部写入」的注解,用于让 updateListener 跳过 onChange 回调。
15
- *
16
- * @internal
17
- * 模块私有,不对外导出:每个框架绑定各有一个互不相同的 Annotation 实例,
18
- * 一旦暴露就等于承诺了一个三端无法对齐的语义(React / Vue 两端同样是模块私有)。
19
- */
1
+ import { SUPPORT_LANGUAGES, buildCodeEditorContentAttributes, codeEditorLanguageLoadFailed, codeEditorLanguageNotFound, computeMinimalDocumentChange, isSameResolvedLanguage, resolveCodeEditorLanguage, shouldAutoFocusCodeEditor } from "@aiao/code-editor";
2
+ import { isPlatformBrowser } from "@angular/common";
3
+ import * as i0 from "@angular/core";
4
+ import { ChangeDetectionStrategy, Component, ElementRef, PLATFORM_ID, booleanAttribute, computed, forwardRef, inject, input, output, signal } from "@angular/core";
5
+ import { NG_VALUE_ACCESSOR } from "@angular/forms";
6
+ import { indentWithTab } from "@codemirror/commands";
7
+ import { indentUnit } from "@codemirror/language";
8
+ import { Annotation, Compartment, EditorState, Transaction } from "@codemirror/state";
9
+ import { oneDark } from "@codemirror/theme-one-dark";
10
+ import { EditorView, highlightWhitespace, keymap, placeholder } from "@codemirror/view";
11
+ import { basicSetup, minimalSetup } from "codemirror";
20
12
  const External = Annotation.define();
21
- /**
22
- * CodeEditor Angular 组件
23
- *
24
- * 基于 CodeMirror 6 的代码编辑器 Angular 组件,支持多种语言和主题
25
- *
26
- * @example
27
- * ```html
28
- * <ao-code-editor
29
- * [(ngModel)]="code"
30
- * language="typescript"
31
- * theme="dark"
32
- * (aoChange)="onCodeChange($event)"
33
- * />
34
- * ```
35
- */
36
- class CodeEditor {
37
- /**
38
- * EditorView 实例。
39
- * https://codemirror.net/docs/ref/#view.EditorView
40
- */
41
- #view;
42
- #pendingValue;
43
- #languageRequest = 0;
44
- /** 最近一次**已生效**的语言解析结果,用于跳过等价的重复配置(CEA-006)。 */
45
- #appliedLanguage;
46
- #elementRef = inject(ElementRef);
47
- #platformId = inject(PLATFORM_ID);
48
- #isBrowser = isPlatformBrowser(this.#platformId);
49
- // 用于动态重新配置的 Compartments。
50
- // https://codemirror.net/docs/ref/#state.Compartment
51
- #editableConf = new Compartment();
52
- #readonlyConf = new Compartment();
53
- #themeConf = new Compartment();
54
- #placeholderConf = new Compartment();
55
- #indentWithTabConf = new Compartment();
56
- #indentUnitConf = new Compartment();
57
- #lineWrappingConf = new Compartment();
58
- #highlightWhitespaceConf = new Compartment();
59
- #languageConf = new Compartment();
60
- #a11yConf = new Compartment();
61
- /** `setup` 预设的专属槽 —— 与 React(`CodeEditor.tsx`)、Vue(`CodeEditor.vue`)同构。 */
62
- #setupConf = new Compartment();
63
- /** 消费者经 {@link CodeEditor.setExtensions} 装入的扩展的专属槽,见 CEA-003。 */
64
- #userExtensionsConf = new Compartment();
65
- // 内容变化监听器。
66
- #updateListener = EditorView.updateListener.of(vu => {
67
- if (vu.docChanged && !vu.transactions.some(tr => tr.annotation(External))) {
68
- const value = vu.state.doc.toString();
69
- this.#onChange?.(value);
70
- this.aoChange.emit(value);
71
- }
72
- });
73
- // 焦点事件必须随初始 extensions 一起装配,不能在 EditorView 构造完再 addEventListener:
74
- // `autoFocus` 的 `view.focus()` 紧跟构造同步派发 focus,后注册的监听器永远吞掉第一次。
75
- // 用 domEventObservers 还顺带解决了裸监听器没有 removeEventListener 的问题 ——
76
- // 它随 view.destroy() 一起回收。与 Vue(`CodeEditor.vue:75-78`)同构。
77
- #domEventObservers = EditorView.domEventObservers({
78
- focus: () => {
79
- this.aoFocus.emit();
80
- },
81
- blur: () => {
82
- this.#onTouched?.();
83
- this.aoBlur.emit();
84
- }
85
- });
86
- /**
87
- * 表单侧的禁用状态,只由 {@link CodeEditor.setDisabledState} 写入。
88
- *
89
- * @internal
90
- * 必须私有:它只在 `#effectiveDisabled` 里被消费,外部 `.set()` 写了也不会同步到编辑器。
91
- */
92
- #formDisabled = signal(false, /* @ts-ignore */
93
- ...(ngDevMode ? [{ debugName: "#formDisabled" }] : /* istanbul ignore next */ []));
94
- /**
95
- * 有效禁用状态 = `disabled` 输入与表单禁用**任一为真**。
96
- *
97
- * @internal
98
- * CEA-004:两条来源早先被压进同一个 `linkedSignal(() => this.disabled())`,
99
- * 成了「最后写的赢」。它们是**独立的事实**:模板的 `[disabled]="true"` 说
100
- * 「这个控件永远禁用」,`setDisabledState()` 说「这个 FormControl 当前 disable 了」。
101
- * 压成一个值有两个可复现的后果 —— Reactive Forms 在控件 enabled 时会主动调
102
- * `setDisabledState(false)`,把 `[disabled]="true"` 静默抹掉;反过来 `disabled`
103
- * 输入一变化 `linkedSignal` 就重算,把表单侧刚设的禁用丢掉。
104
- */
105
- #effectiveDisabled = computed(() => this.disabled() || this.#formDisabled(), /* @ts-ignore */
106
- ...(ngDevMode ? [{ debugName: "#effectiveDisabled" }] : /* istanbul ignore next */ []));
107
- /** @internal 只能经 {@link registerOnChange} 注入,公开会让消费者覆盖后静默切断表单双向绑定。 */
108
- #onChange;
109
- /** @internal 只能经 {@link registerOnTouched} 注入,理由同 {@link registerOnChange}。 */
110
- #onTouched;
111
- /**
112
- * 宿主元素上的 `aria-disabled`。
113
- *
114
- * @internal
115
- * 未禁用时返回 `null` 让 Angular **移除**属性,而不是写 `aria-disabled="false"`;
116
- * 理由与 {@link buildCodeEditorContentAttributes} 一致。`protected` 是可见性下限 ——
117
- * host 绑定表达式读不到 `#` 私有字段。
118
- */
119
- _ariaDisabled = computed(() => (this.#effectiveDisabled() ? 'true' : null), /* @ts-ignore */
120
- ...(ngDevMode ? [{ debugName: "_ariaDisabled" }] : /* istanbul ignore next */ []));
121
- // 输入属性。
122
- /** 初始化后立即聚焦编辑器。**仅初始化生效**,后续变更被忽略。 @defaultValue false */
123
- autoFocus = input(false, { ...(ngDevMode ? { debugName: "autoFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
124
- /**
125
- * 补充说明元素的 id,写成内部 textbox 的 `aria-describedby`。可动态更新。
126
- *
127
- * @defaultValue ''(空串按「未设置」处理,不产出属性)
128
- * @remarks 属性落在 CodeMirror 的 `.cm-content` 上 —— 承担 `role="textbox"` 的是它,
129
- * 不是本组件的宿主元素。三端同名同义。
130
- */
131
- describedBy = input('', /* @ts-ignore */
132
- ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
133
- /**
134
- * 禁用编辑(只读且不可聚焦)。可动态更新。
135
- *
136
- * @defaultValue false
137
- * @remarks 与表单的 `setDisabledState()` 是**两条独立来源**,任一为真即禁用。
138
- * 用 Reactive Forms 时把控件设成 `disabled` 与在模板上写 `[disabled]="true"` 效果一样,
139
- * 但只解开其中一条不会恢复可编辑 —— 两条都得放开。
140
- */
141
- disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
142
- /** 显示空白字符。可动态更新。 @defaultValue false */
143
- highlightWhitespace = input(false, { ...(ngDevMode ? { debugName: "highlightWhitespace" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
144
- /** 一级缩进使用的字符串。可动态更新。 @defaultValue ' '(两个空格,三端一致) */
145
- indentUnit = input(' ', /* @ts-ignore */
146
- ...(ngDevMode ? [{ debugName: "indentUnit" }] : /* istanbul ignore next */ []));
147
- /** 允许 Tab 键缩进(会牺牲键盘可达性)。可动态更新。 @defaultValue false */
148
- indentWithTab = input(false, { ...(ngDevMode ? { debugName: "indentWithTab" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
149
- /** 语法高亮语言名,取值见 {@link languages};`'plaintext'` 表示不高亮。可动态更新。 @defaultValue 'sql' */
150
- language = input('sql', /* @ts-ignore */
151
- ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
152
- /** 可选语言列表。可动态更新。 @defaultValue SUPPORT_LANGUAGES */
153
- languages = input(SUPPORT_LANGUAGES, /* @ts-ignore */
154
- ...(ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []));
155
- /**
156
- * 可访问名称,写成内部 textbox 的 `aria-label`。可动态更新。
157
- *
158
- * @defaultValue ''(空串按「未设置」处理,不产出属性)
159
- * @remarks 落点与 {@link CodeEditor.describedBy} 相同。
160
- */
161
- label = input('', /* @ts-ignore */
162
- ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
163
- /**
164
- * 承担标签的元素 id,写成内部 textbox 的 `aria-labelledby`。可动态更新。
165
- *
166
- * @defaultValue ''(空串按「未设置」处理,不产出属性)
167
- * @remarks 落点与 {@link CodeEditor.describedBy} 相同。
168
- */
169
- labelledBy = input('', /* @ts-ignore */
170
- ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
171
- /** 自动换行。可动态更新。 @defaultValue false */
172
- lineWrapping = input(false, { ...(ngDevMode ? { debugName: "lineWrapping" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
173
- /** 空文档时显示的占位文本。可动态更新。 @defaultValue '' */
174
- placeholder = input('', /* @ts-ignore */
175
- ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
176
- /** 只读(仍可聚焦与选择)。可动态更新。 @defaultValue false */
177
- readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
178
- /** CodeMirror 挂载的 DOM 根,用于 Shadow DOM 场景。**仅初始化生效**。 */
179
- root = input(/* @ts-ignore */
180
- ...(ngDevMode ? [undefined, { debugName: "root" }] : /* istanbul ignore next */ []));
181
- /** 预设扩展集;`null` 表示不加载任何预设。可动态更新。 @defaultValue 'basic' */
182
- setup = input('basic', /* @ts-ignore */
183
- ...(ngDevMode ? [{ debugName: "setup" }] : /* istanbul ignore next */ []));
184
- /** 主题。可动态更新。 @defaultValue 'light' */
185
- theme = input('light', /* @ts-ignore */
186
- ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
187
- /** 文档内容。作为受控输入使用时由外部驱动;与 `ngModel` 同时使用以 `ngModel` 为准。 @defaultValue '' */
188
- value = input('', /* @ts-ignore */
189
- ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
190
- // 输出事件。
191
- /** 编辑器获得焦点。 */
192
- aoFocus = output();
193
- /** 文档内容变化(外部写入不触发,见 {@link External})。 */
194
- aoChange = output();
195
- /** 编辑器失去焦点,同时触发表单 touched。 */
196
- aoBlur = output();
197
- /**
198
- * 语言解析或加载失败。
199
- *
200
- * @remarks
201
- * CEA-007:这条路径此前只有一条 `console.error`,宿主完全观测不到 ——
202
- * 拿不到错误、没有重试或降级的机会,用户只看到高亮突然消失。
203
- * 降级策略不变(清空语言扩展退回纯文本),日志也保留,本事件是**新增的**观测通道。
204
- * 与 React 的 `onLanguageError` 属性、Vue 的 `language-error` 事件同载荷。
205
- */
206
- aoLanguageError = output();
207
- /**
208
- * 底层 CodeMirror `EditorView`;未初始化或已销毁时为 `null`。
209
- *
210
- * @remarks 三端命令式面统一为 `view` / `host` / `focus()` / `blur()`(CEA-009)。
211
- */
212
- get view() {
213
- return this.#view ?? null;
214
- }
215
- /**
216
- * CodeMirror 挂载所在的宿主元素;未初始化或已销毁时为 `null`。
217
- *
218
- * @remarks
219
- * Angular 的宿主是 `<ao-code-editor>` 元素本身,React / Vue 是组件渲染出的 `div`——
220
- * 类型不同但语义一致:「CodeMirror 挂在哪个元素里」。
221
- */
222
- get host() {
223
- return this.#view ? this.#elementRef.nativeElement : null;
224
- }
225
- /** 把键盘焦点交给编辑器。未初始化时是空操作。 */
226
- focus() {
227
- this.#view?.focus();
228
- }
229
- /** 移开键盘焦点。未初始化时是空操作。 */
230
- blur() {
231
- this.#view?.contentDOM.blur();
232
- }
233
- ngOnInit() {
234
- if (!this.#isBrowser)
235
- return;
236
- const extensions = this._get_all_extensions();
237
- const state = EditorState.create({ doc: this.#pendingValue ?? this.value(), extensions });
238
- this.#view = new EditorView({
239
- parent: this.#elementRef.nativeElement,
240
- root: this.root(),
241
- state
242
- });
243
- // 访问状态必须先落地:`autoFocus` 要读**已生效**的禁用 / 只读语义,而 `focus()`
244
- // 又会同步派发 focus 事件,晚同步等于让监听者先读到一个还没对齐的状态。
245
- this.#syncAccessState();
246
- // CEA-008(用户可见的行为修正):`disabled` / `readonly` 都会把编辑器配成不可编辑,
247
- // 此时仍然抢焦点,用户只会得到一个无法输入、也无从得知为什么无法输入的控件。
248
- if (this.autoFocus() &&
249
- shouldAutoFocusCodeEditor({ disabled: this.#effectiveDisabled(), readonly: this.readonly() })) {
250
- this.#view.focus();
251
- }
252
- this._dispatch_effects(this.#themeConf.reconfigure(this.theme() === 'dark' ? oneDark : []));
253
- this._dispatch_effects(this.#placeholderConf.reconfigure(this.placeholder() ? placeholder(this.placeholder()) : []));
254
- this._dispatch_effects(this.#indentWithTabConf.reconfigure(this.indentWithTab() ? keymap.of([indentWithTab]) : []));
255
- this._dispatch_effects(this.#indentUnitConf.reconfigure(this.indentUnit() ? indentUnit.of(this.indentUnit()) : []));
256
- this._dispatch_effects(this.#lineWrappingConf.reconfigure(this.lineWrapping() ? EditorView.lineWrapping : []));
257
- this._dispatch_effects(this.#highlightWhitespaceConf.reconfigure(this.highlightWhitespace() ? highlightWhitespace() : []));
258
- this.setLanguage(this.language());
259
- }
260
- ngOnChanges(changes) {
261
- if (!this.#view)
262
- return;
263
- if (changes['value'] && this.#view) {
264
- const newValue = this.value();
265
- const change = computeMinimalDocumentChange(this.#view.state.doc.toString(), newValue);
266
- if (change) {
267
- this.#view.dispatch({
268
- changes: change,
269
- annotations: [External.of(true), Transaction.addToHistory.of(false)],
270
- scrollIntoView: false
271
- });
272
- }
273
- }
274
- if (changes['disabled'] || changes['readonly']) {
275
- this.#syncAccessState();
276
- }
277
- if (changes['describedBy'] || changes['label'] || changes['labelledBy']) {
278
- this.#syncContentAttributes();
279
- }
280
- if (changes['theme']) {
281
- this._dispatch_effects(this.#themeConf.reconfigure(this.theme() === 'dark' ? oneDark : []));
282
- }
283
- if (changes['placeholder']) {
284
- this._dispatch_effects(this.#placeholderConf.reconfigure(this.placeholder() ? placeholder(this.placeholder()) : []));
285
- }
286
- if (changes['indentWithTab']) {
287
- this._dispatch_effects(this.#indentWithTabConf.reconfigure(this.indentWithTab() ? keymap.of([indentWithTab]) : []));
288
- }
289
- if (changes['indentUnit']) {
290
- this._dispatch_effects(this.#indentUnitConf.reconfigure(this.indentUnit() ? indentUnit.of(this.indentUnit()) : []));
291
- }
292
- if (changes['lineWrapping']) {
293
- this._dispatch_effects(this.#lineWrappingConf.reconfigure(this.lineWrapping() ? EditorView.lineWrapping : []));
294
- }
295
- if (changes['highlightWhitespace']) {
296
- this._dispatch_effects(this.#highlightWhitespaceConf.reconfigure(this.highlightWhitespace() ? highlightWhitespace() : []));
297
- }
298
- if (changes['language'] || changes['languages']) {
299
- this.#syncLanguage();
300
- }
301
- if (changes['setup']) {
302
- // CEA-003:早先这里调的是 `setExtensions(this._get_all_extensions())`,
303
- // 即换个预设就把整棵扩展树连同消费者自己装的扩展一起重建。
304
- this._dispatch_effects(this.#setupConf.reconfigure(this.#setupExtension()));
305
- }
306
- }
307
- ngOnDestroy() {
308
- this.#languageRequest += 1;
309
- const view = this.#view;
310
- this.#view = undefined;
311
- view?.destroy();
312
- }
313
- /**
314
- * `ControlValueAccessor` 的写入入口,由 Angular Forms 调用。
315
- *
316
- * @param value `null` / `undefined` 规整成空串;`string` 原样写入
317
- * @throws TypeError 收到其他类型时立即抛出,错误信息含包名与实际类型
318
- *
319
- * @remarks
320
- * CEA-005:签名此前写成 `string | null`,但 CVA 的契约是 `any` —— Angular
321
- * **不做**运行时校验,`FormControl` 里放什么就原样送到这里。类型标注只骗过了编译期。
322
- * 于是 `FormControl(42)` 在初始化前进 `#pendingValue`、在 `EditorState.create` 处炸,
323
- * 初始化后又在 `computeMinimalDocumentChange` 处炸 —— 同一个错误配置,两个时机两种错,
324
- * 且都指不回 `writeValue`。现在两个时机抛同一个错,且直接点名包与类型。
325
- */
326
- writeValue(value) {
327
- const normalized = this.#normalizeFormValue(value);
328
- if (!this.#view) {
329
- this.#pendingValue = normalized;
330
- return;
331
- }
332
- const change = computeMinimalDocumentChange(this.#view.state.doc.toString(), normalized);
333
- if (change) {
334
- this.#view.dispatch({
335
- changes: change,
336
- annotations: [External.of(true), Transaction.addToHistory.of(false)],
337
- scrollIntoView: false
338
- });
339
- }
340
- }
341
- registerOnChange(fn) {
342
- this.#onChange = fn;
343
- }
344
- registerOnTouched(fn) {
345
- this.#onTouched = fn;
346
- }
347
- /**
348
- * 表单侧的禁用开关,由 Angular Forms 调用。
349
- *
350
- * @remarks 只写 {@link CodeEditor.#formDisabled} 这一条来源;模板上的
351
- * `[disabled]` 输入是另一条独立来源,两者取或,见 {@link CodeEditor.#effectiveDisabled}。
352
- */
353
- setDisabledState(isDisabled) {
354
- this.#formDisabled.set(isDisabled);
355
- this.#syncAccessState();
356
- }
357
- /**
358
- * 装入消费者自己的 CodeMirror 扩展。
359
- *
360
- * @param value 本次要生效的扩展;**整体替换**上一次传入的那批,传 `[]` 即全部撤下
361
- *
362
- * @remarks
363
- * CEA-003:早先这里发的是全局 `StateEffect.reconfigure.of(value)`,
364
- * 它替换的是**整棵扩展树** —— 一次调用就清掉 updateListener、全部 Compartment、
365
- * 主题、语言与 a11y 属性,名字叫「设置扩展」,实际是「摧毁编辑器配置」。
366
- * 现在只重配 {@link CodeEditor.#userExtensionsConf} 这一个专属槽,
367
- * 组件自身的配置与消费者的扩展互不影响。
368
- */
369
- setExtensions(value) {
370
- this._dispatch_effects(this.#userExtensionsConf.reconfigure(value));
371
- }
372
- /**
373
- * 命令式地(重新)应用语言配置。
374
- *
375
- * @param lang 语言名或别名;空串与 `'plaintext'` 表示不高亮
376
- *
377
- * @remarks
378
- * **无条件重新加载**,即使解析结果与当前生效的完全一致 —— 这是宿主换掉了同名描述
379
- * 背后的 loader 实现时的显式重载入口,`language` 输入做不到(它走的是
380
- * {@link CodeEditor.#syncLanguage} 里的等价性判断,CEA-006,不会重复加载)。
381
- * 日常切换语言应当改 `language` 输入。
382
- *
383
- * 解析失败时清空语言扩展退回纯文本,并经 {@link CodeEditor.aoLanguageError} 报告。
384
- */
385
- setLanguage(lang) {
386
- const request = ++this.#languageRequest;
387
- const view = this.#view;
388
- if (!view)
389
- return;
390
- const resolved = resolveCodeEditorLanguage(lang, this.languages());
391
- this.#appliedLanguage = resolved;
392
- if (resolved.kind === 'none') {
393
- view.dispatch({ effects: this.#languageConf.reconfigure([]) });
394
- return;
395
- }
396
- if (resolved.kind === 'not-found') {
397
- view.dispatch({ effects: this.#languageConf.reconfigure([]) });
398
- // 日志与另两端逐字一致,作为最后兜底诊断保留;结构化事件才是给宿主的契约通道。
399
- console.error(`[CodeEditor] Language '${lang}' not found.`);
400
- this.aoLanguageError.emit(codeEditorLanguageNotFound(lang));
401
- return;
402
- }
403
- // 直接用解析结果里的 description:早先这里又调一次 `_find_language(lang)` 重查一遍,
404
- // 同一个名字解析两遍,两处判定一旦分叉就会出现「解析说找到了、重查说没有」。
405
- void resolved.description.load().then(languageSupport => {
406
- if (request !== this.#languageRequest || this.#view !== view)
407
- return;
408
- view.dispatch({ effects: this.#languageConf.reconfigure(languageSupport.extension) });
409
- }, (error) => {
410
- if (request !== this.#languageRequest || this.#view !== view)
411
- return;
412
- view.dispatch({ effects: this.#languageConf.reconfigure([]) });
413
- console.error(`[CodeEditor] Failed to load language '${lang}':`, error);
414
- this.aoLanguageError.emit(codeEditorLanguageLoadFailed(lang, error));
415
- });
416
- }
417
- _get_all_extensions() {
418
- const extensions = [
419
- this.#updateListener,
420
- this.#domEventObservers,
421
- this.#editableConf.of([]),
422
- this.#readonlyConf.of([]),
423
- this.#themeConf.of([]),
424
- this.#placeholderConf.of([]),
425
- this.#indentWithTabConf.of([]),
426
- this.#indentUnitConf.of([]),
427
- this.#lineWrappingConf.of([]),
428
- this.#highlightWhitespaceConf.of([]),
429
- this.#languageConf.of([]),
430
- this.#a11yConf.of([]),
431
- this.#setupConf.of(this.#setupExtension()),
432
- // 消费者扩展排在最后 —— CodeMirror 里后来的优先级更高,
433
- // 宿主装的 keymap / 主题覆盖才压得住预设。
434
- this.#userExtensionsConf.of([])
435
- ];
436
- return extensions;
437
- }
438
- _dispatch_effects(effects) {
439
- return this.#view?.dispatch({ effects });
440
- }
441
- /**
442
- * `language` / `languages` 输入变化后的语言同步。
443
- *
444
- * CEA-006:早先只要 `languages` 的**数组引用**变了就无条件 `setLanguage()`。
445
- * 父模板写 `[languages]="[myLang]"` 这类内联字面量时每轮变更检测都产生新数组,
446
- * 于是每轮都重新 `load()` 并 reconfigure 语言 Compartment ——
447
- * 大文档等于每次输入都整篇重新词法分析,自定义异步 loader 还会多发请求。
448
- * 判定基准改成「当前语言实际解析到的 description identity」,与 React / Vue 同构。
449
- */
450
- #syncLanguage() {
451
- const language = this.language();
452
- const resolved = resolveCodeEditorLanguage(language, this.languages());
453
- if (isSameResolvedLanguage(this.#appliedLanguage, resolved))
454
- return;
455
- this.setLanguage(language);
456
- }
457
- /**
458
- * 把无障碍属性写到真正承担 `role="textbox"` 的 `.cm-content` 上。
459
- *
460
- * CEA-008 / CER-006:只在宿主 `div` 上补 `aria-*` 是把缺陷换个位置 —— 屏幕阅读器
461
- * 进到 textbox 之后读到的仍然是一个没有可访问名称、只报 `aria-readonly` 的多行文本框。
462
- * 属性字典由核心包计算,三端共用同一份实现。
463
- */
464
- #syncContentAttributes() {
465
- this._dispatch_effects(this.#contentAttributesEffect(this.#effectiveDisabled()));
466
- }
467
- #contentAttributesEffect(disabled) {
468
- const attributes = buildCodeEditorContentAttributes({
469
- describedBy: this.describedBy(),
470
- disabled,
471
- label: this.label(),
472
- labelledBy: this.labelledBy()
473
- });
474
- return this.#a11yConf.reconfigure(EditorView.contentAttributes.of(attributes));
475
- }
476
- #syncAccessState() {
477
- const disabled = this.#effectiveDisabled();
478
- const readonly = this.readonly();
479
- // CEA-004:三个 Compartment 必须在**同一次** dispatch 里重配。早先是
480
- // readonly → editable → a11y 三次派发,中间两拍是「只读但仍标记可编辑」
481
- // 这种自相矛盾的状态,装了 updateListener 的插件全都看得见,还各自重算一遍。
482
- //
483
- // 禁用语义也要写给辅助技术:只配 readonly 的话 CodeMirror 只写出 `aria-readonly`,
484
- // 屏幕阅读器会把禁用控件读成「可编辑但当前只读」,用户以为解锁后就能输入。
485
- this._dispatch_effects([
486
- this.#readonlyConf.reconfigure(EditorState.readOnly.of(readonly || disabled)),
487
- this.#editableConf.reconfigure(EditorView.editable.of(!readonly && !disabled)),
488
- this.#contentAttributesEffect(disabled)
489
- ]);
490
- }
491
- /** 当前 `setup` 预设对应的扩展;`null` 预设为空数组。 */
492
- #setupExtension() {
493
- const setup = this.setup();
494
- if (setup === 'basic')
495
- return basicSetup;
496
- if (setup === 'minimal')
497
- return minimalSetup;
498
- return [];
499
- }
500
- /**
501
- * 把 `ControlValueAccessor` 收到的任意值规整成文档内容,见 {@link CodeEditor.writeValue}。
502
- *
503
- * @throws TypeError 值既不是字符串也不是 `null` / `undefined`
504
- */
505
- #normalizeFormValue(value) {
506
- if (value === null || value === undefined)
507
- return '';
508
- if (typeof value === 'string')
509
- return value;
510
- const actual = typeof value === 'object' ? (value.constructor?.name ?? 'object') : typeof value;
511
- throw new TypeError(`[@aiao/code-editor-angular] writeValue() 只接受 string | null | undefined,实际收到 ${actual}。` +
512
- '请检查绑定的 FormControl 初始值或 ngModel 表达式。');
513
- }
514
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CodeEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
515
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.2", type: CodeEditor, isStandalone: true, selector: "ao-code-editor", inputs: { autoFocus: { classPropertyName: "autoFocus", publicName: "autoFocus", isSignal: true, isRequired: false, transformFunction: null }, describedBy: { classPropertyName: "describedBy", publicName: "describedBy", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, highlightWhitespace: { classPropertyName: "highlightWhitespace", publicName: "highlightWhitespace", isSignal: true, isRequired: false, transformFunction: null }, indentUnit: { classPropertyName: "indentUnit", publicName: "indentUnit", isSignal: true, isRequired: false, transformFunction: null }, indentWithTab: { classPropertyName: "indentWithTab", publicName: "indentWithTab", isSignal: true, isRequired: false, transformFunction: null }, language: { classPropertyName: "language", publicName: "language", isSignal: true, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, labelledBy: { classPropertyName: "labelledBy", publicName: "labelledBy", isSignal: true, isRequired: false, transformFunction: null }, lineWrapping: { classPropertyName: "lineWrapping", publicName: "lineWrapping", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: false, transformFunction: null }, setup: { classPropertyName: "setup", publicName: "setup", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { aoFocus: "aoFocus", aoChange: "aoChange", aoBlur: "aoBlur", aoLanguageError: "aoLanguageError" }, host: { properties: { "attr.aria-disabled": "_ariaDisabled()" } }, providers: [
516
- {
517
- provide: NG_VALUE_ACCESSOR,
518
- useExisting: forwardRef(() => CodeEditor),
519
- multi: true
520
- }
521
- ], usesOnChanges: true, ngImport: i0, template: ``, isInline: true, styles: [":host{display:block;height:100%}:host ::ng-deep .cm-editor{height:100%}:host ::ng-deep .cm-editor.cm-focused{outline:2px solid Highlight;outline-offset:-2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
522
- }
523
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CodeEditor, decorators: [{
524
- type: Component,
525
- args: [{ selector: 'ao-code-editor', template: ``, changeDetection: ChangeDetectionStrategy.OnPush, host: {
526
- '[attr.aria-disabled]': '_ariaDisabled()'
527
- }, providers: [
528
- {
529
- provide: NG_VALUE_ACCESSOR,
530
- useExisting: forwardRef(() => CodeEditor),
531
- multi: true
532
- }
533
- ], styles: [":host{display:block;height:100%}:host ::ng-deep .cm-editor{height:100%}:host ::ng-deep .cm-editor.cm-focused{outline:2px solid Highlight;outline-offset:-2px}\n"] }]
534
- }], propDecorators: { autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], describedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "describedBy", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], highlightWhitespace: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightWhitespace", required: false }] }], indentUnit: [{ type: i0.Input, args: [{ isSignal: true, alias: "indentUnit", required: false }] }], indentWithTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "indentWithTab", required: false }] }], language: [{ type: i0.Input, args: [{ isSignal: true, alias: "language", required: false }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelledBy", required: false }] }], lineWrapping: [{ type: i0.Input, args: [{ isSignal: true, alias: "lineWrapping", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: false }] }], setup: [{ type: i0.Input, args: [{ isSignal: true, alias: "setup", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], aoFocus: [{ type: i0.Output, args: ["aoFocus"] }], aoChange: [{ type: i0.Output, args: ["aoChange"] }], aoBlur: [{ type: i0.Output, args: ["aoBlur"] }], aoLanguageError: [{ type: i0.Output, args: ["aoLanguageError"] }] } });
535
-
536
- /**
537
- * @fileoverview `@aiao/code-editor-angular` 包入口。
538
- *
539
- * 导出 Angular `CodeEditor` 组件、`CodeEditorSetup` 与 `CodeEditorTheme` 类型。
540
- * 与 React / Vue 绑定同名同语义,三端共用 `@aiao/code-editor` 的语言解析与无障碍工具。
541
- *
542
- * @example
543
- * ```ts
544
- * import { CodeEditor } from '@aiao/code-editor-angular';
545
- * ```
546
- *
547
- * @module @aiao/code-editor-angular
548
- */
549
-
550
- /**
551
- * Generated bundle index. Do not edit.
552
- */
553
-
13
+ var CodeEditor = class CodeEditor {
14
+ #view;
15
+ #pendingValue;
16
+ #languageRequest = 0;
17
+ #appliedLanguage;
18
+ #elementRef = inject(ElementRef);
19
+ #platformId = inject(PLATFORM_ID);
20
+ #isBrowser = isPlatformBrowser(this.#platformId);
21
+ #editableConf = new Compartment();
22
+ #readonlyConf = new Compartment();
23
+ #themeConf = new Compartment();
24
+ #placeholderConf = new Compartment();
25
+ #indentWithTabConf = new Compartment();
26
+ #indentUnitConf = new Compartment();
27
+ #lineWrappingConf = new Compartment();
28
+ #highlightWhitespaceConf = new Compartment();
29
+ #languageConf = new Compartment();
30
+ #a11yConf = new Compartment();
31
+ #setupConf = new Compartment();
32
+ #userExtensionsConf = new Compartment();
33
+ #updateListener = EditorView.updateListener.of((vu) => {
34
+ if (vu.docChanged && !vu.transactions.some((tr) => tr.annotation(External))) {
35
+ const value = vu.state.doc.toString();
36
+ this.#onChange?.(value);
37
+ this.aoChange.emit(value);
38
+ }
39
+ });
40
+ #domEventObservers = EditorView.domEventObservers({
41
+ focus: () => {
42
+ this.aoFocus.emit();
43
+ },
44
+ blur: () => {
45
+ this.#onTouched?.();
46
+ this.aoBlur.emit();
47
+ }
48
+ });
49
+ #formDisabled = signal(false, ...ngDevMode ? [{ debugName: "#formDisabled" }] : /* istanbul ignore next */ []);
50
+ #effectiveDisabled = computed(() => this.disabled() || this.#formDisabled(), ...ngDevMode ? [{ debugName: "#effectiveDisabled" }] : /* istanbul ignore next */ []);
51
+ #onChange;
52
+ #onTouched;
53
+ #formOwnsValue = false;
54
+ _ariaDisabled = computed(() => this.#effectiveDisabled() ? "true" : null, ...ngDevMode ? [{ debugName: "_ariaDisabled" }] : /* istanbul ignore next */ []);
55
+ autoFocus = input(false, {
56
+ ...ngDevMode ? { debugName: "autoFocus" } : /* istanbul ignore next */ {},
57
+ transform: booleanAttribute
58
+ });
59
+ describedBy = input("", ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
60
+ disabled = input(false, {
61
+ ...ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {},
62
+ transform: booleanAttribute
63
+ });
64
+ highlightWhitespace = input(false, {
65
+ ...ngDevMode ? { debugName: "highlightWhitespace" } : /* istanbul ignore next */ {},
66
+ transform: booleanAttribute
67
+ });
68
+ indentUnit = input(" ", ...ngDevMode ? [{ debugName: "indentUnit" }] : /* istanbul ignore next */ []);
69
+ indentWithTab = input(false, {
70
+ ...ngDevMode ? { debugName: "indentWithTab" } : /* istanbul ignore next */ {},
71
+ transform: booleanAttribute
72
+ });
73
+ language = input("sql", ...ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []);
74
+ languages = input(SUPPORT_LANGUAGES, ...ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []);
75
+ label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
76
+ labelledBy = input("", ...ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []);
77
+ lineWrapping = input(false, {
78
+ ...ngDevMode ? { debugName: "lineWrapping" } : /* istanbul ignore next */ {},
79
+ transform: booleanAttribute
80
+ });
81
+ placeholder = input("", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
82
+ readonly = input(false, {
83
+ ...ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {},
84
+ transform: booleanAttribute
85
+ });
86
+ root = input(...ngDevMode ? [void 0, { debugName: "root" }] : /* istanbul ignore next */ []);
87
+ setup = input("basic", ...ngDevMode ? [{ debugName: "setup" }] : /* istanbul ignore next */ []);
88
+ theme = input("light", ...ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []);
89
+ value = input("", ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
90
+ aoFocus = output();
91
+ aoChange = output();
92
+ aoBlur = output();
93
+ aoLanguageError = output();
94
+ get view() {
95
+ return this.#view ?? null;
96
+ }
97
+ get host() {
98
+ return this.#view ? this.#elementRef.nativeElement : null;
99
+ }
100
+ focus() {
101
+ this.#view?.focus();
102
+ }
103
+ blur() {
104
+ this.#view?.contentDOM.blur();
105
+ }
106
+ ngOnInit() {
107
+ if (!this.#isBrowser) return;
108
+ const extensions = this._get_all_extensions();
109
+ const state = EditorState.create({
110
+ doc: this.#pendingValue ?? this.value(),
111
+ extensions
112
+ });
113
+ this.#view = new EditorView({
114
+ parent: this.#elementRef.nativeElement,
115
+ root: this.root(),
116
+ state
117
+ });
118
+ this.#syncAccessState();
119
+ if (this.autoFocus() && shouldAutoFocusCodeEditor({
120
+ disabled: this.#effectiveDisabled(),
121
+ readonly: this.readonly()
122
+ })) this.#view.focus();
123
+ this._dispatch_effects(this.#themeConf.reconfigure(this.theme() === "dark" ? oneDark : []));
124
+ this._dispatch_effects(this.#placeholderConf.reconfigure(this.placeholder() ? placeholder(this.placeholder()) : []));
125
+ this._dispatch_effects(this.#indentWithTabConf.reconfigure(this.indentWithTab() ? keymap.of([indentWithTab]) : []));
126
+ this._dispatch_effects(this.#indentUnitConf.reconfigure(this.indentUnit() ? indentUnit.of(this.indentUnit()) : []));
127
+ this._dispatch_effects(this.#lineWrappingConf.reconfigure(this.lineWrapping() ? EditorView.lineWrapping : []));
128
+ this._dispatch_effects(this.#highlightWhitespaceConf.reconfigure(this.highlightWhitespace() ? highlightWhitespace() : []));
129
+ this.setLanguage(this.language());
130
+ }
131
+ ngOnChanges(changes) {
132
+ if (!this.#view) return;
133
+ if (changes["value"] && this.#view && !this.#formOwnsValue) {
134
+ const newValue = this.value();
135
+ const change = computeMinimalDocumentChange(this.#view.state.doc.toString(), newValue);
136
+ if (change) this.#view.dispatch({
137
+ changes: change,
138
+ annotations: [External.of(true), Transaction.addToHistory.of(false)],
139
+ scrollIntoView: false
140
+ });
141
+ }
142
+ if (changes["disabled"] || changes["readonly"]) this.#syncAccessState();
143
+ if (changes["describedBy"] || changes["label"] || changes["labelledBy"]) this.#syncContentAttributes();
144
+ if (changes["theme"]) this._dispatch_effects(this.#themeConf.reconfigure(this.theme() === "dark" ? oneDark : []));
145
+ if (changes["placeholder"]) this._dispatch_effects(this.#placeholderConf.reconfigure(this.placeholder() ? placeholder(this.placeholder()) : []));
146
+ if (changes["indentWithTab"]) this._dispatch_effects(this.#indentWithTabConf.reconfigure(this.indentWithTab() ? keymap.of([indentWithTab]) : []));
147
+ if (changes["indentUnit"]) this._dispatch_effects(this.#indentUnitConf.reconfigure(this.indentUnit() ? indentUnit.of(this.indentUnit()) : []));
148
+ if (changes["lineWrapping"]) this._dispatch_effects(this.#lineWrappingConf.reconfigure(this.lineWrapping() ? EditorView.lineWrapping : []));
149
+ if (changes["highlightWhitespace"]) this._dispatch_effects(this.#highlightWhitespaceConf.reconfigure(this.highlightWhitespace() ? highlightWhitespace() : []));
150
+ if (changes["language"] || changes["languages"]) this.#syncLanguage();
151
+ if (changes["setup"]) this._dispatch_effects(this.#setupConf.reconfigure(this.#setupExtension()));
152
+ }
153
+ ngOnDestroy() {
154
+ this.#languageRequest += 1;
155
+ const view = this.#view;
156
+ this.#view = void 0;
157
+ view?.destroy();
158
+ }
159
+ writeValue(value) {
160
+ const normalized = this.#normalizeFormValue(value);
161
+ this.#formOwnsValue = true;
162
+ if (!this.#view) {
163
+ this.#pendingValue = normalized;
164
+ return;
165
+ }
166
+ const change = computeMinimalDocumentChange(this.#view.state.doc.toString(), normalized);
167
+ if (change) this.#view.dispatch({
168
+ changes: change,
169
+ annotations: [External.of(true), Transaction.addToHistory.of(false)],
170
+ scrollIntoView: false
171
+ });
172
+ }
173
+ registerOnChange(fn) {
174
+ this.#onChange = fn;
175
+ }
176
+ registerOnTouched(fn) {
177
+ this.#onTouched = fn;
178
+ }
179
+ setDisabledState(isDisabled) {
180
+ this.#formDisabled.set(isDisabled);
181
+ this.#syncAccessState();
182
+ }
183
+ setExtensions(value) {
184
+ this._dispatch_effects(this.#userExtensionsConf.reconfigure(value));
185
+ }
186
+ setLanguage(lang) {
187
+ const request = ++this.#languageRequest;
188
+ const view = this.#view;
189
+ if (!view) return;
190
+ const resolved = resolveCodeEditorLanguage(lang, this.languages());
191
+ this.#appliedLanguage = resolved;
192
+ if (resolved.kind === "none") {
193
+ view.dispatch({ effects: this.#languageConf.reconfigure([]) });
194
+ return;
195
+ }
196
+ if (resolved.kind === "not-found") {
197
+ view.dispatch({ effects: this.#languageConf.reconfigure([]) });
198
+ console.error(`[CodeEditor] Language '${lang}' not found.`);
199
+ this.aoLanguageError.emit(codeEditorLanguageNotFound(lang));
200
+ return;
201
+ }
202
+ let loadPromise;
203
+ try {
204
+ loadPromise = Promise.resolve(resolved.description.load());
205
+ } catch (error) {
206
+ loadPromise = Promise.reject(error);
207
+ }
208
+ loadPromise.then((languageSupport) => {
209
+ if (request !== this.#languageRequest || this.#view !== view) return;
210
+ view.dispatch({ effects: this.#languageConf.reconfigure(languageSupport.extension) });
211
+ }, (error) => {
212
+ if (request !== this.#languageRequest || this.#view !== view) return;
213
+ view.dispatch({ effects: this.#languageConf.reconfigure([]) });
214
+ console.error(`[CodeEditor] Failed to load language '${lang}':`, error);
215
+ this.aoLanguageError.emit(codeEditorLanguageLoadFailed(lang, error));
216
+ });
217
+ }
218
+ _get_all_extensions() {
219
+ return [
220
+ this.#updateListener,
221
+ this.#domEventObservers,
222
+ this.#editableConf.of([]),
223
+ this.#readonlyConf.of([]),
224
+ this.#themeConf.of([]),
225
+ this.#placeholderConf.of([]),
226
+ this.#indentWithTabConf.of([]),
227
+ this.#indentUnitConf.of([]),
228
+ this.#lineWrappingConf.of([]),
229
+ this.#highlightWhitespaceConf.of([]),
230
+ this.#languageConf.of([]),
231
+ this.#a11yConf.of([]),
232
+ this.#setupConf.of(this.#setupExtension()),
233
+ this.#userExtensionsConf.of([])
234
+ ];
235
+ }
236
+ _dispatch_effects(effects) {
237
+ return this.#view?.dispatch({ effects });
238
+ }
239
+ #syncLanguage() {
240
+ const language = this.language();
241
+ const resolved = resolveCodeEditorLanguage(language, this.languages());
242
+ if (isSameResolvedLanguage(this.#appliedLanguage, resolved)) return;
243
+ this.setLanguage(language);
244
+ }
245
+ #syncContentAttributes() {
246
+ this._dispatch_effects(this.#contentAttributesEffect(this.#effectiveDisabled()));
247
+ }
248
+ #contentAttributesEffect(disabled) {
249
+ const attributes = buildCodeEditorContentAttributes({
250
+ describedBy: this.describedBy(),
251
+ disabled,
252
+ label: this.label(),
253
+ labelledBy: this.labelledBy()
254
+ });
255
+ return this.#a11yConf.reconfigure(EditorView.contentAttributes.of(attributes));
256
+ }
257
+ #syncAccessState() {
258
+ const disabled = this.#effectiveDisabled();
259
+ const readonly = this.readonly();
260
+ this._dispatch_effects([
261
+ this.#readonlyConf.reconfigure(EditorState.readOnly.of(readonly || disabled)),
262
+ this.#editableConf.reconfigure(EditorView.editable.of(!readonly && !disabled)),
263
+ this.#contentAttributesEffect(disabled)
264
+ ]);
265
+ }
266
+ #setupExtension() {
267
+ const setup = this.setup();
268
+ if (setup === "basic") return basicSetup;
269
+ if (setup === "minimal") return minimalSetup;
270
+ return [];
271
+ }
272
+ #normalizeFormValue(value) {
273
+ if (value === null || value === void 0) return "";
274
+ if (typeof value === "string") return value;
275
+ const actual = typeof value === "object" ? value.constructor?.name ?? "object" : typeof value;
276
+ throw new TypeError(`[@aiao/code-editor-angular] writeValue() 只接受 string | null | undefined,实际收到 ${actual}。请检查绑定的 FormControl 初始值或 ngModel 表达式。`);
277
+ }
278
+ static ɵfac = i0.ɵɵngDeclareFactory({
279
+ minVersion: "12.0.0",
280
+ version: "22.2.3",
281
+ ngImport: i0,
282
+ type: CodeEditor,
283
+ deps: [],
284
+ target: i0.ɵɵFactoryTarget.Component
285
+ });
286
+ static ɵcmp = i0.ɵɵngDeclareComponent({
287
+ minVersion: "17.1.0",
288
+ version: "22.2.3",
289
+ type: CodeEditor,
290
+ isStandalone: true,
291
+ selector: "ao-code-editor",
292
+ inputs: {
293
+ autoFocus: {
294
+ classPropertyName: "autoFocus",
295
+ publicName: "autoFocus",
296
+ isSignal: true,
297
+ isRequired: false,
298
+ transformFunction: null
299
+ },
300
+ describedBy: {
301
+ classPropertyName: "describedBy",
302
+ publicName: "describedBy",
303
+ isSignal: true,
304
+ isRequired: false,
305
+ transformFunction: null
306
+ },
307
+ disabled: {
308
+ classPropertyName: "disabled",
309
+ publicName: "disabled",
310
+ isSignal: true,
311
+ isRequired: false,
312
+ transformFunction: null
313
+ },
314
+ highlightWhitespace: {
315
+ classPropertyName: "highlightWhitespace",
316
+ publicName: "highlightWhitespace",
317
+ isSignal: true,
318
+ isRequired: false,
319
+ transformFunction: null
320
+ },
321
+ indentUnit: {
322
+ classPropertyName: "indentUnit",
323
+ publicName: "indentUnit",
324
+ isSignal: true,
325
+ isRequired: false,
326
+ transformFunction: null
327
+ },
328
+ indentWithTab: {
329
+ classPropertyName: "indentWithTab",
330
+ publicName: "indentWithTab",
331
+ isSignal: true,
332
+ isRequired: false,
333
+ transformFunction: null
334
+ },
335
+ language: {
336
+ classPropertyName: "language",
337
+ publicName: "language",
338
+ isSignal: true,
339
+ isRequired: false,
340
+ transformFunction: null
341
+ },
342
+ languages: {
343
+ classPropertyName: "languages",
344
+ publicName: "languages",
345
+ isSignal: true,
346
+ isRequired: false,
347
+ transformFunction: null
348
+ },
349
+ label: {
350
+ classPropertyName: "label",
351
+ publicName: "label",
352
+ isSignal: true,
353
+ isRequired: false,
354
+ transformFunction: null
355
+ },
356
+ labelledBy: {
357
+ classPropertyName: "labelledBy",
358
+ publicName: "labelledBy",
359
+ isSignal: true,
360
+ isRequired: false,
361
+ transformFunction: null
362
+ },
363
+ lineWrapping: {
364
+ classPropertyName: "lineWrapping",
365
+ publicName: "lineWrapping",
366
+ isSignal: true,
367
+ isRequired: false,
368
+ transformFunction: null
369
+ },
370
+ placeholder: {
371
+ classPropertyName: "placeholder",
372
+ publicName: "placeholder",
373
+ isSignal: true,
374
+ isRequired: false,
375
+ transformFunction: null
376
+ },
377
+ readonly: {
378
+ classPropertyName: "readonly",
379
+ publicName: "readonly",
380
+ isSignal: true,
381
+ isRequired: false,
382
+ transformFunction: null
383
+ },
384
+ root: {
385
+ classPropertyName: "root",
386
+ publicName: "root",
387
+ isSignal: true,
388
+ isRequired: false,
389
+ transformFunction: null
390
+ },
391
+ setup: {
392
+ classPropertyName: "setup",
393
+ publicName: "setup",
394
+ isSignal: true,
395
+ isRequired: false,
396
+ transformFunction: null
397
+ },
398
+ theme: {
399
+ classPropertyName: "theme",
400
+ publicName: "theme",
401
+ isSignal: true,
402
+ isRequired: false,
403
+ transformFunction: null
404
+ },
405
+ value: {
406
+ classPropertyName: "value",
407
+ publicName: "value",
408
+ isSignal: true,
409
+ isRequired: false,
410
+ transformFunction: null
411
+ }
412
+ },
413
+ outputs: {
414
+ aoFocus: "aoFocus",
415
+ aoChange: "aoChange",
416
+ aoBlur: "aoBlur",
417
+ aoLanguageError: "aoLanguageError"
418
+ },
419
+ host: { properties: { "attr.aria-disabled": "_ariaDisabled()" } },
420
+ providers: [{
421
+ provide: NG_VALUE_ACCESSOR,
422
+ useExisting: forwardRef(() => CodeEditor),
423
+ multi: true
424
+ }],
425
+ usesOnChanges: true,
426
+ ngImport: i0,
427
+ template: ``,
428
+ isInline: true,
429
+ styles: [":host{display:block;height:100%}:host ::ng-deep .cm-editor{height:100%}:host ::ng-deep .cm-editor.cm-focused{outline:2px solid Highlight;outline-offset:-2px}\n"],
430
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
431
+ });
432
+ };
433
+ i0.ɵɵngDeclareClassMetadata({
434
+ minVersion: "12.0.0",
435
+ version: "22.2.3",
436
+ ngImport: i0,
437
+ type: CodeEditor,
438
+ decorators: [{
439
+ type: Component,
440
+ args: [{
441
+ selector: "ao-code-editor",
442
+ template: ``,
443
+ changeDetection: ChangeDetectionStrategy.OnPush,
444
+ host: { "[attr.aria-disabled]": "_ariaDisabled()" },
445
+ providers: [{
446
+ provide: NG_VALUE_ACCESSOR,
447
+ useExisting: forwardRef(() => CodeEditor),
448
+ multi: true
449
+ }],
450
+ styles: [":host{display:block;height:100%}:host ::ng-deep .cm-editor{height:100%}:host ::ng-deep .cm-editor.cm-focused{outline:2px solid Highlight;outline-offset:-2px}\n"]
451
+ }]
452
+ }],
453
+ propDecorators: {
454
+ autoFocus: [{
455
+ type: i0.Input,
456
+ args: [{
457
+ isSignal: true,
458
+ alias: "autoFocus",
459
+ required: false
460
+ }]
461
+ }],
462
+ describedBy: [{
463
+ type: i0.Input,
464
+ args: [{
465
+ isSignal: true,
466
+ alias: "describedBy",
467
+ required: false
468
+ }]
469
+ }],
470
+ disabled: [{
471
+ type: i0.Input,
472
+ args: [{
473
+ isSignal: true,
474
+ alias: "disabled",
475
+ required: false
476
+ }]
477
+ }],
478
+ highlightWhitespace: [{
479
+ type: i0.Input,
480
+ args: [{
481
+ isSignal: true,
482
+ alias: "highlightWhitespace",
483
+ required: false
484
+ }]
485
+ }],
486
+ indentUnit: [{
487
+ type: i0.Input,
488
+ args: [{
489
+ isSignal: true,
490
+ alias: "indentUnit",
491
+ required: false
492
+ }]
493
+ }],
494
+ indentWithTab: [{
495
+ type: i0.Input,
496
+ args: [{
497
+ isSignal: true,
498
+ alias: "indentWithTab",
499
+ required: false
500
+ }]
501
+ }],
502
+ language: [{
503
+ type: i0.Input,
504
+ args: [{
505
+ isSignal: true,
506
+ alias: "language",
507
+ required: false
508
+ }]
509
+ }],
510
+ languages: [{
511
+ type: i0.Input,
512
+ args: [{
513
+ isSignal: true,
514
+ alias: "languages",
515
+ required: false
516
+ }]
517
+ }],
518
+ label: [{
519
+ type: i0.Input,
520
+ args: [{
521
+ isSignal: true,
522
+ alias: "label",
523
+ required: false
524
+ }]
525
+ }],
526
+ labelledBy: [{
527
+ type: i0.Input,
528
+ args: [{
529
+ isSignal: true,
530
+ alias: "labelledBy",
531
+ required: false
532
+ }]
533
+ }],
534
+ lineWrapping: [{
535
+ type: i0.Input,
536
+ args: [{
537
+ isSignal: true,
538
+ alias: "lineWrapping",
539
+ required: false
540
+ }]
541
+ }],
542
+ placeholder: [{
543
+ type: i0.Input,
544
+ args: [{
545
+ isSignal: true,
546
+ alias: "placeholder",
547
+ required: false
548
+ }]
549
+ }],
550
+ readonly: [{
551
+ type: i0.Input,
552
+ args: [{
553
+ isSignal: true,
554
+ alias: "readonly",
555
+ required: false
556
+ }]
557
+ }],
558
+ root: [{
559
+ type: i0.Input,
560
+ args: [{
561
+ isSignal: true,
562
+ alias: "root",
563
+ required: false
564
+ }]
565
+ }],
566
+ setup: [{
567
+ type: i0.Input,
568
+ args: [{
569
+ isSignal: true,
570
+ alias: "setup",
571
+ required: false
572
+ }]
573
+ }],
574
+ theme: [{
575
+ type: i0.Input,
576
+ args: [{
577
+ isSignal: true,
578
+ alias: "theme",
579
+ required: false
580
+ }]
581
+ }],
582
+ value: [{
583
+ type: i0.Input,
584
+ args: [{
585
+ isSignal: true,
586
+ alias: "value",
587
+ required: false
588
+ }]
589
+ }],
590
+ aoFocus: [{
591
+ type: i0.Output,
592
+ args: ["aoFocus"]
593
+ }],
594
+ aoChange: [{
595
+ type: i0.Output,
596
+ args: ["aoChange"]
597
+ }],
598
+ aoBlur: [{
599
+ type: i0.Output,
600
+ args: ["aoBlur"]
601
+ }],
602
+ aoLanguageError: [{
603
+ type: i0.Output,
604
+ args: ["aoLanguageError"]
605
+ }]
606
+ }
607
+ });
554
608
  export { CodeEditor };
555
- //# sourceMappingURL=aiao-code-editor-angular.mjs.map
609
+
610
+ //# sourceMappingURL=aiao-code-editor-angular.mjs.map