@jvs-milkdown/components 1.2.29 → 1.2.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jvs-milkdown/components",
3
- "version": "1.2.29",
3
+ "version": "1.2.31",
4
4
  "keywords": [
5
5
  "milkdown",
6
6
  "milkdown plugin"
@@ -55,15 +55,15 @@
55
55
  "dependencies": {
56
56
  "@codemirror/merge": "^6.12.1",
57
57
  "@floating-ui/dom": "^1.5.1",
58
- "@jvs-milkdown/core": "^1.2.29",
59
- "@jvs-milkdown/ctx": "^1.2.29",
60
- "@jvs-milkdown/exception": "^1.2.29",
61
- "@jvs-milkdown/plugin-tooltip": "^1.2.29",
62
- "@jvs-milkdown/preset-commonmark": "^1.2.29",
63
- "@jvs-milkdown/preset-gfm": "^1.2.29",
64
- "@jvs-milkdown/prose": "^1.2.29",
65
- "@jvs-milkdown/transformer": "^1.2.29",
66
- "@jvs-milkdown/utils": "^1.2.29",
58
+ "@jvs-milkdown/core": "^1.2.31",
59
+ "@jvs-milkdown/ctx": "^1.2.31",
60
+ "@jvs-milkdown/exception": "^1.2.31",
61
+ "@jvs-milkdown/plugin-tooltip": "^1.2.31",
62
+ "@jvs-milkdown/preset-commonmark": "^1.2.31",
63
+ "@jvs-milkdown/preset-gfm": "^1.2.31",
64
+ "@jvs-milkdown/prose": "^1.2.31",
65
+ "@jvs-milkdown/transformer": "^1.2.31",
66
+ "@jvs-milkdown/utils": "^1.2.31",
67
67
  "@types/lodash-es": "^4.17.12",
68
68
  "clsx": "^2.0.0",
69
69
  "dompurify": "^3.2.5",
@@ -222,12 +222,6 @@ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
222
222
  }
223
223
 
224
224
  // --- Crop box interaction ---
225
- const borderStyleOptions = [
226
- { label: 'none', value: 'none' },
227
- { label: 'solid', value: 'solid' },
228
- { label: 'dashed', value: 'dashed' },
229
- { label: 'dotted', value: 'dotted' },
230
- ]
231
225
 
232
226
  const onToggleCropPanel = (e: MouseEvent) => {
233
227
  e.preventDefault()
@@ -241,11 +235,352 @@ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
241
235
  }
242
236
  }
243
237
 
