@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 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
- if (typeof getPos === "function") {
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.commands.updateAttributes("resizableImage", {
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
- const { from } = editor.state.selection;
1381
- editor.chain().focus().setResizableImage(imageData).setNodeSelection(from).run();
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
- .setNodeSelection(pos)
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
- event.preventDefault();
7500
- event.stopPropagation();
7501
- this._isDragOver.set(false);
7502
- const files = event.dataTransfer?.files;
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
- async insertImageFromFile(file) {
7511
- const currentEditor = this.editor();
7512
- if (currentEditor) {
7513
- try {
7514
- const config = this.finalImageUploadConfig();
7515
- await this.editorCommandsService.uploadImage(currentEditor, file, {
7516
- quality: config.quality,
7517
- maxWidth: config.maxWidth,
7518
- maxHeight: config.maxHeight,
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() {