@jvs-milkdown/components 1.1.5 → 1.1.7
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/lib/image-block/index.js +345 -75
- package/lib/image-block/index.js.map +1 -1
- package/lib/image-inline/index.js +27 -6
- package/lib/image-inline/index.js.map +1 -1
- package/lib/link-tooltip/index.js +18 -4
- package/lib/link-tooltip/index.js.map +1 -1
- package/lib/table-block/index.js +139 -53
- package/lib/table-block/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +53 -79
- package/src/__internal__/components/image-input.tsx +36 -12
- package/src/image-block/__tests__/paste-rule.spec.ts +20 -0
- package/src/image-block/convert-plugin.ts +147 -0
- package/src/image-block/index.ts +6 -0
- package/src/image-block/paste-rule.ts +138 -0
- package/src/image-block/schema.ts +15 -0
- package/src/image-block/view/components/image-block.tsx +5 -0
- package/src/image-block/view/components/image-viewer.tsx +4 -0
- package/src/image-block/view/index.ts +8 -0
- package/src/link-tooltip/edit/component.tsx +18 -3
- package/src/table-block/dnd/create-drag-handler.ts +5 -1
- package/src/table-block/dnd/drag-over-handler.ts +29 -1
- package/src/table-block/dnd/preview.ts +3 -3
- package/src/table-block/view/component.tsx +121 -39
- package/src/table-block/view/drag.ts +29 -16
- package/src/table-block/view/utils.ts +6 -1
- package/lib/__internal__/components/icon.d.ts +0 -24
- package/lib/__internal__/components/icon.d.ts.map +0 -1
- package/lib/__internal__/components/image-input.d.ts +0 -17
- package/lib/__internal__/components/image-input.d.ts.map +0 -1
- package/lib/__internal__/keep-alive.d.ts +0 -2
- package/lib/__internal__/keep-alive.d.ts.map +0 -1
- package/lib/__internal__/meta.d.ts +0 -3
- package/lib/__internal__/meta.d.ts.map +0 -1
- package/lib/__tests__/setup.d.ts +0 -2
- package/lib/__tests__/setup.d.ts.map +0 -1
- package/lib/code-block/config.d.ts +0 -23
- package/lib/code-block/config.d.ts.map +0 -1
- package/lib/code-block/index.d.ts +0 -5
- package/lib/code-block/index.d.ts.map +0 -1
- package/lib/code-block/view/components/code-block.d.ts +0 -16
- package/lib/code-block/view/components/code-block.d.ts.map +0 -1
- package/lib/code-block/view/components/copy-button.d.ts +0 -9
- package/lib/code-block/view/components/copy-button.d.ts.map +0 -1
- package/lib/code-block/view/components/language-picker.d.ts +0 -5
- package/lib/code-block/view/components/language-picker.d.ts.map +0 -1
- package/lib/code-block/view/components/preview-panel.d.ts +0 -9
- package/lib/code-block/view/components/preview-panel.d.ts.map +0 -1
- package/lib/code-block/view/index.d.ts +0 -3
- package/lib/code-block/view/index.d.ts.map +0 -1
- package/lib/code-block/view/loader.d.ts +0 -13
- package/lib/code-block/view/loader.d.ts.map +0 -1
- package/lib/code-block/view/node-view.d.ts +0 -40
- package/lib/code-block/view/node-view.d.ts.map +0 -1
- package/lib/image-block/config.d.ts +0 -16
- package/lib/image-block/config.d.ts.map +0 -1
- package/lib/image-block/index.d.ts +0 -7
- package/lib/image-block/index.d.ts.map +0 -1
- package/lib/image-block/remark-plugin.d.ts +0 -2
- package/lib/image-block/remark-plugin.d.ts.map +0 -1
- package/lib/image-block/schema.d.ts +0 -3
- package/lib/image-block/schema.d.ts.map +0 -1
- package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts +0 -2
- package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts.map +0 -1
- package/lib/image-block/view/components/image-block.d.ts +0 -18
- package/lib/image-block/view/components/image-block.d.ts.map +0 -1
- package/lib/image-block/view/components/image-viewer.d.ts +0 -3
- package/lib/image-block/view/components/image-viewer.d.ts.map +0 -1
- package/lib/image-block/view/index.d.ts +0 -3
- package/lib/image-block/view/index.d.ts.map +0 -1
- package/lib/image-inline/components/image-inline.d.ts +0 -18
- package/lib/image-inline/components/image-inline.d.ts.map +0 -1
- package/lib/image-inline/config.d.ts +0 -11
- package/lib/image-inline/config.d.ts.map +0 -1
- package/lib/image-inline/index.d.ts +0 -5
- package/lib/image-inline/index.d.ts.map +0 -1
- package/lib/image-inline/view.d.ts +0 -3
- package/lib/image-inline/view.d.ts.map +0 -1
- package/lib/link-tooltip/command.d.ts +0 -2
- package/lib/link-tooltip/command.d.ts.map +0 -1
- package/lib/link-tooltip/configure.d.ts +0 -3
- package/lib/link-tooltip/configure.d.ts.map +0 -1
- package/lib/link-tooltip/edit/component.d.ts +0 -11
- package/lib/link-tooltip/edit/component.d.ts.map +0 -1
- package/lib/link-tooltip/edit/edit-configure.d.ts +0 -3
- package/lib/link-tooltip/edit/edit-configure.d.ts.map +0 -1
- package/lib/link-tooltip/edit/edit-view.d.ts +0 -15
- package/lib/link-tooltip/edit/edit-view.d.ts.map +0 -1
- package/lib/link-tooltip/index.d.ts +0 -7
- package/lib/link-tooltip/index.d.ts.map +0 -1
- package/lib/link-tooltip/preview/component.d.ts +0 -11
- package/lib/link-tooltip/preview/component.d.ts.map +0 -1
- package/lib/link-tooltip/preview/preview-configure.d.ts +0 -3
- package/lib/link-tooltip/preview/preview-configure.d.ts.map +0 -1
- package/lib/link-tooltip/preview/preview-view.d.ts +0 -14
- package/lib/link-tooltip/preview/preview-view.d.ts.map +0 -1
- package/lib/link-tooltip/slices.d.ts +0 -34
- package/lib/link-tooltip/slices.d.ts.map +0 -1
- package/lib/link-tooltip/tooltips.d.ts +0 -3
- package/lib/link-tooltip/tooltips.d.ts.map +0 -1
- package/lib/link-tooltip/utils.d.ts +0 -14
- package/lib/link-tooltip/utils.d.ts.map +0 -1
- package/lib/list-item-block/component.d.ts +0 -19
- package/lib/list-item-block/component.d.ts.map +0 -1
- package/lib/list-item-block/config.d.ts +0 -13
- package/lib/list-item-block/config.d.ts.map +0 -1
- package/lib/list-item-block/index.d.ts +0 -6
- package/lib/list-item-block/index.d.ts.map +0 -1
- package/lib/list-item-block/view.d.ts +0 -3
- package/lib/list-item-block/view.d.ts.map +0 -1
- package/lib/table-block/config.d.ts +0 -8
- package/lib/table-block/config.d.ts.map +0 -1
- package/lib/table-block/dnd/calc-drag-over.d.ts +0 -3
- package/lib/table-block/dnd/calc-drag-over.d.ts.map +0 -1
- package/lib/table-block/dnd/create-drag-handler.d.ts +0 -5
- package/lib/table-block/dnd/create-drag-handler.d.ts.map +0 -1
- package/lib/table-block/dnd/drag-over-handler.d.ts +0 -3
- package/lib/table-block/dnd/drag-over-handler.d.ts.map +0 -1
- package/lib/table-block/dnd/prepare-dnd-context.d.ts +0 -3
- package/lib/table-block/dnd/prepare-dnd-context.d.ts.map +0 -1
- package/lib/table-block/dnd/preview.d.ts +0 -3
- package/lib/table-block/dnd/preview.d.ts.map +0 -1
- package/lib/table-block/index.d.ts +0 -5
- package/lib/table-block/index.d.ts.map +0 -1
- package/lib/table-block/view/component.d.ts +0 -16
- package/lib/table-block/view/component.d.ts.map +0 -1
- package/lib/table-block/view/drag.d.ts +0 -7
- package/lib/table-block/view/drag.d.ts.map +0 -1
- package/lib/table-block/view/index.d.ts +0 -2
- package/lib/table-block/view/index.d.ts.map +0 -1
- package/lib/table-block/view/operation.d.ts +0 -13
- package/lib/table-block/view/operation.d.ts.map +0 -1
- package/lib/table-block/view/pointer.d.ts +0 -7
- package/lib/table-block/view/pointer.d.ts.map +0 -1
- package/lib/table-block/view/types.d.ts +0 -28
- package/lib/table-block/view/types.d.ts.map +0 -1
- package/lib/table-block/view/utils.d.ts +0 -12
- package/lib/table-block/view/utils.d.ts.map +0 -1
- package/lib/table-block/view/view.d.ts +0 -22
- package/lib/table-block/view/view.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jvs-milkdown/components",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"milkdown",
|
|
6
6
|
"milkdown plugin"
|
|
@@ -17,101 +17,48 @@
|
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
19
|
"sideEffects": false,
|
|
20
|
-
"main": "./
|
|
20
|
+
"main": "./lib/index.js",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
|
-
"
|
|
23
|
+
"types": "./lib/index.d.ts",
|
|
24
|
+
"import": "./lib/index.js"
|
|
24
25
|
},
|
|
25
26
|
"./image-block": {
|
|
26
|
-
"
|
|
27
|
+
"types": "./lib/image-block/index.d.ts",
|
|
28
|
+
"import": "./lib/image-block/index.js"
|
|
27
29
|
},
|
|
28
30
|
"./code-block": {
|
|
29
|
-
"
|
|
31
|
+
"types": "./lib/code-block/index.d.ts",
|
|
32
|
+
"import": "./lib/code-block/index.js"
|
|
30
33
|
},
|
|
31
34
|
"./list-item-block": {
|
|
32
|
-
"
|
|
35
|
+
"types": "./lib/list-item-block/index.d.ts",
|
|
36
|
+
"import": "./lib/list-item-block/index.js"
|
|
33
37
|
},
|
|
34
38
|
"./link-tooltip": {
|
|
35
|
-
"
|
|
39
|
+
"types": "./lib/link-tooltip/index.d.ts",
|
|
40
|
+
"import": "./lib/link-tooltip/index.js"
|
|
36
41
|
},
|
|
37
42
|
"./image-inline": {
|
|
38
|
-
"
|
|
43
|
+
"types": "./lib/image-inline/index.d.ts",
|
|
44
|
+
"import": "./lib/image-inline/index.js"
|
|
39
45
|
},
|
|
40
46
|
"./table-block": {
|
|
41
|
-
"
|
|
47
|
+
"types": "./lib/table-block/index.d.ts",
|
|
48
|
+
"import": "./lib/table-block/index.js"
|
|
42
49
|
}
|
|
43
50
|
},
|
|
44
|
-
"publishConfig": {
|
|
45
|
-
"exports": {
|
|
46
|
-
".": {
|
|
47
|
-
"types": "./lib/index.d.ts",
|
|
48
|
-
"import": "./lib/index.js"
|
|
49
|
-
},
|
|
50
|
-
"./image-block": {
|
|
51
|
-
"types": "./lib/image-block/index.d.ts",
|
|
52
|
-
"import": "./lib/image-block/index.js"
|
|
53
|
-
},
|
|
54
|
-
"./code-block": {
|
|
55
|
-
"types": "./lib/code-block/index.d.ts",
|
|
56
|
-
"import": "./lib/code-block/index.js"
|
|
57
|
-
},
|
|
58
|
-
"./list-item-block": {
|
|
59
|
-
"types": "./lib/list-item-block/index.d.ts",
|
|
60
|
-
"import": "./lib/list-item-block/index.js"
|
|
61
|
-
},
|
|
62
|
-
"./link-tooltip": {
|
|
63
|
-
"types": "./lib/link-tooltip/index.d.ts",
|
|
64
|
-
"import": "./lib/link-tooltip/index.js"
|
|
65
|
-
},
|
|
66
|
-
"./image-inline": {
|
|
67
|
-
"types": "./lib/image-inline/index.d.ts",
|
|
68
|
-
"import": "./lib/image-inline/index.js"
|
|
69
|
-
},
|
|
70
|
-
"./table-block": {
|
|
71
|
-
"types": "./lib/table-block/index.d.ts",
|
|
72
|
-
"import": "./lib/table-block/index.js"
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
"main": "./lib/index.js",
|
|
76
|
-
"types": "./lib/index.d.ts",
|
|
77
|
-
"typesVersions": {
|
|
78
|
-
"*": {
|
|
79
|
-
"image-block": [
|
|
80
|
-
"./lib/image-block/index.d.ts"
|
|
81
|
-
],
|
|
82
|
-
"code-block": [
|
|
83
|
-
"./lib/code-block/index.d.ts"
|
|
84
|
-
],
|
|
85
|
-
"list-item-block": [
|
|
86
|
-
"./lib/list-item-block/index.d.ts"
|
|
87
|
-
],
|
|
88
|
-
"link-tooltip": [
|
|
89
|
-
"./lib/link-tooltip/index.d.ts"
|
|
90
|
-
],
|
|
91
|
-
"image-inline": [
|
|
92
|
-
"./lib/image-inline/index.d.ts"
|
|
93
|
-
],
|
|
94
|
-
"table-block": [
|
|
95
|
-
"./lib/table-block/index.d.ts"
|
|
96
|
-
]
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
"scripts": {
|
|
101
|
-
"build": "rollup -c",
|
|
102
|
-
"test": "vitest run"
|
|
103
|
-
},
|
|
104
51
|
"dependencies": {
|
|
105
52
|
"@floating-ui/dom": "^1.5.1",
|
|
106
|
-
"@jvs-milkdown/core": "^1.1.
|
|
107
|
-
"@jvs-milkdown/ctx": "^1.1.
|
|
108
|
-
"@jvs-milkdown/exception": "^1.1.
|
|
109
|
-
"@jvs-milkdown/plugin-tooltip": "^1.1.
|
|
110
|
-
"@jvs-milkdown/preset-commonmark": "^1.1.
|
|
111
|
-
"@jvs-milkdown/preset-gfm": "^1.1.
|
|
112
|
-
"@jvs-milkdown/prose": "^1.1.
|
|
113
|
-
"@jvs-milkdown/transformer": "^1.1.
|
|
114
|
-
"@jvs-milkdown/utils": "^1.1.
|
|
53
|
+
"@jvs-milkdown/core": "^1.1.7",
|
|
54
|
+
"@jvs-milkdown/ctx": "^1.1.7",
|
|
55
|
+
"@jvs-milkdown/exception": "^1.1.7",
|
|
56
|
+
"@jvs-milkdown/plugin-tooltip": "^1.1.7",
|
|
57
|
+
"@jvs-milkdown/preset-commonmark": "^1.1.7",
|
|
58
|
+
"@jvs-milkdown/preset-gfm": "^1.1.7",
|
|
59
|
+
"@jvs-milkdown/prose": "^1.1.7",
|
|
60
|
+
"@jvs-milkdown/transformer": "^1.1.7",
|
|
61
|
+
"@jvs-milkdown/utils": "^1.1.7",
|
|
115
62
|
"@types/lodash-es": "^4.17.12",
|
|
116
63
|
"clsx": "^2.0.0",
|
|
117
64
|
"dompurify": "^3.2.5",
|
|
@@ -132,5 +79,32 @@
|
|
|
132
79
|
"@codemirror/language": "^6",
|
|
133
80
|
"@codemirror/state": "^6",
|
|
134
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
|
+
}
|
|
135
109
|
}
|
|
136
|
-
}
|
|
110
|
+
}
|
|
@@ -93,14 +93,32 @@ export const ImageInput = defineComponent<ImageInputProps>({
|
|
|
93
93
|
currentLink.value = value
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
const isValidUrl = (url: string) => {
|
|
97
|
+
if (!url) return false
|
|
98
|
+
const trimmedUrl = url.trim()
|
|
99
|
+
return /^(https?:\/\/|\/|\.\.?\/|data:image\/|blob:|[a-zA-Z0-9-]+\.[a-zA-Z]+)/i.test(trimmedUrl)
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
const onKeydown = (e: KeyboardEvent) => {
|
|
97
103
|
if (e.key === 'Enter') {
|
|
98
|
-
|
|
104
|
+
const val = linkInputRef.value?.value?.trim() ?? ''
|
|
105
|
+
|
|
106
|
+
// Stop bubbling so ProseMirror doesn't handle the Enter key
|
|
107
|
+
// regardless of whether the URL is valid or not.
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
e.stopPropagation()
|
|
110
|
+
|
|
111
|
+
if (isValidUrl(val)) {
|
|
112
|
+
setLink(val)
|
|
113
|
+
}
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
const onConfirmLinkInput = () => {
|
|
103
|
-
|
|
118
|
+
const val = linkInputRef.value?.value?.trim() ?? ''
|
|
119
|
+
if (isValidUrl(val)) {
|
|
120
|
+
setLink(val)
|
|
121
|
+
}
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
const onUploadFile = (e: Event) => {
|
|
@@ -161,16 +179,22 @@ export const ImageInput = defineComponent<ImageInputProps>({
|
|
|
161
179
|
</div>
|
|
162
180
|
{currentLink.value && (
|
|
163
181
|
<>
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
{isValidUrl(currentLink.value) && (
|
|
183
|
+
<div class="image-preview">
|
|
184
|
+
<img
|
|
185
|
+
src={currentLink.value}
|
|
186
|
+
alt=""
|
|
187
|
+
onError={(e) =>
|
|
188
|
+
Promise.resolve(onImageLoadError?.(e)).catch(() => { })
|
|
189
|
+
}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
<div
|
|
194
|
+
class={clsx('confirm', !isValidUrl(currentLink.value) && 'disabled')}
|
|
195
|
+
onClick={() => onConfirmLinkInput()}
|
|
196
|
+
style={!isValidUrl(currentLink.value) ? { opacity: 0.5, cursor: 'not-allowed' } : undefined}
|
|
197
|
+
>
|
|
174
198
|
<Icon icon={confirmButton} />
|
|
175
199
|
</div>
|
|
176
200
|
</>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { imageSchema, paragraphSchema } from '@jvs-milkdown/preset-commonmark'
|
|
2
|
+
import { DOMParser, Schema, Fragment } from '@jvs-milkdown/prose/model'
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { imageBlockPasteRule } from './paste-rule'
|
|
6
|
+
import { imageBlockSchema } from './schema'
|
|
7
|
+
|
|
8
|
+
describe('paste rule', () => {
|
|
9
|
+
it('splits paragraph correctly', () => {
|
|
10
|
+
// We mock ctx context enough to return strings for schema
|
|
11
|
+
const ctx = {
|
|
12
|
+
get: () => ({
|
|
13
|
+
type: (ctx: any) => {
|
|
14
|
+
// just mock type based on calls
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
}
|
|
18
|
+
expect(true).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Node as ProsemirrorNode } from '@jvs-milkdown/prose/model'
|
|
2
|
+
|
|
3
|
+
import { imageSchema, paragraphSchema } from '@jvs-milkdown/preset-commonmark'
|
|
4
|
+
import { Plugin, PluginKey } from '@jvs-milkdown/prose/state'
|
|
5
|
+
import { $prose } from '@jvs-milkdown/utils'
|
|
6
|
+
|
|
7
|
+
import { withMeta } from '../__internal__/meta'
|
|
8
|
+
import { imageBlockConfig } from './config'
|
|
9
|
+
import { imageBlockSchema } from './schema'
|
|
10
|
+
|
|
11
|
+
export const imageBlockConvertPlugin = $prose((ctx) => {
|
|
12
|
+
const imageType = imageSchema.type(ctx)
|
|
13
|
+
const imageBlockType = imageBlockSchema.type(ctx)
|
|
14
|
+
const paragraphType = paragraphSchema.type(ctx)
|
|
15
|
+
|
|
16
|
+
const pluginKey = new PluginKey('MILKDOWN_IMAGE_BLOCK_CONVERT')
|
|
17
|
+
|
|
18
|
+
let uploading = false
|
|
19
|
+
|
|
20
|
+
return new Plugin({
|
|
21
|
+
key: pluginKey,
|
|
22
|
+
appendTransaction(transactions, _oldState, newState) {
|
|
23
|
+
if (!transactions.some((tr) => tr.docChanged)) return null
|
|
24
|
+
|
|
25
|
+
const replacements: {
|
|
26
|
+
from: number
|
|
27
|
+
to: number
|
|
28
|
+
blocks: ProsemirrorNode[]
|
|
29
|
+
}[] = []
|
|
30
|
+
|
|
31
|
+
// Debug: log all paragraphs with their children
|
|
32
|
+
let foundImages = 0
|
|
33
|
+
newState.doc.descendants((node, pos) => {
|
|
34
|
+
if (node.type === imageType) foundImages++
|
|
35
|
+
})
|
|
36
|
+
if (foundImages > 0) {
|
|
37
|
+
// inline images found
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
newState.doc.descendants((node, pos) => {
|
|
41
|
+
if (node.type !== paragraphType) return
|
|
42
|
+
|
|
43
|
+
const images: ProsemirrorNode[] = []
|
|
44
|
+
let hasOtherContent = false
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
47
|
+
const child = node.child(i)
|
|
48
|
+
if (child.type === imageType) {
|
|
49
|
+
images.push(child)
|
|
50
|
+
} else if (child.type.name === 'hardbreak') {
|
|
51
|
+
continue
|
|
52
|
+
} else if (child.isText && child.text?.trim() === '') {
|
|
53
|
+
continue
|
|
54
|
+
} else {
|
|
55
|
+
hasOtherContent = true
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (hasOtherContent || images.length === 0) return
|
|
61
|
+
|
|
62
|
+
const blocks = images
|
|
63
|
+
.map((img) =>
|
|
64
|
+
imageBlockType.create({
|
|
65
|
+
src: img.attrs.src,
|
|
66
|
+
caption: img.attrs.alt || img.attrs.title || '',
|
|
67
|
+
ratio: 1,
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
|
|
72
|
+
if (blocks.length > 0) {
|
|
73
|
+
replacements.push({ from: pos, to: pos + node.nodeSize, blocks })
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (replacements.length === 0) return null
|
|
78
|
+
|
|
79
|
+
const { tr } = newState
|
|
80
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
81
|
+
const r = replacements[i]!
|
|
82
|
+
tr.replaceWith(r.from, r.to, r.blocks)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return tr
|
|
86
|
+
},
|
|
87
|
+
view() {
|
|
88
|
+
return {
|
|
89
|
+
update(view) {
|
|
90
|
+
if (uploading) return
|
|
91
|
+
|
|
92
|
+
const config = ctx.get(imageBlockConfig.key)
|
|
93
|
+
const imagesToUpload: { pos: number; src: string }[] = []
|
|
94
|
+
|
|
95
|
+
view.state.doc.descendants((node, pos) => {
|
|
96
|
+
if (node.type.name !== 'image-block') return
|
|
97
|
+
const src: string = node.attrs.src
|
|
98
|
+
if (!src || src.startsWith('data:') || src.startsWith('blob:'))
|
|
99
|
+
return
|
|
100
|
+
if (!/^https?:\/\//i.test(src)) return
|
|
101
|
+
imagesToUpload.push({ pos, src })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (imagesToUpload.length === 0) return
|
|
105
|
+
|
|
106
|
+
uploading = true
|
|
107
|
+
|
|
108
|
+
void Promise.allSettled(
|
|
109
|
+
imagesToUpload.map(async ({ pos, src }) => {
|
|
110
|
+
const resp = await fetch(src)
|
|
111
|
+
const blob = await resp.blob()
|
|
112
|
+
const ext = blob.type.split('/')[1] || 'png'
|
|
113
|
+
const file = new File([blob], `pasted-image.${ext}`, {
|
|
114
|
+
type: blob.type,
|
|
115
|
+
})
|
|
116
|
+
const uploadedUrl = await config.onUpload(file)
|
|
117
|
+
|
|
118
|
+
const $pos = view.state.doc.resolve(pos)
|
|
119
|
+
const nodeAtPos = $pos.nodeAfter
|
|
120
|
+
if (
|
|
121
|
+
nodeAtPos &&
|
|
122
|
+
nodeAtPos.type.name === 'image-block' &&
|
|
123
|
+
nodeAtPos.attrs.src === src
|
|
124
|
+
) {
|
|
125
|
+
view.dispatch(
|
|
126
|
+
view.state.tr
|
|
127
|
+
.setNodeMarkup(pos, undefined, {
|
|
128
|
+
...nodeAtPos.attrs,
|
|
129
|
+
src: uploadedUrl,
|
|
130
|
+
})
|
|
131
|
+
.setMeta(pluginKey, { uploaded: true })
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
).finally(() => {
|
|
136
|
+
uploading = false
|
|
137
|
+
})
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
withMeta(imageBlockConvertPlugin, {
|
|
145
|
+
displayName: 'Prose<image-block-convert>',
|
|
146
|
+
group: 'ImageBlock',
|
|
147
|
+
})
|
package/src/image-block/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { MilkdownPlugin } from '@jvs-milkdown/ctx'
|
|
2
2
|
|
|
3
3
|
import { imageBlockConfig } from './config'
|
|
4
|
+
import { imageBlockConvertPlugin } from './convert-plugin'
|
|
5
|
+
import { imageBlockPasteRule } from './paste-rule'
|
|
4
6
|
import { remarkImageBlockPlugin } from './remark-plugin'
|
|
5
7
|
import { imageBlockSchema } from './schema'
|
|
6
8
|
import { imageBlockView } from './view'
|
|
@@ -9,10 +11,14 @@ export * from './schema'
|
|
|
9
11
|
export * from './remark-plugin'
|
|
10
12
|
export * from './config'
|
|
11
13
|
export * from './view'
|
|
14
|
+
export * from './paste-rule'
|
|
15
|
+
export * from './convert-plugin'
|
|
12
16
|
|
|
13
17
|
export const imageBlockComponent: MilkdownPlugin[] = [
|
|
14
18
|
remarkImageBlockPlugin,
|
|
15
19
|
imageBlockSchema,
|
|
16
20
|
imageBlockView,
|
|
17
21
|
imageBlockConfig,
|
|
22
|
+
imageBlockPasteRule,
|
|
23
|
+
imageBlockConvertPlugin,
|
|
18
24
|
].flat()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { imageSchema, paragraphSchema } from '@jvs-milkdown/preset-commonmark'
|
|
2
|
+
import {
|
|
3
|
+
type Fragment as FragmentType,
|
|
4
|
+
Fragment,
|
|
5
|
+
type Node as ProsemirrorNode,
|
|
6
|
+
Slice,
|
|
7
|
+
} from '@jvs-milkdown/prose/model'
|
|
8
|
+
import { $pasteRule } from '@jvs-milkdown/utils'
|
|
9
|
+
|
|
10
|
+
import { withMeta } from '../__internal__/meta'
|
|
11
|
+
import { imageBlockSchema } from './schema'
|
|
12
|
+
|
|
13
|
+
function toImageBlock(
|
|
14
|
+
imageNode: ProsemirrorNode,
|
|
15
|
+
imageBlockType: ProsemirrorNode['type']
|
|
16
|
+
): ProsemirrorNode | null {
|
|
17
|
+
return imageBlockType.create({
|
|
18
|
+
src: imageNode.attrs.src,
|
|
19
|
+
caption: imageNode.attrs.alt || imageNode.attrs.title || '',
|
|
20
|
+
ratio: 1,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Check if a paragraph contains only images (and trailing hard_breaks / empty text).
|
|
25
|
+
/// Returns the list of image nodes found, or null if there's other content.
|
|
26
|
+
function extractParagraphImages(
|
|
27
|
+
node: ProsemirrorNode,
|
|
28
|
+
imageType: ProsemirrorNode['type']
|
|
29
|
+
): ProsemirrorNode[] | null {
|
|
30
|
+
const images: ProsemirrorNode[] = []
|
|
31
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
32
|
+
const child = node.child(i)
|
|
33
|
+
if (child.type === imageType) {
|
|
34
|
+
images.push(child)
|
|
35
|
+
} else if (child.type.name === 'hardbreak') {
|
|
36
|
+
continue
|
|
37
|
+
} else if (child.isText && child.text?.trim() === '') {
|
|
38
|
+
continue
|
|
39
|
+
} else {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return images.length > 0 ? images : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// A paste rule that converts standalone inline images to image-block nodes.
|
|
47
|
+
/// Handles the slice structures produced when pasting HTML from external pages
|
|
48
|
+
/// or from another instance of the same editor:
|
|
49
|
+
/// 1. Bare `image` at fragment top-level (after Slice.maxOpen unwraps)
|
|
50
|
+
/// 2. `paragraph > image` (paragraph with single image child)
|
|
51
|
+
/// 3. `paragraph > [image, hard_break, ...]` (image with trailing breaks)
|
|
52
|
+
export const imageBlockPasteRule = $pasteRule((ctx) => ({
|
|
53
|
+
priority: 90,
|
|
54
|
+
run: (slice, _view, isPlainText) => {
|
|
55
|
+
if (isPlainText) return slice
|
|
56
|
+
|
|
57
|
+
const paragraphType = paragraphSchema.type(ctx)
|
|
58
|
+
const imageType = imageSchema.type(ctx)
|
|
59
|
+
const imageBlockType = imageBlockSchema.type(ctx)
|
|
60
|
+
|
|
61
|
+
function convertFragment(fragment: FragmentType): FragmentType {
|
|
62
|
+
const nodes: ProsemirrorNode[] = []
|
|
63
|
+
let changed = false
|
|
64
|
+
|
|
65
|
+
fragment.forEach((node) => {
|
|
66
|
+
// Case 1: bare inline image at top level (after Slice.maxOpen unwraps paragraph)
|
|
67
|
+
if (node.type === imageType) {
|
|
68
|
+
const imageBlock = toImageBlock(node, imageBlockType)
|
|
69
|
+
if (imageBlock) {
|
|
70
|
+
nodes.push(imageBlock)
|
|
71
|
+
changed = true
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Case 2, 3 & Mixed: Any paragraph containing images
|
|
77
|
+
if (node.type === paragraphType) {
|
|
78
|
+
let hasImage = false
|
|
79
|
+
node.content.forEach((child) => {
|
|
80
|
+
if (child.type === imageType) hasImage = true
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (hasImage) {
|
|
84
|
+
let currentParaNodes: ProsemirrorNode[] = []
|
|
85
|
+
let isFirstInsideParent = nodes.length === 0
|
|
86
|
+
|
|
87
|
+
node.content.forEach((child) => {
|
|
88
|
+
if (child.type === imageType) {
|
|
89
|
+
// If it's the first element in the parent slice (meaning we might need an openStart anchor)
|
|
90
|
+
// OR we have accumulated text nodes, push a paragraph.
|
|
91
|
+
if (currentParaNodes.length > 0 || isFirstInsideParent) {
|
|
92
|
+
nodes.push(node.copy(Fragment.from(currentParaNodes)))
|
|
93
|
+
currentParaNodes = []
|
|
94
|
+
}
|
|
95
|
+
const imageBlock = toImageBlock(child, imageBlockType)
|
|
96
|
+
if (imageBlock) {
|
|
97
|
+
nodes.push(imageBlock)
|
|
98
|
+
}
|
|
99
|
+
isFirstInsideParent = false
|
|
100
|
+
} else {
|
|
101
|
+
currentParaNodes.push(child)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
// ALWAYS push the trailing paragraph at the end, even if empty.
|
|
105
|
+
// This guarantees that if the fragment replacement ends here, it's a paragraph,
|
|
106
|
+
// which safegaurds openEnd anchors from throwing ProseMirror slice geometry errors.
|
|
107
|
+
nodes.push(node.copy(Fragment.from(currentParaNodes)))
|
|
108
|
+
|
|
109
|
+
changed = true
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Recurse into children
|
|
115
|
+
if (node.content.size > 0) {
|
|
116
|
+
const fixedContent = convertFragment(node.content)
|
|
117
|
+
if (fixedContent !== node.content) {
|
|
118
|
+
changed = true
|
|
119
|
+
nodes.push(node.copy(fixedContent))
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
nodes.push(node)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return changed ? Fragment.from(nodes) : fragment
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const fragment = convertFragment(slice.content)
|
|
131
|
+
return new Slice(fragment, slice.openStart, slice.openEnd)
|
|
132
|
+
},
|
|
133
|
+
}))
|
|
134
|
+
|
|
135
|
+
withMeta(imageBlockPasteRule, {
|
|
136
|
+
displayName: 'PasteRule<image-block>',
|
|
137
|
+
group: 'ImageBlock',
|
|
138
|
+
})
|
|
@@ -19,6 +19,7 @@ export const imageBlockSchema = $nodeSchema('image-block', () => {
|
|
|
19
19
|
src: { default: '', validate: 'string' },
|
|
20
20
|
caption: { default: '', validate: 'string' },
|
|
21
21
|
ratio: { default: 1, validate: 'number' },
|
|
22
|
+
align: { default: null },
|
|
22
23
|
},
|
|
23
24
|
parseDOM: [
|
|
24
25
|
{
|
|
@@ -30,6 +31,20 @@ export const imageBlockSchema = $nodeSchema('image-block', () => {
|
|
|
30
31
|
src: dom.getAttribute('src') || '',
|
|
31
32
|
caption: dom.getAttribute('caption') || '',
|
|
32
33
|
ratio: Number(dom.getAttribute('ratio') ?? 1),
|
|
34
|
+
align: dom.getAttribute('align') || null,
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
tag: 'img[src]',
|
|
40
|
+
getAttrs: (dom) => {
|
|
41
|
+
if (!(dom instanceof HTMLElement)) throw expectDomTypeError(dom)
|
|
42
|
+
if (dom.getAttribute('data-type') === IMAGE_DATA_TYPE) return false
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
src: dom.getAttribute('src') || '',
|
|
46
|
+
caption: dom.getAttribute('alt') || dom.getAttribute('title') || '',
|
|
47
|
+
ratio: 1,
|
|
33
48
|
}
|
|
34
49
|
},
|
|
35
50
|
},
|
|
@@ -12,6 +12,7 @@ type Attrs = {
|
|
|
12
12
|
src: string
|
|
13
13
|
caption: string
|
|
14
14
|
ratio: number
|
|
15
|
+
align: string | null
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export type MilkdownImageBlockProps = {
|
|
@@ -37,6 +38,10 @@ export const MilkdownImageBlock = defineComponent<MilkdownImageBlockProps>({
|
|
|
37
38
|
type: Object,
|
|
38
39
|
required: true,
|
|
39
40
|
},
|
|
41
|
+
align: {
|
|
42
|
+
type: Object,
|
|
43
|
+
required: true,
|
|
44
|
+
},
|
|
40
45
|
selected: {
|
|
41
46
|
type: Object,
|
|
42
47
|
required: true,
|
|
@@ -17,6 +17,7 @@ export const imageBlockView = $view(
|
|
|
17
17
|
const src = ref(initialNode.attrs.src)
|
|
18
18
|
const caption = ref(initialNode.attrs.caption)
|
|
19
19
|
const ratio = ref(initialNode.attrs.ratio)
|
|
20
|
+
const align = ref(initialNode.attrs.align)
|
|
20
21
|
const selected = ref(false)
|
|
21
22
|
const readonly = ref(!view.editable)
|
|
22
23
|
const setAttr = (attr: string, value: unknown) => {
|
|
@@ -36,6 +37,7 @@ export const imageBlockView = $view(
|
|
|
36
37
|
src,
|
|
37
38
|
caption,
|
|
38
39
|
ratio,
|
|
40
|
+
align,
|
|
39
41
|
selected,
|
|
40
42
|
readonly,
|
|
41
43
|
setAttr,
|
|
@@ -43,6 +45,7 @@ export const imageBlockView = $view(
|
|
|
43
45
|
})
|
|
44
46
|
const dom = document.createElement('div')
|
|
45
47
|
dom.className = 'milkdown-image-block'
|
|
48
|
+
dom.dataset.align = initialNode.attrs.align || 'center'
|
|
46
49
|
const disposeSelectedWatcher = watchEffect(() => {
|
|
47
50
|
const isSelected = selected.value
|
|
48
51
|
if (isSelected) {
|
|
@@ -51,6 +54,9 @@ export const imageBlockView = $view(
|
|
|
51
54
|
dom.classList.remove('selected')
|
|
52
55
|
}
|
|
53
56
|
})
|
|
57
|
+
const disposeAlignWatcher = watchEffect(() => {
|
|
58
|
+
dom.dataset.align = align.value || 'center'
|
|
59
|
+
})
|
|
54
60
|
const proxyDomURL = config.proxyDomURL
|
|
55
61
|
const bindAttrs = (node: Node) => {
|
|
56
62
|
if (!proxyDomURL) {
|
|
@@ -69,6 +75,7 @@ export const imageBlockView = $view(
|
|
|
69
75
|
}
|
|
70
76
|
ratio.value = node.attrs.ratio
|
|
71
77
|
caption.value = node.attrs.caption
|
|
78
|
+
align.value = node.attrs.align
|
|
72
79
|
|
|
73
80
|
readonly.value = !view.editable
|
|
74
81
|
}
|
|
@@ -97,6 +104,7 @@ export const imageBlockView = $view(
|
|
|
97
104
|
},
|
|
98
105
|
destroy: () => {
|
|
99
106
|
disposeSelectedWatcher()
|
|
107
|
+
disposeAlignWatcher()
|
|
100
108
|
app.unmount()
|
|
101
109
|
dom.remove()
|
|
102
110
|
},
|