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.
- package/README.md +6 -0
- package/npm/index.js +131 -9
- 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
|
-
|
|
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
|
-
|
|
83
|
+
applyDimensionStyle(this.img, 'width', node.attrs.width)
|
|
47
84
|
if (node.attrs.height) {
|
|
48
|
-
this.img
|
|
85
|
+
applyDimensionStyle(this.img, 'height', node.attrs.height)
|
|
49
86
|
} else if (this.options && this.options.height) {
|
|
50
|
-
this.img
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|