diff --git a/API.md b/API.md
index fa6dcb5..cd6972f 100644
--- a/API.md
+++ b/API.md
@@ -100,11 +100,13 @@ export interface AteEditorConfig {
## 📤 Outputs
-| Output | Type | Description |
-| ---------------- | --------------------------------------- | --------------------------------- |
-| `contentChange` | `string` | Emitted when content changes |
-| `editorCreated` | `Editor` | Emitted when editor is created |
-| `editorUpdate` | `{ editor: Editor; transaction: any }` | Emitted on every editor update |
-| `editorFocus` | `{ editor: Editor; event: FocusEvent }` | Emitted when editor gains focus |
-| `editorBlur` | `{ editor: Editor; event: FocusEvent }` | Emitted when editor loses focus |
-| `editableChange` | `boolean` | Emitted when edit mode is toggled |
+| Output | Type | Description |
+| ----------------- | ------------------------------------------ | --------------------------------- |
+| `contentChange` | `string` | Emitted when content changes |
+| `editorCreated` | `Editor` | Emitted when editor is created |
+| `editorUpdate` | `{ editor: Editor; transaction: any }` | Emitted on every editor update |
+| `editorFocus` | `{ editor: Editor; event: FocusEvent }` | Emitted when editor gains focus |
+| `editorBlur` | `{ editor: Editor; event: FocusEvent }` | Emitted when editor loses focus |
+| `editableChange` | `boolean` | Emitted when edit mode is toggled |
+| `imageUploaded` | `AteImageUploadResult` | Emitted when an image is uploaded |
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb16f67..12657f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to `@flogeez/angular-tiptap-editor` will be documented in th
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), with the exception that the major version is specifically aligned with the major version of [Tiptap](https://tiptap.dev).
+## [3.2.1] - 2026-06-18
+
+### Added
+
+- **Image Upload Hook**: Introduced `imageUploaded` output (emits `AteImageUploadResult` when an image is successfully uploaded and inserted).
+
## [3.2.0] - 2026-06-09
### Added
diff --git a/angular.json b/angular.json
index a5f5f08..3ee05ea 100644
--- a/angular.json
+++ b/angular.json
@@ -29,15 +29,11 @@
}
},
"options": {
- "assets": [
- "src/favicon.ico"
- ],
+ "assets": ["src/favicon.ico"],
"index": "src/index.html",
"browser": "src/main.ts",
"outputPath": "demo-dist",
- "polyfills": [
- "zone.js"
- ],
+ "polyfills": ["zone.js"],
"scripts": [],
"styles": [
"node_modules/@fontsource/material-symbols-outlined/index.css",
@@ -62,10 +58,7 @@
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
- "lintFilePatterns": [
- "src/**/*.ts",
- "src/**/*.html"
- ]
+ "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
},
@@ -100,10 +93,7 @@
"builder": "@angular/build:karma",
"options": {
"tsConfig": "projects/angular-tiptap-editor/tsconfig.spec.json",
- "polyfills": [
- "zone.js",
- "zone.js/testing"
- ]
+ "polyfills": ["zone.js", "zone.js/testing"]
}
},
"lint": {
@@ -120,4 +110,4 @@
}
},
"version": 1
-}
\ No newline at end of file
+}
diff --git a/projects/angular-tiptap-editor/package.json b/projects/angular-tiptap-editor/package.json
index d682d2e..f92aeb2 100644
--- a/projects/angular-tiptap-editor/package.json
+++ b/projects/angular-tiptap-editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@flogeez/angular-tiptap-editor",
- "version": "3.2.0",
+ "version": "3.2.1",
"description": "A modern, customizable rich-text editor for Angular (18+), built with Tiptap and featuring complete internationalization support",
"keywords": [
"angular",
@@ -45,4 +45,4 @@
"tslib": "^2.3.0"
},
"sideEffects": false
-}
\ No newline at end of file
+}
diff --git a/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-bubble-menu.ts b/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-bubble-menu.ts
index 6df29ed..e76f5dc 100644
--- a/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-bubble-menu.ts
+++ b/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-bubble-menu.ts
@@ -41,15 +41,18 @@ export abstract class AteBaseBubbleMenu implements AfterViewInit, OnDestroy {
constructor() {
// Effect to reactively update the menu when editor state
// or toolbar interaction changes.
- effect(() => {
- this.state();
- this.isToolbarInteracting();
- // Also react to link/color edit modes to hide when sub-menus activate
- this.editorCommands.linkEditMode();
- this.editorCommands.colorEditMode();
-
- this.updateMenu();
- }, { allowSignalWrites: true });
+ effect(
+ () => {
+ this.state();
+ this.isToolbarInteracting();
+ // Also react to link/color edit modes to hide when sub-menus activate
+ this.editorCommands.linkEditMode();
+ this.editorCommands.colorEditMode();
+
+ this.updateMenu();
+ },
+ { allowSignalWrites: true }
+ );
}
ngAfterViewInit() {
@@ -72,7 +75,9 @@ export abstract class AteBaseBubbleMenu implements AfterViewInit, OnDestroy {
*/
protected initTippy() {
const nativeElement = this.menuRef().nativeElement;
- if (!nativeElement) {return;}
+ if (!nativeElement) {
+ return;
+ }
const ed = this.editor();
if (this.tippyInstance) {
diff --git a/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-sub-bubble-menu.ts b/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-sub-bubble-menu.ts
index 6f53e86..0cf7206 100644
--- a/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-sub-bubble-menu.ts
+++ b/projects/angular-tiptap-editor/src/lib/components/bubble-menus/base/ate-base-sub-bubble-menu.ts
@@ -37,13 +37,16 @@ export abstract class AteBaseSubBubbleMenu implements AfterViewInit, OnDestroy {
constructor() {
// Reactive effect for menu updates (re-positioning)
- effect(() => {
- // Monitor editor state and specific sub-menu states
- this.state();
- this.onStateChange();
+ effect(
+ () => {
+ // Monitor editor state and specific sub-menu states
+ this.state();
+ this.onStateChange();
- this.updateMenu();
- }, { allowSignalWrites: true });
+ this.updateMenu();
+ },
+ { allowSignalWrites: true }
+ );
}
ngAfterViewInit() {
@@ -65,7 +68,9 @@ export abstract class AteBaseSubBubbleMenu implements AfterViewInit, OnDestroy {
*/
protected initTippy() {
const nativeElement = this.menuRef().nativeElement;
- if (!nativeElement) {return;}
+ if (!nativeElement) {
+ return;
+ }
const ed = this.editor();
if (this.tippyInstance) {
diff --git a/projects/angular-tiptap-editor/src/lib/components/bubble-menus/link/ate-link-bubble-menu.component.ts b/projects/angular-tiptap-editor/src/lib/components/bubble-menus/link/ate-link-bubble-menu.component.ts
index 21e5d49..2e45768 100644
--- a/projects/angular-tiptap-editor/src/lib/components/bubble-menus/link/ate-link-bubble-menu.component.ts
+++ b/projects/angular-tiptap-editor/src/lib/components/bubble-menus/link/ate-link-bubble-menu.component.ts
@@ -131,18 +131,21 @@ export class AteLinkBubbleMenuComponent extends AteBaseSubBubbleMenu {
super();
// Reactive effect for URL sync
- effect(() => {
- const state = this.state();
- const isInteracting = this.linkSvc.isInteracting();
- const currentLinkHref = state.marks.linkHref || "";
-
- // SYNC LOGIC:
- // If we are NOT currently typing (interacting),
- // always keep the input in sync with the current editor selection.
- if (!isInteracting) {
- this.editUrl.set(currentLinkHref);
- }
- }, { allowSignalWrites: true });
+ effect(
+ () => {
+ const state = this.state();
+ const isInteracting = this.linkSvc.isInteracting();
+ const currentLinkHref = state.marks.linkHref || "";
+
+ // SYNC LOGIC:
+ // If we are NOT currently typing (interacting),
+ // always keep the input in sync with the current editor selection.
+ if (!isInteracting) {
+ this.editUrl.set(currentLinkHref);
+ }
+ },
+ { allowSignalWrites: true }
+ );
}
protected override onStateChange() {
diff --git a/projects/angular-tiptap-editor/src/lib/components/editor/angular-tiptap-editor.component.ts b/projects/angular-tiptap-editor/src/lib/components/editor/angular-tiptap-editor.component.ts
index 24151cc..85a7bed 100644
--- a/projects/angular-tiptap-editor/src/lib/components/editor/angular-tiptap-editor.component.ts
+++ b/projects/angular-tiptap-editor/src/lib/components/editor/angular-tiptap-editor.component.ts
@@ -93,7 +93,11 @@ import {
ATE_DEFAULT_CONFIG,
} from "../../config/ate-editor.config";
import { concat, defer, Observable, of, tap } from "rxjs";
-import { AteImageUploadHandler, AteImageUploadOptions } from "../../models/ate-image.model";
+import {
+ AteImageUploadHandler,
+ AteImageUploadOptions,
+ AteImageUploadResult,
+} from "../../models/ate-image.model";
// Slash commands configuration is handled dynamically via slashCommandsConfigComputed
@@ -138,7 +142,13 @@ import { AteImageUploadHandler, AteImageUploadOptions } from "../../models/ate-i
AteEditToggleComponent,
AteBlockControlsComponent,
],
- providers: [AteEditorCommandsService, AteImageService, AteColorPickerService, AteLinkService, AteExportService],
+ providers: [
+ AteEditorCommandsService,
+ AteImageService,
+ AteColorPickerService,
+ AteLinkService,
+ AteExportService,
+ ],
template: `
@@ -1067,6 +1077,7 @@ export class AngularTiptapEditorComponent implements AfterViewInit, OnDestroy {
editorFocus = output<{ editor: Editor; event: FocusEvent }>();
editorBlur = output<{ editor: Editor; event: FocusEvent }>();
editableChange = output
();
+ imageUploaded = output();
// ViewChild with signal
editorElement = viewChild.required("editorElement");
@@ -1384,109 +1395,131 @@ export class AngularTiptapEditorComponent implements AfterViewInit, OnDestroy {
constructor() {
// Effect to update editor content (with anti-echo)
- effect(() => {
- const content = this.content(); // Sole reactive dependency
+ effect(
+ () => {
+ const content = this.content(); // Sole reactive dependency
- untracked(() => {
- const editor = this.editor();
- const hasFormControl = !!(this.ngControl as { control?: unknown })?.control;
+ untracked(() => {
+ const editor = this.editor();
+ const hasFormControl = !!(this.ngControl as { control?: unknown })?.control;
- if (!editor || content === undefined) {
- return;
- }
+ if (!editor || content === undefined) {
+ return;
+ }
- // Anti-écho : on ignore ce qu'on vient d'émettre nous-mêmes
- if (content === this.lastEmittedHtml) {
- return;
- }
+ // Anti-écho : on ignore ce qu'on vient d'émettre nous-mêmes
+ if (content === this.lastEmittedHtml) {
+ return;
+ }
- // Double sécurité : on vérifie le contenu actuel de l'éditeur
- if (content === editor.getHTML()) {
- return;
- }
+ // Double sécurité : on vérifie le contenu actuel de l'éditeur
+ if (content === editor.getHTML()) {
+ return;
+ }
- // Do not overwrite content if we have a FormControl and content is empty
- if (hasFormControl && !content) {
- return;
- }
+ // Do not overwrite content if we have a FormControl and content is empty
+ if (hasFormControl && !content) {
+ return;
+ }
- editor.commands.setContent(content, { emitUpdate: false });
- });
- }, { allowSignalWrites: true });
+ editor.commands.setContent(content, { emitUpdate: false });
+ });
+ },
+ { allowSignalWrites: true }
+ );
// Effect to update height properties
- effect(() => {
- const minHeight = this.finalMinHeight();
- const height = this.finalHeight();
- const maxHeight = this.finalMaxHeight();
- const element = this.editorElement()?.nativeElement;
-
- // Automatically calculate if scroll is needed
- const needsScroll = height !== undefined || maxHeight !== undefined;
-
- if (element) {
- element.style.setProperty("--editor-min-height", minHeight ?? "auto");
- element.style.setProperty("--editor-height", height ?? "auto");
- element.style.setProperty("--editor-max-height", maxHeight ?? "none");
- element.style.setProperty("--editor-overflow", needsScroll ? "auto" : "visible");
- }
- }, { allowSignalWrites: true });
+ effect(
+ () => {
+ const minHeight = this.finalMinHeight();
+ const height = this.finalHeight();
+ const maxHeight = this.finalMaxHeight();
+ const element = this.editorElement()?.nativeElement;
+
+ // Automatically calculate if scroll is needed
+ const needsScroll = height !== undefined || maxHeight !== undefined;
+
+ if (element) {
+ element.style.setProperty("--editor-min-height", minHeight ?? "auto");
+ element.style.setProperty("--editor-height", height ?? "auto");
+ element.style.setProperty("--editor-max-height", maxHeight ?? "none");
+ element.style.setProperty("--editor-overflow", needsScroll ? "auto" : "visible");
+ }
+ },
+ { allowSignalWrites: true }
+ );
// Effect to monitor editability changes
- effect(() => {
- const currentEditor = this.editor();
- // An editor is "editable" if it's not disabled and editable mode is ON
- const isEditable = this.finalEditable() && !this.mergedDisabled();
- // An editor is "readonly" if it's explicitly non-editable and not disabled
- // const isReadOnly = !this.finalEditable() && !this.mergedDisabled(); // Unused variable
-
- if (currentEditor) {
- this.editorCommandsService.setEditable(currentEditor, isEditable);
- }
- }, { allowSignalWrites: true });
+ effect(
+ () => {
+ const currentEditor = this.editor();
+ // An editor is "editable" if it's not disabled and editable mode is ON
+ const isEditable = this.finalEditable() && !this.mergedDisabled();
+ // An editor is "readonly" if it's explicitly non-editable and not disabled
+ // const isReadOnly = !this.finalEditable() && !this.mergedDisabled(); // Unused variable
+
+ if (currentEditor) {
+ this.editorCommandsService.setEditable(currentEditor, isEditable);
+ }
+ },
+ { allowSignalWrites: true }
+ );
// Effect to synchronize image upload handler with the service
- effect(() => {
- const handler = this.finalImageUploadHandler();
- this.editorCommandsService.uploadHandler = handler || null;
- }, { allowSignalWrites: true });
+ effect(
+ () => {
+ const handler = this.finalImageUploadHandler();
+ this.editorCommandsService.uploadHandler = handler || null;
+ },
+ { allowSignalWrites: true }
+ );
// Effect to update character count limit dynamically
- effect(() => {
- const editor = this.editor();
- const limit = this.finalMaxCharacters();
+ effect(
+ () => {
+ const editor = this.editor();
+ const limit = this.finalMaxCharacters();
- if (editor && editor.extensionManager) {
- const characterCountExtension = editor.extensionManager.extensions.find(
- ext => ext.name === "characterCount"
- );
+ if (editor && editor.extensionManager) {
+ const characterCountExtension = editor.extensionManager.extensions.find(
+ ext => ext.name === "characterCount"
+ );
- if (characterCountExtension) {
- characterCountExtension.options.limit = limit;
+ if (characterCountExtension) {
+ characterCountExtension.options.limit = limit;
+ }
}
- }
- }, { allowSignalWrites: true });
+ },
+ { allowSignalWrites: true }
+ );
// Effect to re-initialize editor when technical configuration changes
- effect(() => {
- // Monitor technical dependencies
- this.finalTiptapExtensions();
- this.finalTiptapOptions();
- this.finalAngularNodesConfig();
- this.finalBlockControls();
-
- untracked(() => {
- // Only if already initialized (post AfterViewInit)
- if (this.editorFullyInitialized()) {
- const currentEditor = this.editor();
- if (currentEditor) {
- currentEditor.destroy();
- this._editorFullyInitialized.set(false);
- this.initEditor();
+ effect(
+ () => {
+ // Monitor technical dependencies
+ this.finalTiptapExtensions();
+ this.finalTiptapOptions();
+ this.finalAngularNodesConfig();
+ this.finalBlockControls();
+
+ untracked(() => {
+ // Only if already initialized (post AfterViewInit)
+ if (this.editorFullyInitialized()) {
+ const currentEditor = this.editor();
+ if (currentEditor) {
+ currentEditor.destroy();
+ this._editorFullyInitialized.set(false);
+ this.initEditor();
+ }
}
- }
- });
- }, { allowSignalWrites: true });
+ });
+ },
+ { allowSignalWrites: true }
+ );
+
+ this.editorCommandsService.onImageUploaded = result => {
+ this.imageUploaded.emit(result);
+ };
}
ngAfterViewInit() {
diff --git a/projects/angular-tiptap-editor/src/lib/components/slash-commands/ate-slash-commands.component.ts b/projects/angular-tiptap-editor/src/lib/components/slash-commands/ate-slash-commands.component.ts
index 03e52db..fe30a00 100644
--- a/projects/angular-tiptap-editor/src/lib/components/slash-commands/ate-slash-commands.component.ts
+++ b/projects/angular-tiptap-editor/src/lib/components/slash-commands/ate-slash-commands.component.ts
@@ -214,29 +214,32 @@ export class AteSlashCommandsComponent implements AfterViewInit, OnDestroy {
});
constructor() {
- effect(() => {
- const ed = this.editor();
- if (!ed) {
- return;
- }
+ effect(
+ () => {
+ const ed = this.editor();
+ if (!ed) {
+ return;
+ }
- // Clean up old listeners
- ed.off("selectionUpdate", this.updateMenu);
- ed.off("transaction", this.updateMenu);
- ed.off("focus", this.updateMenu);
- ed.off("blur", this.handleBlur);
+ // Clean up old listeners
+ ed.off("selectionUpdate", this.updateMenu);
+ ed.off("transaction", this.updateMenu);
+ ed.off("focus", this.updateMenu);
+ ed.off("blur", this.handleBlur);
- // Add new listeners
- ed.on("selectionUpdate", this.updateMenu);
- ed.on("transaction", this.updateMenu);
- ed.on("focus", this.updateMenu);
- ed.on("blur", this.handleBlur);
+ // Add new listeners
+ ed.on("selectionUpdate", this.updateMenu);
+ ed.on("transaction", this.updateMenu);
+ ed.on("focus", this.updateMenu);
+ ed.on("blur", this.handleBlur);
- // Use ProseMirror plugin system to intercept keys
- this.addKeyboardPlugin(ed);
+ // Use ProseMirror plugin system to intercept keys
+ this.addKeyboardPlugin(ed);
- // updateMenu() will be called automatically when editor is ready
- }, { allowSignalWrites: true });
+ // updateMenu() will be called automatically when editor is ready
+ },
+ { allowSignalWrites: true }
+ );
}
ngAfterViewInit() {
@@ -260,7 +263,9 @@ export class AteSlashCommandsComponent implements AfterViewInit, OnDestroy {
private initTippy() {
const nativeElement = this.menuRef().nativeElement;
- if (!nativeElement) {return;}
+ if (!nativeElement) {
+ return;
+ }
if (this.tippyInstance) {
this.tippyInstance.destroy();
diff --git a/projects/angular-tiptap-editor/src/lib/models/ate-editor-ref.ts b/projects/angular-tiptap-editor/src/lib/models/ate-editor-ref.ts
index 84f58be..b74b09e 100644
--- a/projects/angular-tiptap-editor/src/lib/models/ate-editor-ref.ts
+++ b/projects/angular-tiptap-editor/src/lib/models/ate-editor-ref.ts
@@ -47,97 +47,135 @@ export class AteEditorRef {
toggleBold(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleBold(editor);}
+ if (editor) {
+ this.commandsService.toggleBold(editor);
+ }
}
toggleItalic(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleItalic(editor);}
+ if (editor) {
+ this.commandsService.toggleItalic(editor);
+ }
}
toggleStrike(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleStrike(editor);}
+ if (editor) {
+ this.commandsService.toggleStrike(editor);
+ }
}
toggleCode(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleCode(editor);}
+ if (editor) {
+ this.commandsService.toggleCode(editor);
+ }
}
toggleCodeBlock(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleCodeBlock(editor);}
+ if (editor) {
+ this.commandsService.toggleCodeBlock(editor);
+ }
}
toggleUnderline(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleUnderline(editor);}
+ if (editor) {
+ this.commandsService.toggleUnderline(editor);
+ }
}
toggleSuperscript(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleSuperscript(editor);}
+ if (editor) {
+ this.commandsService.toggleSuperscript(editor);
+ }
}
toggleSubscript(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleSubscript(editor);}
+ if (editor) {
+ this.commandsService.toggleSubscript(editor);
+ }
}
toggleHeading(level: 1 | 2 | 3): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleHeading(editor, level);}
+ if (editor) {
+ this.commandsService.toggleHeading(editor, level);
+ }
}
toggleBulletList(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleBulletList(editor);}
+ if (editor) {
+ this.commandsService.toggleBulletList(editor);
+ }
}
toggleOrderedList(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleOrderedList(editor);}
+ if (editor) {
+ this.commandsService.toggleOrderedList(editor);
+ }
}
toggleBlockquote(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleBlockquote(editor);}
+ if (editor) {
+ this.commandsService.toggleBlockquote(editor);
+ }
}
setTextAlign(alignment: "left" | "center" | "right" | "justify"): void {
const editor = this.editor;
- if (editor) {this.commandsService.setTextAlign(editor, alignment);}
+ if (editor) {
+ this.commandsService.setTextAlign(editor, alignment);
+ }
}
insertHorizontalRule(): void {
const editor = this.editor;
- if (editor) {this.commandsService.insertHorizontalRule(editor);}
+ if (editor) {
+ this.commandsService.insertHorizontalRule(editor);
+ }
}
insertImage(options: AteImageUploadOptions): void {
const editor = this.editor;
- if (editor) {this.commandsService.insertImage(editor, options);}
+ if (editor) {
+ this.commandsService.insertImage(editor, options);
+ }
}
uploadImage(file: File, options?: AteImageUploadOptions): void {
const editor = this.editor;
- if (editor) {this.commandsService.uploadImage(editor, file, options);}
+ if (editor) {
+ this.commandsService.uploadImage(editor, file, options);
+ }
}
toggleHighlight(color: string): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleHighlight(editor, color);}
+ if (editor) {
+ this.commandsService.toggleHighlight(editor, color);
+ }
}
undo(): void {
const editor = this.editor;
- if (editor) {this.commandsService.undo(editor);}
+ if (editor) {
+ this.commandsService.undo(editor);
+ }
}
redo(): void {
const editor = this.editor;
- if (editor) {this.commandsService.redo(editor);}
+ if (editor) {
+ this.commandsService.redo(editor);
+ }
}
// ============================================
@@ -146,62 +184,86 @@ export class AteEditorRef {
insertTable(rows?: number, cols?: number): void {
const editor = this.editor;
- if (editor) {this.commandsService.insertTable(editor, rows, cols);}
+ if (editor) {
+ this.commandsService.insertTable(editor, rows, cols);
+ }
}
addColumnBefore(): void {
const editor = this.editor;
- if (editor) {this.commandsService.addColumnBefore(editor);}
+ if (editor) {
+ this.commandsService.addColumnBefore(editor);
+ }
}
addColumnAfter(): void {
const editor = this.editor;
- if (editor) {this.commandsService.addColumnAfter(editor);}
+ if (editor) {
+ this.commandsService.addColumnAfter(editor);
+ }
}
deleteColumn(): void {
const editor = this.editor;
- if (editor) {this.commandsService.deleteColumn(editor);}
+ if (editor) {
+ this.commandsService.deleteColumn(editor);
+ }
}
addRowBefore(): void {
const editor = this.editor;
- if (editor) {this.commandsService.addRowBefore(editor);}
+ if (editor) {
+ this.commandsService.addRowBefore(editor);
+ }
}
addRowAfter(): void {
const editor = this.editor;
- if (editor) {this.commandsService.addRowAfter(editor);}
+ if (editor) {
+ this.commandsService.addRowAfter(editor);
+ }
}
deleteRow(): void {
const editor = this.editor;
- if (editor) {this.commandsService.deleteRow(editor);}
+ if (editor) {
+ this.commandsService.deleteRow(editor);
+ }
}
deleteTable(): void {
const editor = this.editor;
- if (editor) {this.commandsService.deleteTable(editor);}
+ if (editor) {
+ this.commandsService.deleteTable(editor);
+ }
}
mergeCells(): void {
const editor = this.editor;
- if (editor) {this.commandsService.mergeCells(editor);}
+ if (editor) {
+ this.commandsService.mergeCells(editor);
+ }
}
splitCell(): void {
const editor = this.editor;
- if (editor) {this.commandsService.splitCell(editor);}
+ if (editor) {
+ this.commandsService.splitCell(editor);
+ }
}
toggleHeaderColumn(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleHeaderColumn(editor);}
+ if (editor) {
+ this.commandsService.toggleHeaderColumn(editor);
+ }
}
toggleHeaderRow(): void {
const editor = this.editor;
- if (editor) {this.commandsService.toggleHeaderRow(editor);}
+ if (editor) {
+ this.commandsService.toggleHeaderRow(editor);
+ }
}
// ============================================
@@ -263,12 +325,16 @@ export class AteEditorRef {
clearContent(): void {
const editor = this.editor;
- if (editor) {this.commandsService.clearContent(editor);}
+ if (editor) {
+ this.commandsService.clearContent(editor);
+ }
}
setContent(content: string, emitUpdate = true): void {
const editor = this.editor;
- if (editor) {this.commandsService.setContent(editor, content, emitUpdate);}
+ if (editor) {
+ this.commandsService.setContent(editor, content, emitUpdate);
+ }
}
getContent(format: AteExportFormat): string {
@@ -304,11 +370,15 @@ export class AteEditorRef {
focus(): void {
const editor = this.editor;
- if (editor) {this.commandsService.focus(editor);}
+ if (editor) {
+ this.commandsService.focus(editor);
+ }
}
blur(): void {
const editor = this.editor;
- if (editor) {this.commandsService.blur(editor);}
+ if (editor) {
+ this.commandsService.blur(editor);
+ }
}
}
diff --git a/projects/angular-tiptap-editor/src/lib/services/ate-editor-commands.service.ts b/projects/angular-tiptap-editor/src/lib/services/ate-editor-commands.service.ts
index 6e43934..000fea0 100644
--- a/projects/angular-tiptap-editor/src/lib/services/ate-editor-commands.service.ts
+++ b/projects/angular-tiptap-editor/src/lib/services/ate-editor-commands.service.ts
@@ -5,7 +5,11 @@ import { AteColorPickerService } from "./ate-color-picker.service";
import { AteLinkService } from "./ate-link.service";
import { AteExportService, AteExportFormat, AteExportOptions } from "./ate-export.service";
import { AteEditorStateSnapshot, ATE_INITIAL_EDITOR_STATE } from "../models/ate-editor-state.model";
-import { AteImageUploadHandler, AteImageUploadOptions } from "../models/ate-image.model";
+import {
+ AteImageUploadHandler,
+ AteImageUploadOptions,
+ AteImageUploadResult,
+} from "../models/ate-image.model";
@Injectable()
export class AteEditorCommandsService {
@@ -81,6 +85,10 @@ export class AteEditorCommandsService {
readonly isUploading = this.imageService.isUploading.asReadonly();
readonly uploadProgress = this.imageService.uploadProgress.asReadonly();
readonly uploadMessage = this.imageService.uploadMessage.asReadonly();
+
+ set onImageUploaded(callback: ((result: AteImageUploadResult) => void) | null) {
+ this.imageService.onImageUploadedCallback = callback || undefined;
+ }
set uploadHandler(handler: AteImageUploadHandler | null) {
this.imageService.uploadHandler = handler;
}
@@ -496,7 +504,9 @@ export class AteEditorCommandsService {
* @param format 'html' | 'markdown' | 'text'
*/
getContent(editor: Editor, format: AteExportFormat): string {
- if (!editor) {return "";}
+ if (!editor) {
+ return "";
+ }
return this.exportSvc.getContent(editor, format);
}
@@ -512,7 +522,9 @@ export class AteEditorCommandsService {
method: "clipboard" | "download",
options?: AteExportOptions
): Promise {
- if (!editor) {return;}
+ if (!editor) {
+ return;
+ }
if (method === "clipboard") {
await this.exportSvc.exportToClipboard(editor, format);
} else {
@@ -581,7 +593,11 @@ export class AteEditorCommandsService {
return this.imageService.handleDrop(editor, event, options);
}
- handleImagePaste(editor: Editor, event: ClipboardEvent, options?: AteImageUploadOptions): boolean {
+ handleImagePaste(
+ editor: Editor,
+ event: ClipboardEvent,
+ options?: AteImageUploadOptions
+ ): boolean {
return this.imageService.handlePaste(editor, event, options);
}
}
diff --git a/projects/angular-tiptap-editor/src/lib/services/ate-image.service.ts b/projects/angular-tiptap-editor/src/lib/services/ate-image.service.ts
index a43a091..59f0142 100644
--- a/projects/angular-tiptap-editor/src/lib/services/ate-image.service.ts
+++ b/projects/angular-tiptap-editor/src/lib/services/ate-image.service.ts
@@ -12,6 +12,8 @@ import {
@Injectable()
export class AteImageService {
+ onImageUploadedCallback?: (result: AteImageUploadResult) => void;
+
/** Signals for image state */
selectedImage = signal(null);
isImageSelected = computed(() => this.selectedImage() !== null);
@@ -371,6 +373,10 @@ export class AteImageService {
// Final insertion
insertionStrategy(editor, result);
+ if (this.onImageUploadedCallback) {
+ this.onImageUploadedCallback(result);
+ }
+
this.resetUploadState();
} catch (error) {
this.resetUploadState();
diff --git a/projects/angular-tiptap-editor/src/lib/utils/ate-markdown.utils.ts b/projects/angular-tiptap-editor/src/lib/utils/ate-markdown.utils.ts
index 241b692..57202ba 100644
--- a/projects/angular-tiptap-editor/src/lib/utils/ate-markdown.utils.ts
+++ b/projects/angular-tiptap-editor/src/lib/utils/ate-markdown.utils.ts
@@ -47,7 +47,9 @@ function convertNode(
if (tag === "p") {
const content = inner().trim();
- if (!content) {return "";}
+ if (!content) {
+ return "";
+ }
return `\n\n${content}\n\n`;
}
@@ -63,9 +65,7 @@ function convertNode(
if (tag === "pre") {
// Extract language from code class e.g. "language-typescript"
const codeEl = el.querySelector("code");
- const lang = codeEl
- ? (codeEl.className.match(/language-(\S+)/) || [])[1] || ""
- : "";
+ const lang = codeEl ? (codeEl.className.match(/language-(\S+)/) || [])[1] || "" : "";
const code = codeEl ? codeEl.textContent || "" : el.textContent || "";
return `\n\n\`\`\`${lang}\n${code}\n\`\`\`\n\n`;
}
@@ -124,8 +124,12 @@ function convertNode(
if (tag === "a") {
const href = el.getAttribute("href") || "";
const content = inner().trim();
- if (!content && !href) {return "";}
- if (!content) {return href;}
+ if (!content && !href) {
+ return "";
+ }
+ if (!content) {
+ return href;
+ }
return `[${content}](${href})`;
}
@@ -166,8 +170,9 @@ function convertListItem(
// Check if li contains nested lists
const nestedListNodes = Array.from(li.childNodes).filter(
- c => (c as HTMLElement).tagName?.toLowerCase() === "ul" ||
- (c as HTMLElement).tagName?.toLowerCase() === "ol"
+ c =>
+ (c as HTMLElement).tagName?.toLowerCase() === "ul" ||
+ (c as HTMLElement).tagName?.toLowerCase() === "ol"
);
const textNodes = Array.from(li.childNodes).filter(
c => !["ul", "ol"].includes((c as HTMLElement).tagName?.toLowerCase())
@@ -188,7 +193,9 @@ function convertListItem(
function convertTable(table: HTMLElement): string {
const rows = Array.from(table.querySelectorAll("tr"));
- if (rows.length === 0) {return "";}
+ if (rows.length === 0) {
+ return "";
+ }
const tableData: string[][] = rows.map(row =>
Array.from(row.querySelectorAll("th, td")).map(
@@ -196,13 +203,17 @@ function convertTable(table: HTMLElement): string {
)
);
- if (tableData.length === 0) {return "";}
+ if (tableData.length === 0) {
+ return "";
+ }
const colCount = Math.max(...tableData.map(r => r.length));
// Pad rows to same width
const normalized = tableData.map(row => {
- while (row.length < colCount) {row.push("");}
+ while (row.length < colCount) {
+ row.push("");
+ }
return row;
});
diff --git a/src/components/configuration-panel.component.ts b/src/components/configuration-panel.component.ts
index d0956e9..046442a 100644
--- a/src/components/configuration-panel.component.ts
+++ b/src/components/configuration-panel.component.ts
@@ -61,15 +61,24 @@ import {