Skip to content

Commit a7c23b5

Browse files
authored
fix(Checkbox): added parse dom rules and fixed pasting of checkboxes (#523)
1 parent dc049af commit a7c23b5

File tree

8 files changed

+122
-17
lines changed

8 files changed

+122
-17
lines changed

src/extensions/yfm/Checkbox/Checkbox.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {builders} from 'prosemirror-test-builder';
22

3+
import {parseDOM} from '../../../../tests/parse-dom';
34
import {createMarkupChecker} from '../../../../tests/sameMarkup';
45
import {ExtensionsManager} from '../../../core';
56
import {BaseNode, BaseSchemaSpecs} from '../../base/specs';
67
import {BoldSpecs, boldMarkName} from '../../markdown/specs';
78

8-
import {CheckboxNode, CheckboxSpecs} from './CheckboxSpecs';
9+
import {CheckboxAttr, CheckboxNode, CheckboxSpecs} from './CheckboxSpecs';
10+
import {fixPastePlugin} from './plugins/fix-paste';
911

1012
const {
1113
schema,
@@ -96,4 +98,31 @@ describe('Checkbox extension', () => {
9698
'[ ] checkbox-placeholder',
9799
);
98100
});
101+
102+
it('should parse dom with checkbox', () => {
103+
parseDOM(
104+
schema,
105+
`
106+
<meta charset='utf-8'>
107+
<div class="checkbox">
108+
<input type="checkbox" id="checkbox1" disabled="" checked="true">
109+
<label for="checkbox1">два</label>
110+
</div>`,
111+
doc(checkbox(cbInput({[CheckboxAttr.Checked]: 'true'}), cbLabel('два'))),
112+
[fixPastePlugin()],
113+
);
114+
});
115+
116+
it('should parse dom with input[type=checkbox]', () => {
117+
parseDOM(
118+
schema,
119+
`
120+
<input type="checkbox" id="checkbox2" disabled="">
121+
<span></span>
122+
<label for="checkbox2">todo2</label>
123+
`,
124+
doc(checkbox(cbInput(), cbLabel('todo2'))),
125+
[fixPastePlugin()],
126+
);
127+
});
99128
});

src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {cn} from '../../../../classname';
2+
import {nodeTypeFactory} from '../../../../utils/schema';
23

34
export enum CheckboxNode {
45
Checkbox = 'checkbox',
@@ -17,3 +18,7 @@ export const CheckboxAttr = {
1718
export const idPrefix = 'yfm-editor-checkbox';
1819

1920
export const b = cn('checkbox');
21+
22+
export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox);
23+
export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label);
24+
export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input);

src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ import checkboxPlugin from '@diplodoc/transform/lib/plugins/checkbox';
22
import type {NodeSpec} from 'prosemirror-model';
33

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

76
import {CheckboxNode, b, idPrefix} from './const';
87
import {parserTokens} from './parser';
98
import {getSchemaSpecs} from './schema';
109
import {serializerTokens} from './serializer';
1110

12-
export {CheckboxAttr, CheckboxNode} from './const';
13-
export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox);
14-
export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label);
15-
export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input);
11+
export {
12+
CheckboxAttr,
13+
CheckboxNode,
14+
checkboxType,
15+
checkboxLabelType,
16+
checkboxInputType,
17+
} from './const';
1618

