@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.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/lib/__internal__/components/icon.d.ts +24 -0
- package/lib/__internal__/components/icon.d.ts.map +1 -0
- package/lib/__internal__/components/image-input.d.ts +17 -0
- package/lib/__internal__/components/image-input.d.ts.map +1 -0
- package/lib/__internal__/keep-alive.d.ts +2 -0
- package/lib/__internal__/keep-alive.d.ts.map +1 -0
- package/lib/__internal__/meta.d.ts +3 -0
- package/lib/__internal__/meta.d.ts.map +1 -0
- package/lib/__tests__/setup.d.ts +2 -0
- package/lib/__tests__/setup.d.ts.map +1 -0
- package/lib/code-block/config.d.ts +23 -0
- package/lib/code-block/config.d.ts.map +1 -0
- package/lib/code-block/index.d.ts +5 -0
- package/lib/code-block/index.d.ts.map +1 -0
- package/lib/code-block/index.js +4160 -0
- package/lib/code-block/index.js.map +1 -0
- package/lib/code-block/view/components/code-block.d.ts +16 -0
- package/lib/code-block/view/components/code-block.d.ts.map +1 -0
- package/lib/code-block/view/components/copy-button.d.ts +9 -0
- package/lib/code-block/view/components/copy-button.d.ts.map +1 -0
- package/lib/code-block/view/components/language-picker.d.ts +5 -0
- package/lib/code-block/view/components/language-picker.d.ts.map +1 -0
- package/lib/code-block/view/components/preview-panel.d.ts +9 -0
- package/lib/code-block/view/components/preview-panel.d.ts.map +1 -0
- package/lib/code-block/view/index.d.ts +3 -0
- package/lib/code-block/view/index.d.ts.map +1 -0
- package/lib/code-block/view/loader.d.ts +13 -0
- package/lib/code-block/view/loader.d.ts.map +1 -0
- package/lib/code-block/view/node-view.d.ts +40 -0
- package/lib/code-block/view/node-view.d.ts.map +1 -0
- package/lib/image-block/config.d.ts +16 -0
- package/lib/image-block/config.d.ts.map +1 -0
- package/lib/image-block/index.d.ts +7 -0
- package/lib/image-block/index.d.ts.map +1 -0
- package/lib/image-block/index.js +660 -0
- package/lib/image-block/index.js.map +1 -0
- package/lib/image-block/remark-plugin.d.ts +2 -0
- package/lib/image-block/remark-plugin.d.ts.map +1 -0
- package/lib/image-block/schema.d.ts +3 -0
- package/lib/image-block/schema.d.ts.map +1 -0
- package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts +2 -0
- package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts.map +1 -0
- package/lib/image-block/view/components/image-block.d.ts +18 -0
- package/lib/image-block/view/components/image-block.d.ts.map +1 -0
- package/lib/image-block/view/components/image-viewer.d.ts +3 -0
- package/lib/image-block/view/components/image-viewer.d.ts.map +1 -0
- package/lib/image-block/view/index.d.ts +3 -0
- package/lib/image-block/view/index.d.ts.map +1 -0
- package/lib/image-inline/components/image-inline.d.ts +18 -0
- package/lib/image-inline/components/image-inline.d.ts.map +1 -0
- package/lib/image-inline/config.d.ts +11 -0
- package/lib/image-inline/config.d.ts.map +1 -0
- package/lib/image-inline/index.d.ts +5 -0
- package/lib/image-inline/index.d.ts.map +1 -0
- package/lib/image-inline/index.js +377 -0
- package/lib/image-inline/index.js.map +1 -0
- package/lib/image-inline/view.d.ts +3 -0
- package/lib/image-inline/view.d.ts.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -0
- package/lib/link-tooltip/command.d.ts +2 -0
- package/lib/link-tooltip/command.d.ts.map +1 -0
- package/lib/link-tooltip/configure.d.ts +3 -0
- package/lib/link-tooltip/configure.d.ts.map +1 -0
- package/lib/link-tooltip/edit/component.d.ts +11 -0
- package/lib/link-tooltip/edit/component.d.ts.map +1 -0
- package/lib/link-tooltip/edit/edit-configure.d.ts +3 -0
- package/lib/link-tooltip/edit/edit-configure.d.ts.map +1 -0
- package/lib/link-tooltip/edit/edit-view.d.ts +15 -0
- package/lib/link-tooltip/edit/edit-view.d.ts.map +1 -0
- package/lib/link-tooltip/index.d.ts +7 -0
- package/lib/link-tooltip/index.d.ts.map +1 -0
- package/lib/link-tooltip/index.js +2526 -0
- package/lib/link-tooltip/index.js.map +1 -0
- package/lib/link-tooltip/preview/component.d.ts +11 -0
- package/lib/link-tooltip/preview/component.d.ts.map +1 -0
- package/lib/link-tooltip/preview/preview-configure.d.ts +3 -0
- package/lib/link-tooltip/preview/preview-configure.d.ts.map +1 -0
- package/lib/link-tooltip/preview/preview-view.d.ts +14 -0
- package/lib/link-tooltip/preview/preview-view.d.ts.map +1 -0
- package/lib/link-tooltip/slices.d.ts +34 -0
- package/lib/link-tooltip/slices.d.ts.map +1 -0
- package/lib/link-tooltip/tooltips.d.ts +3 -0
- package/lib/link-tooltip/tooltips.d.ts.map +1 -0
- package/lib/link-tooltip/utils.d.ts +14 -0
- package/lib/link-tooltip/utils.d.ts.map +1 -0
- package/lib/list-item-block/component.d.ts +19 -0
- package/lib/list-item-block/component.d.ts.map +1 -0
- package/lib/list-item-block/config.d.ts +13 -0
- package/lib/list-item-block/config.d.ts.map +1 -0
- package/lib/list-item-block/index.d.ts +6 -0
- package/lib/list-item-block/index.d.ts.map +1 -0
- package/lib/list-item-block/index.js +2149 -0
- package/lib/list-item-block/index.js.map +1 -0
- package/lib/list-item-block/view.d.ts +3 -0
- package/lib/list-item-block/view.d.ts.map +1 -0
- package/lib/table-block/config.d.ts +8 -0
- package/lib/table-block/config.d.ts.map +1 -0
- package/lib/table-block/dnd/calc-drag-over.d.ts +3 -0
- package/lib/table-block/dnd/calc-drag-over.d.ts.map +1 -0
- package/lib/table-block/dnd/create-drag-handler.d.ts +5 -0
- package/lib/table-block/dnd/create-drag-handler.d.ts.map +1 -0
- package/lib/table-block/dnd/drag-over-handler.d.ts +3 -0
- package/lib/table-block/dnd/drag-over-handler.d.ts.map +1 -0
- package/lib/table-block/dnd/prepare-dnd-context.d.ts +3 -0
- package/lib/table-block/dnd/prepare-dnd-context.d.ts.map +1 -0
- package/lib/table-block/dnd/preview.d.ts +3 -0
- package/lib/table-block/dnd/preview.d.ts.map +1 -0
- package/lib/table-block/index.d.ts +5 -0
- package/lib/table-block/index.d.ts.map +1 -0
- package/lib/table-block/index.js +3961 -0
- package/lib/table-block/index.js.map +1 -0
- package/lib/table-block/view/component.d.ts +16 -0
- package/lib/table-block/view/component.d.ts.map +1 -0
- package/lib/table-block/view/drag.d.ts +7 -0
- package/lib/table-block/view/drag.d.ts.map +1 -0
- package/lib/table-block/view/index.d.ts +2 -0
- package/lib/table-block/view/index.d.ts.map +1 -0
- package/lib/table-block/view/operation.d.ts +11 -0
- package/lib/table-block/view/operation.d.ts.map +1 -0
- package/lib/table-block/view/pointer.d.ts +7 -0
- package/lib/table-block/view/pointer.d.ts.map +1 -0
- package/lib/table-block/view/types.d.ts +32 -0
- package/lib/table-block/view/types.d.ts.map +1 -0
- package/lib/table-block/view/utils.d.ts +21 -0
- package/lib/table-block/view/utils.d.ts.map +1 -0
- package/lib/table-block/view/view.d.ts +22 -0
- package/lib/table-block/view/view.d.ts.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +110 -0
- package/src/__internal__/components/icon.tsx +38 -0
- package/src/__internal__/components/image-input.tsx +182 -0
- package/src/__internal__/keep-alive.ts +3 -0
- package/src/__internal__/meta.ts +15 -0
- package/src/__tests__/setup.ts +6 -0
- package/src/code-block/config.ts +54 -0
- package/src/code-block/index.ts +12 -0
- package/src/code-block/view/components/code-block.tsx +170 -0
- package/src/code-block/view/components/copy-button.tsx +96 -0
- package/src/code-block/view/components/language-picker.tsx +239 -0
- package/src/code-block/view/components/preview-panel.tsx +79 -0
- package/src/code-block/view/index.ts +24 -0
- package/src/code-block/view/loader.ts +40 -0
- package/src/code-block/view/node-view.ts +310 -0
- package/src/image-block/config.ts +37 -0
- package/src/image-block/index.ts +18 -0
- package/src/image-block/remark-plugin.ts +51 -0
- package/src/image-block/schema.ts +71 -0
- package/src/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.tsx +42 -0
- package/src/image-block/view/components/image-block.tsx +80 -0
- package/src/image-block/view/components/image-viewer.tsx +186 -0
- package/src/image-block/view/index.ts +111 -0
- package/src/image-inline/components/image-inline.tsx +85 -0
- package/src/image-inline/config.ts +30 -0
- package/src/image-inline/index.ts +12 -0
- package/src/image-inline/view.ts +109 -0
- package/src/index.ts +1 -0
- package/src/link-tooltip/command.ts +19 -0
- package/src/link-tooltip/configure.ts +9 -0
- package/src/link-tooltip/edit/component.tsx +82 -0
- package/src/link-tooltip/edit/edit-configure.ts +29 -0
- package/src/link-tooltip/edit/edit-view.ts +165 -0
- package/src/link-tooltip/index.ts +19 -0
- package/src/link-tooltip/preview/component.tsx +87 -0
- package/src/link-tooltip/preview/preview-configure.ts +65 -0
- package/src/link-tooltip/preview/preview-view.ts +101 -0
- package/src/link-tooltip/slices.ts +69 -0
- package/src/link-tooltip/tooltips.ts +22 -0
- package/src/link-tooltip/utils.ts +56 -0
- package/src/list-item-block/component.tsx +133 -0
- package/src/list-item-block/config.ts +39 -0
- package/src/list-item-block/index.ts +13 -0
- package/src/list-item-block/view.ts +130 -0
- package/src/table-block/config.ts +53 -0
- package/src/table-block/dnd/calc-drag-over.ts +46 -0
- package/src/table-block/dnd/create-drag-handler.ts +99 -0
- package/src/table-block/dnd/drag-over-handler.ts +113 -0
- package/src/table-block/dnd/prepare-dnd-context.ts +46 -0
- package/src/table-block/dnd/preview.ts +58 -0
- package/src/table-block/index.ts +9 -0
- package/src/table-block/view/component.tsx +219 -0
- package/src/table-block/view/drag.ts +121 -0
- package/src/table-block/view/index.ts +1 -0
- package/src/table-block/view/operation.ts +148 -0
- package/src/table-block/view/pointer.ts +165 -0
- package/src/table-block/view/types.ts +35 -0
- package/src/table-block/view/utils.ts +192 -0
- 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
|
+
})
|