@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 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
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "dist",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
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
+ }
@@ -0,0 +1,8 @@
1
+ import { NgModule } from "@angular/core";
2
+ import { EditorComponent } from "./editor.component.js";
3
+
4
+ @NgModule({
5
+ imports: [EditorComponent],
6
+ exports: [EditorComponent],
7
+ })
8
+ export class EditorModule {}
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./public-api.js";
@@ -0,0 +1,2 @@
1
+ export { EditorComponent } from "./editor.component.js";
2
+ export { EditorModule } from "./editor.module.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
+ }