Skip to content

Commit

Permalink
fix(Checkbox): added parse dom rules and fixed pasting of checkboxes (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
d3m1d0v authored Dec 17, 2024
1 parent dc049af commit a7c23b5
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 17 deletions.
31 changes: 30 additions & 1 deletion src/extensions/yfm/Checkbox/Checkbox.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {builders} from 'prosemirror-test-builder';

import {parseDOM} from '../../../../tests/parse-dom';
import {createMarkupChecker} from '../../../../tests/sameMarkup';
import {ExtensionsManager} from '../../../core';
import {BaseNode, BaseSchemaSpecs} from '../../base/specs';
import {BoldSpecs, boldMarkName} from '../../markdown/specs';

import {CheckboxNode, CheckboxSpecs} from './CheckboxSpecs';
import {CheckboxAttr, CheckboxNode, CheckboxSpecs} from './CheckboxSpecs';
import {fixPastePlugin} from './plugins/fix-paste';

const {
schema,
Expand Down Expand Up @@ -96,4 +98,31 @@ describe('Checkbox extension', () => {
'[ ] checkbox-placeholder',
);
});

it('should parse dom with checkbox', () => {
parseDOM(
schema,
`
<meta charset='utf-8'>
<div class="checkbox">
<input type="checkbox" id="checkbox1" disabled="" checked="true">
<label for="checkbox1">два</label>
</div>`,
doc(checkbox(cbInput({[CheckboxAttr.Checked]: 'true'}), cbLabel('два'))),
[fixPastePlugin()],
);
});

it('should parse dom with input[type=checkbox]', () => {
parseDOM(
schema,
`
<input type="checkbox" id="checkbox2" disabled="">
<span></span>
<label for="checkbox2">todo2</label>
`,
doc(checkbox(cbInput(), cbLabel('todo2'))),
[fixPastePlugin()],
);
});
});
5 changes: 5 additions & 0 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {cn} from '../../../../classname';
import {nodeTypeFactory} from '../../../../utils/schema';

export enum CheckboxNode {
Checkbox = 'checkbox',
Expand All @@ -17,3 +18,7 @@ export const CheckboxAttr = {
export const idPrefix = 'yfm-editor-checkbox';

export const b = cn('checkbox');

export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox);
export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label);
export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input);
12 changes: 7 additions & 5 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import checkboxPlugin from '@diplodoc/transform/lib/plugins/checkbox';
import type {NodeSpec} from 'prosemirror-model';

import type {ExtensionAuto, ExtensionNodeSpec} from '../../../../core';
import {nodeTypeFactory} from '../../../../utils/schema';

import {CheckboxNode, b, idPrefix} from './const';
import {parserTokens} from './parser';
import {getSchemaSpecs} from './schema';
import {serializerTokens} from './serializer';

export {CheckboxAttr, CheckboxNode} from './const';
export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox);
export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label);
export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input);
export {
CheckboxAttr,
CheckboxNode,
checkboxType,
checkboxLabelType,
checkboxInputType,
} from './const';

