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 @@ -5,3 +5,4 @@
@import './comments.css';
@import './list-items.css';
@import './noderesizer.css';
@import './document-section.css';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.sd-document-section-block {
background-color: #fafafa;
border: 1px solid #ababab;
border-radius: 4px;
position: relative;
}
.sd-document-section-block-info {
position: absolute;
top: -19px;
left: -1px;
max-width: 100px;
min-width: 0;
height: 18px;
border: 1px solid #ababab;
border-bottom: none;
border-radius: 6px 6px 0 0;
padding: 0 8px;
align-items: center;
font-size: 10px;
display: none;
z-index: 100;
background-color: #fafafa;
}

.sd-document-section-block:hover {
border-radius: 0 4px 4px 4px;
}

.sd-document-section-block:hover .sd-document-section-block-info {
display: flex;
align-items: center;
}

.sd-document-section-block-info span {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
4 changes: 4 additions & 0 deletions packages/super-editor/src/components/slash-menu/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const ICONS = {
cut: scissorsIconSvg,
copy: copyIconSvg,
paste: pasteIconSvg,
addDocumentSection: plusIconSvg,
removeDocumentSection: trashIconSvg,
};

// Table actions constant
Expand All @@ -52,6 +54,8 @@ export const TEXTS = {
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
removeDocumentSection: 'Remove section',
createDocumentSection: 'Create section',
};

export const tableActionsOptions = [
Expand Down
28 changes: 28 additions & 0 deletions packages/super-editor/src/components/slash-menu/menuItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const isModuleEnabled = (editorOptions, moduleName) => {
* requiresSelection?: boolean,
* requiresClipboard?: boolean
* requiresTableParent?: boolean
* requiredSectionParent?: boolean,
* requiresModule?: string
* }>
* }>} Array of menu sections
Expand All @@ -53,6 +54,7 @@ export function getItems(context) {
const { editor, selectedText, trigger, clipboardContent } = context;

const isInTable = selectionHasNodeOrMark(editor.view.state, 'table', { requireEnds: true });
const isInSectionNode = selectionHasNodeOrMark(editor.view.state, 'documentSection', { requireEnds: true });

const sections = [
{
Expand All @@ -74,6 +76,30 @@ export function getItems(context) {
},
],
},
{
id: 'document-sections',
items: [
{
id: 'insert-document-section',
label: TEXTS.createDocumentSection,
icon: ICONS.addDocumentSection,
action: (editor) => {
editor.commands.createDocumentSection();
},
allowedTriggers: [TRIGGERS.click],
},
{
id: 'remove-section',
label: TEXTS.removeDocumentSection,
icon: ICONS.removeDocumentSection,
action: (editor) => {
editor.commands.removeSectionAtSelection();
},
allowedTriggers: [TRIGGERS.click],
requiresSectionParent: true,
},
],
},
{
id: 'general',
items: [
Expand Down Expand Up @@ -189,6 +215,8 @@ export function getItems(context) {
// If the item requires a table parent and there is no table parent, return false
// Or if we are in a table, do not show 'insert table'
if ((item.requiresTableParent && !isInTable) || (item.id === 'insert-table' && isInTable)) return false;
// If the item requires a section parent and there is no section parent, return false
if (item.requiresSectionParent && !isInSectionNode) return false;
return true;
});

Expand Down
19 changes: 15 additions & 4 deletions packages/super-editor/src/core/InputRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,7 @@ export function isWordHtml(html) {
* @returns {Boolean} Returns true if the paste was handled.
*/
export function handleHtmlPaste(html, editor) {
const flatHtml = flattenListsInHtml(html, editor);

const htmlWithPtSizing = convertEmToPt(flatHtml);
const cleanedHtml = sanitizeHtml(htmlWithPtSizing);
const cleanedHtml = htmlHandler(html, editor);
const doc = PMDOMParser.fromSchema(editor.schema).parse(cleanedHtml);

const { dispatch, state } = editor.view;
Expand All @@ -269,6 +266,20 @@ export function handleHtmlPaste(html, editor) {
return true;
}

/**
* Handle HTML content before it is inserted into the editor.
* This function is used to clean and sanitize HTML content,
* converting em units to pt and removing unnecessary tags.
* @param {String} html The HTML string to be processed.
* @param {Editor} editor The editor instance.
* @returns {String} The processed HTML string.
*/
export function htmlHandler(html, editor) {
const flatHtml = flattenListsInHtml(html, editor);
const htmlWithPtSizing = convertEmToPt(flatHtml);
return sanitizeHtml(htmlWithPtSizing);
}

/**
* Process the HTML string to convert em units to pt units in font-size
*
Expand Down
164 changes: 164 additions & 0 deletions packages/super-editor/src/core/helpers/documentSectionHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { DOMSerializer } from 'prosemirror-model';

/**
* Get all sections in the editor document.
* This function traverses the document and collects all nodes of the specified section type.
* @param {Editor} editor - The editor instance to search within.
* @returns {Array} An array of objects containing the node and its position in the document
*/
const getAllSections = (editor) => {
if (!editor) return [];
const type = editor.schema.nodes.documentSection;
if (!type) return [];

const sections = [];
const { state } = editor;
state.doc.descendants((node, pos) => {
if (node.type.name === type.name) {
sections.push({ node, pos });
}
});
return sections;
};

/**
* Export all sections to HTML format.
* This function retrieves all sections in the editor and converts them to HTML.
* @param {Editor} editor - The editor instance containing the sections.
* @returns {Array} An array of objects containing section details and their HTML representation.
*/
export const exportSectionsToHTML = (editor) => {
const sections = getAllSections(editor);

const processedSections = new Set();
const result = [];
sections.forEach(({ node }) => {
const { attrs } = node;
const { id, title, description } = attrs;
if (processedSections.has(id)) return;
processedSections.add(id);

const html = getHTMLFromNode(node, editor);
result.push({
id,
title,
description,
html,
});
});
return result;
};

/**
* Get HTML representation of a ProseMirror node.
* @param {Node} node - The ProseMirror node to convert.
* @param {Editor} editor - The editor instance used for serialization.
* @returns {String} The HTML representation of the node's content.
*/
const getHTMLFromNode = (node, editor) => {
const tempDocument = document.implementation.createHTMLDocument();
const container = tempDocument.createElement('div');
const fragment = DOMSerializer.fromSchema(editor.schema).serializeFragment(node.content);
container.appendChild(fragment);
let html = container.innerHTML;
return html;
};

/**
* Export all sections to JSON format.
* This function retrieves all sections in the editor and converts them to JSON.
* @param {Editor} editor - The editor instance containing the sections.
* @returns {Array} An array of objects containing section details and their JSON representation.
*/
export const exportSectionsToJSON = (editor) => {
const sections = getAllSections(editor);
const processedSections = new Set();
const result = [];
sections.forEach(({ node }) => {
const { attrs } = node;
const { id, title, description } = attrs;
if (processedSections.has(id)) return;
processedSections.add(id);

result.push({
id,
title,
description,
content: node.toJSON(),
});
});
return result;
};

/**
* Get a linked section editor by its ID.
* This function creates a child editor for a specific section, allowing for editing of that section's content.
* @param {String} id - The ID of the section to link to.
* @param {Object} options - Options for the child editor.
* @param {Editor} editor - The parent editor instance.
* @returns {Editor|null} The child editor instance for the linked section, or null if the section is not found.
*/
export const getLinkedSectionEditor = (id, options, editor) => {
const sections = getAllSections(editor);
const section = sections.find((s) => s.node.attrs.id === id);
if (!section) return null;

const child = editor.createChildEditor({
...options,
onUpdate: ({ editor: childEditor, transaction }) => {
const isFromtLinkedParent = transaction.getMeta('fromLinkedParent');
if (isFromtLinkedParent) return; // Prevent feedback loop

// 1. Get updated content from child editor
const updatedContent = childEditor.state.doc.content;

// 2. Find the section node and its position in the parent
const sectionNode = getAllSections(editor)?.find((s) => s.node.attrs.id === id);
if (!sectionNode) return;

const { pos, node } = sectionNode;

// 3. Create a new node with the same type and attrs, but updated content
const newNode = node.type.create(node.attrs, updatedContent, node.marks);

// 4. Replace the old node with the new node in the parent editor
const tr = editor.state.tr.replaceWith(pos, pos + node.nodeSize, newNode);
tr.setMeta('fromLinkedChild', true); // Prevent feedback loop
editor.view.dispatch(tr);
},
});

editor.on('update', ({ transaction }) => {
const isFromLinkedChild = transaction.getMeta('fromLinkedChild');
if (isFromLinkedChild) return; // Prevent feedback loop

const sectionNode = getAllSections(editor)?.find((s) => s.node.attrs.id === id);
if (!sectionNode) return;

// Only update if content is actually different
const sectionContent = sectionNode.node.content;

const json = {
type: 'doc',
content: sectionContent.content.map((node) => node.toJSON()),
};

const childTr = child.state.tr;
childTr.setMeta('fromLinkedParent', true); // Prevent feedback loop
childTr.replaceWith(0, child.state.doc.content.size, child.schema.nodeFromJSON(json));
child.view.dispatch(childTr);
});

return child;
};

/**
* SectionHelpers provides utility functions for working with sections in the editor.
* It includes methods to retrieve all sections and manage section-related data.
*/
export const SectionHelpers = {
getAllSections,
exportSectionsToHTML,
exportSectionsToJSON,
getLinkedSectionEditor,
};
1 change: 1 addition & 0 deletions packages/super-editor/src/core/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './findChildren.js';
export * from './posToDOMRect.js';
export * from './findMark.js';
export * from './isInTable.js';
export * from './documentSectionHelpers.js';
33 changes: 6 additions & 27 deletions packages/super-editor/src/core/super-converter/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import { generateDocxRandomId } from '@helpers/generateDocxRandomId.js';
import { DEFAULT_DOCX_DEFS } from './exporter-docx-defs.js';
import { TrackDeleteMarkName, TrackFormatMarkName, TrackInsertMarkName } from '@extensions/track-changes/constants.js';
import { carbonCopy } from '../utilities/carbonCopy.js';
import { baseBulletList, baseOrderedListDef } from './v2/exporter/helpers/base-list.definitions.js';
import { translateCommentNode } from './v2/exporter/commentsExporter.js';
import { createColGroup } from '@extensions/table/tableHelpers/createColGroup.js';
import { sanitizeHtml } from '../InputRule.js';
import { ListHelpers } from '@helpers/list-numbering-helpers.js';
import { translateChildNodes, baseBulletList, baseOrderedListDef } from './v2/exporter/helpers/index.js';
import { translateDocumentSection } from './v2/exporter/index.js';

/**
* @typedef {Object} ExportParams
Expand Down Expand Up @@ -93,6 +94,7 @@ export function exportSchemaToJson(params) {
shapeTextbox: translateShapeTextbox,
contentBlock: translateContentBlock,
structuredContent: translateStructuredContent,
documentSection: translateDocumentSection,
'page-number': translatePageNumberNode,
'total-page-number': translateTotalPageNumberNode,
};
Expand Down Expand Up @@ -191,11 +193,13 @@ export function translateParagraphNode(params) {
attributes['w:rsidRDefault'] = params.node.attrs.rsidRDefault;
}

return {
const result = {
name: 'w:p',
elements,
attributes,
};

return result;
}

/**
Expand Down Expand Up @@ -375,31 +379,6 @@ function processAttributes(attrs) {
return processedAttrs;
}

/**
* Process child nodes, ignoring any that are not valid
*
* @param {SchemaNode[]} nodes The input nodes
* @returns {XmlReadyNode[]} The processed child nodes
*/
function translateChildNodes(params) {
const { content: nodes } = params.node;
if (!nodes) return [];

const translatedNodes = [];
nodes.forEach((node) => {
let translatedNode = exportSchemaToJson({ ...params, node });

const nodeType = translatedNode?.name || translatedNode?.type;
// if (nodeType !== 'w:sdt') translatedNode = isolateAnnotations(translatedNode);

if (translatedNode instanceof Array) translatedNodes.push(...translatedNode);
else translatedNodes.push(translatedNode);
});

// Filter out any null nodes
return translatedNodes.filter((n) => n);
}

/**
* Process nodes to isolate sdt annotations from simple text nodes
* since having sdt annotation with text run in one paragraph inside table cell
Expand Down
Loading
Loading