From b1a4fa0936e50929d01bc9e8415774707fbd63fa Mon Sep 17 00:00:00 2001 From: d3m1d0v Date: Mon, 2 Dec 2024 12:44:06 +0300 Subject: [PATCH] fix(Checkbox): correct handling of checkbox click --- .../yfm/Checkbox/CheckboxSpecs/const.ts | 8 ++ .../yfm/Checkbox/CheckboxSpecs/index.ts | 2 +- .../yfm/Checkbox/CheckboxSpecs/schema.ts | 15 ++-- .../yfm/Checkbox/CheckboxSpecs/serializer.ts | 5 +- src/extensions/yfm/Checkbox/const.ts | 2 +- src/extensions/yfm/Checkbox/index.ts | 69 +++-------------- src/extensions/yfm/Checkbox/nodeviews.ts | 77 +++++++++++++++++++ 7 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 src/extensions/yfm/Checkbox/nodeviews.ts diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts index 57f0070a..cdf2aceb 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts @@ -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'); diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts index 20c3b5be..72b01332 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts @@ -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); diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts index cde03e16..3210ec87 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts @@ -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'; @@ -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]; @@ -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]; @@ -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: { diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/serializer.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/serializer.ts index f689a326..b43ef871 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/serializer.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/serializer.ts @@ -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.Checkbox]: (state, node) => { @@ -9,7 +10,7 @@ export const serializerTokens: Record = { }, [CheckboxNode.Input]: (state, node) => { - const checked = node.attrs.checked === 'true'; + const checked = node.attrs[CheckboxAttr.Checked] === 'true'; state.write(`[${checked ? 'X' : ' '}] `); }, diff --git a/src/extensions/yfm/Checkbox/const.ts b/src/extensions/yfm/Checkbox/const.ts index ffc49b5c..e65f3b86 100644 --- a/src/extensions/yfm/Checkbox/const.ts +++ b/src/extensions/yfm/Checkbox/const.ts @@ -1 +1 @@ -export {CheckboxNode, b} from './CheckboxSpecs/const'; +export {CheckboxAttr, CheckboxNode, b} from './CheckboxSpecs/const'; diff --git a/src/extensions/yfm/Checkbox/index.ts b/src/extensions/yfm/Checkbox/index.ts index ae752e5d..4ca8c867 100644 --- a/src/extensions/yfm/Checkbox/index.ts +++ b/src/extensions/yfm/Checkbox/index.ts @@ -1,12 +1,9 @@ -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'; @@ -14,66 +11,20 @@ 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 & {}; export const Checkbox: ExtensionAuto = (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 diff --git a/src/extensions/yfm/Checkbox/nodeviews.ts b/src/extensions/yfm/Checkbox/nodeviews.ts new file mode 100644 index 00000000..7e9252f9 --- /dev/null +++ b/src/extensions/yfm/Checkbox/nodeviews.ts @@ -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(); + }; +}