mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-18 04:37:51 +01:00
Merge branch 'master' into beta
This commit is contained in:
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { keybindings } from "@notesnook/common";
|
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 { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||||
|
import { CheckList } from "../check-list/check-list";
|
||||||
|
|
||||||
export interface CheckListItemOptions {
|
export interface CheckListItemOptions {
|
||||||
onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean;
|
onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean;
|
||||||
@@ -97,94 +103,76 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
|
|||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ({ node, getPos, editor }) => {
|
return ({ node, getPos, editor }) => {
|
||||||
const listItem = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
const checkboxWrapper = document.createElement("label");
|
if (node.attrs.checked) li.classList.add("checked");
|
||||||
const checkboxStyler = document.createElement("span");
|
else li.classList.remove("checked");
|
||||||
const checkbox = document.createElement("input");
|
|
||||||
const content = document.createElement("div");
|
|
||||||
|
|
||||||
checkboxWrapper.contentEditable = "false";
|
function onClick(e: MouseEvent | TouchEvent) {
|
||||||
checkbox.type = "checkbox";
|
if (e instanceof MouseEvent && e.button !== 0) return;
|
||||||
|
if (!(e.target instanceof HTMLElement)) return;
|
||||||
|
|
||||||
checkbox.addEventListener("mousedown", (event) => {
|
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||||
if (globalThis.keyboardShown) {
|
if (typeof pos !== "number") return;
|
||||||
event.preventDefault();
|
const resolvedPos = editor.state.doc.resolve(pos);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
checkbox.addEventListener("change", (event) => {
|
const { x, y, right } = li.getBoundingClientRect();
|
||||||
event.preventDefault();
|
|
||||||
// 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) {
|
|
||||||
checkbox.checked = !checkbox.checked;
|
|
||||||
|
|
||||||
return;
|
const clientX =
|
||||||
|
e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { checked } = event.target as any;
|
if (xStart && xEnd && yStart && yEnd) {
|
||||||
|
e.preventDefault();
|
||||||
if (editor.isEditable && typeof getPos === "function") {
|
editor.commands.command(({ tr }) => {
|
||||||
editor
|
tr.setNodeAttribute(
|
||||||
.chain()
|
pos,
|
||||||
.command(({ tr }) => {
|
"checked",
|
||||||
const position = getPos();
|
!li.classList.contains("checked")
|
||||||
const currentNode = tr.doc.nodeAt(position);
|
);
|
||||||
|
return true;
|
||||||
tr.setNodeMarkup(position, undefined, {
|
});
|
||||||
...currentNode?.attrs,
|
|
||||||
checked
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
if (!editor.isEditable && this.options.onReadOnlyChecked) {
|
|
||||||
// Reset state if onReadOnlyChecked returns false
|
|
||||||
if (!this.options.onReadOnlyChecked(node, checked)) {
|
|
||||||
checkbox.checked = !checkbox.checked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (node.attrs.checked) {
|
|
||||||
checkbox.setAttribute("checked", "checked");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkboxWrapper.append(checkbox, checkboxStyler);
|
li.onmousedown = onClick;
|
||||||
listItem.append(checkboxWrapper, content);
|
li.ontouchstart = onClick;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dom: listItem,
|
dom: li,
|
||||||
contentDOM: content,
|
contentDOM: li,
|
||||||
update: (updatedNode) => {
|
update: (updatedNode) => {
|
||||||
if (updatedNode.type !== this.type) {
|
if (updatedNode.type !== this.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const isNested = updatedNode.lastChild?.type.name === CheckList.name;
|
||||||
|
|
||||||
listItem.dataset.checked = updatedNode.attrs.checked;
|
if (updatedNode.attrs.checked) li.classList.add("checked");
|
||||||
if (updatedNode.attrs.checked) {
|
else li.classList.remove("checked");
|
||||||
checkbox.setAttribute("checked", "checked");
|
|
||||||
} else {
|
|
||||||
checkbox.removeAttribute("checked");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// addInputRules() {
|
|
||||||
// return [
|
|
||||||
// wrappingInputRule({
|
|
||||||
// find: inputRegex,
|
|
||||||
// type: this.type,
|
|
||||||
// getAttributes: (match) => ({
|
|
||||||
// checked: match[match.length - 1] === "x"
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// ];
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`migration > inline image in outline list 1`] = `
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "item 1",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "hello",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"align": undefined,
|
||||||
|
"aspectRatio": 1,
|
||||||
|
"filename": undefined,
|
||||||
|
"hash": undefined,
|
||||||
|
"height": null,
|
||||||
|
"mime": undefined,
|
||||||
|
"progress": 0,
|
||||||
|
"size": undefined,
|
||||||
|
"src": "image.png",
|
||||||
|
"type": "image",
|
||||||
|
"width": null,
|
||||||
|
},
|
||||||
|
"type": "image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "world",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "sub item 2",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "sub item 3",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineList",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "item 4",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineList",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`migration > inline image in paragraph 1`] = `
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "hello",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"align": undefined,
|
||||||
|
"aspectRatio": 1,
|
||||||
|
"filename": undefined,
|
||||||
|
"hash": undefined,
|
||||||
|
"height": null,
|
||||||
|
"mime": undefined,
|
||||||
|
"progress": 0,
|
||||||
|
"size": undefined,
|
||||||
|
"src": "image.png",
|
||||||
|
"type": "image",
|
||||||
|
"width": null,
|
||||||
|
},
|
||||||
|
"type": "image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "world",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
}
|
||||||
|
`;
|
||||||
75
packages/editor/src/extensions/image/__tests__/image.test.ts
Normal file
75
packages/editor/src/extensions/image/__tests__/image.test.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
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 {
|
||||||
|
createEditor,
|
||||||
|
h,
|
||||||
|
p,
|
||||||
|
outlineList,
|
||||||
|
outlineListItem
|
||||||
|
} from "../../../../test-utils/index.js";
|
||||||
|
import { test, expect, describe } from "vitest";
|
||||||
|
import { ImageNode } from "../index.js";
|
||||||
|
import { OutlineList } from "../../outline-list/outline-list.js";
|
||||||
|
import { OutlineListItem } from "../../outline-list-item/outline-list-item.js";
|
||||||
|
|
||||||
|
describe("migration", () => {
|
||||||
|
test(`inline image in paragraph`, async () => {
|
||||||
|
const el = p(["hello", h("img", [], { src: "image.png" }), "world"]);
|
||||||
|
const {
|
||||||
|
builder: { image },
|
||||||
|
editor
|
||||||
|
} = createEditor({
|
||||||
|
initialContent: el.outerHTML,
|
||||||
|
extensions: {
|
||||||
|
image: ImageNode.configure({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editor.getJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`inline image in outline list`, async () => {
|
||||||
|
const el = outlineList(
|
||||||
|
outlineListItem(["item 1"]),
|
||||||
|
outlineListItem(
|
||||||
|
["hello", h("img", [], { src: "image.png" }), "world"],
|
||||||
|
outlineList(
|
||||||
|
outlineListItem(["sub item 2"]),
|
||||||
|
outlineListItem(["sub item 3"])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
outlineListItem(["item 4"])
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
builder: { image },
|
||||||
|
editor
|
||||||
|
} = createEditor({
|
||||||
|
initialContent: el.outerHTML,
|
||||||
|
extensions: {
|
||||||
|
outlineList: OutlineList,
|
||||||
|
outlineListItem: OutlineListItem,
|
||||||
|
image: ImageNode
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editor.getJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -131,39 +131,6 @@ export const ImageNode = Node.create<ImageOptions>({
|
|||||||
getAttrs(node) {
|
getAttrs(node) {
|
||||||
if (node.querySelectorAll("img").length <= 0) return false;
|
if (node.querySelectorAll("img").length <= 0) return false;
|
||||||
return null;
|
return null;
|
||||||
},
|
|
||||||
getContent: (dom, schema) => {
|
|
||||||
const wrapper = document.createElement("div");
|
|
||||||
let buffer = "";
|
|
||||||
|
|
||||||
const flushBuffer = () => {
|
|
||||||
if (buffer.trim().length > 0) {
|
|
||||||
const pEl = document.createElement("p");
|
|
||||||
pEl.innerHTML = buffer;
|
|
||||||
wrapper.appendChild(pEl);
|
|
||||||
buffer = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const child of dom.childNodes) {
|
|
||||||
if (
|
|
||||||
child.nodeType === globalThis.Node.ELEMENT_NODE &&
|
|
||||||
(child as HTMLElement).tagName === "IMG"
|
|
||||||
) {
|
|
||||||
flushBuffer();
|
|
||||||
wrapper.appendChild(child);
|
|
||||||
} else {
|
|
||||||
if (child.nodeType === globalThis.Node.ELEMENT_NODE) {
|
|
||||||
buffer += (child as HTMLElement).outerHTML;
|
|
||||||
} else if (child.nodeType === globalThis.Node.TEXT_NODE) {
|
|
||||||
buffer += child.textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flushBuffer();
|
|
||||||
|
|
||||||
const parser = DOMParser.fromSchema(schema);
|
|
||||||
return parser.parse(wrapper).content;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`outline list item > code block in outline list item 1`] = `
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "item 1",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "hello",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"caretPosition": undefined,
|
||||||
|
"id": "codeblock-test-id-123456",
|
||||||
|
"indentLength": 2,
|
||||||
|
"indentType": "space",
|
||||||
|
"language": null,
|
||||||
|
"lines": [],
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "const x = 1;",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "codeblock",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "world",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "sub item 2",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "sub item 3",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineList",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collapsed": false,
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "item 4",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "paragraph",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineListItem",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "outlineList",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
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 {
|
||||||
|
createEditor,
|
||||||
|
h,
|
||||||
|
li,
|
||||||
|
outlineList,
|
||||||
|
outlineListItem
|
||||||
|
} from "../../../../test-utils/index.js";
|
||||||
|
import { test, expect, describe, beforeAll, vi } from "vitest";
|
||||||
|
import { OutlineList } from "../../outline-list/outline-list.js";
|
||||||
|
import { OutlineListItem } from "../outline-list-item.js";
|
||||||
|
import { CodeBlock } from "../../code-block/code-block.js";
|
||||||
|
|
||||||
|
describe("outline list item", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.mock("nanoid", () => ({
|
||||||
|
nanoid: () => "test-id-123456"
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`code block in outline list item`, async () => {
|
||||||
|
const subList = outlineList(
|
||||||
|
outlineListItem(["sub item 2"]),
|
||||||
|
outlineListItem(["sub item 3"])
|
||||||
|
);
|
||||||
|
const listItemWithCodeBlock = li(
|
||||||
|
[
|
||||||
|
h("p", ["hello"]),
|
||||||
|
h("pre", [h("code", ["const x = 1;"])]),
|
||||||
|
h("p", ["world"]),
|
||||||
|
subList
|
||||||
|
],
|
||||||
|
{ "data-type": "outlineListItem" }
|
||||||
|
);
|
||||||
|
const el = outlineList(
|
||||||
|
outlineListItem(["item 1"]),
|
||||||
|
listItemWithCodeBlock,
|
||||||
|
outlineListItem(["item 4"])
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
builder: { codeBlock },
|
||||||
|
editor
|
||||||
|
} = createEditor({
|
||||||
|
initialContent: el.outerHTML,
|
||||||
|
extensions: {
|
||||||
|
outlineList: OutlineList,
|
||||||
|
outlineListItem: OutlineListItem,
|
||||||
|
codeBlock: CodeBlock
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editor.getJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from "../../utils/prosemirror.js";
|
} from "../../utils/prosemirror.js";
|
||||||
import { OutlineList } from "../outline-list/outline-list.js";
|
import { OutlineList } from "../outline-list/outline-list.js";
|
||||||
import { keybindings, tiptapKeys } from "@notesnook/common";
|
import { keybindings, tiptapKeys } from "@notesnook/common";
|
||||||
|
import { Paragraph } from "../paragraph/paragraph.js";
|
||||||
|
|
||||||
export interface ListItemOptions {
|
export interface ListItemOptions {
|
||||||
HTMLAttributes: Record<string, unknown>;
|
HTMLAttributes: Record<string, unknown>;
|
||||||
@@ -55,13 +56,14 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
content: "paragraph+ list?",
|
content: "block+",
|
||||||
|
|
||||||
defining: true,
|
defining: true,
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
priority: 100,
|
||||||
tag: `li[data-type="${this.name}"]`
|
tag: `li[data-type="${this.name}"]`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -95,21 +97,37 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
Enter: () => {
|
Enter: ({ editor }) => {
|
||||||
// const subList = findSublist(editor, this.type);
|
const { $anchor } = editor.state.selection;
|
||||||
// if (!subList) return this.editor.commands.splitListItem(this.name);
|
if (
|
||||||
|
$anchor.parent.type.name !== Paragraph.name ||
|
||||||
// const { isCollapsed, subListPos } = subList;
|
($anchor.parent.type.name === Paragraph.name &&
|
||||||
|
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
|
||||||
// if (isCollapsed) {
|
)
|
||||||
// return this.editor.commands.toggleOutlineCollapse(subListPos, false);
|
return false;
|
||||||
// }
|
|
||||||
|
|
||||||
return this.editor.commands.splitListItem(this.name);
|
return this.editor.commands.splitListItem(this.name);
|
||||||
},
|
},
|
||||||
Tab: () => this.editor.commands.sinkListItem(this.name),
|
Tab: ({ editor }) => {
|
||||||
[keybindings.liftListItem.keys]: () =>
|
const { $anchor } = editor.state.selection;
|
||||||
this.editor.commands.liftListItem(this.name)
|
if (
|
||||||
|
$anchor.parent.type.name !== Paragraph.name ||
|
||||||
|
($anchor.parent.type.name === Paragraph.name &&
|
||||||
|
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
return this.editor.commands.sinkListItem(this.name);
|
||||||
|
},
|
||||||
|
[keybindings.liftListItem.keys]: ({ editor }) => {
|
||||||
|
const { $anchor } = editor.state.selection;
|
||||||
|
if (
|
||||||
|
$anchor.parent.type.name !== Paragraph.name ||
|
||||||
|
($anchor.parent.type.name === Paragraph.name &&
|
||||||
|
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
return this.editor.commands.liftListItem(this.name);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -127,7 +145,7 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
|||||||
|
|
||||||
function onClick(e: MouseEvent | TouchEvent) {
|
function onClick(e: MouseEvent | TouchEvent) {
|
||||||
if (e instanceof MouseEvent && e.button !== 0) return;
|
if (e instanceof MouseEvent && e.button !== 0) return;
|
||||||
if (!(e.target instanceof HTMLParagraphElement)) return;
|
if (!(e.target instanceof HTMLElement)) return;
|
||||||
if (!li.classList.contains("nested")) return;
|
if (!li.classList.contains("nested")) return;
|
||||||
|
|
||||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const TEXT_DIRECTION_TYPES = [
|
|||||||
"orderedList",
|
"orderedList",
|
||||||
"bulletList",
|
"bulletList",
|
||||||
"outlineList",
|
"outlineList",
|
||||||
|
"checkList",
|
||||||
"taskList",
|
"taskList",
|
||||||
"table",
|
"table",
|
||||||
"blockquote",
|
"blockquote",
|
||||||
|
|||||||
@@ -76,6 +76,14 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror li:last-of-type {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror li:first-of-type {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror ul.tasklist-content-wrapper {
|
.ProseMirror ul.tasklist-content-wrapper {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
@@ -536,11 +544,11 @@ p > *::selection {
|
|||||||
.outline-list li.collapsed .outline-list {
|
.outline-list li.collapsed .outline-list {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.outline-list li > :first-child {
|
.outline-list li {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline-list > li > :first-child::before {
|
.outline-list > li::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -560,13 +568,13 @@ p > *::selection {
|
|||||||
left: -22px;
|
left: -22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline-list li:not(.nested) > :first-child::before {
|
.outline-list li:not(.nested)::before {
|
||||||
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik0xMiAyQTEwIDEwIDAgMCAwIDIgMTJhMTAgMTAgMCAwIDAgMTAgMTBhMTAgMTAgMCAwIDAgMTAtMTBBMTAgMTAgMCAwIDAgMTIgMloiLz48L3N2Zz4=)
|
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik0xMiAyQTEwIDEwIDAgMCAwIDIgMTJhMTAgMTAgMCAwIDAgMTAgMTBhMTAgMTAgMCAwIDAgMTAtMTBBMTAgMTAgMCAwIDAgMTIgMloiLz48L3N2Zz4=)
|
||||||
no-repeat 50% 50%;
|
no-repeat 50% 50%;
|
||||||
scale: 0.4;
|
scale: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline-list li.collapsed > :first-child::before {
|
.outline-list li.collapsed::before {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,40 +646,71 @@ p > *::selection {
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.simple-checklist[dir="rtl"] li::after {
|
||||||
|
left: unset;
|
||||||
|
right: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-checklist[dir="rtl"] li.checked::before {
|
||||||
|
left: unset;
|
||||||
|
right: -22px;
|
||||||
|
}
|
||||||
|
|
||||||
[dir="rtl"] .taskItemTools { right: unset; left: 0 }
|
[dir="rtl"] .taskItemTools { right: unset; left: 0 }
|
||||||
|
|
||||||
/* Check list */
|
/* Check list */
|
||||||
.ProseMirror ul.simple-checklist {
|
.ProseMirror ul.simple-checklist {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
margin-block: 0px !important;
|
||||||
|
padding-inline: 0px !important;
|
||||||
|
padding-inline-start: 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror ul.simple-checklist > li {
|
.ProseMirror li.nested > ul.simple-checklist {
|
||||||
display: flex;
|
padding-inline-start: 15px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror ul.simple-checklist > li input {
|
.simple-checklist li {
|
||||||
flex: 0 0 auto;
|
position: relative;
|
||||||
margin-right: 0.5rem;
|
|
||||||
user-select: none;
|
|
||||||
height: 1rem;
|
|
||||||
width: 1rem;
|
|
||||||
accent-color: var(--accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror ul.simple-checklist > li > div {
|
.simple-checklist > li::after {
|
||||||
flex: 1 1 auto;
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
content: "";
|
||||||
|
background-size: 18px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
border: 2px solid var(--icon);
|
||||||
|
border-radius: 5px;
|
||||||
|
left: -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
.simple-checklist > li.checked::after {
|
||||||
.ProseMirror ul.simple-checklist > li > input {
|
border: 2px solid var(--accent);
|
||||||
height: 21px;
|
}
|
||||||
width: 21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror ul.simple-checklist > li > div {
|
.simple-checklist > li.checked::before {
|
||||||
margin-top: 2px;
|
position: absolute;
|
||||||
}
|
top: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
content: "";
|
||||||
|
background-size: 18px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
left: -22px;
|
||||||
|
|
||||||
|
background-color: var(--accent);
|
||||||
|
mask: url(data:image/svg+xml;base64,ICA8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxwYXRoIGQ9Ik0yMSw3TDksMTlMMy41LDEzLjVMNC45MSwxMi4wOUw5LDE2LjE3TDE5LjU5LDUuNTlMMjEsN1oiLz4KICA8L3N2Zz4K)
|
||||||
|
no-repeat 50% 50%;
|
||||||
|
mask-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-checklist > li.checked > p {
|
||||||
|
opacity: 0.8;
|
||||||
|
text-decoration-line: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Callout */
|
/* Callout */
|
||||||
|
|||||||
@@ -92,3 +92,19 @@ export const p = elem("p");
|
|||||||
export function text(text: string) {
|
export function text(text: string) {
|
||||||
return document.createTextNode(text);
|
return document.createTextNode(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function outlineList(...children: HTMLLIElement[]) {
|
||||||
|
return ul(children, { "data-type": "outlineList" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function outlineListItem(
|
||||||
|
paragraphChildren: (string | HTMLElement)[],
|
||||||
|
subList?: HTMLUListElement
|
||||||
|
) {
|
||||||
|
const children: HTMLElement[] = [h("p", paragraphChildren)];
|
||||||
|
if (subList) children.push(subList);
|
||||||
|
|
||||||
|
return li(children, {
|
||||||
|
"data-type": "outlineListItem"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user