@seorii/tiptap 0.3.0 → 0.4.0

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/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export default TipTap;
2
- export { getDetail };
3
2
  import TipTap from './tiptap/index.js';
4
3
  import { getDetail } from './plugin/command/suggest.js';
4
+ import { insertUploadSkeleton } from './plugin/upload/skeleton/index.js';
5
+ export { getDetail, insertUploadSkeleton };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Reexport your entry components here
2
2
  import TipTap from './tiptap/index.js';
3
3
  import { getDetail } from './plugin/command/suggest.js';
4
+ import { insertUploadSkeleton } from './plugin/upload/skeleton/index.js';
4
5
 
5
6
  export default TipTap;
6
- export { getDetail };
7
+ export { getDetail, insertUploadSkeleton };
@@ -3,6 +3,7 @@ import i18n from '../../i18n';
3
3
  import enUs from '../../i18n/en-us/index';
4
4
  import koKr from '../../i18n/ko-kr/index';
5
5
  import { fallbackUpload, releaseObjectUrlOnImageSettled } from '../image/dragdrop';
6
+ import { insertUploadSkeleton } from '../upload/skeleton';
6
7
  import { PluginKey, TextSelection } from '@tiptap/pm/state';
7
8
  import Suggestion, {} from '@tiptap/suggestion';
8
9
  const normalizeSearch = (value) => value.toLowerCase().trim();
@@ -156,10 +157,27 @@ export const suggest = {
156
157
  const file = input.files[0];
157
158
  if (!file)
158
159
  return;
159
- const upload = window.__image_uploader ?? fallbackUpload;
160
- const src = await upload(file);
161
- editor.chain().focus().deleteRange(range).setImage({ src }).run();
162
- releaseObjectUrlOnImageSettled(editor.view, src);
160
+ const skeleton = insertUploadSkeleton(editor, {
161
+ kind: 'image',
162
+ height: 220
163
+ });
164
+ try {
165
+ const upload = window.__image_uploader ?? fallbackUpload;
166
+ const src = await upload(file);
167
+ if (skeleton) {
168
+ skeleton.replaceWith({
169
+ type: 'image',
170
+ attrs: { src }
171
+ });
172
+ }
173
+ else {
174
+ editor.chain().focus().setImage({ src }).run();
175
+ }
176
+ releaseObjectUrlOnImageSettled(editor.view, src);
177
+ }
178
+ catch {
179
+ skeleton?.remove();
180
+ }
163
181
  };
164
182
  input.click();
165
183
  }
@@ -34,7 +34,7 @@ export default Node.create({
34
34
  renderHTML({ HTMLAttributes }) {
35
35
  return [
36
36
  'div',
37
- this.options.HTMLAttributes,
37
+ mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
38
38
  ['embed', mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })]
39
39
  ];
40
40
  },
