@flogeez/angular-tiptap-editor 0.3.4 → 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
|
|
@@ -4618,6 +4663,7 @@ class AngularTiptapEditorComponent {
|
|
|
4618
4663
|
this.enableSlashCommands = input(true);
|
|
4619
4664
|
this.slashCommandsConfig = input(undefined);
|
|
4620
4665
|
this.locale = input(undefined);
|
|
4666
|
+
this.autofocus = input(false);
|
|
4621
4667
|
// Nouveaux inputs pour les bubble menus
|
|
4622
4668
|
this.showBubbleMenu = input(true);
|
|
4623
4669
|
this.bubbleMenu = input(DEFAULT_BUBBLE_MENU_CONFIG);
|
|
@@ -4627,6 +4673,26 @@ class AngularTiptapEditorComponent {
|
|
|
4627
4673
|
this.toolbar = input({});
|
|
4628
4674
|
// Nouveau input pour la configuration de l'upload d'images
|
|
4629
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);
|
|
4630
4696
|
// Nouveaux outputs
|
|
4631
4697
|
this.contentChange = output();
|
|
4632
4698
|
this.editorCreated = output();
|
|
@@ -4750,6 +4816,11 @@ class AngularTiptapEditorComponent {
|
|
|
4750
4816
|
this.editorCommandsService.setEditable(currentEditor, isEditable);
|
|
4751
4817
|
}
|
|
4752
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
|
+
});
|
|
4753
4824
|
// Effect pour la détection du survol des tables
|
|
4754
4825
|
effect(() => {
|
|
4755
4826
|
const currentEditor = this.editor();
|
|
@@ -4827,6 +4898,7 @@ class AngularTiptapEditorComponent {
|
|
|
4827
4898
|
extensions,
|
|
4828
4899
|
content: this.content(),
|
|
4829
4900
|
editable: this.editable(),
|
|
4901
|
+
autofocus: this.autofocus(),
|
|
4830
4902
|
onUpdate: ({ editor, transaction }) => {
|
|
4831
4903
|
const html = editor.getHTML();
|
|
4832
4904
|
this.contentChange.emit(html);
|
|
@@ -5005,7 +5077,7 @@ class AngularTiptapEditorComponent {
|
|
|
5005
5077
|
}
|
|
5006
5078
|
}
|
|
5007
5079
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AngularTiptapEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5008
|
-
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 }, 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: `
|
|
5009
5081
|
<div class="tiptap-editor">
|
|
5010
5082
|
<!-- Toolbar -->
|
|
5011
5083
|
@if (showToolbar() && editor()) {
|