@jvs-milkdown/components 1.2.0 → 1.2.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 (139) hide show
  1. package/lib/code-block/index.js +37 -2
  2. package/lib/code-block/index.js.map +1 -1
  3. package/lib/image-block/index.js +628 -107
  4. package/lib/image-block/index.js.map +1 -1
  5. package/lib/table-block/index.js +67 -44
  6. package/lib/table-block/index.js.map +1 -1
  7. package/lib/tsconfig.tsbuildinfo +1 -1
  8. package/package.json +10 -10
  9. package/src/code-block/config.ts +4 -0
  10. package/src/code-block/view/components/code-block.tsx +24 -0
  11. package/src/code-block/view/node-view.ts +15 -0
  12. package/src/image-block/config.ts +10 -0
  13. package/src/image-block/schema.ts +16 -0
  14. package/src/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.tsx +14 -0
  15. package/src/image-block/view/components/image-block.tsx +40 -0
  16. package/src/image-block/view/components/image-viewer.tsx +548 -65
  17. package/src/image-block/view/index.ts +42 -0
  18. package/src/table-block/view/component.tsx +31 -14
  19. package/src/table-block/view/operation.ts +49 -30
  20. package/lib/__internal__/components/icon.d.ts +0 -24
  21. package/lib/__internal__/components/icon.d.ts.map +0 -1
  22. package/lib/__internal__/components/image-input.d.ts +0 -17
  23. package/lib/__internal__/components/image-input.d.ts.map +0 -1
  24. package/lib/__internal__/keep-alive.d.ts +0 -2
  25. package/lib/__internal__/keep-alive.d.ts.map +0 -1
  26. package/lib/__internal__/meta.d.ts +0 -3
  27. package/lib/__internal__/meta.d.ts.map +0 -1
  28. package/lib/__tests__/setup.d.ts +0 -2
  29. package/lib/__tests__/setup.d.ts.map +0 -1
  30. package/lib/code-block/config.d.ts +0 -23
  31. package/lib/code-block/config.d.ts.map +0 -1
  32. package/lib/code-block/index.d.ts +0 -5
  33. package/lib/code-block/index.d.ts.map +0 -1
  34. package/lib/code-block/view/components/code-block.d.ts +0 -16
  35. package/lib/code-block/view/components/code-block.d.ts.map +0 -1
  36. package/lib/code-block/view/components/copy-button.d.ts +0 -9
  37. package/lib/code-block/view/components/copy-button.d.ts.map +0 -1
  38. package/lib/code-block/view/components/language-picker.d.ts +0 -5
  39. package/lib/code-block/view/components/language-picker.d.ts.map +0 -1
  40. package/lib/code-block/view/components/preview-panel.d.ts +0 -9
  41. package/lib/code-block/view/components/preview-panel.d.ts.map +0 -1
  42. package/lib/code-block/view/index.d.ts +0 -3
  43. package/lib/code-block/view/index.d.ts.map +0 -1
  44. package/lib/code-block/view/loader.d.ts +0 -13
  45. package/lib/code-block/view/loader.d.ts.map +0 -1
  46. package/lib/code-block/view/node-view.d.ts +0 -40
  47. package/lib/code-block/view/node-view.d.ts.map +0 -1
  48. package/lib/image-block/config.d.ts +0 -16
  49. package/lib/image-block/config.d.ts.map +0 -1
  50. package/lib/image-block/convert-plugin.d.ts +0 -2
  51. package/lib/image-block/convert-plugin.d.ts.map +0 -1
  52. package/lib/image-block/index.d.ts +0 -9
  53. package/lib/image-block/index.d.ts.map +0 -1
  54. package/lib/image-block/paste-rule.d.ts +0 -2
  55. package/lib/image-block/paste-rule.d.ts.map +0 -1
  56. package/lib/image-block/remark-plugin.d.ts +0 -2
  57. package/lib/image-block/remark-plugin.d.ts.map +0 -1
  58. package/lib/image-block/schema.d.ts +0 -3
  59. package/lib/image-block/schema.d.ts.map +0 -1
  60. package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts +0 -2
  61. package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts.map +0 -1
  62. package/lib/image-block/view/components/image-block.d.ts +0 -19
  63. package/lib/image-block/view/components/image-block.d.ts.map +0 -1
  64. package/lib/image-block/view/components/image-viewer.d.ts +0 -3
  65. package/lib/image-block/view/components/image-viewer.d.ts.map +0 -1
  66. package/lib/image-block/view/index.d.ts +0 -3
  67. package/lib/image-block/view/index.d.ts.map +0 -1
  68. package/lib/image-inline/components/image-inline.d.ts +0 -18
  69. package/lib/image-inline/components/image-inline.d.ts.map +0 -1
  70. package/lib/image-inline/config.d.ts +0 -11
  71. package/lib/image-inline/config.d.ts.map +0 -1
  72. package/lib/image-inline/index.d.ts +0 -5
  73. package/lib/image-inline/index.d.ts.map +0 -1
  74. package/lib/image-inline/view.d.ts +0 -3
  75. package/lib/image-inline/view.d.ts.map +0 -1
  76. package/lib/index.d.ts +0 -2
  77. package/lib/index.d.ts.map +0 -1
  78. package/lib/link-tooltip/command.d.ts +0 -2
  79. package/lib/link-tooltip/command.d.ts.map +0 -1
  80. package/lib/link-tooltip/configure.d.ts +0 -3
  81. package/lib/link-tooltip/configure.d.ts.map +0 -1
  82. package/lib/link-tooltip/edit/component.d.ts +0 -11
  83. package/lib/link-tooltip/edit/component.d.ts.map +0 -1
  84. package/lib/link-tooltip/edit/edit-configure.d.ts +0 -3
  85. package/lib/link-tooltip/edit/edit-configure.d.ts.map +0 -1
  86. package/lib/link-tooltip/edit/edit-view.d.ts +0 -15
  87. package/lib/link-tooltip/edit/edit-view.d.ts.map +0 -1
  88. package/lib/link-tooltip/index.d.ts +0 -7
  89. package/lib/link-tooltip/index.d.ts.map +0 -1
  90. package/lib/link-tooltip/preview/component.d.ts +0 -11
  91. package/lib/link-tooltip/preview/component.d.ts.map +0 -1
  92. package/lib/link-tooltip/preview/preview-configure.d.ts +0 -3
  93. package/lib/link-tooltip/preview/preview-configure.d.ts.map +0 -1
  94. package/lib/link-tooltip/preview/preview-view.d.ts +0 -14
  95. package/lib/link-tooltip/preview/preview-view.d.ts.map +0 -1
  96. package/lib/link-tooltip/slices.d.ts +0 -34
  97. package/lib/link-tooltip/slices.d.ts.map +0 -1
  98. package/lib/link-tooltip/tooltips.d.ts +0 -3
  99. package/lib/link-tooltip/tooltips.d.ts.map +0 -1
  100. package/lib/link-tooltip/utils.d.ts +0 -14
  101. package/lib/link-tooltip/utils.d.ts.map +0 -1
  102. package/lib/list-item-block/component.d.ts +0 -19
  103. package/lib/list-item-block/component.d.ts.map +0 -1
  104. package/lib/list-item-block/config.d.ts +0 -13
  105. package/lib/list-item-block/config.d.ts.map +0 -1
  106. package/lib/list-item-block/index.d.ts +0 -6
  107. package/lib/list-item-block/index.d.ts.map +0 -1
  108. package/lib/list-item-block/view.d.ts +0 -3
  109. package/lib/list-item-block/view.d.ts.map +0 -1
  110. package/lib/table-block/config.d.ts +0 -8
  111. package/lib/table-block/config.d.ts.map +0 -1
  112. package/lib/table-block/dnd/calc-drag-over.d.ts +0 -3
  113. package/lib/table-block/dnd/calc-drag-over.d.ts.map +0 -1
  114. package/lib/table-block/dnd/create-drag-handler.d.ts +0 -5
  115. package/lib/table-block/dnd/create-drag-handler.d.ts.map +0 -1
  116. package/lib/table-block/dnd/drag-over-handler.d.ts +0 -3
  117. package/lib/table-block/dnd/drag-over-handler.d.ts.map +0 -1
  118. package/lib/table-block/dnd/prepare-dnd-context.d.ts +0 -3
  119. package/lib/table-block/dnd/prepare-dnd-context.d.ts.map +0 -1
  120. package/lib/table-block/dnd/preview.d.ts +0 -3
  121. package/lib/table-block/dnd/preview.d.ts.map +0 -1
  122. package/lib/table-block/index.d.ts +0 -5
  123. package/lib/table-block/index.d.ts.map +0 -1
  124. package/lib/table-block/view/component.d.ts +0 -16
  125. package/lib/table-block/view/component.d.ts.map +0 -1
  126. package/lib/table-block/view/drag.d.ts +0 -10
  127. package/lib/table-block/view/drag.d.ts.map +0 -1
  128. package/lib/table-block/view/index.d.ts +0 -2
  129. package/lib/table-block/view/index.d.ts.map +0 -1
  130. package/lib/table-block/view/operation.d.ts +0 -13
  131. package/lib/table-block/view/operation.d.ts.map +0 -1
  132. package/lib/table-block/view/pointer.d.ts +0 -7
  133. package/lib/table-block/view/pointer.d.ts.map +0 -1
  134. package/lib/table-block/view/types.d.ts +0 -28
  135. package/lib/table-block/view/types.d.ts.map +0 -1
  136. package/lib/table-block/view/utils.d.ts +0 -12
  137. package/lib/table-block/view/utils.d.ts.map +0 -1
  138. package/lib/table-block/view/view.d.ts +0 -22
  139. package/lib/table-block/view/view.d.ts.map +0 -1