244
- const onToggleBorderPanel = (e: MouseEvent) => {
238
+ const onToggleBorder = (e: MouseEvent) => {
245
239
  e.preventDefault()
246
240
  e.stopPropagation()
247
241
  if (readonly.value) return
248
- showBorderPanel.value = !showBorderPanel.value
242
+
243
+ const isSolid =
244
+ borderStyle.value === 'solid' && (borderWidth.value ?? 0) === 1
245
+ if (isSolid) {
246
+ setAttr('borderStyle', 'none')
247
+ setAttr('borderWidth', 0)
248
+ } else {
249
+ setAttr('borderStyle', 'solid')
250
+ setAttr('borderWidth', 1)
251
+ if (!borderColor.value) {
252
+ setAttr('borderColor', '#000000')
253
+ }
254
+ }
255
+ }
256
+
257
+ const openLightbox = (images: string[], startIndex: number) => {
258
+ let currentIndex = startIndex
259
+ let scale = 1.0
260
+ let rotate = 0
261
+ const doc = wrapperRef.value?.ownerDocument || document
262
+
263
+ const overlay = doc.createElement('div')
264
+ overlay.style.position = 'fixed'
265
+ overlay.style.inset = '0'
266
+ overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
267
+ overlay.style.zIndex = '99999'
268
+ overlay.style.display = 'flex'
269
+ overlay.style.flexDirection = 'column'
270
+ overlay.style.alignItems = 'center'
271
+ overlay.style.justifyContent = 'center'
272
+ overlay.style.userSelect = 'none'
273
+
274
+ // Image container
275
+ const imgContainer = doc.createElement('div')
276
+ imgContainer.style.position = 'relative'
277
+ imgContainer.style.display = 'flex'
278
+ imgContainer.style.alignItems = 'center'
279
+ imgContainer.style.justifyContent = 'center'
280
+ imgContainer.style.maxWidth = '90%'
281
+ imgContainer.style.maxHeight = '80%'
282
+
283
+ const img = doc.createElement('img')
284
+ img.src = images[currentIndex] || ''
285
+ img.style.maxWidth = '100%'
286
+ img.style.maxHeight = '100%'
287
+ img.style.objectFit = 'contain'
288
+ img.style.borderRadius = '4px'
289
+ img.style.transition = 'transform 0.2s ease-out, opacity 0.2s ease-out'
290
+ imgContainer.appendChild(img)
291
+
292
+ const applyTransform = () => {
293
+ img.style.transform = `scale(${scale}) rotate(${rotate}deg)`
294
+ }
295
+
296
+ // Navigation controls
297
+ const createArrow = (direction: 'left' | 'right') => {
298
+ const arrow = doc.createElement('div')
299
+ arrow.style.position = 'absolute'
300
+ arrow.style.top = '50%'
301
+ arrow.style.transform = 'translateY(-50%)'
302
+ arrow.style.width = '48px'
303
+ arrow.style.height = '48px'
304
+ arrow.style.borderRadius = '50%'
305
+ arrow.style.backgroundColor = 'rgba(0, 0, 0, 0.4)'
306
+ arrow.style.backdropFilter = 'blur(8px)'
307
+ ;(arrow.style as any).webkitBackdropFilter = 'blur(8px)'
308
+ arrow.style.color = '#ffffff'
309
+ arrow.style.display = 'flex'
310
+ arrow.style.alignItems = 'center'
311
+ arrow.style.justifyContent = 'center'
312
+ arrow.style.cursor = 'pointer'
313
+ arrow.style.transition = 'background-color 0.2s, transform 0.2s'
314
+ arrow.style.zIndex = '15'
315
+
316
+ const chevronSvg =
317
+ direction === 'left'
318
+ ? `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`
319
+ : `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`
320
+
321
+ arrow.innerHTML = chevronSvg
322
+
323
+ if (direction === 'left') {
324
+ arrow.style.left = '24px'
325
+ } else {
326
+ arrow.style.right = '24px'
327
+ }
328
+
329
+ arrow.addEventListener('mouseenter', () => {
330
+ arrow.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'
331
+ arrow.style.transform = 'translateY(-50%) scale(1.05)'
332
+ })
333
+ arrow.addEventListener('mouseleave', () => {
334
+ arrow.style.backgroundColor = 'rgba(0, 0, 0, 0.4)'
335
+ arrow.style.transform = 'translateY(-50%) scale(1)'
336
+ })
337
+ return arrow
338
+ }
339
+
340
+ const leftArrow = createArrow('left')
341
+ const rightArrow = createArrow('right')
342
+ overlay.appendChild(leftArrow)
343
+ overlay.appendChild(rightArrow)
344
+
345
+ const updateArrowsVisibility = () => {
346
+ leftArrow.style.display = currentIndex > 0 ? 'flex' : 'none'
347
+ rightArrow.style.display =
348
+ currentIndex < images.length - 1 ? 'flex' : 'none'
349
+ }
350
+
351
+ const showImage = (index: number) => {
352
+ if (index < 0 || index >= images.length) return
353
+ currentIndex = index
354
+ scale = 1.0
355
+ rotate = 0
356
+ img.style.transform = `scale(0.95) rotate(0deg)`
357
+ img.style.opacity = '0.5'
358
+ setTimeout(() => {
359
+ img.src = images[currentIndex] || ''
360
+ img.style.transform = `scale(1) rotate(0deg)`
361
+ img.style.opacity = '1'
362
+ }, 150)
363
+ updateArrowsVisibility()
364
+ updateIndicator()
365
+ }
366
+
367
+ leftArrow.addEventListener('click', (e) => {
368
+ e.stopPropagation()
369
+ showImage(currentIndex - 1)
370
+ })
371
+
372
+ rightArrow.addEventListener('click', (e) => {
373
+ e.stopPropagation()
374
+ showImage(currentIndex + 1)
375
+ })
376
+
377
+ // Bottom Container
378
+ const bottomContainer = doc.createElement('div')
379
+ bottomContainer.style.position = 'absolute'
380
+ bottomContainer.style.bottom = '24px'
381
+ bottomContainer.style.left = '50%'
382
+ bottomContainer.style.transform = 'translateX(-50%)'
383
+ bottomContainer.style.display = 'flex'
384
+ bottomContainer.style.flexDirection = 'column'
385
+ bottomContainer.style.alignItems = 'center'
386
+ bottomContainer.style.gap = '12px'
387
+ bottomContainer.style.zIndex = '15'
388
+
389
+ // Image Indicator
390
+ const indicator = doc.createElement('div')
391
+ indicator.style.color = '#ffffff'
392
+ indicator.style.fontSize = '14px'
393
+ indicator.style.fontFamily = 'system-ui, sans-serif'
394
+ indicator.style.fontWeight = '500'
395
+
396
+ const updateIndicator = () => {
397
+ indicator.textContent = `${currentIndex + 1} / ${images.length}`
398
+ }
399
+ updateIndicator()
400
+ updateArrowsVisibility()
401
+
402
+ // Pill Toolbar Container
403
+ const pillToolbar = doc.createElement('div')
404
+ pillToolbar.style.display = 'flex'
405
+ pillToolbar.style.alignItems = 'center'
406
+ pillToolbar.style.gap = '8px'
407
+ pillToolbar.style.padding = '6px 12px'
408
+ pillToolbar.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'
409
+ pillToolbar.style.backdropFilter = 'blur(12px)'
410
+ ;(pillToolbar.style as any).webkitBackdropFilter = 'blur(12px)'
411
+ pillToolbar.style.borderRadius = '24px'
412
+ pillToolbar.style.border = '1px solid rgba(255, 255, 255, 0.1)'
413
+ pillToolbar.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.3)'
414
+
415
+ const createPillButton = (
416
+ svgContent: string,
417
+ btnTitle: string,
418
+ onClickHandler: () => void
419
+ ) => {
420
+ const btn = doc.createElement('div')
421
+ btn.style.width = '32px'
422
+ btn.style.height = '32px'
423
+ btn.style.borderRadius = '50%'
424
+ btn.style.display = 'flex'
425
+ btn.style.alignItems = 'center'
426
+ btn.style.justifyContent = 'center'
427
+ btn.style.cursor = 'pointer'
428
+ btn.style.color = '#ffffff'
429
+ btn.style.transition = 'background-color 0.2s, transform 0.1s'
430
+ btn.title = btnTitle
431
+ btn.innerHTML = svgContent
432
+
433
+ btn.addEventListener('mouseenter', () => {
434
+ btn.style.backgroundColor = 'rgba(255, 255, 255, 0.15)'
435
+ })
436
+ btn.addEventListener('mouseleave', () => {
437
+ btn.style.backgroundColor = 'transparent'
438
+ })
439
+ btn.addEventListener('click', (e) => {
440
+ e.stopPropagation()
441
+ onClickHandler()
442
+ })
443
+ return btn
444
+ }
445
+
446
+ // Pill Buttons
447
+ const zoomOutSvg = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`
448
+ const zoomInSvg = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`
449
+ const resetSvg = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path></svg>`
450
+ const rotateLeftSvg = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><polyline points="3 3 3 8 8 8"></polyline></svg>`
451
+ const rotateRightSvg = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><polyline points="21 3 21 8 16 8"></polyline></svg>`
452
+
453
+ const btnZoomOut = createPillButton(zoomOutSvg, '缩小', () => {
454
+ scale = Math.max(0.2, Number((scale - 0.2).toFixed(2)))
455
+ applyTransform()
456
+ })
457
+ const btnZoomIn = createPillButton(zoomInSvg, '放大', () => {
458
+ scale = Math.min(5.0, Number((scale + 0.2).toFixed(2)))
459
+ applyTransform()
460
+ })
461
+ const btnReset = createPillButton(resetSvg, '还原', () => {
462
+ scale = 1.0
463
+ rotate = 0
464
+ applyTransform()
465
+ })
466
+ const btnRotateLeft = createPillButton(rotateLeftSvg, '向左旋转', () => {
467
+ rotate -= 90
468
+ applyTransform()
469
+ })
470
+ const btnRotateRight = createPillButton(
471
+ rotateRightSvg,
472
+ '向右旋转',
473
+ () => {
474
+ rotate += 90
475
+ applyTransform()
476
+ }
477
+ )
478
+
479
+ pillToolbar.appendChild(btnZoomOut)
480
+ pillToolbar.appendChild(btnZoomIn)
481
+ pillToolbar.appendChild(btnReset)
482
+ pillToolbar.appendChild(btnRotateLeft)
483
+ pillToolbar.appendChild(btnRotateRight)
484
+
485
+ bottomContainer.appendChild(indicator)
486
+ bottomContainer.appendChild(pillToolbar)
487
+
488
+ // Close button
489
+ const closeBtn = doc.createElement('div')
490
+ closeBtn.style.position = 'absolute'
491
+ closeBtn.style.top = '24px'
492
+ closeBtn.style.right = '24px'
493
+ closeBtn.style.width = '40px'
494
+ closeBtn.style.height = '40px'
495
+ closeBtn.style.borderRadius = '50%'
496
+ closeBtn.style.color = '#ffffff'
497
+ closeBtn.style.display = 'flex'
498
+ closeBtn.style.alignItems = 'center'
499
+ closeBtn.style.justifyContent = 'center'
500
+ closeBtn.style.cursor = 'pointer'
501
+ closeBtn.style.fontSize = '28px'
502
+ closeBtn.style.transition = 'background-color 0.2s'
503
+ closeBtn.style.zIndex = '15'
504
+ closeBtn.innerHTML = '&times;'
505
+ closeBtn.addEventListener('mouseenter', () => {
506
+ closeBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'
507
+ })
508
+ closeBtn.addEventListener('mouseleave', () => {
509
+ closeBtn.style.backgroundColor = 'transparent'
510
+ })
511
+
512
+ const closeLightbox = () => {
513
+ doc.removeEventListener('keydown', handleKeyDown)
514
+ overlay.style.opacity = '0'
515
+ overlay.style.transition = 'opacity 0.2s ease-out'
516
+ setTimeout(() => {
517
+ overlay.remove()
518
+ }, 200)
519
+ }
520
+
521
+ closeBtn.addEventListener('click', (e) => {
522
+ e.stopPropagation()
523
+ closeLightbox()
524
+ })
525
+
526
+ overlay.addEventListener('click', () => {
527
+ closeLightbox()
528
+ })
529
+
530
+ const handleKeyDown = (e: KeyboardEvent) => {
531
+ if (e.key === 'Escape') {
532
+ closeLightbox()
533
+ } else if (e.key === 'ArrowLeft' && currentIndex > 0) {
534
+ showImage(currentIndex - 1)
535
+ } else if (e.key === 'ArrowRight' && currentIndex < images.length - 1) {
536
+ showImage(currentIndex + 1)
537
+ }
538
+ }
539
+
540
+ doc.addEventListener('keydown', handleKeyDown)
541
+
542
+ overlay.appendChild(closeBtn)
543
+ overlay.appendChild(imgContainer)
544
+ overlay.appendChild(bottomContainer)
545
+
546
+ overlay.style.opacity = '0'
547
+ overlay.style.transition = 'opacity 0.2s ease-out'
548
+ doc.body.appendChild(overlay)
549
+ requestAnimationFrame(() => {
550
+ overlay.style.opacity = '1'
551
+ })
552
+ }
553
+
554
+ const onImageDoubleClick = (e: MouseEvent) => {
555
+ e.preventDefault()
556
+ e.stopPropagation()
557
+
558
+ const currentImg = imageRef.value
559
+ const currentSrc = currentImg ? currentImg.src : src.value
560
+
561
+ const rootNode =
562
+ (wrapperRef.value?.getRootNode() as Document | ShadowRoot | null) ||
563
+ document
564
+ const allImageElements = rootNode.querySelectorAll('img')
565
+ const images: string[] = []
566
+ allImageElements.forEach((img: any) => {
567
+ // Exclude cover images and header decorations
568
+ if (
569
+ img.closest('.milkdown-document-cover, .milkdown-document-header')
570
+ ) {
571
+ return
572
+ }
573
+ if (img.src && !images.includes(img.src)) {
574
+ images.push(img.src)
575
+ }
576
+ })
577
+
578
+ if (images.length === 0 && currentSrc) {
579
+ images.push(currentSrc)
580
+ }
581
+
582
+ const startIndex = currentSrc ? images.indexOf(currentSrc) : 0
583
+ openLightbox(images, startIndex >= 0 ? startIndex : 0)
249
584
  }
250
585
 
251
586
  const closePanels = () => {
@@ -525,106 +860,13 @@ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
525
860
  ) : null}
526
861
  {config.borderIcon ? (
527
862
  <div
528
- class="image-toolbar-item"
863
+ class={`image-toolbar-item ${borderStyle.value === 'solid' && (borderWidth.value ?? 0) >= 1 ? 'active' : ''}`}
529
864
  title="边框"
530
- onClick={onToggleBorderPanel}
865
+ onClick={onToggleBorder}
531
866
  >
532
867
  <Icon icon={config.borderIcon} />
533
868
  </div>
534
869
  ) : null}
535
-
536
- {showBorderPanel.value ? (
537
- <div
538
- class="setting-panel border-panel"
539
- draggable="true"
540
- onClick={(e) => e.stopPropagation()}
541
- onMousedown={(e) => e.stopPropagation()}
542
- onPointerdown={(e) => e.stopPropagation()}
543
- onDragstart={(e) => {
544
- e.preventDefault()
545
- e.stopPropagation()
546
- }}
547
- >
548
- <div class="setting-panel-title">{'边框设置'}</div>
549
- <div class="setting-row">
550
- <span class="setting-label">{'样式'}</span>
551
- <div class="border-style-options">
552
- {borderStyleOptions.map((opt) => (
553
- <div
554
- class={`border-style-option ${currentBorderStyle === opt.value ? 'active' : ''}`}
555
- onClick={(e: MouseEvent) => {
556
- e.stopPropagation()
557
- setAttr('borderStyle', opt.value)
558
- }}
559
- >
560
- {opt.label === 'none'
561
- ? '无'
562
- : opt.label === 'solid'
563
- ? '实线'
564
- : opt.label === 'dashed'
565
- ? '虚线'
566
- : '点线'}
567
- </div>
568
- ))}
569
- </div>
570
- </div>
571
- {currentBorderStyle !== 'none' ? (
572
- <div class="setting-row">
573
- <span class="setting-label">{'宽度'}</span>
574
- <input
575
- draggable="true"
576
- type="range"
577
- min="1"
578
- max="10"
579
- value={currentBorderWidth}
580
- onInput={(e: Event) => {
581
- const target = e.target as HTMLInputElement
582
- localBorderWidth.value = Number(target.value)
583
- }}
584
- onChange={(e: Event) => {
585
- const target = e.target as HTMLInputElement
586
- setAttr('borderWidth', Number(target.value))
587
- localBorderWidth.value = null
588
- }}
589
- onClick={(e: MouseEvent) => e.stopPropagation()}
590
- onMousedown={(e: MouseEvent) => e.stopPropagation()}
591
- onPointerdown={(e: PointerEvent) => e.stopPropagation()}
592
- onDragstart={(e: DragEvent) => {
593
- e.preventDefault()
594
- e.stopPropagation()
595
- }}
596
- />
597
- <span class="setting-value">{currentBorderWidth}px</span>
598
- </div>
599
- ) : null}
600
- {currentBorderStyle !== 'none' ? (
601
- <div class="setting-row">
602
- <span class="setting-label">{'颜色'}</span>
603
- <input
604
- draggable="true"
605
- type="color"
606
- value={currentBorderColor}
607
- onInput={(e: Event) => {
608
- const target = e.target as HTMLInputElement
609
- localBorderColor.value = target.value
610
- }}
611
- onChange={(e: Event) => {
612
- const target = e.target as HTMLInputElement
613
- setAttr('borderColor', target.value)
614
- localBorderColor.value = null
615
- }}
616
- onClick={(e: MouseEvent) => e.stopPropagation()}
617
- onMousedown={(e: MouseEvent) => e.stopPropagation()}
618
- onPointerdown={(e: PointerEvent) => e.stopPropagation()}
619
- onDragstart={(e: DragEvent) => {
620
- e.preventDefault()
621
- e.stopPropagation()
622
- }}
623
- />
624
- </div>
625
- ) : null}
626
- </div>
627
- ) : null}
628
870
  </div>
629
871
  ) : null}
630
872
 
@@ -633,6 +875,7 @@ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
633
875
  ref={wrapperRef}
634
876
  style={wrapperStyle}
635
877
  onClick={closePanels}
878
+ onDblclick={onImageDoubleClick}
636
879
  >
637
880
  {cropClipStyle ? (
638
881
  <div class="crop-clip" style={cropClipStyle}>