@seorii/tiptap 0.4.3 → 0.4.5

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.
@@ -29,33 +29,37 @@ function matchItem(item, query, compactQuery) {
29
29
  return [item.title, item.subtitle ?? '', ...(item.keywords ?? [])].some((value) => matchQuery(value, query, compactQuery));
30
30
  }
31
31
  function fixRange(editor, rawRange, split = '/') {
32
- const range = { ...rawRange };
33
- const { state } = editor.view;
34
- const { selection, doc } = state;
35
- if (selection.$to.nodeBefore?.text?.includes?.(split)) {
36
- range.from = range.to;
37
- while (range.from > 0 && doc.textBetween(range.from - 1, range.from) !== split) {
38
- try {
39
- range.from -= 1;
40
- }
41
- catch {
42
- range.from += 2;
32
+ const doc = editor.state.doc;
33
+ const docSize = doc.content.size;
34
+ const range = {
35
+ from: Math.max(0, Math.min(rawRange.from, docSize)),
36
+ to: Math.max(0, Math.min(rawRange.to, docSize))
37
+ };
38
+ if (range.to < range.from)
39
+ range.to = range.from;
40
+ if (doc.textBetween(range.from, Math.min(range.from + 1, docSize)) !== split && range.from > 0) {
41
+ const minFrom = Math.max(0, range.from - 64);
42
+ for (let cursor = range.from; cursor > minFrom; cursor -= 1) {
43
+ const char = doc.textBetween(cursor - 1, cursor);
44
+ if (!char || /\s/.test(char))
43
45
  break;
44
- }
46
+ if (char !== split)
47
+ continue;
48
+ range.from = cursor - 1;
49
+ break;
45
50
  }
46
- range.from -= 1;
47
51
  }
48
- while (range.to < selection.to && doc.textBetween(range.to, range.to + 1) !== ' ') {
49
- try {
50
- range.to += 1;
51
- }
52
- catch {
53
- range.to -= 1;
52
+ while (range.to < docSize) {
53
+ const next = doc.textBetween(range.to, range.to + 1);
54
+ if (!next || /\s/.test(next))
54
55
  break;
55
- }
56
+ range.to += 1;
56
57
  }
57
58
  return range;
58
59
  }
60
+ function clampDocPos(editor, pos) {
61
+ return Math.max(0, Math.min(pos, editor.state.doc.content.size));
62
+ }
59
63
  export function getDetail(editor, range, option) {
60
64
  slashState.selection = () => {
61
65
  editor.chain().focus().deleteRange(fixRange(editor, range)).run();
@@ -147,7 +151,18 @@ export const suggest = {
147
151
  subtitle: i18n('imageInfo'),
148
152
  keywords: createKeywords(['image', 'imageInfo']),
149
153
  command: ({ editor, range }) => {
150
- editor.chain().focus().deleteRange(fixRange(editor, range)).run();
154
+ const fixedRange = fixRange(editor, range);
155
+ editor.chain().focus().deleteRange(fixedRange).run();
156
+ let insertAt = clampDocPos(editor, fixedRange.from);
157
+ let insertParagraph = true;
158
+ const { selection } = editor.state;
159
+ if (selection.empty &&
160
+ selection.$from.depth > 0 &&
161
+ selection.$from.parent.isTextblock &&
162
+ selection.$from.parent.content.size === 0) {
163
+ insertAt = clampDocPos(editor, selection.$from.before(selection.$from.depth));
164
+ insertParagraph = false;
165
+ }
151
166
  const input = document.createElement('input');
152
167
  input.type = 'file';
153
168
  input.accept = 'image/*';
@@ -159,10 +174,13 @@ export const suggest = {
159
174
  return;
160
175
  const skeleton = insertUploadSkeleton(editor, {
161
176
  kind: 'image',
162
- height: 220
177
+ height: 220,
178
+ at: insertAt,
179
+ insertParagraph
163
180
  });
164
181
  try {
165
- const upload = window.__image_uploader ?? fallbackUpload;
182
+ const imageUploader = window.__image_uploader;
183
+ const upload = imageUploader ?? fallbackUpload;
166
184
  const src = await upload(file);
167
185
  if (skeleton) {
168
186
  skeleton.replaceWith({
@@ -171,9 +189,16 @@ export const suggest = {
171
189
  });
172
190
  }
173
191
  else {
174
- editor.chain().focus().setImage({ src }).run();
192
+ editor
193
+ .chain()
194
+ .insertContentAt(clampDocPos(editor, insertAt), insertParagraph
195
+ ? [{ type: 'image', attrs: { src } }, { type: 'paragraph' }]
196
+ : { type: 'image', attrs: { src } })
197
+ .run();
198
+ }
199
+ if (imageUploader) {
200
+ releaseObjectUrlOnImageSettled(editor.view, src);
175
201
  }
176
- releaseObjectUrlOnImageSettled(editor.view, src);
177
202
  }
178
203
  catch {
179
204
  skeleton?.remove();
@@ -67,17 +67,17 @@ export const dropImagePlugin = () => {
67
67
  props: {
68
68
  handleDOMEvents: {
69
69
  paste(view, event) {
70
- const upload = window.__image_uploader || fallbackUpload;
70
+ const imageUploader = window.__image_uploader;
71
+ const upload = imageUploader || fallbackUpload;
71
72
  const items = Array.from(event.clipboardData?.items || []);
72
73
  const { schema } = view.state;
74
+ let handledImagePaste = false;
73
75
  items.forEach((item) => {
74
76
  const image = item.getAsFile();
75
77
  if (item.type.indexOf('image') === 0) {
78
+ handledImagePaste = true;
76
79
  event.preventDefault();
77
- const skeleton = insertUploadSkeleton({
78
- state: view.state,
79
- view
80
- }, {
80
+ const skeleton = insertUploadSkeleton({ view }, {
81
81
  kind: 'image',
82
82
  height: 220
83
83
  });
@@ -95,7 +95,9 @@ export const dropImagePlugin = () => {
95
95
  const transaction = view.state.tr.replaceSelectionWith(node);
96
96
  view.dispatch(transaction);
97
97
  }
98
- releaseObjectUrlOnImageSettled(view, src);
98
+ if (imageUploader) {
99
+ releaseObjectUrlOnImageSettled(view, src);
100
+ }
99
101
  })
100
102
  .catch(() => {
101
103
  skeleton?.remove();
@@ -116,10 +118,11 @@ export const dropImagePlugin = () => {
116
118
  reader.readAsDataURL(image);
117
119
  }
118
120
  });
119
- return false;
121
+ return handledImagePaste;
120
122
  },
121
123
  drop: (view, event) => {
122
- const upload = window.__image_uploader || fallbackUpload;
124
+ const imageUploader = window.__image_uploader;
125
+ const upload = imageUploader || fallbackUpload;
123
126
  const hasFiles = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
124
127
  if (!hasFiles) {
125
128
  return false;
@@ -138,10 +141,7 @@ export const dropImagePlugin = () => {
138
141
  return false;
139
142
  images.forEach(async (image) => {
140
143
  const reader = new FileReader();
141
- const skeleton = insertUploadSkeleton({
142
- state: view.state,
143
- view
144
- }, {
144
+ const skeleton = insertUploadSkeleton({ view }, {
145
145
  kind: 'image',
146
146
  height: 220,
147
147
  at: coordinates.pos
@@ -160,7 +160,9 @@ export const dropImagePlugin = () => {
160
160
  const transaction = view.state.tr.insert(coordinates.pos, node);
161
161
  view.dispatch(transaction);
162
162
  }
163
- releaseObjectUrlOnImageSettled(view, src);
163
+ if (imageUploader) {
164
+ releaseObjectUrlOnImageSettled(view, src);
165
+ }
164
166
  }
165
167
  catch {
166
168
  skeleton?.remove();
@@ -28,6 +28,17 @@ const aspectRatioOptions = [
28
28
  { label: '4:3', value: '4:3' },
29
29
  { label: '9:16', value: '9:16' }
30
30
  ];
31
+ const horizontalAlignValues = ['left', 'center', 'right'];
32
+ const horizontalAlignOptions = [
33
+ { label: 'Auto', value: null },
34
+ { label: 'Left', value: 'left' },
35
+ { label: 'Center', value: 'center' },
36
+ { label: 'Right', value: 'right' }
37
+ ];
38
+ const widthPresetOptions = [
39
+ { label: '100%', value: '100%' },
40
+ { label: '50%', value: '50%' }
41
+ ];
31
42
  function isResizableType(value) {
32
43
  return typeSet.has(value);
33
44
  }
@@ -109,6 +120,28 @@ function sameAspectRatio(left, right) {
109
120
  return false;
110
121
  return Math.abs(leftRatio - rightRatio) <= 0.001;
111
122
  }
123
+ function normalizeHorizontalAlignAttr(value) {
124
+ if (typeof value !== 'string')
125
+ return null;
126
+ const normalized = value.trim().toLowerCase();
127
+ if (!normalized)
128
+ return null;
129
+ return horizontalAlignValues.includes(normalized)
130
+ ? normalized
131
+ : null;
132
+ }
133
+ function normalizeWidthPreset(value) {
134
+ if (typeof value !== 'string' && typeof value !== 'number')
135
+ return null;
136
+ const normalized = String(value).trim();
137
+ if (!normalized)
138
+ return null;
139
+ if (normalized === '100' || normalized === '100%')
140
+ return '100%';
141
+ if (normalized === '50' || normalized === '50%')
142
+ return '50%';
143
+ return null;
144
+ }
112
145
  function normalizeStringAttr(value) {
113
146
  if (typeof value !== 'string')
114
147
  return null;
@@ -261,41 +294,209 @@ function buildResizeAttrs(kind, node, height, imageRatio, aspectRatio = normaliz
261
294
  function canUseAspectRatioPreset(kind) {
262
295
  return kind === 'iframe' || kind === 'embed';
263
296
  }
297
+ function canUseLayoutOptions(kind) {
298
+ return kind === 'image' || canUseAspectRatioPreset(kind);
299
+ }
300
+ function createToolbarGroupIcon(group) {
301
+ const icon = document.createElement('span');
302
+ icon.className = 'tiptap-media-toolbar-group-icon';
303
+ icon.dataset.group = group;
304
+ icon.setAttribute('aria-hidden', 'true');
305
+ return icon;
306
+ }
264
307
  function createResizeHandleDecoration(nodePos, widgetPos, resizeMeta, node) {
308
+ const widthKey = normalizeWidthAttr(node.attrs.width) || 'auto';
309
+ const aspectRatioKey = normalizeAspectRatioAttr(node.attrs.aspectRatio) || 'auto';
310
+ const horizontalAlignKey = normalizeHorizontalAlignAttr(node.attrs.horizontalAlign) || 'auto';
265
311
  return Decoration.widget(widgetPos, () => {
266
312
  const anchor = document.createElement('div');
267
313
  anchor.className = 'tiptap-media-resize-anchor';
314
+ const isImageAnchor = resizeMeta.kind === 'image';
315
+ let controlsContainer = anchor;
316
+ if (isImageAnchor) {
317
+ anchor.classList.add('is-image-anchor');
318
+ const controls = document.createElement('div');
319
+ controls.className = 'tiptap-media-resize-controls';
320
+ controls.style.left = '0px';
321
+ controls.style.width = '100%';
322
+ anchor.append(controls);
323
+ controlsContainer = controls;
324
+ }
325
+ else {
326
+ const normalizedWidth = normalizeWidthAttr(node.attrs.width);
327
+ if (normalizedWidth) {
328
+ if (normalizedWidth.endsWith('%')) {
329
+ anchor.style.width = normalizedWidth;
330
+ }
331
+ else {
332
+ const numericWidth = parseNumericSize(normalizedWidth);
333
+ if (numericWidth !== null) {
334
+ anchor.style.width = `${Math.round(numericWidth)}px`;
335
+ }
336
+ }
337
+ anchor.style.maxWidth = '100%';
338
+ }
339
+ const horizontalAlign = normalizeHorizontalAlignAttr(node.attrs.horizontalAlign);
340
+ if (horizontalAlign === 'center') {
341
+ anchor.style.marginLeft = 'auto';
342
+ anchor.style.marginRight = 'auto';
343
+ }
344
+ else if (horizontalAlign === 'right') {
345
+ anchor.style.marginLeft = 'auto';
346
+ anchor.style.marginRight = '0';
347
+ }
348
+ else {
349
+ anchor.style.marginLeft = '0';
350
+ anchor.style.marginRight = '0';
351
+ }
352
+ }
353
+ if (isImageAnchor) {
354
+ requestAnimationFrame(() => {
355
+ if (!anchor.isConnected)
356
+ return;
357
+ let candidate = anchor.previousElementSibling;
358
+ let imageElement = null;
359
+ while (candidate && !imageElement) {
360
+ if (candidate instanceof HTMLImageElement) {
361
+ imageElement = candidate;
362
+ }
363
+ else if (candidate instanceof HTMLElement) {
364
+ imageElement = candidate.querySelector('img');
365
+ }
366
+ candidate = candidate.previousElementSibling;
367
+ }
368
+ if (!imageElement && anchor.parentElement instanceof HTMLElement) {
369
+ imageElement =
370
+ anchor.parentElement.querySelector('figure.ProseMirror-selectednode img, img.ProseMirror-selectednode') || null;
371
+ }
372
+ if (!(imageElement instanceof HTMLElement))
373
+ return;
374
+ if (!(controlsContainer instanceof HTMLElement))
375
+ return;
376
+ const rect = imageElement.getBoundingClientRect();
377
+ if (rect.width <= 0 || rect.height <= 0)
378
+ return;
379
+ const anchorRect = anchor.getBoundingClientRect();
380
+ const leftOffset = rect.left - anchorRect.left;
381
+ const maxLeft = Math.max(0, anchorRect.width - rect.width);
382
+ const clampedLeft = clamp(leftOffset, 0, maxLeft);
383
+ const topOffset = rect.bottom - anchorRect.top + 6;
384
+ controlsContainer.style.top = `${Math.round(topOffset)}px`;
385
+ controlsContainer.style.left = `${Math.round(clampedLeft)}px`;
386
+ controlsContainer.style.width = `${Math.round(rect.width)}px`;
387
+ });
388
+ }
268
389
  const button = document.createElement('button');
269
390
  button.type = 'button';
270
391
  button.className = 'tiptap-media-resize-handle';
271
392
  button.dataset.resizePos = String(nodePos);
272
393
  button.dataset.resizeKind = resizeMeta.kind;
273
- button.setAttribute('aria-label', 'Resize media height (click for aspect ratio)');
274
- anchor.append(button);
275
- if (canUseAspectRatioPreset(resizeMeta.kind)) {
394
+ button.setAttribute('aria-label', canUseLayoutOptions(resizeMeta.kind)
395
+ ? 'Resize media height (click for layout options)'
396
+ : 'Resize media height');
397
+ controlsContainer.append(button);
398
+ if (canUseLayoutOptions(resizeMeta.kind)) {
276
399
  const selectedAspectRatio = normalizeAspectRatioAttr(node.attrs.aspectRatio);
400
+ const selectedHorizontalAlign = normalizeHorizontalAlignAttr(node.attrs.horizontalAlign);
401
+ const selectedWidthPreset = normalizeWidthPreset(node.attrs.width);
402
+ const supportsAspectRatio = canUseAspectRatioPreset(resizeMeta.kind);
403
+ const supportsBottomWidthPreset = resizeMeta.kind === 'image';
404
+ if (supportsAspectRatio) {
405
+ const widthHandle = document.createElement('button');
406
+ widthHandle.type = 'button';
407
+ widthHandle.className = 'tiptap-media-width-resize-handle';
408
+ widthHandle.dataset.resizePos = String(nodePos);
409
+ widthHandle.dataset.resizeKind = resizeMeta.kind;
410
+ widthHandle.setAttribute('aria-label', 'Resize media width (click for width presets)');
411
+ controlsContainer.append(widthHandle);
412
+ }
277
413
  const toolbar = document.createElement('div');
278
414
  toolbar.className = 'tiptap-media-aspect-ratio-toolbar';
279
415
  toolbar.setAttribute('role', 'toolbar');
280
- toolbar.setAttribute('aria-label', 'Aspect ratio presets');
416
+ toolbar.setAttribute('aria-label', 'Media resize options');
281
417
  toolbar.dataset.resizePos = String(nodePos);
282
- for (const option of aspectRatioOptions) {
418
+ let hasToolbarItems = false;
419
+ if (supportsAspectRatio) {
420
+ toolbar.append(createToolbarGroupIcon('aspect'));
421
+ for (const option of aspectRatioOptions) {
422
+ const optionButton = document.createElement('button');
423
+ optionButton.type = 'button';
424
+ optionButton.className = 'tiptap-media-aspect-ratio-option';
425
+ optionButton.dataset.resizePos = String(nodePos);
426
+ optionButton.dataset.aspectRatio = option.value ?? 'auto';
427
+ optionButton.textContent = option.label;
428
+ const isActive = option.value
429
+ ? sameAspectRatio(option.value, selectedAspectRatio)
430
+ : !selectedAspectRatio;
431
+ optionButton.setAttribute('aria-pressed', isActive ? 'true' : 'false');
432
+ toolbar.append(optionButton);
433
+ }
434
+ hasToolbarItems = true;
435
+ }
436
+ if (hasToolbarItems) {
437
+ const separator = document.createElement('span');
438
+ separator.className = 'tiptap-media-toolbar-separator';
439
+ separator.setAttribute('aria-hidden', 'true');
440
+ toolbar.append(separator);
441
+ }
442
+ toolbar.append(createToolbarGroupIcon('align'));
443
+ for (const option of horizontalAlignOptions) {
283
444
  const optionButton = document.createElement('button');
284
445
  optionButton.type = 'button';
285
- optionButton.className = 'tiptap-media-aspect-ratio-option';
446
+ optionButton.className = 'tiptap-media-horizontal-align-option';
286
447
  optionButton.dataset.resizePos = String(nodePos);
287
- optionButton.dataset.aspectRatio = option.value ?? 'auto';
448
+ optionButton.dataset.horizontalAlign = option.value ?? 'auto';
288
449
  optionButton.textContent = option.label;
289
450
  const isActive = option.value
290
- ? sameAspectRatio(option.value, selectedAspectRatio)
291
- : !selectedAspectRatio;
451
+ ? option.value === selectedHorizontalAlign
452
+ : !selectedHorizontalAlign;
292
453
  optionButton.setAttribute('aria-pressed', isActive ? 'true' : 'false');
293
454
  toolbar.append(optionButton);
294
455
  }
295
- anchor.append(toolbar);
456
+ hasToolbarItems = true;
457
+ if (supportsBottomWidthPreset) {
458
+ const separator = document.createElement('span');
459
+ separator.className = 'tiptap-media-toolbar-separator';
460
+ separator.setAttribute('aria-hidden', 'true');
461
+ toolbar.append(separator);
462
+ toolbar.append(createToolbarGroupIcon('width'));
463
+ for (const option of widthPresetOptions) {
464
+ const optionButton = document.createElement('button');
465
+ optionButton.type = 'button';
466
+ optionButton.className = 'tiptap-media-width-option';
467
+ optionButton.dataset.resizePos = String(nodePos);
468
+ optionButton.dataset.widthPreset = option.value;
469
+ optionButton.textContent = option.label;
470
+ optionButton.setAttribute('aria-pressed', selectedWidthPreset === option.value ? 'true' : 'false');
471
+ toolbar.append(optionButton);
472
+ }
473
+ }
474
+ controlsContainer.append(toolbar);
475
+ if (supportsAspectRatio) {
476
+ const widthToolbar = document.createElement('div');
477
+ widthToolbar.className = 'tiptap-media-width-toolbar';
478
+ widthToolbar.setAttribute('role', 'toolbar');
479
+ widthToolbar.setAttribute('aria-label', 'Media width presets');
480
+ widthToolbar.dataset.resizePos = String(nodePos);
481
+ widthToolbar.append(createToolbarGroupIcon('width'));
482
+ for (const option of widthPresetOptions) {
483
+ const optionButton = document.createElement('button');
484
+ optionButton.type = 'button';
485
+ optionButton.className = 'tiptap-media-width-option';
486
+ optionButton.dataset.resizePos = String(nodePos);
487
+ optionButton.dataset.widthPreset = option.value;
488
+ optionButton.textContent = option.label;
489
+ optionButton.setAttribute('aria-pressed', selectedWidthPreset === option.value ? 'true' : 'false');
490
+ widthToolbar.append(optionButton);
491
+ }
492
+ controlsContainer.append(widthToolbar);
493
+ }
296
494
  }
297
495
  return anchor;
298
- }, { side: 1, key: `media-resize-${nodePos}-${resizeMeta.typeName}-${resizeMeta.kind}` });
496
+ }, {
497
+ side: 1,
498
+ key: `media-resize-${nodePos}-${resizeMeta.typeName}-${resizeMeta.kind}-${widthKey}-${aspectRatioKey}-${horizontalAlignKey}`
499
+ });
299
500
  }
300
501
  function tryCreateNodeSelection(doc, pos) {
301
502
  if (pos < 0 || pos > doc.content.size)
@@ -413,6 +614,14 @@ export default Extension.create({
413
614
  const aspectRatio = normalizeAspectRatioAttr(attributes.aspectRatio);
414
615
  return aspectRatio ? { 'data-resize-aspect-ratio': aspectRatio } : {};
415
616
  }
617
+ },
618
+ horizontalAlign: {
619
+ default: null,
620
+ parseHTML: (element) => normalizeHorizontalAlignAttr(element.getAttribute('data-resize-horizontal-align')),
621
+ renderHTML: (attributes) => {
622
+ const horizontalAlign = normalizeHorizontalAlignAttr(attributes.horizontalAlign);
623
+ return horizontalAlign ? { 'data-resize-horizontal-align': horizontalAlign } : {};
624
+ }
416
625
  }
417
626
  }
418
627
  }
@@ -422,11 +631,11 @@ export default Extension.create({
422
631
  let removeDragListeners = null;
423
632
  const closeOpenToolbars = (view, except = null) => {
424
633
  view.dom
425
- .querySelectorAll('.tiptap-media-resize-anchor.is-toolbar-open')
634
+ .querySelectorAll('.tiptap-media-resize-anchor.is-toolbar-open, .tiptap-media-resize-anchor.is-width-toolbar-open')
426
635
  .forEach((anchor) => {
427
636
  if (except && anchor === except)
428
637
  return;
429
- anchor.classList.remove('is-toolbar-open');
638
+ anchor.classList.remove('is-toolbar-open', 'is-width-toolbar-open');
430
639
  });
431
640
  };
432
641
  return [
@@ -490,6 +699,60 @@ export default Extension.create({
490
699
  return false;
491
700
  if (!(event.target instanceof HTMLElement))
492
701
  return false;
702
+ const widthOption = event.target.closest('.tiptap-media-width-option');
703
+ if (widthOption) {
704
+ event.preventDefault();
705
+ event.stopPropagation();
706
+ const pos = Number.parseInt(widthOption.dataset.resizePos || '', 10);
707
+ if (!Number.isFinite(pos))
708
+ return true;
709
+ const node = view.state.doc.nodeAt(pos);
710
+ if (!node)
711
+ return true;
712
+ const resizeMeta = resolveResizeMeta(node);
713
+ if (!resizeMeta || !canUseLayoutOptions(resizeMeta.kind))
714
+ return true;
715
+ const widthPreset = normalizeWidthPreset(widthOption.dataset.widthPreset);
716
+ if (!widthPreset)
717
+ return true;
718
+ if (node.attrs.width !== widthPreset) {
719
+ view.dispatch(view.state.tr.setNodeMarkup(pos, node.type, {
720
+ ...node.attrs,
721
+ width: widthPreset
722
+ }));
723
+ }
724
+ closeOpenToolbars(view);
725
+ return true;
726
+ }
727
+ const horizontalAlignOption = event.target.closest('.tiptap-media-horizontal-align-option');
728
+ if (horizontalAlignOption) {
729
+ event.preventDefault();
730
+ event.stopPropagation();
731
+ const pos = Number.parseInt(horizontalAlignOption.dataset.resizePos || '', 10);
732
+ if (!Number.isFinite(pos))
733
+ return true;
734
+ const node = view.state.doc.nodeAt(pos);
735
+ if (!node)
736
+ return true;
737
+ const resizeMeta = resolveResizeMeta(node);
738
+ if (!resizeMeta || !canUseLayoutOptions(resizeMeta.kind))
739
+ return true;
740
+ const selectedHorizontalAlign = horizontalAlignOption.dataset.horizontalAlign || 'auto';
741
+ const normalizedHorizontalAlign = selectedHorizontalAlign === 'auto'
742
+ ? null
743
+ : normalizeHorizontalAlignAttr(selectedHorizontalAlign);
744
+ if (selectedHorizontalAlign !== 'auto' && !normalizedHorizontalAlign)
745
+ return true;
746
+ const currentHorizontalAlign = normalizeHorizontalAlignAttr(node.attrs.horizontalAlign);
747
+ if (normalizedHorizontalAlign !== currentHorizontalAlign) {
748
+ view.dispatch(view.state.tr.setNodeMarkup(pos, node.type, {
749
+ ...node.attrs,
750
+ horizontalAlign: normalizedHorizontalAlign
751
+ }));
752
+ }
753
+ closeOpenToolbars(view);
754
+ return true;
755
+ }
493
756
  const ratioOption = event.target.closest('.tiptap-media-aspect-ratio-option');
494
757
  if (ratioOption) {
495
758
  event.preventDefault();
@@ -529,6 +792,151 @@ export default Extension.create({
529
792
  closeOpenToolbars(view);
530
793
  return true;
531
794
  }
795
+ const widthHandle = event.target.closest('.tiptap-media-width-resize-handle');
796
+ if (widthHandle) {
797
+ const pos = Number.parseInt(widthHandle.dataset.resizePos || '', 10);
798
+ if (!Number.isFinite(pos))
799
+ return false;
800
+ const node = view.state.doc.nodeAt(pos);
801
+ if (!node)
802
+ return false;
803
+ const resizeMeta = resolveResizeMeta(node);
804
+ if (!resizeMeta || !canUseAspectRatioPreset(resizeMeta.kind))
805
+ return false;
806
+ const resizeKind = widthHandle.dataset.resizeKind;
807
+ if (resizeKind && resizeMeta.kind !== resizeKind)
808
+ return false;
809
+ const target = resolveTargetElement(view, pos, resizeMeta, node);
810
+ if (!target)
811
+ return false;
812
+ event.preventDefault();
813
+ event.stopPropagation();
814
+ const anchor = widthHandle.closest('.tiptap-media-resize-anchor');
815
+ const startX = event.clientX;
816
+ const startY = event.clientY;
817
+ const startHeight = resolveStartHeight(resizeMeta.kind, node, target);
818
+ const targetParent = target.parentElement;
819
+ const shouldShowProxy = resizeMeta.kind !== 'image' && Boolean(targetParent);
820
+ const currentHorizontalAlign = normalizeHorizontalAlignAttr(node.attrs.horizontalAlign);
821
+ const startWidth = Math.max(1, resolveElementWidth(node, target) ||
822
+ targetParent?.getBoundingClientRect().width ||
823
+ 0);
824
+ let frame = 0;
825
+ let pendingWidth = startWidth;
826
+ let resizeProxy = null;
827
+ let restoreTarget = null;
828
+ let isDragging = false;
829
+ let appliedDragCursor = false;
830
+ const previousCursor = document.body.style.cursor;
831
+ const previousSelect = document.body.style.userSelect;
832
+ const dispatchWidth = (width) => {
833
+ const current = view.state.doc.nodeAt(pos);
834
+ if (!current || current.type.name !== resizeMeta.typeName)
835
+ return;
836
+ const currentMeta = resolveResizeMeta(current);
837
+ if (!currentMeta || !canUseAspectRatioPreset(currentMeta.kind))
838
+ return;
839
+ const containerWidth = target.parentElement?.getBoundingClientRect().width || 0;
840
+ const nextWidth = containerWidth > 0
841
+ ? `${Math.round(clamp((width / containerWidth) * 100, 10, 100))}%`
842
+ : String(Math.max(1, Math.round(width)));
843
+ if (nextWidth === current.attrs.width)
844
+ return;
845
+ view.dispatch(view.state.tr.setNodeMarkup(pos, current.type, {
846
+ ...current.attrs,
847
+ width: nextWidth
848
+ }));
849
+ };
850
+ const beginDrag = () => {
851
+ if (isDragging)
852
+ return;
853
+ isDragging = true;
854
+ closeOpenToolbars(view);
855
+ if (shouldShowProxy && targetParent) {
856
+ const targetElement = target;
857
+ const originalDisplay = targetElement.style.display;
858
+ resizeProxy = document.createElement('div');
859
+ resizeProxy.className = 'tiptap-media-resize-proxy';
860
+ resizeProxy.style.height = `${Math.round(startHeight)}px`;
861
+ resizeProxy.style.width = `${Math.round(startWidth)}px`;
862
+ if (currentHorizontalAlign === 'center') {
863
+ resizeProxy.style.marginLeft = 'auto';
864
+ resizeProxy.style.marginRight = 'auto';
865
+ }
866
+ else if (currentHorizontalAlign === 'right') {
867
+ resizeProxy.style.marginLeft = 'auto';
868
+ resizeProxy.style.marginRight = '0';
869
+ }
870
+ else {
871
+ resizeProxy.style.marginLeft = '0';
872
+ resizeProxy.style.marginRight = '0';
873
+ }
874
+ targetParent.insertBefore(resizeProxy, targetElement);
875
+ targetElement.style.display = 'none';
876
+ restoreTarget = () => {
877
+ targetElement.style.display = originalDisplay;
878
+ resizeProxy?.remove();
879
+ resizeProxy = null;
880
+ restoreTarget = null;
881
+ };
882
+ }
883
+ document.body.style.cursor = 'ew-resize';
884
+ document.body.style.userSelect = 'none';
885
+ appliedDragCursor = true;
886
+ };
887
+ const onMove = (moveEvent) => {
888
+ const deltaX = moveEvent.clientX - startX;
889
+ const deltaY = moveEvent.clientY - startY;
890
+ if (!isDragging && Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4)
891
+ return;
892
+ if (!isDragging)
893
+ beginDrag();
894
+ const containerWidth = target.parentElement?.getBoundingClientRect().width || 0;
895
+ const minWidth = containerWidth > 0 ? Math.max(120, containerWidth * 0.2) : 120;
896
+ const maxWidth = containerWidth > 0 ? containerWidth : maxHeight;
897
+ const nextWidth = clamp(startWidth + deltaX, minWidth, maxWidth);
898
+ pendingWidth = nextWidth;
899
+ if (resizeProxy)
900
+ resizeProxy.style.width = `${Math.round(nextWidth)}px`;
901
+ if (shouldShowProxy)
902
+ return;
903
+ if (frame)
904
+ cancelAnimationFrame(frame);
905
+ frame = requestAnimationFrame(() => dispatchWidth(nextWidth));
906
+ };
907
+ const cleanup = () => {
908
+ if (frame)
909
+ cancelAnimationFrame(frame);
910
+ window.removeEventListener('mousemove', onMove);
911
+ window.removeEventListener('mouseup', onUp);
912
+ if (appliedDragCursor) {
913
+ document.body.style.cursor = previousCursor;
914
+ document.body.style.userSelect = previousSelect;
915
+ }
916
+ restoreTarget?.();
917
+ removeDragListeners = null;
918
+ };
919
+ const onUp = () => {
920
+ if (!isDragging) {
921
+ const shouldOpen = Boolean(anchor) &&
922
+ !(anchor?.classList.contains('is-width-toolbar-open') ?? false);
923
+ closeOpenToolbars(view, shouldOpen && anchor ? anchor : null);
924
+ if (shouldOpen)
925
+ anchor?.classList.remove('is-toolbar-open');
926
+ anchor?.classList.toggle('is-width-toolbar-open', shouldOpen);
927
+ cleanup();
928
+ return;
929
+ }
930
+ if (shouldShowProxy)
931
+ dispatchWidth(pendingWidth);
932
+ cleanup();
933
+ };
934
+ removeDragListeners?.();
935
+ window.addEventListener('mousemove', onMove);
936
+ window.addEventListener('mouseup', onUp);
937
+ removeDragListeners = cleanup;
938
+ return true;
939
+ }
532
940
  const handle = event.target.closest('.tiptap-media-resize-handle');
533
941
  if (!handle) {
534
942
  closeOpenToolbars(view);
@@ -556,7 +964,9 @@ export default Extension.create({
556
964
  const startY = event.clientY;
557
965
  const startHeight = resolveStartHeight(resizeMeta.kind, node, target);
558
966
  const imageRatio = resizeMeta.kind === 'image' ? resolveImageRatio(node, target) : 1;
559
- const shouldShowProxy = resizeMeta.kind !== 'image';
967
+ const shouldShowProxy = true;
968
+ const currentHorizontalAlign = normalizeHorizontalAlignAttr(node.attrs.horizontalAlign);
969
+ const startWidth = resolveElementWidth(node, target);
560
970
  let resizeProxy = null;
561
971
  let restoreTarget = null;
562
972
  let frame = 0;
@@ -596,6 +1006,21 @@ export default Extension.create({
596
1006
  resizeProxy = document.createElement('div');
597
1007
  resizeProxy.className = 'tiptap-media-resize-proxy';
598
1008
  resizeProxy.style.height = `${Math.round(startHeight)}px`;
1009
+ if (startWidth > 0) {
1010
+ resizeProxy.style.width = `${Math.round(startWidth)}px`;
1011
+ }
1012
+ if (currentHorizontalAlign === 'center') {
1013
+ resizeProxy.style.marginLeft = 'auto';
1014
+ resizeProxy.style.marginRight = 'auto';
1015
+ }
1016
+ else if (currentHorizontalAlign === 'right') {
1017
+ resizeProxy.style.marginLeft = 'auto';
1018
+ resizeProxy.style.marginRight = '0';
1019
+ }
1020
+ else {
1021
+ resizeProxy.style.marginLeft = '0';
1022
+ resizeProxy.style.marginRight = '0';
1023
+ }
599
1024
  target.parentElement.insertBefore(resizeProxy, targetElement);
600
1025
  targetElement.style.display = 'none';
601
1026
  restoreTarget = () => {
@@ -642,6 +1067,8 @@ export default Extension.create({
642
1067
  if (!isDragging) {
643
1068
  const shouldOpen = Boolean(anchor) && !(anchor?.classList.contains('is-toolbar-open') ?? false);
644
1069
  closeOpenToolbars(view, shouldOpen && anchor ? anchor : null);
1070
+ if (shouldOpen)
1071
+ anchor?.classList.remove('is-width-toolbar-open');
645
1072
  anchor?.classList.toggle('is-toolbar-open', shouldOpen);
646
1073
  cleanup();
647
1074
  return;
@@ -18,7 +18,7 @@ type InsertUploadSkeletonOptions = {
18
18
  type ReplaceOptions = {
19
19
  select?: boolean;
20
20
  };
21
- type EditorLike = Pick<Editor, 'state' | 'view'>;
21
+ type EditorLike = Pick<Editor, 'view'>;
22
22
  export type UploadSkeletonHandle = {
23
23
  id: string;
24
24
  exists: () => boolean;
@@ -11,20 +11,16 @@ const defaultHeight = {
11
11
  block: 180
12
12
  };
13
13
  function findUploadSkeleton(doc, id) {
14
- let foundPos = null;
15
- let foundNode = null;
14
+ let target = null;
16
15
  doc.descendants((node, pos) => {
17
16
  if (node.type.name !== UPLOAD_SKELETON_NODE)
18
17
  return;
19
18
  if (node.attrs.uploadId !== id)
20
19
  return;
21
- foundPos = pos;
22
- foundNode = node;
20
+ target = { pos, node };
23
21
  return false;
24
22
  });
25
- if (foundPos === null || foundNode === null)
26
- return null;
27
- return { pos: foundPos, node: foundNode };
23
+ return target;
28
24
  }
29
25
  function tryCreateNodeSelection(doc, pos) {
30
26
  if (pos < 0 || pos > doc.content.size)
@@ -40,15 +36,16 @@ function tryCreateNodeSelection(doc, pos) {
40
36
  }
41
37
  }
42
38
  export function insertUploadSkeleton(editor, { kind = 'block', height = defaultHeight[kind], at, select = true, insertParagraph = true } = {}) {
43
- const skeletonType = editor.state.schema.nodes[UPLOAD_SKELETON_NODE];
39
+ const getState = () => editor.view.state;
40
+ const skeletonType = getState().schema.nodes[UPLOAD_SKELETON_NODE];
44
41
  if (!skeletonType)
45
42
  return null;
46
43
  const clampedHeight = Math.max(44, Math.min(1200, Math.round(height)));
47
44
  const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
48
45
  const node = skeletonType.create({ uploadId, kind, height: clampedHeight });
49
- const paragraph = editor.state.schema.nodes.paragraph?.create();
50
- const safePos = Math.max(0, Math.min(at ?? editor.state.selection.from, editor.state.doc.content.size));
51
- const tr = editor.state.tr.insert(safePos, node);
46
+ const paragraph = getState().schema.nodes.paragraph?.create();
47
+ const safePos = Math.max(0, Math.min(at ?? getState().selection.from, getState().doc.content.size));
48
+ const tr = getState().tr.insert(safePos, node);
52
49
  if (insertParagraph && paragraph) {
53
50
  tr.insert(safePos + node.nodeSize, paragraph);
54
51
  }
@@ -60,21 +57,22 @@ export function insertUploadSkeleton(editor, { kind = 'block', height = defaultH
60
57
  editor.view.dispatch(tr);
61
58
  return {
62
59
  id: uploadId,
63
- exists: () => Boolean(findUploadSkeleton(editor.state.doc, uploadId)),
60
+ exists: () => Boolean(findUploadSkeleton(getState().doc, uploadId)),
64
61
  replaceWith: (content, options = {}) => {
65
- const target = findUploadSkeleton(editor.state.doc, uploadId);
62
+ const state = getState();
63
+ const target = findUploadSkeleton(state.doc, uploadId);
66
64
  if (!target)
67
65
  return false;
68
66
  if (!content?.type)
69
67
  return false;
70
68
  let nextNode;
71
69
  try {
72
- nextNode = editor.state.schema.nodeFromJSON(content);
70
+ nextNode = state.schema.nodeFromJSON(content);
73
71
  }
74
72
  catch {
75
73
  return false;
76
74
  }
77
- const tr = editor.state.tr.replaceWith(target.pos, target.pos + target.node.nodeSize, nextNode);
75
+ const tr = state.tr.replaceWith(target.pos, target.pos + target.node.nodeSize, nextNode);
78
76
  if (options.select ?? true) {
79
77
  const nodeSelection = tryCreateNodeSelection(tr.doc, target.pos);
80
78
  if (nodeSelection)
@@ -84,16 +82,17 @@ export function insertUploadSkeleton(editor, { kind = 'block', height = defaultH
84
82
  return true;
85
83
  },
86
84
  remove: () => {
87
- const target = findUploadSkeleton(editor.state.doc, uploadId);
85
+ const state = getState();
86
+ const target = findUploadSkeleton(state.doc, uploadId);
88
87
  if (!target)
89
88
  return false;
90
89
  const removeFrom = target.pos;
91
90
  let removeTo = target.pos + target.node.nodeSize;
92
- const nextNode = editor.state.doc.nodeAt(removeTo);
91
+ const nextNode = state.doc.nodeAt(removeTo);
93
92
  if (nextNode?.type.name === 'paragraph' && nextNode.content.size === 0) {
94
93
  removeTo += nextNode.nodeSize;
95
94
  }
96
- editor.view.dispatch(editor.state.tr.deleteRange(removeFrom, removeTo));
95
+ editor.view.dispatch(state.tr.deleteRange(removeFrom, removeTo));
97
96
  return true;
98
97
  }
99
98
  };
@@ -12,11 +12,9 @@
12
12
  import { quartOut } from 'svelte/easing';
13
13
  import defaultI18n, { I18N_CONTEXT, type I18nTranslate } from '../i18n';
14
14
 
15
- const editor = getContext<{ v: any }>('editor');
16
15
  const i18nFromContext = getContext<I18nTranslate | undefined>(I18N_CONTEXT);
17
16
  const i18n: I18nTranslate = (...args) =>
18
17
  i18nFromContext ? i18nFromContext(...args) : defaultI18n(...args);
19
- const tiptap = $derived(editor.v);
20
18
 
21
19
  let height = $state(0);
22
20
  let input = $state(''),
@@ -32,7 +30,6 @@
32
30
  if (!editor || !range) return;
33
31
 
34
32
  item.command({ editor, range });
35
- setTimeout(() => tiptap?.commands?.focus?.());
36
33
  }
37
34
 
38
35
  function runDetailCommand() {
@@ -18,7 +18,6 @@
18
18
  type I18nTranslate
19
19
  } from '../i18n';
20
20
  import type { UploadFn } from '../plugin/image/dragdrop';
21
- import { fallbackUpload } from '../plugin/image/dragdrop';
22
21
  import MediaResize, { type ResizeOptions } from '../plugin/resize';
23
22
  import { Render } from 'nunui';
24
23
 
@@ -51,7 +50,7 @@
51
50
  ref = $bindable(null),
52
51
  options = {},
53
52
  loaded = $bindable(false),
54
- imageUpload = fallbackUpload,
53
+ imageUpload,
55
54
  style = '',
56
55
  blocks = [],
57
56
  placeholder,
@@ -82,6 +81,7 @@
82
81
  'data-resize-min-height',
83
82
  'data-resize-max-height',
84
83
  'data-resize-aspect-ratio',
84
+ 'data-resize-horizontal-align',
85
85
  'data-bubble-menu',
86
86
  'data-hide-bubble-menu'
87
87
  ];
@@ -341,6 +341,10 @@
341
341
  outline: 3px solid var(--primary);
342
342
  }
343
343
 
344
+ .editable :global(.ProseMirror) {
345
+ position: relative;
346
+ }
347
+
344
348
  .editable :global(.tiptap-media-resize-anchor) {
345
349
  width: 100%;
346
350
  display: flex;
@@ -353,6 +357,27 @@
353
357
  overflow: visible;
354
358
  }
355
359
 
360
+ .editable :global(.tiptap-media-resize-anchor.is-image-anchor) {
361
+ position: relative;
362
+ display: block;
363
+ width: 100%;
364
+ height: 0;
365
+ margin: 0;
366
+ z-index: 4;
367
+ }
368
+
369
+ .editable :global(.tiptap-media-resize-anchor.is-image-anchor .tiptap-media-resize-controls) {
370
+ position: absolute;
371
+ top: 0;
372
+ left: 0;
373
+ display: flex;
374
+ flex-direction: column;
375
+ align-items: center;
376
+ line-height: 0;
377
+ pointer-events: none;
378
+ overflow: visible;
379
+ }
380
+
356
381
  .editable :global(.tiptap-media-resize-handle) {
357
382
  appearance: none;
358
383
  -webkit-appearance: none;
@@ -383,6 +408,39 @@
383
408
  transform: translateY(1px);
384
409
  }
385
410
 
411
+ .editable :global(.tiptap-media-width-resize-handle) {
412
+ appearance: none;
413
+ -webkit-appearance: none;
414
+ position: absolute;
415
+ top: 0;
416
+ right: 0;
417
+ width: 12px;
418
+ height: 12px;
419
+ margin: 0;
420
+ padding: 0;
421
+ border: 1px solid var(--primary-light3, rgba(120, 120, 120, 0.45));
422
+ border-radius: 999px;
423
+ background: var(--primary-light6, rgba(120, 120, 120, 0.2));
424
+ cursor: ew-resize;
425
+ pointer-events: auto;
426
+ transform: translate(40%, -40%);
427
+ transition:
428
+ background-color 0.15s ease,
429
+ border-color 0.15s ease,
430
+ transform 0.15s ease;
431
+ }
432
+
433
+ .editable :global(.tiptap-media-width-resize-handle:hover),
434
+ .editable :global(.tiptap-media-width-resize-handle:focus-visible) {
435
+ background: var(--primary-light4, rgba(120, 120, 120, 0.35));
436
+ border-color: var(--primary-light2, rgba(100, 100, 100, 0.55));
437
+ outline: none;
438
+ }
439
+
440
+ .editable :global(.tiptap-media-width-resize-handle:active) {
441
+ transform: translate(40%, -40%) scale(0.95);
442
+ }
443
+
386
444
  .editable :global(.tiptap-media-aspect-ratio-toolbar) {
387
445
  display: none;
388
446
  align-items: center;
@@ -402,12 +460,81 @@
402
460
  white-space: nowrap;
403
461
  }
404
462
 
463
+ .editable :global(.tiptap-media-width-toolbar) {
464
+ display: none;
465
+ align-items: center;
466
+ gap: 4px;
467
+ padding: 4px;
468
+ border: 1px solid var(--primary-light3, rgba(120, 120, 120, 0.4));
469
+ border-radius: 999px;
470
+ background: var(--surface, #fff);
471
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
472
+ pointer-events: auto;
473
+ line-height: 1;
474
+ position: absolute;
475
+ top: calc(100% + 6px);
476
+ right: 0;
477
+ z-index: 4;
478
+ white-space: nowrap;
479
+ }
480
+
405
481
  .editable
406
482
  :global(.tiptap-media-resize-anchor.is-toolbar-open .tiptap-media-aspect-ratio-toolbar) {
407
483
  display: flex;
408
484
  }
409
485
 
410
- .editable :global(.tiptap-media-aspect-ratio-option) {
486
+ .editable :global(.tiptap-media-resize-anchor.is-width-toolbar-open .tiptap-media-width-toolbar) {
487
+ display: flex;
488
+ }
489
+
490
+ .editable :global(.tiptap-media-toolbar-separator) {
491
+ width: 1px;
492
+ height: 16px;
493
+ background: var(--primary-light2, rgba(120, 120, 120, 0.35));
494
+ opacity: 0.9;
495
+ }
496
+
497
+ .editable :global(.tiptap-media-toolbar-group-icon) {
498
+ width: 14px;
499
+ height: 14px;
500
+ border: 1px solid var(--primary-light3, rgba(120, 120, 120, 0.45));
501
+ border-radius: 4px;
502
+ color: var(--on-surface, #333);
503
+ opacity: 0.75;
504
+ flex: 0 0 auto;
505
+ pointer-events: none;
506
+ }
507
+
508
+ .editable :global(.tiptap-media-toolbar-group-icon[data-group='aspect']) {
509
+ background: linear-gradient(
510
+ 135deg,
511
+ transparent 42%,
512
+ currentColor 43%,
513
+ currentColor 57%,
514
+ transparent 58%
515
+ )
516
+ center / 100% 100% no-repeat;
517
+ }
518
+
519
+ .editable :global(.tiptap-media-toolbar-group-icon[data-group='align']) {
520
+ background:
521
+ linear-gradient(currentColor, currentColor) left 2px top 3px / 8px 1.5px no-repeat,
522
+ linear-gradient(currentColor, currentColor) center top 6px / 10px 1.5px no-repeat,
523
+ linear-gradient(currentColor, currentColor) right 2px top 9px / 8px 1.5px no-repeat;
524
+ }
525
+
526
+ .editable :global(.tiptap-media-toolbar-group-icon[data-group='width']) {
527
+ background:
528
+ linear-gradient(currentColor, currentColor) center / 8px 1.5px no-repeat,
529
+ linear-gradient(45deg, transparent 38%, currentColor 39%, currentColor 61%, transparent 62%)
530
+ left 2px center / 4px 4px no-repeat,
531
+ linear-gradient(-45deg, transparent 38%, currentColor 39%, currentColor 61%, transparent 62%)
532
+ right 2px center / 4px 4px no-repeat;
533
+ }
534
+
535
+ .editable :global(.tiptap-media-aspect-ratio-option),
536
+ .editable :global(.tiptap-media-horizontal-align-option),
537
+ .editable :global(.tiptap-media-width-option) {
411
538
  appearance: none;
412
539
  -webkit-appearance: none;
413
540
  margin: 0;
@@ -423,12 +550,18 @@
423
550
  }
424
551
 
425
552
  .editable :global(.tiptap-media-aspect-ratio-option:hover),
426
- .editable :global(.tiptap-media-aspect-ratio-option:focus-visible) {
553
+ .editable :global(.tiptap-media-aspect-ratio-option:focus-visible),
554
+ .editable :global(.tiptap-media-horizontal-align-option:hover),
555
+ .editable :global(.tiptap-media-horizontal-align-option:focus-visible),
556
+ .editable :global(.tiptap-media-width-option:hover),
557
+ .editable :global(.tiptap-media-width-option:focus-visible) {
427
558
  background: var(--primary-light1, rgba(120, 120, 120, 0.14));
428
559
  outline: none;
429
560
  }
430
561
 
431
- .editable :global(.tiptap-media-aspect-ratio-option[aria-pressed='true']) {
562
+ .editable :global(.tiptap-media-aspect-ratio-option[aria-pressed='true']),
563
+ .editable :global(.tiptap-media-horizontal-align-option[aria-pressed='true']),
564
+ .editable :global(.tiptap-media-width-option[aria-pressed='true']) {
432
565
  background: var(--primary-light4, rgba(120, 120, 120, 0.3));
433
566
  color: var(--on-primary, #000);
434
567
  }
@@ -469,6 +602,10 @@
469
602
  position: relative;
470
603
  }
471
604
 
605
+ & :global(figure[data-bubble-menu='false']) {
606
+ margin: 0;
607
+ }
608
+
472
609
  & :global(code.inline) {
473
610
  background: var(--primary-light1);
474
611
  padding: 2px 4px;
@@ -535,8 +672,38 @@
535
672
  & :global(iframe),
536
673
  & :global(embed) {
537
674
  display: block;
538
- width: 100%;
539
675
  max-width: 100%;
676
+ margin-left: var(--tiptap-media-horizontal-margin-left, 0);
677
+ margin-right: var(--tiptap-media-horizontal-margin-right, 0);
678
+ }
679
+
680
+ & :global(img[data-resize-horizontal-align]) {
681
+ display: block;
682
+ margin-left: var(--tiptap-media-horizontal-margin-left, 0);
683
+ margin-right: var(--tiptap-media-horizontal-margin-right, 0);
684
+ }
685
+
686
+ & :global(img[width='100%']) {
687
+ width: 100%;
688
+ }
689
+
690
+ & :global(img[width='50%']) {
691
+ width: 50%;
692
+ }
693
+
694
+ & :global([data-resize-horizontal-align='left']) {
695
+ --tiptap-media-horizontal-margin-left: 0;
696
+ --tiptap-media-horizontal-margin-right: auto;
697
+ }
698
+
699
+ & :global([data-resize-horizontal-align='center']) {
700
+ --tiptap-media-horizontal-margin-left: auto;
701
+ --tiptap-media-horizontal-margin-right: auto;
702
+ }
703
+
704
+ & :global([data-resize-horizontal-align='right']) {
705
+ --tiptap-media-horizontal-margin-left: auto;
706
+ --tiptap-media-horizontal-margin-right: 0;
540
707
  }
541
708
 
542
709
  & :global([data-resize-aspect-ratio='16:9']) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seorii/tiptap",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "svelte-kit sync && svelte-package",