Skip to content

Commit 3adeac6

Browse files
committed
feat(attachments): allow to listen for changes via editorApi
Required for Collectives to track attachments. Signed-off-by: Jonas <jonas@freesources.org>
1 parent 9a77da1 commit 3adeac6

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

src/editor.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ class TextEditorEmbed {
7474
return this
7575
}
7676

77+
onAttachmentsUpdated(onAttachmentsUpdatedCallback = () => {}) {
78+
subscribe('text:editor:attachments:updated', onAttachmentsUpdatedCallback)
79+
return this
80+
}
81+
7782
render(el) {
7883
el.innerHTML = ''
7984
const element = document.createElement('div')
@@ -256,6 +261,7 @@ window.OCA.Text.createEditor = async function ({
256261
onMentionInsert = undefined,
257262
openLinkHandler = undefined,
258263
onSearch = undefined,
264+
onAttachmentsUpdated = ({ attachmentSrcs }) => {},
259265
}) {
260266
const { default: MarkdownContentEditor } = await import(
261267
'./components/Editor/MarkdownContentEditor.vue'
@@ -339,6 +345,7 @@ window.OCA.Text.createEditor = async function ({
339345
.onTocToggle(onOutlineToggle)
340346
.onTocToggle(onTocToggle)
341347
.onTocPin(onTocPin)
348+
.onAttachmentsUpdated(onAttachmentsUpdated)
342349
.render(el)
343350
}
344351

src/nodes/Image.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import { emit } from '@nextcloud/event-bus'
67
import TiptapImage from '@tiptap/extension-image'
78
import { defaultMarkdownSerializer } from '@tiptap/pm/markdown'
8-
import { Plugin } from '@tiptap/pm/state'
9+
import { Plugin, PluginKey } from '@tiptap/pm/state'
910
import { VueNodeViewRenderer } from '@tiptap/vue-2'
11+
import extractAttachmentSrcs from '../plugins/extractAttachmentSrcs.ts'
1012
import ImageView from './ImageView.vue'
1113

14+
const imageFileDropPluginKey = new PluginKey('imageFileDrop')
15+
const imageExtractAttachmentsKey = new PluginKey('imageExtractAttachments')
16+
1217
const Image = TiptapImage.extend({
1318
selectable: false,
1419

@@ -41,6 +46,7 @@ const Image = TiptapImage.extend({
4146
addProseMirrorPlugins() {
4247
return [
4348
new Plugin({
49+
key: imageFileDropPluginKey,
4450
props: {
4551
handleDrop: (view, event, slice) => {
4652
// only catch the drop if it contains files
@@ -82,6 +88,31 @@ const Image = TiptapImage.extend({
8288
},
8389
},
8490
}),
91+
new Plugin({
92+
key: imageExtractAttachmentsKey,
93+
state: {
94+
init(_, { doc }) {
95+
const attachmentSrcs = extractAttachmentSrcs(doc)
96+
emit('text:editor:attachments:updated', { attachmentSrcs })
97+
return { attachmentSrcs }
98+
},
99+
apply(tr, value, _oldState, newState) {
100+
if (!tr.docChanged) {
101+
return value
102+
}
103+
const attachmentSrcs = extractAttachmentSrcs(newState.doc)
104+
if (
105+
JSON.stringify(attachmentSrcs)
106+
=== JSON.stringify(value?.attachmentSrcs)
107+
) {
108+
return value
109+
}
110+
111+
emit('text:editor:attachments:updated', { attachmentSrcs })
112+
return { attachmentSrcs }
113+
},
114+
},
115+
}),
85116
]
86117
},
87118

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { Node } from '@tiptap/pm/model'
7+
8+
/**
9+
* Extract attachment src attributes from doc
10+
*
11+
* @param doc - the prosemirror doc
12+
* @return src attributes of attachments found in the doc
13+
*/
14+
export default function extractAttachmentSrcs(doc: Node) {
15+
const attachmentSrcs: string[] = []
16+
17+
doc.descendants((node) => {
18+
if (node.type.name !== 'image' && node.type.name !== 'imageInline') {
19+
return
20+
}
21+
22+
// ignore empty src
23+
if (!node.attrs.src) {
24+
return
25+
}
26+
27+
attachmentSrcs.push(node.attrs.src)
28+
})
29+
30+
return attachmentSrcs
31+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import Image from '../../nodes/Image.js'
7+
import extractAttachmentSrcs from '../../plugins/extractAttachmentSrcs.ts'
8+
import createCustomEditor from '../testHelpers/createCustomEditor.ts'
9+
10+
describe('extractAttachmentSrcs', () => {
11+
it('returns an empty array for an empty doc', () => {
12+
const doc = prepareDoc('')
13+
const attachmentSrcs = extractAttachmentSrcs(doc)
14+
expect(attachmentSrcs).toEqual([])
15+
})
16+
17+
it('returns headings', () => {
18+
const content =
19+
'<figure><img src=".attachments.123/test.pdf"></figure><br><figure><img src=".attachments.456/test2.png"></figure>'
20+
const doc = prepareDoc(content)
21+
const attachmentSrcs = extractAttachmentSrcs(doc)
22+
expect(attachmentSrcs).toEqual([
23+
'.attachments.123/test.pdf',
24+
'.attachments.456/test2.png',
25+
])
26+
})
27+
28+
it('ignores an empty src', () => {
29+
const content = '<img>'
30+
const doc = prepareDoc(content)
31+
const attachmentSrcs = extractAttachmentSrcs(doc)
32+
expect(attachmentSrcs).toEqual([])
33+
})
34+
})
35+
36+
const prepareDoc = (content) => {
37+
const editor = createCustomEditor(content, [Image])
38+
const doc = editor.state.doc
39+
editor.destroy()
40+
return doc
41+
}

0 commit comments

Comments
 (0)