@flogeez/angular-tiptap-editor 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -229,6 +229,80 @@ Professional image management:
229
229
  - **Auto-Compression**: Images automatically compressed (max 1920x1080)
230
230
  - **Resizable**: Images can be resized with handles
231
231
  - **Bubble Menu**: Context menu for image operations
232
+ - **Custom Upload Handler**: Upload images to your own server instead of base64
233
+
234
+ #### Custom Image Upload Handler
235
+
236
+ By default, images are converted to base64 and embedded directly in the HTML content. You can provide a custom upload handler to upload images to your own server (S3, Cloudinary, custom API, etc.) and use the returned URL instead.
237
+
238
+ The handler can return either an **Observable** or a **Promise**.
239
+
240
+ #### Using Observable (recommended for Angular)
241
+
242
+ ```typescript
243
+ import { Component, inject } from '@angular/core';
244
+ import { HttpClient } from '@angular/common/http';
245
+ import { map } from 'rxjs/operators';
246
+ import {
247
+ AngularTiptapEditorComponent,
248
+ ImageUploadHandler
249
+ } from '@flogeez/angular-tiptap-editor';
250
+
251
+ @Component({
252
+ selector: 'app-custom-upload',
253
+ standalone: true,
254
+ imports: [AngularTiptapEditorComponent],
255
+ template: `
256
+ <angular-tiptap-editor
257
+ [content]="content"
258
+ [imageUploadHandler]="uploadHandler"
259
+ (contentChange)="onContentChange($event)"
260
+ />
261
+ `
262
+ })
263
+ export class CustomUploadComponent {
264
+ private http = inject(HttpClient);
265
+ content = '';
266
+
267
+ uploadHandler: ImageUploadHandler = (ctx) => {
268
+ const formData = new FormData();
269
+ formData.append('image', ctx.file);
270
+
271
+ return this.http.post<{ url: string }>('/api/upload', formData).pipe(
272
+ map(result => ({ src: result.url }))
273
+ );
274
+ };
275
+
276
+ onContentChange(newContent: string) {
277
+ this.content = newContent;
278
+ }
279
+ }
280
+ ```
281
+
282
+ #### Using Promise (async/await)
283
+
284
+ ```typescript
285
+ uploadHandler: ImageUploadHandler = async (ctx) => {
286
+ const formData = new FormData();
287
+ formData.append('image', ctx.file);
288
+
289
+ const result = await firstValueFrom(
290
+ this.http.post<{ url: string }>('/api/upload', formData)
291
+ );
292
+
293
+ return { src: result.url };
294
+ };
295
+ ```
296
+
297
+ The `ImageUploadContext` provides:
298
+ - `file`: The original File object
299
+ - `width`: Processed image width
300
+ - `height`: Processed image height
301
+ - `type`: MIME type (e.g., 'image/jpeg')
302
+ - `base64`: Base64 data URL of the processed image (fallback)
303
+
304
+ The handler must return an `ImageUploadHandlerResult` with at least a `src` property containing the image URL.
305
+
232
306
 
233
307
  ### 📝 Word & Character Counting
234
308
 
