1
1
import { Image } from '@tiptap/extension-image' ;
2
- import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown' ;
2
+ import { VueNodeViewRenderer } from '@tiptap/vue-2' ;
3
+ import { Plugin , PluginKey } from 'prosemirror-state' ;
4
+ import { __ } from '~/locale' ;
5
+ import ImageWrapper from '../components/wrappers/image.vue' ;
6
+ import { uploadFile } from '../services/upload_file' ;
7
+ import { getImageAlt , readFileAsDataURL } from '../services/utils' ;
8
+
9
+ export const acceptedMimes = [ 'image/jpeg' , 'image/png' , 'image/gif' , 'image/jpg' ] ;
10
+
11
+ const resolveImageEl = ( element ) =>
12
+ element . nodeName === 'IMG' ? element : element . querySelector ( 'img' ) ;
13
+
14
+ const startFileUpload = async ( { editor, file, uploadsPath, renderMarkdown } ) => {
15
+ const encodedSrc = await readFileAsDataURL ( file ) ;
16
+ const { view } = editor ;
17
+
18
+ editor . commands . setImage ( { uploading : true , src : encodedSrc } ) ;
19
+
20
+ const { state } = view ;
21
+ const position = state . selection . from - 1 ;
22
+ const { tr } = state ;
23
+
24
+ try {
25
+ const { src, canonicalSrc } = await uploadFile ( { file, uploadsPath, renderMarkdown } ) ;
26
+
27
+ view . dispatch (
28
+ tr . setNodeMarkup ( position , undefined , {
29
+ uploading : false ,
30
+ src : encodedSrc ,
31
+ alt : getImageAlt ( src ) ,
32
+ canonicalSrc,
33
+ } ) ,
34
+ ) ;
35
+ } catch ( e ) {
36
+ editor . commands . deleteRange ( { from : position , to : position + 1 } ) ;
37
+ editor . emit ( 'error' , __ ( 'An error occurred while uploading the image. Please try again.' ) ) ;
38
+ }
39
+ } ;
40
+
41
+ const handleFileEvent = ( { editor, file, uploadsPath, renderMarkdown } ) => {
42
+ if ( acceptedMimes . includes ( file ?. type ) ) {
43
+ startFileUpload ( { editor, file, uploadsPath, renderMarkdown } ) ;
44
+
45
+ return true ;
46
+ }
47
+
48
+ return false ;
49
+ } ;
3
50
4
51
const ExtendedImage = Image . extend ( {
52
+ defaultOptions : {
53
+ ...Image . options ,
54
+ uploadsPath : null ,
55
+ renderMarkdown : null ,
56
+ } ,
5
57
addAttributes ( ) {
6
58
return {
7
59
...this . parent ?. ( ) ,
60
+ uploading : {
61
+ default : false ,
62
+ } ,
8
63
src : {
9
64
default : null ,
10
65
/*
@@ -14,17 +69,25 @@ const ExtendedImage = Image.extend({
14
69
* attribute.
15
70
*/
16
71
parseHTML : ( element ) => {
17
- const img = element . querySelector ( 'img' ) ;
72
+ const img = resolveImageEl ( element ) ;
18
73
19
74
return {
20
75
src : img . dataset . src || img . getAttribute ( 'src' ) ,
21
76
} ;
22
77
} ,
23
78
} ,
79
+ canonicalSrc : {
80
+ default : null ,
81
+ parseHTML : ( element ) => {
82
+ return {
83
+ canonicalSrc : element . dataset . canonicalSrc ,
84
+ } ;
85
+ } ,
86
+ } ,
24
87
alt : {
25
88
default : null ,
26
89
parseHTML : ( element ) => {
27
- const img = element . querySelector ( 'img' ) ;
90
+ const img = resolveImageEl ( element ) ;
28
91
29
92
return {
30
93
alt : img . getAttribute ( 'alt' ) ,
@@ -44,9 +107,58 @@ const ExtendedImage = Image.extend({
44
107
} ,
45
108
] ;
46
109
} ,
110
+ addCommands ( ) {
111
+ return {
112
+ ...this . parent ( ) ,
113
+ uploadImage : ( { file } ) => ( ) => {
114
+ const { uploadsPath, renderMarkdown } = this . options ;
115
+
116
+ handleFileEvent ( { file, uploadsPath, renderMarkdown, editor : this . editor } ) ;
117
+ } ,
118
+ } ;
119
+ } ,
120
+ addProseMirrorPlugins ( ) {
121
+ const { editor } = this ;
122
+
123
+ return [
124
+ new Plugin ( {
125
+ key : new PluginKey ( 'handleDropAndPasteImages' ) ,
126
+ props : {
127
+ handlePaste : ( _ , event ) => {
128
+ const { uploadsPath, renderMarkdown } = this . options ;
129
+
130
+ return handleFileEvent ( {
131
+ editor,
132
+ file : event . clipboardData . files [ 0 ] ,
133
+ uploadsPath,
134
+ renderMarkdown,
135
+ } ) ;
136
+ } ,
137
+ handleDrop : ( _ , event ) => {
138
+ const { uploadsPath, renderMarkdown } = this . options ;
139
+
140
+ return handleFileEvent ( {
141
+ editor,
142
+ file : event . dataTransfer . files [ 0 ] ,
143
+ uploadsPath,
144
+ renderMarkdown,
145
+ } ) ;
146
+ } ,
147
+ } ,
148
+ } ) ,
149
+ ] ;
150
+ } ,
151
+ addNodeView ( ) {
152
+ return VueNodeViewRenderer ( ImageWrapper ) ;
153
+ } ,
47
154
} ) ;
48
155
49
- const serializer = defaultMarkdownSerializer . nodes . image ;
156
+ const serializer = ( state , node ) => {
157
+ const { alt, canonicalSrc, src, title } = node . attrs ;
158
+ const quotedTitle = title ? ` ${ state . quote ( title ) } ` : '' ;
159
+
160
+ state . write ( ` } ${ quotedTitle } )` ) ;
161
+ } ;
50
162
51
163
export const configure = ( { renderMarkdown, uploadsPath } ) => {
52
164
return {
0 commit comments