@notectl/angular 1.0.3 → 1.0.5

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Test harness for `NotectlEditorComponent` in consumer tests.
3
+ *
4
+ * Wraps a `ComponentFixture` and provides convenience methods
5
+ * for common test operations.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const fixture = TestBed.createComponent(NotectlEditorComponent);
10
+ * const harness = new NotectlTestHarness(fixture);
11
+ * await harness.whenReady();
12
+ * harness.setHTML('<p>Hello</p>');
13
+ * expect(harness.getText()).toBe('Hello');
14
+ * ```
15
+ */
16
+ class NotectlTestHarness {
17
+ fixture;
18
+ constructor(fixture) {
19
+ this.fixture = fixture;
20
+ }
21
+ /** Returns the component instance. */
22
+ get component() {
23
+ return this.fixture.componentInstance;
24
+ }
25
+ /** Waits for the editor to be ready and triggers change detection. */
26
+ async whenReady() {
27
+ await this.component.whenReady();
28
+ this.fixture.detectChanges();
29
+ }
30
+ /** Executes a command and triggers change detection. */
31
+ executeCommand(name) {
32
+ const result = this.component.executeCommand(name);
33
+ this.fixture.detectChanges();
34
+ return result;
35
+ }
36
+ /** Sets HTML content and triggers change detection. */
37
+ setHTML(html) {
38
+ this.component.setHTML(html);
39
+ this.fixture.detectChanges();
40
+ }
41
+ /** Returns the current HTML content. */
42
+ getHTML() {
43
+ return this.component.getHTML();
44
+ }
45
+ /** Returns the current plain text content. */
46
+ getText() {
47
+ return this.component.getText();
48
+ }
49
+ /** Returns whether the editor is empty. */
50
+ get isEmpty() {
51
+ return this.component.isEmpty();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Generated bundle index. Do not edit.
57
+ */
58
+
59
+ export { NotectlTestHarness };
60
+ //# sourceMappingURL=notectl-angular-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notectl-angular-testing.mjs","sources":["../../testing/src/test-harness.ts","../../testing/src/notectl-angular-testing.ts"],"sourcesContent":["import type { ComponentFixture } from '@angular/core/testing';\nimport type { NotectlEditorComponent } from '@notectl/angular';\n\n/**\n * Test harness for `NotectlEditorComponent` in consumer tests.\n *\n * Wraps a `ComponentFixture` and provides convenience methods\n * for common test operations.\n *\n * @example\n * ```typescript\n * const fixture = TestBed.createComponent(NotectlEditorComponent);\n * const harness = new NotectlTestHarness(fixture);\n * await harness.whenReady();\n * harness.setHTML('<p>Hello</p>');\n * expect(harness.getText()).toBe('Hello');\n * ```\n */\nexport class NotectlTestHarness {\n\tconstructor(private readonly fixture: ComponentFixture<NotectlEditorComponent>) {}\n\n\t/** Returns the component instance. */\n\tget component(): NotectlEditorComponent {\n\t\treturn this.fixture.componentInstance;\n\t}\n\n\t/** Waits for the editor to be ready and triggers change detection. */\n\tasync whenReady(): Promise<void> {\n\t\tawait this.component.whenReady();\n\t\tthis.fixture.detectChanges();\n\t}\n\n\t/** Executes a command and triggers change detection. */\n\texecuteCommand(name: string): boolean {\n\t\tconst result: boolean = this.component.executeCommand(name);\n\t\tthis.fixture.detectChanges();\n\t\treturn result;\n\t}\n\n\t/** Sets HTML content and triggers change detection. */\n\tsetHTML(html: string): void {\n\t\tthis.component.setHTML(html);\n\t\tthis.fixture.detectChanges();\n\t}\n\n\t/** Returns the current HTML content. */\n\tgetHTML(): string {\n\t\treturn this.component.getHTML();\n\t}\n\n\t/** Returns the current plain text content. */\n\tgetText(): string {\n\t\treturn this.component.getText();\n\t}\n\n\t/** Returns whether the editor is empty. */\n\tget isEmpty(): boolean {\n\t\treturn this.component.isEmpty();\n\t}\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;AAcG;MACU,kBAAkB,CAAA;AACD,IAAA,OAAA;AAA7B,IAAA,WAAA,CAA6B,OAAiD,EAAA;QAAjD,IAAA,CAAA,OAAO,GAAP,OAAO;IAA6C;;AAGjF,IAAA,IAAI,SAAS,GAAA;AACZ,QAAA,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB;IACtC;;AAGA,IAAA,MAAM,SAAS,GAAA;AACd,QAAA,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE;AAChC,QAAA,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE;IAC7B;;AAGA,IAAA,cAAc,CAAC,IAAY,EAAA;QAC1B,MAAM,MAAM,GAAY,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC;AAC3D,QAAA,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE;AAC5B,QAAA,OAAO,MAAM;IACd;;AAGA,IAAA,OAAO,CAAC,IAAY,EAAA;AACnB,QAAA,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;AAC5B,QAAA,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE;IAC7B;;IAGA,OAAO,GAAA;AACN,QAAA,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE;IAChC;;IAGA,OAAO,GAAA;AACN,QAAA,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE;IAChC;;AAGA,IAAA,IAAI,OAAO,GAAA;AACV,QAAA,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE;IAChC;AACA;;AC3DD;;AAEG;;;;"}
@@ -0,0 +1,549 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, DestroyRef, input, model, output, signal, computed, viewChild, afterNextRender, effect, ChangeDetectionStrategy, Component, forwardRef, Directive, Injectable } from '@angular/core';
3
+ import { ThemePreset, NotectlEditor } from '@notectl/core';
4
+ export { AlignmentPlugin, BlockquotePlugin, CodeBlockPlugin, DARK_THEME, FontPlugin, FontSizePlugin, HardBreakPlugin, HeadingPlugin, HighlightPlugin, HorizontalRulePlugin, ImagePlugin, LIGHT_THEME, LinkPlugin, ListPlugin, StrikethroughPlugin, SuperSubPlugin, TablePlugin, TextColorPlugin, TextFormattingPlugin, ThemePreset, ToolbarPlugin, createTheme } from '@notectl/core';
5
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
6
+ import { Subject } from 'rxjs';
7
+ export { STARTER_FONTS } from '@notectl/core/fonts';
8
+
9
+ /**
10
+ * Default configuration applied to all `<notectl-editor>` instances within
11
+ * the injector scope. Component-level inputs override these defaults.
12
+ */
13
+ const NOTECTL_DEFAULT_CONFIG = new InjectionToken('notectl-default-config');
14
+ /**
15
+ * Content format used by the `ControlValueAccessor` for forms integration.
16
+ * Determines how editor content is serialized when reading/writing form values.
17
+ *
18
+ * - `'json'` — `Document` objects (default)
19
+ * - `'html'` — sanitized HTML strings
20
+ * - `'text'` — plain text strings
21
+ */
22
+ const NOTECTL_CONTENT_FORMAT = new InjectionToken('notectl-content-format');
23
+ /**
24
+ * Configures the notectl editor at the environment injector level.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * bootstrapApplication(App, {
29
+ * providers: [
30
+ * provideNotectl({
31
+ * config: { theme: ThemePreset.Light, placeholder: 'Start typing...' },
32
+ * contentFormat: 'json',
33
+ * }),
34
+ * ],
35
+ * });
36
+ * ```
37
+ */
38
+ function provideNotectl(options = {}) {
39
+ const providers = [];
40
+ if (options.config) {
41
+ providers.push({ provide: NOTECTL_DEFAULT_CONFIG, useValue: options.config });
42
+ }
43
+ if (options.contentFormat) {
44
+ providers.push({ provide: NOTECTL_CONTENT_FORMAT, useValue: options.contentFormat });
45
+ }
46
+ return makeEnvironmentProviders(providers);
47
+ }
48
+
49
+ /**
50
+ * Angular standalone component wrapping the `<ntl-editor>` Web Component.
51
+ *
52
+ * Uses `afterNextRender()` for SSR-safe initialization and `effect()` for
53
+ * reactive input tracking — no lifecycle interfaces needed.
54
+ *
55
+ * @example
56
+ * ```html
57
+ * <!-- Basic usage -->
58
+ * <ntl-editor [toolbar]="toolbar" [plugins]="plugins" />
59
+ *
60
+ * <!-- Two-way content binding -->
61
+ * <ntl-editor [(content)]="myDocument" [toolbar]="toolbar" />
62
+ *
63
+ * <!-- Reactive forms -->
64
+ * <ntl-editor [formControl]="editorControl" [toolbar]="toolbar" />
65
+ * ```
66
+ */
67
+ class NotectlEditorComponent {
68
+ // --- Injected dependencies ---
69
+ destroyRef = inject(DestroyRef);
70
+ defaultConfig = inject(NOTECTL_DEFAULT_CONFIG, { optional: true });
71
+ // --- Signal Inputs (1:1 with Web Component config) ---
72
+ plugins = input([], ...(ngDevMode ? [{ debugName: "plugins" }] : []));
73
+ toolbar = input(...(ngDevMode ? [undefined, { debugName: "toolbar" }] : []));
74
+ features = input(...(ngDevMode ? [undefined, { debugName: "features" }] : []));
75
+ placeholder = input('Start typing...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
76
+ readonlyMode = input(false, ...(ngDevMode ? [{ debugName: "readonlyMode" }] : []));
77
+ autofocus = input(false, ...(ngDevMode ? [{ debugName: "autofocus" }] : []));
78
+ maxHistoryDepth = input(...(ngDevMode ? [undefined, { debugName: "maxHistoryDepth" }] : []));
79
+ theme = input(ThemePreset.Light, ...(ngDevMode ? [{ debugName: "theme" }] : []));
80
+ // --- Two-way content binding via model() ---
81
+ /**
82
+ * Two-way bindable document content.
83
+ *
84
+ * `undefined` means no external content was provided — the editor manages
85
+ * its own state. Once the editor emits changes, the model updates to the
86
+ * current `Document`.
87
+ *
88
+ * @example
89
+ * ```html
90
+ * <ntl-editor [(content)]="myDocument" />
91
+ * ```
92
+ */
93
+ content = model(...(ngDevMode ? [undefined, { debugName: "content" }] : []));
94
+ // --- Signal Outputs (events bridged from Web Component) ---
95
+ stateChange = output();
96
+ selectionChange = output();
97
+ editorFocus = output();
98
+ editorBlur = output();
99
+ ready = output();
100
+ // --- Reactive State ---
101
+ editorState = signal(null, ...(ngDevMode ? [{ debugName: "editorState" }] : []));
102
+ isEmpty = computed(() => {
103
+ const state = this.editorState();
104
+ if (!state)
105
+ return true;
106
+ const doc = state.doc;
107
+ if (doc.children.length === 0)
108
+ return true;
109
+ if (doc.children.length > 1)
110
+ return false;
111
+ const block = doc.children[0];
112
+ if (!block)
113
+ return true;
114
+ return block.type === 'paragraph' && block.children.length === 0;
115
+ }, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
116
+ // --- Internal state ---
117
+ hostRef = viewChild.required('host');
118
+ editorRef = null;
119
+ readyResolve = null;
120
+ readyPromise = new Promise((resolve) => {
121
+ this.readyResolve = resolve;
122
+ });
123
+ initialized = signal(false, ...(ngDevMode ? [{ debugName: "initialized" }] : []));
124
+ /** Tracks the last document set from within the editor to prevent feedback loops. */
125
+ lastEditorDoc = null;
126
+ constructor() {
127
+ // SSR-safe: only runs in the browser after first render
128
+ afterNextRender(() => {
129
+ this.initEditor();
130
+ });
131
+ // React to input changes via effect() — replaces ngOnChanges
132
+ effect(() => {
133
+ const currentTheme = this.theme();
134
+ const currentPlaceholder = this.placeholder();
135
+ const currentReadonly = this.readonlyMode();
136
+ const editor = this.editorRef;
137
+ if (!this.initialized() || !editor)
138
+ return;
139
+ editor.setTheme(currentTheme);
140
+ editor.configure({
141
+ placeholder: currentPlaceholder,
142
+ readonly: currentReadonly,
143
+ });
144
+ });
145
+ // Sync external content model changes into the editor.
146
+ // Skips when the document was set by the editor itself (feedback loop prevention).
147
+ effect(() => {
148
+ const doc = this.content();
149
+ const editor = this.editorRef;
150
+ if (!this.initialized() || !editor || !doc)
151
+ return;
152
+ // Skip if this document originated from the editor's own state change
153
+ if (doc === this.lastEditorDoc)
154
+ return;
155
+ editor.setJSON(doc);
156
+ });
157
+ this.destroyRef.onDestroy(() => {
158
+ this.destroyEditor();
159
+ });
160
+ }
161
+ // --- Public API (proxy to Web Component) ---
162
+ /** Returns the document as JSON. */
163
+ getJSON() {
164
+ return this.requireEditor().getJSON();
165
+ }
166
+ /** Sets the document from JSON. */
167
+ setJSON(doc) {
168
+ this.requireEditor().setJSON(doc);
169
+ }
170
+ /** Returns sanitized HTML representation of the document. */
171
+ getHTML() {
172
+ return this.requireEditor().getHTML();
173
+ }
174
+ /** Sets content from HTML (sanitized). */
175
+ setHTML(html) {
176
+ this.requireEditor().setHTML(html);
177
+ }
178
+ /** Returns plain text content. */
179
+ getText() {
180
+ return this.requireEditor().getText();
181
+ }
182
+ /** Proxy to the Web Component's `commands` object. */
183
+ get commands() {
184
+ return this.requireEditor().commands;
185
+ }
186
+ /** Proxy to the Web Component's `can()` capability checker. */
187
+ can() {
188
+ return this.requireEditor().can();
189
+ }
190
+ /** Executes a named command registered by a plugin. */
191
+ executeCommand(name) {
192
+ if (!this.editorRef)
193
+ return false;
194
+ return this.editorRef.executeCommand(name);
195
+ }
196
+ /** Configures a plugin at runtime. */
197
+ configurePlugin(pluginId, config) {
198
+ this.editorRef?.configurePlugin(pluginId, config);
199
+ }
200
+ /** Dispatches a transaction. */
201
+ dispatch(tr) {
202
+ this.editorRef?.dispatch(tr);
203
+ }
204
+ /** Returns the current editor state. */
205
+ getState() {
206
+ return this.requireEditor().getState();
207
+ }
208
+ /** Changes the theme at runtime. */
209
+ setTheme(theme) {
210
+ this.editorRef?.setTheme(theme);
211
+ }
212
+ /** Returns the current theme setting. */
213
+ getTheme() {
214
+ return this.editorRef?.getTheme() ?? this.theme();
215
+ }
216
+ /** Sets the readonly state programmatically (used by ControlValueAccessor). */
217
+ setReadonly(readonlyState) {
218
+ this.editorRef?.configure({ readonly: readonlyState });
219
+ }
220
+ /** Resolves when the editor is ready. */
221
+ whenReady() {
222
+ return this.readyPromise;
223
+ }
224
+ // --- Private ---
225
+ initEditor() {
226
+ const hostElement = this.hostRef().nativeElement;
227
+ const config = this.buildConfig();
228
+ const editor = new NotectlEditor();
229
+ this.editorRef = editor;
230
+ // Register event listeners BEFORE appending to DOM, because
231
+ // appendChild triggers connectedCallback → init() synchronously.
232
+ editor.on('stateChange', (event) => {
233
+ this.editorState.set(event.newState);
234
+ this.syncContentModel(event.newState.doc);
235
+ this.stateChange.emit(event);
236
+ });
237
+ editor.on('selectionChange', (event) => {
238
+ this.selectionChange.emit(event);
239
+ });
240
+ editor.on('focus', () => {
241
+ this.editorFocus.emit();
242
+ });
243
+ editor.on('blur', () => {
244
+ this.editorBlur.emit();
245
+ });
246
+ editor.on('ready', () => {
247
+ this.initialized.set(true);
248
+ const state = editor.getState();
249
+ this.editorState.set(state);
250
+ // Apply initial content model if set before editor was ready
251
+ const initialContent = this.content();
252
+ if (initialContent) {
253
+ editor.setJSON(initialContent);
254
+ }
255
+ this.readyResolve?.();
256
+ this.ready.emit();
257
+ });
258
+ // Init with config BEFORE appending to DOM. appendChild triggers
259
+ // connectedCallback which calls init() without config — by calling
260
+ // init(config) first, the editor initializes with the full config
261
+ // and connectedCallback's init() becomes a no-op (already initialized).
262
+ editor.init(config);
263
+ hostElement.appendChild(editor);
264
+ }
265
+ buildConfig() {
266
+ const defaults = this.defaultConfig ?? {};
267
+ const toolbar = this.toolbar();
268
+ const features = this.features();
269
+ const maxHistory = this.maxHistoryDepth();
270
+ const config = {
271
+ ...defaults,
272
+ plugins: this.plugins(),
273
+ placeholder: this.placeholder(),
274
+ readonly: this.readonlyMode(),
275
+ autofocus: this.autofocus(),
276
+ theme: this.theme(),
277
+ };
278
+ if (toolbar !== undefined) {
279
+ config.toolbar = toolbar;
280
+ }
281
+ if (features !== undefined) {
282
+ config.features = features;
283
+ }
284
+ if (maxHistory !== undefined) {
285
+ config.maxHistoryDepth = maxHistory;
286
+ }
287
+ return config;
288
+ }
289
+ /** Syncs editor document changes to the content model without feedback loops. */
290
+ syncContentModel(doc) {
291
+ this.lastEditorDoc = doc;
292
+ this.content.set(doc);
293
+ }
294
+ destroyEditor() {
295
+ if (this.editorRef) {
296
+ this.editorRef.destroy();
297
+ this.editorRef = null;
298
+ }
299
+ this.initialized.set(false);
300
+ }
301
+ requireEditor() {
302
+ const editor = this.editorRef;
303
+ if (!editor) {
304
+ throw new Error('NotectlEditor is not initialized. Await whenReady() first.');
305
+ }
306
+ return editor;
307
+ }
308
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: NotectlEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
309
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.4", type: NotectlEditorComponent, isStandalone: true, selector: "ntl-editor", inputs: { plugins: { classPropertyName: "plugins", publicName: "plugins", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, features: { classPropertyName: "features", publicName: "features", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonlyMode: { classPropertyName: "readonlyMode", publicName: "readonlyMode", isSignal: true, isRequired: false, transformFunction: null }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", isSignal: true, isRequired: false, transformFunction: null }, maxHistoryDepth: { classPropertyName: "maxHistoryDepth", publicName: "maxHistoryDepth", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { content: "contentChange", stateChange: "stateChange", selectionChange: "selectionChange", editorFocus: "editorFocus", editorBlur: "editorBlur", ready: "ready" }, viewQueries: [{ propertyName: "hostRef", first: true, predicate: ["host"], descendants: true, isSignal: true }], ngImport: i0, template: '<div #host></div>', isInline: true, styles: [":host{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
310
+ }
311
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: NotectlEditorComponent, decorators: [{
312
+ type: Component,
313
+ args: [{ selector: 'ntl-editor', template: '<div #host></div>', changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}\n"] }]
314
+ }], ctorParameters: () => [], propDecorators: { plugins: [{ type: i0.Input, args: [{ isSignal: true, alias: "plugins", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonlyMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonlyMode", required: false }] }], autofocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autofocus", required: false }] }], maxHistoryDepth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHistoryDepth", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }, { type: i0.Output, args: ["contentChange"] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], editorFocus: [{ type: i0.Output, args: ["editorFocus"] }], editorBlur: [{ type: i0.Output, args: ["editorBlur"] }], ready: [{ type: i0.Output, args: ["ready"] }], hostRef: [{ type: i0.ViewChild, args: ['host', { isSignal: true }] }] } });
315
+
316
+ /** Escapes HTML special characters to prevent XSS when inserting plain text. */
317
+ function escapeHtml(text) {
318
+ return text
319
+ .replace(/&/g, '&amp;')
320
+ .replace(/</g, '&lt;')
321
+ .replace(/>/g, '&gt;')
322
+ .replace(/"/g, '&quot;')
323
+ .replace(/'/g, '&#39;');
324
+ }
325
+ /**
326
+ * `ControlValueAccessor` directive for Angular forms integration.
327
+ *
328
+ * Supports Reactive Forms (`formControl`, `formControlName`) and
329
+ * template-driven forms (`ngModel`).
330
+ *
331
+ * The content format is configurable via `provideNotectl({ contentFormat })` or
332
+ * the `NOTECTL_CONTENT_FORMAT` injection token:
333
+ * - `'json'` (default) — form value is a `Document` object
334
+ * - `'html'` — form value is a sanitized HTML string
335
+ * - `'text'` — form value is a plain text string
336
+ */
337
+ class NotectlValueAccessorDirective {
338
+ editor = inject(NotectlEditorComponent);
339
+ destroyRef = inject(DestroyRef);
340
+ format = inject(NOTECTL_CONTENT_FORMAT, { optional: true }) ?? 'json';
341
+ onChange = () => { };
342
+ onTouched = () => { };
343
+ pendingValue = null;
344
+ suppressEmit = false;
345
+ stateChangeSub = this.editor.stateChange.subscribe((_event) => {
346
+ if (this.suppressEmit)
347
+ return;
348
+ const value = this.readValue();
349
+ this.onChange(value);
350
+ });
351
+ blurSub = this.editor.editorBlur.subscribe(() => {
352
+ this.onTouched();
353
+ });
354
+ constructor() {
355
+ this.destroyRef.onDestroy(() => {
356
+ this.stateChangeSub.unsubscribe();
357
+ this.blurSub.unsubscribe();
358
+ });
359
+ }
360
+ writeValue(value) {
361
+ if (!value)
362
+ return;
363
+ try {
364
+ this.editor.getState();
365
+ this.writeValueToEditor(value);
366
+ return;
367
+ }
368
+ catch {
369
+ // Editor not ready yet — defer
370
+ }
371
+ this.pendingValue = value;
372
+ this.editor.whenReady().then(() => {
373
+ if (this.pendingValue !== null) {
374
+ this.writeValueToEditor(this.pendingValue);
375
+ this.pendingValue = null;
376
+ }
377
+ });
378
+ }
379
+ registerOnChange(fn) {
380
+ this.onChange = fn;
381
+ }
382
+ registerOnTouched(fn) {
383
+ this.onTouched = fn;
384
+ }
385
+ setDisabledState(isDisabled) {
386
+ this.editor.setReadonly(isDisabled);
387
+ }
388
+ writeValueToEditor(value) {
389
+ this.suppressEmit = true;
390
+ try {
391
+ if (this.format === 'json' && typeof value === 'object') {
392
+ this.editor.setJSON(value);
393
+ }
394
+ else if (this.format === 'html' && typeof value === 'string') {
395
+ this.editor.setHTML(value);
396
+ }
397
+ else if (this.format === 'text' && typeof value === 'string') {
398
+ this.editor.setHTML(`<p>${escapeHtml(value)}</p>`);
399
+ }
400
+ else if (typeof value === 'string') {
401
+ this.editor.setHTML(value);
402
+ }
403
+ else {
404
+ this.editor.setJSON(value);
405
+ }
406
+ }
407
+ finally {
408
+ this.suppressEmit = false;
409
+ }
410
+ }
411
+ readValue() {
412
+ try {
413
+ switch (this.format) {
414
+ case 'html':
415
+ return this.editor.getHTML();
416
+ case 'text':
417
+ return this.editor.getText();
418
+ default:
419
+ return this.editor.getJSON();
420
+ }
421
+ }
422
+ catch {
423
+ return null;
424
+ }
425
+ }
426
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: NotectlValueAccessorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
427
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.4", type: NotectlValueAccessorDirective, isStandalone: true, selector: "ntl-editor[formControl], ntl-editor[formControlName], ntl-editor[ngModel]", providers: [
428
+ {
429
+ provide: NG_VALUE_ACCESSOR,
430
+ useExisting: forwardRef(() => NotectlValueAccessorDirective),
431
+ multi: true,
432
+ },
433
+ ], ngImport: i0 });
434
+ }
435
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: NotectlValueAccessorDirective, decorators: [{
436
+ type: Directive,
437
+ args: [{
438
+ selector: 'ntl-editor[formControl], ntl-editor[formControlName], ntl-editor[ngModel]',
439
+ providers: [
440
+ {
441
+ provide: NG_VALUE_ACCESSOR,
442
+ useExisting: forwardRef(() => NotectlValueAccessorDirective),
443
+ multi: true,
444
+ },
445
+ ],
446
+ }]
447
+ }], ctorParameters: () => [] });
448
+
449
+ /**
450
+ * Optional injectable service for programmatic editor access via DI.
451
+ *
452
+ * Useful when a toolbar or other component needs to interact with
453
+ * the editor without a direct template reference.
454
+ *
455
+ * The service must be provided at a shared injector level and
456
+ * connected to the editor component via `register()`.
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * @Component({
461
+ * providers: [NotectlEditorService],
462
+ * template: `
463
+ * <app-toolbar />
464
+ * <ntl-editor #editor [plugins]="plugins" />
465
+ * `,
466
+ * })
467
+ * export class EditorPage {
468
+ * private readonly editor = viewChild.required<NotectlEditorComponent>('editor');
469
+ * private readonly service = inject(NotectlEditorService);
470
+ *
471
+ * constructor() {
472
+ * afterNextRender(() => {
473
+ * this.service.register(this.editor());
474
+ * });
475
+ * }
476
+ * }
477
+ * ```
478
+ */
479
+ class NotectlEditorService {
480
+ destroyRef = inject(DestroyRef);
481
+ editorRef = signal(null, ...(ngDevMode ? [{ debugName: "editorRef" }] : []));
482
+ stateChangeSubject = new Subject();
483
+ /** Whether an editor is currently registered with this service. */
484
+ hasEditor = computed(() => this.editorRef() !== null, ...(ngDevMode ? [{ debugName: "hasEditor" }] : []));
485
+ /** Observable stream of state change events from the editor. */
486
+ stateChanges$ = this.stateChangeSubject.asObservable();
487
+ stateChangeSub = null;
488
+ constructor() {
489
+ this.destroyRef.onDestroy(() => {
490
+ this.unregister();
491
+ this.stateChangeSubject.complete();
492
+ });
493
+ }
494
+ /** Registers an editor component with this service. */
495
+ register(editor) {
496
+ this.unregister();
497
+ this.editorRef.set(editor);
498
+ this.stateChangeSub = editor.stateChange.subscribe((event) => {
499
+ this.stateChangeSubject.next(event);
500
+ });
501
+ }
502
+ /** Unregisters the current editor from this service. */
503
+ unregister() {
504
+ this.stateChangeSub?.unsubscribe();
505
+ this.stateChangeSub = null;
506
+ this.editorRef.set(null);
507
+ }
508
+ /** Executes a named command on the registered editor. */
509
+ executeCommand(name) {
510
+ const editor = this.editorRef();
511
+ if (!editor)
512
+ return false;
513
+ return editor.executeCommand(name);
514
+ }
515
+ /** Returns the current editor state, or `null` if no editor is registered. */
516
+ getState() {
517
+ const editor = this.editorRef();
518
+ if (!editor)
519
+ return null;
520
+ try {
521
+ return editor.getState();
522
+ }
523
+ catch {
524
+ return null;
525
+ }
526
+ }
527
+ /** Dispatches a transaction on the registered editor. */
528
+ dispatch(tr) {
529
+ this.editorRef()?.dispatch(tr);
530
+ }
531
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: NotectlEditorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
532
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: NotectlEditorService });
533
+ }
534
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: NotectlEditorService, decorators: [{
535
+ type: Injectable
536
+ }], ctorParameters: () => [] });
537
+
538
+ /**
539
+ * @notectl/angular — Angular integration for the notectl rich text editor.
540
+ * @packageDocumentation
541
+ */
542
+ // --- Angular Bindings ---
543
+
544
+ /**
545
+ * Generated bundle index. Do not edit.
546
+ */
547
+
548
+ export { NOTECTL_CONTENT_FORMAT, NOTECTL_DEFAULT_CONFIG, NotectlEditorComponent, NotectlEditorService, NotectlValueAccessorDirective, provideNotectl };
549
+ //# sourceMappingURL=notectl-angular.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notectl-angular.mjs","sources":["../../src/lib/tokens.ts","../../src/lib/notectl-editor.component.ts","../../src/lib/value-accessor.directive.ts","../../src/lib/notectl-editor.service.ts","../../src/public-api.ts","../../src/notectl-angular.ts"],"sourcesContent":["import { type EnvironmentProviders, InjectionToken, makeEnvironmentProviders } from '@angular/core';\nimport type { NotectlEditorConfig } from '@notectl/core';\n\n/**\n * Default configuration applied to all `<notectl-editor>` instances within\n * the injector scope. Component-level inputs override these defaults.\n */\nexport const NOTECTL_DEFAULT_CONFIG: InjectionToken<Partial<NotectlEditorConfig>> =\n\tnew InjectionToken<Partial<NotectlEditorConfig>>('notectl-default-config');\n\n/**\n * Content format used by the `ControlValueAccessor` for forms integration.\n * Determines how editor content is serialized when reading/writing form values.\n *\n * - `'json'` — `Document` objects (default)\n * - `'html'` — sanitized HTML strings\n * - `'text'` — plain text strings\n */\nexport const NOTECTL_CONTENT_FORMAT: InjectionToken<ContentFormat> =\n\tnew InjectionToken<ContentFormat>('notectl-content-format');\n\n/** Supported content serialization formats for forms integration. */\nexport type ContentFormat = 'json' | 'html' | 'text';\n\n/** Options for `provideNotectl()`. */\nexport interface NotectlProviderOptions {\n\t/** Default editor configuration applied to all instances. */\n\treadonly config?: Partial<NotectlEditorConfig>;\n\t/** Content serialization format for forms integration. Defaults to `'json'`. */\n\treadonly contentFormat?: ContentFormat;\n}\n\n/**\n * Configures the notectl editor at the environment injector level.\n *\n * @example\n * ```typescript\n * bootstrapApplication(App, {\n * providers: [\n * provideNotectl({\n * config: { theme: ThemePreset.Light, placeholder: 'Start typing...' },\n * contentFormat: 'json',\n * }),\n * ],\n * });\n * ```\n */\nexport function provideNotectl(options: NotectlProviderOptions = {}): EnvironmentProviders {\n\tconst providers = [];\n\n\tif (options.config) {\n\t\tproviders.push({ provide: NOTECTL_DEFAULT_CONFIG, useValue: options.config });\n\t}\n\n\tif (options.contentFormat) {\n\t\tproviders.push({ provide: NOTECTL_CONTENT_FORMAT, useValue: options.contentFormat });\n\t}\n\n\treturn makeEnvironmentProviders(providers);\n}\n","import {\n\tChangeDetectionStrategy,\n\tComponent,\n\tDestroyRef,\n\ttype ElementRef,\n\ttype ModelSignal,\n\tafterNextRender,\n\tcomputed,\n\teffect,\n\tinject,\n\tinput,\n\tmodel,\n\toutput,\n\tsignal,\n\tviewChild,\n} from '@angular/core';\nimport type {\n\tDocument,\n\tEditorSelection,\n\tEditorState,\n\tNotectlEditorConfig,\n\tPlugin,\n\tPluginConfig,\n\tStateChangeEvent,\n\tTextFormattingConfig,\n\tTheme,\n\tTransaction,\n} from '@notectl/core';\nimport { NotectlEditor, ThemePreset } from '@notectl/core';\n\nimport { NOTECTL_DEFAULT_CONFIG } from './tokens';\nimport type { SelectionChangeEvent } from './types';\n\n/**\n * Angular standalone component wrapping the `<ntl-editor>` Web Component.\n *\n * Uses `afterNextRender()` for SSR-safe initialization and `effect()` for\n * reactive input tracking — no lifecycle interfaces needed.\n *\n * @example\n * ```html\n * <!-- Basic usage -->\n * <ntl-editor [toolbar]=\"toolbar\" [plugins]=\"plugins\" />\n *\n * <!-- Two-way content binding -->\n * <ntl-editor [(content)]=\"myDocument\" [toolbar]=\"toolbar\" />\n *\n * <!-- Reactive forms -->\n * <ntl-editor [formControl]=\"editorControl\" [toolbar]=\"toolbar\" />\n * ```\n */\n@Component({\n\tselector: 'ntl-editor',\n\ttemplate: '<div #host></div>',\n\tstyles: ':host { display: block; }',\n\tchangeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class NotectlEditorComponent {\n\t// --- Injected dependencies ---\n\n\tprivate readonly destroyRef: DestroyRef = inject(DestroyRef);\n\tprivate readonly defaultConfig: Partial<NotectlEditorConfig> | null = inject(\n\t\tNOTECTL_DEFAULT_CONFIG,\n\t\t{ optional: true },\n\t);\n\n\t// --- Signal Inputs (1:1 with Web Component config) ---\n\n\treadonly plugins = input<Plugin[]>([]);\n\treadonly toolbar = input<ReadonlyArray<ReadonlyArray<Plugin>>>();\n\treadonly features = input<Partial<TextFormattingConfig>>();\n\treadonly placeholder = input<string>('Start typing...');\n\treadonly readonlyMode = input<boolean>(false);\n\treadonly autofocus = input<boolean>(false);\n\treadonly maxHistoryDepth = input<number>();\n\treadonly theme = input<ThemePreset | Theme>(ThemePreset.Light);\n\n\t// --- Two-way content binding via model() ---\n\n\t/**\n\t * Two-way bindable document content.\n\t *\n\t * `undefined` means no external content was provided — the editor manages\n\t * its own state. Once the editor emits changes, the model updates to the\n\t * current `Document`.\n\t *\n\t * @example\n\t * ```html\n\t * <ntl-editor [(content)]=\"myDocument\" />\n\t * ```\n\t */\n\treadonly content: ModelSignal<Document | undefined> = model<Document>();\n\n\t// --- Signal Outputs (events bridged from Web Component) ---\n\n\treadonly stateChange = output<StateChangeEvent>();\n\treadonly selectionChange = output<SelectionChangeEvent>();\n\treadonly editorFocus = output<void>();\n\treadonly editorBlur = output<void>();\n\treadonly ready = output<void>();\n\n\t// --- Reactive State ---\n\n\treadonly editorState = signal<EditorState | null>(null);\n\n\treadonly isEmpty = computed<boolean>(() => {\n\t\tconst state: EditorState | null = this.editorState();\n\t\tif (!state) return true;\n\t\tconst doc: Document = state.doc;\n\t\tif (doc.children.length === 0) return true;\n\t\tif (doc.children.length > 1) return false;\n\t\tconst block = doc.children[0];\n\t\tif (!block) return true;\n\t\treturn block.type === 'paragraph' && block.children.length === 0;\n\t});\n\n\t// --- Internal state ---\n\n\tprivate readonly hostRef = viewChild.required<ElementRef<HTMLDivElement>>('host');\n\tprivate editorRef: NotectlEditor | null = null;\n\tprivate readyResolve: (() => void) | null = null;\n\tprivate readonly readyPromise: Promise<void> = new Promise<void>((resolve) => {\n\t\tthis.readyResolve = resolve;\n\t});\n\tprivate readonly initialized = signal(false);\n\n\t/** Tracks the last document set from within the editor to prevent feedback loops. */\n\tprivate lastEditorDoc: Document | null = null;\n\n\tconstructor() {\n\t\t// SSR-safe: only runs in the browser after first render\n\t\tafterNextRender(() => {\n\t\t\tthis.initEditor();\n\t\t});\n\n\t\t// React to input changes via effect() — replaces ngOnChanges\n\t\teffect(() => {\n\t\t\tconst currentTheme: ThemePreset | Theme = this.theme();\n\t\t\tconst currentPlaceholder: string = this.placeholder();\n\t\t\tconst currentReadonly: boolean = this.readonlyMode();\n\n\t\t\tconst editor: NotectlEditor | null = this.editorRef;\n\t\t\tif (!this.initialized() || !editor) return;\n\n\t\t\teditor.setTheme(currentTheme);\n\t\t\teditor.configure({\n\t\t\t\tplaceholder: currentPlaceholder,\n\t\t\t\treadonly: currentReadonly,\n\t\t\t});\n\t\t});\n\n\t\t// Sync external content model changes into the editor.\n\t\t// Skips when the document was set by the editor itself (feedback loop prevention).\n\t\teffect(() => {\n\t\t\tconst doc: Document | undefined = this.content();\n\t\t\tconst editor: NotectlEditor | null = this.editorRef;\n\t\t\tif (!this.initialized() || !editor || !doc) return;\n\n\t\t\t// Skip if this document originated from the editor's own state change\n\t\t\tif (doc === this.lastEditorDoc) return;\n\n\t\t\teditor.setJSON(doc);\n\t\t});\n\n\t\tthis.destroyRef.onDestroy(() => {\n\t\t\tthis.destroyEditor();\n\t\t});\n\t}\n\n\t// --- Public API (proxy to Web Component) ---\n\n\t/** Returns the document as JSON. */\n\tgetJSON(): Document {\n\t\treturn this.requireEditor().getJSON();\n\t}\n\n\t/** Sets the document from JSON. */\n\tsetJSON(doc: Document): void {\n\t\tthis.requireEditor().setJSON(doc);\n\t}\n\n\t/** Returns sanitized HTML representation of the document. */\n\tgetHTML(): string {\n\t\treturn this.requireEditor().getHTML();\n\t}\n\n\t/** Sets content from HTML (sanitized). */\n\tsetHTML(html: string): void {\n\t\tthis.requireEditor().setHTML(html);\n\t}\n\n\t/** Returns plain text content. */\n\tgetText(): string {\n\t\treturn this.requireEditor().getText();\n\t}\n\n\t/** Proxy to the Web Component's `commands` object. */\n\tget commands(): NotectlEditor['commands'] {\n\t\treturn this.requireEditor().commands;\n\t}\n\n\t/** Proxy to the Web Component's `can()` capability checker. */\n\tcan(): ReturnType<NotectlEditor['can']> {\n\t\treturn this.requireEditor().can();\n\t}\n\n\t/** Executes a named command registered by a plugin. */\n\texecuteCommand(name: string): boolean {\n\t\tif (!this.editorRef) return false;\n\t\treturn this.editorRef.executeCommand(name);\n\t}\n\n\t/** Configures a plugin at runtime. */\n\tconfigurePlugin(pluginId: string, config: PluginConfig): void {\n\t\tthis.editorRef?.configurePlugin(pluginId, config);\n\t}\n\n\t/** Dispatches a transaction. */\n\tdispatch(tr: Transaction): void {\n\t\tthis.editorRef?.dispatch(tr);\n\t}\n\n\t/** Returns the current editor state. */\n\tgetState(): EditorState {\n\t\treturn this.requireEditor().getState();\n\t}\n\n\t/** Changes the theme at runtime. */\n\tsetTheme(theme: ThemePreset | Theme): void {\n\t\tthis.editorRef?.setTheme(theme);\n\t}\n\n\t/** Returns the current theme setting. */\n\tgetTheme(): ThemePreset | Theme {\n\t\treturn this.editorRef?.getTheme() ?? this.theme();\n\t}\n\n\t/** Sets the readonly state programmatically (used by ControlValueAccessor). */\n\tsetReadonly(readonlyState: boolean): void {\n\t\tthis.editorRef?.configure({ readonly: readonlyState });\n\t}\n\n\t/** Resolves when the editor is ready. */\n\twhenReady(): Promise<void> {\n\t\treturn this.readyPromise;\n\t}\n\n\t// --- Private ---\n\n\tprivate initEditor(): void {\n\t\tconst hostElement: HTMLDivElement = this.hostRef().nativeElement;\n\t\tconst config: NotectlEditorConfig = this.buildConfig();\n\n\t\tconst editor: NotectlEditor = new NotectlEditor();\n\t\tthis.editorRef = editor;\n\n\t\t// Register event listeners BEFORE appending to DOM, because\n\t\t// appendChild triggers connectedCallback → init() synchronously.\n\t\teditor.on('stateChange', (event: StateChangeEvent) => {\n\t\t\tthis.editorState.set(event.newState);\n\t\t\tthis.syncContentModel(event.newState.doc);\n\t\t\tthis.stateChange.emit(event);\n\t\t});\n\n\t\teditor.on('selectionChange', (event: { selection: EditorSelection }) => {\n\t\t\tthis.selectionChange.emit(event);\n\t\t});\n\n\t\teditor.on('focus', () => {\n\t\t\tthis.editorFocus.emit();\n\t\t});\n\n\t\teditor.on('blur', () => {\n\t\t\tthis.editorBlur.emit();\n\t\t});\n\n\t\teditor.on('ready', () => {\n\t\t\tthis.initialized.set(true);\n\t\t\tconst state: EditorState = editor.getState();\n\t\t\tthis.editorState.set(state);\n\n\t\t\t// Apply initial content model if set before editor was ready\n\t\t\tconst initialContent: Document | undefined = this.content();\n\t\t\tif (initialContent) {\n\t\t\t\teditor.setJSON(initialContent);\n\t\t\t}\n\n\t\t\tthis.readyResolve?.();\n\t\t\tthis.ready.emit();\n\t\t});\n\n\t\t// Init with config BEFORE appending to DOM. appendChild triggers\n\t\t// connectedCallback which calls init() without config — by calling\n\t\t// init(config) first, the editor initializes with the full config\n\t\t// and connectedCallback's init() becomes a no-op (already initialized).\n\t\teditor.init(config);\n\t\thostElement.appendChild(editor);\n\t}\n\n\tprivate buildConfig(): NotectlEditorConfig {\n\t\tconst defaults: Partial<NotectlEditorConfig> = this.defaultConfig ?? {};\n\t\tconst toolbar: ReadonlyArray<ReadonlyArray<Plugin>> | undefined = this.toolbar();\n\t\tconst features: Partial<TextFormattingConfig> | undefined = this.features();\n\t\tconst maxHistory: number | undefined = this.maxHistoryDepth();\n\n\t\tconst config: NotectlEditorConfig = {\n\t\t\t...defaults,\n\t\t\tplugins: this.plugins(),\n\t\t\tplaceholder: this.placeholder(),\n\t\t\treadonly: this.readonlyMode(),\n\t\t\tautofocus: this.autofocus(),\n\t\t\ttheme: this.theme(),\n\t\t};\n\n\t\tif (toolbar !== undefined) {\n\t\t\tconfig.toolbar = toolbar;\n\t\t}\n\t\tif (features !== undefined) {\n\t\t\tconfig.features = features;\n\t\t}\n\t\tif (maxHistory !== undefined) {\n\t\t\tconfig.maxHistoryDepth = maxHistory;\n\t\t}\n\n\t\treturn config;\n\t}\n\n\t/** Syncs editor document changes to the content model without feedback loops. */\n\tprivate syncContentModel(doc: Document): void {\n\t\tthis.lastEditorDoc = doc;\n\t\tthis.content.set(doc);\n\t}\n\n\tprivate destroyEditor(): void {\n\t\tif (this.editorRef) {\n\t\t\tthis.editorRef.destroy();\n\t\t\tthis.editorRef = null;\n\t\t}\n\t\tthis.initialized.set(false);\n\t}\n\n\tprivate requireEditor(): NotectlEditor {\n\t\tconst editor: NotectlEditor | null = this.editorRef;\n\t\tif (!editor) {\n\t\t\tthrow new Error('NotectlEditor is not initialized. Await whenReady() first.');\n\t\t}\n\t\treturn editor;\n\t}\n}\n","import { DestroyRef, Directive, forwardRef, inject } from '@angular/core';\nimport { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport type { Document, StateChangeEvent } from '@notectl/core';\n\nimport { NotectlEditorComponent } from './notectl-editor.component';\nimport { type ContentFormat, NOTECTL_CONTENT_FORMAT } from './tokens';\n\ntype OnChangeFn = (value: Document | string | null) => void;\ntype OnTouchedFn = () => void;\n\n/** Escapes HTML special characters to prevent XSS when inserting plain text. */\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, '&amp;')\n\t\t.replace(/</g, '&lt;')\n\t\t.replace(/>/g, '&gt;')\n\t\t.replace(/\"/g, '&quot;')\n\t\t.replace(/'/g, '&#39;');\n}\n\n/**\n * `ControlValueAccessor` directive for Angular forms integration.\n *\n * Supports Reactive Forms (`formControl`, `formControlName`) and\n * template-driven forms (`ngModel`).\n *\n * The content format is configurable via `provideNotectl({ contentFormat })` or\n * the `NOTECTL_CONTENT_FORMAT` injection token:\n * - `'json'` (default) — form value is a `Document` object\n * - `'html'` — form value is a sanitized HTML string\n * - `'text'` — form value is a plain text string\n */\n@Directive({\n\tselector: 'ntl-editor[formControl], ntl-editor[formControlName], ntl-editor[ngModel]',\n\tproviders: [\n\t\t{\n\t\t\tprovide: NG_VALUE_ACCESSOR,\n\t\t\tuseExisting: forwardRef(() => NotectlValueAccessorDirective),\n\t\t\tmulti: true,\n\t\t},\n\t],\n})\nexport class NotectlValueAccessorDirective implements ControlValueAccessor {\n\tprivate readonly editor: NotectlEditorComponent = inject(NotectlEditorComponent);\n\tprivate readonly destroyRef: DestroyRef = inject(DestroyRef);\n\tprivate readonly format: ContentFormat =\n\t\tinject(NOTECTL_CONTENT_FORMAT, { optional: true }) ?? 'json';\n\n\tprivate onChange: OnChangeFn = () => {};\n\tprivate onTouched: OnTouchedFn = () => {};\n\tprivate pendingValue: Document | string | null = null;\n\tprivate suppressEmit = false;\n\n\tprivate readonly stateChangeSub = this.editor.stateChange.subscribe(\n\t\t(_event: StateChangeEvent) => {\n\t\t\tif (this.suppressEmit) return;\n\t\t\tconst value: Document | string | null = this.readValue();\n\t\t\tthis.onChange(value);\n\t\t},\n\t);\n\n\tprivate readonly blurSub = this.editor.editorBlur.subscribe(() => {\n\t\tthis.onTouched();\n\t});\n\n\tconstructor() {\n\t\tthis.destroyRef.onDestroy(() => {\n\t\t\tthis.stateChangeSub.unsubscribe();\n\t\t\tthis.blurSub.unsubscribe();\n\t\t});\n\t}\n\n\twriteValue(value: Document | string | null): void {\n\t\tif (!value) return;\n\n\t\ttry {\n\t\t\tthis.editor.getState();\n\t\t\tthis.writeValueToEditor(value);\n\t\t\treturn;\n\t\t} catch {\n\t\t\t// Editor not ready yet — defer\n\t\t}\n\n\t\tthis.pendingValue = value;\n\t\tthis.editor.whenReady().then(() => {\n\t\t\tif (this.pendingValue !== null) {\n\t\t\t\tthis.writeValueToEditor(this.pendingValue);\n\t\t\t\tthis.pendingValue = null;\n\t\t\t}\n\t\t});\n\t}\n\n\tregisterOnChange(fn: OnChangeFn): void {\n\t\tthis.onChange = fn;\n\t}\n\n\tregisterOnTouched(fn: OnTouchedFn): void {\n\t\tthis.onTouched = fn;\n\t}\n\n\tsetDisabledState(isDisabled: boolean): void {\n\t\tthis.editor.setReadonly(isDisabled);\n\t}\n\n\tprivate writeValueToEditor(value: Document | string): void {\n\t\tthis.suppressEmit = true;\n\t\ttry {\n\t\t\tif (this.format === 'json' && typeof value === 'object') {\n\t\t\t\tthis.editor.setJSON(value as Document);\n\t\t\t} else if (this.format === 'html' && typeof value === 'string') {\n\t\t\t\tthis.editor.setHTML(value);\n\t\t\t} else if (this.format === 'text' && typeof value === 'string') {\n\t\t\t\tthis.editor.setHTML(`<p>${escapeHtml(value)}</p>`);\n\t\t\t} else if (typeof value === 'string') {\n\t\t\t\tthis.editor.setHTML(value);\n\t\t\t} else {\n\t\t\t\tthis.editor.setJSON(value as Document);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.suppressEmit = false;\n\t\t}\n\t}\n\n\tprivate readValue(): Document | string | null {\n\t\ttry {\n\t\t\tswitch (this.format) {\n\t\t\t\tcase 'html':\n\t\t\t\t\treturn this.editor.getHTML();\n\t\t\t\tcase 'text':\n\t\t\t\t\treturn this.editor.getText();\n\t\t\t\tdefault:\n\t\t\t\t\treturn this.editor.getJSON();\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n","import { DestroyRef, Injectable, computed, inject, signal } from '@angular/core';\nimport type { EditorState, StateChangeEvent, Transaction } from '@notectl/core';\nimport { type Observable, Subject } from 'rxjs';\n\nimport type { NotectlEditorComponent } from './notectl-editor.component';\n\n/**\n * Optional injectable service for programmatic editor access via DI.\n *\n * Useful when a toolbar or other component needs to interact with\n * the editor without a direct template reference.\n *\n * The service must be provided at a shared injector level and\n * connected to the editor component via `register()`.\n *\n * @example\n * ```typescript\n * @Component({\n * providers: [NotectlEditorService],\n * template: `\n * <app-toolbar />\n * <ntl-editor #editor [plugins]=\"plugins\" />\n * `,\n * })\n * export class EditorPage {\n * private readonly editor = viewChild.required<NotectlEditorComponent>('editor');\n * private readonly service = inject(NotectlEditorService);\n *\n * constructor() {\n * afterNextRender(() => {\n * this.service.register(this.editor());\n * });\n * }\n * }\n * ```\n */\n@Injectable()\nexport class NotectlEditorService {\n\tprivate readonly destroyRef: DestroyRef = inject(DestroyRef);\n\tprivate readonly editorRef = signal<NotectlEditorComponent | null>(null);\n\tprivate readonly stateChangeSubject = new Subject<StateChangeEvent>();\n\n\t/** Whether an editor is currently registered with this service. */\n\treadonly hasEditor = computed<boolean>(() => this.editorRef() !== null);\n\n\t/** Observable stream of state change events from the editor. */\n\treadonly stateChanges$: Observable<StateChangeEvent> = this.stateChangeSubject.asObservable();\n\n\tprivate stateChangeSub: { unsubscribe(): void } | null = null;\n\n\tconstructor() {\n\t\tthis.destroyRef.onDestroy(() => {\n\t\t\tthis.unregister();\n\t\t\tthis.stateChangeSubject.complete();\n\t\t});\n\t}\n\n\t/** Registers an editor component with this service. */\n\tregister(editor: NotectlEditorComponent): void {\n\t\tthis.unregister();\n\t\tthis.editorRef.set(editor);\n\n\t\tthis.stateChangeSub = editor.stateChange.subscribe((event: StateChangeEvent) => {\n\t\t\tthis.stateChangeSubject.next(event);\n\t\t});\n\t}\n\n\t/** Unregisters the current editor from this service. */\n\tunregister(): void {\n\t\tthis.stateChangeSub?.unsubscribe();\n\t\tthis.stateChangeSub = null;\n\t\tthis.editorRef.set(null);\n\t}\n\n\t/** Executes a named command on the registered editor. */\n\texecuteCommand(name: string): boolean {\n\t\tconst editor: NotectlEditorComponent | null = this.editorRef();\n\t\tif (!editor) return false;\n\t\treturn editor.executeCommand(name);\n\t}\n\n\t/** Returns the current editor state, or `null` if no editor is registered. */\n\tgetState(): EditorState | null {\n\t\tconst editor: NotectlEditorComponent | null = this.editorRef();\n\t\tif (!editor) return null;\n\t\ttry {\n\t\t\treturn editor.getState();\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/** Dispatches a transaction on the registered editor. */\n\tdispatch(tr: Transaction): void {\n\t\tthis.editorRef()?.dispatch(tr);\n\t}\n}\n","/**\n * @notectl/angular — Angular integration for the notectl rich text editor.\n * @packageDocumentation\n */\n\n// --- Angular Bindings ---\nexport { NotectlEditorComponent } from './lib/notectl-editor.component';\nexport { NotectlValueAccessorDirective } from './lib/value-accessor.directive';\nexport { NotectlEditorService } from './lib/notectl-editor.service';\n\n// --- Provider Function ---\nexport {\n\tprovideNotectl,\n\ttype NotectlProviderOptions,\n} from './lib/tokens';\n\n// --- Injection Tokens ---\nexport {\n\tNOTECTL_DEFAULT_CONFIG,\n\tNOTECTL_CONTENT_FORMAT,\n\ttype ContentFormat,\n} from './lib/tokens';\n\n// --- Angular-specific Types ---\nexport type { SelectionChangeEvent } from './lib/types';\n\n// --- Re-exports from @notectl/core (convenience) ---\n\n// Model types\nexport type {\n\tDocument,\n\tBlockNode,\n\tTextNode,\n\tInlineNode,\n\tMark,\n\tBlockAttrs,\n} from '@notectl/core';\n\n// Selection types\nexport type { EditorSelection, Position, Selection } from '@notectl/core';\n\n// State types\nexport type {\n\tEditorState,\n\tTransaction,\n\tTransactionMetadata,\n\tStateChangeEvent,\n} from '@notectl/core';\n\n// Plugin types\nexport type { Plugin, PluginConfig, PluginContext } from '@notectl/core';\n\n// Theme types\nexport type { Theme, PartialTheme, ThemePrimitives } from '@notectl/core';\nexport { ThemePreset, LIGHT_THEME, DARK_THEME, createTheme } from '@notectl/core';\n\n// Editor config\nexport type {\n\tNotectlEditorConfig,\n\tTextFormattingConfig,\n} from '@notectl/core';\n\n// Plugin config types\nexport type { FontDefinition } from '@notectl/core';\n\n// Starter fonts\n/** @deprecated Import from '@notectl/core/fonts' instead. */\nexport { STARTER_FONTS } from '@notectl/core/fonts';\n\n// Plugins (tree-shakable re-exports)\nexport {\n\tTextFormattingPlugin,\n\tHeadingPlugin,\n\tListPlugin,\n\tLinkPlugin,\n\tBlockquotePlugin,\n\tCodeBlockPlugin,\n\tTablePlugin,\n\tImagePlugin,\n\tHorizontalRulePlugin,\n\tHardBreakPlugin,\n\tStrikethroughPlugin,\n\tHighlightPlugin,\n\tTextColorPlugin,\n\tFontPlugin,\n\tFontSizePlugin,\n\tAlignmentPlugin,\n\tSuperSubPlugin,\n\tToolbarPlugin,\n} from '@notectl/core';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { ComponentFixture } from '@angular/core/testing';
2
+ import { NotectlEditorComponent } from '@notectl/angular';
3
+
4
+ /**
5
+ * Test harness for `NotectlEditorComponent` in consumer tests.
6
+ *
7
+ * Wraps a `ComponentFixture` and provides convenience methods
8
+ * for common test operations.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const fixture = TestBed.createComponent(NotectlEditorComponent);
13
+ * const harness = new NotectlTestHarness(fixture);
14
+ * await harness.whenReady();
15
+ * harness.setHTML('<p>Hello</p>');
16
+ * expect(harness.getText()).toBe('Hello');
17
+ * ```
18
+ */
19
+ declare class NotectlTestHarness {
20
+ private readonly fixture;
21
+ constructor(fixture: ComponentFixture<NotectlEditorComponent>);
22
+ /** Returns the component instance. */
23
+ get component(): NotectlEditorComponent;
24
+ /** Waits for the editor to be ready and triggers change detection. */
25
+ whenReady(): Promise<void>;
26
+ /** Executes a command and triggers change detection. */
27
+ executeCommand(name: string): boolean;
28
+ /** Sets HTML content and triggers change detection. */
29
+ setHTML(html: string): void;
30
+ /** Returns the current HTML content. */
31
+ getHTML(): string;
32
+ /** Returns the current plain text content. */
33
+ getText(): string;
34
+ /** Returns whether the editor is empty. */
35
+ get isEmpty(): boolean;
36
+ }
37
+
38
+ export { NotectlTestHarness };
@@ -0,0 +1,241 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { ModelSignal, InjectionToken, EnvironmentProviders } from '@angular/core';
3
+ import { EditorSelection, Plugin, TextFormattingConfig, ThemePreset, Theme, Document, StateChangeEvent, EditorState, NotectlEditor, PluginConfig, Transaction, NotectlEditorConfig } from '@notectl/core';
4
+ export { AlignmentPlugin, BlockAttrs, BlockNode, BlockquotePlugin, CodeBlockPlugin, DARK_THEME, Document, EditorSelection, EditorState, FontDefinition, FontPlugin, FontSizePlugin, HardBreakPlugin, HeadingPlugin, HighlightPlugin, HorizontalRulePlugin, ImagePlugin, InlineNode, LIGHT_THEME, LinkPlugin, ListPlugin, Mark, NotectlEditorConfig, PartialTheme, Plugin, PluginConfig, PluginContext, Position, Selection, StateChangeEvent, StrikethroughPlugin, SuperSubPlugin, TablePlugin, TextColorPlugin, TextFormattingConfig, TextFormattingPlugin, TextNode, Theme, ThemePreset, ThemePrimitives, ToolbarPlugin, Transaction, TransactionMetadata, createTheme } from '@notectl/core';
5
+ import { ControlValueAccessor } from '@angular/forms';
6
+ import { Observable } from 'rxjs';
7
+ export { STARTER_FONTS } from '@notectl/core/fonts';
8
+
9
+ /** Event payload emitted on selection changes. */
10
+ interface SelectionChangeEvent {
11
+ readonly selection: EditorSelection;
12
+ }
13
+
14
+ /**
15
+ * Angular standalone component wrapping the `<ntl-editor>` Web Component.
16
+ *
17
+ * Uses `afterNextRender()` for SSR-safe initialization and `effect()` for
18
+ * reactive input tracking — no lifecycle interfaces needed.
19
+ *
20
+ * @example
21
+ * ```html
22
+ * <!-- Basic usage -->
23
+ * <ntl-editor [toolbar]="toolbar" [plugins]="plugins" />
24
+ *
25
+ * <!-- Two-way content binding -->
26
+ * <ntl-editor [(content)]="myDocument" [toolbar]="toolbar" />
27
+ *
28
+ * <!-- Reactive forms -->
29
+ * <ntl-editor [formControl]="editorControl" [toolbar]="toolbar" />
30
+ * ```
31
+ */
32
+ declare class NotectlEditorComponent {
33
+ private readonly destroyRef;
34
+ private readonly defaultConfig;
35
+ readonly plugins: _angular_core.InputSignal<Plugin<Record<string, unknown>>[]>;
36
+ readonly toolbar: _angular_core.InputSignal<readonly (readonly Plugin<Record<string, unknown>>[])[]>;
37
+ readonly features: _angular_core.InputSignal<Partial<TextFormattingConfig>>;
38
+ readonly placeholder: _angular_core.InputSignal<string>;
39
+ readonly readonlyMode: _angular_core.InputSignal<boolean>;
40
+ readonly autofocus: _angular_core.InputSignal<boolean>;
41
+ readonly maxHistoryDepth: _angular_core.InputSignal<number>;
42
+ readonly theme: _angular_core.InputSignal<ThemePreset | Theme>;
43
+ /**
44
+ * Two-way bindable document content.
45
+ *
46
+ * `undefined` means no external content was provided — the editor manages
47
+ * its own state. Once the editor emits changes, the model updates to the
48
+ * current `Document`.
49
+ *
50
+ * @example
51
+ * ```html
52
+ * <ntl-editor [(content)]="myDocument" />
53
+ * ```
54
+ */
55
+ readonly content: ModelSignal<Document | undefined>;
56
+ readonly stateChange: _angular_core.OutputEmitterRef<StateChangeEvent>;
57
+ readonly selectionChange: _angular_core.OutputEmitterRef<SelectionChangeEvent>;
58
+ readonly editorFocus: _angular_core.OutputEmitterRef<void>;
59
+ readonly editorBlur: _angular_core.OutputEmitterRef<void>;
60
+ readonly ready: _angular_core.OutputEmitterRef<void>;
61
+ readonly editorState: _angular_core.WritableSignal<EditorState>;
62
+ readonly isEmpty: _angular_core.Signal<boolean>;
63
+ private readonly hostRef;
64
+ private editorRef;
65
+ private readyResolve;
66
+ private readonly readyPromise;
67
+ private readonly initialized;
68
+ /** Tracks the last document set from within the editor to prevent feedback loops. */
69
+ private lastEditorDoc;
70
+ constructor();
71
+ /** Returns the document as JSON. */
72
+ getJSON(): Document;
73
+ /** Sets the document from JSON. */
74
+ setJSON(doc: Document): void;
75
+ /** Returns sanitized HTML representation of the document. */
76
+ getHTML(): string;
77
+ /** Sets content from HTML (sanitized). */
78
+ setHTML(html: string): void;
79
+ /** Returns plain text content. */
80
+ getText(): string;
81
+ /** Proxy to the Web Component's `commands` object. */
82
+ get commands(): NotectlEditor['commands'];
83
+ /** Proxy to the Web Component's `can()` capability checker. */
84
+ can(): ReturnType<NotectlEditor['can']>;
85
+ /** Executes a named command registered by a plugin. */
86
+ executeCommand(name: string): boolean;
87
+ /** Configures a plugin at runtime. */
88
+ configurePlugin(pluginId: string, config: PluginConfig): void;
89
+ /** Dispatches a transaction. */
90
+ dispatch(tr: Transaction): void;
91
+ /** Returns the current editor state. */
92
+ getState(): EditorState;
93
+ /** Changes the theme at runtime. */
94
+ setTheme(theme: ThemePreset | Theme): void;
95
+ /** Returns the current theme setting. */
96
+ getTheme(): ThemePreset | Theme;
97
+ /** Sets the readonly state programmatically (used by ControlValueAccessor). */
98
+ setReadonly(readonlyState: boolean): void;
99
+ /** Resolves when the editor is ready. */
100
+ whenReady(): Promise<void>;
101
+ private initEditor;
102
+ private buildConfig;
103
+ /** Syncs editor document changes to the content model without feedback loops. */
104
+ private syncContentModel;
105
+ private destroyEditor;
106
+ private requireEditor;
107
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NotectlEditorComponent, never>;
108
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NotectlEditorComponent, "ntl-editor", never, { "plugins": { "alias": "plugins"; "required": false; "isSignal": true; }; "toolbar": { "alias": "toolbar"; "required": false; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "readonlyMode": { "alias": "readonlyMode"; "required": false; "isSignal": true; }; "autofocus": { "alias": "autofocus"; "required": false; "isSignal": true; }; "maxHistoryDepth": { "alias": "maxHistoryDepth"; "required": false; "isSignal": true; }; "theme": { "alias": "theme"; "required": false; "isSignal": true; }; "content": { "alias": "content"; "required": false; "isSignal": true; }; }, { "content": "contentChange"; "stateChange": "stateChange"; "selectionChange": "selectionChange"; "editorFocus": "editorFocus"; "editorBlur": "editorBlur"; "ready": "ready"; }, never, never, true, never>;
109
+ }
110
+
111
+ type OnChangeFn = (value: Document | string | null) => void;
112
+ type OnTouchedFn = () => void;
113
+ /**
114
+ * `ControlValueAccessor` directive for Angular forms integration.
115
+ *
116
+ * Supports Reactive Forms (`formControl`, `formControlName`) and
117
+ * template-driven forms (`ngModel`).
118
+ *
119
+ * The content format is configurable via `provideNotectl({ contentFormat })` or
120
+ * the `NOTECTL_CONTENT_FORMAT` injection token:
121
+ * - `'json'` (default) — form value is a `Document` object
122
+ * - `'html'` — form value is a sanitized HTML string
123
+ * - `'text'` — form value is a plain text string
124
+ */
125
+ declare class NotectlValueAccessorDirective implements ControlValueAccessor {
126
+ private readonly editor;
127
+ private readonly destroyRef;
128
+ private readonly format;
129
+ private onChange;
130
+ private onTouched;
131
+ private pendingValue;
132
+ private suppressEmit;
133
+ private readonly stateChangeSub;
134
+ private readonly blurSub;
135
+ constructor();
136
+ writeValue(value: Document | string | null): void;
137
+ registerOnChange(fn: OnChangeFn): void;
138
+ registerOnTouched(fn: OnTouchedFn): void;
139
+ setDisabledState(isDisabled: boolean): void;
140
+ private writeValueToEditor;
141
+ private readValue;
142
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NotectlValueAccessorDirective, never>;
143
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NotectlValueAccessorDirective, "ntl-editor[formControl], ntl-editor[formControlName], ntl-editor[ngModel]", never, {}, {}, never, never, true, never>;
144
+ }
145
+
146
+ /**
147
+ * Optional injectable service for programmatic editor access via DI.
148
+ *
149
+ * Useful when a toolbar or other component needs to interact with
150
+ * the editor without a direct template reference.
151
+ *
152
+ * The service must be provided at a shared injector level and
153
+ * connected to the editor component via `register()`.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * @Component({
158
+ * providers: [NotectlEditorService],
159
+ * template: `
160
+ * <app-toolbar />
161
+ * <ntl-editor #editor [plugins]="plugins" />
162
+ * `,
163
+ * })
164
+ * export class EditorPage {
165
+ * private readonly editor = viewChild.required<NotectlEditorComponent>('editor');
166
+ * private readonly service = inject(NotectlEditorService);
167
+ *
168
+ * constructor() {
169
+ * afterNextRender(() => {
170
+ * this.service.register(this.editor());
171
+ * });
172
+ * }
173
+ * }
174
+ * ```
175
+ */
176
+ declare class NotectlEditorService {
177
+ private readonly destroyRef;
178
+ private readonly editorRef;
179
+ private readonly stateChangeSubject;
180
+ /** Whether an editor is currently registered with this service. */
181
+ readonly hasEditor: _angular_core.Signal<boolean>;
182
+ /** Observable stream of state change events from the editor. */
183
+ readonly stateChanges$: Observable<StateChangeEvent>;
184
+ private stateChangeSub;
185
+ constructor();
186
+ /** Registers an editor component with this service. */
187
+ register(editor: NotectlEditorComponent): void;
188
+ /** Unregisters the current editor from this service. */
189
+ unregister(): void;
190
+ /** Executes a named command on the registered editor. */
191
+ executeCommand(name: string): boolean;
192
+ /** Returns the current editor state, or `null` if no editor is registered. */
193
+ getState(): EditorState | null;
194
+ /** Dispatches a transaction on the registered editor. */
195
+ dispatch(tr: Transaction): void;
196
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<NotectlEditorService, never>;
197
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<NotectlEditorService>;
198
+ }
199
+
200
+ /**
201
+ * Default configuration applied to all `<notectl-editor>` instances within
202
+ * the injector scope. Component-level inputs override these defaults.
203
+ */
204
+ declare const NOTECTL_DEFAULT_CONFIG: InjectionToken<Partial<NotectlEditorConfig>>;
205
+ /**
206
+ * Content format used by the `ControlValueAccessor` for forms integration.
207
+ * Determines how editor content is serialized when reading/writing form values.
208
+ *
209
+ * - `'json'` — `Document` objects (default)
210
+ * - `'html'` — sanitized HTML strings
211
+ * - `'text'` — plain text strings
212
+ */
213
+ declare const NOTECTL_CONTENT_FORMAT: InjectionToken<ContentFormat>;
214
+ /** Supported content serialization formats for forms integration. */
215
+ type ContentFormat = 'json' | 'html' | 'text';
216
+ /** Options for `provideNotectl()`. */
217
+ interface NotectlProviderOptions {
218
+ /** Default editor configuration applied to all instances. */
219
+ readonly config?: Partial<NotectlEditorConfig>;
220
+ /** Content serialization format for forms integration. Defaults to `'json'`. */
221
+ readonly contentFormat?: ContentFormat;
222
+ }
223
+ /**
224
+ * Configures the notectl editor at the environment injector level.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * bootstrapApplication(App, {
229
+ * providers: [
230
+ * provideNotectl({
231
+ * config: { theme: ThemePreset.Light, placeholder: 'Start typing...' },
232
+ * contentFormat: 'json',
233
+ * }),
234
+ * ],
235
+ * });
236
+ * ```
237
+ */
238
+ declare function provideNotectl(options?: NotectlProviderOptions): EnvironmentProviders;
239
+
240
+ export { NOTECTL_CONTENT_FORMAT, NOTECTL_DEFAULT_CONFIG, NotectlEditorComponent, NotectlEditorService, NotectlValueAccessorDirective, provideNotectl };
241
+ export type { ContentFormat, NotectlProviderOptions, SelectionChangeEvent };
package/package.json CHANGED
@@ -1,55 +1,58 @@
1
1
  {
2
- "name": "@notectl/angular",
3
- "version": "1.0.3",
4
- "description": "Angular integration for the notectl rich text editor. Provides a standalone component, reactive forms support, and DI service.",
5
- "license": "MIT",
6
- "type": "module",
7
- "module": "fesm2022/notectl-angular.mjs",
8
- "typings": "types/notectl-angular.d.ts",
9
- "sideEffects": false,
10
- "files": [
11
- "dist",
12
- "README.md"
13
- ],
14
- "keywords": [
15
- "angular",
16
- "rich-text-editor",
17
- "notectl",
18
- "web-component",
19
- "editor",
20
- "wysiwyg",
21
- "reactive-forms",
22
- "signals"
23
- ],
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/Samyssmile/notectl.git",
27
- "directory": "packages/angular"
28
- },
29
- "homepage": "https://github.com/Samyssmile/notectl",
30
- "bugs": {
31
- "url": "https://github.com/Samyssmile/notectl/issues"
32
- },
33
- "dependencies": {
34
- "tslib": "^2.3.0"
35
- },
36
- "peerDependencies": {
37
- "@angular/core": ">=21.0.0",
38
- "@angular/forms": ">=21.0.0",
39
- "@notectl/core": "^1.0.0",
40
- "rxjs": ">=7.0.0"
41
- },
42
- "exports": {
43
- "./package.json": {
44
- "default": "./package.json"
45
- },
46
- ".": {
47
- "types": "./types/notectl-angular.d.ts",
48
- "default": "./fesm2022/notectl-angular.mjs"
49
- },
50
- "./testing": {
51
- "types": "./types/notectl-angular-testing.d.ts",
52
- "default": "./fesm2022/notectl-angular-testing.mjs"
53
- }
54
- }
55
- }
2
+ "name": "@notectl/angular",
3
+ "version": "1.0.5",
4
+ "description": "Angular integration for the notectl rich text editor. Provides a standalone component, reactive forms support, and DI service.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "module": "./dist/fesm2022/notectl-angular.mjs",
8
+ "typings": "./dist/types/notectl-angular.d.ts",
9
+ "sideEffects": false,
10
+ "files": ["dist", "README.md"],
11
+ "keywords": [
12
+ "angular",
13
+ "rich-text-editor",
14
+ "notectl",
15
+ "web-component",
16
+ "editor",
17
+ "wysiwyg",
18
+ "reactive-forms",
19
+ "signals"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/Samyssmile/notectl.git",
24
+ "directory": "packages/angular"
25
+ },
26
+ "homepage": "https://github.com/Samyssmile/notectl",
27
+ "bugs": {
28
+ "url": "https://github.com/Samyssmile/notectl/issues"
29
+ },
30
+ "scripts": {
31
+ "build": "ng-packagr -p ng-package.json",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "dependencies": {
37
+ "tslib": "^2.3.0"
38
+ },
39
+ "peerDependencies": {
40
+ "@angular/core": ">=21.0.0",
41
+ "@angular/forms": ">=21.0.0",
42
+ "@notectl/core": "^1.0.0",
43
+ "rxjs": ">=7.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@angular/compiler": "^21.0.0",
47
+ "@angular/compiler-cli": "^21.0.0",
48
+ "@angular/core": "^21.0.0",
49
+ "@angular/forms": "^21.0.0",
50
+ "@angular/platform-browser": "^21.0.0",
51
+ "@angular/platform-browser-dynamic": "^21.0.0",
52
+ "@notectl/core": "workspace:*",
53
+ "ng-packagr": "^21.0.0",
54
+ "rxjs": "~7.8.0",
55
+ "vite": "^6.0.0",
56
+ "vitest": "^4.0.0"
57
+ }
58
+ }