@@ -262,22 +336,24 @@ Open [http://localhost:4200](http://localhost:4200) to view the demo.
262
336
 
263
337
  #### Inputs
264
338
 
265
- | Input | Type | Default | Description |
266
- | -------------------- | --------------------- | ------------------- | ---------------------------- |
267
- | `content` | `string` | `""` | Initial HTML content |
268
- | `placeholder` | `string` | `"Start typing..."` | Placeholder text |
269
- | `locale` | `'en' \| 'fr'` | Auto-detect | Editor language |
270
- | `editable` | `boolean` | `true` | Whether editor is editable |
271
- | `height` | `number` | `undefined` | Fixed height in pixels |
272
- | `maxHeight` | `number` | `undefined` | Maximum height in pixels |
273
- | `minHeight` | `number` | `200` | Minimum height in pixels |
274
- | `showToolbar` | `boolean` | `true` | Show toolbar |
275
- | `showBubbleMenu` | `boolean` | `true` | Show bubble menu |
276
- | `showCharacterCount` | `boolean` | `true` | Show character counter |
277
- | `showWordCount` | `boolean` | `true` | Show word counter |
278
- | `toolbar` | `ToolbarConfig` | All enabled | Toolbar configuration |
279
- | `bubbleMenu` | `BubbleMenuConfig` | All enabled | Bubble menu configuration |
280
- | `slashCommands` | `SlashCommandsConfig` | All enabled | Slash commands configuration |
339
+ | Input | Type | Default | Description |
340
+ | -------------------- | --------------------- | ------------------- | ----------------------------- |
341
+ | `content` | `string` | `""` | Initial HTML content |
342
+ | `placeholder` | `string` | `"Start typing..."` | Placeholder text |
343
+ | `locale` | `'en' \| 'fr'` | Auto-detect | Editor language |
344
+ | `editable` | `boolean` | `true` | Whether editor is editable |
345
+ | `height` | `number` | `undefined` | Fixed height in pixels |
346
+ | `maxHeight` | `number` | `undefined` | Maximum height in pixels |
347
+ | `minHeight` | `number` | `200` | Minimum height in pixels |
348
+ | `showToolbar` | `boolean` | `true` | Show toolbar |
349
+ | `showBubbleMenu` | `boolean` | `true` | Show bubble menu |
350
+ | `showCharacterCount` | `boolean` | `true` | Show character counter |
351
+ | `showWordCount` | `boolean` | `true` | Show word counter |
352
+ | `toolbar` | `ToolbarConfig` | All enabled | Toolbar configuration |
353
+ | `bubbleMenu` | `BubbleMenuConfig` | All enabled | Bubble menu configuration |
354
+ | `slashCommands` | `SlashCommandsConfig` | All enabled | Slash commands configuration |
355
+ | `imageUploadHandler` | `ImageUploadHandler` | `undefined` | Custom image upload function |
356
+
281
357
 
282
358
  #### Outputs
283
359
 
@@ -446,6 +522,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
446
522
 
447
523
  ### Latest Updates
448
524
 
525
+ - ✅ **Custom Image Upload Handler**: Upload images to your own server (S3, Cloudinary, etc.)
449
526
  - ✅ **Table Support**: Full table management with bubble menus
450
527
  - ✅ **Slash Commands**: Intuitive content insertion commands
451
528
  - ✅ **Word/Character Count**: Real-time counting with proper pluralization
@@ -454,6 +531,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
454
531
  - ✅ **Office Paste**: Clean pasting from Microsoft Office applications
455
532
  - ✅ **Enhanced i18n**: Improved internationalization with better architecture
456
533
 
534
+
457
535
  ---
458
536
 
459
537
  Made with ❤️ by [FloGeez](https://github.com/FloGeez)
@@ -20,10 +20,10 @@ import TableCell from '@tiptap/extension-table-cell';
20
20
  import TableHeader from '@tiptap/extension-table-header';
21
21
  import tippy from 'tippy.js';
22
22
  import { CellSelection } from '@tiptap/pm/tables';
23
+ import { isObservable, firstValueFrom, concat, defer, of, tap } from 'rxjs';
23
24
  import { CommonModule } from '@angular/common';
24
25
  import { Plugin as Plugin$1, PluginKey as PluginKey$1 } from 'prosemirror-state';
25
26
  import { NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
26
- import { concat, defer, of, tap } from 'rxjs';
27
27
 
28
28
  const ResizableImage = Node.create({
29
29
  name: "resizableImage",
@@ -1010,6 +1010,23 @@ class ImageService {
1010
1010
  this.isUploading = signal(false);
1011
1011
  this.uploadProgress = signal(0);
1012
1012
  this.uploadMessage = signal("");
1013
+ /**
1014
+ * Custom upload handler for images.
1015
+ * When set, this handler will be called instead of the default base64 conversion.
1016
+ * This allows users to implement their own image storage logic.
1017
+ *
1018
+ * @example
1019
+ * ```typescript
1020
+ * imageService.uploadHandler = async (context) => {
1021
+ * const formData = new FormData();
1022
+ * formData.append('image', context.file);
1023
+ * const response = await fetch('/api/upload', { method: 'POST', body: formData });
1024
+ * const data = await response.json();
1025
+ * return { src: data.url };
1026
+ * };
1027
+ * ```
1028
+ */
1029
+ this.uploadHandler = null;
1013
1030
  // Référence à l'éditeur pour les mises à jour
1014
1031
  this.currentEditor = null;
1015
1032
  }
@@ -1261,6 +1278,34 @@ class ImageService {
1261
1278
  await new Promise((resolve) => setTimeout(resolve, 200));
1262
1279
  const result = await this.compressImage(file, options?.quality || 0.8, options?.maxWidth || 1920, options?.maxHeight || 1080);
1263
1280
  this.uploadProgress.set(80);
1281
+ // Si un handler personnalisé est défini, l'utiliser pour l'upload
1282
+ if (this.uploadHandler) {
1283
+ this.uploadMessage.set("Upload vers le serveur...");
1284
+ this.forceEditorUpdate();
1285
+ try {
1286
+ const handlerResponse = this.uploadHandler({
1287
+ file,
1288
+ width: result.width || 0,
1289
+ height: result.height || 0,
1290
+ type: result.type,
1291
+ base64: result.src,
1292
+ });
1293
+ // Convertir Observable en Promise si nécessaire
1294
+ const handlerResult = isObservable(handlerResponse)
1295
+ ? await firstValueFrom(handlerResponse)
1296
+ : await handlerResponse;
1297
+ // Remplacer le src base64 par l'URL retournée par le handler
1298
+ result.src = handlerResult.src;
1299
+ // Appliquer les overrides optionnels du handler
1300
+ if (handlerResult.alt) {
1301
+ result.name = handlerResult.alt;
1302
+ }
1303
+ }
1304
+ catch (handlerError) {
1305
+ console.error("Erreur lors de l'upload personnalisé:", handlerError);
1306
+ throw handlerError;
1307
+ }
1308
+ }
1264
1309
  this.uploadMessage.set(actionMessage);
1265
1310
  this.forceEditorUpdate();
1266
1311
  // Petit délai pour l'action
@@ -4628,6 +4673,26 @@ class AngularTiptapEditorComponent {
4628
4673
  this.toolbar = input({});
4629
4674
  // Nouveau input pour la configuration de l'upload d'images
4630
4675
  this.imageUpload = input({});
4676
+ /**
4677
+ * Custom handler for image uploads.
4678
+ * When provided, images will be processed through this handler instead of being converted to base64.
4679
+ * This allows you to upload images to your own server/storage and use the returned URL.
4680
+ *
4681
+ * @example
4682
+ * ```typescript
4683
+ * myUploadHandler: ImageUploadHandler = async (context) => {
4684
+ * const formData = new FormData();
4685
+ * formData.append('image', context.file);
4686
+ * const response = await fetch('/api/upload', { method: 'POST', body: formData });
4687
+ * const data = await response.json();
4688
+ * return { src: data.imageUrl };
4689
+ * };
4690
+ *
4691
+ * // In template:
4692
+ * // <angular-tiptap-editor [imageUploadHandler]="myUploadHandler" />
4693
+ * ```
4694
+ */
4695
+ this.imageUploadHandler = input(undefined);
4631
4696
  // Nouveaux outputs
4632
4697
  this.contentChange = output();
4633
4698
  this.editorCreated = output();
@@ -4751,6 +4816,11 @@ class AngularTiptapEditorComponent {
4751
4816
  this.editorCommandsService.setEditable(currentEditor, isEditable);
4752
4817
  }
4753
4818
  });
4819
+ // Effect pour synchroniser le handler d'upload d'images avec le service
4820
+ effect(() => {
4821
+ const handler = this.imageUploadHandler();
4822
+ this.imageService.uploadHandler = handler || null;
4823
+ });
4754
4824
  // Effect pour la détection du survol des tables
4755
4825
  effect(() => {
4756
4826
  const currentEditor = this.editor();
@@ -5007,7 +5077,7 @@ class AngularTiptapEditorComponent {
5007
5077
  }
5008
5078
  }
5009
5079
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AngularTiptapEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5010
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: AngularTiptapEditorComponent, isStandalone: true, selector: "angular-tiptap-editor", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showCharacterCount: { classPropertyName: "showCharacterCount", publicName: "showCharacterCount", isSignal: true, isRequired: false, transformFunction: null }, maxCharacters: { classPropertyName: "maxCharacters", publicName: "maxCharacters", isSignal: true, isRequired: false, transformFunction: null }, enableOfficePaste: { classPropertyName: "enableOfficePaste", publicName: "enableOfficePaste", isSignal: true, isRequired: false, transformFunction: null }, enableSlashCommands: { classPropertyName: "enableSlashCommands", publicName: "enableSlashCommands", isSignal: true, isRequired: false, transformFunction: null }, slashCommandsConfig: { classPropertyName: "slashCommandsConfig", publicName: "slashCommandsConfig", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", isSignal: true, isRequired: false, transformFunction: null }, showBubbleMenu: { classPropertyName: "showBubbleMenu", publicName: "showBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, bubbleMenu: { classPropertyName: "bubbleMenu", publicName: "bubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, showImageBubbleMenu: { classPropertyName: "showImageBubbleMenu", publicName: "showImageBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, imageBubbleMenu: { classPropertyName: "imageBubbleMenu", publicName: "imageBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, imageUpload: { classPropertyName: "imageUpload", publicName: "imageUpload", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange", editorCreated: "editorCreated", editorUpdate: "editorUpdate", editorFocus: "editorFocus", editorBlur: "editorBlur" }, viewQueries: [{ propertyName: "editorElement", first: true, predicate: ["editorElement"], descendants: true, isSignal: true }], hostDirectives: [{ directive: NoopValueAccessorDirective }], ngImport: i0, template: `
5080
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: AngularTiptapEditorComponent, isStandalone: true, selector: "angular-tiptap-editor", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showCharacterCount: { classPropertyName: "showCharacterCount", publicName: "showCharacterCount", isSignal: true, isRequired: false, transformFunction: null }, maxCharacters: { classPropertyName: "maxCharacters", publicName: "maxCharacters", isSignal: true, isRequired: false, transformFunction: null }, enableOfficePaste: { classPropertyName: "enableOfficePaste", publicName: "enableOfficePaste", isSignal: true, isRequired: false, transformFunction: null }, enableSlashCommands: { classPropertyName: "enableSlashCommands", publicName: "enableSlashCommands", isSignal: true, isRequired: false, transformFunction: null }, slashCommandsConfig: { classPropertyName: "slashCommandsConfig", publicName: "slashCommandsConfig", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", isSignal: true, isRequired: false, transformFunction: null }, showBubbleMenu: { classPropertyName: "showBubbleMenu", publicName: "showBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, bubbleMenu: { classPropertyName: "bubbleMenu", publicName: "bubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, showImageBubbleMenu: { classPropertyName: "showImageBubbleMenu", publicName: "showImageBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, imageBubbleMenu: { classPropertyName: "imageBubbleMenu", publicName: "imageBubbleMenu", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, imageUpload: { classPropertyName: "imageUpload", publicName: "imageUpload", isSignal: true, isRequired: false, transformFunction: null }, imageUploadHandler: { classPropertyName: "imageUploadHandler", publicName: "imageUploadHandler", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange", editorCreated: "editorCreated", editorUpdate: "editorUpdate", editorFocus: "editorFocus", editorBlur: "editorBlur" }, viewQueries: [{ propertyName: "editorElement", first: true, predicate: ["editorElement"], descendants: true, isSignal: true }], hostDirectives: [{ directive: NoopValueAccessorDirective }], ngImport: i0, template: `
5011
5081
  <div class="tiptap-editor">
5012
5082
  <!-- Toolbar -->
5013
5083
  @if (showToolbar() && editor()) {