@jvs-milkdown/components 1.0.0

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 (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/lib/__internal__/components/icon.d.ts +24 -0
  4. package/lib/__internal__/components/icon.d.ts.map +1 -0
  5. package/lib/__internal__/components/image-input.d.ts +17 -0
  6. package/lib/__internal__/components/image-input.d.ts.map +1 -0
  7. package/lib/__internal__/keep-alive.d.ts +2 -0
  8. package/lib/__internal__/keep-alive.d.ts.map +1 -0
  9. package/lib/__internal__/meta.d.ts +3 -0
  10. package/lib/__internal__/meta.d.ts.map +1 -0
  11. package/lib/__tests__/setup.d.ts +2 -0
  12. package/lib/__tests__/setup.d.ts.map +1 -0
  13. package/lib/code-block/config.d.ts +23 -0
  14. package/lib/code-block/config.d.ts.map +1 -0
  15. package/lib/code-block/index.d.ts +5 -0
  16. package/lib/code-block/index.d.ts.map +1 -0
  17. package/lib/code-block/index.js +4160 -0
  18. package/lib/code-block/index.js.map +1 -0
  19. package/lib/code-block/view/components/code-block.d.ts +16 -0
  20. package/lib/code-block/view/components/code-block.d.ts.map +1 -0
  21. package/lib/code-block/view/components/copy-button.d.ts +9 -0
  22. package/lib/code-block/view/components/copy-button.d.ts.map +1 -0
  23. package/lib/code-block/view/components/language-picker.d.ts +5 -0
  24. package/lib/code-block/view/components/language-picker.d.ts.map +1 -0
  25. package/lib/code-block/view/components/preview-panel.d.ts +9 -0
  26. package/lib/code-block/view/components/preview-panel.d.ts.map +1 -0
  27. package/lib/code-block/view/index.d.ts +3 -0
  28. package/lib/code-block/view/index.d.ts.map +1 -0
  29. package/lib/code-block/view/loader.d.ts +13 -0
  30. package/lib/code-block/view/loader.d.ts.map +1 -0
  31. package/lib/code-block/view/node-view.d.ts +40 -0
  32. package/lib/code-block/view/node-view.d.ts.map +1 -0
  33. package/lib/image-block/config.d.ts +16 -0
  34. package/lib/image-block/config.d.ts.map +1 -0
  35. package/lib/image-block/index.d.ts +7 -0
  36. package/lib/image-block/index.d.ts.map +1 -0
  37. package/lib/image-block/index.js +660 -0
  38. package/lib/image-block/index.js.map +1 -0
  39. package/lib/image-block/remark-plugin.d.ts +2 -0
  40. package/lib/image-block/remark-plugin.d.ts.map +1 -0
  41. package/lib/image-block/schema.d.ts +3 -0
  42. package/lib/image-block/schema.d.ts.map +1 -0
  43. package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts +2 -0
  44. package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts.map +1 -0
  45. package/lib/image-block/view/components/image-block.d.ts +18 -0
  46. package/lib/image-block/view/components/image-block.d.ts.map +1 -0
  47. package/lib/image-block/view/components/image-viewer.d.ts +3 -0
  48. package/lib/image-block/view/components/image-viewer.d.ts.map +1 -0
  49. package/lib/image-block/view/index.d.ts +3 -0
  50. package/lib/image-block/view/index.d.ts.map +1 -0
  51. package/lib/image-inline/components/image-inline.d.ts +18 -0
  52. package/lib/image-inline/components/image-inline.d.ts.map +1 -0
  53. package/lib/image-inline/config.d.ts +11 -0
  54. package/lib/image-inline/config.d.ts.map +1 -0
  55. package/lib/image-inline/index.d.ts +5 -0
  56. package/lib/image-inline/index.d.ts.map +1 -0
  57. package/lib/image-inline/index.js +377 -0
  58. package/lib/image-inline/index.js.map +1 -0
  59. package/lib/image-inline/view.d.ts +3 -0
  60. package/lib/image-inline/view.d.ts.map +1 -0
  61. package/lib/index.d.ts +2 -0
  62. package/lib/index.d.ts.map +1 -0
  63. package/lib/index.js +35 -0
  64. package/lib/index.js.map +1 -0
  65. package/lib/link-tooltip/command.d.ts +2 -0
  66. package/lib/link-tooltip/command.d.ts.map +1 -0
  67. package/lib/link-tooltip/configure.d.ts +3 -0
  68. package/lib/link-tooltip/configure.d.ts.map +1 -0
  69. package/lib/link-tooltip/edit/component.d.ts +11 -0
  70. package/lib/link-tooltip/edit/component.d.ts.map +1 -0
  71. package/lib/link-tooltip/edit/edit-configure.d.ts +3 -0
  72. package/lib/link-tooltip/edit/edit-configure.d.ts.map +1 -0
  73. package/lib/link-tooltip/edit/edit-view.d.ts +15 -0
  74. package/lib/link-tooltip/edit/edit-view.d.ts.map +1 -0
  75. package/lib/link-tooltip/index.d.ts +7 -0
  76. package/lib/link-tooltip/index.d.ts.map +1 -0
  77. package/lib/link-tooltip/index.js +2526 -0
  78. package/lib/link-tooltip/index.js.map +1 -0
  79. package/lib/link-tooltip/preview/component.d.ts +11 -0
  80. package/lib/link-tooltip/preview/component.d.ts.map +1 -0
  81. package/lib/link-tooltip/preview/preview-configure.d.ts +3 -0
  82. package/lib/link-tooltip/preview/preview-configure.d.ts.map +1 -0
  83. package/lib/link-tooltip/preview/preview-view.d.ts +14 -0
  84. package/lib/link-tooltip/preview/preview-view.d.ts.map +1 -0
  85. package/lib/link-tooltip/slices.d.ts +34 -0
  86. package/lib/link-tooltip/slices.d.ts.map +1 -0
  87. package/lib/link-tooltip/tooltips.d.ts +3 -0
  88. package/lib/link-tooltip/tooltips.d.ts.map +1 -0
  89. package/lib/link-tooltip/utils.d.ts +14 -0
  90. package/lib/link-tooltip/utils.d.ts.map +1 -0
  91. package/lib/list-item-block/component.d.ts +19 -0
  92. package/lib/list-item-block/component.d.ts.map +1 -0
  93. package/lib/list-item-block/config.d.ts +13 -0
  94. package/lib/list-item-block/config.d.ts.map +1 -0
  95. package/lib/list-item-block/index.d.ts +6 -0
  96. package/lib/list-item-block/index.d.ts.map +1 -0
  97. package/lib/list-item-block/index.js +2149 -0
  98. package/lib/list-item-block/index.js.map +1 -0
  99. package/lib/list-item-block/view.d.ts +3 -0
  100. package/lib/list-item-block/view.d.ts.map +1 -0
  101. package/lib/table-block/config.d.ts +8 -0
  102. package/lib/table-block/config.d.ts.map +1 -0
  103. package/lib/table-block/dnd/calc-drag-over.d.ts +3 -0
  104. package/lib/table-block/dnd/calc-drag-over.d.ts.map +1 -0
  105. package/lib/table-block/dnd/create-drag-handler.d.ts +5 -0
  106. package/lib/table-block/dnd/create-drag-handler.d.ts.map +1 -0
  107. package/lib/table-block/dnd/drag-over-handler.d.ts +3 -0
  108. package/lib/table-block/dnd/drag-over-handler.d.ts.map +1 -0
  109. package/lib/table-block/dnd/prepare-dnd-context.d.ts +3 -0
  110. package/lib/table-block/dnd/prepare-dnd-context.d.ts.map +1 -0
  111. package/lib/table-block/dnd/preview.d.ts +3 -0
  112. package/lib/table-block/dnd/preview.d.ts.map +1 -0
  113. package/lib/table-block/index.d.ts +5 -0
  114. package/lib/table-block/index.d.ts.map +1 -0
  115. package/lib/table-block/index.js +3961 -0
  116. package/lib/table-block/index.js.map +1 -0
  117. package/lib/table-block/view/component.d.ts +16 -0
  118. package/lib/table-block/view/component.d.ts.map +1 -0
  119. package/lib/table-block/view/drag.d.ts +7 -0
  120. package/lib/table-block/view/drag.d.ts.map +1 -0
  121. package/lib/table-block/view/index.d.ts +2 -0
  122. package/lib/table-block/view/index.d.ts.map +1 -0
  123. package/lib/table-block/view/operation.d.ts +11 -0
  124. package/lib/table-block/view/operation.d.ts.map +1 -0
  125. package/lib/table-block/view/pointer.d.ts +7 -0
  126. package/lib/table-block/view/pointer.d.ts.map +1 -0
  127. package/lib/table-block/view/types.d.ts +32 -0
  128. package/lib/table-block/view/types.d.ts.map +1 -0
  129. package/lib/table-block/view/utils.d.ts +21 -0
  130. package/lib/table-block/view/utils.d.ts.map +1 -0
  131. package/lib/table-block/view/view.d.ts +22 -0
  132. package/lib/table-block/view/view.d.ts.map +1 -0
  133. package/lib/tsconfig.tsbuildinfo +1 -0
  134. package/package.json +110 -0
  135. package/src/__internal__/components/icon.tsx +38 -0
  136. package/src/__internal__/components/image-input.tsx +182 -0
  137. package/src/__internal__/keep-alive.ts +3 -0
  138. package/src/__internal__/meta.ts +15 -0
  139. package/src/__tests__/setup.ts +6 -0
  140. package/src/code-block/config.ts +54 -0
  141. package/src/code-block/index.ts +12 -0
  142. package/src/code-block/view/components/code-block.tsx +170 -0
  143. package/src/code-block/view/components/copy-button.tsx +96 -0
  144. package/src/code-block/view/components/language-picker.tsx +239 -0
  145. package/src/code-block/view/components/preview-panel.tsx +79 -0
  146. package/src/code-block/view/index.ts +24 -0
  147. package/src/code-block/view/loader.ts +40 -0
  148. package/src/code-block/view/node-view.ts +310 -0
  149. package/src/image-block/config.ts +37 -0
  150. package/src/image-block/index.ts +18 -0
  151. package/src/image-block/remark-plugin.ts +51 -0
  152. package/src/image-block/schema.ts +71 -0
  153. package/src/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.tsx +42 -0
  154. package/src/image-block/view/components/image-block.tsx +80 -0
  155. package/src/image-block/view/components/image-viewer.tsx +186 -0
  156. package/src/image-block/view/index.ts +111 -0
  157. package/src/image-inline/components/image-inline.tsx +85 -0
  158. package/src/image-inline/config.ts +30 -0
  159. package/src/image-inline/index.ts +12 -0
  160. package/src/image-inline/view.ts +109 -0
  161. package/src/index.ts +1 -0
  162. package/src/link-tooltip/command.ts +19 -0
  163. package/src/link-tooltip/configure.ts +9 -0
  164. package/src/link-tooltip/edit/component.tsx +82 -0
  165. package/src/link-tooltip/edit/edit-configure.ts +29 -0
  166. package/src/link-tooltip/edit/edit-view.ts +165 -0
  167. package/src/link-tooltip/index.ts +19 -0
  168. package/src/link-tooltip/preview/component.tsx +87 -0
  169. package/src/link-tooltip/preview/preview-configure.ts +65 -0
  170. package/src/link-tooltip/preview/preview-view.ts +101 -0
  171. package/src/link-tooltip/slices.ts +69 -0
  172. package/src/link-tooltip/tooltips.ts +22 -0
  173. package/src/link-tooltip/utils.ts +56 -0
  174. package/src/list-item-block/component.tsx +133 -0
  175. package/src/list-item-block/config.ts +39 -0
  176. package/src/list-item-block/index.ts +13 -0
  177. package/src/list-item-block/view.ts +130 -0
  178. package/src/table-block/config.ts +53 -0
  179. package/src/table-block/dnd/calc-drag-over.ts +46 -0
  180. package/src/table-block/dnd/create-drag-handler.ts +99 -0
  181. package/src/table-block/dnd/drag-over-handler.ts +113 -0
  182. package/src/table-block/dnd/prepare-dnd-context.ts +46 -0
  183. package/src/table-block/dnd/preview.ts +58 -0
  184. package/src/table-block/index.ts +9 -0
  185. package/src/table-block/view/component.tsx +219 -0
  186. package/src/table-block/view/drag.ts +121 -0
  187. package/src/table-block/view/index.ts +1 -0
  188. package/src/table-block/view/operation.ts +148 -0
  189. package/src/table-block/view/pointer.ts +165 -0
  190. package/src/table-block/view/types.ts +35 -0
  191. package/src/table-block/view/utils.ts +192 -0
  192. package/src/table-block/view/view.ts +165 -0
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "@jvs-milkdown/components",
3
+ "version": "1.0.0",
4
+ "keywords": [
5
+ "milkdown",
6
+ "milkdown plugin"
7
+ ],
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Milkdown/milkdown.git",
12
+ "directory": "packages/components"
13
+ },
14
+ "files": [
15
+ "lib",
16
+ "src"
17
+ ],
18
+ "type": "module",
19
+ "sideEffects": false,
20
+ "main": "./lib/index.js",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/index.d.ts",
24
+ "import": "./lib/index.js"
25
+ },
26
+ "./image-block": {
27
+ "types": "./lib/image-block/index.d.ts",
28
+ "import": "./lib/image-block/index.js"
29
+ },
30
+ "./code-block": {
31
+ "types": "./lib/code-block/index.d.ts",
32
+ "import": "./lib/code-block/index.js"
33
+ },
34
+ "./list-item-block": {
35
+ "types": "./lib/list-item-block/index.d.ts",
36
+ "import": "./lib/list-item-block/index.js"
37
+ },
38
+ "./link-tooltip": {
39
+ "types": "./lib/link-tooltip/index.d.ts",
40
+ "import": "./lib/link-tooltip/index.js"
41
+ },
42
+ "./image-inline": {
43
+ "types": "./lib/image-inline/index.d.ts",
44
+ "import": "./lib/image-inline/index.js"
45
+ },
46
+ "./table-block": {
47
+ "types": "./lib/table-block/index.d.ts",
48
+ "import": "./lib/table-block/index.js"
49
+ }
50
+ },
51
+ "dependencies": {
52
+ "@floating-ui/dom": "^1.5.1",
53
+ "@types/lodash-es": "^4.17.12",
54
+ "clsx": "^2.0.0",
55
+ "dompurify": "^3.2.5",
56
+ "lodash-es": "^4.17.21",
57
+ "nanoid": "^5.0.9",
58
+ "unist-util-visit": "^5.0.0",
59
+ "vue": "^3.5.20",
60
+ "@jvs-milkdown/core": "1.0.0",
61
+ "@jvs-milkdown/exception": "1.0.0",
62
+ "@jvs-milkdown/ctx": "1.0.0",
63
+ "@jvs-milkdown/preset-commonmark": "1.0.0",
64
+ "@jvs-milkdown/preset-gfm": "1.0.0",
65
+ "@jvs-milkdown/transformer": "1.0.0",
66
+ "@jvs-milkdown/plugin-tooltip": "1.0.0",
67
+ "@jvs-milkdown/utils": "1.0.0",
68
+ "@jvs-milkdown/prose": "1.0.0"
69
+ },
70
+ "devDependencies": {
71
+ "@codemirror/language": "^6.10.1",
72
+ "@codemirror/state": "^6.4.1",
73
+ "@codemirror/view": "^6.26.0",
74
+ "@testing-library/vue": "^8.1.0",
75
+ "jsdom": "^29.0.0",
76
+ "vitest": "^4.0.0"
77
+ },
78
+ "peerDependencies": {
79
+ "@codemirror/language": "^6",
80
+ "@codemirror/state": "^6",
81
+ "@codemirror/view": "^6"
82
+ },
83
+ "scripts": {
84
+ "build": "rollup -c",
85
+ "test": "vitest run"
86
+ },
87
+ "types": "./lib/index.d.ts",
88
+ "typesVersions": {
89
+ "*": {
90
+ "image-block": [
91
+ "./lib/image-block/index.d.ts"
92
+ ],
93
+ "code-block": [
94
+ "./lib/code-block/index.d.ts"
95
+ ],
96
+ "list-item-block": [
97
+ "./lib/list-item-block/index.d.ts"
98
+ ],
99
+ "link-tooltip": [
100
+ "./lib/link-tooltip/index.d.ts"
101
+ ],
102
+ "image-inline": [
103
+ "./lib/image-inline/index.d.ts"
104
+ ],
105
+ "table-block": [
106
+ "./lib/table-block/index.d.ts"
107
+ ]
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,38 @@
1
+ import clsx from 'clsx'
2
+ import DOMPurify from 'dompurify'
3
+ import { h } from 'vue'
4
+
5
+ import { keepAlive } from '../keep-alive'
6
+
7
+ keepAlive(h)
8
+
9
+ type IconProps = {
10
+ icon?: string | null
11
+ class?: string
12
+ onClick?: (event: PointerEvent) => void
13
+ }
14
+
15
+ export function Icon({ icon, class: className, onClick }: IconProps) {
16
+ return (
17
+ <span
18
+ class={clsx('milkdown-icon', className)}
19
+ onPointerdown={onClick}
20
+ innerHTML={icon ? DOMPurify.sanitize(icon.trim()) : undefined}
21
+ />
22
+ )
23
+ }
24
+
25
+ Icon.props = {
26
+ icon: {
27
+ type: String,
28
+ required: false,
29
+ },
30
+ class: {
31
+ type: String,
32
+ required: false,
33
+ },
34
+ onClick: {
35
+ type: Function,
36
+ required: false,
37
+ },
38
+ }
@@ -0,0 +1,182 @@
1
+ import clsx from 'clsx'
2
+ import { customAlphabet } from 'nanoid'
3
+ import { defineComponent, ref, h, type Ref } from 'vue'
4
+
5
+ import { keepAlive } from '../keep-alive'
6
+ import { Icon } from './icon'
7
+
8
+ keepAlive(h)
9
+
10
+ const nanoid = customAlphabet('abcdefg', 8)
11
+
12
+ type ImageInputProps = {
13
+ src: Ref<string | undefined>
14
+ selected: Ref<boolean>
15
+ readonly: Ref<boolean>
16
+ setLink: (link: string) => void
17
+
18
+ imageIcon?: string
19
+ uploadButton?: string
20
+ confirmButton?: string
21
+ uploadPlaceholderText?: string
22
+
23
+ className?: string
24
+
25
+ onUpload: (file: File) => Promise<string>
26
+ onImageLoadError?: (event: Event) => void | Promise<void>
27
+ }
28
+
29
+ export const ImageInput = defineComponent<ImageInputProps>({
30
+ props: {
31
+ src: {
32
+ type: Object,
33
+ required: true,
34
+ },
35
+ selected: {
36
+ type: Object,
37
+ required: true,
38
+ },
39
+ readonly: {
40
+ type: Object,
41
+ required: true,
42
+ },
43
+ setLink: {
44
+ type: Function,
45
+ required: true,
46
+ },
47
+ imageIcon: {
48
+ type: String,
49
+ required: false,
50
+ },
51
+ uploadButton: {
52
+ type: String,
53
+ required: false,
54
+ },
55
+ confirmButton: {
56
+ type: String,
57
+ required: false,
58
+ },
59
+ uploadPlaceholderText: {
60
+ type: String,
61
+ required: false,
62
+ },
63
+ onUpload: {
64
+ type: Function,
65
+ required: true,
66
+ },
67
+ onImageLoadError: {
68
+ type: Function,
69
+ required: false,
70
+ },
71
+ },
72
+ setup({
73
+ readonly,
74
+ src,
75
+ setLink,
76
+ onUpload,
77
+ imageIcon,
78
+ uploadButton,
79
+ confirmButton,
80
+ uploadPlaceholderText,
81
+ className,
82
+ onImageLoadError,
83
+ }) {
84
+ const focusLinkInput = ref(false)
85
+ const linkInputRef = ref<HTMLInputElement>()
86
+ const currentLink = ref(src.value ?? '')
87
+ const uuid = ref(nanoid())
88
+ const hidePlaceholder = ref(src.value?.length !== 0)
89
+ const onEditLink = (e: Event) => {
90
+ const target = e.target as HTMLInputElement
91
+ const value = target.value
92
+ hidePlaceholder.value = value.length !== 0
93
+ currentLink.value = value
94
+ }
95
+
96
+ const onKeydown = (e: KeyboardEvent) => {
97
+ if (e.key === 'Enter') {
98
+ setLink(linkInputRef.value?.value ?? '')
99
+ }
100
+ }
101
+
102
+ const onConfirmLinkInput = () => {
103
+ setLink(linkInputRef.value?.value ?? '')
104
+ }
105
+
106
+ const onUploadFile = (e: Event) => {
107
+ const file = (e.target as HTMLInputElement).files?.[0]
108
+ if (!file) return
109
+
110
+ onUpload(file)
111
+ .then((url) => {
112
+ if (!url) return
113
+
114
+ setLink(url)
115
+ hidePlaceholder.value = true
116
+ })
117
+ .catch((err) => {
118
+ console.error('An error occurred while uploading image')
119
+ console.error(err)
120
+ })
121
+ }
122
+
123
+ return () => {
124
+ return (
125
+ <div class={clsx('image-edit', className)}>
126
+ <Icon icon={imageIcon} class="image-icon" />
127
+ <div class={clsx('link-importer', focusLinkInput.value && 'focus')}>
128
+ <input
129
+ ref={linkInputRef}
130
+ draggable="true"
131
+ onDragstart={(e) => {
132
+ e.preventDefault()
133
+ e.stopPropagation()
134
+ }}
135
+ disabled={readonly.value}
136
+ class="link-input-area"
137
+ value={currentLink.value}
138
+ onInput={onEditLink}
139
+ onKeydown={onKeydown}
140
+ onFocus={() => (focusLinkInput.value = true)}
141
+ onBlur={() => (focusLinkInput.value = false)}
142
+ />
143
+ {!hidePlaceholder.value && (
144
+ <div class="placeholder">
145
+ <input
146
+ disabled={readonly.value}
147
+ class="hidden"
148
+ id={uuid.value}
149
+ type="file"
150
+ accept="image/*"
151
+ onChange={onUploadFile}
152
+ />
153
+ <label class="uploader" for={uuid.value}>
154
+ <Icon icon={uploadButton} />
155
+ </label>
156
+ <span class="text" onClick={() => linkInputRef.value?.focus()}>
157
+ {uploadPlaceholderText}
158
+ </span>
159
+ </div>
160
+ )}
161
+ </div>
162
+ {currentLink.value && (
163
+ <>
164
+ <div class="image-preview">
165
+ <img
166
+ src={currentLink.value}
167
+ alt=""
168
+ onError={(e) =>
169
+ Promise.resolve(onImageLoadError?.(e)).catch(() => {})
170
+ }
171
+ />
172
+ </div>
173
+ <div class="confirm" onClick={() => onConfirmLinkInput()}>
174
+ <Icon icon={confirmButton} />
175
+ </div>
176
+ </>
177
+ )}
178
+ </div>
179
+ )
180
+ }
181
+ },
182
+ })
@@ -0,0 +1,3 @@
1
+ // Prevent tree-shaking from removing Vue's `h` and `Fragment`,
2
+ // which are required at runtime for TSX to work.
3
+ export function keepAlive(..._args: unknown[]) {}
@@ -0,0 +1,15 @@
1
+ import type { Meta, MilkdownPlugin } from '@jvs-milkdown/ctx'
2
+
3
+ export function withMeta<T extends MilkdownPlugin>(
4
+ plugin: T,
5
+ meta: Partial<Meta> & Pick<Meta, 'displayName'>
6
+ ): T {
7
+ Object.assign(plugin, {
8
+ meta: {
9
+ package: '@jvs-milkdown/components',
10
+ ...meta,
11
+ },
12
+ })
13
+
14
+ return plugin
15
+ }
@@ -0,0 +1,6 @@
1
+ import { cleanup } from '@testing-library/vue'
2
+ import { afterEach } from 'vitest'
3
+
4
+ afterEach(() => {
5
+ cleanup()
6
+ })
@@ -0,0 +1,54 @@
1
+ import type { LanguageDescription } from '@codemirror/language'
2
+ import type { Extension } from '@codemirror/state'
3
+
4
+ import { $ctx } from '@jvs-milkdown/utils'
5
+
6
+ import { withMeta } from '../__internal__/meta'
7
+
8
+ export interface CodeBlockConfig {
9
+ extensions: Extension[]
10
+ languages: LanguageDescription[]
11
+ expandIcon: string
12
+ searchIcon: string
13
+ clearSearchIcon: string
14
+ searchPlaceholder: string
15
+ noResultText: string
16
+ copyText: string
17
+ copyIcon: string
18
+ onCopy?: (text: string) => void
19
+ renderLanguage: (language: string, selected: boolean) => string
20
+ renderPreview: (
21
+ language: string,
22
+ content: string,
23
+ applyPreview: (value: null | string | HTMLElement) => void
24
+ ) => void | null | string | HTMLElement
25
+ previewToggleButton: (previewOnlyMode: boolean) => string
26
+ previewLabel: string
27
+ previewOnlyByDefault?: boolean
28
+ previewLoading: string | HTMLElement
29
+ }
30
+
31
+ export const defaultConfig: CodeBlockConfig = {
32
+ extensions: [],
33
+ languages: [],
34
+ expandIcon: '⬇',
35
+ searchIcon: '🔍',
36
+ clearSearchIcon: '⌫',
37
+ searchPlaceholder: 'Search language',
38
+ noResultText: 'No result',
39
+ copyText: 'Copy',
40
+ copyIcon: '📋',
41
+ onCopy: () => {},
42
+ renderLanguage: (language) => language,
43
+ renderPreview: () => null,
44
+ previewToggleButton: (previewOnlyMode) => (previewOnlyMode ? 'Edit' : 'Hide'),
45
+ previewLabel: 'Preview',
46
+ previewLoading: 'Loading...',
47
+ }
48
+
49
+ export const codeBlockConfig = $ctx(defaultConfig, 'codeBlockConfigCtx')
50
+
51
+ withMeta(codeBlockConfig, {
52
+ displayName: 'Config<code-block>',
53
+ group: 'CodeBlock',
54
+ })
@@ -0,0 +1,12 @@
1
+ import type { MilkdownPlugin } from '@jvs-milkdown/ctx'
2
+
3
+ import { codeBlockConfig } from './config'
4
+ import { codeBlockView } from './view'
5
+
6
+ export * from './config'
7
+ export * from './view'
8
+
9
+ export const codeBlockComponent: MilkdownPlugin[] = [
10
+ codeBlockView,
11
+ codeBlockConfig,
12
+ ]
@@ -0,0 +1,170 @@
1
+ import type { EditorView as CodeMirror } from '@codemirror/view'
2
+
3
+ import clsx from 'clsx'
4
+ import DOMPurify from 'dompurify'
5
+ import {
6
+ defineComponent,
7
+ ref,
8
+ type Ref,
9
+ h,
10
+ Fragment,
11
+ onMounted,
12
+ watch,
13
+ } from 'vue'
14
+
15
+ import type { CodeBlockConfig } from '../../config'
16
+ import type { LanguageInfo } from '../loader'
17
+
18
+ import { Icon } from '../../../__internal__/components/icon'
19
+ import { keepAlive } from '../../../__internal__/keep-alive'
20
+ import { CopyButton } from './copy-button'
21
+ import { LanguagePicker } from './language-picker'
22
+ import { PreviewPanel } from './preview-panel'
23
+
24
+ keepAlive(h, Fragment)
25
+
26
+ export type CodeBlockProps = {
27
+ text: Ref<string>
28
+ selected: Ref<boolean>
29
+ getReadOnly: () => boolean
30
+ codemirror: CodeMirror
31
+ language: Ref<string>
32
+ getAllLanguages: () => Array<LanguageInfo>
33
+ setLanguage: (language: string) => void
34
+ config: Omit<CodeBlockConfig, 'languages' | 'extensions'>
35
+ }
36
+
37
+ export const CodeBlock = defineComponent<CodeBlockProps>({
38
+ props: {
39
+ text: {
40
+ type: Object,
41
+ required: true,
42
+ },
43
+ selected: {
44
+ type: Object,
45
+ required: true,
46
+ },
47
+ getReadOnly: {
48
+ type: Function,
49
+ required: true,
50
+ },
51
+ codemirror: {
52
+ type: Object,
53
+ required: true,
54
+ },
55
+ language: {
56
+ type: Object,
57
+ required: true,
58
+ },
59
+ getAllLanguages: {
60
+ type: Function,
61
+ required: true,
62
+ },
63
+ setLanguage: {
64
+ type: Function,
65
+ required: true,
66
+ },
67
+ config: {
68
+ type: Object,
69
+ required: true,
70
+ },
71
+ },
72
+ setup(props) {
73
+ const previewOnlyByDefault =
74
+ props.config.previewOnlyByDefault ?? props.getReadOnly()
75
+ const previewOnlyMode = ref(previewOnlyByDefault)
76
+ const codemirrorHostRef = ref<HTMLDivElement>()
77
+ const preview = ref<null | string | HTMLElement>(null)
78
+
79
+ onMounted(() => {
80
+ while (codemirrorHostRef.value?.firstChild) {
81
+ codemirrorHostRef.value.removeChild(codemirrorHostRef.value.firstChild)
82
+ }
83
+
84
+ if (codemirrorHostRef.value) {
85
+ codemirrorHostRef.value.appendChild(props.codemirror.dom)
86
+ }
87
+ })
88
+
89
+ watch(
90
+ () => [props.text.value, props.language.value],
91
+ () => {
92
+ const result = props.config.renderPreview(
93
+ props.language.value,
94
+ props.text.value,
95
+ (value) => (preview.value = value)
96
+ )
97
+ if (result) {
98
+ preview.value = result
99
+ }
100
+
101
+ // set default value for async renderPreview
102
+ const isAsyncPreview = result === undefined
103
+ if (isAsyncPreview && !preview.value) {
104
+ preview.value = DOMPurify.sanitize(props.config.previewLoading)
105
+ }
106
+
107
+ if (result === null) {
108
+ preview.value = null
109
+ }
110
+ },
111
+ { immediate: true }
112
+ )
113
+
114
+ const empty = () => {}
115
+
116
+ return () => {
117
+ return (
118
+ <>
119
+ <div class="tools">
120
+ <LanguagePicker
121
+ language={props.language}
122
+ config={props.config}
123
+ setLanguage={props.setLanguage}
124
+ getAllLanguages={props.getAllLanguages}
125
+ getReadOnly={props.getReadOnly}
126
+ />
127
+
128
+ <div class="tools-button-group">
129
+ <CopyButton
130
+ copyIcon={props.config.copyIcon}
131
+ copyText={props.config.copyText}
132
+ onCopy={props.config.onCopy ?? empty}
133
+ text={props.text.value}
134
+ />
135
+
136
+ {preview.value ? (
137
+ <button
138
+ class="preview-toggle-button"
139
+ onClick={() =>
140
+ (previewOnlyMode.value = !previewOnlyMode.value)
141
+ }
142
+ >
143
+ <Icon
144
+ icon={props.config.previewToggleButton(
145
+ previewOnlyMode.value
146
+ )}
147
+ />
148
+ </button>
149
+ ) : null}
150
+ </div>
151
+ </div>
152
+ <div
153
+ ref={codemirrorHostRef}
154
+ class={clsx(
155
+ 'codemirror-host',
156
+ preview.value && previewOnlyMode.value && 'hidden'
157
+ )}
158
+ />
159
+ <PreviewPanel
160
+ text={props.text}
161
+ language={props.language}
162
+ config={props.config}
163
+ previewOnlyMode={previewOnlyMode}
164
+ preview={preview}
165
+ />
166
+ </>
167
+ )
168
+ }
169
+ },
170
+ })
@@ -0,0 +1,96 @@
1
+ import { defineComponent, h, Fragment } from 'vue'
2
+
3
+ import { Icon } from '../../../__internal__/components/icon'
4
+ import { keepAlive } from '../../../__internal__/keep-alive'
5
+
6
+ keepAlive(h, Fragment)
7
+
8
+ type CopyButtonProps = {
9
+ copyText: string
10
+ copyIcon: string
11
+ onCopy: (text: string) => void
12
+ text: string
13
+ }
14
+
15
+ async function copyToClipboard(text: string) {
16
+ try {
17
+ return navigator.clipboard.writeText(text)
18
+ } catch {
19
+ const element = document.createElement('textarea')
20
+ const previouslyFocusedElement = document.activeElement
21
+
22
+ element.value = text
23
+
24
+ // Prevent keyboard from showing on mobile
25
+ element.setAttribute('readonly', '')
26
+
27
+ element.style.contain = 'strict'
28
+ element.style.position = 'absolute'
29
+ element.style.left = '-9999px'
30
+ element.style.fontSize = '12pt' // Prevent zooming on iOS
31
+
32
+ const selection = document.getSelection()
33
+ const originalRange = selection
34
+ ? selection.rangeCount > 0 && selection.getRangeAt(0)
35
+ : null
36
+
37
+ document.body.appendChild(element)
38
+ element.select()
39
+
40
+ // Explicit selection workaround for iOS
41
+ element.selectionStart = 0
42
+ element.selectionEnd = text.length
43
+
44
+ document.execCommand('copy')
45
+ document.body.removeChild(element)
46
+
47
+ if (originalRange) {
48
+ selection!.removeAllRanges() // originalRange can't be truthy when selection is falsy
49
+ selection!.addRange(originalRange)
50
+ }
51
+
52
+ // Get the focus back on the previously focused element, if any
53
+ if (previouslyFocusedElement) {
54
+ ;(previouslyFocusedElement as HTMLElement).focus()
55
+ }
56
+ }
57
+ }
58
+
59
+ export const CopyButton = defineComponent<CopyButtonProps>({
60
+ props: {
61
+ copyText: {
62
+ type: String,
63
+ required: true,
64
+ },
65
+ copyIcon: {
66
+ type: String,
67
+ required: true,
68
+ },
69
+ onCopy: {
70
+ type: Function,
71
+ required: true,
72
+ },
73
+ text: {
74
+ type: String,
75
+ required: true,
76
+ },
77
+ },
78
+ setup(props) {
79
+ const onCopyCode = () => {
80
+ copyToClipboard(props.text)
81
+ .then(() => props.onCopy(props.text))
82
+ .catch(console.error)
83
+ }
84
+
85
+ return () => {
86
+ return (
87
+ <>
88
+ <button type="button" class="copy-button" onClick={onCopyCode}>
89
+ <Icon icon={props.copyIcon} />
90
+ {props.copyText}
91
+ </button>
92
+ </>
93
+ )
94
+ }
95
+ },
96
+ })