Files

153 lines
4.3 KiB
TypeScript

import { Decoration, EditorView, WidgetType } from '@codemirror/view';
import { SyntaxNodeRef } from '@lezer/common';
import makeReplaceExtension from './utils/makeInlineReplaceExtension';
import toggleCheckboxAt from '../../utils/markdown/toggleCheckboxAt';
const checkboxClassName = 'cm-ext-checkbox-toggle';
class CheckboxWidget extends WidgetType {
public constructor(private checked: boolean, private depth: number, private label: string) {
super();
}
public eq(other: CheckboxWidget) {
return other.checked === this.checked && other.depth === this.depth && other.label === this.label;
}
private applyContainerClasses(container: HTMLElement) {
container.classList.add(checkboxClassName);
for (const className of [...container.classList]) {
if (className.startsWith('-depth-')) {
container.classList.remove(className);
}
}
container.classList.add(`-depth-${this.depth}`);
}
public toDOM(view: EditorView) {
const container = document.createElement('span');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = this.checked;
checkbox.ariaLabel = this.label;
checkbox.title = this.label;
container.appendChild(checkbox);
checkbox.oninput = () => {
toggleCheckboxAt(view.posAtDOM(container))(view);
};
this.applyContainerClasses(container);
return container;
}
public updateDOM(dom: HTMLElement): boolean {
this.applyContainerClasses(dom);
const input = dom.querySelector('input');
if (input) {
input.checked = this.checked;
return true;
}
return false;
}
public ignoreEvent() {
return false;
}
}
const completedTaskClassName = 'cm-md-completed-item';
const completedListItemDecoration = Decoration.line({ class: completedTaskClassName, isFullLine: true });
const replaceCheckboxes = [
EditorView.theme({
[`& .${checkboxClassName}`]: {
'& > input': {
width: '1.1em',
height: '1.1em',
margin: '4px',
verticalAlign: 'middle',
},
'&:not(.-depth-1) > input': {
marginInlineStart: 0,
},
},
[`& .${completedTaskClassName}`]: {
opacity: 0.69,
},
}),
EditorView.domEventHandlers({
mousedown: (event) => {
const target = event.target as Element;
if (target.nodeName === 'INPUT' && target.parentElement?.classList?.contains(checkboxClassName)) {
// Let the checkbox handle the event
return true;
}
return false;
},
}),
makeReplaceExtension({
getRevealStrategy: (node, state) => {
if (node.name === 'TaskMarker') {
const container = node.node.parent?.parent;
const listMarker = container?.getChild('ListMark');
// Intersection check logic similar to nodeIntersectsSelection but with custom range
const selection = state.selection.main;
const rangeFrom = listMarker ? listMarker.from : node.from;
const rangeTo = node.to;
const rangeContains = (point: number) => point >= rangeFrom && point <= rangeTo;
const selectionContains = (point: number) => point >= selection.from && point <= selection.to;
// Reveal if cursor touches the checkbox or the list bullet point
return rangeContains(selection.from) || rangeContains(selection.to)
|| selectionContains(rangeFrom) || selectionContains(rangeTo);
}
return 'line';
},
createDecoration: (node, state, parentTags) => {
const markerIsChecked = (marker: SyntaxNodeRef) => {
const content = state.doc.sliceString(marker.from, marker.to);
return content.toLowerCase().indexOf('x') !== -1;
};
if (node.name === 'TaskMarker') {
const containerLine = state.doc.lineAt(node.from);
const labelText = state.doc.sliceString(node.to, containerLine.to);
return new CheckboxWidget(markerIsChecked(node), parentTags.get('ListItem') ?? 0, labelText);
} else if (node.name === 'Task') {
const marker = node.node.getChild('TaskMarker');
if (marker && markerIsChecked(marker)) {
return completedListItemDecoration;
}
}
return null;
},
getDecorationRange: (node, state) => {
if (node.name === 'TaskMarker') {
const container = node.node.parent?.parent;
const listMarker = container?.getChild('ListMark');
if (!listMarker) {
return null;
}
return [listMarker.from, node.to];
} else if (node.name === 'Task') {
const taskLine = state.doc.lineAt(node.from);
return [taskLine.from];
}
return null;
},
}),
];
export default replaceCheckboxes;