Skip to content

Commit 0fb9de7

Browse files
committed
HAR-9435 Anchor images
1 parent 118be61 commit 0fb9de7

3 files changed

Lines changed: 157 additions & 2 deletions

File tree

packages/super-editor/src/core/super-converter/v2/importer/imageImporter.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function handleImageImport(node, currentFileName, params) {
5757
const shapeURI = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
5858
if (!!uri && uri === shapeURI) {
5959
return handleShapeDrawing(params, node, graphicData);
60-
};
60+
}
6161

6262
const picture = graphicData.elements.find((el) => el.name === 'pic:pic');
6363
if (!picture || !picture.elements) return null;
@@ -68,11 +68,26 @@ export function handleImageImport(node, currentFileName, params) {
6868
const positionHTag = node.elements.find((el) => el.name === 'wp:positionH');
6969
const positionH = positionHTag?.elements.find((el) => el.name === 'wp:posOffset');
7070
const positionHValue = emuToPixels(positionH?.elements[0]?.text);
71+
const hRelativeFrom = positionHTag?.attributes.relativeFrom;
72+
const alignH = positionHTag?.elements.find((el) => el.name === 'wp:align')?.elements[0]?.text;
7173

7274
const positionVTag = node.elements.find((el) => el.name === 'wp:positionV');
7375
const positionV = positionVTag?.elements.find((el) => el.name === 'wp:posOffset');
7476
const positionVValue = emuToPixels(positionV?.elements[0]?.text);
77+
const vRelativeFrom = positionVTag?.attributes.relativeFrom;
78+
const alignV = positionVTag?.elements.find((el) => el.name === 'wp:align')?.elements[0]?.text;
79+
80+
let anchorData = null;
81+
if (hRelativeFrom || alignH || vRelativeFrom || alignV) {
82+
anchorData = {
83+
hRelativeFrom,
84+
vRelativeFrom,
85+
alignH,
86+
alignV,
87+
};
88+
}
7589

90+
7691
const marginOffset = {
7792
left: positionHValue,
7893
top: positionVValue,
@@ -101,6 +116,7 @@ export function handleImageImport(node, currentFileName, params) {
101116
padding,
102117
marginOffset,
103118
size,
119+
anchorData,
104120
originalPadding: {
105121
distT: attributes['distT'],
106122
distB: attributes['distB'],

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Node, Attribute } from '@core/index.js';
22
import { ImagePlaceholderPlugin } from './imageHelpers/imagePlaceholderPlugin.js';
3+
import { ImagePositionPlugin } from './imageHelpers/imagePositionPlugin.js';
34

45
export const Image = Node.create({
56
name: 'image',
@@ -54,6 +55,11 @@ export const Image = Node.create({
5455
rendered: false,
5556
},
5657

58+
anchorData: {
59+
default: null,
60+
rendered: false,
61+
},
62+
5763
size: {
5864
default: {},
5965
renderDOM: ({ size }) => {
@@ -64,6 +70,19 @@ export const Image = Node.create({
6470
return { style };
6571
},
6672
},
73+
74+
padding: {
75+
default: {},
76+
renderDOM: ({ padding, marginOffset }) => {
77+
let { left = 0, top = 0, bottom = 0, right = 0 } = padding ?? {};
78+
let style = '';
79+
if (left && !marginOffset?.left) style += `margin-left: ${left}px;`;
80+
if (top && !marginOffset?.top) style += `margin-top: ${top}px;`;
81+
if (bottom) style += `margin-bottom: ${bottom}px;`;
82+
if (right) style += `margin-right: ${right}px;`;
83+
return { style };
84+
},
85+
},
6786

6887
marginOffset: {
6988
default: {},
@@ -113,6 +132,6 @@ export const Image = Node.create({
113132
},
114133

115134
addPmPlugins() {
116-
return [ImagePlaceholderPlugin()];
135+
return [ImagePlaceholderPlugin(), ImagePositionPlugin({editor: this.editor })];
117136
},
118137
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Plugin, PluginKey } from 'prosemirror-state';
2+
import { Decoration, DecorationSet } from 'prosemirror-view';
3+
import { PaginationPluginKey } from '../../pagination/pagination-helpers.js';
4+
5+
const ImagePositionPluginKey = new PluginKey('ImagePosition');
6+
export const ImagePositionPlugin = ({ editor }) => {
7+
const { view } = editor;
8+
let shouldUpdate = false;
9+
return new Plugin({
10+
name: 'ImagePositionPlugin',
11+
key: ImagePositionPluginKey,
12+
13+
state: {
14+
init(_, state) {
15+
return DecorationSet.empty;
16+
},
17+
18+
apply(tr, oldDecorationSet, oldState, newState) {
19+
const decorations = getImagePositionDecorations(newState, view);
20+
return DecorationSet.create(newState.doc, decorations);
21+
},
22+
},
23+
24+
view: (view) => {
25+
return {
26+
update: (view, lastState) => {
27+
const pagination = PaginationPluginKey.getState(lastState);
28+
if (shouldUpdate) {
29+
shouldUpdate = false;
30+
const decorations = getImagePositionDecorations(lastState, view);
31+
const updateTransaction = view.state.tr.setMeta(
32+
ImagePositionPluginKey,
33+
{ decorations }
34+
);
35+
view.dispatch(updateTransaction);
36+
}
37+
if (pagination.isReadyToInit) {
38+
shouldUpdate = true;
39+
}
40+
},
41+
};
42+
},
43+
44+
props: {
45+
decorations(state) {
46+
return this.getState(state);
47+
},
48+
},
49+
});
50+
};
51+
52+
const getImagePositionDecorations = (state, view) => {
53+
let decorations = [];
54+
state.doc.descendants((node, pos) => {
55+
if (node.attrs.anchorData) {
56+
let style = '';
57+
let className = '';
58+
const { vRelativeFrom, alignH } = node.attrs.anchorData;
59+
const { size, padding } = node.attrs;
60+
61+
const pageBreak = findPreviousDomNodeWithClass(view, pos, 'pagination-break-wrapper');
62+
if (pageBreak) {
63+
switch (alignH) {
64+
case 'left':
65+
style += 'float: left; left: 0; margin-left: 0; ';
66+
break;
67+
case 'right':
68+
style += 'float: right; right: 0; margin-right: 0; ';
69+
break;
70+
case 'center':
71+
style += 'display: block; margin-left: auto; margin-right: auto; ';
72+
break;
73+
}
74+
style += vRelativeFrom === 'margin' ? `position: absolute; top: ${pageBreak?.offsetTop + pageBreak?.offsetHeight}px; ` : '';
75+
76+
if (vRelativeFrom === 'margin') {
77+
const nextPos = view.posAtDOM(pageBreak, 1);
78+
const imageBlock = document.createElement('div');
79+
imageBlock.className = 'anchor-image-placeholder';
80+
imageBlock.style.float = alignH;
81+
imageBlock.style.width = size.width + parseInt(padding[alignH]) + 'px';
82+
imageBlock.style.height = size.height + parseInt(padding.top) + parseInt(padding.bottom) + 'px';
83+
decorations.push(Decoration.widget(nextPos, imageBlock, { key: 'stable-key' }));
84+
}
85+
}
86+
87+
decorations.push(
88+
Decoration.node(pos, pos + node.nodeSize, { style, class: className }),
89+
);
90+
}
91+
});
92+
return decorations;
93+
};
94+
95+
const findPreviousDomNodeWithClass = (view, pos, className) => {
96+
let { node } = view.domAtPos(pos);
97+
98+
// If you get a text node, go to its parent
99+
if (node.nodeType === 3) {
100+
node = node.parentNode;
101+
}
102+
103+
// Walk backward over siblings and their ancestors
104+
while (node) {
105+
if (node.classList && node.classList.contains(className)) {
106+
return node;
107+
}
108+
if (node.previousSibling) {
109+
node = node.previousSibling;
110+
// Dive to the last child if it's an element with children
111+
while (node && node.lastChild) {
112+
node = node.lastChild;
113+
}
114+
} else {
115+
node = node.parentNode;
116+
}
117+
}
118+
119+
return null; // Not found
120+
}

0 commit comments

Comments
 (0)