Skip to content

Commit 874d144

Browse files
committed
refactor: add code-editor
1 parent e1fd437 commit 874d144

File tree

6 files changed

+355
-342
lines changed

6 files changed

+355
-342
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"@angular/platform-browser": "^17.3.0",
3232
"@angular/platform-browser-dynamic": "^17.3.0",
3333
"@angular/router": "^17.3.0",
34+
"@codemirror/theme-one-dark": "^6.1.2",
35+
"codemirror": "^6.0.1",
3436
"lodash-es": "^4.17.21",
3537
"rxjs": "~7.8.0",
3638
"tslib": "^2.3.0",
Lines changed: 178 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,184 @@
1-
import { Component } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
ElementRef,
5+
EventEmitter,
6+
Input,
7+
OnChanges,
8+
OnDestroy,
9+
OnInit,
10+
Output,
11+
SimpleChanges,
12+
forwardRef,
13+
} from '@angular/core';
14+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
15+
16+
import { Annotation, EditorState, Extension, StateEffect } from '@codemirror/state';
17+
import { oneDark } from '@codemirror/theme-one-dark';
18+
import { placeholder } from '@codemirror/view';
19+
import { EditorView, basicSetup } from 'codemirror';
20+
21+
const External = Annotation.define<boolean>();
22+
23+
export type Theme = 'light' | 'dark' | Extension;
224

325
@Component({
426
selector: 'code-editor',
527
standalone: true,
628
imports: [],
7-
template: `<p>code-editor works!</p>`,
8-
styles: ``,
29+
template: ``,
30+
changeDetection: ChangeDetectionStrategy.OnPush,
31+
providers: [
32+
{
33+
provide: NG_VALUE_ACCESSOR,
34+
useExisting: forwardRef(() => CodeEditor),
35+
multi: true,
36+
},
37+
],
938
})
10-
export class CodeEditor {}
39+
export class CodeEditor implements OnChanges, OnInit, OnDestroy, ControlValueAccessor {
40+
@Input() root?: Document | ShadowRoot;
41+
42+
/** Editor's value. */
43+
@Input()
44+
get value() {
45+
return this._value;
46+
}
47+
set value(newValue: string) {
48+
this._value = newValue;
49+
this.setValue(newValue);
50+
}
51+
_value = '';
52+
53+
/** Editor's theme. */
54+
@Input() theme: Theme = 'light';
55+
56+
/** Editor's placecholder. */
57+
@Input() placeholder = '';
58+
59+
/** Whether the editor is disabled. */
60+
@Input() disabled = false;
61+
62+
/** Whether the editor is readonly. */
63+
@Input() readonly = false;
64+
65+
/** Whether focus on the editor when init. */
66+
@Input() autoFocus = false;
67+
68+
/**
69+
* EditorState's [extensions](https://codemirror.net/docs/ref/#state.EditorStateConfig.extensions).
70+
*/
71+
@Input() extensions: Extension[] = [basicSetup];
72+
73+
/** Event emitted when the editor's value changes. */
74+
@Output() change = new EventEmitter<string>();
75+
76+
/** Event emitted when focus on the editor. */
77+
@Output() focus = new EventEmitter<void>();
78+
79+
/** Event emitted when the editor has lost focus. */
80+
@Output() blur = new EventEmitter<void>();
81+
82+
view?: EditorView | null;
83+
84+
private _onChange: (value: string) => void = () => {};
85+
private _onTouched: () => void = () => {};
86+
87+
constructor(private _elementRef: ElementRef<Element>) {}
88+
89+
/** Register a function to be called every time the view updates. */
90+
private _updateListener = EditorView.updateListener.of(vu => {
91+
if (vu.docChanged && !vu.transactions.some(tr => tr.annotation(External))) {
92+
const doc = vu.state.doc;
93+
const value = doc.toString();
94+
this._onChange(value);
95+
this.change.emit(value);
96+
}
97+
});
98+
99+
/** Get the extensions of the editor. */
100+
getExtensions(): Extension[] {
101+
const basicExtensions = [
102+
this._updateListener,
103+
EditorView.editable.of(!this.disabled),
104+
EditorState.readOnly.of(this.readonly),
105+
placeholder(this.placeholder),
106+
];
107+
108+
if (this.theme == 'light') {
109+
// nothing to do
110+
} else if (this.theme == 'dark') {
111+
basicExtensions.push(oneDark);
112+
} else {
113+
basicExtensions.push(this.theme);
114+
}
115+
116+
return basicExtensions.concat(this.extensions);
117+
}
118+
119+
/** Reconfigure the root extensions of the editor. */
120+
reconfigure() {
121+
this.view?.dispatch({
122+
effects: StateEffect.reconfigure.of(this.getExtensions()),
123+
});
124+
}
125+
126+
/** Sets the editor's value. */
127+
setValue(value: string) {
128+
this.view?.dispatch({
129+
changes: { from: 0, to: this.view.state.doc.length, insert: value },
130+
});
131+
}
132+
133+
ngOnChanges(changes: SimpleChanges): void {
134+
if (this.view) {
135+
this.reconfigure();
136+
}
137+
}
138+
139+
ngOnInit(): void {
140+
this.view = new EditorView({
141+
root: this.root,
142+
parent: this._elementRef.nativeElement,
143+
state: EditorState.create({ doc: this.value, extensions: this.getExtensions() }),
144+
});
145+
146+
if (this.autoFocus) {
147+
this.view?.focus();
148+
}
149+
150+
this.view?.contentDOM.addEventListener('focus', () => {
151+
this._onTouched();
152+
this.focus.emit();
153+
});
154+
155+
this.view?.contentDOM.addEventListener('blur', () => {
156+
this._onTouched();
157+
this.blur.emit();
158+
});
159+
}
160+
161+
ngOnDestroy(): void {
162+
this.view?.destroy();
163+
this.view = null;
164+
}
165+
166+
writeValue(value: string): void {
167+
if (this.view) {
168+
this.setValue(value);
169+
}
170+
}
171+
172+
registerOnChange(fn: (value: string) => void) {
173+
this._onChange = fn;
174+
}
175+
176+
registerOnTouched(fn: () => void) {
177+
this._onTouched = fn;
178+
}
179+
180+
setDisabledState(isDisabled: boolean) {
181+
this.disabled = isDisabled;
182+
this.reconfigure();
183+
}
184+
}

projects/code-editor/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './public-api';

0 commit comments

Comments
 (0)