Skip to content

Commit 46beadb

Browse files
fix: add relationship and rId (#776)
1 parent f57ea0f commit 46beadb

6 files changed

Lines changed: 161 additions & 46 deletions

File tree

packages/super-editor/src/core/super-converter/docx-helpers/document-rels.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ export const insertNewRelationship = (target, type, editor) => {
138138
},
139139
};
140140

141+
if (type === 'hyperlink') {
142+
newRel.attributes.TargetMode = 'External';
143+
}
144+
141145
// Insert the new relationship
142146
relationshipsTag.elements.push(newRel);
143147

packages/super-editor/src/core/super-converter/docx-helpers/document-rels.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ describe('insertNewRelationship', () => {
308308
Id: 'rId43',
309309
Type: RELATIONSHIP_TYPES.hyperlink,
310310
Target: 'bar',
311+
TargetMode: 'External',
311312
},
312313
});
313314
});
Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { ImagePlaceholderPluginKey, findPlaceholder } from './imagePlaceholderPlugin.js';
22
import { handleImageUpload as handleImageUploadDefault } from './handleImageUpload.js';
33
import { processUploadedImage } from './processUploadedImage.js';
4+
import { insertNewRelationship } from '@core/super-converter/docx-helpers/document-rels.js';
45

56
export const startImageUpload = async ({ editor, view, file }) => {
6-
// Handler from config or default
7-
let imageUploadHandler =
7+
const imageUploadHandler =
88
typeof editor.options.handleImageUpload === 'function'
99
? editor.options.handleImageUpload
1010
: handleImageUploadDefault;
1111

12-
let fileSizeMb = (file.size / (1024 * 1024)).toFixed(4);
12+
let fileSizeMb = Number((file.size / (1024 * 1024)).toFixed(4));
1313

1414
if (fileSizeMb > 5) {
1515
window.alert('Image size must be less than 5MB');
@@ -29,6 +29,16 @@ export const startImageUpload = async ({ editor, view, file }) => {
2929
return;
3030
}
3131

32+
await uploadImage({
33+
editor,
34+
view,
35+
file,
36+
size: { width, height },
37+
uploadHandler: imageUploadHandler,
38+
});
39+
};
40+
41+
export async function uploadImage({ editor, view, file, size, uploadHandler }) {
3242
// A fresh object to act as the ID for this upload
3343
let id = {};
3444

@@ -52,45 +62,63 @@ export const startImageUpload = async ({ editor, view, file }) => {
5262
tr.setMeta(ImagePlaceholderPluginKey, imageMeta);
5363
view.dispatch(tr);
5464

55-
imageUploadHandler(file).then(
56-
(url) => {
57-
let fileName = file.name.replace(' ', '_');
58-
let placeholderPos = findPlaceholder(view.state, id);
59-
60-
// If the content around the placeholder has been deleted,
61-
// drop the image
62-
if (placeholderPos == null) {
63-
return;
64-
}
65-
66-
// Otherwise, insert it at the placeholder's position, and remove
67-
// the placeholder
68-
let removeMeta = { type: 'remove', id };
69-
70-
let mediaPath = `word/media/${fileName}`;
71-
let imageNode = schema.nodes.image.create({
72-
src: mediaPath,
73-
size: { width, height },
74-
});
75-
76-
editor.storage.image.media = Object.assign(editor.storage.image.media, { [mediaPath]: url });
77-
78-
// If we are in collaboration, we need to share the image with other clients
79-
if (editor.options.ydoc) {
80-
editor.commands.addImageToCollaboration({ mediaPath, fileData: url });
81-
}
82-
83-
view.dispatch(
84-
view.state.tr
85-
.replaceWith(placeholderPos, placeholderPos, imageNode) // or .insert(placeholderPos, imageNode)
86-
.setMeta(ImagePlaceholderPluginKey, removeMeta),
87-
);
88-
},
89-
() => {
90-
let removeMeta = { type: 'remove', id };
91-
92-
// On failure, just clean up the placeholder
93-
view.dispatch(tr.setMeta(ImagePlaceholderPluginKey, removeMeta));
94-
},
95-
);
96-
};
65+
try {
66+
let url = await uploadHandler(file);
67+
68+
let fileName = file.name.replace(' ', '_');
69+
let placeholderPos = findPlaceholder(view.state, id);
70+
71+
// If the content around the placeholder has been deleted,
72+
// drop the image
73+
if (placeholderPos == null) {
74+
return;
75+
}
76+
77+
// Otherwise, insert it at the placeholder's position, and remove
78+
// the placeholder
79+
let removeMeta = { type: 'remove', id };
80+
81+
let mediaPath = `word/media/${fileName}`;
82+
83+
let rId = null;
84+
if (editor.options.mode === 'docx') {
85+
const [, path] = mediaPath.split('word/'); // Path without 'word/' part.
86+
const id = addImageRelationship({ editor, path });
87+
if (id) rId = id;
88+
}
89+
90+
let imageNode = schema.nodes.image.create({
91+
src: mediaPath,
92+
size,
93+
rId,
94+
});
95+
96+
editor.storage.image.media = Object.assign(editor.storage.image.media, { [mediaPath]: url });
97+
98+
// If we are in collaboration, we need to share the image with other clients
99+
if (editor.options.ydoc) {
100+
editor.commands.addImageToCollaboration({ mediaPath, fileData: url });
101+
}
102+
103+
view.dispatch(
104+
view.state.tr
105+
.replaceWith(placeholderPos, placeholderPos, imageNode) // or .insert(placeholderPos, imageNode)
106+
.setMeta(ImagePlaceholderPluginKey, removeMeta),
107+
);
108+
} catch {
109+
let removeMeta = { type: 'remove', id };
110+
// On failure, just clean up the placeholder
111+
view.dispatch(tr.setMeta(ImagePlaceholderPluginKey, removeMeta));
112+
}
113+
}
114+
115+
function addImageRelationship({ editor, path }) {
116+
const target = path;
117+
const type = 'image';
118+
try {
119+
const id = insertNewRelationship(target, type, editor);
120+
return id;
121+
} catch {
122+
return null;
123+
}
124+
}

packages/super-editor/src/extensions/link/link.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import { Mark, Attribute } from '@core/index.js';
1414
import { getMarkRange } from '@core/helpers/getMarkRange.js';
15+
import { insertNewRelationship } from '@core/super-converter/docx-helpers/document-rels.js';
1516

1617
/**
1718
* @module Link
@@ -134,7 +135,15 @@ export const Link = Mark.create({
134135
if (underlineMarkType) tr = tr.removeMark(from, to, underlineMarkType);
135136

136137
if (underlineMarkType) tr = tr.addMark(from, to, underlineMarkType.create());
137-
tr = tr.addMark(from, to, linkMarkType.create({ href, text: finalText }));
138+
139+
let rId = null;
140+
if (editor.options.mode === 'docx') {
141+
const id = addLinkRelationship({ editor, href });
142+
if (id) rId = id;
143+
}
144+
145+
const newLinkMarkType = linkMarkType.create({ href, text: finalText, rId });
146+
tr = tr.addMark(from, to, newLinkMarkType);
138147

139148
dispatch(tr.scrollIntoView());
140149
return true;
@@ -239,3 +248,14 @@ const trimRange = (doc, from, to) => {
239248
// starting and ending without doc specific whitespace
240249
return { from, to };
241250
};
251+
252+
function addLinkRelationship({ editor, href }) {
253+
const target = href;
254+
const type = 'hyperlink';
255+
try {
256+
const id = insertNewRelationship(target, type, editor);
257+
return id;
258+
} catch {
259+
return null;
260+
}
261+
}

packages/super-editor/src/tests/editor/data/imageBase64.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { loadTestDataForEditorTests, initTestEditor } from '@tests/helpers/helpers.js';
2+
import { TextSelection } from 'prosemirror-state';
3+
import { expect } from 'vitest';
4+
import { getDocumentRelationshipElements } from '@core/super-converter/docx-helpers/document-rels.js';
5+
import { uploadImage } from '@extensions/image/imageHelpers/startImageUpload.js';
6+
import { handleImageUpload as handleImageUploadDefault } from '@extensions/image/imageHelpers/handleImageUpload.js';
7+
import { imageBase64 } from './data/imageBase64.js';
8+
9+
describe('Relationships tests', () => {
10+
window.URL.createObjectURL = vi.fn().mockImplementation((file) => {
11+
return file.name;
12+
});
13+
14+
const filename = 'blank-doc.docx';
15+
let docx, media, mediaFiles, fonts, editor;
16+
17+
beforeAll(async () => ({ docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(filename)));
18+
beforeEach(() => ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts })));
19+
20+
it('tests that the inserted link has a rId and a relationship', () => {
21+
editor.commands.insertContentAt(0, 'link');
22+
23+
editor.view.dispatch(editor.state.tr.setSelection(TextSelection.create(editor.state.doc, 0, 5)));
24+
editor.commands.setLink({ href: 'https://www.superdoc.dev' });
25+
26+
const linkMark = editor.state.doc.firstChild.firstChild.marks[0];
27+
28+
expect(linkMark.type.name).toBe('link');
29+
expect(linkMark.attrs.rId).toBeTruthy();
30+
31+
const relationships = getDocumentRelationshipElements(editor);
32+
const found = relationships.find((i) => i.attributes.Id === linkMark.attrs.rId);
33+
34+
expect(found).toBeTruthy();
35+
expect(found.attributes.Target).toBe('https://www.superdoc.dev');
36+
});
37+
38+
it('tests that the uploaded image has a rId and a relationship', async () => {
39+
const blob = await fetch(imageBase64).then((res) => res.blob());
40+
const file = new File([blob], 'image.png', { type: 'image/png' });
41+
42+
await uploadImage({
43+
editor,
44+
view: editor.view,
45+
file,
46+
size: { width: 100, height: 100 },
47+
uploadHandler: handleImageUploadDefault,
48+
});
49+
50+
const imageNode = editor.state.doc.firstChild.firstChild;
51+
52+
expect(imageNode.type.name).toBe('image');
53+
expect(imageNode.attrs.rId).toBeTruthy();
54+
55+
const relationships = getDocumentRelationshipElements(editor);
56+
const found = relationships.find((i) => i.attributes.Id === imageNode.attrs.rId);
57+
58+
expect(found).toBeTruthy();
59+
expect(found.attributes.Target).toBe('media/image.png');
60+
});
61+
});

0 commit comments

Comments
 (0)