@jvs-milkdown/crepe 1.2.25 → 1.2.26

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/feature/fixed-toolbar/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAA;AAkChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAqDxD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,EAClC,OAAO,CAAC,EAAE,yBAAyB,EACnC,GAAG,CAAC,EAAE,GAAG;;;;;;;;IAmhBV"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/feature/fixed-toolbar/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAA;AAkChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAqDxD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,EAClC,OAAO,CAAC,EAAE,yBAAyB,EACnC,GAAG,CAAC,EAAE,GAAG;;;;;;;;IAyiBV"}
@@ -29,8 +29,10 @@ export interface FixedToolbarConfig {
29
29
  showHistory?: boolean;
30
30
  showExport?: boolean;
31
31
  onExport?: (markdown: string, ctx: Ctx) => void;
32
+ exportItems?: ('markdown' | 'word' | 'pdf')[];
32
33
  showImport?: boolean;
33
34
  onImport?: (replaceContent: (markdown: string) => void, ctx: Ctx) => void;
35
+ importItems?: ('markdown' | 'word' | 'pdf')[];
34
36
  useLocalStorage?: boolean;
35
37
  id?: string;
36
38
  outlineVisible?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/feature/fixed-toolbar/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAA;AAIhD,OAAO,EAAU,SAAS,EAAiB,MAAM,+BAA+B,CAAA;AAehF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAUpD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;IAC3D,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAClC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC/C,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE;QACb,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,MAAM,CAAC,EAAE,OAAO,CAAA;KACjB,CAAA;IACD,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IACzE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,MAAM,MAAM,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;AAEnE,eAAO,MAAM,kBAAkB,0FAG9B,CAAA;AAED,eAAO,MAAM,eAAe,gBAA0C,CAAA;AAoXtE,eAAO,MAAM,kBAAkB,sCAK7B,CAAA;AAEF,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,yBAAyB,CA6BjE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/feature/fixed-toolbar/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAA;AAIhD,OAAO,EAAU,SAAS,EAAiB,MAAM,+BAA+B,CAAA;AAehF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAUpD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAA;AAE1B,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;IAC3D,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAClC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC/C,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE;QACb,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,MAAM,CAAC,EAAE,OAAO,CAAA;KACjB,CAAA;IACD,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/C,WAAW,CAAC,EAAE,CAAC,UAAU,GAAG,MAAM,GAAG,KAAK,CAAC,EAAE,CAAA;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IACzE,WAAW,CAAC,EAAE,CAAC,UAAU,GAAG,MAAM,GAAG,KAAK,CAAC,EAAE,CAAA;IAC7C,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,MAAM,MAAM,yBAAyB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;AAEnE,eAAO,MAAM,kBAAkB,0FAG9B,CAAA;AAED,eAAO,MAAM,eAAe,gBAA0C,CAAA;AAoXtE,eAAO,MAAM,kBAAkB,sCAK7B,CAAA;AAEF,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,yBAAyB,CA6BjE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jvs-milkdown/crepe",
3
- "version": "1.2.25",
3
+ "version": "1.2.26",
4
4
  "keywords": [
5
5
  "crepe",
6
6
  "editor",
@@ -107,9 +107,9 @@
107
107
  "@codemirror/theme-one-dark": "^6.1.2",
108
108
  "@codemirror/view": "^6.26.0",
109
109
  "@floating-ui/dom": "^1.7.6",
110
- "@jvs-milkdown/kit": "^1.2.25",
111
- "@jvs-milkdown/prose": "^1.2.25",
112
- "@jvs-milkdown/utils": "^1.2.25",
110
+ "@jvs-milkdown/kit": "^1.2.26",
111
+ "@jvs-milkdown/prose": "^1.2.26",
112
+ "@jvs-milkdown/utils": "^1.2.26",
113
113
  "@types/lodash-es": "^4.17.12",
114
114
  "clsx": "^2.0.0",
115
115
  "codemirror": "^6.0.1",
@@ -25,3 +25,18 @@ test('should use custom config to override the default config', () => {
25
25
  mockCppLanguageDescription,
26
26
  ])
27
27
  })
