tiptap-extension-izzy-resizable-image 0.1.8 → 0.1.10

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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/npm/index.js +89 -10
  3. package/package.json +8 -2
package/README.md CHANGED
@@ -4,6 +4,8 @@ Resizable inline image node for TipTap v2 with eight resize handles, optional al
4
4
 
5
5
  Demo: https://startinn.github.io/TipTap-Extension-Izzy-Resizable-Image/
6
6
 
7
+ Download: https://www.npmjs.com/package/tiptap-extension-izzy-resizable-image
8
+
7
9
  ## Features
8
10
 
9
11
  - Inline resizable image with 8 handles (nw, n, ne, e, se, s, sw, w)
@@ -91,6 +93,8 @@ const editor = new Editor({
91
93
  // size50: true,
92
94
  // size100: true,
93
95
  },
96
+ fitToContainerSelector: '.tiptap',
97
+ autoFitWhenOversized: true,
94
98
  };
95
99
  },
96
100
  }),
@@ -108,6 +112,8 @@ editor.commands.insertResizableImage({ src: 'https://picsum.photos/300/200', wid
108
112
  - `alignMenuPosition: 'above' | 'below'` — where the menu appears relative to the image.
109
113
  - `alignMenuIcons: { left: string, center: string, right: string, clear: string, preview: string }` — HTML strings for menu buttons.
110
114
  - `alignMenuButtonsHide: Record<string, boolean>` — hide specific buttons when `true` (supports `left`, `center`, `right`, `clear`, `preview`, `size50`, `size100`).
115
+ - `fitToContainerSelector: string` — CSS selector used as width reference for the initial image fit (default: `.tiptap`).
116
+ - `autoFitWhenOversized: boolean` — when `true`, oversized images inserted without explicit width are automatically set to `width: 100%` and `height: auto` (default: `true`).
111
117
 
112
118
  Reference:
113
119
  - Defined in `resizable-image.js:387-394`
package/npm/index.js CHANGED
@@ -9,13 +9,45 @@ function createHandle(direction) {
9
9
  return handle
10
10
  }
11
11
 
12
+ function applyDimensionStyle(el, property, value) {
13
+ if (value == null || value === '') {
14
+ el.style.removeProperty(property)
15
+ return
16
+ }
17
+
18
+ if (typeof value === 'number') {
19
+ el.style[property] = value + 'px'
20
+ return
21
+ }
22
+
23
+ const raw = String(value).trim()
24
+ if (!raw) {
25
+ el.style.removeProperty(property)
26
+ return
27
+ }
28
+
29
+ if (/^\d+(\.\d+)?$/.test(raw)) {
30
+ el.style[property] = raw + 'px'
31
+ return
32
+ }
33
+
34
+ el.style[property] = raw
35
+ }
36
+
37
+ function toFiniteNumber(value) {
38
+ const n = Number(value)
39
+ return Number.isFinite(n) ? n : null
40
+ }
41
+
12
42
  class ResizableImageView {
13
43
  constructor(node, view, getPos, options) {
14
44
  this.node = node
15
45
  this.view = view
16
46
  this.getPos = getPos
17
47
  this.options = options || {}
18
- this.aspect = (node.attrs.height && node.attrs.width) ? node.attrs.width / node.attrs.height : null
48
+ const initialWidth = toFiniteNumber(node.attrs.width)
49
+ const initialHeight = toFiniteNumber(node.attrs.height)
50
+ this.aspect = (initialWidth && initialHeight) ? initialWidth / initialHeight : null
19
51
 
20
52
  this.dom = document.createElement('span')
21
53
  this.dom.className = 'tiptap-izzy-resizable-image'
@@ -43,16 +75,17 @@ class ResizableImageView {
43
75
  this.img.style.pointerEvents = 'none'
44
76
  if (node.attrs.id) this.img.id = node.attrs.id
45
77
  if (node.attrs.class) this.img.className = node.attrs.class
46
- if (node.attrs.width) this.img.style.width = node.attrs.width + 'px'
78
+ applyDimensionStyle(this.img, 'width', node.attrs.width)
47
79
  if (node.attrs.height) {
48
- this.img.style.height = node.attrs.height + 'px'
80
+ applyDimensionStyle(this.img, 'height', node.attrs.height)
49
81
  } else if (this.options && this.options.height) {
50
- this.img.style.height = this.options.height + 'px'
82
+ applyDimensionStyle(this.img, 'height', this.options.height)
51
83
  }
52
84
  this.img.addEventListener('load', () => {
53
85
  if (!this.aspect && this.img.naturalWidth && this.img.naturalHeight) {
54
86
  this.aspect = this.img.naturalWidth / this.img.naturalHeight
55
87
  }
88
+ this.applyInitialFitIfNeeded()
56
89
  })
57
90
  this.inner.appendChild(this.img)
58
91
 
@@ -160,6 +193,50 @@ class ResizableImageView {
160
193
 
161
194
  }
162
195
 
196
+ getEditorContentWidth() {
197
+ const selector = this.options?.fitToContainerSelector
198
+ const root = this.view?.dom || this.dom?.parentElement
199
+ if (!root) return null
200
+
201
+ if (selector) {
202
+ const bySelector = root.closest?.(selector) || root.querySelector?.(selector)
203
+ if (bySelector) {
204
+ const w = Math.round(bySelector.getBoundingClientRect().width)
205
+ if (w > 0) return w
206
+ }
207
+ }
208
+
209
+ const tiptap = this.dom?.closest?.('.tiptap') || root.closest?.('.tiptap') || root.querySelector?.('.tiptap')
210
+ if (tiptap) {
211
+ const w = Math.round(tiptap.getBoundingClientRect().width)
212
+ if (w > 0) return w
213
+ }
214
+
215
+ const rootWidth = Math.round(root.getBoundingClientRect().width)
216
+ return rootWidth > 0 ? rootWidth : null
217
+ }
218
+
219
+ applyInitialFitIfNeeded() {
220
+ if (this.options?.autoFitWhenOversized === false) return
221
+ if (this.node?.attrs?.width != null) return
222
+
223
+ const containerWidth = this.getEditorContentWidth()
224
+ if (!containerWidth || !this.img?.naturalWidth) return
225
+
226
+ if (this.img.naturalWidth <= containerWidth) return
227
+
228
+ applyDimensionStyle(this.img, 'width', '100%')
229
+ applyDimensionStyle(this.img, 'height', 'auto')
230
+
231
+ const pos = this.getPos()
232
+ const tr = this.view.state.tr.setNodeMarkup(pos, null, {
233
+ ...this.node.attrs,
234
+ width: '100%',
235
+ height: 'auto',
236
+ })
237
+ this.view.dispatch(tr)
238
+ }
239
+
163
240
  updateMenuPosition(pos) {
164
241
  this.menu.classList.remove('menu-above', 'menu-below')
165
242
  if (pos === 'above') {
@@ -270,10 +347,8 @@ class ResizableImageView {
270
347
  this.node = node
271
348
  if (node.attrs.src !== this.img.src) this.img.src = node.attrs.src
272
349
  this.img.alt = node.attrs.alt || ''
273
- if (node.attrs.width) this.img.style.width = node.attrs.width + 'px'
274
- else this.img.style.removeProperty('width')
275
- if (node.attrs.height) this.img.style.height = node.attrs.height + 'px'
276
- else this.img.style.removeProperty('height')
350
+ applyDimensionStyle(this.img, 'width', node.attrs.width)
351
+ applyDimensionStyle(this.img, 'height', node.attrs.height)
277
352
  this.applyAlignment(node.attrs.align)
278
353
  this.updateMenuPosition(node.attrs.alignMenuPosition != null ? node.attrs.alignMenuPosition : (this.options && this.options.alignMenuPosition != null ? this.options.alignMenuPosition : 'below'))
279
354
  this.btnLeft.innerHTML = (node.attrs.iconLeft != null ? node.attrs.iconLeft : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.left : '⟸'))
@@ -281,7 +356,9 @@ class ResizableImageView {
281
356
  this.btnRight.innerHTML = (node.attrs.iconRight != null ? node.attrs.iconRight : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.right : '⟹'))
282
357
  this.btnClear.innerHTML = (node.attrs.iconClear != null ? node.attrs.iconClear : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.clear : 'x'))
283
358
  if (this.btnView) this.btnView.innerHTML = (node.attrs.iconView != null ? node.attrs.iconView : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.preview : '🔍'))
284
- this.aspect = (node.attrs.height && node.attrs.width) ? node.attrs.width / node.attrs.height : this.aspect
359
+ const widthNum = toFiniteNumber(node.attrs.width)
360
+ const heightNum = toFiniteNumber(node.attrs.height)
361
+ this.aspect = (widthNum && heightNum) ? widthNum / heightNum : this.aspect
285
362
  return true
286
363
  }
287
364
 
@@ -357,7 +434,7 @@ export const TiptapIzzyExtensionResizableImage = Node.create({
357
434
  height: { default: null },
358
435
  id: { default: null },
359
436
  class: { default: null },
360
- align: { default: 'left' },
437
+ align: { default: null },
361
438
  showAlignMenu: { default: null },
362
439
  alignMenuPosition: { default: null },
363
440
  iconLeft: { default: null },
@@ -387,6 +464,8 @@ export const TiptapIzzyExtensionResizableImage = Node.create({
387
464
  alignMenuPosition: 'below',
388
465
  alignMenuIcons: { left: '⟸', center: '⇔', right: '⟹', clear: 'x', preview: '🔍' },
389
466
  alignMenuButtonsHide: {},
467
+ fitToContainerSelector: '.tiptap',
468
+ autoFitWhenOversized: true,
390
469
  }
391
470
  },
392
471
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiptap-extension-izzy-resizable-image",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Resizable image NodeView for TipTap v2 with alignment menu and defaults",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,13 @@
8
8
  "exports": {
9
9
  ".": "./npm/index.js"
10
10
  },
11
- "homepage": "https://bernanr.github.io/TipTap-Extension-Izzy-Resizable-Image",
11
+ "homepage": "https://startinn.github.io/TipTap-Extension-Izzy-Resizable-Image/",
12
+ "bugs": {
13
+ "url": "https://github.com/startinn/TipTap-Extension-Izzy-Resizable-Image/issues"
14
+ },
15
+ "publishConfig": {
16
+ "registry": "https://registry.npmjs.org/"
17
+ },
12
18
  "files": [
13
19
  "npm/index.js",
14
20
  "README.md"