Skip to content

Commit 4fa09c2

Browse files
authored
Merge pull request #481 from Harbour-Enterprises/artem-HAR-9460
HAR-9460 - handle shape textbox
2 parents cba5771 + 9a2a6b3 commit 4fa09c2

15 files changed

Lines changed: 376 additions & 6 deletions

File tree

packages/super-editor/src/assets/styles/extensions/pagination.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,19 @@
4040
flex-direction: column;
4141
background-color: white;
4242
}
43+
44+
/**
45+
Workaround to display pagination in footer
46+
on the right if it is inside shape textbox.
47+
*/
48+
.pagination-section-footer .pm-shape-container:has(
49+
[data-id="auto-page-number"],
50+
[data-id="auto-total-pages"]
51+
) {
52+
margin-left: auto;
53+
}
54+
55+
.pagination-section-header img[contenteditable="false"],
56+
.pagination-section-footer img[contenteditable="false"] {
57+
pointer-events: none;
58+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class SuperConverter {
5454
{ name: 'w:spacing', type: 'lineHeight', mark: 'textStyle', property: 'lineHeight' },
5555
{ name: 'link', type: 'link', mark: 'link', property: 'href' },
5656
{ name: 'w:highlight', type: 'highlight', mark: 'highlight', property: 'color' },
57-
// { name: 'w:shd', type: 'highlight', mark: 'highlight', property: 'color'}
57+
{ name: 'w:shd', type: 'highlight', mark: 'highlight', property: 'color'}
5858
];
5959

6060
static propertyTypes = Object.freeze({

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export function exportSchemaToJson(params) {
8888
commentRangeStart: () => translateCommentNode(params, 'Start'),
8989
commentRangeEnd: () => translateCommentNode(params, 'End'),
9090
commentReference: () => null,
91+
shapeContainer: translateShapeContainer,
92+
shapeTextbox: translateShapeTextbox,
9193
};
9294

9395
if (!router[type]) {
@@ -1815,6 +1817,65 @@ export function translateHardBreak() {
18151817
};
18161818
}
18171819

1820+
function translateShapeContainer(params) {
1821+
const { node } = params;
1822+
const elements = translateChildNodes(params);
1823+
1824+
const shape = {
1825+
name: 'v:shape',
1826+
attributes: {
1827+
...node.attrs.attributes,
1828+
fillcolor: node.attrs.fillcolor,
1829+
},
1830+
elements: [
1831+
...elements,
1832+
...(
1833+
node.attrs.wrapAttributes
1834+
? [{
1835+
name: 'w10:wrap',
1836+
attributes: { ...node.attrs.wrapAttributes },
1837+
}]
1838+
: []
1839+
),
1840+
],
1841+
};
1842+
1843+
const pict = {
1844+
name: 'w:pict',
1845+
attributes: {
1846+
'w14:anchorId': Math.floor(Math.random() * 0xffffffff).toString(),
1847+
},
1848+
elements: [shape],
1849+
};
1850+
1851+
const par = {
1852+
name: 'w:p',
1853+
elements: [wrapTextInRun(pict)],
1854+
};
1855+
1856+
return par;
1857+
}
1858+
1859+
function translateShapeTextbox(params) {
1860+
const { node } = params;
1861+
const elements = translateChildNodes(params);
1862+
1863+
const textboxContent = {
1864+
name: 'w:txbxContent',
1865+
elements,
1866+
};
1867+
1868+
const textbox = {
1869+
name: 'v:textbox',
1870+
attributes: {
1871+
...node.attrs.attributes,
1872+
},
1873+
elements: [textboxContent],
1874+
};
1875+
1876+
return textbox;
1877+
}
1878+
18181879
export class DocxExporter {
18191880
constructor(converter) {
18201881
this.converter = converter;

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
@@ -16,6 +16,7 @@ import { bookmarkNodeHandlerEntity } from './bookmarkNodeImporter.js';
1616
import { autoPageHandlerEntity, autoTotalPageCountEntity } from './autoPageNumberImporter.js';
1717
import { tabNodeEntityHandler } from './tabImporter.js';
1818
import { listHandlerEntity } from './listImporter.js';
19+
import { pictNodeHandlerEntity } from './pictNodeImporter.js';
1920
import { importCommentData } from './documentCommentsImporter.js';
2021
import { getDefaultStyleDefinition } from './paragraphNodeImporter.js';
2122
import { baseNumbering } from '../exporter/helpers/base-list.definitions.js';
@@ -121,6 +122,7 @@ export const createDocumentJson = (docx, converter, editor) => {
121122
export const defaultNodeListHandler = () => {
122123
const entities = [
123124
runNodeHandlerEntity,
125+
pictNodeHandlerEntity,
124126
paragraphNodeHandlerEntity,
125127
listHandlerEntity,
126128
textNodeHandlerEntity,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ function getLineHeightValue(attributes) {
186186
}
187187

188188
function getHighLightValue(attributes) {
189-
if (attributes['w:fill']) return `#${attributes['w:fill']}`;
189+
const fill = attributes['w:fill'];
190+
if (fill && fill !== 'auto') return `#${fill}`;
190191
if (isValidHexColor(attributes?.['w:val'])) return `#${attributes['w:val']}`;
191192
return getHexColorFromDocxSystem(attributes?.['w:val']) || null;
192193
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,16 @@ export const handleParagraphNode = (params) => {
5454
const framePr = pPr?.elements?.find((el) => el.name === 'w:framePr');
5555

5656
if (nestedRPr) {
57-
schemaNode.attrs.marksAttrs = parseMarks(nestedRPr, []);
57+
let marks = parseMarks(nestedRPr, []);
58+
59+
if (!schemaNode.content?.length) {
60+
let highlightIndex = marks?.findIndex((i) => i.type === 'highlight');
61+
if (highlightIndex !== -1) {
62+
marks.splice(highlightIndex, 1)
63+
}
64+
}
65+
66+
schemaNode.attrs.marksAttrs = marks;
5867
}
5968

6069
if (styleTag) {
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { handleParagraphNode } from './paragraphNodeImporter.js';
2+
import { defaultNodeListHandler } from './docxImporter.js';
3+
4+
export const handlePictNode = (params) => {
5+
const { nodes } = params;
6+
7+
if (!nodes.length || nodes[0].name !== 'w:p') {
8+
return { nodes: [], consumed: 0 };
9+
}
10+
11+
const [pNode] = nodes;
12+
const run = pNode.elements?.find((el) => el.name === 'w:r');
13+
const pict = run?.elements?.find((el) => el.name === 'w:pict');
14+
15+
// if there is no pict, then process as a paragraph or list.
16+
if (!pict) {
17+
return { nodes: [], consumed: 0 };
18+
}
19+
20+
const node = pict;
21+
const shape = node.elements?.find((el) => el.name === 'v:shape');
22+
const shapetype = node.elements?.find((el) => el.name === 'v:shapetype');
23+
const group = node.elements?.find((el) => el.name === 'v:group');
24+
25+
// such a case probably shouldn't exist.
26+
if (!shape && !group) {
27+
return { nodes: [], consumed: 0 };
28+
}
29+
30+
let result = null;
31+
32+
const isGroup = group && !shape;
33+
34+
if (isGroup) {
35+
// there should be a group of shapes being processed here (skip for now).
36+
result = null;
37+
} else {
38+
const textbox = shape.elements?.find((el) => el.name === 'v:textbox');
39+
40+
// process shapes with textbox.
41+
if (textbox) {
42+
result = handleShapTextboxImport({
43+
pict,
44+
pNode,
45+
shape,
46+
params,
47+
});
48+
}
49+
}
50+
51+
return { nodes: result ? [result] : [], consumed: 1 };
52+
};
53+
54+
export function handleShapTextboxImport({
55+
pict,
56+
pNode,
57+
shape,
58+
params,
59+
}) {
60+
const schemaAttrs = {};
61+
const schemaTextboxAttrs = {};
62+
63+
const shapeAttrs = shape.attributes || {};
64+
65+
schemaAttrs.attributes = shapeAttrs;
66+
67+
if (shapeAttrs.fillcolor) {
68+
schemaAttrs.fillcolor = shapeAttrs.fillcolor;
69+
}
70+
71+
const parsedStyle = parseInlineStyles(shapeAttrs.style);
72+
const shapeStyle = buildStyles(parsedStyle);
73+
74+
if (shapeStyle) {
75+
schemaAttrs.style = shapeStyle;
76+
}
77+
78+
const textbox = shape.elements?.find((el) => el.name === 'v:textbox');
79+
const wrap = shape.elements?.find((el) => el.name === 'w10:wrap');
80+
81+
if (wrap?.attributes) {
82+
schemaAttrs.wrapAttributes = wrap.attributes;
83+
}
84+
85+
if (textbox?.attributes) {
86+
schemaTextboxAttrs.attributes = textbox.attributes;
87+
}
88+
89+
const textboxContent = textbox?.elements?.find((el) => el.name === 'w:txbxContent');
90+
const textboxContentElems = textboxContent?.elements || [];
91+
92+
const content = textboxContentElems.map((elem) => handleParagraphNode({
93+
nodes: [elem],
94+
docx: params.docx,
95+
nodeListHandler: defaultNodeListHandler(),
96+
}));
97+
const contentNodes = content.reduce((acc, current) => (
98+
[...acc, ...current.nodes]
99+
), []);
100+
101+
const shapeTextbox = {
102+
type: 'shapeTextbox',
103+
attrs: schemaTextboxAttrs,
104+
content: contentNodes,
105+
};
106+
107+
const shapeContainer = {
108+
type: 'shapeContainer',
109+
attrs: schemaAttrs,
110+
content: [shapeTextbox],
111+
};
112+
113+
return shapeContainer;
114+
}
115+
116+
function parseInlineStyles(styleString) {
117+
if (!styleString) return {};
118+
return styleString
119+
.split(';')
120+
.filter((style) => !!style.trim())
121+
.reduce((acc, style) => {
122+
const [prop, value] = style.split(':').map((str) => str.trim());
123+
if (prop && value) acc[prop] = value;
124+
return acc;
125+
}, {});
126+
}
127+
128+
function buildStyles(styleObject) {
129+
const allowed = [
130+
'width',
131+
'height',
132+
133+
// these styles should probably work relative to the page,
134+
// since in the doc it is positioned absolutely.
135+
// 'margin-left',
136+
// 'margin-right',
137+
138+
// causes pagination issues.
139+
// 'margin-top',
140+
// 'margin-bottom',
141+
142+
// styleObject - also contains other word styles (mso-).
143+
];
144+
145+
let style = '';
146+
for (const [prop, value] of Object.entries(styleObject)) {
147+
if (allowed.includes(prop)) {
148+
style += `${prop}: ${value};`;
149+
}
150+
}
151+
152+
return style;
153+
}
154+
155+
export const pictNodeHandlerEntity = {
156+
handlerName: 'handlePictNode',
157+
handler: handlePictNode,
158+
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ export const Image = Node.create({
7777
},
7878

7979
style: {
80-
default: {},
80+
default: null,
8181
rendered: true,
8282
renderDOM: ({ style }) => {
83+
if (!style) return {};
8384
return { style };
8485
},
8586
},

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { Image } from './image/index.js';
3434
import { BookmarkStart } from './bookmarks/index.js';
3535
import { Mention } from './mention/index.js';
3636
import { PageNumber, TotalPageCount } from './page-number/index.js';
37+
import { ShapeContainer } from './shape-container/index.js';
38+
import { ShapeTextbox } from './shape-textbox/index.js';
3739

3840
// Marks extensions
3941
import { TextStyle } from './text-style/text-style.js';
@@ -155,6 +157,8 @@ const getStarterExtensions = () => {
155157
AiAnimationMark,
156158
PageNumber,
157159
TotalPageCount,
160+
ShapeContainer,
161+
ShapeTextbox,
158162
];
159163
};
160164

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,9 @@ function generateInternalPageBreaks(doc, view, editor, sectionData) {
406406
const sectionContainer = decoration.type.toDOM;
407407
const totalPageNumber = sectionContainer?.querySelector('span[data-id="auto-total-pages"]');
408408
if (totalPageNumber) {
409+
const fontSize = totalPageNumber.previousElementSibling?.style?.fontSize ||
410+
totalPageNumber.nextElementSibling?.style?.fontSize;
411+
if (fontSize) totalPageNumber.style.fontSize = fontSize;
409412
totalPageNumber.innerText = currentPageNumber;
410413
};
411414
});
@@ -484,7 +487,12 @@ function createFooter(pageMargins, pageSize, sectionData, footerId, currentPageN
484487
if (!sectionContainer) sectionContainer = document.createElement('div');
485488

486489
const autoPageNumber = sectionContainer?.querySelector('span[data-id="auto-page-number"]');
487-
if (autoPageNumber) autoPageNumber.innerText = currentPageNumber;
490+
if (autoPageNumber) {
491+
const fontSize = autoPageNumber.previousElementSibling?.style?.fontSize ||
492+
autoPageNumber.nextElementSibling?.style?.fontSize;
493+
if (fontSize) autoPageNumber.style.fontSize = fontSize;
494+
autoPageNumber.innerText = currentPageNumber;
495+
}
488496

489497
sectionContainer.className = 'pagination-section-footer';
490498
sectionContainer.style.height = footerHeight + 'px';

0 commit comments

Comments
 (0)