@thescaffold/editor-angular 0.0.0
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 +102 -0
- package/ng-package.json +7 -0
- package/package.json +39 -0
- package/src/editor.component.ts +214 -0
- package/src/editor.module.ts +8 -0
- package/src/index.ts +1 -0
- package/src/public-api.ts +2 -0
- package/tsconfig.json +26 -0
- package/tsconfig.lib.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @thescaffold/editor-angular
|
|
2
|
+
|
|
3
|
+
Angular wrapper for X Editor. Provides a standalone `EditorComponent` with `@Input` bindings, `@Output` events, and `ControlValueAccessor` support for Angular Forms.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @thescaffold/editor-angular @thescaffold/editor-core @thescaffold/editor-addons
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Standalone component
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Component } from "@angular/core";
|
|
17
|
+
import { EditorComponent } from "@thescaffold/editor-angular";
|
|
18
|
+
import {
|
|
19
|
+
SchemaAddon,
|
|
20
|
+
CommandsAddon,
|
|
21
|
+
ToolbarAddon,
|
|
22
|
+
BoldAddon,
|
|
23
|
+
ItalicAddon,
|
|
24
|
+
} from "@thescaffold/editor-addons";
|
|
25
|
+
import "@thescaffold/editor-core/define";
|
|
26
|
+
|
|
27
|
+
@Component({
|
|
28
|
+
standalone: true,
|
|
29
|
+
imports: [EditorComponent],
|
|
30
|
+
template: `
|
|
31
|
+
<x-editor-ng
|
|
32
|
+
[mode]="'doc'"
|
|
33
|
+
[addons]="addons"
|
|
34
|
+
[theme]="'auto'"
|
|
35
|
+
(change)="onDocChange($event)"
|
|
36
|
+
(ready)="onReady($event)"
|
|
37
|
+
/>
|
|
38
|
+
`,
|
|
39
|
+
})
|
|
40
|
+
export class AppComponent {
|
|
41
|
+
addons = [
|
|
42
|
+
SchemaAddon(),
|
|
43
|
+
CommandsAddon(),
|
|
44
|
+
ToolbarAddon(),
|
|
45
|
+
BoldAddon(),
|
|
46
|
+
ItalicAddon(),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
onDocChange(event: { doc: unknown; content: string }) {
|
|
50
|
+
console.log("Doc changed:", event.content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onReady(event: { editor: unknown }) {
|
|
54
|
+
console.log("Editor ready:", event.editor);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Angular Forms — ControlValueAccessor
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
@Component({
|
|
63
|
+
standalone: true,
|
|
64
|
+
imports: [ReactiveFormsModule, EditorComponent],
|
|
65
|
+
template: `
|
|
66
|
+
<form [formGroup]="form">
|
|
67
|
+
<x-editor-ng formControlName="content" [addons]="addons" />
|
|
68
|
+
</form>
|
|
69
|
+
`,
|
|
70
|
+
})
|
|
71
|
+
export class FormComponent {
|
|
72
|
+
addons = [SchemaAddon(), CommandsAddon()];
|
|
73
|
+
form = new FormGroup({ content: new FormControl("") });
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Inputs
|
|
78
|
+
|
|
79
|
+
| Input | Type | Default | Description |
|
|
80
|
+
| ---------- | ------------------------------- | -------- | ------------------------------- |
|
|
81
|
+
| `mode` | `'doc' \| 'markdown' \| 'code'` | `'doc'` | Editor mode |
|
|
82
|
+
| `addons` | `AddonDefinition[]` | `[]` | Active addons |
|
|
83
|
+
| `content` | `string \| JSONContent` | — | Initial content |
|
|
84
|
+
| `editable` | `boolean` | `true` | Whether the editor is editable |
|
|
85
|
+
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Color theme |
|
|
86
|
+
| `language` | `string` | — | CodeMirror language (code mode) |
|
|
87
|
+
|
|
88
|
+
## Outputs
|
|
89
|
+
|
|
90
|
+
| Output | Payload | When |
|
|
91
|
+
| ----------------- | ------------------ | ---------------- |
|
|
92
|
+
| `change` | `{ doc, content }` | Document mutated |
|
|
93
|
+
| `selectionChange` | `{ selection }` | Cursor moved |
|
|
94
|
+
| `focus` | — | Focused |
|
|
95
|
+
| `blur` | — | Blurred |
|
|
96
|
+
| `modeChange` | `{ from, to }` | Mode switched |
|
|
97
|
+
| `ready` | `{ editor }` | Initialized |
|
|
98
|
+
| `error` | `{ error }` | Internal error |
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thescaffold/editor-angular",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Angular wrapper for X Editor",
|
|
5
|
+
"author": "Isaiah Iroko <isaiahiroko@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/fesm2022/thescaffold-editor-angular.mjs",
|
|
9
|
+
"module": "./dist/fesm2022/thescaffold-editor-angular.mjs",
|
|
10
|
+
"typings": "./dist/types/thescaffold-editor-angular.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/types/thescaffold-editor-angular.d.ts",
|
|
14
|
+
"default": "./dist/fesm2022/thescaffold-editor-angular.mjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
|
19
|
+
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
|
20
|
+
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
|
21
|
+
"@thescaffold/editor-core": "*",
|
|
22
|
+
"rxjs": "~7.8.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "ng-packagr -p ng-package.json",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@angular/animations": "^21.2.0",
|
|
30
|
+
"@angular/common": "^21.2.0",
|
|
31
|
+
"@angular/compiler": "^21.2.0",
|
|
32
|
+
"@angular/compiler-cli": "^21.2.0",
|
|
33
|
+
"@angular/core": "^21.2.0",
|
|
34
|
+
"@angular/forms": "^21.2.0",
|
|
35
|
+
"@angular/platform-browser": "^21.2.0",
|
|
36
|
+
"ng-packagr": "^21.2.0",
|
|
37
|
+
"typescript": "~5.9.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
Input,
|
|
7
|
+
NgZone,
|
|
8
|
+
OnChanges,
|
|
9
|
+
OnDestroy,
|
|
10
|
+
Output,
|
|
11
|
+
SimpleChanges,
|
|
12
|
+
forwardRef,
|
|
13
|
+
} from "@angular/core";
|
|
14
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
|
15
|
+
import type {
|
|
16
|
+
AddonDefinition,
|
|
17
|
+
CollabConfig,
|
|
18
|
+
EditorConfig,
|
|
19
|
+
EditorMode,
|
|
20
|
+
JSONContent,
|
|
21
|
+
} from "@thescaffold/editor-core";
|
|
22
|
+
|
|
23
|
+
@Component({
|
|
24
|
+
selector: "x-editor-ng",
|
|
25
|
+
standalone: true,
|
|
26
|
+
template: "<ng-content />",
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
styles: [":host { display: block; }"],
|
|
29
|
+
providers: [
|
|
30
|
+
{
|
|
31
|
+
provide: NG_VALUE_ACCESSOR,
|
|
32
|
+
useExisting: forwardRef(() => EditorComponent),
|
|
33
|
+
multi: true,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
export class EditorComponent
|
|
38
|
+
implements OnChanges, OnDestroy, ControlValueAccessor
|
|
39
|
+
{
|
|
40
|
+
@Input() mode: EditorMode = "doc";
|
|
41
|
+
@Input() language?: string;
|
|
42
|
+
@Input() content?: string | JSONContent;
|
|
43
|
+
@Input() editable = true;
|
|
44
|
+
@Input() theme: "light" | "dark" | "auto" = "auto";
|
|
45
|
+
@Input() addons: AddonDefinition[] = [];
|
|
46
|
+
@Input() collab?: CollabConfig;
|
|
47
|
+
|
|
48
|
+
@Output() readonly change = new EventEmitter<{
|
|
49
|
+
doc: unknown;
|
|
50
|
+
content: string;
|
|
51
|
+
}>();
|
|
52
|
+
@Output() readonly selectionChange = new EventEmitter<{
|
|
53
|
+
selection: unknown;
|
|
54
|
+
}>();
|
|
55
|
+
@Output() readonly focus = new EventEmitter<void>();
|
|
56
|
+
@Output() readonly blur = new EventEmitter<void>();
|
|
57
|
+
@Output() readonly modeChange = new EventEmitter<{
|
|
58
|
+
from: EditorMode;
|
|
59
|
+
to: EditorMode;
|
|
60
|
+
}>();
|
|
61
|
+
@Output() readonly ready = new EventEmitter<{ editor: unknown }>();
|
|
62
|
+
@Output() readonly error = new EventEmitter<{ error: unknown }>();
|
|
63
|
+
|
|
64
|
+
private _el: HTMLElement;
|
|
65
|
+
private _xEditorEl: (HTMLElement & { config?: EditorConfig }) | null = null;
|
|
66
|
+
private _editorCtx: {
|
|
67
|
+
setContent(c: string | JSONContent, f?: string): void;
|
|
68
|
+
} | null = null;
|
|
69
|
+
private _onChange: (value: string) => void = () => {};
|
|
70
|
+
private _onTouched: () => void = () => {};
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
private readonly _elementRef: ElementRef<HTMLElement>,
|
|
74
|
+
private readonly _zone: NgZone,
|
|
75
|
+
) {
|
|
76
|
+
this._el = this._elementRef.nativeElement;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
80
|
+
// Only structural inputs (mode/addons/language/theme/editable) require a
|
|
81
|
+
// full editor rebuild; content changes route through setContent() on the
|
|
82
|
+
// live editor context so user focus and selection aren't disrupted.
|
|
83
|
+
const structural = !!(
|
|
84
|
+
changes["mode"] ||
|
|
85
|
+
changes["addons"] ||
|
|
86
|
+
changes["language"] ||
|
|
87
|
+
changes["theme"] ||
|
|
88
|
+
changes["editable"] ||
|
|
89
|
+
changes["collab"]
|
|
90
|
+
);
|
|
91
|
+
if (structural || !this._editorCtx) {
|
|
92
|
+
this._updateConfig();
|
|
93
|
+
} else if (changes["content"]) {
|
|
94
|
+
this._applyContent(this.content);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private _applyContent(value: string | JSONContent | null | undefined): void {
|
|
99
|
+
if (value === null || value === undefined) return;
|
|
100
|
+
this._editorCtx?.setContent(
|
|
101
|
+
value,
|
|
102
|
+
typeof value === "string" ? "text" : "json",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ngAfterViewInit(): void {
|
|
107
|
+
this._zone.runOutsideAngular(() => {
|
|
108
|
+
const xeditor = document.createElement("x-editor") as HTMLElement & {
|
|
109
|
+
config?: EditorConfig;
|
|
110
|
+
};
|
|
111
|
+
xeditor.style.height = "100%";
|
|
112
|
+
this._el.appendChild(xeditor);
|
|
113
|
+
this._xEditorEl = xeditor;
|
|
114
|
+
|
|
115
|
+
xeditor.addEventListener("xe:ready", (e: Event) => {
|
|
116
|
+
const detail = (
|
|
117
|
+
e as CustomEvent<{
|
|
118
|
+
editor: { setContent(c: string | JSONContent, f?: string): void };
|
|
119
|
+
}>
|
|
120
|
+
).detail;
|
|
121
|
+
this._editorCtx = detail.editor;
|
|
122
|
+
// Apply any pending content that arrived via writeValue before mount.
|
|
123
|
+
if (this.content !== undefined && this.content !== null) {
|
|
124
|
+
this._applyContent(this.content);
|
|
125
|
+
}
|
|
126
|
+
this._zone.run(() => this.ready.emit(detail));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
xeditor.addEventListener("xe:change", (e: Event) => {
|
|
130
|
+
const detail = (e as CustomEvent<{ doc: unknown; content: string }>)
|
|
131
|
+
.detail;
|
|
132
|
+
this._zone.run(() => {
|
|
133
|
+
this.change.emit(detail);
|
|
134
|
+
this._onChange(detail.content ?? "");
|
|
135
|
+
this._onTouched();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
xeditor.addEventListener("xe:selection-change", (e: Event) => {
|
|
140
|
+
const detail = (e as CustomEvent<{ selection: unknown }>).detail;
|
|
141
|
+
this._zone.run(() => this.selectionChange.emit(detail));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
xeditor.addEventListener("xe:focus", () => {
|
|
145
|
+
this._zone.run(() => this.focus.emit());
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
xeditor.addEventListener("xe:blur", () => {
|
|
149
|
+
this._zone.run(() => {
|
|
150
|
+
this.blur.emit();
|
|
151
|
+
this._onTouched();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
xeditor.addEventListener("xe:mode-change", (e: Event) => {
|
|
156
|
+
const detail = (e as CustomEvent<{ from: EditorMode; to: EditorMode }>)
|
|
157
|
+
.detail;
|
|
158
|
+
this._zone.run(() => this.modeChange.emit(detail));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
xeditor.addEventListener("xe:error", (e: Event) => {
|
|
162
|
+
const detail = (e as CustomEvent<{ error: unknown }>).detail;
|
|
163
|
+
this._zone.run(() => this.error.emit(detail));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this._updateConfig();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ngOnDestroy(): void {
|
|
171
|
+
this._xEditorEl?.remove();
|
|
172
|
+
this._xEditorEl = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private _updateConfig(): void {
|
|
176
|
+
if (!this._xEditorEl) return;
|
|
177
|
+
this._xEditorEl.config = {
|
|
178
|
+
mode: this.mode,
|
|
179
|
+
language: this.language,
|
|
180
|
+
content: this.content,
|
|
181
|
+
editable: this.editable,
|
|
182
|
+
theme: this.theme,
|
|
183
|
+
addons: this.addons,
|
|
184
|
+
collab: this.collab,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── ControlValueAccessor ─────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
writeValue(value: string | JSONContent | null): void {
|
|
191
|
+
if (value === null || value === undefined) return;
|
|
192
|
+
this.content = value;
|
|
193
|
+
if (this._editorCtx) {
|
|
194
|
+
// Live editor: just push the new content without disturbing the adapter.
|
|
195
|
+
this._applyContent(value);
|
|
196
|
+
} else {
|
|
197
|
+
// Editor not mounted yet — defer; ngAfterViewInit / xe:ready will apply.
|
|
198
|
+
this._updateConfig();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
registerOnChange(fn: (value: string) => void): void {
|
|
203
|
+
this._onChange = fn;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
registerOnTouched(fn: () => void): void {
|
|
207
|
+
this._onTouched = fn;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
setDisabledState(isDisabled: boolean): void {
|
|
211
|
+
this.editable = !isDisabled;
|
|
212
|
+
this._updateConfig();
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./public-api.js";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"module": "ES2020",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"lib": ["ES2020", "DOM"],
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"experimentalDecorators": true,
|
|
12
|
+
"emitDecoratorMetadata": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"baseUrl": ".",
|
|
15
|
+
"paths": {
|
|
16
|
+
"@thescaffold/editor-core": ["../core/src/index.ts"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"angularCompilerOptions": {
|
|
20
|
+
"strictInjectionParameters": true,
|
|
21
|
+
"strictInputAccessModifiers": true,
|
|
22
|
+
"strictTemplates": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules", "dist"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"declarationDir": "../../dist/out-tsc"
|
|
6
|
+
},
|
|
7
|
+
"angularCompilerOptions": {
|
|
8
|
+
"skipTemplateCodegen": true,
|
|
9
|
+
"strictMetadataEmit": true,
|
|
10
|
+
"enableResourceInlining": true
|
|
11
|
+
},
|
|
12
|
+
"exclude": ["src/test.ts", "**/*.spec.ts"]
|
|
13
|
+
}
|