tiptap-extension-izzy-resizable-image 0.1.9 → 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.
- package/README.md +4 -0
- package/npm/index.js +88 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@ const editor = new Editor({
|
|
|
93
93
|
// size50: true,
|
|
94
94
|
// size100: true,
|
|
95
95
|
},
|
|
96
|
+
fitToContainerSelector: '.tiptap',
|
|
97
|
+
autoFitWhenOversized: true,
|
|
96
98
|
};
|
|
97
99
|
},
|
|
98
100
|
}),
|
|
@@ -110,6 +112,8 @@ editor.commands.insertResizableImage({ src: 'https://picsum.photos/300/200', wid
|
|
|
110
112
|
- `alignMenuPosition: 'above' | 'below'` — where the menu appears relative to the image.
|
|
111
113
|
- `alignMenuIcons: { left: string, center: string, right: string, clear: string, preview: string }` — HTML strings for menu buttons.
|
|
112
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`).
|
|
113
117
|
|
|
114
118
|
Reference:
|
|
115
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
|
-
|
|
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
|
-
|
|
78
|
+
applyDimensionStyle(this.img, 'width', node.attrs.width)
|
|
47
79
|
if (node.attrs.height) {
|
|
48
|
-
this.img
|
|
80
|
+
applyDimensionStyle(this.img, 'height', node.attrs.height)
|
|
49
81
|
} else if (this.options && this.options.height) {
|
|
50
|
-
this.img
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|