@jvs-milkdown/components 1.1.5 → 1.1.6

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/lib/__internal__/components/image-input.d.ts.map +1 -1
  3. package/lib/image-block/convert-plugin.d.ts +2 -0
  4. package/lib/image-block/convert-plugin.d.ts.map +1 -0
  5. package/lib/image-block/index.d.ts +2 -0
  6. package/lib/image-block/index.d.ts.map +1 -1
  7. package/lib/image-block/index.js +345 -75
  8. package/lib/image-block/index.js.map +1 -1
  9. package/lib/image-block/paste-rule.d.ts +2 -0
  10. package/lib/image-block/paste-rule.d.ts.map +1 -0
  11. package/lib/image-block/schema.d.ts.map +1 -1
  12. package/lib/image-block/view/components/image-block.d.ts +1 -0
  13. package/lib/image-block/view/components/image-block.d.ts.map +1 -1
  14. package/lib/image-block/view/components/image-viewer.d.ts.map +1 -1
  15. package/lib/image-block/view/index.d.ts.map +1 -1
  16. package/lib/image-inline/index.js +27 -6
  17. package/lib/image-inline/index.js.map +1 -1
  18. package/lib/link-tooltip/edit/component.d.ts.map +1 -1
  19. package/lib/link-tooltip/index.js +18 -4
  20. package/lib/link-tooltip/index.js.map +1 -1
  21. package/lib/table-block/dnd/drag-over-handler.d.ts.map +1 -1
  22. package/lib/table-block/index.js +139 -53
  23. package/lib/table-block/index.js.map +1 -1
  24. package/lib/table-block/view/component.d.ts.map +1 -1
  25. package/lib/table-block/view/drag.d.ts +3 -0
  26. package/lib/table-block/view/drag.d.ts.map +1 -1
  27. package/lib/table-block/view/utils.d.ts.map +1 -1
  28. package/lib/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +53 -79
  30. package/src/__internal__/components/image-input.tsx +45 -12
  31. package/src/image-block/__tests__/paste-rule.spec.ts +20 -0
  32. package/src/image-block/convert-plugin.ts +147 -0
  33. package/src/image-block/index.ts +6 -0
  34. package/src/image-block/paste-rule.ts +138 -0
  35. package/src/image-block/schema.ts +15 -0
  36. package/src/image-block/view/components/image-block.tsx +5 -0
  37. package/src/image-block/view/components/image-viewer.tsx +4 -0
  38. package/src/image-block/view/index.ts +8 -0
  39. package/src/link-tooltip/edit/component.tsx +27 -3
  40. package/src/table-block/dnd/create-drag-handler.ts +5 -1
  41. package/src/table-block/dnd/drag-over-handler.ts +29 -1
  42. package/src/table-block/dnd/preview.ts +3 -3
  43. package/src/table-block/view/component.tsx +121 -39
  44. package/src/table-block/view/drag.ts +29 -16
  45. package/src/table-block/view/utils.ts +6 -1
@@ -1,3 +1,4 @@
1
+ import clsx from 'clsx'
1
2
  import { defineComponent, ref, watch, type Ref, h } from 'vue'
2
3
 
3
4
  import type { LinkTooltipConfig } from '../slices'
@@ -40,15 +41,30 @@ export const EditLink = defineComponent<EditLinkProps>({
40
41
  link.value = value
41
42
  })
42
43
 
44
+ const isValidUrl = (url: string) => {
45
+ if (!url) return false
46
+ const trimmedUrl = url.trim()
47
+ // Accept standard http/https links, relative links, anchors, or mailto
48
+ return /^(https?:\/\/|\/|\.\.?\/|mailto:|#|[a-zA-Z0-9-]+\.[a-zA-Z]+)/i.test(
49
+ trimmedUrl
50
+ )
51
+ }
52
+
43
53
  const onConfirmEdit = () => {
44
- onConfirm(link.value)
54
+ const val = link.value?.trim() ?? ''
55
+ if (isValidUrl(val)) {
56
+ onConfirm(val)
57
+ }
45
58
  }
