editor: use single li element for simple checklists

This commit is contained in:
Abdullah Atta
2025-10-16 14:43:51 +05:00
parent b01d1fc87e
commit 0ba25419f3
2 changed files with 98 additions and 121 deletions

View File

@@ -17,8 +17,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { keybindings } from "@notesnook/common";
import { KeyboardShortcutCommand, mergeAttributes, Node } from "@tiptap/core";
import {
findParentNodeClosestToPos,
KeyboardShortcutCommand,
mergeAttributes,
Node
} from "@tiptap/core";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { CheckList } from "../check-list/check-list";
export interface CheckListItemOptions {
onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean;
@@ -97,98 +103,78 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
addNodeView() {
return ({ node, getPos, editor }) => {
const listItem = document.createElement("li");
const checkboxWrapper = document.createElement("div");
const content = document.createElement("div");
const isNested = node.lastChild?.type.name === CheckList.name;
checkboxWrapper.contentEditable = "false";
checkboxWrapper.className = "checkbox-wrapper";
checkboxWrapper.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/>
</svg>
`;
const li = document.createElement("li");
if (node.attrs.checked) li.classList.add("checked");
else li.classList.remove("checked");
content.className = "checklist-item-content";
function onClick(e: MouseEvent | TouchEvent) {
if (e instanceof MouseEvent && e.button !== 0) return;
if (!(e.target instanceof HTMLElement)) return;
checkboxWrapper.addEventListener("mousedown", (event) => {
if (globalThis.keyboardShown) {
event.preventDefault();
}
});
const pos = typeof getPos === "function" ? getPos() : 0;
if (typeof pos !== "number") return;
const resolvedPos = editor.state.doc.resolve(pos);
checkboxWrapper.addEventListener("click", (event) => {
event.preventDefault();
const { x, y, right } = li.getBoundingClientRect();
const isChecked = checkboxWrapper.classList.contains("checked");
const clientX =
e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
// if the editor isn't editable and we don't have a handler for
// readonly checks we have to undo the latest change
if (!editor.isEditable && !this.options.onReadOnlyChecked) {
return;
const clientY =
e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
const hitArea = { width: 40, height: 40 };
const isRtl =
e.target.dir === "rtl" ||
findParentNodeClosestToPos(
resolvedPos,
(node) => !!node.attrs.textDirection
)?.node.attrs.textDirection === "rtl";
let xStart = clientX >= x - hitArea.width;
let xEnd = clientX <= x;
const yStart = clientY >= y;
const yEnd = clientY <= y + hitArea.height;
if (isRtl) {
xEnd = clientX <= right + hitArea.width;
xStart = clientX >= right;
}
if (editor.isEditable && typeof getPos === "function") {
editor
.chain()
.command(({ tr }) => {
const position = getPos();
const currentNode = tr.doc.nodeAt(position);
tr.setNodeMarkup(position, undefined, {
...currentNode?.attrs,
checked: !isChecked
});
return true;
})
.run();
if (xStart && xEnd && yStart && yEnd) {
e.preventDefault();
editor.commands.command(({ tr }) => {
tr.setNodeAttribute(
pos,
"checked",
!li.classList.contains("checked")
);
return true;
});
}
if (!editor.isEditable && this.options.onReadOnlyChecked) {
// Reset state if onReadOnlyChecked returns false
if (!this.options.onReadOnlyChecked(node, !isChecked)) {
return;
}
}
});
if (node.attrs.checked) {
checkboxWrapper.classList.add("checked");
listItem.dataset.checked = node.attrs.checked;
}
listItem.append(checkboxWrapper, content);
li.onmousedown = onClick;
li.ontouchstart = onClick;
return {
dom: listItem,
contentDOM: content,
dom: li,
contentDOM: li,
update: (updatedNode) => {
if (updatedNode.type !== this.type) {
return false;
}
const isNested = updatedNode.lastChild?.type.name === CheckList.name;
listItem.dataset.checked = updatedNode.attrs.checked;
if (updatedNode.attrs.checked) {
checkboxWrapper.classList.add("checked");
} else {
checkboxWrapper.classList.remove("checked");
}
if (updatedNode.attrs.checked) li.classList.add("checked");
else li.classList.remove("checked");
return true;
}
};
};
}
// addInputRules() {
// return [
// wrappingInputRule({
// find: inputRegex,
// type: this.type,
// getAttributes: (match) => ({
// checked: match[match.length - 1] === "x"
// })
// })
// ];
// }
});

View File

@@ -76,6 +76,14 @@
margin-bottom: 5px;
}
.ProseMirror li:last-of-type {
margin-bottom: 0px;
}
.ProseMirror li:first-of-type {
margin-top: 5px;
}
.ProseMirror ul.tasklist-content-wrapper {
padding-left: 0px;
}
@@ -643,68 +651,51 @@ p > *::selection {
/* Check list */
.ProseMirror ul.simple-checklist {
list-style: none;
margin-left: -25px;
margin-block: 0px !important;
padding-inline: 0px !important;
padding-inline-start: 24px !important;
}
.ProseMirror ul.simple-checklist > li {
display: flex;
align-items: center;
.ProseMirror li.nested > ul.simple-checklist {
padding-inline-start: 15px !important;
}
.ProseMirror ul.simple-checklist > li > div.checklist-item-content {
flex: 1 1 auto;
display: flex;
justify-content: center;
flex-direction: column;
.simple-checklist li {
position: relative;
}
.ProseMirror ul.simple-checklist > li[data-checked="true"] > div.checklist-item-content {
opacity: 0.6;
}
.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper {
border: 2px solid var(--icon);
width: 0.9rem;
height: 0.9rem;
border-radius: 4px;
margin-right: 7px;
display: inline-flex;
align-items: center;
justify-content: center;
.simple-checklist > li::after {
position: absolute;
top: 0px;
cursor: pointer;
transition: all 250ms cubic-bezier(1, 0, .37, .91);
background-color: transparent;
content: "";
background-size: 18px;
width: 14px;
height: 14px;
border: 2px solid var(--icon);
border-radius: 5px;
left: -24px;
}
.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper:hover {
border-color: var(--accent);
.simple-checklist > li.checked::after {
border: 2px solid var(--accent);
}
.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper.checked {
border-color: var(--accent);
}
.simple-checklist > li.checked::before {
position: absolute;
top: 2px;
cursor: pointer;
content: "";
background-size: 18px;
width: 14px;
height: 14px;
left: -22px;
.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper svg {
width: 1rem;
height: 1rem;
fill: var(--accent);
opacity: 0;
transition: opacity 250ms cubic-bezier(1, 0, .37, .91);
}
.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper.checked svg {
opacity: 1;
}
@media screen and (max-width: 480px) {
.ProseMirror ul.simple-checklist > li > input {
height: 21px;
width: 21px;
}
.ProseMirror ul.simple-checklist > li > div {
margin-top: 2px;
}
background-color: var(--accent);
mask: url(data:image/svg+xml;base64,ICA8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxwYXRoIGQ9Ik0yMSw3TDksMTlMMy41LDEzLjVMNC45MSwxMi4wOUw5LDE2LjE3TDE5LjU5LDUuNTlMMjEsN1oiLz4KICA8L3N2Zz4K)
no-repeat 50% 50%;
mask-size: cover;
}
/* Callout */