Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Loading