@acrodata/code-editor
Version:
CodeMirror 6 wrapper for Angular
594 lines (587 loc) • 26.2 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, booleanAttribute, forwardRef, Component, ViewEncapsulation, ChangeDetectionStrategy, Input, Output, NgModule } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { indentWithTab } from '@codemirror/commands';
import { indentUnit } from '@codemirror/language';
import { Annotation, Compartment, EditorState, StateEffect } from '@codemirror/state';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, placeholder, keymap, highlightWhitespace } from '@codemirror/view';
import { basicSetup, minimalSetup } from 'codemirror';
import { MergeView } from '@codemirror/merge';
const External = Annotation.define();
class CodeEditor {
constructor(_elementRef) {
this._elementRef = _elementRef;
/**
* Whether focus on the editor after init.
*
* Don't support change dynamically!
*/
this.autoFocus = false;
/** The editor's value. */
this.value = '';
/** Whether the editor is disabled. */
this.disabled = false;
/** Whether the editor is readonly. */
this.readonly = false;
/** The editor's theme. */
this.theme = 'light';
/** The editor's placecholder. */
this.placeholder = '';
/** Whether indent with Tab key. */
this.indentWithTab = false;
/** Should be a string consisting either entirely of the same whitespace character. */
this.indentUnit = '';
/** Whether the editor wraps lines. */
this.lineWrapping = false;
/** Whether highlight the whitespace. */
this.highlightWhitespace = false;
/**
* An array of language descriptions for known
* [language-data](https://github.com/codemirror/language-data/blob/main/src/language-data.ts).
*
* Don't support change dynamically!
*/
this.languages = [];
/** The editor's language. You should set the `languages` prop at first. */
this.language = '';
/**
* The editor's built-in setup. The value can be set to
* [`basic`](https://codemirror.net/docs/ref/#codemirror.basicSetup),
* [`minimal`](https://codemirror.net/docs/ref/#codemirror.minimalSetup) or `null`.
*/
this.setup = 'basic';
/**
* It will be appended to the root
* [extensions](https://codemirror.net/docs/ref/#state.EditorStateConfig.extensions).
*/
this.extensions = [];
/** Event emitted when the editor's value changes. */
this.change = new EventEmitter();
/** Event emitted when focus on the editor. */
this.focus = new EventEmitter();
/** Event emitted when the editor has lost focus. */
this.blur = new EventEmitter();
this._onChange = () => { };
this._onTouched = () => { };
this._updateListener = EditorView.updateListener.of(vu => {
if (vu.docChanged && !vu.transactions.some(tr => tr.annotation(External))) {
const value = vu.state.doc.toString();
this._onChange(value);
this.change.emit(value);
}
});
// Extension compartments can be used to make a configuration dynamic.
// https://codemirror.net/docs/ref/#state.Compartment
this._editableConf = new Compartment();
this._readonlyConf = new Compartment();
this._themeConf = new Compartment();
this._placeholderConf = new Compartment();
this._indentWithTabConf = new Compartment();
this._indentUnitConf = new Compartment();
this._lineWrappingConf = new Compartment();
this._highlightWhitespaceConf = new Compartment();
this._languageConf = new Compartment();
}
_getAllExtensions() {
return [
this._updateListener,
this._editableConf.of([]),
this._readonlyConf.of([]),
this._themeConf.of([]),
this._placeholderConf.of([]),
this._indentWithTabConf.of([]),
this._indentUnitConf.of([]),
this._lineWrappingConf.of([]),
this._highlightWhitespaceConf.of([]),
this._languageConf.of([]),
this.setup === 'basic' ? basicSetup : this.setup === 'minimal' ? minimalSetup : [],
...this.extensions,
];
}
ngOnChanges(changes) {
if (changes['value']) {
this.setValue(this.value);
}
if (changes['disabled']) {
this.setEditable(!this.disabled);
}
if (changes['readonly']) {
this.setReadonly(this.readonly);
}
if (changes['theme']) {
this.setTheme(this.theme);
}
if (changes['placeholder']) {
this.setPlaceholder(this.placeholder);
}
if (changes['indentWithTab']) {
this.setIndentWithTab(this.indentWithTab);
}
if (changes['indentUnit']) {
this.setIndentUnit(this.indentUnit);
}
if (changes['lineWrapping']) {
this.setLineWrapping(this.lineWrapping);
}
if (changes['highlightWhitespace']) {
this.setHighlightWhitespace(this.highlightWhitespace);
}
if (changes['language']) {
this.setLanguage(this.language);
}
if (changes['setup'] || changes['extensions']) {
this.setExtensions(this._getAllExtensions());
}
}
ngOnInit() {
this.view = new EditorView({
root: this.root,
parent: this._elementRef.nativeElement,
state: EditorState.create({ doc: this.value, extensions: this._getAllExtensions() }),
});
if (this.autoFocus) {
this.view?.focus();
}
this.view?.contentDOM.addEventListener('focus', () => {
this._onTouched();
this.focus.emit();
});
this.view?.contentDOM.addEventListener('blur', () => {
this._onTouched();
this.blur.emit();
});
this.setEditable(!this.disabled);
this.setReadonly(this.readonly);
this.setTheme(this.theme);
this.setPlaceholder(this.placeholder);
this.setIndentWithTab(this.indentWithTab);
this.setIndentUnit(this.indentUnit);
this.setLineWrapping(this.lineWrapping);
this.setHighlightWhitespace(this.highlightWhitespace);
this.setLanguage(this.language);
}
ngOnDestroy() {
this.view?.destroy();
}
writeValue(value) {
if (this.view) {
this.setValue(value);
}
}
registerOnChange(fn) {
this._onChange = fn;
}
registerOnTouched(fn) {
this._onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this.setEditable(!isDisabled);
}
/** Sets editor's value. */
setValue(value) {
this.view?.dispatch({
changes: { from: 0, to: this.view.state.doc.length, insert: value },
});
}
_dispatchEffects(effects) {
return this.view?.dispatch({ effects });
}
/** Sets the root extensions of the editor. */
setExtensions(value) {
this._dispatchEffects(StateEffect.reconfigure.of(value));
}
/** Sets editor's editable state. */
setEditable(value) {
this._dispatchEffects(this._editableConf.reconfigure(EditorView.editable.of(value)));
}
/** Sets editor's readonly state. */
setReadonly(value) {
this._dispatchEffects(this._readonlyConf.reconfigure(EditorState.readOnly.of(value)));
}
/** Sets editor's theme. */
setTheme(value) {
this._dispatchEffects(this._themeConf.reconfigure(value === 'light' ? [] : value === 'dark' ? oneDark : value));
}
/** Sets editor's placeholder. */
setPlaceholder(value) {
this._dispatchEffects(this._placeholderConf.reconfigure(value ? placeholder(value) : []));
}
/** Sets editor' indentWithTab. */
setIndentWithTab(value) {
this._dispatchEffects(this._indentWithTabConf.reconfigure(value ? keymap.of([indentWithTab]) : []));
}
/** Sets editor's indentUnit. */
setIndentUnit(value) {
this._dispatchEffects(this._indentUnitConf.reconfigure(value ? indentUnit.of(value) : []));
}
/** Sets editor's lineWrapping. */
setLineWrapping(value) {
this._dispatchEffects(this._lineWrappingConf.reconfigure(value ? EditorView.lineWrapping : []));
}
/** Sets editor's highlightWhitespace. */
setHighlightWhitespace(value) {
this._dispatchEffects(this._highlightWhitespaceConf.reconfigure(value ? highlightWhitespace() : []));
}
/** Sets editor's language dynamically. */
setLanguage(lang) {
if (!lang || lang == 'plaintext') {
this._dispatchEffects(this._languageConf.reconfigure([]));
return;
}
if (this.languages.length === 0) {
if (this.view) {
console.error('No supported languages. Please set the `languages` prop at first.');
}
return;
}
const langDesc = this._findLanguage(lang);
langDesc?.load().then(lang => {
this._dispatchEffects(this._languageConf.reconfigure([lang]));
});
}
/** Find the language's extension by its name. Case insensitive. */
_findLanguage(name) {
for (const lang of this.languages) {
for (const alias of [lang.name, ...lang.alias]) {
if (name.toLowerCase() === alias.toLowerCase()) {
return lang;
}
}
}
console.error('Language not found:', name);
console.info('Supported language names:', this.languages.map(lang => lang.name).join(', '));
return null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: CodeEditor, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "18.2.8", type: CodeEditor, isStandalone: true, selector: "code-editor", inputs: { root: "root", autoFocus: ["autoFocus", "autoFocus", booleanAttribute], value: "value", disabled: ["disabled", "disabled", booleanAttribute], readonly: ["readonly", "readonly", booleanAttribute], theme: "theme", placeholder: "placeholder", indentWithTab: ["indentWithTab", "indentWithTab", booleanAttribute], indentUnit: "indentUnit", lineWrapping: ["lineWrapping", "lineWrapping", booleanAttribute], highlightWhitespace: ["highlightWhitespace", "highlightWhitespace", booleanAttribute], languages: "languages", language: "language", setup: "setup", extensions: "extensions" }, outputs: { change: "change", focus: "focus", blur: "blur" }, host: { classAttribute: "code-editor" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CodeEditor),
multi: true,
},
], usesOnChanges: true, ngImport: i0, template: ``, isInline: true, styles: [".code-editor{display:block}.code-editor .cm-editor{height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: CodeEditor, decorators: [{
type: Component,
args: [{ selector: 'code-editor', standalone: true, template: ``, host: {
class: 'code-editor',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CodeEditor),
multi: true,
},
], styles: [".code-editor{display:block}.code-editor .cm-editor{height:100%}\n"] }]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { root: [{
type: Input
}], autoFocus: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], value: [{
type: Input
}], disabled: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], readonly: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], theme: [{
type: Input
}], placeholder: [{
type: Input
}], indentWithTab: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], indentUnit: [{
type: Input
}], lineWrapping: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], highlightWhitespace: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], languages: [{
type: Input
}], language: [{
type: Input
}], setup: [{
type: Input
}], extensions: [{
type: Input
}], change: [{
type: Output
}], focus: [{
type: Output
}], blur: [{
type: Output
}] } });
class DiffEditor {
constructor(_elementRef) {
this._elementRef = _elementRef;
/**
* The editor's built-in setup. The value can be set to
* [`basic`](https://codemirror.net/docs/ref/#codemirror.basicSetup),
* [`minimal`](https://codemirror.net/docs/ref/#codemirror.minimalSetup) or `null`.
*
* Don't support change dynamically!
*/
this.setup = 'basic';
/** The diff-editor's original value. */
this.originalValue = '';
/**
* The MergeView original config's
* [extensions](https://codemirror.net/docs/ref/#state.EditorStateConfig.extensions).
*
* Don't support change dynamically!
*/
this.originalExtensions = [];
/** The diff-editor's modified value. */
this.modifiedValue = '';
/**
* The MergeView modified config's
* [extensions](https://codemirror.net/docs/ref/#state.EditorStateConfig.extensions).
*
* Don't support change dynamically!
*/
this.modifiedExtensions = [];
/**
* By default, the merge view will mark inserted and deleted text
* in changed chunks. Set this to false to turn that off.
*/
this.highlightChanges = true;
/** Controls whether a gutter marker is shown next to changed lines. */
this.gutter = true;
/** Whether the diff-editor is disabled. */
this.disabled = false;
/** Event emitted when the editor's original value changes. */
this.originalValueChange = new EventEmitter();
/** Event emitted when focus on the original editor. */
this.originalFocus = new EventEmitter();
/** Event emitted when blur on the original editor. */
this.originalBlur = new EventEmitter();
/** Event emitted when the editor's modified value changes. */
this.modifiedValueChange = new EventEmitter();
/** Event emitted when focus on the modified editor. */
this.modifiedFocus = new EventEmitter();
/** Event emitted when blur on the modified editor. */
this.modifiedBlur = new EventEmitter();
this._onChange = () => { };
this._onTouched = () => { };
this._updateListener = (editor) => {
return EditorView.updateListener.of(vu => {
if (vu.docChanged && !vu.transactions.some(tr => tr.annotation(External))) {
const value = vu.state.doc.toString();
if (editor == 'a') {
this._onChange({ original: value, modified: this.modifiedValue });
this.originalValue = value;
this.originalValueChange.emit(value);
}
else if (editor == 'b') {
this._onChange({ original: this.originalValue, modified: value });
this.modifiedValue = value;
this.modifiedValueChange.emit(value);
}
}
});
};
this._editableConf = new Compartment();
}
ngOnChanges(changes) {
if (changes['originalValue']) {
this.setValue('a', this.originalValue);
}
if (changes['modifiedValue']) {
this.setValue('b', this.modifiedValue);
}
if (changes['orientation']) {
this.mergeView?.reconfigure({ orientation: this.orientation });
}
if (changes['revertControls']) {
this.mergeView?.reconfigure({ revertControls: this.revertControls });
}
if (changes['renderRevertControl']) {
this.mergeView?.reconfigure({ renderRevertControl: this.renderRevertControl });
}
if (changes['highlightChanges']) {
this.mergeView?.reconfigure({ highlightChanges: this.highlightChanges });
}
if (changes['gutter']) {
this.mergeView?.reconfigure({ gutter: this.gutter });
}
if (changes['collapseUnchanged']) {
this.mergeView?.reconfigure({ collapseUnchanged: this.collapseUnchanged });
}
if (changes['diffConfig']) {
this.mergeView?.reconfigure({ diffConfig: this.diffConfig });
}
if (changes['disabled']) {
this.setEditable('a', !this.disabled);
this.setEditable('b', !this.disabled);
}
}
ngOnInit() {
this.mergeView = new MergeView({
parent: this._elementRef.nativeElement,
a: {
doc: this.originalValue,
extensions: [
this._updateListener('a'),
this._editableConf.of([]),
this.setup === 'basic' ? basicSetup : this.setup === 'minimal' ? minimalSetup : [],
...this.originalExtensions,
],
},
b: {
doc: this.modifiedValue,
extensions: [
this._updateListener('b'),
this._editableConf.of([]),
this.setup === 'basic' ? basicSetup : this.setup === 'minimal' ? minimalSetup : [],
...this.modifiedExtensions,
],
},
orientation: this.orientation,
revertControls: this.revertControls,
renderRevertControl: this.renderRevertControl,
highlightChanges: this.highlightChanges,
gutter: this.gutter,
collapseUnchanged: this.collapseUnchanged,
diffConfig: this.diffConfig,
});
this.mergeView?.a.contentDOM.addEventListener('focus', () => {
this._onTouched();
this.originalFocus.emit();
});
this.mergeView?.a.contentDOM.addEventListener('blur', () => {
this._onTouched();
this.originalBlur.emit();
});
this.mergeView?.b.contentDOM.addEventListener('focus', () => {
this._onTouched();
this.modifiedFocus.emit();
});
this.mergeView?.b.contentDOM.addEventListener('blur', () => {
this._onTouched();
this.modifiedBlur.emit();
});
this.setEditable('a', !this.disabled);
this.setEditable('b', !this.disabled);
}
ngOnDestroy() {
this.mergeView?.destroy();
}
writeValue(value) {
if (this.mergeView && value != null && typeof value === 'object') {
this.originalValue = value.original;
this.modifiedValue = value.modified;
this.setValue('a', value.original);
this.setValue('b', value.modified);
}
}
registerOnChange(fn) {
this._onChange = fn;
}
registerOnTouched(fn) {
this._onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this.setEditable('a', !isDisabled);
this.setEditable('b', !isDisabled);
}
/** Sets diff-editor's value. */
setValue(editor, value) {
this.mergeView?.[editor].dispatch({
changes: { from: 0, to: this.mergeView[editor].state.doc.length, insert: value },
});
}
/** Sets diff-editor's editable state. */
setEditable(editor, value) {
this.mergeView?.[editor].dispatch({
effects: this._editableConf.reconfigure(EditorView.editable.of(value)),
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: DiffEditor, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "18.2.8", type: DiffEditor, isStandalone: true, selector: "diff-editor", inputs: { setup: "setup", originalValue: "originalValue", originalExtensions: "originalExtensions", modifiedValue: "modifiedValue", modifiedExtensions: "modifiedExtensions", orientation: "orientation", revertControls: "revertControls", renderRevertControl: "renderRevertControl", highlightChanges: ["highlightChanges", "highlightChanges", booleanAttribute], gutter: ["gutter", "gutter", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], collapseUnchanged: "collapseUnchanged", diffConfig: "diffConfig" }, outputs: { originalValueChange: "originalValueChange", originalFocus: "originalFocus", originalBlur: "originalBlur", modifiedValueChange: "modifiedValueChange", modifiedFocus: "modifiedFocus", modifiedBlur: "modifiedBlur" }, host: { classAttribute: "diff-editor" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DiffEditor),
multi: true,
},
], usesOnChanges: true, ngImport: i0, template: ``, isInline: true, styles: [".diff-editor{display:block}.diff-editor :is(.cm-mergeView,.cm-mergeViewEditors){height:100%}.diff-editor :is(.cm-mergeView .cm-editor,.cm-mergeView .cm-scroller){height:100%!important}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: DiffEditor, decorators: [{
type: Component,
args: [{ selector: 'diff-editor', standalone: true, template: ``, host: {
class: 'diff-editor',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DiffEditor),
multi: true,
},
], styles: [".diff-editor{display:block}.diff-editor :is(.cm-mergeView,.cm-mergeViewEditors){height:100%}.diff-editor :is(.cm-mergeView .cm-editor,.cm-mergeView .cm-scroller){height:100%!important}\n"] }]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { setup: [{
type: Input
}], originalValue: [{
type: Input
}], originalExtensions: [{
type: Input
}], modifiedValue: [{
type: Input
}], modifiedExtensions: [{
type: Input
}], orientation: [{
type: Input
}], revertControls: [{
type: Input
}], renderRevertControl: [{
type: Input
}], highlightChanges: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], gutter: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], disabled: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], collapseUnchanged: [{
type: Input
}], diffConfig: [{
type: Input
}], originalValueChange: [{
type: Output
}], originalFocus: [{
type: Output
}], originalBlur: [{
type: Output
}], modifiedValueChange: [{
type: Output
}], modifiedFocus: [{
type: Output
}], modifiedBlur: [{
type: Output
}] } });
class CodeEditorModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: CodeEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.8", ngImport: i0, type: CodeEditorModule, imports: [CodeEditor, DiffEditor], exports: [CodeEditor, DiffEditor] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: CodeEditorModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: CodeEditorModule, decorators: [{
type: NgModule,
args: [{
imports: [CodeEditor, DiffEditor],
exports: [CodeEditor, DiffEditor],
}]
}] });
/*
* Public API Surface of code-editor
*/
/**
* Generated bundle index. Do not edit.
*/
export { CodeEditor, CodeEditorModule, DiffEditor, External };
//# sourceMappingURL=acrodata-code-editor.mjs.map