diff --git a/packages/super-editor/src/extensions/block-node/block-node.js b/packages/super-editor/src/extensions/block-node/block-node.js new file mode 100644 index 0000000000..a384f0dc47 --- /dev/null +++ b/packages/super-editor/src/extensions/block-node/block-node.js @@ -0,0 +1,184 @@ +import { Extension } from '@core/Extension.js'; +import { helpers } from '@core/index.js'; +import { Plugin, PluginKey } from 'prosemirror-state'; +import { ReplaceStep } from 'prosemirror-transform'; +import { v4 as uuidv4 } from 'uuid'; + +const { findChildren } = helpers; +const SD_BLOCK_ID_ATTRIBUTE_NAME = 'sdBlockId'; +export const BlockNodePluginKey = new PluginKey('blockNodePlugin'); +export const BlockNode = Extension.create({ + name: 'blockNode', + + addCommands() { + return { + replaceBlockNodeById: + (id, contentNode) => + ({ dispatch, tr }) => { + const blockNode = this.editor.helpers.blockNode.getBlockNodeById(id); + if (!blockNode || blockNode.length > 1) { + return false; + } + + if (dispatch) { + let { pos, node } = blockNode[0]; + let newPosFrom = tr.mapping.map(pos); + let newPosTo = tr.mapping.map(pos + node.nodeSize); + + let currentNode = tr.doc.nodeAt(newPosFrom); + if (node.eq(currentNode)) { + tr.replaceWith(newPosFrom, newPosTo, contentNode); + } + } + + return true; + }, + + deleteBlockNodeById: + (id) => + ({ dispatch, tr }) => { + const blockNode = this.editor.helpers.blockNode.getBlockNodeById(id); + if (!blockNode || blockNode.length > 1) { + return false; + } + + if (dispatch) { + let { pos, node } = blockNode[0]; + let newPosFrom = tr.mapping.map(pos); + let newPosTo = tr.mapping.map(pos + node.nodeSize); + + let currentNode = tr.doc.nodeAt(newPosFrom); + if (node.eq(currentNode)) { + tr.delete(newPosFrom, newPosTo); + } + } + + return true; + }, + + updateBlockNodeAttributes: + (id, attrs = {}) => + ({ dispatch, tr }) => { + const blockNode = this.editor.helpers.blockNode.getBlockNodeById(id); + if (!blockNode || blockNode.length > 1) { + return false; + } + if (dispatch) { + let { pos, node } = blockNode[0]; + let newPos = tr.mapping.map(pos); + let currentNode = tr.doc.nodeAt(newPos); + if (node.eq(currentNode)) { + tr.setNodeMarkup(newPos, undefined, { + ...node.attrs, + ...attrs, + }); + } + + return true; + } + }, + }; + }, + + addHelpers() { + return { + getBlockNodes: () => { + return findChildren(this.editor.state.doc, (node) => nodeAllowsSdBlockIdAttr(node)); + }, + + getBlockNodeById: (id) => { + return findChildren(this.editor.state.doc, (node) => node.attrs.sdBlockId === id); + }, + + getBlockNodesByType: (type) => { + return findChildren(this.editor.state.doc, (node) => node.type.name === type); + }, + + getBlockNodesInRange: (from, to) => { + let blockNodes = []; + + this.editor.state.doc.nodesBetween(from, to, (node, pos) => { + if (nodeAllowsSdBlockIdAttr(node)) { + blockNodes.push({ + node, + pos, + }); + } + }); + + return blockNodes; + }, + }; + }, + addPmPlugins() { + let hasInitialized = false; + + return [ + new Plugin({ + key: BlockNodePluginKey, + appendTransaction: (transactions, _oldState, newState) => { + if (hasInitialized && !transactions.some((tr) => tr.docChanged)) return null; + + // Check for new block nodes and if none found, we don't need to do anything + if (hasInitialized && !checkForNewBlockNodesInTrs(transactions)) return null; + + let tr = null; + let changed = false; + newState.doc.descendants((node, pos) => { + // Only allow block nodes with a valid sdBlockId attribute + if (!nodeAllowsSdBlockIdAttr(node) || !nodeNeedsSdBlockId(node)) return null; + + tr = tr ?? newState.tr; + tr.setNodeMarkup( + pos, + undefined, + { + ...node.attrs, + sdBlockId: uuidv4(), + }, + node.marks, + ); + changed = true; + }); + + if (changed && !hasInitialized) hasInitialized = true; + return changed ? tr : null; + }, + }), + ]; + }, +}); + +/** + * Check if a node allows sdBlockId attribute + * @param {import("prosemirror-model").Node} node - The node to check + * @returns {boolean} - True if the node type supports sdBlockId attribute + */ +export const nodeAllowsSdBlockIdAttr = (node) => { + return !!(node?.isBlock && node?.type?.spec?.attrs?.[SD_BLOCK_ID_ATTRIBUTE_NAME]); +}; + +/** + * Check if a node needs an sdBlockId (doesn't have one or has null/empty value) + * @param {import("prosemirror-model").Node} node - The node to check + * @returns {boolean} - True if the node needs an sdBlockId assigned + */ +export const nodeNeedsSdBlockId = (node) => { + const currentId = node?.attrs?.[SD_BLOCK_ID_ATTRIBUTE_NAME]; + return !currentId; +}; + +/** + * Check for new block nodes in ProseMirror transactions. + * Iterate through the list of transactions, and in each tr check if there are any new block nodes. + * @param {Array} transactions - The ProseMirror transactions to check. + * @returns {boolean} - True if new block nodes are found, false otherwise. + */ +export const checkForNewBlockNodesInTrs = (transactions) => { + return transactions.some((tr) => { + return tr.steps.some((step) => { + const hasValidSdBlockNodes = step.slice?.content?.content?.some((node) => nodeAllowsSdBlockIdAttr(node)); + return step instanceof ReplaceStep && hasValidSdBlockNodes; + }); + }); +}; diff --git a/packages/super-editor/src/extensions/block-node/block-node.test.js b/packages/super-editor/src/extensions/block-node/block-node.test.js new file mode 100644 index 0000000000..01bb96162b --- /dev/null +++ b/packages/super-editor/src/extensions/block-node/block-node.test.js @@ -0,0 +1,555 @@ +import { describe, it, expect } from 'vitest'; +import { ReplaceStep } from 'prosemirror-transform'; +import { nodeAllowsSdBlockIdAttr, nodeNeedsSdBlockId, checkForNewBlockNodesInTrs } from './block-node.js'; +import { initTestEditor, loadTestDataForEditorTests } from '../../tests/helpers/helpers.js'; + +// Mock +class OtherStep {} + +describe('block-node: nodeAllowsSdBlockIdAttr', () => { + it('should return true for block nodes with sdBlockId attribute', () => { + const mockNode = { + isBlock: true, + type: { + spec: { + attrs: { + sdBlockId: { + default: null, + keepOnSplit: false, + }, + otherAttr: { default: 'value' }, + }, + }, + }, + }; + + expect(nodeAllowsSdBlockIdAttr(mockNode)).toBe(true); + }); + + it('should return false for inline nodes with sdBlockId attribute', () => { + const mockNode = { + isBlock: false, + type: { + spec: { + attrs: { + sdBlockId: { + default: null, + keepOnSplit: false, + }, + }, + }, + }, + }; + + expect(nodeAllowsSdBlockIdAttr(mockNode)).toBe(false); + }); + + it('should return false for block nodes without sdBlockId attribute', () => { + const mockNode = { + isBlock: true, + type: { + spec: { + attrs: { + otherAttr: { default: 'value' }, + anotherAttr: { default: 'another' }, + }, + }, + }, + }; + + expect(nodeAllowsSdBlockIdAttr(mockNode)).toBe(false); + }); + + it('should return false for block nodes with no attrs spec', () => { + const mockNode = { + isBlock: true, + type: { + spec: {}, + }, + }; + + expect(nodeAllowsSdBlockIdAttr(mockNode)).toBe(false); + }); + + it('should return false for block nodes with null attrs', () => { + const mockNode = { + isBlock: true, + type: { + spec: { + attrs: null, + }, + }, + }; + + expect(nodeAllowsSdBlockIdAttr(mockNode)).toBe(false); + }); + + it('should return false for nodes without type.spec', () => { + const mockNode = { + isBlock: true, + type: {}, + }; + + expect(nodeAllowsSdBlockIdAttr(mockNode)).toBe(false); + }); + + it('should handle undefined/null nodes gracefully', () => { + expect(nodeAllowsSdBlockIdAttr(null)).toBe(false); + expect(nodeAllowsSdBlockIdAttr(undefined)).toBe(false); + expect(nodeAllowsSdBlockIdAttr({})).toBe(false); + }); +}); + +describe('block-node: nodeNeedsSdBlockId', () => { + it('should return true when node has no sdBlockId attribute', () => { + const mockNode = { + attrs: { + otherAttr: 'value', + anotherAttr: 'another', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return true when sdBlockId is null', () => { + const mockNode = { + attrs: { + sdBlockId: null, + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return true when sdBlockId is undefined', () => { + const mockNode = { + attrs: { + sdBlockId: undefined, + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return true when sdBlockId is empty string', () => { + const mockNode = { + attrs: { + sdBlockId: '', + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return true when sdBlockId is 0', () => { + const mockNode = { + attrs: { + sdBlockId: 0, + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return true when sdBlockId is false', () => { + const mockNode = { + attrs: { + sdBlockId: false, + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return false when sdBlockId has a valid string value', () => { + const mockNode = { + attrs: { + sdBlockId: 'block-id-123', + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(false); + }); + + it('should return false when sdBlockId has a valid numeric value', () => { + const mockNode = { + attrs: { + sdBlockId: 42, + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(false); + }); + + it('should return false when sdBlockId is true', () => { + const mockNode = { + attrs: { + sdBlockId: true, + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(false); + }); + + it('should return false when sdBlockId is an object', () => { + const mockNode = { + attrs: { + sdBlockId: { id: 'block-123' }, + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(false); + }); + + it('should return false when sdBlockId is an array', () => { + const mockNode = { + attrs: { + sdBlockId: ['block-id'], + otherAttr: 'value', + }, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(false); + }); + + it('should return true when node has no attrs property', () => { + const mockNode = {}; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return true when node attrs is null', () => { + const mockNode = { + attrs: null, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should return true when node attrs is undefined', () => { + const mockNode = { + attrs: undefined, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); + + it('should handle null/undefined nodes gracefully', () => { + expect(nodeNeedsSdBlockId(null)).toBe(true); + expect(nodeNeedsSdBlockId(undefined)).toBe(true); + }); + + it('should return true when attrs is empty object', () => { + const mockNode = { + attrs: {}, + }; + + expect(nodeNeedsSdBlockId(mockNode)).toBe(true); + }); +}); + +describe('checkForNewBlockNodesInTrs', () => { + // Helper function to create mock nodes + const createMockNode = (isBlock, hasAttribute) => ({ + isBlock, + type: { + spec: { + attrs: hasAttribute ? { sdBlockId: { default: null } } : {}, + }, + }, + }); + + // Helper function to create mock transactions + const createMockTransaction = (steps) => ({ steps }); + + it('should return true when ReplaceStep contains block nodes with sdBlockId attribute', () => { + const blockNode = createMockNode(true, true); + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [blockNode], + }, + }); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(true); + }); + + it('should return false when ReplaceStep contains only inline nodes', () => { + const inlineNode = createMockNode(false, true); + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [inlineNode], + }, + }); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should return false when ReplaceStep contains block nodes without sdBlockId attribute', () => { + const blockNodeWithoutAttr = createMockNode(true, false); + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [blockNodeWithoutAttr], + }, + }); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should return false when step is not a ReplaceStep', () => { + const blockNode = createMockNode(true, true); + const otherStep = new OtherStep(); + otherStep.slice = { + content: { + content: [blockNode], + }, + }; + + const transaction = createMockTransaction([otherStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should return false when ReplaceStep has no slice', () => { + const replaceStep = new ReplaceStep(0, 1, null); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should return false when slice has no content', () => { + const replaceStep = new ReplaceStep(0, 1, {}); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should return false when content has no content array', () => { + const replaceStep = new ReplaceStep(0, 1, { + content: {}, + }); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should return false when content array is empty', () => { + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [], + }, + }); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should return true when multiple transactions contain valid block nodes', () => { + const blockNode = createMockNode(true, true); + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [blockNode], + }, + }); + + const transaction1 = createMockTransaction([new OtherStep()]); + const transaction2 = createMockTransaction([replaceStep]); + const transactions = [transaction1, transaction2]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(true); + }); + + it('should return true when transaction has multiple steps with valid block nodes', () => { + const blockNode = createMockNode(true, true); + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [blockNode], + }, + }); + + const transaction = createMockTransaction([new OtherStep(), replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(true); + }); + + it('should return true when ReplaceStep contains mixed nodes but at least one valid block', () => { + const inlineNode = createMockNode(false, true); + const blockNodeWithoutAttr = createMockNode(true, false); + const validBlockNode = createMockNode(true, true); + + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [inlineNode, blockNodeWithoutAttr, validBlockNode], + }, + }); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(true); + }); + + it('should handle empty transactions array', () => { + expect(checkForNewBlockNodesInTrs([])).toBe(false); + }); + + it('should handle transactions with empty steps arrays', () => { + const transaction = createMockTransaction([]); + const transactions = [transaction]; + + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); + + it('should handle null/undefined values gracefully', () => { + const replaceStep = new ReplaceStep(0, 1, { + content: { + content: [null, undefined], + }, + }); + + const transaction = createMockTransaction([replaceStep]); + const transactions = [transaction]; + + // This should not throw an error + expect(checkForNewBlockNodesInTrs(transactions)).toBe(false); + }); +}); + +describe('BlockNode helpers', () => { + const filename = 'doc_with_spaces_from_styles.docx'; + let docx, media, mediaFiles, fonts, editor; + beforeAll(async () => ({ docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(filename))); + beforeEach(() => ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts }))); + + it('getBlockNodes returns only nodes that allow sdBlockId', () => { + const blocks = editor.helpers.blockNode.getBlockNodes(); + expect(blocks.length).toBeGreaterThan(0); + blocks.forEach(({ node }) => { + expect(node.type?.spec?.attrs?.sdBlockId).not.toBeUndefined(); + expect(node.isBlock).toBe(true); + }); + }); + + it('getBlockNodeById finds node by sdBlockId', () => { + const blocks = editor.helpers.blockNode.getBlockNodes(); + const sample = blocks.find((b) => !!b.node.attrs.sdBlockId); + expect(sample).toBeTruthy(); + const id = sample.node.attrs.sdBlockId; + const byId = editor.helpers.blockNode.getBlockNodeById(id); + expect(byId.length).toBe(1); + expect(byId[0].node.eq(sample.node)).toBe(true); + }); + + it('getBlockNodesByType returns nodes of the given type', () => { + const blocks = editor.helpers.blockNode.getBlockNodes(); + const targetType = blocks[0].node.type.name; + const byType = editor.helpers.blockNode.getBlockNodesByType(targetType); + const expectedCount = blocks.filter((b) => b.node.type.name === targetType).length; + expect(byType.length).toBe(expectedCount); + byType.forEach((entry) => expect(entry.node.type.name).toBe(targetType)); + }); + + it('getBlockNodesInRange returns nodes within the specified range', () => { + const allBlocks = editor.helpers.blockNode.getBlockNodes(); + const allInDoc = editor.helpers.blockNode.getBlockNodesInRange(0, editor.state.doc.content.size); + expect(allInDoc.length).toBe(allBlocks.length); + + const sample = allBlocks[0]; + const from = sample.pos; + const to = sample.pos + sample.node.nodeSize; + const inRange = editor.helpers.blockNode.getBlockNodesInRange(from, to); + expect(inRange.length).toBe(1); + expect(inRange[0].node.eq(sample.node)).toBe(true); + }); +}); + +describe('BlockNode commands', () => { + const filename = 'doc_with_spaces_from_styles.docx'; + let docx, media, mediaFiles, fonts, editor; + beforeAll(async () => ({ docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(filename))); + beforeEach(() => ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts }))); + + it('replaceBlockNodeById replaces node content', () => { + const blocks = editor.helpers.blockNode.getBlockNodes(); + const target = blocks.find((b) => ['paragraph', 'heading'].includes(b.node.type.name)); + expect(target).toBeTruthy(); + const oldId = target.node.attrs.sdBlockId; + const newId = `${oldId}-new`; + + const typeName = target.node.type.name; + const replacement = editor.schema.nodes[typeName].create({ sdBlockId: newId }, editor.schema.text('Replaced')); + + const result = editor.commands.replaceBlockNodeById(oldId, replacement); + expect(result).toBe(true); + + const oldNode = editor.helpers.blockNode.getBlockNodeById(oldId); + expect(oldNode.length).toBe(0); + const newNode = editor.helpers.blockNode.getBlockNodeById(newId); + expect(newNode.length).toBe(1); + expect(newNode[0].node.textContent).toBe('Replaced'); + }); + + it('deleteBlockNodeById removes the node', () => { + const blocks = editor.helpers.blockNode.getBlockNodes(); + const target = blocks.find((b) => ['paragraph', 'heading'].includes(b.node.type.name)); + expect(target).toBeTruthy(); + const id = target.node.attrs.sdBlockId; + + const before = editor.helpers.blockNode.getBlockNodeById(id); + expect(before.length).toBe(1); + + const result = editor.commands.deleteBlockNodeById(id); + expect(result).toBe(true); + + const after = editor.helpers.blockNode.getBlockNodeById(id); + expect(after.length).toBe(0); + }); + + it('updateBlockNodeAttributes updates node attributes', () => { + const blocks = editor.helpers.blockNode.getBlockNodes(); + const target = blocks.find((b) => ['paragraph', 'heading'].includes(b.node.type.name)); + expect(target).toBeTruthy(); + const oldId = target.node.attrs.sdBlockId; + const newId = `${oldId}-updated`; + + const before = editor.helpers.blockNode.getBlockNodeById(oldId); + expect(before.length).toBe(1); + + const result = editor.commands.updateBlockNodeAttributes(oldId, { sdBlockId: newId }); + expect(result).toBe(true); + + const oldNode = editor.helpers.blockNode.getBlockNodeById(oldId); + expect(oldNode.length).toBe(0); + const updated = editor.helpers.blockNode.getBlockNodeById(newId); + expect(updated.length).toBe(1); + expect(['heading', 'paragraph']).toContain(updated[0].node.type.name); + }); +}); diff --git a/packages/super-editor/src/extensions/block-node/index.js b/packages/super-editor/src/extensions/block-node/index.js new file mode 100644 index 0000000000..7939bdc856 --- /dev/null +++ b/packages/super-editor/src/extensions/block-node/index.js @@ -0,0 +1 @@ +export * from './block-node.js'; diff --git a/packages/super-editor/src/extensions/bullet-list/bullet-list.js b/packages/super-editor/src/extensions/bullet-list/bullet-list.js index c22469c2c2..5468be4475 100644 --- a/packages/super-editor/src/extensions/bullet-list/bullet-list.js +++ b/packages/super-editor/src/extensions/bullet-list/bullet-list.js @@ -50,6 +50,15 @@ export const BulletList = Node.create({ rendered: false, }, + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, + attributes: { rendered: false, keepOnSplit: true, diff --git a/packages/super-editor/src/extensions/content-block/content-block.js b/packages/super-editor/src/extensions/content-block/content-block.js index 22f54044d6..956f1c3209 100644 --- a/packages/super-editor/src/extensions/content-block/content-block.js +++ b/packages/super-editor/src/extensions/content-block/content-block.js @@ -3,11 +3,13 @@ import { Node, Attribute } from '@core/index.js'; export const ContentBlock = Node.create({ name: 'contentBlock', - group: 'block', + group: 'inline', content: '', isolating: true, + atom: true, + inline: true, addOptions() { return { diff --git a/packages/super-editor/src/extensions/heading/heading.js b/packages/super-editor/src/extensions/heading/heading.js index ed6e42e1f4..57047dfc2b 100644 --- a/packages/super-editor/src/extensions/heading/heading.js +++ b/packages/super-editor/src/extensions/heading/heading.js @@ -25,6 +25,14 @@ export const Heading = Node.create({ rendered: false, }, tabStops: { rendered: false }, + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, }; }, diff --git a/packages/super-editor/src/extensions/index.js b/packages/super-editor/src/extensions/index.js index 1b5afd1e71..f38fbea6d4 100644 --- a/packages/super-editor/src/extensions/index.js +++ b/packages/super-editor/src/extensions/index.js @@ -39,6 +39,7 @@ import { ShapeContainer } from './shape-container/index.js'; import { ShapeTextbox } from './shape-textbox/index.js'; import { ContentBlock } from './content-block/index.js'; import { StructuredContent, DocumentSection } from './structured-content/index.js'; +import { BlockNode } from './block-node/index.js'; // Marks extensions import { TextStyle } from './text-style/text-style.js'; @@ -112,6 +113,7 @@ const getRichTextExtensions = () => { const getStarterExtensions = () => { return [ Bold, + BlockNode, BulletList, Color, CommentRangeStart, @@ -216,6 +218,7 @@ export { TableHeader, Placeholder, DropCursor, + BlockNode, FieldAnnotation, fieldAnnotationHelpers, Image, diff --git a/packages/super-editor/src/extensions/ordered-list/ordered-list.js b/packages/super-editor/src/extensions/ordered-list/ordered-list.js index 6c67d89234..9157fc70de 100644 --- a/packages/super-editor/src/extensions/ordered-list/ordered-list.js +++ b/packages/super-editor/src/extensions/ordered-list/ordered-list.js @@ -43,6 +43,15 @@ export const OrderedList = Node.create({ }, }, + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, + syncId: { default: null, parseDOM: (elem) => elem.getAttribute('data-sync-id'), diff --git a/packages/super-editor/src/extensions/paragraph/paragraph.js b/packages/super-editor/src/extensions/paragraph/paragraph.js index f0328555fa..d807ee9b12 100644 --- a/packages/super-editor/src/extensions/paragraph/paragraph.js +++ b/packages/super-editor/src/extensions/paragraph/paragraph.js @@ -86,6 +86,14 @@ export const Paragraph = Node.create({ }, }, styleId: {}, + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, attributes: { rendered: false, }, diff --git a/packages/super-editor/src/extensions/shape-container/shape-container.js b/packages/super-editor/src/extensions/shape-container/shape-container.js index c5e03db851..f40afcf730 100644 --- a/packages/super-editor/src/extensions/shape-container/shape-container.js +++ b/packages/super-editor/src/extensions/shape-container/shape-container.js @@ -28,7 +28,14 @@ export const ShapeContainer = Node.create({ }; }, }, - + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, style: { renderDOM: (attrs) => { if (!attrs.style) return {}; diff --git a/packages/super-editor/src/extensions/shape-textbox/shape-textbox.js b/packages/super-editor/src/extensions/shape-textbox/shape-textbox.js index debf57b1a9..9d02e7e2e8 100644 --- a/packages/super-editor/src/extensions/shape-textbox/shape-textbox.js +++ b/packages/super-editor/src/extensions/shape-textbox/shape-textbox.js @@ -20,6 +20,14 @@ export const ShapeTextbox = Node.create({ addAttributes() { return { + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, attributes: { rendered: false, }, diff --git a/packages/super-editor/src/extensions/structured-content/document-section.js b/packages/super-editor/src/extensions/structured-content/document-section.js index adc602d80c..97e884344d 100644 --- a/packages/super-editor/src/extensions/structured-content/document-section.js +++ b/packages/super-editor/src/extensions/structured-content/document-section.js @@ -74,6 +74,14 @@ export const DocumentSection = Node.create({ addAttributes() { return { id: {}, + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, title: {}, description: {}, sectionType: {}, diff --git a/packages/super-editor/src/extensions/table/table.js b/packages/super-editor/src/extensions/table/table.js index fde5908949..cd529a346a 100644 --- a/packages/super-editor/src/extensions/table/table.js +++ b/packages/super-editor/src/extensions/table/table.js @@ -72,6 +72,14 @@ export const Table = Node.create({ }; }, }, */ + sdBlockId: { + default: null, + keepOnSplit: false, + parseDOM: (elem) => elem.getAttribute('data-sd-block-id'), + renderDOM: (attrs) => { + return attrs.sdBlockId ? { 'data-sd-block-id': attrs.sdBlockId } : {}; + }, + }, tableIndent: { renderDOM: ({ tableIndent }) => {