@@ -8,45 +8,61 @@ import { IMAGE_DATA_TYPE } from '../../schema'
8
8
 
9
9
  keepAlive(h, Fragment)
10
10
 
11
+ const ratioMap: Record<string, number> = {
12
+ '1:1': 1,
13
+ '4:3': 4 / 3,
14
+ '3:2': 3 / 2,
15
+ '16:9': 16 / 9,
16
+ }
17
+
11
18
  export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
12
19
  props: {
13
- src: {
14
- type: Object,
15
- required: true,
16
- },
17
- caption: {
18
- type: Object,
19
- required: true,
20
- },
21
- ratio: {
22
- type: Object,
23
- required: true,
24
- },
25
- align: {
26
- type: Object,
27
- required: true,
28
- },
29
- selected: {
30
- type: Object,
31
- required: true,
32
- },
33
- readonly: {
34
- type: Object,
35
- required: true,
36
- },
37
- setAttr: {
38
- type: Function,
39
- required: true,
40
- },
41
- config: {
42
- type: Object,
43
- required: true,
44
- },
20
+ src: { type: Object, required: true },
21
+ caption: { type: Object, required: true },
22
+ ratio: { type: Object, required: true },
23
+ align: { type: Object, required: true },
24
+ cropRatio: { type: Object, required: true },
25
+ cropTop: { type: Object, required: true },
26
+ cropLeft: { type: Object, required: true },
27
+ cropWidth: { type: Object, required: true },
28
+ cropHeight: { type: Object, required: true },
29
+ borderWidth: { type: Object, required: true },
30
+ borderColor: { type: Object, required: true },
31
+ borderStyle: { type: Object, required: true },
32
+ selected: { type: Object, required: true },
33
+ readonly: { type: Object, required: true },
34
+ setAttr: { type: Function, required: true },
35
+ config: { type: Object, required: true },
45
36
  },