1719
export type CheckboxSpecsOptions = {
1820
/**

src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type {NodeSpec} from 'prosemirror-model';
1+
import {Fragment, type NodeSpec} from 'prosemirror-model';
22

3-
import {PlaceholderOptions} from '../../../../utils/placeholder';
3+
import type {PlaceholderOptions} from '../../../../utils/placeholder';
44

5-
import {CheckboxAttr, CheckboxNode, b} from './const';
5+
import {CheckboxAttr, CheckboxNode, b, checkboxInputType, checkboxLabelType} from './const';
66

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

@@ -13,22 +13,59 @@ export const getSchemaSpecs = (
1313
placeholder?: PlaceholderOptions,
1414
): Record<CheckboxNode, NodeSpec> => ({
1515
[CheckboxNode.Checkbox]: {
16-
group: 'block',
16+
group: 'block checkbox',
1717
content: `${CheckboxNode.Input} ${CheckboxNode.Label}`,
1818
selectable: true,
1919
allowSelection: false,
20-
parseDOM: [],
2120
attrs: {
2221
[CheckboxAttr.Class]: {default: b()},
2322
},
23+
parseDOM: [
24+
{
25+
tag: 'div.checkbox',
26+
priority: 100,
27+
getContent(node, schema) {
28+
const input = (node as HTMLElement).querySelector<HTMLInputElement>(
29+
'input[type=checkbox]',
30+
);
31+
const label = (node as HTMLElement).querySelector<HTMLLabelElement>(
32+
'label[for]',
33+
);
34+
35+
const checked = input?.checked ? 'true' : null;
36+
const text = label?.textContent;
37+
38+
return Fragment.from([
39+
checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}),
40+
checkboxLabelType(schema).create(null, text ? schema.text(text) : null),
41+
]);
42+
},
43+
},
44+
{
45+
tag: 'input[type=checkbox]',
46+
priority: 50,
47+
getContent(node, schema) {
48+
const id = (node as HTMLElement).id;
49+
const checked = (node as HTMLInputElement).checked ? 'true' : null;
50+
const text = node.parentNode?.querySelector<HTMLLabelElement>(
51+
`label[for=${id}]`,
52+
)?.textContent;
53+
54+
return Fragment.from([
55+
checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}),
56+
checkboxLabelType(schema).create(null, text ? schema.text(text) : null),
57+
]);
58+
},
59+
},
60+
],
2461
toDOM(node) {
2562
return ['div', node.attrs, 0];
2663
},
2764
complex: 'root',
2865
},
2966

3067
[CheckboxNode.Input]: {
31-
group: 'block',
68+
group: 'block checkbox',
3269
parseDOM: [],
3370
attrs: {
3471
[CheckboxAttr.Type]: {default: 'checkbox'},
@@ -45,14 +82,21 @@ export const getSchemaSpecs = (
4582

4683
[CheckboxNode.Label]: {
4784
content: 'inline*',
48-
group: 'block',
85+
group: 'block checkbox',
4986
parseDOM: [
5087
{
5188
tag: `span[class="${b('label')}"]`,
5289
getAttrs: (node) => ({
5390
[CheckboxAttr.For]: (node as Element).getAttribute(CheckboxAttr.For) || '',
5491
}),
5592
},
93+
{
94+
// input handled by checkbox node parse rule
95+
// ignore label
96+
tag: 'input[type=checkbox] ~ label[for]',
97+
ignore: true,
98+
consuming: true,
99+
},
56100
],
57101
attrs: {
58102
[CheckboxAttr.For]: {default: null},

src/extensions/yfm/Checkbox/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {CheckboxSpecs, type CheckboxSpecsOptions} from './CheckboxSpecs';
55
import {addCheckbox} from './actions';
66
import {CheckboxInputView} from './nodeviews';
77
import {keymapPlugin} from './plugin';
8+
import {fixPastePlugin} from './plugins/fix-paste';
89
import {checkboxInputType, checkboxType} from './utils';
910

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

3031
builder
3132
.addPlugin(keymapPlugin, builder.Priority.High)
33+
.addPlugin(fixPastePlugin)
3234
.addAction(checkboxAction, () => addCheckbox())
3335
.addInputRules(({schema}) => ({
3436
rules: [
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {Slice} from 'prosemirror-model';
2+
import {Plugin} from 'prosemirror-state';
3+
4+
import {checkboxType} from '../CheckboxSpecs';
5+
6+
export const fixPastePlugin = () =>
7+
new Plugin({
8+
props: {
9+
transformPasted(slice) {
10+
const {firstChild} = slice.content;
11+
if (firstChild && firstChild.type === checkboxType(firstChild.type.schema)) {
12+
// When paste html with checkboxes and checkbox is first node,
13+
// pm creates slice with broken openStart and openEnd.
14+
// And content is inserted without a container block for checkboxes.
15+
// It is fixed by create new slice with zeroed openStart and openEnd.
16+
return new Slice(slice.content, 0, 0);
17+
}
18+
19+
return slice;
20+
},
21+
},
22+
});

src/utils/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Node, NodeType, Schema} from 'prosemirror-model';
1+
import {Node, type NodeType, type Schema} from 'prosemirror-model';
22

33
export const nodeTypeFactory = (nodeName: string) => (schema: Schema) => schema.nodes[nodeName];
44
export const markTypeFactory = (markName: string) => (schema: Schema) => schema.marks[markName];

tests/parse-dom.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* eslint-disable no-implicit-globals */
22
import type {Node, Schema} from 'prosemirror-model';
3-
import {EditorState} from 'prosemirror-state';
3+
import {EditorState, type Plugin} from 'prosemirror-state';
44
import {EditorView} from 'prosemirror-view';
5+
56
import {dispatchPasteEvent} from './dispatch-event';
67

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

0 commit comments

Comments
 (0)