Skip to content

Commit

Permalink
fix(Checkbox): correct handling of checkbox click (#510)
Browse files Browse the repository at this point in the history
  • Loading branch information
d3m1d0v authored Dec 2, 2024
1 parent d2fd709 commit c214076
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 70 deletions.
8 changes: 8 additions & 0 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ export enum CheckboxNode {
Label = 'checkbox_label',
}

export const CheckboxAttr = {
Class: 'class',
Type: 'type',
Id: 'id',
Checked: 'checked',
For: 'for',
} as const;

export const idPrefix = 'yfm-editor-checkbox';

export const b = cn('checkbox');
2 changes: 1 addition & 1 deletion src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {parserTokens} from './parser';
import {getSchemaSpecs} from './schema';
import {serializerTokens} from './serializer';

export {CheckboxNode} from './const';
export {CheckboxAttr, CheckboxNode} from './const';
export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox);
export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label);
export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input);
Expand Down
15 changes: 8 additions & 7 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {NodeSpec} from 'prosemirror-model';

import {PlaceholderOptions} from '../../../../utils/placeholder';
import {CheckboxNode, b} from '../const';

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

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

Expand All @@ -18,7 +19,7 @@ export const getSchemaSpecs = (
allowSelection: false,
parseDOM: [],
attrs: {
class: {default: b()},
[CheckboxAttr.Class]: {default: b()},
},
toDOM(node) {
return ['div', node.attrs, 0];
Expand All @@ -30,9 +31,9 @@ export const getSchemaSpecs = (
group: 'block',
parseDOM: [],
attrs: {
type: {default: 'checkbox'},
id: {default: null},
checked: {default: null},
[CheckboxAttr.Type]: {default: 'checkbox'},
[CheckboxAttr.Id]: {default: null},
[CheckboxAttr.Checked]: {default: null},
},
toDOM(node) {
return ['div', node.attrs];
Expand All @@ -49,12 +50,12 @@ export const getSchemaSpecs = (
{
tag: `span[class="${b('label')}"]`,
getAttrs: (node) => ({
for: (node as Element).getAttribute('for') || '',
[CheckboxAttr.For]: (node as Element).getAttribute(CheckboxAttr.For) || '',
}),
},
],
attrs: {
for: {default: null},
[CheckboxAttr.For]: {default: null},
},
escapeText: false,
placeholder: {
Expand Down
5 changes: 3 additions & 2 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/serializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {SerializerNodeToken} from '../../../../core';
import {getPlaceholderContent} from '../../../../utils/placeholder';
import {CheckboxNode} from '../const';

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

export const serializerTokens: Record<CheckboxNode, SerializerNodeToken> = {
[CheckboxNode.Checkbox]: (state, node) => {
Expand All @@ -9,7 +10,7 @@ export const serializerTokens: Record<CheckboxNode, SerializerNodeToken> = {
},

[CheckboxNode.Input]: (state, node) => {
const checked = node.attrs.checked === 'true';
const checked = node.attrs[CheckboxAttr.Checked] === 'true';
state.write(`[${checked ? 'X' : ' '}] `);
},

Expand Down
2 changes: 1 addition & 1 deletion src/extensions/yfm/Checkbox/const.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {CheckboxNode, b} from './CheckboxSpecs/const';
export {CheckboxAttr, CheckboxNode, b} from './CheckboxSpecs/const';
69 changes: 10 additions & 59 deletions src/extensions/yfm/Checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,30 @@
import {replaceParentNodeOfType} from 'prosemirror-utils';

import type {Action, ExtensionAuto} from '../../../core';
import {nodeInputRule} from '../../../utils/inputrules';
import {pType} from '../../base/BaseSchema';

import {CheckboxSpecs, CheckboxSpecsOptions} from './CheckboxSpecs';
import {CheckboxSpecs, type CheckboxSpecsOptions} from './CheckboxSpecs';
import {addCheckbox} from './actions';
import {CheckboxNode, b} from './const';
import {CheckboxInputView} from './nodeviews';
import {keymapPlugin} from './plugin';
import {checkboxInputType, checkboxType} from './utils';

import './index.scss';

const checkboxAction = 'addCheckbox';

export {CheckboxNode, checkboxType, checkboxLabelType, checkboxInputType} from './CheckboxSpecs';
export {
CheckboxAttr,
CheckboxNode,
checkboxType,
checkboxLabelType,
checkboxInputType,
} from './CheckboxSpecs';

export type CheckboxOptions = Pick<CheckboxSpecsOptions, 'checkboxLabelPlaceholder'> & {};

export const Checkbox: ExtensionAuto<CheckboxOptions> = (builder, opts) => {
builder.use(CheckboxSpecs, {
...opts,
inputView: () => (node, view, getPos) => {
const dom = document.createElement('input');

for (const attr in node.attrs) {
if (node.attrs[attr]) dom.setAttribute(attr, node.attrs[attr]);
}

dom.setAttribute('class', b('input'));

dom.addEventListener('click', (e) => {
const elem = e.target as HTMLElement;
const checkedAttr = elem.getAttribute('checked');
const checked = checkedAttr ? '' : 'true';
const pos = getPos();

if (pos !== undefined) {
view.dispatch(
view.state.tr.setNodeMarkup(pos, undefined, {
...node.attrs,
checked,
}),
);
}

elem.setAttribute('checked', checked);
});

return {
dom,
ignoreMutation: () => true,
update: () => true,
destroy() {
const pos = getPos();
if (pos !== undefined) {
const resolved = view.state.doc.resolve(pos);
if (
resolved.parent.type.name === CheckboxNode.Checkbox &&
resolved.parent.lastChild
) {
view.dispatch(
replaceParentNodeOfType(
resolved.parent.type,
pType(view.state.schema).create(
resolved.parent.lastChild.content,
),
)(view.state.tr),
);
}
}
dom.remove();
},
};
},
inputView: () => CheckboxInputView.create,
});

builder
Expand Down
77 changes: 77 additions & 0 deletions src/extensions/yfm/Checkbox/nodeviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type {Node} from 'prosemirror-model';
import type {EditorView, NodeView, NodeViewConstructor} from 'prosemirror-view';

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

export class CheckboxInputView implements NodeView {
static create: NodeViewConstructor = (node, view, getPos) => new this(node, view, getPos);

dom: HTMLInputElement;

private _node: Node;
private _view: EditorView;
private _getPos: () => number | undefined;

private constructor(node: Node, view: EditorView, getPos: () => number | undefined) {
this._node = node;
this._view = view;
this._getPos = getPos;

this.dom = this._createDomElem();
this._applyNodeAttrsToDomElem();
}

ignoreMutation(): boolean {
return true;
}

update(node: Node): boolean {
if (node.type !== this._node.type) return false;

this._node = node;
this._applyNodeAttrsToDomElem();

return true;
}

destroy(): void {
this.dom.removeEventListener('click', this._onInputClick);
}

private _createDomElem(): HTMLInputElement {
const dom = document.createElement('input');
dom.setAttribute('class', b('input'));
dom.addEventListener('click', this._onInputClick);
return dom;
}

private _applyNodeAttrsToDomElem(): void {
const {dom, _node: node} = this;

for (const [key, value] of Object.entries(node.attrs)) {
if (value) dom.setAttribute(key, value);
else dom.removeAttribute(key);
}

const checked = node.attrs[CheckboxAttr.Checked] === 'true';
this.dom.checked = checked;
}

private _onInputClick = (event: MouseEvent): void => {
if (event.target instanceof HTMLInputElement) {
const {checked} = event.target;
const pos = this._getPos();

if (pos !== undefined)
this._view.dispatch(
this._view.state.tr.setNodeAttribute(
pos,
CheckboxAttr.Checked,
checked ? 'true' : null,
),
);
}

this._view.focus();
};
}

0 comments on commit c214076

Please sign in to comment.