tiptap-extension-izzy-resizable-image 0.1.1

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 +170 -0
  2. package/npm/index.js +406 -0
  3. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # TipTap Izzy Extension: Resizable Image
2
+
3
+ Resizable inline image node for TipTap v2 with eight resize handles, optional alignment menu, drag-and-drop, and sensible defaults via `.extend()`.
4
+
5
+ Demo: https://bernanr.github.io/TipTap-Extension-Izzy-Resizable-Image
6
+
7
+ ## Features
8
+
9
+ - Inline resizable image with 8 handles (nw, n, ne, e, se, s, sw, w)
10
+ - Drag-and-drop repositioning using browser native drag
11
+ - Optional inline alignment menu (left, center, right, clear)
12
+ - Menu position configurable (`above` or `below`)
13
+ - Keeps aspect ratio while resizing
14
+ - Menu icons accept HTML strings (e.g., `<i class="...">`)
15
+ - Deselects image and hides UI when clicking outside
16
+
17
+ ## Installation
18
+
19
+ This demo loads TipTap and the extension via ESM in the browser. Include `resizable-image.js` next to your HTML file and import it.
20
+
21
+ ```html
22
+ <script type="module">
23
+ import { Editor } from 'https://esm.sh/@tiptap/core@2';
24
+ import StarterKit from 'https://esm.sh/@tiptap/starter-kit@2';
25
+ import { TiptapIzzyExtensionResizableImage } from './resizable-image.js';
26
+ // ...
27
+ </script>
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```js
33
+ const editor = new Editor({
34
+ element: document.querySelector('#editor'),
35
+ extensions: [StarterKit, TiptapIzzyExtensionResizableImage],
36
+ });
37
+
38
+ // Insert an image
39
+ editor.commands.insertResizableImage({ src: 'https://picsum.photos/300/200', width: 300, height: 200 });
40
+
41
+ // Align the selected image
42
+ editor.commands.setResizableImageAlignment('center');
43
+ ```
44
+
45
+ ### Using from npm (bundler)
46
+
47
+ ```bash
48
+ npm install tiptap-extension-izzy-resizable-image @tiptap/core@^3 prosemirror-state
49
+ ```
50
+
51
+ ```js
52
+ import { Editor } from '@tiptap/core';
53
+ import StarterKit from '@tiptap/starter-kit';
54
+ import { TiptapIzzyExtensionResizableImage } from 'tiptap-extension-izzy-resizable-image';
55
+
56
+ const editor = new Editor({
57
+ element: document.querySelector('#editor'),
58
+ extensions: [StarterKit, TiptapIzzyExtensionResizableImage],
59
+ });
60
+ ```
61
+
62
+ ## Defaults via `.extend()`
63
+
64
+ You can set extension-wide defaults using `.extend()` to override `addOptions()`.
65
+
66
+ ```js
67
+ const editor = new Editor({
68
+ element: document.querySelector('#editor'),
69
+ extensions: [
70
+ StarterKit,
71
+ TiptapIzzyExtensionResizableImage.extend({
72
+ addOptions() {
73
+ return {
74
+ height: 200,
75
+ showAlignMenu: true,
76
+ alignMenuPosition: 'below',
77
+ alignMenuIcons: {
78
+ left: '<i class="icon-left"></i>',
79
+ center: '⇔',
80
+ right: '⟹',
81
+ clear: 'x',
82
+ },
83
+ };
84
+ },
85
+ }),
86
+ ],
87
+ });
88
+
89
+ // With defaults set, you can omit height/menu attrs in insert:
90
+ editor.commands.insertResizableImage({ src: 'https://picsum.photos/300/200', width: 300 });
91
+ ```
92
+
93
+ ## Options (via `.extend()`)
94
+
95
+ - `height: number | null` — default height in pixels when node `height` attr is missing.
96
+ - `showAlignMenu: boolean` — whether the inline alignment menu is shown by default.
97
+ - `alignMenuPosition: 'above' | 'below'` — where the menu appears relative to the image.
98
+ - `alignMenuIcons: { left: string, center: string, right: string, clear: string }` — HTML strings for menu buttons.
99
+
100
+ Reference:
101
+ - Defined in `resizable-image.js:387-394`
102
+ - Passed into the NodeView in `resizable-image.js:383-385`
103
+ - Used for insertion defaults in `resizable-image.js:402-414`
104
+
105
+ ## Node Attributes
106
+
107
+ These can be set per-node, overriding options.
108
+
109
+ - `src: string` — image URL.
110
+ - `alt: string | null`
111
+ - `title: string | null`
112
+ - `width: number | null` — width in px.
113
+ - `height: number | null` — height in px.
114
+ - `align: 'left' | 'center' | 'right' | null` — alignment.
115
+ - `showAlignMenu: boolean | null` — show/hide menu per node.
116
+ - `alignMenuPosition: 'above' | 'below' | null`
117
+ - `iconLeft: string | null` — HTML or text for the left button.
118
+ - `iconCenter: string | null`
119
+ - `iconRight: string | null`
120
+ - `iconClear: string | null`
121
+
122
+ Reference: `resizable-image.js:358-372`
123
+
124
+ ## Commands
125
+
126
+ - `insertResizableImage({ src, alt, title, width, height })`
127
+ - Inserts the resizable image node. Missing attrs fall back to options (e.g., `height`).
128
+ - Reference: `resizable-image.js:398-416`
129
+
130
+ - `setResizableImageAlignment(align)`
131
+ - Sets `align` on the currently selected image node.
132
+ - Reference: `resizable-image.js:417-433`
133
+
134
+ ## Keyboard Shortcuts
135
+
136
+ - `Enter` when an image is selected inserts a paragraph after it and moves the cursor there.
137
+ - Reference: `resizable-image.js:412-431`
138
+
139
+ ## Alignment Behavior
140
+
141
+ - Alignment is applied via `text-align` on the wrapper and toggling wrapper display.
142
+ - `left | center | right` set `text-align` accordingly; unaligned flows inline.
143
+ - Reference: `resizable-image.js:172-193`
144
+
145
+ ## Resizing Behavior
146
+
147
+ - Eight handles adjust width/height; aspect ratio is preserved by default.
148
+ - Live preview updates `style.width`/`style.height`; final size is persisted to node attrs on pointerup.
149
+ - Reference: `resizable-image.js:195-271`
150
+
151
+ ## Selection and Focus
152
+
153
+ - Menu and handles overlay appear only when the image node is selected.
154
+ - Clicking outside the image deselects it and hides menu/overlay.
155
+ - Reference: show/hide logic `resizable-image.js:302-321`, `resizable-image.js:324-334`; outside-click handling `resizable-image.js:456-483`.
156
+
157
+ ## HTML Icons
158
+
159
+ - Menu buttons render using `innerHTML`, allowing HTML strings for icons or rich content.
160
+ - Use trusted sources; avoid unsanitized user input to prevent XSS.
161
+ - Reference: `resizable-image.js:120-123` and `resizable-image.js:291-294`.
162
+
163
+ ## Demo Hooks
164
+
165
+ - Example toolbar integration in the demo (`index.html:37-73`) shows inserting images and alignment controls.
166
+
167
+ ## Notes
168
+
169
+ - The extension is inline and draggable; selection toggles menu/handles.
170
+ - Deselecting removes the overlay/menu from the DOM to keep markup clean.
package/npm/index.js ADDED
@@ -0,0 +1,406 @@
1
+ import { Node } from '@tiptap/core'
2
+ import { NodeSelection, TextSelection, Plugin } from 'prosemirror-state'
3
+
4
+ function createHandle(direction) {
5
+ const handle = document.createElement('div')
6
+ handle.className = `resize-handle handle-${direction}`
7
+ handle.dataset.direction = direction
8
+ handle.title = direction
9
+ return handle
10
+ }
11
+
12
+ class ResizableImageView {
13
+ constructor(node, view, getPos, options) {
14
+ this.node = node
15
+ this.view = view
16
+ this.getPos = getPos
17
+ this.options = options || {}
18
+ this.aspect = (node.attrs.height && node.attrs.width) ? node.attrs.width / node.attrs.height : null
19
+
20
+ this.dom = document.createElement('span')
21
+ this.dom.className = 'tiptap-izzy-resizable-image'
22
+ this.dom.style.display = ''
23
+ this.dom.style.width = ''
24
+ this.dom.setAttribute('draggable', 'true')
25
+
26
+ this.wrapper = document.createElement('div')
27
+ this.wrapper.className = 'resizable-image-wrapper'
28
+ this.wrapper.style.display = 'inline-block'
29
+ this.wrapper.style.width = ''
30
+ this.dom.appendChild(this.wrapper)
31
+
32
+ this.inner = document.createElement('span')
33
+ this.inner.className = 'resizable-image-inner'
34
+ this.inner.style.position = 'relative'
35
+ this.inner.style.display = 'inline-block'
36
+ this.wrapper.appendChild(this.inner)
37
+
38
+ this.img = document.createElement('img')
39
+ this.img.src = node.attrs.src
40
+ this.img.alt = node.attrs.alt || ''
41
+ this.img.style.display = 'block'
42
+ this.img.style.userSelect = 'none'
43
+ this.img.style.pointerEvents = 'none'
44
+ if (node.attrs.width) this.img.style.width = node.attrs.width + 'px'
45
+ if (node.attrs.height) {
46
+ this.img.style.height = node.attrs.height + 'px'
47
+ } else if (this.options && this.options.height) {
48
+ this.img.style.height = this.options.height + 'px'
49
+ }
50
+ this.img.addEventListener('load', () => {
51
+ if (!this.aspect && this.img.naturalWidth && this.img.naturalHeight) {
52
+ this.aspect = this.img.naturalWidth / this.img.naturalHeight
53
+ }
54
+ })
55
+ this.inner.appendChild(this.img)
56
+
57
+ this.overlay = document.createElement('div')
58
+ this.overlay.className = 'resize-overlay'
59
+ this.overlay.style.position = 'absolute'
60
+ this.overlay.style.left = '0'
61
+ this.overlay.style.top = '0'
62
+ this.overlay.style.right = '0'
63
+ this.overlay.style.bottom = '0'
64
+ this.overlay.style.pointerEvents = 'none'
65
+
66
+ const directions = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']
67
+ this.handles = directions.map(dir => {
68
+ const h = createHandle(dir)
69
+ h.style.pointerEvents = 'auto'
70
+ h.addEventListener('pointerdown', this.onPointerDown.bind(this))
71
+ this.overlay.appendChild(h)
72
+ return h
73
+ })
74
+
75
+ this.menu = document.createElement('div')
76
+ this.menu.className = 'resizable-image-menu'
77
+ this.menu.style.position = 'absolute'
78
+ this.menu.style.left = '50%'
79
+ this.menu.style.transform = 'translateX(-50%)'
80
+ this.menu.style.pointerEvents = 'auto'
81
+ this.menu.style.zIndex = '10'
82
+
83
+ this.btnLeft = document.createElement('button')
84
+ this.btnCenter = document.createElement('button')
85
+ this.btnRight = document.createElement('button')
86
+ this.btnClear = document.createElement('button')
87
+
88
+ this.btnLeft.innerHTML = (node.attrs.iconLeft != null ? node.attrs.iconLeft : (this.options.alignMenuIcons?.left ?? '⟸'))
89
+ this.btnCenter.innerHTML = (node.attrs.iconCenter != null ? node.attrs.iconCenter : (this.options.alignMenuIcons?.center ?? '⇔'))
90
+ this.btnRight.innerHTML = (node.attrs.iconRight != null ? node.attrs.iconRight : (this.options.alignMenuIcons?.right ?? '⟹'))
91
+ this.btnClear.innerHTML = (node.attrs.iconClear != null ? node.attrs.iconClear : (this.options.alignMenuIcons?.clear ?? 'x'))
92
+
93
+ this.btnLeft.addEventListener('click', () => this.applyAlignAttr('left'))
94
+ this.btnCenter.addEventListener('click', () => this.applyAlignAttr('center'))
95
+ this.btnRight.addEventListener('click', () => this.applyAlignAttr('right'))
96
+ this.btnClear.addEventListener('click', () => this.applyAlignAttr(null))
97
+
98
+ this.menu.appendChild(this.btnLeft)
99
+ this.menu.appendChild(this.btnCenter)
100
+ this.menu.appendChild(this.btnRight)
101
+ this.menu.appendChild(this.btnClear)
102
+
103
+ this.updateMenuPosition(node.attrs.alignMenuPosition != null ? node.attrs.alignMenuPosition : (this.options && this.options.alignMenuPosition != null ? this.options.alignMenuPosition : 'below'))
104
+
105
+ this.dom.addEventListener('dragstart', (e) => {
106
+ const dragImg = document.createElement('canvas')
107
+ dragImg.width = 1
108
+ dragImg.height = 1
109
+ e.dataTransfer.setDragImage(dragImg, 0, 0)
110
+ })
111
+
112
+ this.dragging = null
113
+ this.applyAlignment(node.attrs.align)
114
+ }
115
+
116
+ updateMenuPosition(pos) {
117
+ this.menu.classList.remove('menu-above', 'menu-below')
118
+ if (pos === 'above') {
119
+ this.menu.classList.add('menu-above')
120
+ this.menu.style.top = '-32px'
121
+ this.menu.style.bottom = ''
122
+ } else {
123
+ this.menu.classList.add('menu-below')
124
+ this.menu.style.bottom = '-32px'
125
+ this.menu.style.top = ''
126
+ }
127
+ }
128
+
129
+ applyAlignAttr(align) {
130
+ const pos = this.getPos()
131
+ const tr = this.view.state.tr.setNodeMarkup(pos, null, { ...this.node.attrs, align })
132
+ this.view.dispatch(tr)
133
+ }
134
+
135
+ applyAlignment(align) {
136
+ this.inner.style.marginLeft = ''
137
+ this.inner.style.marginRight = ''
138
+ this.wrapper.style.textAlign = ''
139
+ this.inner.style.display = 'inline-block'
140
+ if (align === 'center' || align === 'left' || align === 'right') {
141
+ this.wrapper.style.display = 'block'
142
+ } else {
143
+ this.wrapper.style.display = 'inline-block'
144
+ }
145
+ if (align === 'center') {
146
+ this.wrapper.style.textAlign = 'center'
147
+ } else if (align === 'right') {
148
+ this.wrapper.style.textAlign = 'right'
149
+ } else if (align === 'left') {
150
+ this.wrapper.style.textAlign = 'left'
151
+ }
152
+ }
153
+
154
+ onPointerDown(event) {
155
+ event.preventDefault()
156
+ const direction = event.currentTarget.dataset.direction
157
+ const rect = this.img.getBoundingClientRect()
158
+ this.dragging = {
159
+ direction,
160
+ startX: event.clientX,
161
+ startY: event.clientY,
162
+ startWidth: rect.width,
163
+ startHeight: rect.height,
164
+ startLeft: rect.left,
165
+ startTop: rect.top,
166
+ keepAspect: true,
167
+ }
168
+ this._moveHandler = this.onPointerMove.bind(this)
169
+ this._upHandler = this.onPointerUp.bind(this)
170
+ window.addEventListener('pointermove', this._moveHandler)
171
+ window.addEventListener('pointerup', this._upHandler, { once: true })
172
+ }
173
+
174
+ onPointerMove(event) {
175
+ if (!this.dragging) return
176
+ const d = this.dragging
177
+ const dx = event.clientX - d.startX
178
+ const dy = event.clientY - d.startY
179
+ let newWidth = d.startWidth
180
+ let newHeight = d.startHeight
181
+ const dir = d.direction
182
+ const signX = (dir.includes('e') ? 1 : (dir.includes('w') ? -1 : 0))
183
+ const signY = (dir.includes('s') ? 1 : (dir.includes('n') ? -1 : 0))
184
+ if (signX !== 0) newWidth = Math.max(20, d.startWidth + dx * signX)
185
+ if (signY !== 0) newHeight = Math.max(20, d.startHeight + dy * signY)
186
+ if (d.keepAspect && this.aspect) {
187
+ if (signX !== 0 && signY === 0) {
188
+ newHeight = Math.round(newWidth / this.aspect)
189
+ } else if (signY !== 0 && signX === 0) {
190
+ newWidth = Math.round(newHeight * this.aspect)
191
+ } else {
192
+ newHeight = Math.round(newWidth / this.aspect)
193
+ }
194
+ }
195
+ this.img.style.width = newWidth + 'px'
196
+ this.img.style.height = newHeight + 'px'
197
+ }
198
+
199
+ onPointerUp(event) {
200
+ window.removeEventListener('pointermove', this._moveHandler)
201
+ this._moveHandler = null
202
+ if (!this.dragging) return
203
+ const finalRect = this.img.getBoundingClientRect()
204
+ const newWidth = Math.round(finalRect.width)
205
+ const newHeight = Math.round(finalRect.height)
206
+ this.dragging = null
207
+ const pos = this.getPos()
208
+ const tr = this.view.state.tr.setNodeMarkup(pos, null, { ...this.node.attrs, width: newWidth, height: newHeight })
209
+ this.view.dispatch(tr)
210
+ }
211
+
212
+ update(node) {
213
+ if (node.type !== this.node.type) return false
214
+ this.node = node
215
+ if (node.attrs.src !== this.img.src) this.img.src = node.attrs.src
216
+ this.img.alt = node.attrs.alt || ''
217
+ if (node.attrs.width) this.img.style.width = node.attrs.width + 'px'
218
+ else this.img.style.removeProperty('width')
219
+ if (node.attrs.height) this.img.style.height = node.attrs.height + 'px'
220
+ else this.img.style.removeProperty('height')
221
+ this.applyAlignment(node.attrs.align)
222
+ this.updateMenuPosition(node.attrs.alignMenuPosition != null ? node.attrs.alignMenuPosition : (this.options && this.options.alignMenuPosition != null ? this.options.alignMenuPosition : 'below'))
223
+ this.btnLeft.innerHTML = (node.attrs.iconLeft != null ? node.attrs.iconLeft : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.left : '⟸'))
224
+ this.btnCenter.innerHTML = (node.attrs.iconCenter != null ? node.attrs.iconCenter : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.center : '⇔'))
225
+ this.btnRight.innerHTML = (node.attrs.iconRight != null ? node.attrs.iconRight : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.right : '⟹'))
226
+ this.btnClear.innerHTML = (node.attrs.iconClear != null ? node.attrs.iconClear : (this.options && this.options.alignMenuIcons ? this.options.alignMenuIcons.clear : 'x'))
227
+ this.aspect = (node.attrs.height && node.attrs.width) ? node.attrs.width / node.attrs.height : this.aspect
228
+ return true
229
+ }
230
+
231
+ selectNode() {
232
+ this.dom.classList.add('selected')
233
+ const showMenu = (this.node.attrs.showAlignMenu != null ? this.node.attrs.showAlignMenu : (this.options && this.options.showAlignMenu != null ? this.options.showAlignMenu : true))
234
+ if (showMenu !== false) {
235
+ this.dom.classList.add('show-menu')
236
+ if (!this.inner.contains(this.menu)) {
237
+ this.inner.appendChild(this.menu)
238
+ }
239
+ this.menu.style.display = ''
240
+ } else {
241
+ this.dom.classList.remove('show-menu')
242
+ if (this.inner.contains(this.menu)) {
243
+ this.inner.removeChild(this.menu)
244
+ }
245
+ }
246
+ if (!this.inner.contains(this.overlay)) {
247
+ this.inner.appendChild(this.overlay)
248
+ }
249
+ this.overlay.style.pointerEvents = 'auto'
250
+ }
251
+
252
+ deselectNode() {
253
+ this.dom.classList.remove('selected')
254
+ this.dom.classList.remove('show-menu')
255
+ if (this.inner.contains(this.menu)) {
256
+ this.inner.removeChild(this.menu)
257
+ }
258
+ if (this.inner.contains(this.overlay)) {
259
+ this.inner.removeChild(this.overlay)
260
+ }
261
+ this.overlay.style.pointerEvents = 'none'
262
+ }
263
+
264
+ stopEvent(event) {
265
+ return (
266
+ (event.target.classList && event.target.classList.contains('resize-handle')) ||
267
+ (this.menu && this.menu.contains(event.target))
268
+ )
269
+ }
270
+
271
+ destroy() {
272
+ window.removeEventListener('pointermove', this._moveHandler)
273
+ window.removeEventListener('pointerup', this._upHandler)
274
+ }
275
+ }
276
+
277
+ export const TiptapIzzyExtensionResizableImage = Node.create({
278
+ name: 'tiptap-izzy-extension-resizable-image',
279
+ inline: true,
280
+ group: 'inline',
281
+ draggable: true,
282
+ selectable: true,
283
+ atom: true,
284
+
285
+ addAttributes() {
286
+ return {
287
+ src: { default: null },
288
+ alt: { default: null },
289
+ title: { default: null },
290
+ width: { default: null },
291
+ height: { default: null },
292
+ align: { default: 'left' },
293
+ showAlignMenu: { default: null },
294
+ alignMenuPosition: { default: null },
295
+ iconLeft: { default: null },
296
+ iconCenter: { default: null },
297
+ iconRight: { default: null },
298
+ iconClear: { default: null },
299
+ }
300
+ },
301
+
302
+ parseHTML() {
303
+ return [{ tag: 'img[src]' }]
304
+ },
305
+
306
+ renderHTML({ HTMLAttributes }) {
307
+ return ['img', HTMLAttributes]
308
+ },
309
+
310
+ addNodeView() {
311
+ return ({ node, view, getPos }) => new ResizableImageView(node, view, getPos, this.options)
312
+ },
313
+
314
+ addOptions() {
315
+ return {
316
+ height: null,
317
+ showAlignMenu: true,
318
+ alignMenuPosition: 'below',
319
+ alignMenuIcons: { left: '⟸', center: '⇔', right: '⟹', clear: 'x' },
320
+ }
321
+ },
322
+
323
+ addCommands() {
324
+ return {
325
+ insertResizableImage:
326
+ ({ src, alt, title, width, height }) => ({ commands }) => {
327
+ return commands.insertContent({
328
+ type: this.name,
329
+ attrs: {
330
+ src,
331
+ alt,
332
+ title,
333
+ width,
334
+ height: height != null ? height : this.options.height,
335
+ showAlignMenu: this.options.showAlignMenu,
336
+ alignMenuPosition: this.options.alignMenuPosition,
337
+ iconLeft: this.options.alignMenuIcons?.left,
338
+ iconCenter: this.options.alignMenuIcons?.center,
339
+ iconRight: this.options.alignMenuIcons?.right,
340
+ iconClear: this.options.alignMenuIcons?.clear,
341
+ },
342
+ })
343
+ },
344
+ setResizableImageAlignment:
345
+ (align) => ({ editor }) => {
346
+ const { state } = editor
347
+ const sel = state.selection
348
+ if (sel instanceof NodeSelection && sel.node && sel.node.type.name === this.name) {
349
+ const pos = sel.$from.pos
350
+ const tr = state.tr.setNodeMarkup(pos, null, { ...sel.node.attrs, align })
351
+ editor.view.dispatch(tr.scrollIntoView())
352
+ return true
353
+ }
354
+ return false
355
+ },
356
+ }
357
+ },
358
+
359
+ addKeyboardShortcuts() {
360
+ return {
361
+ Enter: ({ editor }) => {
362
+ const { state } = editor
363
+ const { selection } = state
364
+ if (selection instanceof NodeSelection && selection.node && selection.node.type.name === this.name) {
365
+ const paragraph = state.schema.nodes.paragraph?.createAndFill()
366
+ if (!paragraph) return false
367
+ const tr = state.tr.insert(selection.to, paragraph)
368
+ const resolved = tr.doc.resolve(selection.to + 1)
369
+ tr.setSelection(TextSelection.create(tr.doc, resolved.pos))
370
+ editor.view.dispatch(tr.scrollIntoView())
371
+ return true
372
+ }
373
+ return false
374
+ },
375
+ }
376
+ },
377
+
378
+ addProseMirrorPlugins() {
379
+ return [
380
+ new Plugin({
381
+ props: {
382
+ handleDOMEvents: {
383
+ mousedown: (view, event) => {
384
+ const sel = view.state.selection
385
+ if (sel instanceof NodeSelection && sel.node && sel.node.type.name === this.name) {
386
+ const nodeDOM = view.nodeDOM(sel.$from.pos)
387
+ const target = event.target
388
+ if (nodeDOM && target && nodeDOM.contains(target)) {
389
+ return false
390
+ }
391
+ const coords = { left: event.clientX, top: event.clientY }
392
+ const found = view.posAtCoords(coords)
393
+ if (found && typeof found.pos === 'number') {
394
+ const tr = view.state.tr.setSelection(TextSelection.create(view.state.doc, found.pos))
395
+ view.dispatch(tr)
396
+ return true
397
+ }
398
+ }
399
+ return false
400
+ },
401
+ },
402
+ },
403
+ }),
404
+ ]
405
+ },
406
+ })
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "tiptap-extension-izzy-resizable-image",
3
+ "version": "0.1.1",
4
+ "description": "Resizable image NodeView for TipTap v2 with alignment menu and defaults",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "npm/index.js",
8
+ "exports": {
9
+ ".": "./npm/index.js"
10
+ },
11
+ "homepage": "https://bernanr.github.io/TipTap-Extension-Izzy-Resizable-Image",
12
+ "files": [
13
+ "npm/index.js",
14
+ "README.md"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/BernanR/TipTap-Extension-Izzy-Resizable-Image.git"
19
+ },
20
+ "keywords": [
21
+ "tiptap",
22
+ "prosemirror",
23
+ "image",
24
+ "resizable",
25
+ "extension",
26
+ "nodeview"
27
+ ],
28
+ "peerDependencies": {
29
+ "@tiptap/core": ">=3.0.0",
30
+ "prosemirror-state": "^1.0.0"
31
+ }
32
+ }