export type CheckboxSpecsOptions = {
/**
Expand Down
58 changes: 51 additions & 7 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {NodeSpec} from 'prosemirror-model';
import {Fragment, type NodeSpec} from 'prosemirror-model';

import {PlaceholderOptions} from '../../../../utils/placeholder';
import type {PlaceholderOptions} from '../../../../utils/placeholder';

import {CheckboxAttr, CheckboxNode, b} from './const';
import {CheckboxAttr, CheckboxNode, b, checkboxInputType, checkboxLabelType} from './const';

import type {CheckboxSpecsOptions} from './index';

Expand All @@ -13,22 +13,59 @@ export const getSchemaSpecs = (
placeholder?: PlaceholderOptions,
): Record<CheckboxNode, NodeSpec> => ({
[CheckboxNode.Checkbox]: {
group: 'block',
group: 'block checkbox',
content: `${CheckboxNode.Input} ${CheckboxNode.Label}`,
selectable: true,
allowSelection: false,
parseDOM: [],
attrs: {
[CheckboxAttr.Class]: {default: b()},
},
parseDOM: [
{
tag: 'div.checkbox',
priority: 100,
getContent(node, schema) {
const input = (node as HTMLElement).querySelector<HTMLInputElement>(
'input[type=checkbox]',
);
const label = (node as HTMLElement).querySelector<HTMLLabelElement>(
'label[for]',
);

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),
]);
},
},
{
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<HTMLLabelElement>(
`label[for=${id}]`,
)?.textContent;

return Fragment.from([
checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}),
checkboxLabelType(schema).create(null, text ? schema.text(text) : null),
]);
},
},
],
toDOM(node) {
return ['div', node.attrs, 0];
},
complex: 'root',
},

[CheckboxNode.Input]: {
group: 'block',
group: 'block checkbox',
parseDOM: [],
attrs: {
[CheckboxAttr.Type]: {default: 'checkbox'},
Expand All @@ -45,14 +82,21 @@ export const getSchemaSpecs = (

[CheckboxNode.Label]: {
content: 'inline*',
group: 'block',
group: 'block checkbox',
parseDOM: [
{
tag: `span[class="${b('label')}"]`,
getAttrs: (node) => ({
[CheckboxAttr.For]: (node as Element).getAttribute(CheckboxAttr.For) || '',
}),
},
{
// input handled by checkbox node parse rule
// ignore label
tag: 'input[type=checkbox] ~ label[for]',
ignore: true,
consuming: true,
},
],
attrs: {
[CheckboxAttr.For]: {default: null},
Expand Down
2 changes: 2 additions & 0 deletions src/extensions/yfm/Checkbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {CheckboxSpecs, type CheckboxSpecsOptions} from './CheckboxSpecs';
import {addCheckbox} from './actions';
import {CheckboxInputView} from './nodeviews';
import {keymapPlugin} from './plugin';
import {fixPastePlugin} from './plugins/fix-paste';
import {checkboxInputType, checkboxType} from './utils';

import './index.scss';
Expand All @@ -29,6 +30,7 @@ export const Checkbox: ExtensionAuto<CheckboxOptions> = (builder, opts) => {

builder
.addPlugin(keymapPlugin, builder.Priority.High)
.addPlugin(fixPastePlugin)
.addAction(checkboxAction, () => addCheckbox())
.addInputRules(({schema}) => ({
rules: [
Expand Down
22 changes: 22 additions & 0 deletions src/extensions/yfm/Checkbox/plugins/fix-paste.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Slice} from 'prosemirror-model';
import {Plugin} from 'prosemirror-state';

import {checkboxType} from '../CheckboxSpecs';

export const fixPastePlugin = () =>
new Plugin({
props: {
transformPasted(slice) {
const {firstChild} = slice.content;
if (firstChild && firstChild.type === checkboxType(firstChild.type.schema)) {
// When paste html with checkboxes and checkbox is first node,
// pm creates slice with broken openStart and openEnd.
// And content is inserted without a container block for checkboxes.
// It is fixed by create new slice with zeroed openStart and openEnd.
return new Slice(slice.content, 0, 0);
}

return slice;
},
},
});
2 changes: 1 addition & 1 deletion src/utils/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Node, NodeType, Schema} from 'prosemirror-model';
import {Node, type NodeType, type Schema} from 'prosemirror-model';

export const nodeTypeFactory = (nodeName: string) => (schema: Schema) => schema.nodes[nodeName];
export const markTypeFactory = (markName: string) => (schema: Schema) => schema.marks[markName];
Expand Down
7 changes: 4 additions & 3 deletions tests/parse-dom.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-disable no-implicit-globals */
import type {Node, Schema} from 'prosemirror-model';
import {EditorState} from 'prosemirror-state';
import {EditorState, type Plugin} from 'prosemirror-state';
import {EditorView} from 'prosemirror-view';

import {dispatchPasteEvent} from './dispatch-event';

export function parseDOM(schema: Schema, html: string, doc: Node): void {
const view = new EditorView(null, {state: EditorState.create({schema})});
export function parseDOM(schema: Schema, html: string, doc: Node, plugins?: Plugin[]): void {
const view = new EditorView(null, {state: EditorState.create({schema}), plugins});
dispatchPasteEvent(view, {'text/html': html});
expect(view.state.doc).toMatchNode(doc);
}

0 comments on commit a7c23b5

Please sign in to comment.