Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,13 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html
display: block;
}

.ProseMirror div[data-type="contentBlock"] {
position: absolute;
outline: none;
user-select: none;
z-index: -1;
}

.super-doc-dropcap {
float: left;
display: flex;
Expand Down
1 change: 1 addition & 0 deletions packages/super-editor/src/core/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ export class Editor extends EventEmitter {
element.style.paddingLeft = pageMargins.left + 'in';
element.style.paddingRight = pageMargins.right + 'in';
element.style.boxSizing = 'border-box';
element.style.isolation = 'isolate'; // to create new stacking context.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context

To make z-index: -1 work for the element. This is probably a safer way than assigning z-index to the editor.


proseMirror.style.outline = 'none';
proseMirror.style.border = 'none';
Expand Down
37 changes: 36 additions & 1 deletion packages/super-editor/src/core/super-converter/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function exportSchemaToJson(params) {
commentReference: () => null,
shapeContainer: translateShapeContainer,
shapeTextbox: translateShapeTextbox,
contentBlock: translateContentBlock,
};

if (!router[type]) {
Expand Down Expand Up @@ -1876,6 +1877,40 @@ function translateShapeTextbox(params) {
return textbox;
}

function translateContentBlock(params) {
const { node } = params;
const { drawingContent } = node.attrs;

const drawing = {
name: 'w:drawing',
elements: [
...(
drawingContent
? [...(drawingContent.elements || [])]
: []
)
],
};

const choice = {
name: 'mc:Choice',
attributes: { Requires: 'wps' },
elements: [drawing],
};

const alternateContent = {
name: 'mc:AlternateContent',
elements: [choice],
};

const par = {
name: 'w:p',
elements: [wrapTextInRun(alternateContent)],
};

return par;
}

export class DocxExporter {
constructor(converter) {
this.converter = converter;
Expand Down Expand Up @@ -1924,7 +1959,7 @@ export class DocxExporter {

if (name === 'w:instrText') {
tags.push(elements[0].text);
} else if (name === 'w:t' || name === 'w:delText') {
} else if (name === 'w:t' || name === 'w:delText' || name === 'wp:posOffset') {
const text = this.#replaceSpecialCharacters(elements[0].text);
tags.push(text);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @type {import("docxImporter").NodeHandler}
*/
const handleAlternateChoice = (params) => {
const skipHandlerResponse = { nodes: [], consumed: 0 };
const { nodes, nodeListHandler, parentStyleId, docx } = params;

if (nodes.length === 0 || nodes[0].name !== 'w:p') {
return skipHandlerResponse;
}

const mainNode = nodes[0];
const node = mainNode?.elements?.find((el) => el.name === 'w:r');
const hasAltChoice = node?.elements?.some((el) => el.name === 'mc:AlternateContent');

if (!hasAltChoice) {
return skipHandlerResponse;
}

const altChoiceNode = node.elements.find((el) => el.name === 'mc:AlternateContent');
const altChoiceNodeIndex = node.elements.findIndex((el) => el.name === 'mc:AlternateContent');
const allowedNamespaces = ['wps', 'wp14', 'w14', 'w15'];

const wpsNode = altChoiceNode.elements.find(
(el) => el.name === 'mc:Choice' && allowedNamespaces.includes(el.attributes['Requires'])
);

if (!wpsNode) {
return skipHandlerResponse;
}

const contents = wpsNode.elements;
const result = nodeListHandler.handler({
...params,
nodes: contents,
});

return { nodes: result, consumed: 1 };
};

/**
* @type {import("docxImporter").NodeHandlerEntry}
*/
export const alternateChoiceHandler = {
handlerName: 'alternateChoiceHandler',
handler: handleAlternateChoice,
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { annotationNodeHandlerEntity } from './annotationImporter.js';
import { standardNodeHandlerEntity } from './standardNodeImporter.js';
import { lineBreakNodeHandlerEntity } from './lineBreakImporter.js';
import { bookmarkNodeHandlerEntity } from './bookmarkNodeImporter.js';
import { alternateChoiceHandler } from './alternateChoiceImporter.js';
import { autoPageHandlerEntity, autoTotalPageCountEntity } from './autoPageNumberImporter.js';
import { tabNodeEntityHandler } from './tabImporter.js';
import { listHandlerEntity } from './listImporter.js';
Expand Down Expand Up @@ -121,6 +122,7 @@ export const createDocumentJson = (docx, converter, editor) => {

export const defaultNodeListHandler = () => {
const entities = [
alternateChoiceHandler,
runNodeHandlerEntity,
pictNodeHandlerEntity,
paragraphNodeHandlerEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import { emuToPixels } from '../../helpers.js';
*/
export const handleDrawingNode = (params) => {
const { nodes, filename } = params;
if (nodes.length === 0 || nodes[0].name !== 'w:drawing') {

const validNodes = ['w:drawing', 'w:p'];
if (nodes.length === 0 || !validNodes.includes(nodes[0].name)) {
return { nodes: [], consumed: 0 };
}
const node = nodes[0];

const mainNode = nodes[0];
let node;

if (mainNode.name === 'w:drawing') node = mainNode;
else node = mainNode.elements.find((el) => el.name === 'w:drawing');

if (!node) return { nodes: [], consumed: 0 };

let result;
const { elements } = node;
Expand All @@ -22,6 +31,7 @@ export const handleDrawingNode = (params) => {
// Others, wp:inline
const inlineImage = elements.find((el) => el.name === 'wp:inline');
if (inlineImage) result = handleImageImport(inlineImage, currentFileName, params);

return { nodes: result ? [result] : [], consumed: 1 };
};

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

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

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

const handleShapeDrawing = (params, node, graphicData) => {
const wsp = graphicData.elements.find((el) => el.name === 'wps:wsp');
const textBox = wsp.elements.find((el) => el.name === 'wps:txbx');
const textBoxContent = textBox?.elements?.find((el) => el.name === 'w:txbxContent');

const isGraphicContainer = node.elements.find((el) => el.name === 'wp:docPr');
const spPr = wsp.elements.find((el) => el.name === 'wps:spPr');
const prstGeom = spPr?.elements.find((el) => el.name === 'a:prstGeom');

if (!!prstGeom && prstGeom.attributes['prst'] === 'rect') {
return getRectangleShape(params, spPr);
}

if (!textBoxContent) {
return null;
}

const { nodeListHandler } = params;
const translatedElement = nodeListHandler.handler({
...params,
node: textBoxContent.elements[0],
nodes: textBoxContent.elements
});

return translatedElement[0];
};

const getRectangleShape = (params, node) => {
const schemaAttrs = {};

const [drawingNode] = params.nodes;

if (drawingNode?.name === 'w:drawing') {
schemaAttrs.drawingContent = drawingNode;
}

const xfrm = node.elements.find((el) => el.name === 'a:xfrm');
const start = xfrm.elements.find((el) => el.name === 'a:off');
const size = xfrm.elements.find((el) => el.name === 'a:ext');
const outline = node.elements.find((el) => el.name === 'a:ln');
const solidFill = node.elements.find((el) => el.name === 'a:solidFill');

const rectangleSize = {
top: emuToPixels(start.attributes['y']),
left: emuToPixels(start.attributes['x']),
width: emuToPixels(size.attributes['cx']),
height: emuToPixels(size.attributes['cy']),
};
schemaAttrs.size = rectangleSize;

const background = solidFill?.elements[0]?.attributes['val'];

if (background) {
schemaAttrs.background = '#' + background;
}

return {
type: 'contentBlock',
attrs: schemaAttrs,
};
};

/**
* @type {import("docxImporter").NodeHandlerEntry}
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Node, Attribute } from '@core/index.js';

export const ContentBlock = Node.create({
name: 'contentBlock',

group: 'block',

content: '',

isolating: true,

addOptions() {
return {
htmlAttributes: {
contenteditable: false,
},
};
},

addAttributes() {
return {
size: {
default: null,
renderDOM: ({ size }) => {
if (!size) return {};

let style = '';
if (size.top) style += `top: ${size.top}px; `;
if (size.left) style += `left: ${size.left}px; `;
if (size.width) style += `width: ${size.width}px; `;
if (size.height) style += `height: ${size.height}px; `;
return { style };
},
},

background: {
default: null,
renderDOM: (attrs) => {
if (!attrs.background) return {};
return {
style: `background-color: ${attrs.background}`,
};
},
},

drawingContent: {
rendered: false,
},

attributes: {
rendered: false,
},
};
},

parseDOM() {
return [
{
tag: `div[data-type="${this.name}"]`,
}
];
},

renderDOM({ htmlAttributes }) {
return [
'div',
Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes, { 'data-type': this.name }),
0,
];
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './content-block.js';
2 changes: 2 additions & 0 deletions packages/super-editor/src/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { Mention } from './mention/index.js';
import { PageNumber, TotalPageCount } from './page-number/index.js';
import { ShapeContainer } from './shape-container/index.js';
import { ShapeTextbox } from './shape-textbox/index.js';
import { ContentBlock } from './content-block/index.js';

// Marks extensions
import { TextStyle } from './text-style/text-style.js';
Expand Down Expand Up @@ -159,6 +160,7 @@ const getStarterExtensions = () => {
TotalPageCount,
ShapeContainer,
ShapeTextbox,
ContentBlock,
];
};

Expand Down
Loading