tiptap-extension-izzy-resizable-image 0.1.9 → 0.1.11

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 +131 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -93,6 +93,9 @@ const editor = new Editor({
93
93
  // size50: true,
94
94
  // size100: true,
95
95
  },
96
+ fitToContainerSelector: '.tiptap',
97
+ autoFitWhenOversized: true,
98
+ maxAllowedWidth: null,
96
99
  };
97
100
  },
98
101
  }),
@@ -110,6 +113,9 @@ editor.commands.insertResizableImage({ src: 'https://picsum.photos/300/200', wid
110
113
  - `alignMenuPosition: 'above' | 'below'` — where the menu appears relative to the image.
111
114
  - `alignMenuIcons: { left: string, center: string, right: string, clear: string, preview: string }` — HTML strings for menu buttons.
112
115
  - `alignMenuButtonsHide: Record<string, boolean>` — hide specific buttons when `true` (supports `left`, `center`, `right`, `clear`, `preview`, `size50`, `size100`).
116
+ - `fitToContainerSelector: string` — CSS selector used as width reference for the initial image fit (default: `.tiptap`).
117
+ - `autoFitWhenOversized: boolean` — when `true`, oversized images inserted without explicit width are automatically set to `width: 100%` and `height: auto` (default: `true`).
118
+ - `maxAllowedWidth: number | null` — maximum allowed width in pixels. If image width exceeds this value, it is forced to `width: 100%` and `height: auto`.
113
119
 
114
120
  Reference:
115
121
  - Defined in `resizable-image.js:387-394`
package/npm/index.js CHANGED
@@ -9,13 +9,50 @@ 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
+
42
+ function isWidth100Percent(value) {
43
+ if (value == null) return false
44
+ return String(value).trim() === '100%'
45
+ }
46
+
12
47
  class ResizableImageView {
13
48
  constructor(node, view, getPos, options) {
14
49
  this.node = node
15
50
  this.view = view
16
51
  this.getPos = getPos
17
52
  this.options = options || {}
18
- this.aspect = (node.attrs.height && node.attrs.width) ? node.attrs.width / node.attrs.height : null
53
+ const initialWidth = toFiniteNumber(node.attrs.width)
54
+ const initialHeight = toFiniteNumber(node.attrs.height)
55
+ this.aspect = (initialWidth && initialHeight) ? initialWidth / initialHeight : null
19
56
 
20
57
  this.dom = document.createElement('span')
21
58
  this.dom.className = 'tiptap-izzy-resizable-image'
@@ -43,16 +80,17 @@ class ResizableImageView {
43
80
  this.img.style.pointerEvents = 'none'
44
81
  if (node.attrs.id) this.img.id = node.attrs.id
45
82
  if (node.attrs.class) this.img.className = node.attrs.class
46
- if (node.attrs.width) this.img.style.width = node.attrs.width + 'px'
83
+ applyDimensionStyle(this.img, 'width', node.attrs.width)
47
84
  if (node.attrs.height) {
48
- this.img.style.height = node.attrs.height + 'px'
85
+ applyDimensionStyle(this.img, 'height', node.attrs.height)
49
86
  } else if (this.options && this.options.height) {
50
- this.img.style.height = this.options.height + 'px'
87
+ applyDimensionStyle(this.img, 'height', this.options.height)
51
88
  }
52
89
  this.img.addEventListener('load', () => {
53
90
  if (!this.aspect && this.img.naturalWidth && this.img.naturalHeight) {
54
91
  this.aspect = this.img.naturalWidth / this.img.naturalHeight
55
92
  }
93
+ this.applyInitialFitIfNeeded()
56
94
  })
57
95
  this.inner.appendChild(this.img)
58
96
 
@@ -160,6 +198,65 @@ class ResizableImageView {
160
198
 
161
199
  }
162
200
 
201
+ getEditorContentWidth() {
202
+ const selector = this.options?.fitToContainerSelector
203
+ const root = this.view?.dom || this.dom?.parentElement
204
+ if (!root) return null
205
+
206
+ if (selector) {
207
+ const bySelector = root.closest?.(selector) || root.querySelector?.(selector)
208
+ if (bySelector) {
209
+ const w = Math.round(bySelector.getBoundingClientRect().width)
210
+ if (w > 0) return w
211
+ }
212
+ }
213
+
214
+ const tiptap = this.dom?.closest?.('.tiptap') || root.closest?.('.tiptap') || root.querySelector?.('.tiptap')
215
+ if (tiptap) {
216
+ const w = Math.round(tiptap.getBoundingClientRect().width)
217
+ if (w > 0) return w
218
+ }
219
+
220
+ const rootWidth = Math.round(root.getBoundingClientRect().width)
221
+ return rootWidth > 0 ? rootWidth : null
222
+ }
223
+
224
+ applyInitialFitIfNeeded() {
225
+ if (!this.img) return
226
+
227
+ const maxAllowedWidth = toFiniteNumber(this.options?.maxAllowedWidth)
228
+ const shouldAutoFit = this.options?.autoFitWhenOversized !== false
229
+ if (!shouldAutoFit && maxAllowedWidth == null) return
230
+
231
+ const attrWidth = this.node?.attrs?.width
232
+ if (isWidth100Percent(attrWidth)) return
233
+
234
+ const containerWidth = this.getEditorContentWidth()
235
+ if (!containerWidth) return
236
+
237
+ let limitWidth = shouldAutoFit ? containerWidth : null
238
+ if (maxAllowedWidth != null) {
239
+ limitWidth = limitWidth == null ? maxAllowedWidth : Math.min(limitWidth, maxAllowedWidth)
240
+ }
241
+ if (limitWidth == null) return
242
+
243
+ const currentWidth = toFiniteNumber(attrWidth) || this.img.naturalWidth || Math.round(this.img.getBoundingClientRect().width)
244
+ if (!currentWidth) return
245
+
246
+ if (currentWidth <= limitWidth) return
247
+
248
+ applyDimensionStyle(this.img, 'width', '100%')
249
+ applyDimensionStyle(this.img, 'height', 'auto')
250
+
251
+ const pos = this.getPos()
252
+ const tr = this.view.state.tr.setNodeMarkup(pos, null, {
253
+ ...this.node.attrs,
254
+ width: '100%',
255
+ height: 'auto',
256
+ })
257
+ this.view.dispatch(tr)
258
+ }
259
+
163
260
  updateMenuPosition(pos) {
164
261
  this.menu.classList.remove('menu-above', 'menu-below')
165
262
  if (pos === 'above') {
@@ -203,6 +300,17 @@ class ResizableImageView {
203
300
  const targetW = Math.max(20, Math.round(naturalW * percent))
204
301
  const targetH = this.aspect ? Math.round(targetW / this.aspect) : Math.round(this.img.getBoundingClientRect().height)
205
302
  const pos = this.getPos()
303
+ const maxAllowedWidth = toFiniteNumber(this.options?.maxAllowedWidth)
304
+ if (maxAllowedWidth != null && targetW > maxAllowedWidth) {
305
+ const tr = this.view.state.tr.setNodeMarkup(pos, null, {
306
+ ...this.node.attrs,
307
+ width: '100%',
308
+ height: 'auto',
309
+ })
310
+ this.view.dispatch(tr)
311
+ return
312
+ }
313
+
206
314
  const tr = this.view.state.tr.setNodeMarkup(pos, null, { ...this.node.attrs, width: targetW, height: targetH })
207
315
  this.view.dispatch(tr)
208
316
  }
@@ -259,8 +367,19 @@ class ResizableImageView {
259
367
  const finalRect = this.img.getBoundingClientRect()
260
368
  const newWidth = Math.round(finalRect.width)
261
369
  const newHeight = Math.round(finalRect.height)
370
+ const maxAllowedWidth = toFiniteNumber(this.options?.maxAllowedWidth)
262
371
  this.dragging = null
263
372
  const pos = this.getPos()
373
+ if (maxAllowedWidth != null && newWidth > maxAllowedWidth) {
374
+ const tr = this.view.state.tr.setNodeMarkup(pos, null, {
375
+ ...this.node.attrs,
376
+ width: '100%',
377
+ height: 'auto',
378
+ })
379
+ this.view.dispatch(tr)
380
+ return
381
+ }
382
+
264
383
  const tr = this.view.state.tr.setNodeMarkup(pos, null, { ...this.node.attrs, width: newWidth, height: newHeight })
265
384
  this.view.dispatch(tr)
266
385
  }
@@ -270,10 +389,8 @@ class ResizableImageView {
270
389
  this.node = node
271
390
  if (node.attrs.src !== this.img.src) this.img.src = node.attrs.src
272
391
  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')
392
+ applyDimensionStyle(this.img, 'width', node.attrs.width)
393
+ applyDimensionStyle(this.img, 'height', node.attrs.height)
277
394
  this.applyAlignment(node.attrs.align)
278
395
  this.updateMenuPosition(node.attrs.alignMenuPosition != null ? node.attrs.alignMenuPosition : (this.options && this.options.alignMenuPosition != null ? this.options.alignMenuPosition : 'below'))
279
396
  this.btnLeft.innerHTML = (node.attrs.iconLeft != null ? node.attrs.iconLeft : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.left : '⟸'))
@@ -281,7 +398,9 @@ class ResizableImageView {
281
398
  this.btnRight.innerHTML = (node.attrs.iconRight != null ? node.attrs.iconRight : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.right : '⟹'))
282
399
  this.btnClear.innerHTML = (node.attrs.iconClear != null ? node.attrs.iconClear : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.clear : 'x'))
283
400
  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
401
+ const widthNum = toFiniteNumber(node.attrs.width)
402
+ const heightNum = toFiniteNumber(node.attrs.height)
403
+ this.aspect = (widthNum && heightNum) ? widthNum / heightNum : this.aspect
285
404
  return true
286
405
  }
287
406
 
@@ -387,6 +506,9 @@ export const TiptapIzzyExtensionResizableImage = Node.create({
387
506
  alignMenuPosition: 'below',
388
507
  alignMenuIcons: { left: '⟸', center: '⇔', right: '⟹', clear: 'x', preview: '🔍' },
389
508
  alignMenuButtonsHide: {},
509
+ fitToContainerSelector: '.tiptap',
510
+ autoFitWhenOversized: true,
511
+ maxAllowedWidth: null,
390
512
  }
391
513
  },
392
514
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiptap-extension-izzy-resizable-image",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Resizable image NodeView for TipTap v2 with alignment menu and defaults",
5
5
  "license": "MIT",
6
6
  "type": "module",