@@ -31,7 +31,7 @@ export default Node.create({
31
31
  renderHTML({ HTMLAttributes }) {
32
32
  return [
33
33
  'div',
34
- this.options.HTMLAttributes,
34
+ mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
35
35
  [
36
36
  'iframe',
37
37
  mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })
@@ -1,4 +1,5 @@
1
1
  import { Plugin } from 'prosemirror-state';
2
+ import { insertUploadSkeleton } from '../upload/skeleton';
2
3
  export const fallbackUpload = async (image) => URL.createObjectURL(image);
3
4
  const OBJECT_URL_PREFIX = 'blob:';
4
5
  const OBJECT_URL_REVOKE_TIMEOUT_MS = 30_000;
@@ -73,14 +74,31 @@ export const dropImagePlugin = () => {
73
74
  const image = item.getAsFile();
74
75
  if (item.type.indexOf('image') === 0) {
75
76
  event.preventDefault();
77
+ const skeleton = insertUploadSkeleton({
78
+ state: view.state,
79
+ view
80
+ }, {
81
+ kind: 'image',
82
+ height: 220
83
+ });
76
84
  if (upload && image) {
77
- upload(image).then((src) => {
78
- const node = schema.nodes.image.create({
79
- src: src
80
- });
81
- const transaction = view.state.tr.replaceSelectionWith(node);
82
- view.dispatch(transaction);
85
+ upload(image)
86
+ .then((src) => {
87
+ if (skeleton) {
88
+ skeleton.replaceWith({
89
+ type: 'image',
90
+ attrs: { src }
91
+ });
92
+ }
93
+ else {
94
+ const node = schema.nodes.image.create({ src });
95
+ const transaction = view.state.tr.replaceSelectionWith(node);
96
+ view.dispatch(transaction);
97
+ }
83
98
  releaseObjectUrlOnImageSettled(view, src);
99
+ })
100
+ .catch(() => {
101
+ skeleton?.remove();
84
102
  });
85
103
  }
86
104
  }
@@ -120,20 +138,49 @@ export const dropImagePlugin = () => {
120
138
  return false;
121
139
  images.forEach(async (image) => {
122
140
  const reader = new FileReader();
141
+ const skeleton = insertUploadSkeleton({
142
+ state: view.state,
143
+ view
144
+ }, {
145
+ kind: 'image',
146
+ height: 220,
147
+ at: coordinates.pos
148
+ });
123
149
  if (upload) {
124
- const src = await upload(image);
125
- const node = schema.nodes.image.create({
126
- src
127
- });
128
- const transaction = view.state.tr.insert(coordinates.pos, node);
129
- view.dispatch(transaction);
130
- releaseObjectUrlOnImageSettled(view, src);
150
+ try {
151
+ const src = await upload(image);
152
+ if (skeleton) {
153
+ skeleton.replaceWith({
154
+ type: 'image',
155
+ attrs: { src }
156
+ });
157
+ }
158
+ else {
159
+ const node = schema.nodes.image.create({ src });
160
+ const transaction = view.state.tr.insert(coordinates.pos, node);
161
+ view.dispatch(transaction);
162
+ }
163
+ releaseObjectUrlOnImageSettled(view, src);
164
+ }
165
+ catch {
166
+ skeleton?.remove();
167
+ }
131
168
  }
132
169
  else {
133
170
  reader.onload = (readerEvent) => {
134
- const node = schema.nodes.image.create({
135
- src: readerEvent.target?.result
136
- });
171
+ const src = readerEvent.target?.result;
172
+ if (typeof src !== 'string') {
173
+ skeleton?.remove();
174
+ return;
175
+ }
176
+ if (skeleton) {
177
+ skeleton.replaceWith({
178
+ type: 'image',
179
+ attrs: { src }
180
+ });
181
+ return;
182
+ }
183
+ const node = schema.nodes.image.create({ src });
137
184
  const transaction = view.state.tr.insert(coordinates.pos, node);
138
185
  view.dispatch(transaction);
139
186
  };
@@ -14,7 +14,7 @@ export default (crossorigin = 'anonymous') => Image.extend({
14
14
  const style = HTMLAttributes.style;
15
15
  return [
16
16
  'figure',
17
- { style },
17
+ { style, 'data-bubble-menu': 'false' },
18
18
  ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
19
19
  ];
20
20
  },
@@ -0,0 +1,8 @@
1
+ import { Extension } from '@tiptap/core';
2
+ export type ResizeOptions = {
3
+ attributeTypes: string[];
4
+ showHandleAlways?: boolean;
5
+ showHandleOnActive?: boolean;
6
+ };
7
+ declare const _default: Extension<ResizeOptions, any>;
8
+ export default _default;
@@ -0,0 +1,454 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import { NodeSelection, Plugin, PluginKey } from '@tiptap/pm/state';
3
+ import { Decoration, DecorationSet } from '@tiptap/pm/view';
4
+ const resizableTypes = ['image', 'iframe', 'embed', 'tiptap-midibus'];
5
+ const typeSet = new Set(resizableTypes);
6
+ const pluginKey = new PluginKey('tiptap-media-height-resize');
7
+ const defaultHeight = {
8
+ image: 360,
9
+ iframe: 600,
10
+ embed: 500,
11
+ 'tiptap-midibus': 600,
12
+ attr: 420
13
+ };
14
+ const minHeight = {
15
+ image: 120,
16
+ iframe: 180,
17
+ embed: 200,
18
+ 'tiptap-midibus': 220,
19
+ attr: 160
20
+ };
21
+ const maxHeight = 1600;
22
+ function isResizableType(value) {
23
+ return typeSet.has(value);
24
+ }
25
+ function clamp(value, min, max) {
26
+ return Math.min(max, Math.max(min, value));
27
+ }
28
+ function parseNumericSize(value) {
29
+ if (typeof value === 'number')
30
+ return Number.isFinite(value) ? value : null;
31
+ if (typeof value !== 'string')
32
+ return null;
33
+ const trimmed = value.trim();
34
+ if (!trimmed)
35
+ return null;
36
+ const normalized = trimmed.toLowerCase().endsWith('px') ? trimmed.slice(0, -2) : trimmed;
37
+ const parsed = Number.parseFloat(normalized);
38
+ return Number.isFinite(parsed) ? parsed : null;
39
+ }
40
+ function normalizeStringAttr(value) {
41
+ if (typeof value !== 'string')
42
+ return null;
43
+ const trimmed = value.trim();
44
+ return trimmed || null;
45
+ }
46
+ function hasResizeHandler(value) {
47
+ if (typeof value === 'string') {
48
+ const normalized = value.trim().toLowerCase();
49
+ if (!normalized)
50
+ return true;
51
+ return !['false', '0', 'off', 'no'].includes(normalized);
52
+ }
53
+ return Boolean(value);
54
+ }
55
+ function normalizeNumericAttr(value) {
56
+ const parsed = parseNumericSize(value);
57
+ if (parsed === null)
58
+ return null;
59
+ return String(Math.round(parsed));
60
+ }
61
+ function normalizeWidthAttr(value) {
62
+ if (typeof value === 'number')
63
+ return String(Math.round(value));
64
+ if (typeof value !== 'string')
65
+ return null;
66
+ const trimmed = value.trim();
67
+ if (!trimmed)
68
+ return null;
69
+ if (trimmed.endsWith('%'))
70
+ return trimmed;
71
+ return normalizeNumericAttr(trimmed);
72
+ }
73
+ function resolveResizeMeta(node) {
74
+ const typeName = node.type.name;
75
+ const kind = isResizableType(typeName)
76
+ ? typeName
77
+ : hasResizeHandler(node.attrs.resizeHandler)
78
+ ? 'attr'
79
+ : null;
80
+ if (!kind)
81
+ return null;
82
+ const minFromAttr = parseNumericSize(node.attrs.minHeight);
83
+ const maxFromAttr = parseNumericSize(node.attrs.maxHeight);
84
+ const resolvedMin = minFromAttr !== null ? Math.max(1, minFromAttr) : minHeight[kind];
85
+ const resolvedMax = maxFromAttr !== null ? Math.max(resolvedMin, maxFromAttr) : maxHeight;
86
+ return {
87
+ kind,
88
+ typeName,
89
+ minHeight: resolvedMin,
90
+ maxHeight: resolvedMax
91
+ };
92
+ }
93
+ function resolveImageRatio(node, element) {
94
+ if (element instanceof HTMLImageElement && element.naturalWidth && element.naturalHeight) {
95
+ return element.naturalWidth / element.naturalHeight;
96
+ }
97
+ const rect = element.getBoundingClientRect();
98
+ if (rect.width > 0 && rect.height > 0) {
99
+ return rect.width / rect.height;
100
+ }
101
+ const width = parseNumericSize(node.attrs.width);
102
+ const height = parseNumericSize(node.attrs.height);
103
+ if (width && height)
104
+ return width / height;
105
+ return 1;
106
+ }
107
+ function resolveStartHeight(kind, node, element) {
108
+ const rect = element.getBoundingClientRect();
109
+ if (rect.height > 0)
110
+ return rect.height;
111
+ const fromAttr = parseNumericSize(node.attrs.height);
112
+ if (fromAttr !== null)
113
+ return fromAttr;
114
+ return defaultHeight[kind];
115
+ }
116
+ function resolveTargetElement(view, pos, resizeMeta, node) {
117
+ const nodeDom = view.nodeDOM(pos);
118
+ if (!(nodeDom instanceof HTMLElement))
119
+ return null;
120
+ if (resizeMeta.kind === 'image') {
121
+ if (nodeDom instanceof HTMLImageElement)
122
+ return nodeDom;
123
+ return nodeDom.querySelector('img');
124
+ }
125
+ if (resizeMeta.kind === 'iframe') {
126
+ if (nodeDom instanceof HTMLIFrameElement)
127
+ return nodeDom;
128
+ return nodeDom.querySelector('iframe');
129
+ }
130
+ if (resizeMeta.kind === 'embed') {
131
+ return (nodeDom.querySelector('[data-tiptap-pdf-container]') ||
132
+ nodeDom.querySelector('embed') ||
133
+ nodeDom);
134
+ }
135
+ if (resizeMeta.kind === 'attr') {
136
+ const preferredSelector = normalizeStringAttr(node.attrs.resizeTarget);
137
+ if (preferredSelector) {
138
+ try {
139
+ const preferredTarget = nodeDom.querySelector(preferredSelector);
140
+ if (preferredTarget)
141
+ return preferredTarget;
142
+ }
143
+ catch {
144
+ // ignore invalid selector values
145
+ }
146
+ }
147
+ return (nodeDom.querySelector('[data-tiptap-resize-target]') ||
148
+ nodeDom.querySelector('[data-resize-target]') ||
149
+ nodeDom.querySelector('iframe, embed, img') ||
150
+ nodeDom);
151
+ }
152
+ return (nodeDom.querySelector('[data-tiptap-midibus-frame]') ||
153
+ nodeDom.querySelector('iframe') ||
154
+ nodeDom);
155
+ }
156
+ function buildResizeAttrs(kind, node, height, imageRatio) {
157
+ const attrs = { ...node.attrs };
158
+ const roundedHeight = String(Math.round(height));
159
+ if (kind === 'image') {
160
+ const roundedWidth = String(Math.max(1, Math.round(height * imageRatio)));
161
+ return { ...attrs, width: roundedWidth, height: roundedHeight };
162
+ }
163
+ if (kind === 'iframe' || kind === 'embed') {
164
+ return { ...attrs, width: attrs.width || '100%', height: roundedHeight };
165
+ }
166
+ return { ...attrs, height: roundedHeight };
167
+ }
168
+ function createResizeHandleDecoration(nodePos, widgetPos, resizeMeta) {
169
+ return Decoration.widget(widgetPos, () => {
170
+ const anchor = document.createElement('div');
171
+ anchor.className = 'tiptap-media-resize-anchor';
172
+ const button = document.createElement('button');
173
+ button.type = 'button';
174
+ button.className = 'tiptap-media-resize-handle';
175
+ button.dataset.resizePos = String(nodePos);
176
+ button.dataset.resizeKind = resizeMeta.kind;
177
+ button.setAttribute('aria-label', 'Resize media height');
178
+ anchor.append(button);
179
+ return anchor;
180
+ }, { side: 1, key: `media-resize-${nodePos}-${resizeMeta.typeName}-${resizeMeta.kind}` });
181
+ }
182
+ function tryCreateNodeSelection(doc, pos) {
183
+ if (pos < 0 || pos > doc.content.size)
184
+ return null;
185
+ const node = doc.nodeAt(pos);
186
+ if (!node || node.type.spec.selectable === false)
187
+ return null;
188
+ try {
189
+ return NodeSelection.create(doc, pos);
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ }
195
+ export default Extension.create({
196
+ name: 'tiptap-media-height-resize',
197
+ addOptions() {
198
+ return {
199
+ attributeTypes: [],
200
+ showHandleAlways: true,
201
+ showHandleOnActive: true
202
+ };
203
+ },
204
+ addGlobalAttributes() {
205
+ const attributeTypes = Array.from(new Set([...(this.options.attributeTypes || []), ...resizableTypes]));
206
+ return [
207
+ {
208
+ types: ['image'],
209
+ attributes: {
210
+ width: {
211
+ default: null,
212
+ parseHTML: (element) => normalizeWidthAttr(element.getAttribute('width') || element.style.width),
213
+ renderHTML: (attributes) => {
214
+ const width = normalizeWidthAttr(attributes.width);
215
+ return width ? { width } : {};
216
+ }
217
+ },
218
+ height: {
219
+ default: null,
220
+ parseHTML: (element) => normalizeNumericAttr(element.getAttribute('height') || element.style.height),
221
+ renderHTML: (attributes) => {
222
+ const height = normalizeNumericAttr(attributes.height);
223
+ return height ? { height } : {};
224
+ }
225
+ }
226
+ }
227
+ },
228
+ {
229
+ types: ['iframe'],
230
+ attributes: {
231
+ width: {
232
+ default: '100%',
233
+ parseHTML: (element) => normalizeWidthAttr(element.getAttribute('width') || element.style.width) || '100%',
234
+ renderHTML: (attributes) => {
235
+ const width = normalizeWidthAttr(attributes.width) || '100%';
236
+ return { width };
237
+ }
238
+ },
239
+ height: {
240
+ default: '600',
241
+ parseHTML: (element) => normalizeNumericAttr(element.getAttribute('height') || element.style.height) || '600',
242
+ renderHTML: (attributes) => {
243
+ const height = normalizeNumericAttr(attributes.height) || '600';
244
+ return { height };
245
+ }
246
+ }
247
+ }
248
+ },
249
+ {
250
+ types: attributeTypes,
251
+ attributes: {
252
+ resizeHandler: {
253
+ default: false,
254
+ parseHTML: (element) => hasResizeHandler(element.getAttribute('data-resize-handler') ||
255
+ element.getAttribute('resize-handler')),
256
+ renderHTML: (attributes) => hasResizeHandler(attributes.resizeHandler)
257
+ ? { 'data-resize-handler': 'true' }
258
+ : {}
259
+ },
260
+ resizeTarget: {
261
+ default: null,
262
+ parseHTML: (element) => normalizeStringAttr(element.getAttribute('data-resize-target')),
263
+ renderHTML: (attributes) => {
264
+ const resizeTarget = normalizeStringAttr(attributes.resizeTarget);
265
+ return resizeTarget ? { 'data-resize-target': resizeTarget } : {};
266
+ }
267
+ },
268
+ minHeight: {
269
+ default: null,
270
+ parseHTML: (element) => normalizeNumericAttr(element.getAttribute('data-resize-min-height')),
271
+ renderHTML: (attributes) => {
272
+ const minHeight = normalizeNumericAttr(attributes.minHeight);
273
+ return minHeight ? { 'data-resize-min-height': minHeight } : {};
274
+ }
275
+ },
276
+ maxHeight: {
277
+ default: null,
278
+ parseHTML: (element) => normalizeNumericAttr(element.getAttribute('data-resize-max-height')),
279
+ renderHTML: (attributes) => {
280
+ const maxHeight = normalizeNumericAttr(attributes.maxHeight);
281
+ return maxHeight ? { 'data-resize-max-height': maxHeight } : {};
282
+ }
283
+ }
284
+ }
285
+ }
286
+ ];
287
+ },
288
+ addProseMirrorPlugins() {
289
+ let removeDragListeners = null;
290
+ return [
291
+ new Plugin({
292
+ key: pluginKey,
293
+ appendTransaction: (transactions, _oldState, newState) => {
294
+ if (!transactions.some((tr) => tr.docChanged))
295
+ return null;
296
+ if (newState.selection instanceof NodeSelection)
297
+ return null;
298
+ const nodeBefore = newState.selection.$from.nodeBefore;
299
+ if (!nodeBefore)
300
+ return null;
301
+ const resizeMeta = resolveResizeMeta(nodeBefore);
302
+ if (!resizeMeta)
303
+ return null;
304
+ const typeName = nodeBefore.type.name;
305
+ const nodePos = newState.selection.from - nodeBefore.nodeSize;
306
+ if (nodePos < 0)
307
+ return null;
308
+ if (newState.doc.nodeAt(nodePos)?.type.name !== typeName)
309
+ return null;
310
+ const nodeSelection = tryCreateNodeSelection(newState.doc, nodePos);
311
+ if (!nodeSelection)
312
+ return null;
313
+ return newState.tr.setSelection(nodeSelection);
314
+ },
315
+ props: {
316
+ decorations: (state) => {
317
+ if (!this.editor.isEditable)
318
+ return DecorationSet.empty;
319
+ const showHandleAlways = this.options.showHandleAlways !== false;
320
+ const showHandleOnActive = this.options.showHandleOnActive !== false;
321
+ if (!showHandleAlways && !showHandleOnActive)
322
+ return DecorationSet.empty;
323
+ const decorations = [];
324
+ const handled = new Set();
325
+ if (showHandleAlways) {
326
+ state.doc.descendants((node, pos) => {
327
+ const resizeMeta = resolveResizeMeta(node);
328
+ if (!resizeMeta || resizeMeta.kind === 'image' || node.isInline)
329
+ return;
330
+ decorations.push(createResizeHandleDecoration(pos, pos + node.nodeSize, resizeMeta));
331
+ handled.add(pos);
332
+ });
333
+ }
334
+ if (showHandleOnActive && state.selection instanceof NodeSelection) {
335
+ const pos = state.selection.from;
336
+ const resizeMeta = resolveResizeMeta(state.selection.node);
337
+ if (resizeMeta && !handled.has(pos)) {
338
+ decorations.push(createResizeHandleDecoration(pos, pos + state.selection.node.nodeSize, resizeMeta));
339
+ }
340
+ }
341
+ if (!decorations.length)
342
+ return DecorationSet.empty;
343
+ return DecorationSet.create(state.doc, decorations);
344
+ },
345
+ handleDOMEvents: {
346
+ mousedown: (view, event) => {
347
+ if (!this.editor.isEditable)
348
+ return false;
349
+ if (!(event.target instanceof HTMLElement))
350
+ return false;
351
+ const handle = event.target.closest('.tiptap-media-resize-handle');
352
+ if (!handle)
353
+ return false;
354
+ const pos = Number.parseInt(handle.dataset.resizePos || '', 10);
355
+ if (!Number.isFinite(pos))
356
+ return false;
357
+ const node = view.state.doc.nodeAt(pos);
358
+ if (!node)
359
+ return false;
360
+ const resizeMeta = resolveResizeMeta(node);
361
+ if (!resizeMeta)
362
+ return false;
363
+ const resizeKind = handle.dataset.resizeKind;
364
+ if (resizeKind && resizeMeta.kind !== resizeKind)
365
+ return false;
366
+ const target = resolveTargetElement(view, pos, resizeMeta, node);
367
+ if (!target)
368
+ return false;
369
+ event.preventDefault();
370
+ event.stopPropagation();
371
+ const startY = event.clientY;
372
+ const startHeight = resolveStartHeight(resizeMeta.kind, node, target);
373
+ const imageRatio = resizeMeta.kind === 'image' ? resolveImageRatio(node, target) : 1;
374
+ const shouldShowProxy = resizeMeta.kind !== 'image';
375
+ let resizeProxy = null;
376
+ let restoreTarget = null;
377
+ let frame = 0;
378
+ let pendingHeight = startHeight;
379
+ if (shouldShowProxy && target.parentElement) {
380
+ const targetElement = target;
381
+ const originalDisplay = targetElement.style.display;
382
+ resizeProxy = document.createElement('div');
383
+ resizeProxy.className = 'tiptap-media-resize-proxy';
384
+ resizeProxy.style.height = `${Math.round(startHeight)}px`;
385
+ target.parentElement.insertBefore(resizeProxy, targetElement);
386
+ targetElement.style.display = 'none';
387
+ restoreTarget = () => {
388
+ targetElement.style.display = originalDisplay;
389
+ resizeProxy?.remove();
390
+ resizeProxy = null;
391
+ restoreTarget = null;
392
+ };
393
+ }
394
+ const dispatchHeight = (height) => {
395
+ const current = view.state.doc.nodeAt(pos);
396
+ if (!current || current.type.name !== resizeMeta.typeName)
397
+ return;
398
+ const currentMeta = resolveResizeMeta(current);
399
+ if (!currentMeta)
400
+ return;
401
+ const nextAttrs = buildResizeAttrs(currentMeta.kind, current, height, imageRatio);
402
+ const nextWidth = 'width' in nextAttrs ? nextAttrs.width : current.attrs.width;
403
+ if (nextAttrs.height === current.attrs.height && nextWidth === current.attrs.width)
404
+ return;
405
+ const tr = view.state.tr.setNodeMarkup(pos, current.type, nextAttrs);
406
+ view.dispatch(tr);
407
+ };
408
+ const previousCursor = document.body.style.cursor;
409
+ const previousSelect = document.body.style.userSelect;
410
+ document.body.style.cursor = 'ns-resize';
411
+ document.body.style.userSelect = 'none';
412
+ const onMove = (moveEvent) => {
413
+ const nextHeight = clamp(startHeight + moveEvent.clientY - startY, resizeMeta.minHeight, resizeMeta.maxHeight);
414
+ pendingHeight = nextHeight;
415
+ if (resizeProxy)
416
+ resizeProxy.style.height = `${Math.round(nextHeight)}px`;
417
+ if (shouldShowProxy)
418
+ return;
419
+ if (frame)
420
+ cancelAnimationFrame(frame);
421
+ frame = requestAnimationFrame(() => dispatchHeight(nextHeight));
422
+ };
423
+ const cleanup = () => {
424
+ if (frame)
425
+ cancelAnimationFrame(frame);
426
+ window.removeEventListener('mousemove', onMove);
427
+ window.removeEventListener('mouseup', onUp);
428
+ document.body.style.cursor = previousCursor;
429
+ document.body.style.userSelect = previousSelect;
430
+ restoreTarget?.();
431
+ removeDragListeners = null;
432
+ };
433
+ const onUp = () => {
434
+ if (shouldShowProxy)
435
+ dispatchHeight(pendingHeight);
436
+ cleanup();
437
+ };
438
+ removeDragListeners?.();
439
+ window.addEventListener('mousemove', onMove);
440
+ window.addEventListener('mouseup', onUp);
441
+ removeDragListeners = cleanup;
442
+ return true;
443
+ }
444
+ }
445
+ },
446
+ view: () => ({
447
+ destroy: () => {
448
+ removeDragListeners?.();
449
+ }
450
+ })
451
+ })
452
+ ];
453
+ }
454
+ });