28
+
29
+ test('should apply exportItems and importItems config to FixedToolbar', () => {
30
+ const myConfig = applyConfig({
31
+ [CrepeFeature.FixedToolbar]: {
32
+ exportItems: ['markdown', 'word'],
33
+ importItems: ['markdown'],
34
+ },
35
+ })
36
+
37
+ expect(myConfig[CrepeFeature.FixedToolbar]?.exportItems).toEqual([
38
+ 'markdown',
39
+ 'word',
40
+ ])
41
+ expect(myConfig[CrepeFeature.FixedToolbar]?.importItems).toEqual(['markdown'])
42
+ })
@@ -3,7 +3,7 @@ import type { Node as ProseNode } from '@jvs-milkdown/kit/prose/model'
3
3
 
4
4
  import { imageBlockSchema } from '@jvs-milkdown/kit/component/image-block'
5
5
  import { toggleLinkCommand } from '@jvs-milkdown/kit/component/link-tooltip'
6
- import { commandsCtx, editorViewCtx } from '@jvs-milkdown/kit/core'
6
+ import { commandsCtx, editorViewCtx, rootCtx } from '@jvs-milkdown/kit/core'
7
7
  import {
8
8
  addBlockTypeCommand,
9
9
  blockquoteSchema,
@@ -564,7 +564,18 @@ export function buildDefaultFixedToolbar(
564
564
  active: () => false,
565
565
  onRun: (ctx) => {
566
566
  const markdown = getMarkdown()(ctx)
567
- if (_config?.onExport) {
567
+ const view = ctx.get(editorViewCtx)
568
+ const root = ctx.get(rootCtx) as HTMLElement
569
+ const rootNode = view.dom.getRootNode() as ShadowRoot | Document
570
+
571
+ if (_config?.exportItems && _config.exportItems.length > 0) {
572
+ const exportItems = _config.exportItems
573
+ if (exportItems.length === 1) {
574
+ handleDirectExport(exportItems[0]!, markdown, view, root)
575
+ } else {
576
+ showDownloadPopover(rootNode, root, view, markdown, exportItems)
577
+ }
578
+ } else if (_config?.onExport) {
568
579
  _config.onExport(markdown, ctx)
569
580
  } else {
570
581
  const blob = new Blob([markdown], {
@@ -587,7 +598,18 @@ export function buildDefaultFixedToolbar(
587
598
  icon: importIcon,
588
599
  active: () => false,
589
600
  onRun: (ctx) => {
590
- if (_config?.onImport) {
601
+ const view = ctx.get(editorViewCtx)
602
+ const root = ctx.get(rootCtx) as HTMLElement
603
+ const rootNode = view.dom.getRootNode() as ShadowRoot | Document
604
+
605
+ if (_config?.importItems && _config.importItems.length > 0) {
606
+ const importItems = _config.importItems
607
+ if (importItems.length === 1) {
608
+ handleDirectImport(importItems[0]!, root, ctx)
609
+ } else {
610
+ showImportPopover(rootNode, root, ctx, importItems)
611
+ }
612
+ } else if (_config?.onImport) {
591
613
  _config.onImport((markdown) => replaceAll(markdown)(ctx), ctx)
592
614
  } else {
593
615
  const input = document.createElement('input')
@@ -622,3 +644,460 @@ export function buildDefaultFixedToolbar(
622
644
 
623
645
  return builder.build()
624
646
  }
647
+
648
+ const activeDownloadPopovers = new WeakMap<HTMLElement, HTMLElement>()
649
+ const activeImportPopovers = new WeakMap<HTMLElement, HTMLElement>()
650
+
651
+ const ensureStyles = (rootNode: ShadowRoot | Document) => {
652
+ const target = rootNode instanceof ShadowRoot ? rootNode : document.head
653
+ if (target.querySelector('#download-popover-styles')) return
654
+
655
+ const styleEl = document.createElement('style')
656
+ styleEl.id = 'download-popover-styles'
657
+ styleEl.textContent = `
658
+ .download-popover, .import-popover {
659
+ position: fixed;
660
+ z-index: 10000;
661
+ width: 140px;
662
+ padding: 6px 0;
663
+ display: flex;
664
+ flex-direction: column;
665
+ background-color: var(--crepe-color-surface, #ffffff);
666
+ border: 1px solid var(--crepe-color-outline-variant, color-mix(in srgb, var(--crepe-color-outline), transparent 80%));
667
+ border-radius: 8px;
668
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
669
+ opacity: 0;
670
+ transform: translateY(-8px);
671
+ transition: opacity 0.15s ease, transform 0.15s ease;
672
+ pointer-events: none;
673
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
674
+ }
675
+ .download-popover.show, .import-popover.show {
676
+ opacity: 1;
677
+ transform: translateY(0);
678
+ pointer-events: auto;
679
+ }
680
+ .download-popover-item, .import-popover-item {
681
+ cursor: pointer;
682
+ padding: 8px 14px;
683
+ display: flex;
684
+ align-items: center;
685
+ gap: 8px;
686
+ font-size: 13px;
687
+ font-weight: 500;
688
+ color: var(--crepe-color-primary, #363B4C);
689
+ transition: background-color 0.2s;
690
+ user-select: none;
691
+ }
692
+ .download-popover-item:hover, .import-popover-item:hover {
693
+ background-color: var(--crepe-color-hover, #f5f5f5);
694
+ }
695
+ .download-popover-item-icon, .import-popover-item-icon {
696
+ display: flex;
697
+ align-items: center;
698
+ justify-content: center;
699
+ width: 14px;
700
+ height: 14px;
701
+ color: var(--crepe-color-primary, #363B4C);
702
+ opacity: 0.8;
703
+ }
704
+ `
705
+ target.appendChild(styleEl)
706
+ }
707
+
708
+ function handleDirectExport(
709
+ type: 'markdown' | 'word' | 'pdf',
710
+ markdown: string,
711
+ view: any,
712
+ root: HTMLElement
713
+ ) {
714
+ if (type === 'markdown') {
715
+ const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8;' })
716
+ const url = URL.createObjectURL(blob)
717
+ const link = document.createElement('a')
718
+ link.href = url
719
+ link.download = 'document.md'
720
+ link.click()
721
+ URL.revokeObjectURL(url)
722
+ }
723
+
724
+ const event = new CustomEvent('download-click', {
725
+ detail: {
726
+ type,
727
+ markdown,
728
+ html: view.dom.innerHTML || '',
729
+ },
730
+ bubbles: true,
731
+ composed: true,
732
+ })
733
+ root.dispatchEvent(event)
734
+ }
735
+
736
+ function handleDirectImport(
737
+ type: 'markdown' | 'word' | 'pdf',
738
+ root: HTMLElement,
739
+ ctx: Ctx
740
+ ) {
741
+ if (type === 'markdown') {
742
+ const input = document.createElement('input')
743
+ input.type = 'file'
744
+ input.accept = '.md'
745
+ input.onchange = (evt) => {
746
+ const file = (evt.target as HTMLInputElement).files?.[0]
747
+ if (!file) return
748
+ file
749
+ .text()
750
+ .then((text) => {
751
+ replaceAll(text)(ctx)
752
+
753
+ const event = new CustomEvent('import-click', {
754
+ detail: { type: 'markdown', file },
755
+ bubbles: true,
756
+ composed: true,
757
+ })
758
+ root.dispatchEvent(event)
759
+ })
760
+ .catch((err) => {
761
+ console.error('Failed to read file:', err)
762
+ })
763
+ }
764
+ input.click()
765
+ } else {
766
+ const event = new CustomEvent('import-click', {
767
+ detail: { type },
768
+ bubbles: true,
769
+ composed: true,
770
+ })
771
+ root.dispatchEvent(event)
772
+ }
773
+ }
774
+
775
+ function showDownloadPopover(
776
+ rootNode: ShadowRoot | Document,
777
+ root: HTMLElement,
778
+ view: any,
779
+ markdown: string,
780
+ exportItems: ('markdown' | 'word' | 'pdf')[]
781
+ ) {
782
+ const button = root.querySelector('button[data-key="export"]') as HTMLElement
783
+ if (!button) return
784
+
785
+ const existing = activeDownloadPopovers.get(button)
786
+ if (existing) {
787
+ ;(existing as any).closePopover()
788
+ return
789
+ }
790
+
791
+ ensureStyles(rootNode)
792
+
793
+ const container = rootNode instanceof ShadowRoot ? rootNode : document.body
794
+ const popover = document.createElement('div')
795
+ popover.className = 'download-popover'
796
+
797
+ let popoverHtml = ''
798
+ if (exportItems.includes('markdown')) {
799
+ popoverHtml += `
800
+ <div class="download-popover-item" id="download-md-btn">
801
+ <span class="download-popover-item-icon">
802
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>
803
+ </span>
804
+ <span>下载 MD</span>
805
+ </div>
806
+ `
807
+ }
808
+ if (exportItems.includes('word')) {
809
+ popoverHtml += `
810
+ <div class="download-popover-item" id="download-word-btn">
811
+ <span class="download-popover-item-icon">
812
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
813
+ </span>
814
+ <span>下载 WORD</span>
815
+ </div>
816
+ `
817
+ }
818
+ if (exportItems.includes('pdf')) {
819
+ popoverHtml += `
820
+ <div class="download-popover-item" id="download-pdf-btn">
821
+ <span class="download-popover-item-icon">
822
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
823
+ </span>
824
+ <span>下载 PDF</span>
825
+ </div>
826
+ `
827
+ }
828
+ popover.innerHTML = popoverHtml
829
+
830
+ container.appendChild(popover)
831
+ activeDownloadPopovers.set(button, popover)
832
+
833
+ const updatePosition = () => {
834
+ const rect = button.getBoundingClientRect()
835
+ popover.style.top = `${rect.bottom + 4}px`
836
+ popover.style.left = `${rect.left + (rect.width - 140) / 2}px`
837
+ }
838
+
839
+ updatePosition()
840
+
841
+ requestAnimationFrame(() => {
842
+ popover.classList.add('show')
843
+ })
844
+
845
+ const closePopover = () => {
846
+ popover.classList.remove('show')
847
+ activeDownloadPopovers.delete(button)
848
+ container.removeEventListener('pointerdown', handleContainerClick, true)
849
+ document.removeEventListener('pointerdown', handleOuterClick, true)
850
+ window.removeEventListener('resize', updatePosition)
851
+ popover.addEventListener(
852
+ 'transitionend',
853
+ () => {
854
+ popover.remove()
855
+ },
856
+ { once: true }
857
+ )
858
+ }
859
+ ;(popover as any).closePopover = closePopover
860
+
861
+ const handleContainerClick = (e: Event) => {
862
+ const target = e.target as HTMLElement
863
+ if (
864
+ !popover.contains(target) &&
865
+ target !== button &&
866
+ !button.contains(target)
867
+ ) {
868
+ closePopover()
869
+ }
870
+ }
871
+
872
+ const handleOuterClick = (e: Event) => {
873
+ const target = e.target as HTMLElement
874
+ if (target !== root && !root.contains(target)) {
875
+ closePopover()
876
+ }
877
+ }
878
+
879
+ popover.addEventListener('pointerdown', (e) => {
880
+ e.stopPropagation()
881
+ })
882
+
883
+ container.addEventListener('pointerdown', handleContainerClick, true)
884
+ document.addEventListener('pointerdown', handleOuterClick, true)
885
+ window.addEventListener('resize', updatePosition)
886
+
887
+ popover.querySelector('#download-md-btn')?.addEventListener('click', (e) => {
888
+ e.stopPropagation()
889
+ const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8;' })
890
+ const url = URL.createObjectURL(blob)
891
+ const link = document.createElement('a')
892
+ link.href = url
893
+ link.download = 'document.md'
894
+ link.click()
895
+ URL.revokeObjectURL(url)
896
+ closePopover()
897
+
898
+ const event = new CustomEvent('download-click', {
899
+ detail: {
900
+ type: 'markdown',
901
+ markdown,
902
+ html: view.dom.innerHTML || '',
903
+ },
904
+ bubbles: true,
905
+ composed: true,
906
+ })
907
+ root.dispatchEvent(event)
908
+ })
909
+
910
+ popover
911
+ .querySelector('#download-word-btn')
912
+ ?.addEventListener('click', (e) => {
913
+ e.stopPropagation()
914
+ closePopover()
915
+ const event = new CustomEvent('download-click', {
916
+ detail: {
917
+ type: 'word',
918
+ markdown,
919
+ html: view.dom.innerHTML || '',
920
+ },
921
+ bubbles: true,
922
+ composed: true,
923
+ })
924
+ root.dispatchEvent(event)
925
+ })
926
+
927
+ popover.querySelector('#download-pdf-btn')?.addEventListener('click', (e) => {
928
+ e.stopPropagation()
929
+ closePopover()
930
+ const event = new CustomEvent('download-click', {
931
+ detail: {
932
+ type: 'pdf',
933
+ markdown,
934
+ html: view.dom.innerHTML || '',
935
+ },
936
+ bubbles: true,
937
+ composed: true,
938
+ })
939
+ root.dispatchEvent(event)
940
+ })
941
+ }
942
+
943
+ function showImportPopover(
944
+ rootNode: ShadowRoot | Document,
945
+ root: HTMLElement,
946
+ ctx: Ctx,
947
+ importItems: ('markdown' | 'word' | 'pdf')[]
948
+ ) {
949
+ const button = root.querySelector('button[data-key="import"]') as HTMLElement
950
+ if (!button) return
951
+
952
+ const existing = activeImportPopovers.get(button)
953
+ if (existing) {
954
+ ;(existing as any).closePopover()
955
+ return
956
+ }
957
+
958
+ ensureStyles(rootNode)
959
+
960
+ const container = rootNode instanceof ShadowRoot ? rootNode : document.body
961
+ const popover = document.createElement('div')
962
+ popover.className = 'import-popover'
963
+
964
+ let popoverHtml = ''
965
+ if (importItems.includes('markdown')) {
966
+ popoverHtml += `
967
+ <div class="import-popover-item" id="import-md-btn">
968
+ <span class="import-popover-item-icon">
969
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>
970
+ </span>
971
+ <span>导入 MD</span>
972
+ </div>
973
+ `
974
+ }
975
+ if (importItems.includes('word')) {
976
+ popoverHtml += `
977
+ <div class="import-popover-item" id="import-word-btn">
978
+ <span class="import-popover-item-icon">
979
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
980
+ </span>
981
+ <span>导入 WORD</span>
982
+ </div>
983
+ `
984
+ }
985
+ if (importItems.includes('pdf')) {
986
+ popoverHtml += `
987
+ <div class="import-popover-item" id="import-pdf-btn">
988
+ <span class="import-popover-item-icon">
989
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
990
+ </span>
991
+ <span>导入 PDF</span>
992
+ </div>
993
+ `
994
+ }
995
+ popover.innerHTML = popoverHtml
996
+
997
+ container.appendChild(popover)
998
+ activeImportPopovers.set(button, popover)
999
+
1000
+ const updatePosition = () => {
1001
+ const rect = button.getBoundingClientRect()
1002
+ popover.style.top = `${rect.bottom + 4}px`
1003
+ popover.style.left = `${rect.left + (rect.width - 140) / 2}px`
1004
+ }
1005
+
1006
+ updatePosition()
1007
+
1008
+ requestAnimationFrame(() => {
1009
+ popover.classList.add('show')
1010
+ })
1011
+
1012
+ const closePopover = () => {
1013
+ popover.classList.remove('show')
1014
+ activeImportPopovers.delete(button)
1015
+ container.removeEventListener('pointerdown', handleContainerClick, true)
1016
+ document.removeEventListener('pointerdown', handleOuterClick, true)
1017
+ window.removeEventListener('resize', updatePosition)
1018
+ popover.addEventListener(
1019
+ 'transitionend',
1020
+ () => {
1021
+ popover.remove()
1022
+ },
1023
+ { once: true }
1024
+ )
1025
+ }
1026
+ ;(popover as any).closePopover = closePopover
1027
+
1028
+ const handleContainerClick = (e: Event) => {
1029
+ const target = e.target as HTMLElement
1030
+ if (
1031
+ !popover.contains(target) &&
1032
+ target !== button &&
1033
+ !button.contains(target)
1034
+ ) {
1035
+ closePopover()
1036
+ }
1037
+ }
1038
+
1039
+ const handleOuterClick = (e: Event) => {
1040
+ const target = e.target as HTMLElement
1041
+ if (target !== root && !root.contains(target)) {
1042
+ closePopover()
1043
+ }
1044
+ }
1045
+
1046
+ popover.addEventListener('pointerdown', (e) => {
1047
+ e.stopPropagation()
1048
+ })
1049
+
1050
+ container.addEventListener('pointerdown', handleContainerClick, true)
1051
+ document.addEventListener('pointerdown', handleOuterClick, true)
1052
+ window.addEventListener('resize', updatePosition)
1053
+
1054
+ popover.querySelector('#import-md-btn')?.addEventListener('click', (e) => {
1055
+ e.stopPropagation()
1056
+ closePopover()
1057
+ const input = document.createElement('input')
1058
+ input.type = 'file'
1059
+ input.accept = '.md'
1060
+ input.onchange = (evt) => {
1061
+ const file = (evt.target as HTMLInputElement).files?.[0]
1062
+ if (!file) return
1063
+ file
1064
+ .text()
1065
+ .then((text) => {
1066
+ replaceAll(text)(ctx)
1067
+
1068
+ const event = new CustomEvent('import-click', {
1069
+ detail: { type: 'markdown', file },
1070
+ bubbles: true,
1071
+ composed: true,
1072
+ })
1073
+ root.dispatchEvent(event)
1074
+ })
1075
+ .catch((err) => {
1076
+ console.error('Failed to read file:', err)
1077
+ })
1078
+ }
1079
+ input.click()
1080
+ })
1081
+
1082
+ popover.querySelector('#import-word-btn')?.addEventListener('click', (e) => {
1083
+ e.stopPropagation()
1084
+ closePopover()
1085
+ const event = new CustomEvent('import-click', {
1086
+ detail: { type: 'word' },
1087
+ bubbles: true,
1088
+ composed: true,
1089
+ })
1090
+ root.dispatchEvent(event)
1091
+ })
1092
+
1093
+ popover.querySelector('#import-pdf-btn')?.addEventListener('click', (e) => {
1094
+ e.stopPropagation()
1095
+ closePopover()
1096
+ const event = new CustomEvent('import-click', {
1097
+ detail: { type: 'pdf' },
1098
+ bubbles: true,
1099
+ composed: true,
1100
+ })
1101
+ root.dispatchEvent(event)
1102
+ })
1103
+ }
@@ -60,8 +60,10 @@ export interface FixedToolbarConfig {
60
60
  showHistory?: boolean
61
61
  showExport?: boolean
62
62
  onExport?: (markdown: string, ctx: Ctx) => void
63
+ exportItems?: ('markdown' | 'word' | 'pdf')[]
63
64
  showImport?: boolean
64
65
  onImport?: (replaceContent: (markdown: string) => void, ctx: Ctx) => void
66
+ importItems?: ('markdown' | 'word' | 'pdf')[]
65
67
  useLocalStorage?: boolean
66
68
  id?: string
67
69
  outlineVisible?: boolean