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 {