diff --git a/src/extensions/yfm/Checkbox/Checkbox.test.ts b/src/extensions/yfm/Checkbox/Checkbox.test.ts index cfbae01e..b31446c7 100644 --- a/src/extensions/yfm/Checkbox/Checkbox.test.ts +++ b/src/extensions/yfm/Checkbox/Checkbox.test.ts @@ -23,6 +23,7 @@ const { const { doc, + p, b, checkbox, cbInput, @@ -125,4 +126,70 @@ describe('Checkbox extension', () => { [fixPastePlugin()], ); }); + + it('should parse dom with multiple checkboxes without id', () => { + parseDOM( + schema, + ` + + + + +`, + doc( + checkbox(cbInput(), cbLabel('First checkbox')), + checkbox(cbInput({checked: 'true'}), cbLabel('Second checkbox')), + ), + [fixPastePlugin()], + ); + }); + + it('should create empty label when next sibling is not a label', () => { + parseDOM( + schema, + `Not a label`, + doc(checkbox(cbInput(), cbLabel()), p('Not a label')), + [fixPastePlugin()], + ); + }); + + it('should parse multiple checkboxes wrapped in div.checkbox', () => { + parseDOM( + schema, + ` +
+ + +
+
+ + +
`, + doc( + checkbox(cbInput(), cbLabel('Task 1')), + checkbox(cbInput({checked: 'true'}), cbLabel('Task 2')), + ), + [fixPastePlugin()], + ); + }); + + it('should parse checkboxes with special characters in id', () => { + parseDOM( + schema, + ` +
+ + +
+
+ + +
`, + doc( + checkbox(cbInput(), cbLabel('Task with invalid ID')), + checkbox(cbInput({checked: 'true'}), cbLabel('Task with valid ID')), + ), + [fixPastePlugin()], + ); + }); }); diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts index e34e8a81..d395a4fe 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts @@ -1,4 +1,4 @@ -import {Fragment, type NodeSpec} from 'prosemirror-model'; +import {Fragment, type NodeSpec, type Schema} from 'prosemirror-model'; import type {PlaceholderOptions} from '../../../../utils/placeholder'; @@ -26,36 +26,30 @@ export const getSchemaSpecs = ( tag: 'div.checkbox', priority: 100, getContent(node, schema) { - const input = (node as HTMLElement).querySelector( - 'input[type=checkbox]', - ); - const label = (node as HTMLElement).querySelector( - 'label[for]', - ); + if (node instanceof HTMLElement) { + const input = node.querySelector('input[type=checkbox]'); - const checked = input?.checked ? 'true' : null; - const text = label?.textContent; - - return Fragment.from([ - checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}), - checkboxLabelType(schema).create(null, text ? schema.text(text) : null), - ]); + if (input && input instanceof HTMLInputElement) { + const label = findLabelForInput(input); + return createCheckboxFragment( + schema, + input.checked, + label?.textContent, + ); + } + } + return Fragment.empty; }, }, { tag: 'input[type=checkbox]', priority: 50, - getContent(node, schema) { - const id = (node as HTMLElement).id; - const checked = (node as HTMLInputElement).checked ? 'true' : null; - const text = node.parentNode?.querySelector( - `label[for=${id}]`, - )?.textContent; - - return Fragment.from([ - checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}), - checkboxLabelType(schema).create(null, text ? schema.text(text) : null), - ]); + getContent(input, schema) { + if (input instanceof HTMLInputElement) { + const label = findLabelForInput(input); + return createCheckboxFragment(schema, input.checked, label?.textContent); + } + return Fragment.empty; }, }, ], @@ -94,7 +88,7 @@ export const getSchemaSpecs = ( { // input handled by checkbox node parse rule // ignore label - tag: 'input[type=checkbox] ~ label[for]', + tag: 'input[type=checkbox] ~ label', ignore: true, consuming: true, }, @@ -119,3 +113,24 @@ export const getSchemaSpecs = ( complex: 'leaf', }, }); + +// fallback for invalid HTML (input without id + label without for) +function findNextSiblingLabel(element: HTMLInputElement): HTMLLabelElement | null { + const nextSibling = element.nextElementSibling; + return nextSibling instanceof HTMLLabelElement ? nextSibling : null; +} + +function findLabelForInput(element: HTMLInputElement): HTMLLabelElement | null { + return element.labels?.[0] || findNextSiblingLabel(element); +} + +function createCheckboxFragment( + schema: Schema, + checked: boolean | null, + labelText: string | null | undefined, +): Fragment { + return Fragment.from([ + checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked ? 'true' : null}), + checkboxLabelType(schema).create(null, labelText ? schema.text(labelText) : null), + ]); +}