@flogeez/angular-tiptap-editor 3.1.0 → 3.1.2
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/CHANGELOG.md +13 -0
- package/README.md +2 -0
- package/fesm2022/flogeez-angular-tiptap-editor.mjs +114 -32
- package/fesm2022/flogeez-angular-tiptap-editor.mjs.map +1 -1
- package/index.d.ts +12 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to `@flogeez/angular-tiptap-editor` will be documented in th
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
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).
|
|
7
7
|
|
|
8
|
+
## [3.1.2] - 2026-04-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Image Upload Handler**: `imageUploadHandler` is now invoked for pasted image files, matching toolbar and drag-and-drop upload behavior.
|
|
13
|
+
- **Image Insertion**: Resolved a regression where images failed to insert when the cursor was positioned within existing text.
|
|
14
|
+
|
|
15
|
+
## [3.1.1] - 2026-02-13
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Image Resizing**: Resolved a bug where multiple selected images were resized simultaneously. The update now precisely targets the intended node.
|
|
20
|
+
|
|
8
21
|
## [3.1.0] - 2026-02-13
|
|
9
22
|
|
|
10
23
|
### Added
|
package/README.md
CHANGED
|
@@ -137,6 +137,8 @@ angularNodes: [
|
|
|
137
137
|
|
|
138
138
|
Avoid base64 by providing a custom uploader (S3, Cloudinary, etc.):
|
|
139
139
|
|
|
140
|
+
The same handler is used for all file-based image insertions: toolbar picker, drag-and-drop, and clipboard paste.
|
|
141
|
+
|
|
140
142
|
```typescript
|
|
141
143
|
uploadHandler: AteImageUploadHandler = ctx => {
|
|
142
144
|
return this.http.post<any>("/api/upload", ctx.file).pipe(map(res => ({ src: res.url })));
|
|
@@ -220,15 +220,17 @@ const AteResizableImage = Node$1.create({
|
|
|
220
220
|
const handleMouseUp = () => {
|
|
221
221
|
isResizing = false;
|
|
222
222
|
document.body.classList.remove("resizing");
|
|
223
|
-
// Update Tiptap node with new dimensions
|
|
224
|
-
|
|
223
|
+
// Update Tiptap node with new dimensions at the specific position
|
|
224
|
+
const pos = typeof getPos === "function" ? getPos() : undefined;
|
|
225
|
+
if (typeof pos === "number") {
|
|
225
226
|
const finalWidth = parseInt(img.getAttribute("width") || "0");
|
|
226
227
|
const finalHeight = parseInt(img.getAttribute("height") || "0");
|
|
227
228
|
if (finalWidth && finalHeight) {
|
|
228
|
-
editor.
|
|
229
|
+
editor.view.dispatch(editor.view.state.tr.setNodeMarkup(pos, undefined, {
|
|
230
|
+
...node.attrs,
|
|
229
231
|
width: finalWidth,
|
|
230
232
|
height: finalHeight,
|
|
231
|
-
});
|
|
233
|
+
}));
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
@@ -1377,8 +1379,17 @@ class AteImageService {
|
|
|
1377
1379
|
}
|
|
1378
1380
|
/** Insert a new image and ensure it's selected */
|
|
1379
1381
|
insertImage(editor, imageData) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
+
editor
|
|
1383
|
+
.chain()
|
|
1384
|
+
.focus()
|
|
1385
|
+
.setResizableImage(imageData)
|
|
1386
|
+
.command(({ tr, commands }) => {
|
|
1387
|
+
// Selection is usually after the node. Node is at tr.selection.from - 1.
|
|
1388
|
+
const { from } = tr.selection;
|
|
1389
|
+
// Try to select node at current pos (if it was already selected) or previous pos
|
|
1390
|
+
return commands.setNodeSelection(from) || commands.setNodeSelection(from - 1) || true;
|
|
1391
|
+
})
|
|
1392
|
+
.run();
|
|
1382
1393
|
}
|
|
1383
1394
|
/** Update attributes of the currently active image */
|
|
1384
1395
|
updateImageAttributes(editor, attributes) {
|
|
@@ -1706,7 +1717,6 @@ class AteImageService {
|
|
|
1706
1717
|
/** Internal helper used by replacement logic */
|
|
1707
1718
|
async uploadAndReplaceImage(editor, file, options) {
|
|
1708
1719
|
// Store current position to ensure we can re-select the image even if selection blurs during upload
|
|
1709
|
-
const pos = editor.state.selection.from;
|
|
1710
1720
|
const wasActive = editor.isActive("resizableImage");
|
|
1711
1721
|
await this.uploadImageWithProgress(editor, file, (ed, result) => {
|
|
1712
1722
|
const imageData = {
|
|
@@ -1721,7 +1731,11 @@ class AteImageService {
|
|
|
1721
1731
|
ed.chain()
|
|
1722
1732
|
.focus()
|
|
1723
1733
|
.updateAttributes("resizableImage", imageData)
|
|
1724
|
-
.
|
|
1734
|
+
.command(({ tr, commands }) => {
|
|
1735
|
+
const { from } = tr.selection;
|
|
1736
|
+
// Re-select the node at its mapped position or current selection
|
|
1737
|
+
return commands.setNodeSelection(from) || commands.setNodeSelection(from - 1) || true;
|
|
1738
|
+
})
|
|
1725
1739
|
.run();
|
|
1726
1740
|
this.updateSelectedImage(imageData);
|
|
1727
1741
|
}
|
|
@@ -1733,6 +1747,45 @@ class AteImageService {
|
|
|
1733
1747
|
this.selectImage(ed);
|
|
1734
1748
|
}, this.t().replacingImage, options);
|
|
1735
1749
|
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Handle image drop event.
|
|
1752
|
+
* Calculates the position from coordinates and sets the selection accordingly.
|
|
1753
|
+
*/
|
|
1754
|
+
handleDrop(editor, event, options) {
|
|
1755
|
+
const files = event.dataTransfer?.files;
|
|
1756
|
+
if (!files || files.length === 0) {
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
const file = Array.from(files).find(f => f.type.startsWith("image/"));
|
|
1760
|
+
if (!file) {
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
event.preventDefault();
|
|
1764
|
+
event.stopPropagation();
|
|
1765
|
+
// Try to find the drop position from coordinates
|
|
1766
|
+
const pos = editor.view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
1767
|
+
if (pos) {
|
|
1768
|
+
editor.commands.setTextSelection(pos.pos);
|
|
1769
|
+
}
|
|
1770
|
+
void this.uploadAndInsertImage(editor, file, options);
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Handle image paste event.
|
|
1775
|
+
*/
|
|
1776
|
+
handlePaste(editor, event, options) {
|
|
1777
|
+
const clipboardFiles = event.clipboardData?.files;
|
|
1778
|
+
if (!clipboardFiles || clipboardFiles.length === 0) {
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
const imageFile = Array.from(clipboardFiles).find(file => file.type.startsWith("image/"));
|
|
1782
|
+
if (!imageFile) {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
event.preventDefault();
|
|
1786
|
+
void this.uploadAndInsertImage(editor, imageFile, options);
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1736
1789
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AteImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1737
1790
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AteImageService }); }
|
|
1738
1791
|
}
|
|
@@ -2580,6 +2633,12 @@ class AteEditorCommandsService {
|
|
|
2580
2633
|
downloadImage(editor) {
|
|
2581
2634
|
this.imageService.downloadImage(editor);
|
|
2582
2635
|
}
|
|
2636
|
+
handleImageDrop(editor, event, options) {
|
|
2637
|
+
return this.imageService.handleDrop(editor, event, options);
|
|
2638
|
+
}
|
|
2639
|
+
handleImagePaste(editor, event, options) {
|
|
2640
|
+
return this.imageService.handlePaste(editor, event, options);
|
|
2641
|
+
}
|
|
2583
2642
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AteEditorCommandsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2584
2643
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AteEditorCommandsService }); }
|
|
2585
2644
|
}
|
|
@@ -7427,6 +7486,33 @@ class AngularTiptapEditorComponent {
|
|
|
7427
7486
|
}
|
|
7428
7487
|
// Also allow any tiptap user options
|
|
7429
7488
|
const userOptions = this.finalTiptapOptions();
|
|
7489
|
+
const userEditorProps = userOptions.editorProps;
|
|
7490
|
+
const userHandlePaste = userEditorProps?.handlePaste;
|
|
7491
|
+
const userHandleDOMPaste = userEditorProps?.handleDOMEvents?.paste;
|
|
7492
|
+
const handleDOMPaste = (view, event) => {
|
|
7493
|
+
const currentEditor = this.editor();
|
|
7494
|
+
if (currentEditor &&
|
|
7495
|
+
this.editorCommandsService.handleImagePaste(currentEditor, event, this.getImageUploadOptions())) {
|
|
7496
|
+
return true;
|
|
7497
|
+
}
|
|
7498
|
+
if (!userHandleDOMPaste) {
|
|
7499
|
+
return false;
|
|
7500
|
+
}
|
|
7501
|
+
const userResult = userHandleDOMPaste(view, event);
|
|
7502
|
+
return userResult === true;
|
|
7503
|
+
};
|
|
7504
|
+
const handlePaste = (view, event, slice) => {
|
|
7505
|
+
const currentEditor = this.editor();
|
|
7506
|
+
if (currentEditor &&
|
|
7507
|
+
this.editorCommandsService.handleImagePaste(currentEditor, event, this.getImageUploadOptions())) {
|
|
7508
|
+
return true;
|
|
7509
|
+
}
|
|
7510
|
+
if (!userHandlePaste) {
|
|
7511
|
+
return false;
|
|
7512
|
+
}
|
|
7513
|
+
const userResult = userHandlePaste(view, event, slice);
|
|
7514
|
+
return userResult === true;
|
|
7515
|
+
};
|
|
7430
7516
|
const newEditor = new Editor({
|
|
7431
7517
|
...userOptions,
|
|
7432
7518
|
element: this.editorElement().nativeElement,
|
|
@@ -7435,9 +7521,16 @@ class AngularTiptapEditorComponent {
|
|
|
7435
7521
|
editable: this.finalEditable() && !this.mergedDisabled(),
|
|
7436
7522
|
autofocus: this.finalAutofocus(),
|
|
7437
7523
|
editorProps: {
|
|
7524
|
+
...userEditorProps,
|
|
7438
7525
|
attributes: {
|
|
7526
|
+
...userEditorProps?.attributes,
|
|
7439
7527
|
spellcheck: this.finalSpellcheck().toString(),
|
|
7440
7528
|
},
|
|
7529
|
+
handleDOMEvents: {
|
|
7530
|
+
...userEditorProps?.handleDOMEvents,
|
|
7531
|
+
paste: handleDOMPaste,
|
|
7532
|
+
},
|
|
7533
|
+
handlePaste,
|
|
7441
7534
|
},
|
|
7442
7535
|
onUpdate: ({ editor, transaction }) => {
|
|
7443
7536
|
const html = editor.getHTML();
|
|
@@ -7496,32 +7589,21 @@ class AngularTiptapEditorComponent {
|
|
|
7496
7589
|
this._isDragOver.set(true);
|
|
7497
7590
|
}
|
|
7498
7591
|
onDrop(event) {
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
if (files && files.length > 0) {
|
|
7504
|
-
const file = files[0];
|
|
7505
|
-
if (file.type.startsWith("image/")) {
|
|
7506
|
-
this.insertImageFromFile(file);
|
|
7507
|
-
}
|
|
7592
|
+
const editor = this.editor();
|
|
7593
|
+
if (editor) {
|
|
7594
|
+
this.editorCommandsService.handleImageDrop(editor, event, this.getImageUploadOptions());
|
|
7595
|
+
this._isDragOver.set(false);
|
|
7508
7596
|
}
|
|
7509
7597
|
}
|
|
7510
|
-
|
|
7511
|
-
const
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
});
|
|
7520
|
-
}
|
|
7521
|
-
catch (_e) {
|
|
7522
|
-
/* ignore */
|
|
7523
|
-
}
|
|
7524
|
-
}
|
|
7598
|
+
getImageUploadOptions() {
|
|
7599
|
+
const config = this.finalImageUploadConfig();
|
|
7600
|
+
return {
|
|
7601
|
+
quality: config.quality,
|
|
7602
|
+
maxWidth: config.maxWidth,
|
|
7603
|
+
maxHeight: config.maxHeight,
|
|
7604
|
+
maxSize: config.maxSize,
|
|
7605
|
+
allowedTypes: config.allowedTypes,
|
|
7606
|
+
};
|
|
7525
7607
|
}
|
|
7526
7608
|
// Public methods
|
|
7527
7609
|
getHTML() {
|