@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
@@ -0,0 +1,37 @@
1
+ import { $ctx } from '@jvs-milkdown/utils'
2
+
3
+ import { withMeta } from '../__internal__/meta'
4
+
5
+ export interface ImageBlockConfig {
6
+ imageIcon: string | undefined
7
+ captionIcon: string | undefined
8
+ uploadButton: string | undefined
9
+ confirmButton: string | undefined
10
+ uploadPlaceholderText: string
11
+ captionPlaceholderText: string
12
+ onUpload: (file: File) => Promise<string>
13
+ proxyDomURL?: (url: string) => Promise<string> | string
14
+ onImageLoadError?: (event: Event) => void | Promise<void>
15
+ maxWidth?: number
16
+ maxHeight?: number
17
+ }
18
+
19
+ export const defaultImageBlockConfig: ImageBlockConfig = {
20
+ imageIcon: '🌌',
21
+ captionIcon: '💬',
22
+ uploadButton: 'Upload file',
23
+ confirmButton: 'Confirm ⏎',
24
+ uploadPlaceholderText: 'or paste the image link ...',
25
+ captionPlaceholderText: 'Image caption',
26
+ onUpload: (file) => Promise.resolve(URL.createObjectURL(file)),
27
+ }
28
+
29
+ export const imageBlockConfig = $ctx(
30
+ defaultImageBlockConfig,
31
+ 'imageBlockConfigCtx'
32
+ )
33
+
34
+ withMeta(imageBlockConfig, {
35
+ displayName: 'Config<image-block>',
36
+ group: 'ImageBlock',
37
+ })
@@ -0,0 +1,18 @@
1
+ import type { MilkdownPlugin } from '@jvs-milkdown/ctx'
2
+
3
+ import { imageBlockConfig } from './config'
4
+ import { remarkImageBlockPlugin } from './remark-plugin'
5
+ import { imageBlockSchema } from './schema'
6
+ import { imageBlockView } from './view'
7
+
8
+ export * from './schema'
9
+ export * from './remark-plugin'
10
+ export * from './config'
11
+ export * from './view'
12
+
13
+ export const imageBlockComponent: MilkdownPlugin[] = [
14
+ remarkImageBlockPlugin,
15
+ imageBlockSchema,
16
+ imageBlockView,
17
+ imageBlockConfig,
18
+ ].flat()
@@ -0,0 +1,51 @@
1
+ import type { Node } from '@jvs-milkdown/transformer'
2
+
3
+ import { $remark } from '@jvs-milkdown/utils'
4
+ import { visit } from 'unist-util-visit'
5
+
6
+ import { withMeta } from '../__internal__/meta'
7
+
8
+ function visitImage(ast: Node) {
9
+ return visit(
10
+ ast,
11
+ 'paragraph',
12
+ (
13
+ node: Node & { children?: Node[] },
14
+ index: number,
15
+ parent: Node & { children: Node[] }
16
+ ) => {
17
+ if (node.children?.length !== 1) return
18
+ const firstChild = node.children?.[0]
19
+ if (!firstChild || firstChild.type !== 'image') return
20
+
21
+ const { url, alt, title } = firstChild as Node & {
22
+ url: string
23
+ alt: string
24
+ title: string
25
+ }
26
+ const newNode = {
27
+ type: 'image-block',
28
+ url,
29
+ alt,
30
+ title,
31
+ }
32
+
33
+ parent.children.splice(index, 1, newNode)
34
+ }
35
+ )
36
+ }
37
+
38
+ export const remarkImageBlockPlugin = $remark(
39
+ 'remark-image-block',
40
+ () => () => visitImage
41
+ )
42
+
43
+ withMeta(remarkImageBlockPlugin.plugin, {
44
+ displayName: 'Remark<remarkImageBlock>',
45
+ group: 'ImageBlock',
46
+ })
47
+
48
+ withMeta(remarkImageBlockPlugin.options, {
49
+ displayName: 'RemarkConfig<remarkImageBlock>',
50
+ group: 'ImageBlock',
51
+ })
@@ -0,0 +1,71 @@
1
+ import { expectDomTypeError } from '@jvs-milkdown/exception'
2
+ import { $nodeSchema } from '@jvs-milkdown/utils'
3
+
4
+ import { withMeta } from '../__internal__/meta'
5
+
6
+ export const IMAGE_DATA_TYPE = 'image-block'
7
+
8
+ export const imageBlockSchema = $nodeSchema('image-block', () => {
9
+ return {
10
+ inline: false,
11
+ group: 'block',
12
+ selectable: true,
13
+ draggable: true,
14
+ isolating: true,
15
+ marks: '',
16
+ atom: true,
17
+ priority: 100,
18
+ attrs: {
19
+ src: { default: '', validate: 'string' },
20
+ caption: { default: '', validate: 'string' },
21
+ ratio: { default: 1, validate: 'number' },
22
+ },
23
+ parseDOM: [
24
+ {
25
+ tag: `img[data-type="${IMAGE_DATA_TYPE}"]`,
26
+ getAttrs: (dom) => {
27
+ if (!(dom instanceof HTMLElement)) throw expectDomTypeError(dom)
28
+
29
+ return {
30
+ src: dom.getAttribute('src') || '',
31
+ caption: dom.getAttribute('caption') || '',
32
+ ratio: Number(dom.getAttribute('ratio') ?? 1),
33
+ }
34
+ },
35
+ },
36
+ ],
37
+ toDOM: (node) => ['img', { 'data-type': IMAGE_DATA_TYPE, ...node.attrs }],
38
+ parseMarkdown: {
39
+ match: ({ type }) => type === 'image-block',
40
+ runner: (state, node, type) => {
41
+ const src = node.url as string
42
+ const caption = node.title as string
43
+ let ratio = Number((node.alt as string) || 1)
44
+ if (Number.isNaN(ratio) || ratio === 0) ratio = 1
45
+
46
+ state.addNode(type, {
47
+ src,
48
+ caption,
49
+ ratio,
50
+ })
51
+ },
52
+ },
53
+ toMarkdown: {
54
+ match: (node) => node.type.name === 'image-block',
55
+ runner: (state, node) => {
56
+ state.openNode('paragraph')
57
+ state.addNode('image', undefined, undefined, {
58
+ title: node.attrs.caption,
59
+ url: node.attrs.src,
60
+ alt: `${Number.parseFloat(node.attrs.ratio).toFixed(2)}`,
61
+ })
62
+ state.closeNode()
63
+ },
64
+ },
65
+ }
66
+ })
67
+
68
+ withMeta(imageBlockSchema.node, {
69
+ displayName: 'NodeSchema<image-block>',
70
+ group: 'ImageBlock',
71
+ })
@@ -0,0 +1,42 @@
1
+ import { render } from '@testing-library/vue'
2
+ import { expect, test, vi } from 'vitest'
3
+ import { ref } from 'vue'
4
+
5
+ import { ImageViewer } from '../image-viewer'
6
+
7
+ test('calls onImageLoadError when img fires error', async () => {
8
+ const onImageLoadError = vi.fn()
9
+ const config = {
10
+ onImageLoadError,
11
+ captionIcon: '💬',
12
+ captionPlaceholderText: 'Image caption',
13
+ imageIcon: '🖼️',
14
+ uploadButton: 'Upload',
15
+ confirmButton: 'Confirm',
16
+ uploadPlaceholderText: 'or paste an image URL',
17
+ onUpload: async () => 'https://example.com/photo.png',
18
+ }
19
+
20
+ // render the ImageViewer component
21
+ const { container } = render(ImageViewer, {
22
+ props: {
23
+ src: ref('https://example.com/photo.png'),
24
+ caption: ref(''),
25
+ ratio: ref(1),
26
+ selected: ref(false),
27
+ readonly: ref(false),
28
+ setAttr: () => {},
29
+ config,
30
+ },
31
+ })
32
+
33
+ const img = container.querySelector('img[data-type="image-block"]')
34
+ expect(img).toBeTruthy()
35
+
36
+ // simulate the image load error
37
+ img!.dispatchEvent(new Event('error'))
38
+
39
+ // expect the onImageLoadError function to have been called
40
+ expect(onImageLoadError).toHaveBeenCalledTimes(1)
41
+ expect(onImageLoadError).toHaveBeenCalledWith(expect.any(Event))
42
+ })
@@ -0,0 +1,80 @@
1
+ import { h, Fragment, type Ref, defineComponent } from 'vue'
2
+
3
+ import type { ImageBlockConfig } from '../../config'
4
+
5
+ import { ImageInput } from '../../../__internal__/components/image-input'
6
+ import { keepAlive } from '../../../__internal__/keep-alive'
7
+ import { ImageViewer } from './image-viewer'
8
+
9
+ keepAlive(h, Fragment)
10
+
11
+ type Attrs = {
12
+ src: string
13
+ caption: string
14
+ ratio: number
15
+ }
16
+
17
+ export type MilkdownImageBlockProps = {
18
+ selected: Ref<boolean>
19
+ readonly: Ref<boolean>
20
+ setAttr: <T extends keyof Attrs>(attr: T, value: Attrs[T]) => void
21
+ config: ImageBlockConfig
22
+ } & {
23
+ [P in keyof Attrs]: Ref<Attrs[P] | undefined>
24
+ }
25
+
26
+ export const MilkdownImageBlock = defineComponent<MilkdownImageBlockProps>({
27
+ props: {
28
+ src: {
29
+ type: Object,
30
+ required: true,
31
+ },
32
+ caption: {
33
+ type: Object,
34
+ required: true,
35
+ },
36
+ ratio: {
37
+ type: Object,
38
+ required: true,
39
+ },
40
+ selected: {
41
+ type: Object,
42
+ required: true,
43
+ },
44
+ readonly: {
45
+ type: Object,
46
+ required: true,
47
+ },
48
+ setAttr: {
49
+ type: Function,
50
+ required: true,
51
+ },
52
+ config: {
53
+ type: Object,
54
+ required: true,
55
+ },
56
+ },
57
+ setup(props) {
58
+ const { src } = props
59
+
60
+ return () => {
61
+ if (!src.value?.length) {
62
+ return (
63
+ <ImageInput
64
+ src={props.src}
65
+ selected={props.selected}
66
+ readonly={props.readonly}
67
+ setLink={(link) => props.setAttr('src', link)}
68
+ imageIcon={props.config.imageIcon}
69
+ uploadButton={props.config.uploadButton}
70
+ confirmButton={props.config.confirmButton}
71
+ uploadPlaceholderText={props.config.uploadPlaceholderText}
72
+ onUpload={props.config.onUpload}
73
+ onImageLoadError={props.config.onImageLoadError}
74
+ />
75
+ )
76
+ }
77
+ return <ImageViewer {...props} />
78
+ }
79
+ },
80
+ })
@@ -0,0 +1,186 @@
1
+ import { defineComponent, ref, h, Fragment } from 'vue'
2
+
3
+ import type { MilkdownImageBlockProps } from './image-block'
4
+
5
+ import { Icon } from '../../../__internal__/components/icon'
6
+ import { keepAlive } from '../../../__internal__/keep-alive'
7
+ import { IMAGE_DATA_TYPE } from '../../schema'
8
+
9
+ keepAlive(h, Fragment)
10
+
11
+ export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
12
+ 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
+ selected: {
26
+ type: Object,
27
+ required: true,
28
+ },
29
+ readonly: {
30
+ type: Object,
31
+ required: true,
32
+ },
33
+ setAttr: {
34
+ type: Function,
35
+ required: true,
36
+ },
37
+ config: {
38
+ type: Object,
39
+ required: true,
40
+ },
41
+ },
42
+ setup({ src, caption, ratio, readonly, setAttr, config }) {
43
+ const imageRef = ref<HTMLImageElement>()
44
+ const resizeHandle = ref<HTMLDivElement>()
45
+ const showCaption = ref(Boolean(caption.value?.length))
46
+ const timer = ref(0)
47
+
48
+ const onImageLoad = () => {
49
+ const image = imageRef.value
50
+ if (!image) return
51
+ const host = image.closest('.milkdown-image-block')
52
+ if (!host) return
53
+
54
+ let maxWidth = host.getBoundingClientRect().width
55
+ if (!maxWidth) return
56
+
57
+ if (config.maxWidth && config.maxWidth < maxWidth)
58
+ maxWidth = config.maxWidth
59
+
60
+ const height = image.naturalHeight
61
+ const width = image.naturalWidth
62
+ let transformedHeight =
63
+ width < maxWidth ? height : maxWidth * (height / width)
64
+
65
+ if (config.maxHeight && transformedHeight > config.maxHeight)
66
+ transformedHeight = config.maxHeight
67
+
68
+ const h = (transformedHeight * (ratio.value ?? 1)).toFixed(2)
69
+ image.dataset.origin = transformedHeight.toFixed(2)
70
+ image.dataset.height = h
71
+ image.style.height = `${h}px`
72
+
73
+ if (config.maxWidth) image.style.maxWidth = `${config.maxWidth}px`
74
+ }
75
+
76
+ const onToggleCaption = (e: PointerEvent) => {
77
+ e.preventDefault()
78
+ e.stopPropagation()
79
+ if (readonly.value) return
80
+ showCaption.value = !showCaption.value
81
+ }
82
+
83
+ const onInputCaption = (e: Event) => {
84
+ const target = e.target as HTMLInputElement
85
+ const value = target.value
86
+ if (timer.value) window.clearTimeout(timer.value)
87
+
88
+ timer.value = window.setTimeout(() => {
89
+ setAttr('caption', value)
90
+ }, 1000)
91
+ }
92
+
93
+ const onBlurCaption = (e: Event) => {
94
+ const target = e.target as HTMLInputElement
95
+ const value = target.value
96
+ if (timer.value) {
97
+ window.clearTimeout(timer.value)
98
+ timer.value = 0
99
+ }
100
+
101
+ setAttr('caption', value)
102
+ }
103
+
104
+ const onResizeHandlePointerMove = (e: PointerEvent) => {
105
+ e.preventDefault()
106
+ const image = imageRef.value
107
+ if (!image) return
108
+ const top = image.getBoundingClientRect().top
109
+ let height = e.clientY - top
110
+ if (height < 100) height = 100
111
+ if (config.maxHeight && height > config.maxHeight)
112
+ height = config.maxHeight
113
+ const h = Number(height).toFixed(2)
114
+ image.dataset.height = h
115
+ image.style.height = `${h}px`
116
+ }
117
+
118
+ const onResizeHandlePointerUp = () => {
119
+ window.removeEventListener('pointermove', onResizeHandlePointerMove)
120
+ window.removeEventListener('pointerup', onResizeHandlePointerUp)
121
+
122
+ const image = imageRef.value
123
+ if (!image) return
124
+
125
+ const originHeight = Number(image.dataset.origin)
126
+ const currentHeight = Number(image.dataset.height)
127
+ const ratio = Number.parseFloat(
128
+ Number(currentHeight / originHeight).toFixed(2)
129
+ )
130
+ if (Number.isNaN(ratio)) return
131
+
132
+ setAttr('ratio', ratio)
133
+ }
134
+
135
+ const onResizeHandlePointerDown = (e: PointerEvent) => {
136
+ if (readonly.value) return
137
+ e.preventDefault()
138
+ e.stopPropagation()
139
+ window.addEventListener('pointermove', onResizeHandlePointerMove)
140
+ window.addEventListener('pointerup', onResizeHandlePointerUp)
141
+ }
142
+
143
+ return () => {
144
+ return (
145
+ <>
146
+ <div class="image-wrapper">
147
+ <div class="operation">
148
+ <div class="operation-item" onPointerdown={onToggleCaption}>
149
+ <Icon icon={config.captionIcon} />
150
+ </div>
151
+ </div>
152
+ <img
153
+ ref={imageRef}
154
+ data-type={IMAGE_DATA_TYPE}
155
+ onLoad={onImageLoad}
156
+ src={src.value}
157
+ alt={caption.value}
158
+ onError={(e) =>
159
+ Promise.resolve(config.onImageLoadError?.(e)).catch(() => {})
160
+ }
161
+ />
162
+ <div
163
+ ref={resizeHandle}
164
+ class="image-resize-handle"
165
+ onPointerdown={onResizeHandlePointerDown}
166
+ />
167
+ </div>
168
+ {showCaption.value && (
169
+ <input
170
+ draggable="true"
171
+ onDragstart={(e) => {
172
+ e.preventDefault()
173
+ e.stopPropagation()
174
+ }}
175
+ class="caption-input"
176
+ placeholder={config?.captionPlaceholderText}
177
+ onInput={onInputCaption}
178
+ onBlur={onBlurCaption}
179
+ value={caption.value}
180
+ />
181
+ )}
182
+ </>
183
+ )
184
+ }
185
+ },
186
+ })
@@ -0,0 +1,111 @@
1
+ import type { Node } from '@jvs-milkdown/prose/model'
2
+ import type { NodeViewConstructor } from '@jvs-milkdown/prose/view'
3
+
4
+ import { $view } from '@jvs-milkdown/utils'
5
+ import DOMPurify from 'dompurify'
6
+ import { createApp, ref, watchEffect } from 'vue'
7
+
8
+ import { withMeta } from '../../__internal__/meta'
9
+ import { imageBlockConfig } from '../config'
10
+ import { imageBlockSchema } from '../schema'
11
+ import { MilkdownImageBlock } from './components/image-block'
12
+
13
+ export const imageBlockView = $view(
14
+ imageBlockSchema.node,
15
+ (ctx): NodeViewConstructor => {
16
+ return (initialNode, view, getPos) => {
17
+ const src = ref(initialNode.attrs.src)
18
+ const caption = ref(initialNode.attrs.caption)
19
+ const ratio = ref(initialNode.attrs.ratio)
20
+ const selected = ref(false)
21
+ const readonly = ref(!view.editable)
22
+ const setAttr = (attr: string, value: unknown) => {
23
+ if (!view.editable) return
24
+ const pos = getPos()
25
+ if (pos == null) return
26
+ view.dispatch(
27
+ view.state.tr.setNodeAttribute(
28
+ pos,
29
+ attr,
30
+ attr === 'src' ? DOMPurify.sanitize(value as string) : value
31
+ )
32
+ )
33
+ }
34
+ const config = ctx.get(imageBlockConfig.key)
35
+ const app = createApp(MilkdownImageBlock, {
36
+ src,
37
+ caption,
38
+ ratio,
39
+ selected,
40
+ readonly,
41
+ setAttr,
42
+ config,
43
+ })
44
+ const dom = document.createElement('div')
45
+ dom.className = 'milkdown-image-block'
46
+ const disposeSelectedWatcher = watchEffect(() => {
47
+ const isSelected = selected.value
48
+ if (isSelected) {
49
+ dom.classList.add('selected')
50
+ } else {
51
+ dom.classList.remove('selected')
52
+ }
53
+ })
54
+ const proxyDomURL = config.proxyDomURL
55
+ const bindAttrs = (node: Node) => {
56
+ if (!proxyDomURL) {
57
+ src.value = node.attrs.src
58
+ } else {
59
+ const proxiedURL = proxyDomURL(node.attrs.src)
60
+ if (typeof proxiedURL === 'string') {
61
+ src.value = proxiedURL
62
+ } else {
63
+ proxiedURL
64
+ .then((url) => {
65
+ src.value = url
66
+ })
67
+ .catch(console.error)
68
+ }
69
+ }
70
+ ratio.value = node.attrs.ratio
71
+ caption.value = node.attrs.caption
72
+
73
+ readonly.value = !view.editable
74
+ }
75
+
76
+ bindAttrs(initialNode)
77
+ app.mount(dom)
78
+
79
+ return {
80
+ dom,
81
+ update: (updatedNode) => {
82
+ if (updatedNode.type !== initialNode.type) return false
83
+
84
+ bindAttrs(updatedNode)
85
+ return true
86
+ },
87
+ stopEvent: (e) => {
88
+ if (e.target instanceof HTMLInputElement) return true
89
+
90
+ return false
91
+ },
92
+ selectNode: () => {
93
+ selected.value = true
94
+ },
95
+ deselectNode: () => {
96
+ selected.value = false
97
+ },
98
+ destroy: () => {
99
+ disposeSelectedWatcher()
100
+ app.unmount()
101
+ dom.remove()
102
+ },
103
+ }
104
+ }
105
+ }
106
+ )
107
+
108
+ withMeta(imageBlockView, {
109
+ displayName: 'NodeView<image-block>',
110
+ group: 'ImageBlock',
111
+ })
@@ -0,0 +1,85 @@
1
+ import { h, Fragment, type Ref, defineComponent } from 'vue'
2
+
3
+ import type { InlineImageConfig } from '../config'
4
+
5
+ import { ImageInput } from '../../__internal__/components/image-input'
6
+ import { keepAlive } from '../../__internal__/keep-alive'
7
+
8
+ keepAlive(h, Fragment)
9
+
10
+ type Attrs = {
11
+ src: string
12
+ alt: string
13
+ title: string
14
+ }
15
+
16
+ type MilkdownImageInlineProps = {
17
+ selected: Ref<boolean>
18
+ readonly: Ref<boolean>
19
+ setAttr: <T extends keyof Attrs>(attr: T, value: Attrs[T]) => void
20
+ config: InlineImageConfig
21
+ } & {
22
+ [P in keyof Attrs]: Ref<Attrs[P] | undefined>
23
+ }
24
+
25
+ export const MilkdownImageInline = defineComponent<MilkdownImageInlineProps>({
26
+ props: {
27
+ src: {
28
+ type: Object,
29
+ required: true,
30
+ },
31
+ alt: {
32
+ type: Object,
33
+ required: true,
34
+ },
35
+ title: {
36
+ type: Object,
37
+ required: true,
38
+ },
39
+ selected: {
40
+ type: Object,
41
+ required: true,
42
+ },
43
+ readonly: {
44
+ type: Object,
45
+ required: true,
46
+ },
47
+ setAttr: {
48
+ type: Function,
49
+ required: true,
50
+ },
51
+ config: {
52
+ type: Object,
53
+ required: true,
54
+ },
55
+ },
56
+ setup(props) {
57
+ const { src, alt, title } = props
58
+ return () => {
59
+ if (!src.value?.length) {
60
+ return (
61
+ <ImageInput
62
+ src={props.src}
63
+ selected={props.selected}
64
+ readonly={props.readonly}
65
+ setLink={(link) => props.setAttr('src', link)}
66
+ imageIcon={props.config.imageIcon}
67
+ uploadButton={props.config.uploadButton}
68
+ confirmButton={props.config.confirmButton}
69
+ uploadPlaceholderText={props.config.uploadPlaceholderText}
70
+ onUpload={props.config.onUpload}
71
+ className="empty-image-inline"
72
+ />
73
+ )
74
+ }
75
+ return (
76
+ <img
77
+ class="image-inline"
78
+ src={src.value}
79
+ alt={alt.value}
80
+ title={title.value}
81
+ />
82
+ )
83
+ }
84
+ },
85
+ })