98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
import {
|
|
ChangeSpec,
|
|
Decoration,
|
|
DecorationSet,
|
|
EditorView,
|
|
NodeType,
|
|
SyntaxNodeRef,
|
|
ViewPlugin,
|
|
ViewUpdate,
|
|
WidgetType,
|
|
} from "../deps.ts";
|
|
import { isCursorInRange, iterateTreeInVisibleRanges } from "./util.ts";
|
|
|
|
/**
|
|
* Plugin to add checkboxes in task lists.
|
|
*/
|
|
class TaskListsPlugin {
|
|
decorations: DecorationSet = Decoration.none;
|
|
constructor(view: EditorView) {
|
|
this.decorations = this.addCheckboxes(view);
|
|
}
|
|
update(update: ViewUpdate) {
|
|
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
|
this.decorations = this.addCheckboxes(update.view);
|
|
}
|
|
}
|
|
addCheckboxes(view: EditorView) {
|
|
const widgets: any[] = [];
|
|
iterateTreeInVisibleRanges(view, {
|
|
enter: this.iterateTree(view, widgets),
|
|
});
|
|
return Decoration.set(widgets, true);
|
|
}
|
|
|
|
private iterateTree(view: EditorView, widgets: any[]) {
|
|
return ({ type, from, to, node }: SyntaxNodeRef) => {
|
|
if (type.name !== "Task") return;
|
|
let checked = false;
|
|
// Iterate inside the task node to find the checkbox
|
|
node.toTree().iterate({
|
|
enter: (ref) => iterateInner(ref.type, ref.from, ref.to),
|
|
});
|
|
if (checked) {
|
|
widgets.push(
|
|
Decoration.mark({
|
|
tagName: "span",
|
|
class: "cm-task-checked",
|
|
}).range(from, to),
|
|
);
|
|
}
|
|
|
|
function iterateInner(type: NodeType, nfrom: number, nto: number) {
|
|
if (type.name !== "TaskMarker") return;
|
|
if (isCursorInRange(view.state, [from + nfrom, from + nto])) return;
|
|
const checkbox = view.state.sliceDoc(from + nfrom, from + nto);
|
|
// Checkbox is checked if it has a 'x' in between the []
|
|
if ("xX".includes(checkbox[1])) checked = true;
|
|
const dec = Decoration.replace({
|
|
widget: new CheckboxWidget(checked, from + nfrom + 1),
|
|
});
|
|
widgets.push(dec.range(from + nfrom, from + nto));
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Widget to render checkbox for a task list item.
|
|
*/
|
|
class CheckboxWidget extends WidgetType {
|
|
constructor(public checked: boolean, readonly pos: number) {
|
|
super();
|
|
}
|
|
toDOM(view: EditorView): HTMLElement {
|
|
const wrap = document.createElement("span");
|
|
wrap.classList.add("sb-checkbox");
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.checked = this.checked;
|
|
checkbox.addEventListener("click", ({ target }) => {
|
|
const change: ChangeSpec = {
|
|
from: this.pos,
|
|
to: this.pos + 1,
|
|
insert: this.checked ? " " : "x",
|
|
};
|
|
view.dispatch({ changes: change });
|
|
this.checked = !this.checked;
|
|
(target as HTMLInputElement).checked = this.checked;
|
|
});
|
|
wrap.appendChild(checkbox);
|
|
return wrap;
|
|
}
|
|
}
|
|
|
|
export const taskListPlugin = ViewPlugin.fromClass(TaskListsPlugin, {
|
|
decorations: (v) => v.decorations,
|
|
});
|