46
59
 
47
60
  const onKeydown = (e: KeyboardEvent) => {
48
61
  e.stopPropagation()
49
62
  if (e.key === 'Enter') {
50
63
  e.preventDefault()
51
- onConfirmEdit()
64
+ const val = link.value?.trim() ?? ''
65
+ if (isValidUrl(val)) {
66
+ onConfirmEdit()
67
+ }
52
68
  }
53
69
  if (e.key === 'Escape') {
54
70
  e.preventDefault()
@@ -70,9 +86,17 @@ export const EditLink = defineComponent<EditLinkProps>({
70
86
  />
71
87
  {link.value ? (
72
88
  <Icon
73
- class="button confirm"
89
+ class={clsx(
90
+ 'button confirm',
91
+ !isValidUrl(link.value) && 'disabled'
92
+ )}
74
93
  icon={config.value.confirmButton}
75
94
  onClick={onConfirmEdit}
95
+ style={
96
+ !isValidUrl(link.value)
97
+ ? { opacity: 0.5, cursor: 'not-allowed' }
98
+ : undefined
99
+ }
76
100
  />
77
101
  ) : null}
78
102
  </div>
@@ -73,7 +73,11 @@ function handleDrag(
73
73
  if (!view?.editable) return
74
74
 
75
75
  event.stopPropagation()
76
- if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
76
+ if (event.dataTransfer) {
77
+ event.dataTransfer.effectAllowed = 'move'
78
+ // Required by some browsers to properly initiate the drag; empty string might be rejected
79
+ event.dataTransfer.setData('text/plain', 'milkdown-table-drag')
80
+ }
77
81
 
78
82
  const context = prepareDndContext(refs)
79
83
 
@@ -8,7 +8,7 @@ import { getDragOverColumn, getDragOverRow } from './calc-drag-over'
8
8
  import { prepareDndContext } from './prepare-dnd-context'
9
9
 
10
10
  export function createDragOverHandler(refs: Refs): (e: DragEvent) => void {
11
- return throttle((e: DragEvent) => {
11
+ const updatePosition = throttle((e: DragEvent) => {
12
12
  const context = prepareDndContext(refs)
13
13
  if (!context) return
14
14
  const { preview, content, contentRoot, xHandle, yHandle } = context
@@ -110,4 +110,32 @@ export function createDragOverHandler(refs: Refs): (e: DragEvent) => void {
110
110
  }
111
111
  }
112
112
  }, 20)
113
+
114
+ return (e: DragEvent) => {
115
+ const context = prepareDndContext(refs)
116
+ if (!context) return
117
+ const { preview, contentRoot } = context
118
+ if (preview.dataset.show === 'false') return
119
+
120
+ e.preventDefault()
121
+ e.stopPropagation()
122
+
123
+ // SYNCHRONOUSLY update the endIndex so that rapid drops don't miss the final position
124
+ const info = refs.dragInfo.value
125
+ if (info) {
126
+ if (info.type === 'col') {
127
+ const dragOverColumn = getDragOverColumn(contentRoot, e.clientX)
128
+ if (dragOverColumn) {
129
+ info.endIndex = dragOverColumn[1]
130
+ }
131
+ } else if (info.type === 'row') {
132
+ const dragOverRow = getDragOverRow(contentRoot, e.clientY)
133
+ if (dragOverRow) {
134
+ info.endIndex = dragOverRow[1]
135
+ }
136
+ }
137
+ }
138
+
139
+ updatePosition(e)
140
+ }
113
141
  }
@@ -9,9 +9,9 @@ export function renderPreview(
9
9
  tableContent: HTMLElement,
10
10
  index: number
11
11
  ) {
12
- const { width: tableWidth, height: tableHeight } = tableContent
13
- .querySelector('tbody')!
14
- .getBoundingClientRect()
12
+ const tableBodyOrContent = tableContent.querySelector('tbody') || tableContent
13
+ const { width: tableWidth, height: tableHeight } =
14
+ tableBodyOrContent.getBoundingClientRect()
15
15
  if (axis === 'y') {
16
16
  const rows = tableContent.querySelectorAll('tr')
17
17
  const row = rows[index]
@@ -3,7 +3,11 @@ import type { Node } from '@jvs-milkdown/prose/model'
3
3
  import type { EditorView } from '@jvs-milkdown/prose/view'
4
4
 
5
5
  import { computePosition } from '@floating-ui/dom'
6
- import { CellSelection } from '@jvs-milkdown/prose/tables'
6
+ import {
7
+ CellSelection,
8
+ mergeCells,
9
+ splitCell,
10
+ } from '@jvs-milkdown/prose/tables'
7
11
  import {
8
12
  defineComponent,
9
13
  ref,
@@ -12,6 +16,7 @@ import {
12
16
  onMounted,
13
17
  onBeforeUnmount,
14
18
  type Ref,
19
+ watch,
15
20
  } from 'vue'
16
21
 
17
22
  import type { TableBlockConfig } from '../config'
@@ -122,7 +127,11 @@ export const TableBlock = defineComponent<TableBlockProps>({
122
127
  }
123
128
 
124
129
  const { pointerLeave, pointerMove } = usePointerHandlers(refs, view)
125
- const { dragRow, dragCol } = useDragHandlers(refs, ctx, getPos)
130
+ const { dragRow, dragCol, onDragOver, onDragEnd, onDrop } = useDragHandlers(
131
+ refs,
132
+ ctx,
133
+ getPos
134
+ )
126
135
  const {
127
136
  addRowByIndex,
128
137
  addColByIndex,
@@ -153,18 +162,16 @@ export const TableBlock = defineComponent<TableBlockProps>({
153
162
  return
154
163
  }
155
164
 
156
- if (selection instanceof CellSelection) {
157
- // Compute merge/split availability
158
- let _cellCount = 0
159
- let hasMerged = false
160
- selection.forEachCell((cell) => {
161
- _cellCount++
162
- if ((cell.attrs.rowspan ?? 1) > 1 || (cell.attrs.colspan ?? 1) > 1) {
163
- hasMerged = true
164
- }
165
- })
166
- canMerge.value = _cellCount > 1
167
- canSplit.value = hasMerged
165
+ // Bypass instanceof checks due to potential multiple instances of prosemirror-tables
166
+ const isCellSelection = (sel: any) =>
167
+ sel &&
168
+ typeof sel.isColSelection === 'function' &&
169
+ typeof sel.forEachCell === 'function'
170
+
171
+ if (isCellSelection(selection)) {
172
+ // Use prosemirror-tables commands (without dispatch) to check if merge/split is currently possible
173
+ canMerge.value = mergeCells(view.state)
174
+ canSplit.value = splitCell(view.state)
168
175
  } else {
169
176
  // TextSelection: only show toolbar for CellSelection-based actions
170
177
  showCellToolbar.value = false
@@ -176,6 +183,11 @@ export const TableBlock = defineComponent<TableBlockProps>({
176
183
  return
177
184
  }
178
185
 
186
+ if (activeColIndex.value !== -1 || activeRowIndex.value !== -1) {
187
+ showCellToolbar.value = false
188
+ return
189
+ }
190
+
179
191
  showCellToolbar.value = true
180
192
 
181
193
  // Position toolbar above the first selected cell using floating-ui
@@ -306,27 +318,39 @@ export const TableBlock = defineComponent<TableBlockProps>({
306
318
 
307
319
  // Listen for ProseMirror state updates
308
320
  const dispatchListener = () => {
309
- updateCellToolbar()
310
-
311
- // Update active handler button groups based on selection
312
- const { selection } = view.state
313
- if (selection instanceof CellSelection) {
314
- if (selection.isColSelection()) {
315
- const { $head } = selection
316
- activeColIndex.value = $head.index($head.depth - 1)
317
- activeRowIndex.value = -1
318
- } else if (selection.isRowSelection()) {
319
- activeColIndex.value = -1
320
- // Simple approach for row index: derive from hoverIndex which is updated by pointer? Or compute.
321
- // recoveryStateBetweenUpdate updates hoverIndex.
321
+ setTimeout(() => {
322
+ // Update active handler button groups based on selection
323
+ const { selection } = view.state
324
+ const isCellSelection = (sel: any) =>
325
+ sel &&
326
+ typeof sel.isColSelection === 'function' &&
327
+ typeof sel.forEachCell === 'function'
328
+
329
+ if (isCellSelection(selection)) {
330
+ const selAny = selection as any
331
+ if (selAny.isColSelection()) {
332
+ const $headCell = selAny.$headCell
333
+ activeColIndex.value = $headCell
334
+ ? $headCell.index($headCell.depth)
335
+ : -1
336
+ activeRowIndex.value = -1
337
+ } else if (selAny.isRowSelection()) {
338
+ const $headCell = selAny.$headCell
339
+ activeColIndex.value = -1
340
+ activeRowIndex.value = $headCell
341
+ ? $headCell.index($headCell.depth - 1)
342
+ : -1
343
+ } else {
344
+ activeColIndex.value = -1
345
+ activeRowIndex.value = -1
346
+ }
322
347
  } else {
323
348
  activeColIndex.value = -1
324
349
  activeRowIndex.value = -1
325
350
  }
326
- } else {
327
- activeColIndex.value = -1
328
- activeRowIndex.value = -1
329
- }
351
+
352
+ updateCellToolbar()
353
+ }, 0)
330
354
  }
331
355
 
332
356
  onMounted(() => {
@@ -378,6 +402,10 @@ export const TableBlock = defineComponent<TableBlockProps>({
378
402
  }
379
403
  })
380
404
 
405
+ watch(node, () => {
406
+ dispatchListener()
407
+ })
408
+
381
409
  onBeforeUnmount(() => {
382
410
  view.dom.removeEventListener('keyup', dispatchListener)
383
411
  view.dom.removeEventListener('pointerup', dispatchListener)
@@ -386,12 +414,26 @@ export const TableBlock = defineComponent<TableBlockProps>({
386
414
  })
387
415
 
388
416
  return () => {
389
- updateCellToolbar()
390
-
391
417
  return (
392
418
  <div
393
- onDragstart={(e) => e.preventDefault()}
394
- onDragover={(e) => e.preventDefault()}
419
+ onDragend={onDragEnd}
420
+ onDragover={(e) => {
421
+ e.preventDefault()
422
+ onDragOver(e)
423
+ // Stop propagation during active drag to prevent ProseMirror interference
424
+ if (dragInfo.value) {
425
+ e.stopPropagation()
426
+ }
427
+ }}
428
+ onDrop={(e) => {
429
+ const isDragging = !!dragInfo.value
430
+ onDrop(e)
431
+ // Stop ProseMirror from handling the drop during table drag
432
+ if (isDragging) {
433
+ e.preventDefault()
434
+ e.stopPropagation()
435
+ }
436
+ }}
395
437
  onDragleave={(e) => e.preventDefault()}
396
438
  onPointermove={pointerMove}
397
439
  onPointerleave={pointerLeave}
@@ -405,7 +447,7 @@ export const TableBlock = defineComponent<TableBlockProps>({
405
447
  contenteditable="false"
406
448
  onPointerdown={(e: PointerEvent) => e.stopPropagation()}
407
449
  >
408
- {canMerge.value && (
450
+ {
409
451
  <button
410
452
  type="button"
411
453
  class="cell-toolbar-btn"
@@ -413,8 +455,8 @@ export const TableBlock = defineComponent<TableBlockProps>({
413
455
  >
414
456
  <Icon icon={config.renderButton('merge_cells')} />
415
457
  </button>
416
- )}
417
- {canSplit.value && (
458
+ }
459
+ {
418
460
  <button
419
461
  type="button"
420
462
  class="cell-toolbar-btn"
@@ -422,7 +464,7 @@ export const TableBlock = defineComponent<TableBlockProps>({
422
464
  >
423
465
  <Icon icon={config.renderButton('split_cell')} />
424
466
  </button>
425
- )}
467
+ }
426
468
  </div>
427
469
 
428
470
  <div class="table-wrapper" ref={tableWrapperRef}>
@@ -442,6 +484,7 @@ export const TableBlock = defineComponent<TableBlockProps>({
442
484
  onClick={() => {
443
485
  hoverIndex.value = [0, i]
444
486
  selectCol()
487
+ dispatchListener()
445
488
  activeColIndex.value = i
446
489
  activeRowIndex.value = -1
447
490
  }}
@@ -489,6 +532,16 @@ export const TableBlock = defineComponent<TableBlockProps>({
489
532
  >
490
533
  <Icon icon={config.renderButton('delete_col')} />
491
534
  </button>
535
+ {canMerge.value && (
536
+ <button type="button" onPointerdown={onMergeCells}>
537
+ <Icon icon={config.renderButton('merge_cells')} />
538
+ </button>
539
+ )}
540
+ {canSplit.value && (
541
+ <button type="button" onPointerdown={onSplitCell}>
542
+ <Icon icon={config.renderButton('split_cell')} />
543
+ </button>
544
+ )}
492
545
  </div>
493
546
  </div>
494
547
  ))}
@@ -548,6 +601,7 @@ export const TableBlock = defineComponent<TableBlockProps>({
548
601
  onClick={() => {
549
602
  hoverIndex.value = [i, 0]
550
603
  selectRow()
604
+ dispatchListener()
551
605
  activeRowIndex.value = i
552
606
  activeColIndex.value = -1
553
607
  }}
@@ -567,6 +621,16 @@ export const TableBlock = defineComponent<TableBlockProps>({
567
621
  >
568
622
  <Icon icon={config.renderButton('delete_row')} />
569
623
  </button>
624
+ {canMerge.value && (
625
+ <button type="button" onPointerdown={onMergeCells}>
626
+ <Icon icon={config.renderButton('merge_cells')} />
627
+ </button>
628
+ )}
629
+ {canSplit.value && (
630
+ <button type="button" onPointerdown={onSplitCell}>
631
+ <Icon icon={config.renderButton('split_cell')} />
632
+ </button>
633
+ )}
570
634
  </div>
571
635
  </div>
572
636
  ))}
@@ -604,6 +668,24 @@ export const TableBlock = defineComponent<TableBlockProps>({
604
668
  </table>
605
669
  </div>
606
670
 
671
+ {/* Drop Indicators */}
672
+ <div
673
+ data-show="false"
674
+ contenteditable="false"
675
+ data-display-type="indicator"
676
+ data-role="x-line-drag-handle"
677
+ class="handle line-handle"
678
+ ref={xLineHandleRef}
679
+ />
680
+ <div
681
+ data-show="false"
682
+ contenteditable="false"
683
+ data-display-type="indicator"
684
+ data-role="y-line-drag-handle"
685
+ class="handle line-handle"
686
+ ref={yLineHandleRef}
687
+ />
688
+
607
689
  <table ref={contentWrapperFunctionRef} class="children">
608
690
  <colgroup>
609
691
  {colWidths.value.map((w, i) => (
@@ -7,7 +7,6 @@ import {
7
7
  selectColCommand,
8
8
  selectRowCommand,
9
9
  } from '@jvs-milkdown/preset-gfm'
10
- import { onMounted, onUnmounted } from 'vue'
11
10
 
12
11
  import type { CellIndex, Refs } from './types'
13
12
 
@@ -27,21 +26,25 @@ export function useDragHandlers(
27
26
  const dragRow = createDragRowHandler(refs, ctx)
28
27
  const dragCol = createDragColHandler(refs, ctx)
29
28
 
30
- const onDragEnd = () => {
29
+ const onDragEnd = (e: DragEvent) => {
31
30
  const preview = dragPreviewRef.value
32
31
  if (!preview) return
33
32
 
34
33
  if (preview.dataset.show === 'false') return
35
34
 
35
+ e.preventDefault()
36
+ e.stopPropagation()
37
+
36
38
  const previewRoot = preview?.querySelector('tbody')
37
39
 
38
40
  while (previewRoot?.firstChild)
39
41
  previewRoot?.removeChild(previewRoot.firstChild)
40
42
 
41
43
  if (preview) preview.dataset.show = 'false'
44
+ dragInfo.value = undefined
42
45
  }
43
46
 
44
- const onDrop = () => {
47
+ const onDrop = (e: DragEvent) => {
45
48
  const preview = dragPreviewRef.value
46
49
  if (!preview) return
47
50
  const yHandle = yLineHandleRef.value
@@ -53,10 +56,22 @@ export function useDragHandlers(
53
56
  if (!ctx) return
54
57
  if (preview.dataset.show === 'false') return
55
58
 
59
+ // Prevent browser default drop behavior and stop ProseMirror from handling
60
+ e.preventDefault()
61
+ e.stopPropagation()
62
+
56
63
  yHandle.dataset.show = 'false'
57
64
  xHandle.dataset.show = 'false'
58
65
 
59
- if (info.startIndex === info.endIndex) return
66
+ if (info.startIndex === info.endIndex) {
67
+ // Clean up preview even when no move is needed
68
+ const previewRoot = preview?.querySelector('tbody')
69
+ while (previewRoot?.firstChild)
70
+ previewRoot?.removeChild(previewRoot.firstChild)
71
+ preview.dataset.show = 'false'
72
+ dragInfo.value = undefined
73
+ return
74
+ }
60
75
 
61
76
  const commands = ctx.get(commandsCtx)
62
77
  const payload = {
@@ -82,26 +97,24 @@ export function useDragHandlers(
82
97
  refs.hoverIndex.value = index
83
98
  }
84
99
 
100
+ // Clean up preview
101
+ const previewRoot = preview?.querySelector('tbody')
102
+ while (previewRoot?.firstChild)
103
+ previewRoot?.removeChild(previewRoot.firstChild)
104
+ preview.dataset.show = 'false'
105
+ dragInfo.value = undefined
106
+
85
107
  requestAnimationFrame(() => {
86
108
  ctx.get(editorViewCtx).focus()
87
109
  })
88
110
  }
89
111
  const onDragOver = createDragOverHandler(refs)
90
112
 
91
- onMounted(() => {
92
- window.addEventListener('dragover', onDragOver)
93
- window.addEventListener('dragend', onDragEnd)
94
- window.addEventListener('drop', onDrop)
95
- })
96
-
97
- onUnmounted(() => {
98
- window.removeEventListener('dragover', onDragOver)
99
- window.removeEventListener('dragend', onDragEnd)
100
- window.removeEventListener('drop', onDrop)
101
- })
102
-
103
113
  return {
104
114
  dragRow,
105
115
  dragCol,
116
+ onDragOver,
117
+ onDrop,
118
+ onDragEnd,
106
119
  }
107
120
  }
@@ -88,7 +88,12 @@ export function recoveryStateBetweenUpdate(
88
88
  if (!node) return
89
89
  if (!view) return
90
90
  const { selection } = view.state
91
- if (!(selection instanceof CellSelection)) return
91
+ const isCellSelection = (sel: any) =>
92
+ sel &&
93
+ typeof sel.isColSelection === 'function' &&
94
+ typeof sel.forEachCell === 'function'
95
+
96
+ if (!isCellSelection(selection)) return
92
97
 
93
98
  const { $from } = selection
94
99
  const table = findTable($from)