Skip to content

Commit 3a9c0cb

Browse files
harbournickartem-harbour
authored andcommitted
handle alternate content node
1 parent 6d7fb46 commit 3a9c0cb

8 files changed

Lines changed: 240 additions & 3 deletions

File tree

packages/super-editor/src/core/Editor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ export class Editor extends EventEmitter {
690690
element.style.paddingLeft = pageMargins.left + 'in';
691691
element.style.paddingRight = pageMargins.right + 'in';
692692
element.style.boxSizing = 'border-box';
693+
element.style.isolation = 'isolate'; // to create new stacking context.
693694

694695
proseMirror.style.outline = 'none';
695696
proseMirror.style.border = 'none';

packages/super-editor/src/core/super-converter/exporter.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export function exportSchemaToJson(params) {
9090
commentReference: () => null,
9191
shapeContainer: translateShapeContainer,
9292
shapeTextbox: translateShapeTextbox,
93+
contentBlock: translateContentBlock,
9394
};
9495

9596
if (!router[type]) {
@@ -1876,6 +1877,40 @@ function translateShapeTextbox(params) {
18761877
return textbox;
18771878
}
18781879

1880+
function translateContentBlock(params) {
1881+
const { node } = params;
1882+
const { drawingContent } = node.attrs;
1883+
1884+
const drawing = {
1885+
name: 'w:drawing',
1886+
elements: [
1887+
...(
1888+
drawingContent
1889+
? [...(drawingContent.elements || [])]
1890+
: []
1891+
)
1892+
],
1893+
};
1894+
1895+
const choice = {
1896+
name: 'mc:Choice',
1897+
attributes: { Requires: 'wps' },
1898+
elements: [drawing],
1899+
};
1900+
1901+
const alternateContent = {
1902+
name: 'mc:AlternateContent',
1903+
elements: [choice],
1904+
};
1905+
1906+
const par = {
1907+
name: 'w:p',
1908+
elements: [wrapTextInRun(alternateContent)],
1909+
};
1910+
1911+
return par;
1912+
}
1913+
18791914
export class DocxExporter {
18801915
constructor(converter) {
18811916
this.converter = converter;
@@ -1924,7 +1959,7 @@ export class DocxExporter {
19241959

19251960
if (name === 'w:instrText') {
19261961
tags.push(elements[0].text);
1927-
} else if (name === 'w:t' || name === 'w:delText') {
1962+
} else if (name === 'w:t' || name === 'w:delText' || name === 'wp:posOffset') {
19281963
const text = this.#replaceSpecialCharacters(elements[0].text);
19291964
tags.push(text);
19301965
} else {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @type {import("docxImporter").NodeHandler}
3+
*/
4+
const handleAlternateChoice = (params) => {
5+
const skipHandlerResponse = { nodes: [], consumed: 0 };
6+
const { nodes, nodeListHandler, parentStyleId, docx } = params;
7+
8+
if (nodes.length === 0 || nodes[0].name !== 'w:p') {
9+
return skipHandlerResponse;
10+
}
11+
12+
const mainNode = nodes[0];
13+
const node = mainNode?.elements?.find((el) => el.name === 'w:r');
14+
const hasAltChoice = node?.elements?.some((el) => el.name === 'mc:AlternateContent');
15+
16+
if (!hasAltChoice) {
17+
return skipHandlerResponse;
18+
}
19+
20+
const altChoiceNode = node.elements.find((el) => el.name === 'mc:AlternateContent');
21+
const altChoiceNodeIndex = node.elements.findIndex((el) => el.name === 'mc:AlternateContent');
22+
const allowedNamespaces = ['wps', 'wp14', 'w14', 'w15'];
23+
24+
const wpsNode = altChoiceNode.elements.find(
25+
(el) => el.name === 'mc:Choice' && allowedNamespaces.includes(el.attributes['Requires'])
26+
);
27+
28+
if (!wpsNode) {
29+
return skipHandlerResponse;
30+
}
31+
32+
const contents = wpsNode.elements;
33+
const result = nodeListHandler.handler({
34+
...params,
35+
nodes: contents,
36+
});
37+
38+
return { nodes: result, consumed: 1 };
39+
};
40+
41+
/**
42+
* @type {import("docxImporter").NodeHandlerEntry}
43+
*/
44+
export const alternateChoiceHandler = {
45+
handlerName: 'alternateChoiceHandler',
46+
handler: handleAlternateChoice,
47+
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { annotationNodeHandlerEntity } from './annotationImporter.js';
1313
import { standardNodeHandlerEntity } from './standardNodeImporter.js';
1414
import { lineBreakNodeHandlerEntity } from './lineBreakImporter.js';
1515
import { bookmarkNodeHandlerEntity } from './bookmarkNodeImporter.js';
16+
import { alternateChoiceHandler } from './alternateChoiceImporter.js';
1617
import { autoPageHandlerEntity, autoTotalPageCountEntity } from './autoPageNumberImporter.js';
1718
import { tabNodeEntityHandler } from './tabImporter.js';
1819
import { listHandlerEntity } from './listImporter.js';
@@ -121,6 +122,7 @@ export const createDocumentJson = (docx, converter, editor) => {
121122

122123
export const defaultNodeListHandler = () => {
123124
const entities = [
125+
alternateChoiceHandler,
124126
runNodeHandlerEntity,
125127
pictNodeHandlerEntity,
126128
paragraphNodeHandlerEntity,

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

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import { emuToPixels } from '../../helpers.js';
55
*/
66
export const handleDrawingNode = (params) => {
77
const { nodes, filename } = params;
8-
if (nodes.length === 0 || nodes[0].name !== 'w:drawing') {
8+
9+
const validNodes = ['w:drawing', 'w:p'];
10+
if (nodes.length === 0 || !validNodes.includes(nodes[0].name)) {
911
return { nodes: [], consumed: 0 };
1012
}
11-
const node = nodes[0];
13+
14+
const mainNode = nodes[0];
15+
let node;
16+
17+
if (mainNode.name === 'w:drawing') node = mainNode;
18+
else node = mainNode.elements.find((el) => el.name === 'w:drawing');
19+
20+
if (!node) return { nodes: [], consumed: 0 };
1221

1322
let result;
1423
const { elements } = node;
@@ -22,6 +31,7 @@ export const handleDrawingNode = (params) => {
2231
// Others, wp:inline
2332
const inlineImage = elements.find((el) => el.name === 'wp:inline');
2433
if (inlineImage) result = handleImageImport(inlineImage, currentFileName, params);
34+
2535
return { nodes: result ? [result] : [], consumed: 1 };
2636
};
2737

@@ -43,6 +53,11 @@ export function handleImageImport(node, currentFileName, params) {
4353

4454
const graphic = node.elements.find((el) => el.name === 'a:graphic');
4555
const graphicData = graphic.elements.find((el) => el.name === 'a:graphicData');
56+
const { uri } = graphicData?.attributes;
57+
const shapeURI = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
58+
if (!!uri && uri === shapeURI) {
59+
return handleShapeDrawing(params, node, graphicData);
60+
};
4661

4762
const picture = graphicData.elements.find((el) => el.name === 'pic:pic');
4863
if (!picture || !picture.elements) return null;
@@ -97,6 +112,68 @@ export function handleImageImport(node, currentFileName, params) {
97112
};
98113
}
99114

115+
const handleShapeDrawing = (params, node, graphicData) => {
116+
const wsp = graphicData.elements.find((el) => el.name === 'wps:wsp');
117+
const textBox = wsp.elements.find((el) => el.name === 'wps:txbx');
118+
const textBoxContent = textBox?.elements?.find((el) => el.name === 'w:txbxContent');
119+
120+
const isGraphicContainer = node.elements.find((el) => el.name === 'wp:docPr');
121+
const spPr = wsp.elements.find((el) => el.name === 'wps:spPr');
122+
const prstGeom = spPr?.elements.find((el) => el.name === 'a:prstGeom');
123+
124+
if (!!prstGeom && prstGeom.attributes['prst'] === 'rect') {
125+
return getRectangleShape(params, spPr);
126+
}
127+
128+
if (!textBoxContent) {
129+
return null;
130+
}
131+
132+
const { nodeListHandler } = params;
133+
const translatedElement = nodeListHandler.handler({
134+
...params,
135+
node: textBoxContent.elements[0],
136+
nodes: textBoxContent.elements
137+
});
138+
139+
return translatedElement[0];
140+
};
141+
142+
const getRectangleShape = (params, node) => {
143+
const schemaAttrs = {};
144+
145+
const [drawingNode] = params.nodes;
146+
147+
if (drawingNode?.name === 'w:drawing') {
148+
schemaAttrs.drawingContent = drawingNode;
149+
}
150+
151+
const xfrm = node.elements.find((el) => el.name === 'a:xfrm');
152+
const start = xfrm.elements.find((el) => el.name === 'a:off');
153+
const size = xfrm.elements.find((el) => el.name === 'a:ext');
154+
const outline = node.elements.find((el) => el.name === 'a:ln');
155+
const solidFill = node.elements.find((el) => el.name === 'a:solidFill');
156+
157+
const rectangleSize = {
158+
top: emuToPixels(start.attributes['y']),
159+
left: emuToPixels(start.attributes['x']),
160+
width: emuToPixels(size.attributes['cx']),
161+
height: emuToPixels(size.attributes['cy']),
162+
};
163+
schemaAttrs.size = rectangleSize;
164+
165+
const background = solidFill?.elements[0]?.attributes['val'];
166+
167+
if (background) {
168+
schemaAttrs.background = '#' + background;
169+
}
170+
171+
return {
172+
type: 'contentBlock',
173+
attrs: schemaAttrs,
174+
};
175+
};
176+
100177
/**
101178
* @type {import("docxImporter").NodeHandlerEntry}
102179
*/
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Node, Attribute } from '@core/index.js';
2+
3+
export const ContentBlock = Node.create({
4+
name: 'contentBlock',
5+
6+
group: 'block',
7+
8+
content: '',
9+
10+
isolating: true,
11+
12+
addOptions() {
13+
return {
14+
htmlAttributes: {
15+
style: 'position: absolute; outline: none; user-select: none; z-index: -1;', //
16+
contenteditable: false,
17+
},
18+
};
19+
},
20+
21+
addAttributes() {
22+
return {
23+
size: {
24+
default: null,
25+
renderDOM: ({ size }) => {
26+
if (!size) return {};
27+
28+
let style = '';
29+
if (size.top) style += `top: ${size.top}px; `;
30+
if (size.left) style += `left: ${size.left}px; `;
31+
if (size.width) style += `width: ${size.width}px; `;
32+
if (size.height) style += `height: ${size.height}px; `;
33+
return { style };
34+
},
35+
},
36+
37+
background: {
38+
default: null,
39+
renderDOM: (attrs) => {
40+
if (!attrs.background) return {};
41+
return {
42+
style: `background-color: ${attrs.background}`,
43+
};
44+
},
45+
},
46+
47+
drawingContent: {
48+
rendered: false,
49+
},
50+
51+
attributes: {
52+
rendered: false,
53+
},
54+
};
55+
},
56+
57+
parseDOM() {
58+
return [
59+
{
60+
tag: `div[data-type="${this.name}"]`,
61+
}
62+
];
63+
},
64+
65+
renderDOM({ htmlAttributes }) {
66+
return [
67+
'div',
68+
Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes, { 'data-type': this.name }),
69+
0,
70+
];
71+
},
72+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './content-block.js';

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { Mention } from './mention/index.js';
3636
import { PageNumber, TotalPageCount } from './page-number/index.js';
3737
import { ShapeContainer } from './shape-container/index.js';
3838
import { ShapeTextbox } from './shape-textbox/index.js';
39+
import { ContentBlock } from './content-block/index.js';
3940

4041
// Marks extensions
4142
import { TextStyle } from './text-style/text-style.js';
@@ -159,6 +160,7 @@ const getStarterExtensions = () => {
159160
TotalPageCount,
160161
ShapeContainer,
161162
ShapeTextbox,
163+
ContentBlock,
162164
];
163165
};
164166

0 commit comments

Comments
 (0)