diff --git a/cypress/helpers/editor.ts b/cypress/helpers/editor.ts index fc526174..6d42a9cb 100644 --- a/cypress/helpers/editor.ts +++ b/cypress/helpers/editor.ts @@ -92,6 +92,29 @@ export const addImageElement = (values: Record = {}) => { }); }; +type ElementFields = { + altTextValue?: string; + captionValue?: string; + srcValue?: string; + codeValue?: string; + useSrcValue?: string; + optionValue?: string; + restrictedTextValue?: string; + customDropdownValue?: string; + mainImageValue?: { + assets: string; + mediaId?: string; + mediaApiUri?: string; + }; +}; + +export const setDocFromHtml = (fields: ElementFields) => { + cy.window().then((win: WindowType) => { + const { htmlToDoc } = win.PM_ELEMENTS; + htmlToDoc(getSerialisedHtml(fields)); + }); +}; + export const getSerialisedHtml = ({ altTextValue = "", captionValue = "

", @@ -106,21 +129,7 @@ export const getSerialisedHtml = ({ mediaId: undefined, mediaApiUri: undefined, }, -}: { - altTextValue?: string; - captionValue?: string; - srcValue?: string; - codeValue?: string; - useSrcValue?: string; - optionValue?: string; - restrictedTextValue?: string; - customDropdownValue?: string; - mainImageValue?: { - assets: string; - mediaId?: string; - mediaApiUri?: string; - }; -}): string => { +}: ElementFields): string => { const mainImageFields = mainImageValue.mediaId || mainImageValue.mediaApiUri ? `"mediaId":"${ diff --git a/cypress/tests/ImageElement.spec.ts b/cypress/tests/ImageElement.spec.ts index d859c335..876da7ec 100644 --- a/cypress/tests/ImageElement.spec.ts +++ b/cypress/tests/ImageElement.spec.ts @@ -10,6 +10,7 @@ import { getSerialisedHtml, italicShortcut, selectDataCy, + setDocFromHtml, typeIntoElementField, visitRoot, } from "../helpers/editor"; @@ -143,6 +144,12 @@ describe("ImageElement", () => { }) ); }); + + it("should deserialise content from HTML into the appropriate node in the document", () => { + const values = { altTextValue: "Alt text" }; + setDocFromHtml(values); + assertDocHtml(getSerialisedHtml(values)); + }); }); describe("Text field", () => { @@ -211,6 +218,12 @@ describe("ImageElement", () => { typeIntoElementField("src", "Src text"); assertDocHtml(getSerialisedHtml({ srcValue: "Src text" })); }); + + it("should deserialise content from HTML into the appropriate node in the document", () => { + const values = { srcValue: "Src text" }; + setDocFromHtml(values); + assertDocHtml(getSerialisedHtml(values)); + }); }); describe("Checkbox field", () => { diff --git a/demo/index.ts b/demo/index.ts index a17f4575..32f24d29 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -256,4 +256,13 @@ window.PM_ELEMENTS = { insertElement: insertElement, docToHtml: () => firstEditor ? docToHtml(serializer, firstEditor.state.doc) : "", + htmlToDoc: (html: string) => { + const node = htmlToDoc(parser, html); + firstEditor?.updateState( + EditorState.create({ + doc: node, + plugins: firstEditor.state.plugins, + }) + ); + }, }; diff --git a/demo/types.ts b/demo/types.ts index 20a0f65f..091ad242 100644 --- a/demo/types.ts +++ b/demo/types.ts @@ -12,5 +12,6 @@ export type WindowType = { view: EditorView; insertElement: typeof insertElement; docToHtml: () => string; + htmlToDoc: (html: string) => void; }; }; diff --git a/src/plugin/nodeSpec.ts b/src/plugin/nodeSpec.ts index dcabb65d..0e6856bb 100644 --- a/src/plugin/nodeSpec.ts +++ b/src/plugin/nodeSpec.ts @@ -85,26 +85,19 @@ export const getNodeSpecForField = ( fieldName: string, field: FieldDescription ): NodeSpec => { + const nodeName = getNodeNameFromField(fieldName, elementName); + switch (field.type) { case "text": return { - [getNodeNameFromField(fieldName, elementName)]: { + [nodeName]: { content: field.isMultiline && !field.isCode ? "(text|hard_break)*" : "text*", - toDOM: getDefaultToDOMForContentNode(elementName, fieldName), + toDOM: getDefaultToDOMForContentNode(nodeName), parseDOM: [ { tag: "div", - getAttrs: (dom: Element) => { - const domFieldName = dom.getAttribute(fieldNameAttr); - if ( - domFieldName !== getNodeNameFromField(fieldName, elementName) - ) { - return false; - } - - return {}; - }, + getAttrs: createGetAttrsForTextNode(nodeName), preserveWhitespace: field.isCode ? "full" : false, }, ], @@ -115,24 +108,25 @@ export const getNodeSpecForField = ( case "richText": { const nodeSpec = field.nodeSpec ?? {}; return { - [getNodeNameFromField(fieldName, elementName)]: { + [nodeName]: { ...nodeSpec, content: nodeSpec.content ?? "paragraph+", - toDOM: - nodeSpec.toDOM ?? - getDefaultToDOMForContentNode(elementName, fieldName), + toDOM: nodeSpec.toDOM ?? getDefaultToDOMForContentNode(nodeName), parseDOM: nodeSpec.parseDOM ?? [ - { tag: getTagForNode(elementName, fieldName) }, + { + tag: "div", + getAttrs: createGetAttrsForTextNode(nodeName), + }, ], }, }; } case "checkbox": return { - [getNodeNameFromField(fieldName, elementName)]: { + [nodeName]: { atom: true, - toDOM: getDefaultToDOMForLeafNode(elementName, fieldName), - parseDOM: getDefaultParseDOMForLeafNode(elementName, fieldName), + toDOM: getDefaultToDOMForLeafNode(nodeName), + parseDOM: getDefaultParseDOMForLeafNode(nodeName), attrs: { fields: { default: field.defaultValue, @@ -142,10 +136,10 @@ export const getNodeSpecForField = ( }; case "dropdown": return { - [getNodeNameFromField(fieldName, elementName)]: { + [nodeName]: { atom: true, - toDOM: getDefaultToDOMForLeafNode(elementName, fieldName), - parseDOM: getDefaultParseDOMForLeafNode(elementName, fieldName), + toDOM: getDefaultToDOMForLeafNode(nodeName), + parseDOM: getDefaultParseDOMForLeafNode(nodeName), attrs: { fields: { default: field.defaultValue, @@ -155,10 +149,10 @@ export const getNodeSpecForField = ( }; case "custom": return { - [getNodeNameFromField(fieldName, elementName)]: { + [nodeName]: { atom: true, - toDOM: getDefaultToDOMForLeafNode(elementName, fieldName), - parseDOM: getDefaultParseDOMForLeafNode(elementName, fieldName), + toDOM: getDefaultToDOMForLeafNode(nodeName), + parseDOM: getDefaultParseDOMForLeafNode(nodeName), attrs: { fields: { default: { value: field.defaultValue }, @@ -169,38 +163,40 @@ export const getNodeSpecForField = ( } }; -const getDefaultToDOMForContentNode = ( - elementName: string, - fieldName: string -) => () => +const createGetAttrsForTextNode = (nodeName: string) => (dom: Element) => { + const domFieldName = dom.getAttribute(fieldNameAttr); + + if (domFieldName !== nodeName) { + return false; + } + + return undefined; +}; + +const getDefaultToDOMForContentNode = (nodeName: string) => () => [ "div", { - [fieldNameAttr]: getNodeNameFromField(fieldName, elementName), + [fieldNameAttr]: nodeName, }, 0, ] as const; -const getDefaultToDOMForLeafNode = (elementName: string, fieldName: string) => ( - node: Node -) => [ +const getDefaultToDOMForLeafNode = (nodeName: string) => (node: Node) => [ "div", { - [fieldNameAttr]: getNodeNameFromField(fieldName, elementName), + [fieldNameAttr]: nodeName, fields: JSON.stringify(node.attrs.fields), "has-errors": JSON.stringify(node.attrs.hasErrors), }, ]; -const getDefaultParseDOMForLeafNode = ( - elementName: string, - fieldName: string -) => [ +const getDefaultParseDOMForLeafNode = (nodeName: string) => [ { tag: "div", getAttrs: (dom: Element) => { const domFieldName = dom.getAttribute(fieldNameAttr); - if (domFieldName !== getNodeNameFromField(fieldName, elementName)) { + if (domFieldName !== nodeName) { return false; } @@ -213,9 +209,6 @@ const getDefaultParseDOMForLeafNode = ( }, ]; -const getTagForNode = (elementName: string, fieldName: string) => - `element-${elementName}-${fieldName}`.toLowerCase(); - export const createNodesForFieldValues = < S extends Schema, FDesc extends FieldDescriptions