editor: add support for simple checklists

This commit is contained in:
Abdullah Atta
2023-12-21 11:08:54 +05:00
parent e3f08668ed
commit 4ea4bbd782
10 changed files with 44 additions and 85 deletions

View File

@@ -2214,6 +2214,8 @@
},
"node_modules/@streetwriters/showdown": {
"version": "3.0.5-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.5-alpha.tgz",
"integrity": "sha512-jD9JFhxLDx6XeyZOLVB0zWtwGduwNiFpxn5rxu6ThyKyWGnu1O+L1w04WLC1L56pyEhypr3Tsk24dzo2Se/50g==",
"license": "MIT",
"bin": {
"showdown": "bin/showdown.js"

View File

@@ -59,11 +59,12 @@ export class Tiptap {
preserveNewlines: true,
selectors: [
{ selector: "table", format: "dataTable" },
{ selector: "ul.checklist", format: "taskList" },
{ selector: "ul.checklist", format: "checkList" },
{ selector: "ul.simple-checklist", format: "checkList" },
{ selector: "p", format: "paragraph" }
],
formatters: {
taskList: (elem, walk, builder, formatOptions) => {
checkList: (elem, walk, builder, formatOptions) => {
return formatList(elem, walk, builder, formatOptions, (elem) => {
return elem.attribs.class && elem.attribs.class.includes("checked")
? " ✅ "

View File

@@ -28,7 +28,6 @@ export interface CheckListItemOptions {
onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean;
nested: boolean;
HTMLAttributes: Record<string, any>;
checkListTypeName: string;
}
export const inputRegex = /^\s*(\[([( |x])?\])\s$/;
@@ -39,8 +38,7 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
addOptions() {
return {
nested: false,
HTMLAttributes: {},
checkListTypeName: "checkList"
HTMLAttributes: {}
};
},
@@ -55,9 +53,9 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
checked: {
default: false,
keepOnSplit: false,
parseHTML: (element) => element.getAttribute("data-checked") === "true",
parseHTML: (element) => element.classList.contains("checked"),
renderHTML: (attributes) => ({
"data-checked": attributes.checked
class: attributes.checked ? "checked" : ""
})
}
};
@@ -66,30 +64,19 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
parseHTML() {
return [
{
tag: `li[data-type="${this.name}"]`,
tag: `li.simple-checklist--item`,
priority: 51
}
];
},
renderHTML({ node, HTMLAttributes }) {
renderHTML({ HTMLAttributes }) {
return [
"li",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name
class: "simple-checklist--item"
}),
[
"label",
[
"input",
{
type: "checkbox",
checked: node.attrs.checked ? "checked" : null
}
],
["span"]
],
["div", 0]
0
];
},
@@ -112,16 +99,15 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
},
addNodeView() {
return ({ node, HTMLAttributes, getPos, editor }) => {
return ({ node, getPos, editor }) => {
const listItem = document.createElement("li");
const checkboxWrapper = document.createElement("label");
const checkboxStyler = document.createElement("span");
const checkbox = document.createElement("input");
const content = document.createElement("div");
checkboxWrapper.contentEditable = "false";
checkbox.contentEditable = "false";
checkbox.type = "checkbox";
checkbox.addEventListener("change", (event) => {
event.preventDefault();
// if the editor isnt editable and we don't have a handler for
// readonly checks we have to undo the latest change
if (!editor.isEditable && !this.options.onReadOnlyChecked) {
@@ -135,7 +121,6 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
if (editor.isEditable && typeof getPos === "function") {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.command(({ tr }) => {
const position = getPos();
const currentNode = tr.doc.nodeAt(position);
@@ -157,21 +142,11 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
}
});
Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => {
listItem.setAttribute(key, value);
});
listItem.dataset.checked = node.attrs.checked;
if (node.attrs.checked) {
checkbox.setAttribute("checked", "checked");
}
checkboxWrapper.append(checkbox, checkboxStyler);
listItem.append(checkboxWrapper, content);
Object.entries(HTMLAttributes).forEach(([key, value]) => {
listItem.setAttribute(key, value);
});
listItem.append(checkbox, content);
return {
dom: listItem,

View File

@@ -53,7 +53,7 @@ export const CheckList = Node.create<CheckListOptions>({
parseHTML() {
return [
{
tag: `ul[data-type="${this.name}"]`,
tag: `ul.simple-checklist`,
priority: 51
}
];
@@ -63,7 +63,7 @@ export const CheckList = Node.create<CheckListOptions>({
return [
"ul",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name
class: "simple-checklist"
}),
0
];

View File

@@ -55,16 +55,7 @@ export const TaskItemNode = TaskItem.extend({
parseHTML() {
return [
{
tag: "li",
getAttrs: (node: any) => {
if (node instanceof Node && node instanceof HTMLElement) {
return node.classList.contains("checklist--item") ||
node.parentElement?.classList.contains("checklist")
? null
: false;
}
return false;
},
tag: "li.checklist--item",
priority: 51
}
];

View File

@@ -89,13 +89,7 @@ export const TaskListNode = TaskList.extend({
parseHTML() {
return [
{
tag: "ul",
getAttrs: (node) => {
if (node instanceof Node && node instanceof HTMLElement) {
return node.classList.contains("checklist") && null;
}
return false;
},
tag: "ul.checklist",
priority: 51
}
];

View File

@@ -284,7 +284,9 @@ const useTiptap = (
KeyMap,
WebClipNode,
CheckList,
CheckListItem,
CheckListItem.configure({
nested: true
}),
// Quirks handlers
Quirks.configure({

View File

@@ -105,8 +105,8 @@ const tools: Record<ToolId, ToolDefinition> = {
title: "Numbered list"
},
checkList: {
icon: "checkList",
title: "Numbered list"
icon: "checklist",
title: "Checklist"
},
fontFamily: {
icon: "fontFamily",

View File

@@ -43,6 +43,8 @@ import { TaskItemNode } from "../extensions/task-item";
import { TaskListNode } from "../extensions/task-list";
import { LIST_NODE_TYPES } from "./node-types";
import TextStyle from "@tiptap/extension-text-style";
import CheckList from "../extensions/check-list";
import CheckListItem from "../extensions/check-list-item";
export type NodeWithOffset = {
node?: ProsemirrorNode;
@@ -61,6 +63,7 @@ export function hasSameAttributes(prev: Attrs, next: Attrs) {
export function findListItemType(editor: Editor): string | null {
const isTaskList = editor.isActive(TaskListNode.name);
const isCheckList = editor.isActive(CheckList.name);
const isOutlineList = editor.isActive(OutlineList.name);
const isList =
editor.isActive(BulletList.name) || editor.isActive(OrderedList.name);
@@ -71,6 +74,8 @@ export function findListItemType(editor: Editor): string | null {
? OutlineListItem.name
: isTaskList
? TaskItemNode.name
: isCheckList
? CheckListItem.name
: null;
}

View File

@@ -621,34 +621,23 @@ p > *::selection {
[dir="rtl"] .taskItemTools { right: unset; left: 0 }
/* Check list */
.ProseMirror ul[data-type="checkList"] {
.ProseMirror ul.simple-checklist {
list-style: none;
padding: 0;
}
p {
margin: 0;
}
.ProseMirror ul.simple-checklist > li {
display: flex;
}
li {
display: flex;
.ProseMirror ul.simple-checklist > li > input {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
height: 18px;
accent-color: var(--accent);
}
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
ul li,
ol li {
display: list-item;
}
ul[data-type="checkList"] > li {
display: flex;
}
}
.ProseMirror ul.simple-checklist > li > div {
flex: 1 1 auto;
}