46
- setup({ src, caption, ratio, readonly, setAttr, config }) {
37
+ setup({
38
+ src,
39
+ caption,
40
+ ratio,
41
+ readonly,
42
+ setAttr,
43
+ config,
44
+ cropRatio,
45
+ cropTop,
46
+ cropLeft,
47
+ cropWidth,
48
+ cropHeight,
49
+ borderWidth,
50
+ borderColor,
51
+ borderStyle,
52
+ }) {
47
53
  const imageRef = ref<HTMLImageElement>()
54
+ const wrapperRef = ref<HTMLDivElement>()
48
55
  const showCaption = ref(Boolean(caption.value?.length))
49
56
  const timer = ref(0)
57
+ const showBorderPanel = ref(false)
58
+ const isCropping = ref(false)
59
+
60
+ // Display dimensions (set in onImageLoad)
61
+ const displayW = ref(0)
62
+ const displayH = ref(0)
63
+
64
+ // Local crop state for interactive editing
65
+ const localCrop = ref({ top: 0, left: 0, width: 1, height: 1 })
50
66
 
51
67
  let resizeDir = ''
52
68
  let startY = 0
@@ -54,6 +70,14 @@ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
54
70
  let startHeight = 0
55
71
  let startWidth = 0
56
72
 
73
+ const isCropped = () => {
74
+ const t = cropTop.value ?? 0
75
+ const l = cropLeft.value ?? 0
76
+ const w = cropWidth.value ?? 1
77
+ const h = cropHeight.value ?? 1
78
+ return t > 0.001 || l > 0.001 || w < 0.999 || h < 0.999
79
+ }
80
+
57
81
  const onImageLoad = () => {
58
82
  const image = imageRef.value
59
83
  if (!image) return
@@ -80,9 +104,13 @@ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
80
104
  image.style.height = `${h}px`
81
105
 
82
106
  if (config.maxWidth) image.style.maxWidth = `${config.maxWidth}px`
107
+
108
+ const dw = width < maxWidth ? width : maxWidth
109
+ displayW.value = dw
110
+ displayH.value = parseFloat(h)
83
111
  }
84
112
 
85
- const onToggleCaption = (e: PointerEvent) => {
113
+ const onToggleCaption = (e: MouseEvent) => {
86
114
  e.preventDefault()
87
115
  e.stopPropagation()
88
116
  if (readonly.value) return
@@ -172,43 +200,498 @@ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
172
200
  window.addEventListener('pointerup', onResizeHandlePointerUp)
173
201
  }
174
202
 
203
+ // --- Crop box interaction ---
204
+ const borderStyleOptions = [
205
+ { label: 'none', value: 'none' },
206
+ { label: 'solid', value: 'solid' },
207
+ { label: 'dashed', value: 'dashed' },
208
+ { label: 'dotted', value: 'dotted' },
209
+ ]
210
+
211
+ const onToggleCropPanel = (e: MouseEvent) => {
212
+ e.preventDefault()
213
+ e.stopPropagation()
214
+ if (readonly.value) return
215
+ showBorderPanel.value = false
216
+ if (isCropping.value) {
217
+ isCropping.value = false
218
+ } else {
219
+ startCropping()
220
+ }
221
+ }
222
+
223
+ const onToggleBorderPanel = (e: MouseEvent) => {
224
+ e.preventDefault()
225
+ e.stopPropagation()
226
+ if (readonly.value) return
227
+ showBorderPanel.value = !showBorderPanel.value
228
+ }
229
+
230
+ const closePanels = () => {
231
+ showBorderPanel.value = false
232
+ }
233
+
234
+ const startCropping = () => {
235
+ localCrop.value = {
236
+ top: cropTop.value ?? 0,
237
+ left: cropLeft.value ?? 0,
238
+ width: cropWidth.value ?? 1,
239
+ height: cropHeight.value ?? 1,
240
+ }
241
+ isCropping.value = true
242
+ }
243
+
244
+ const applyCrop = () => {
245
+ setAttr('cropTop', localCrop.value.top)
246
+ setAttr('cropLeft', localCrop.value.left)
247
+ setAttr('cropWidth', localCrop.value.width)
248
+ setAttr('cropHeight', localCrop.value.height)
249
+ isCropping.value = false
250
+ }
251
+
252
+ const cancelCrop = () => {
253
+ isCropping.value = false
254
+ }
255
+
256
+ const resetCrop = () => {
257
+ setAttr('cropTop', 0)
258
+ setAttr('cropLeft', 0)
259
+ setAttr('cropWidth', 1)
260
+ setAttr('cropHeight', 1)
261
+ }
262
+
263
+ // Crop box drag state
264
+ let cropDragType = '' // 'move' | 'nw' | 'ne' | 'sw' | 'se' | 'n' | 's' | 'e' | 'w'
265
+ let cropStartX = 0
266
+ let cropStartY = 0
267
+ let cropStartBox = { top: 0, left: 0, width: 1, height: 1 }
268
+
269
+ const onCropBoxPointerDown = (e: PointerEvent, type: string) => {
270
+ e.preventDefault()
271
+ e.stopPropagation()
272
+ cropDragType = type
273
+ cropStartX = e.clientX
274
+ cropStartY = e.clientY
275
+ cropStartBox = { ...localCrop.value }
276
+ window.addEventListener('pointermove', onCropBoxPointerMove)
277
+ window.addEventListener('pointerup', onCropBoxPointerUp)
278
+ }
279
+
280
+ const onCropBoxPointerMove = (e: PointerEvent) => {
281
+ const wrapper = wrapperRef.value
282
+ if (!wrapper) return
283
+ const rect = wrapper.getBoundingClientRect()
284
+ if (!rect.width || !rect.height) return
285
+
286
+ const dx = (e.clientX - cropStartX) / rect.width
287
+ const dy = (e.clientY - cropStartY) / rect.height
288
+
289
+ const ar = ratioMap[cropRatio.value ?? 'free']
290
+ let box = { ...cropStartBox }
291
+
292
+ if (cropDragType === 'move') {
293
+ box.left = Math.max(0, Math.min(1 - box.width, cropStartBox.left + dx))
294
+ box.top = Math.max(0, Math.min(1 - box.height, cropStartBox.top + dy))
295
+ } else {
296
+ const isLeft =
297
+ cropDragType === 'nw' || cropDragType === 'sw' || cropDragType === 'w'
298
+ const isTop =
299
+ cropDragType === 'nw' || cropDragType === 'ne' || cropDragType === 'n'
300
+ const isRight =
301
+ cropDragType === 'ne' || cropDragType === 'se' || cropDragType === 'e'
302
+ const isBottom =
303
+ cropDragType === 'sw' || cropDragType === 'se' || cropDragType === 's'
304
+ const isHorizontal = cropDragType === 'n' || cropDragType === 's'
305
+ const isVertical = cropDragType === 'e' || cropDragType === 'w'
306
+
307
+ let newLeft = cropStartBox.left
308
+ let newTop = cropStartBox.top
309
+ let newWidth = cropStartBox.width
310
+ let newHeight = cropStartBox.height
311
+
312
+ if (isLeft) {
313
+ newWidth = cropStartBox.width - dx
314
+ newLeft = cropStartBox.left + dx
315
+ } else if (isRight) {
316
+ newWidth = cropStartBox.width + dx
317
+ }
318
+
319
+ if (isTop) {
320
+ newHeight = cropStartBox.height - dy
321
+ newTop = cropStartBox.top + dy
322
+ } else if (isBottom) {
323
+ newHeight = cropStartBox.height + dy
324
+ }
325
+
326
+ // Enforce aspect ratio if set (only for corner handles)
327
+ if (ar && !isHorizontal && !isVertical) {
328
+ const cropPixelW = newWidth * rect.width
329
+ const desiredH = cropPixelW / ar / rect.height
330
+
331
+ if (isTop) {
332
+ newTop = newTop + newHeight - desiredH
333
+ }
334
+ newHeight = desiredH
335
+ }
336
+
337
+ // Clamp
338
+ if (newWidth < 0.05) newWidth = 0.05
339
+ if (newHeight < 0.05) newHeight = 0.05
340
+ if (newLeft < 0) {
341
+ newWidth += newLeft
342
+ newLeft = 0
343
+ }
344
+ if (newTop < 0) {
345
+ newHeight += newTop
346
+ newTop = 0
347
+ }
348
+ if (newLeft + newWidth > 1) newWidth = 1 - newLeft
349
+ if (newTop + newHeight > 1) newHeight = 1 - newTop
350
+
351
+ box = { left: newLeft, top: newTop, width: newWidth, height: newHeight }
352
+ }
353
+
354
+ localCrop.value = box
355
+ }
356
+
357
+ const onCropBoxPointerUp = () => {
358
+ window.removeEventListener('pointermove', onCropBoxPointerMove)
359
+ window.removeEventListener('pointerup', onCropBoxPointerUp)
360
+ cropDragType = ''
361
+ }
362
+
175
363
  return () => {
364
+ const currentBorderStyle = borderStyle.value ?? 'none'
365
+ const currentBorderWidth = borderWidth.value ?? 0
366
+ const currentBorderColor = borderColor.value ?? '#000000'
367
+ const cTop = cropTop.value ?? 0
368
+ const cLeft = cropLeft.value ?? 0
369
+ const cWidth = cropWidth.value ?? 1
370
+ const cHeight = cropHeight.value ?? 1
371
+
372
+ const cropped = isCropped()
373
+
374
+ const borderStyleStr =
375
+ currentBorderStyle !== 'none' && currentBorderWidth > 0
376
+ ? `${currentBorderWidth}px ${currentBorderStyle} ${currentBorderColor}`
377
+ : undefined
378
+
379
+ const dw = displayW.value
380
+ const dh = displayH.value
381
+
382
+ // Use actual rendered dimensions when available for accurate crop margins
383
+ const imgEl = imageRef.value
384
+ const actualW = imgEl ? imgEl.getBoundingClientRect().width : dw
385
+ const actualH = imgEl ? imgEl.getBoundingClientRect().height : dh
386
+ const baseW = cropped && !isCropping.value && imgEl ? actualW : dw
387
+ const baseH = cropped && !isCropping.value && imgEl ? actualH : dh
388
+
389
+ const cropPxW = baseW * cWidth
390
+ const cropPxH = baseH * cHeight
391
+ const cropPxL = baseW * cLeft
392
+ const cropPxT = baseH * cTop
393
+
394
+ const wrapperStyle: Record<string, string> = {}
395
+ const cropClipStyle: Record<string, string> | null =
396
+ cropped && !isCropping.value && baseW > 0 && baseH > 0
397
+ ? {
398
+ width: `${cropPxW}px`,
399
+ height: `${cropPxH}px`,
400
+ overflow: 'hidden',
401
+ }
402
+ : null
403
+
404
+ const imgStyle: Record<string, string> = {}
405
+ if (cropped && !isCropping.value && baseW > 0 && baseH > 0) {
406
+ imgStyle.width = `${baseW}px`
407
+ imgStyle.height = `${baseH}px`
408
+ imgStyle.maxWidth = 'none'
409
+ imgStyle.marginLeft = `${-cropPxL}px`
410
+ imgStyle.marginTop = `${-cropPxT}px`
411
+ imgStyle.border = borderStyleStr ?? ''
412
+ } else {
413
+ imgStyle.border = borderStyleStr ?? ''
414
+ }
415
+
176
416
  return (
177
417
  <>
178
- <div class="image-wrapper">
179
- <div class="operation">
180
- <div class="operation-item" onPointerdown={onToggleCaption}>
418
+ {!isCropping.value ? (
419
+ <div
420
+ class="image-toolbar"
421
+ onClick={(e) => e.stopPropagation()}
422
+ onMousedown={(e) => e.stopPropagation()}
423
+ onDragstart={(e) => {
424
+ e.preventDefault()
425
+ e.stopPropagation()
426
+ }}
427
+ >
428
+ <div
429
+ class="image-toolbar-item"
430
+ title="标题"
431
+ onClick={onToggleCaption}
432
+ >
181
433
  <Icon icon={config.captionIcon} />
182
434
  </div>
435
+ {config.cropIcon ? (
436
+ <div
437
+ class="image-toolbar-item"
438
+ title="裁剪"
439
+ onClick={onToggleCropPanel}
440
+ >
441
+ <Icon icon={config.cropIcon} />
442
+ </div>
443
+ ) : null}
444
+ {cropped ? (
445
+ <div
446
+ class="image-toolbar-item"
447
+ title="恢复裁剪"
448
+ onClick={(e: MouseEvent) => {
449
+ e.stopPropagation()
450
+ resetCrop()
451
+ }}
452
+ >
453
+ <Icon icon={config.resetCropIcon ?? '↩'} />
454
+ </div>
455
+ ) : null}
456
+ {config.borderIcon ? (
457
+ <div
458
+ class="image-toolbar-item"
459
+ title="边框"
460
+ onClick={onToggleBorderPanel}
461
+ >
462
+ <Icon icon={config.borderIcon} />
463
+ </div>
464
+ ) : null}
465
+
466
+ {showBorderPanel.value ? (
467
+ <div
468
+ class="setting-panel border-panel"
469
+ onClick={(e) => e.stopPropagation()}
470
+ >
471
+ <div class="setting-panel-title">{'边框设置'}</div>
472
+ <div class="setting-row">
473
+ <span class="setting-label">{'样式'}</span>
474
+ <div class="border-style-options">
475
+ {borderStyleOptions.map((opt) => (
476
+ <div
477
+ class={`border-style-option ${currentBorderStyle === opt.value ? 'active' : ''}`}
478
+ onClick={(e: MouseEvent) => {
479
+ e.stopPropagation()
480
+ setAttr('borderStyle', opt.value)
481
+ }}
482
+ >
483
+ {opt.label === 'none'
484
+ ? '无'
485
+ : opt.label === 'solid'
486
+ ? '实线'
487
+ : opt.label === 'dashed'
488
+ ? '虚线'
489
+ : '点线'}
490
+ </div>
491
+ ))}
492
+ </div>
493
+ </div>
494
+ {currentBorderStyle !== 'none' ? (
495
+ <div class="setting-row">
496
+ <span class="setting-label">{'宽度'}</span>
497
+ <input
498
+ type="range"
499
+ min="1"
500
+ max="10"
501
+ value={currentBorderWidth}
502
+ onInput={(e: Event) => {
503
+ const target = e.target as HTMLInputElement
504
+ setAttr('borderWidth', Number(target.value))
505
+ }}
506
+ onClick={(e: MouseEvent) => e.stopPropagation()}
507
+ onPointerdown={(e: PointerEvent) => e.stopPropagation()}
508
+ />
509
+ <span class="setting-value">{currentBorderWidth}px</span>
510
+ </div>
511
+ ) : null}
512
+ {currentBorderStyle !== 'none' ? (
513
+ <div class="setting-row">
514
+ <span class="setting-label">{'颜色'}</span>
515
+ <input
516
+ type="color"
517
+ value={currentBorderColor}
518
+ onInput={(e: Event) => {
519
+ const target = e.target as HTMLInputElement
520
+ setAttr('borderColor', target.value)
521
+ }}
522
+ onClick={(e: MouseEvent) => e.stopPropagation()}
523
+ onPointerdown={(e: PointerEvent) => e.stopPropagation()}
524
+ />
525
+ </div>
526
+ ) : null}
527
+ </div>
528
+ ) : null}
183
529
  </div>
184
- <img
185
- ref={imageRef}
186
- data-type={IMAGE_DATA_TYPE}
187
- onLoad={onImageLoad}
188
- src={src.value}
189
- alt={caption.value}
190
- onError={(e) =>
191
- Promise.resolve(config.onImageLoadError?.(e)).catch(() => {})
192
- }
193
- />
194
- <div
195
- class="image-resize-handle top-left"
196
- onPointerdown={(e) => onResizeHandlePointerDown(e, 'top-left')}
197
- />
198
- <div
199
- class="image-resize-handle top-right"
200
- onPointerdown={(e) => onResizeHandlePointerDown(e, 'top-right')}
201
- />
202
- <div
203
- class="image-resize-handle bottom-left"
204
- onPointerdown={(e) => onResizeHandlePointerDown(e, 'bottom-left')}
205
- />
206
- <div
207
- class="image-resize-handle bottom-right"
208
- onPointerdown={(e) =>
209
- onResizeHandlePointerDown(e, 'bottom-right')
210
- }
211
- />
530
+ ) : null}
531
+
532
+ <div
533
+ class={`image-wrapper${isCropping.value ? ' cropping' : ''}`}
534
+ ref={wrapperRef}
535
+ style={wrapperStyle}
536
+ onClick={closePanels}
537
+ >
538
+ {cropClipStyle ? (
539
+ <div class="crop-clip" style={cropClipStyle}>
540
+ <img
541
+ draggable="false"
542
+ ref={imageRef}
543
+ data-type={IMAGE_DATA_TYPE}
544
+ onLoad={onImageLoad}
545
+ src={src.value}
546
+ alt={caption.value}
547
+ style={imgStyle}
548
+ onError={(e) =>
549
+ Promise.resolve(config.onImageLoadError?.(e)).catch(
550
+ () => {}
551
+ )
552
+ }
553
+ />
554
+ </div>
555
+ ) : (
556
+ <img
557
+ draggable="false"
558
+ ref={imageRef}
559
+ data-type={IMAGE_DATA_TYPE}
560
+ onLoad={onImageLoad}
561
+ src={src.value}
562
+ alt={caption.value}
563
+ style={imgStyle}
564
+ onError={(e) =>
565
+ Promise.resolve(config.onImageLoadError?.(e)).catch(() => {})
566
+ }
567
+ />
568
+ )}
569
+
570
+ {/* Crop overlay */}
571
+ {isCropping.value ? (
572
+ <div class="crop-overlay" onClick={(e) => e.stopPropagation()}>
573
+ <div
574
+ class="crop-box"
575
+ style={{
576
+ top: `${(localCrop.value.top * 100).toFixed(2)}%`,
577
+ left: `${(localCrop.value.left * 100).toFixed(2)}%`,
578
+ width: `${(localCrop.value.width * 100).toFixed(2)}%`,
579
+ height: `${(localCrop.value.height * 100).toFixed(2)}%`,
580
+ }}
581
+ onPointerdown={(e: PointerEvent) =>
582
+ onCropBoxPointerDown(e, 'move')
583
+ }
584
+ >
585
+ {/* 3x3 grid lines */}
586
+ <div class="crop-grid">
587
+ <div class="crop-grid-line horizontal first" />
588
+ <div class="crop-grid-line horizontal second" />
589
+ <div class="crop-grid-line vertical first" />
590
+ <div class="crop-grid-line vertical second" />
591
+ </div>
592
+ {/* Corner handles */}
593
+ <div
594
+ class="crop-handle nw"
595
+ onPointerdown={(e: PointerEvent) =>
596
+ onCropBoxPointerDown(e, 'nw')
597
+ }
598
+ />
599
+ <div
600
+ class="crop-handle ne"
601
+ onPointerdown={(e: PointerEvent) =>
602
+ onCropBoxPointerDown(e, 'ne')
603
+ }
604
+ />
605
+ <div
606
+ class="crop-handle sw"
607
+ onPointerdown={(e: PointerEvent) =>
608
+ onCropBoxPointerDown(e, 'sw')
609
+ }
610
+ />
611
+ <div
612
+ class="crop-handle se"
613
+ onPointerdown={(e: PointerEvent) =>
614
+ onCropBoxPointerDown(e, 'se')
615
+ }
616
+ />
617
+ {/* Edge midpoint handles */}
618
+ <div
619
+ class="crop-handle n"
620
+ onPointerdown={(e: PointerEvent) =>
621
+ onCropBoxPointerDown(e, 'n')
622
+ }
623
+ />
624
+ <div
625
+ class="crop-handle s"
626
+ onPointerdown={(e: PointerEvent) =>
627
+ onCropBoxPointerDown(e, 's')
628
+ }
629
+ />
630
+ <div
631
+ class="crop-handle e"
632
+ onPointerdown={(e: PointerEvent) =>
633
+ onCropBoxPointerDown(e, 'e')
634
+ }
635
+ />
636
+ <div
637
+ class="crop-handle w"
638
+ onPointerdown={(e: PointerEvent) =>
639
+ onCropBoxPointerDown(e, 'w')
640
+ }
641
+ />
642
+ </div>
643
+ {/* Bottom toolbar */}
644
+ <div class="crop-toolbar">
645
+ <div
646
+ class="crop-toolbar-btn"
647
+ onClick={(e: MouseEvent) => {
648
+ e.stopPropagation()
649
+ applyCrop()
650
+ }}
651
+ >
652
+ <Icon icon={config.confirmIcon ?? '✓'} />
653
+ </div>
654
+ <div
655
+ class="crop-toolbar-btn"
656
+ onClick={(e: MouseEvent) => {
657
+ e.stopPropagation()
658
+ cancelCrop()
659
+ }}
660
+ >
661
+ <Icon icon={config.cancelIcon ?? '✕'} />
662
+ </div>
663
+ </div>
664
+ </div>
665
+ ) : null}
666
+
667
+ {!isCropping.value ? (
668
+ <>
669
+ <div
670
+ class="image-resize-handle top-left"
671
+ onPointerdown={(e) =>
672
+ onResizeHandlePointerDown(e, 'top-left')
673
+ }
674
+ />
675
+ <div
676
+ class="image-resize-handle top-right"
677
+ onPointerdown={(e) =>
678
+ onResizeHandlePointerDown(e, 'top-right')
679
+ }
680
+ />
681
+ <div
682
+ class="image-resize-handle bottom-left"
683
+ onPointerdown={(e) =>
684
+ onResizeHandlePointerDown(e, 'bottom-left')
685
+ }
686
+ />
687
+ <div
688
+ class="image-resize-handle bottom-right"
689
+ onPointerdown={(e) =>
690
+ onResizeHandlePointerDown(e, 'bottom-right')
691
+ }
692
+ />
693
+ </>
694
+ ) : null}
212
695
  </div>
213
696
  {showCaption.value && (
214
697
  <input