mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
Merge pull request #8557 from 01zulfi/editor/refactor-collapsible-headings
editor: make headings collapsible
This commit is contained in:
@@ -704,8 +704,10 @@ function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
editor.style.marginLeft = `-${negativeSpace}px`;
|
if (!isMobile() && !isTablet()) {
|
||||||
editor.style.marginRight = `-${negativeSpace}px`;
|
editor.style.marginLeft = `-${negativeSpace}px`;
|
||||||
|
editor.style.marginRight = `-${negativeSpace}px`;
|
||||||
|
}
|
||||||
editor.style.paddingLeft = `${negativeSpace}px`;
|
editor.style.paddingLeft = `${negativeSpace}px`;
|
||||||
editor.style.paddingRight = `${negativeSpace}px`;
|
editor.style.paddingRight = `${negativeSpace}px`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
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 { getParentAttributes } from "../../utils/prosemirror.js";
|
import {
|
||||||
|
getParentAttributes,
|
||||||
|
isClickWithinBounds
|
||||||
|
} from "../../utils/prosemirror.js";
|
||||||
import {
|
import {
|
||||||
InputRule,
|
InputRule,
|
||||||
Node,
|
Node,
|
||||||
@@ -235,37 +238,9 @@ export const Callout = Node.create({
|
|||||||
|
|
||||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||||
if (typeof pos !== "number") return;
|
if (typeof pos !== "number") return;
|
||||||
|
|
||||||
const resolvedPos = editor.state.doc.resolve(pos);
|
const resolvedPos = editor.state.doc.resolve(pos);
|
||||||
|
if (isClickWithinBounds(e, resolvedPos, "right")) {
|
||||||
const { x, y, width } = e.target.getBoundingClientRect();
|
|
||||||
|
|
||||||
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 xEnd = clientX <= x + width;
|
|
||||||
let xStart = clientX >= x + width - hitArea.width;
|
|
||||||
|
|
||||||
const yStart = clientY >= y;
|
|
||||||
const yEnd = clientY <= y + hitArea.height;
|
|
||||||
|
|
||||||
if (isRtl) {
|
|
||||||
xStart = clientX >= x;
|
|
||||||
xEnd = clientX <= x + hitArea.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xStart && xEnd && yStart && yEnd) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
@@ -283,6 +258,12 @@ export const Callout = Node.create({
|
|||||||
container.onmousedown = onClick;
|
container.onmousedown = onClick;
|
||||||
container.ontouchstart = onClick;
|
container.ontouchstart = onClick;
|
||||||
|
|
||||||
|
if (node.attrs.hiddenUnder) {
|
||||||
|
container.dataset.hiddenUnder = node.attrs.hiddenUnder;
|
||||||
|
} else {
|
||||||
|
delete container.dataset.hiddenUnder;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dom: container,
|
dom: container,
|
||||||
contentDOM: container,
|
contentDOM: container,
|
||||||
@@ -294,6 +275,10 @@ export const Callout = Node.create({
|
|||||||
if (updatedNode.attrs.collapsed) container.classList.add("collapsed");
|
if (updatedNode.attrs.collapsed) container.classList.add("collapsed");
|
||||||
else container.classList.remove("collapsed");
|
else container.classList.remove("collapsed");
|
||||||
|
|
||||||
|
if (updatedNode.attrs.hiddenUnder)
|
||||||
|
container.dataset.hiddenUnder = updatedNode.attrs.hiddenUnder;
|
||||||
|
else delete container.dataset.hiddenUnder;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -554,7 +554,8 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
|||||||
return (
|
return (
|
||||||
compareCaretPosition(prev.caretPosition, next.caretPosition) ||
|
compareCaretPosition(prev.caretPosition, next.caretPosition) ||
|
||||||
prev.language !== next.language ||
|
prev.language !== next.language ||
|
||||||
prev.indentType !== next.indentType
|
prev.indentType !== next.indentType ||
|
||||||
|
prev.hiddenUnder !== next.hiddenUnder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`collapse heading > heading collapsed 1`] = `"<h1 data-collapsed="true">Main Heading</h1><p>paragraph.</p><h2>Subheading</h2><p>subheading paragraph</p><h1>Main heading 2</h1><p>paragraph another</p>"`;
|
||||||
|
|
||||||
|
exports[`collapse heading > heading uncollapsed 1`] = `"<h1>Main Heading</h1><p>paragraph.</p><h2>Subheading</h2><p>subheading paragraph</p><h1>Main heading 2</h1><p>paragraph another</p>"`;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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 { test, expect } from "vitest";
|
||||||
|
import { createEditor } from "../../../../test-utils/index.js";
|
||||||
|
import { Heading } from "../heading.js";
|
||||||
|
|
||||||
|
test("collapse heading", () => {
|
||||||
|
const { editor } = createEditor({
|
||||||
|
extensions: {
|
||||||
|
heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] })
|
||||||
|
},
|
||||||
|
initialContent: `
|
||||||
|
<h1>Main Heading</h1>
|
||||||
|
<p>paragraph.</p>
|
||||||
|
<h2>Subheading</h2>
|
||||||
|
<p>subheading paragraph</p>
|
||||||
|
<h1>Main heading 2</h1>
|
||||||
|
<p>paragraph another</p>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const headingPos = 0;
|
||||||
|
|
||||||
|
editor.commands.command(({ tr }) => {
|
||||||
|
tr.setNodeAttribute(headingPos, "collapsed", true);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editor.getHTML()).toMatchSnapshot("heading collapsed");
|
||||||
|
|
||||||
|
editor.commands.command(({ tr }) => {
|
||||||
|
tr.setNodeAttribute(headingPos, "collapsed", false);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(editor.getHTML()).toMatchSnapshot("heading uncollapsed");
|
||||||
|
});
|
||||||
@@ -18,11 +18,49 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { tiptapKeys } from "@notesnook/common";
|
import { tiptapKeys } from "@notesnook/common";
|
||||||
import { textblockTypeInputRule } from "@tiptap/core";
|
import {
|
||||||
|
findParentNodeClosestToPos,
|
||||||
|
textblockTypeInputRule
|
||||||
|
} from "@tiptap/core";
|
||||||
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
|
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
|
||||||
|
import { isClickWithinBounds } from "../../utils/prosemirror";
|
||||||
|
import { Selection, Transaction } from "@tiptap/pm/state";
|
||||||
|
import { Node } from "@tiptap/pm/model";
|
||||||
|
|
||||||
|
const COLLAPSIBLE_BLOCK_TYPES = [
|
||||||
|
"paragraph",
|
||||||
|
"heading",
|
||||||
|
"blockquote",
|
||||||
|
"bulletList",
|
||||||
|
"orderedList",
|
||||||
|
"checkList",
|
||||||
|
"taskList",
|
||||||
|
"table",
|
||||||
|
"callout",
|
||||||
|
"codeblock",
|
||||||
|
"image",
|
||||||
|
"outlineList",
|
||||||
|
"mathBlock",
|
||||||
|
"webclip",
|
||||||
|
"embed"
|
||||||
|
];
|
||||||
|
|
||||||
const HEADING_REGEX = /^(#{1,6})\s$/;
|
const HEADING_REGEX = /^(#{1,6})\s$/;
|
||||||
export const Heading = TiptapHeading.extend({
|
export const Heading = TiptapHeading.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
collapsed: {
|
||||||
|
default: false,
|
||||||
|
keepOnSplit: false,
|
||||||
|
parseHTML: (element) => element.dataset.collapsed === "true",
|
||||||
|
renderHTML: (attributes) => ({
|
||||||
|
"data-collapsed": attributes.collapsed === true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
@@ -45,17 +83,65 @@ export const Heading = TiptapHeading.extend({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
addKeyboardShortcuts() {
|
addGlobalAttributes() {
|
||||||
return this.options.levels.reduce(
|
return [
|
||||||
(items, level) => ({
|
{
|
||||||
...items,
|
types: COLLAPSIBLE_BLOCK_TYPES,
|
||||||
...{
|
attributes: {
|
||||||
[tiptapKeys[`insertHeading${level}`].keys]: () =>
|
hiddenUnder: {
|
||||||
this.editor.commands.setHeading({ level })
|
default: null,
|
||||||
|
keepOnSplit: false,
|
||||||
|
parseHTML: (element) => element.dataset.hiddenUnder || null,
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
if (!attributes.hiddenUnder) return {};
|
||||||
|
return {
|
||||||
|
"data-hidden-under": attributes.hiddenUnder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
{}
|
];
|
||||||
);
|
},
|
||||||
|
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
...this.options.levels.reduce(
|
||||||
|
(items, level) => ({
|
||||||
|
...items,
|
||||||
|
...{
|
||||||
|
[tiptapKeys[`insertHeading${level}`].keys]: () =>
|
||||||
|
this.editor.commands.setHeading({ level })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
Enter: ({ editor }) => {
|
||||||
|
const { state, commands } = editor;
|
||||||
|
const { $from } = state.selection;
|
||||||
|
const node = $from.node();
|
||||||
|
if (node.type.name !== this.name) return false;
|
||||||
|
|
||||||
|
const isAtEnd = $from.parentOffset === node.textContent.length;
|
||||||
|
if (isAtEnd && node.attrs.collapsed) {
|
||||||
|
const headingPos = $from.before();
|
||||||
|
const endPos = findEndOfCollapsedSection(
|
||||||
|
state.doc,
|
||||||
|
headingPos,
|
||||||
|
node.attrs.level
|
||||||
|
);
|
||||||
|
if (endPos === -1) return false;
|
||||||
|
|
||||||
|
return commands.command(({ tr }) => {
|
||||||
|
tr.insert(endPos, state.schema.nodes.paragraph.create());
|
||||||
|
const newPos = endPos + 1;
|
||||||
|
tr.setSelection(Selection.near(tr.doc.resolve(newPos)));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
@@ -71,5 +157,171 @@ export const Heading = TiptapHeading.extend({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ({ node, getPos, editor, HTMLAttributes }) => {
|
||||||
|
const heading = document.createElement(`h${node.attrs.level}`);
|
||||||
|
|
||||||
|
for (const attr in HTMLAttributes) {
|
||||||
|
heading.setAttribute(attr, HTMLAttributes[attr]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.attrs.collapsed) heading.dataset.collapsed = "true";
|
||||||
|
else delete heading.dataset.collapsed;
|
||||||
|
|
||||||
|
function onClick(e: MouseEvent | TouchEvent) {
|
||||||
|
if (e instanceof MouseEvent && e.button !== 0) return;
|
||||||
|
if (!(e.target instanceof HTMLHeadingElement)) return;
|
||||||
|
|
||||||
|
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||||
|
if (typeof pos !== "number") return;
|
||||||
|
|
||||||
|
const resolvedPos = editor.state.doc.resolve(pos);
|
||||||
|
const calloutAncestor = findParentNodeClosestToPos(
|
||||||
|
resolvedPos,
|
||||||
|
(node) => node.type.name === "callout"
|
||||||
|
);
|
||||||
|
if (calloutAncestor) return;
|
||||||
|
|
||||||
|
if (isClickWithinBounds(e, resolvedPos, "left")) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
|
editor.commands.command(({ tr }) => {
|
||||||
|
const currentNode = tr.doc.nodeAt(pos);
|
||||||
|
if (currentNode && currentNode.type.name === "heading") {
|
||||||
|
const shouldCollapse = !currentNode.attrs.collapsed;
|
||||||
|
const headingLevel = currentNode.attrs.level;
|
||||||
|
const headingId = currentNode.attrs.blockId;
|
||||||
|
|
||||||
|
tr.setNodeAttribute(pos, "collapsed", shouldCollapse);
|
||||||
|
toggleNodesUnderHeading(
|
||||||
|
tr,
|
||||||
|
pos,
|
||||||
|
headingLevel,
|
||||||
|
shouldCollapse,
|
||||||
|
headingId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heading.onmousedown = onClick;
|
||||||
|
heading.ontouchstart = onClick;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom: heading,
|
||||||
|
contentDOM: heading,
|
||||||
|
update: (updatedNode) => {
|
||||||
|
if (updatedNode.type !== this.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedNode.attrs.level !== node.attrs.level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedNode.attrs.collapsed) heading.dataset.collapsed = "true";
|
||||||
|
else delete heading.dataset.collapsed;
|
||||||
|
|
||||||
|
if (updatedNode.attrs.hiddenUnder)
|
||||||
|
heading.dataset.hiddenUnder = updatedNode.attrs.hiddenUnder;
|
||||||
|
else delete heading.dataset.hiddenUnder;
|
||||||
|
|
||||||
|
if (updatedNode.attrs.textAlign)
|
||||||
|
heading.style.textAlign =
|
||||||
|
updatedNode.attrs.textAlign === "left"
|
||||||
|
? ""
|
||||||
|
: updatedNode.attrs.textAlign;
|
||||||
|
|
||||||
|
if (updatedNode.attrs.textDirection)
|
||||||
|
heading.dir = updatedNode.attrs.textDirection;
|
||||||
|
else heading.dir = "";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toggleNodesUnderHeading(
|
||||||
|
tr: Transaction,
|
||||||
|
headingPos: number,
|
||||||
|
headingLevel: number,
|
||||||
|
isCollapsing: boolean,
|
||||||
|
headingId: string
|
||||||
|
) {
|
||||||
|
const { doc } = tr;
|
||||||
|
const headingNode = doc.nodeAt(headingPos);
|
||||||
|
if (!headingNode || headingNode.type.name !== "heading") return;
|
||||||
|
|
||||||
|
let nextPos = headingPos + headingNode.nodeSize;
|
||||||
|
const cursorPos = tr.selection.from;
|
||||||
|
let shouldMoveCursor = false;
|
||||||
|
|
||||||
|
while (nextPos < doc.content.size) {
|
||||||
|
const nextNode = doc.nodeAt(nextPos);
|
||||||
|
if (!nextNode) break;
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextNode.type.name === "heading" &&
|
||||||
|
nextNode.attrs.level <= headingLevel
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isCollapsing &&
|
||||||
|
cursorPos >= nextPos &&
|
||||||
|
cursorPos < nextPos + nextNode.nodeSize
|
||||||
|
) {
|
||||||
|
shouldMoveCursor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (COLLAPSIBLE_BLOCK_TYPES.includes(nextNode.type.name)) {
|
||||||
|
if (isCollapsing && typeof nextNode.attrs.hiddenUnder !== "string") {
|
||||||
|
tr.setNodeAttribute(nextPos, "hiddenUnder", headingId);
|
||||||
|
} else if (!isCollapsing && nextNode.attrs.hiddenUnder === headingId) {
|
||||||
|
tr.setNodeAttribute(nextPos, "hiddenUnder", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPos += nextNode.nodeSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldMoveCursor) {
|
||||||
|
const headingEndPos = headingPos + headingNode.nodeSize - 1;
|
||||||
|
tr.setSelection(Selection.near(tr.doc.resolve(headingEndPos)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEndOfCollapsedSection(
|
||||||
|
doc: Node,
|
||||||
|
headingPos: number,
|
||||||
|
headingLevel: number
|
||||||
|
) {
|
||||||
|
const headingNode = doc.nodeAt(headingPos);
|
||||||
|
if (!headingNode || headingNode.type.name !== "heading") return -1;
|
||||||
|
|
||||||
|
let nextPos = headingPos + headingNode.nodeSize;
|
||||||
|
|
||||||
|
while (nextPos < doc.content.size) {
|
||||||
|
const nextNode = doc.nodeAt(nextPos);
|
||||||
|
if (!nextNode) break;
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextNode.type.name === "heading" &&
|
||||||
|
nextNode.attrs.level <= headingLevel
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPos += nextNode.nodeSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextPos;
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ export class MathView implements NodeView, ICursorPosObserver {
|
|||||||
if (options.className) this.dom.classList.add(options.className);
|
if (options.className) this.dom.classList.add(options.className);
|
||||||
this.dom.classList.add("math-node");
|
this.dom.classList.add("math-node");
|
||||||
|
|
||||||
|
if (node.attrs.hiddenUnder) {
|
||||||
|
this.dom.dataset.hiddenUnder = node.attrs.hiddenUnder;
|
||||||
|
} else {
|
||||||
|
delete this.dom.dataset.hiddenUnder;
|
||||||
|
}
|
||||||
|
|
||||||
this._mathRenderElt = document.createElement("span");
|
this._mathRenderElt = document.createElement("span");
|
||||||
this._mathRenderElt.textContent = "";
|
this._mathRenderElt.textContent = "";
|
||||||
this._mathRenderElt.classList.add("math-render");
|
this._mathRenderElt.classList.add("math-render");
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import {
|
|||||||
mergeAttributes,
|
mergeAttributes,
|
||||||
findParentNodeClosestToPos
|
findParentNodeClosestToPos
|
||||||
} from "@tiptap/core";
|
} from "@tiptap/core";
|
||||||
import { findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror.js";
|
import {
|
||||||
|
findParentNodeOfTypeClosestToPos,
|
||||||
|
isClickWithinBounds
|
||||||
|
} 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";
|
||||||
|
|
||||||
@@ -129,36 +132,9 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
|||||||
|
|
||||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||||
if (typeof pos !== "number") return;
|
if (typeof pos !== "number") return;
|
||||||
|
|
||||||
const resolvedPos = editor.state.doc.resolve(pos);
|
const resolvedPos = editor.state.doc.resolve(pos);
|
||||||
|
if (isClickWithinBounds(e, resolvedPos, "left")) {
|
||||||
const { x, y, right } = li.getBoundingClientRect();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xStart && xEnd && yStart && yEnd) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
editor.commands.command(({ tr }) => {
|
editor.commands.command(({ tr }) => {
|
||||||
tr.setNodeAttribute(
|
tr.setNodeAttribute(
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.node.attrs.hiddenUnder) {
|
||||||
|
this.domRef.dataset.hiddenUnder = this.node.attrs.hiddenUnder;
|
||||||
|
} else {
|
||||||
|
delete this.domRef.dataset.hiddenUnder;
|
||||||
|
}
|
||||||
|
|
||||||
portalProviderAPI.render(this.Component, this.domRef);
|
portalProviderAPI.render(this.Component, this.domRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -345,3 +345,52 @@ export function getDeletedNodes(
|
|||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isClickWithinBounds(
|
||||||
|
e: MouseEvent | TouchEvent,
|
||||||
|
pos: ResolvedPos,
|
||||||
|
hitPosition: "left" | "right",
|
||||||
|
hitArea: { width: number; height: number } = { width: 40, height: 40 }
|
||||||
|
) {
|
||||||
|
const { target } = e;
|
||||||
|
if (!(target instanceof HTMLElement)) return false;
|
||||||
|
|
||||||
|
const { x, y, right, width } = target.getBoundingClientRect();
|
||||||
|
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
||||||
|
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
|
||||||
|
const isRtl =
|
||||||
|
target.dir === "rtl" ||
|
||||||
|
findParentNodeClosestToPos(pos, (node) => !!node.attrs.textDirection)?.node
|
||||||
|
.attrs.textDirection === "rtl";
|
||||||
|
|
||||||
|
switch (hitPosition) {
|
||||||
|
case "left": {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return xStart && xEnd && yStart && yEnd;
|
||||||
|
}
|
||||||
|
case "right": {
|
||||||
|
let xEnd = clientX <= x + width;
|
||||||
|
let xStart = clientX >= x + width - hitArea.width;
|
||||||
|
const yStart = clientY >= y;
|
||||||
|
const yEnd = clientY <= y + hitArea.height;
|
||||||
|
|
||||||
|
if (isRtl) {
|
||||||
|
xStart = clientX >= x;
|
||||||
|
xEnd = clientX <= x + hitArea.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return xStart && xEnd && yStart && yEnd;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ p > *::selection {
|
|||||||
|
|
||||||
transform: rotate(0);
|
transform: rotate(0);
|
||||||
transition: transform 250ms ease;
|
transition: transform 250ms ease;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror div.callout > :first-child[dir="rtl"]::after {
|
.ProseMirror div.callout > :first-child[dir="rtl"]::after {
|
||||||
@@ -800,10 +801,158 @@ del.diffdel {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1,
|
||||||
|
.ProseMirror h2,
|
||||||
|
.ProseMirror h3,
|
||||||
|
.ProseMirror h4,
|
||||||
|
.ProseMirror h5,
|
||||||
|
.ProseMirror h6 {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1::before,
|
||||||
|
.ProseMirror h2::before,
|
||||||
|
.ProseMirror h3::before,
|
||||||
|
.ProseMirror h4::before,
|
||||||
|
.ProseMirror h5::before,
|
||||||
|
.ProseMirror h6::before {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
content: "";
|
||||||
|
background-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
background-color: var(--icon);
|
||||||
|
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+)
|
||||||
|
no-repeat 50% 50%;
|
||||||
|
mask-size: cover;
|
||||||
|
border: 1px solid var(--background);
|
||||||
|
|
||||||
|
transform: rotate(0);
|
||||||
|
transition: transform 250ms ease, opacity 200ms ease;
|
||||||
|
left: -22px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1[dir="rtl"]::before,
|
||||||
|
.ProseMirror h2[dir="rtl"]::before,
|
||||||
|
.ProseMirror h3[dir="rtl"]::before,
|
||||||
|
.ProseMirror h4[dir="rtl"]::before,
|
||||||
|
.ProseMirror h5[dir="rtl"]::before,
|
||||||
|
.ProseMirror h6[dir="rtl"]::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1[dir="rtl"]::after,
|
||||||
|
.ProseMirror h2[dir="rtl"]::after,
|
||||||
|
.ProseMirror h3[dir="rtl"]::after,
|
||||||
|
.ProseMirror h4[dir="rtl"]::after,
|
||||||
|
.ProseMirror h5[dir="rtl"]::after,
|
||||||
|
.ProseMirror h6[dir="rtl"]::after {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
content: "";
|
||||||
|
background-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
background-color: var(--icon);
|
||||||
|
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+)
|
||||||
|
no-repeat 50% 50%;
|
||||||
|
mask-size: cover;
|
||||||
|
border: 1px solid var(--background);
|
||||||
|
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 250ms ease, opacity 200ms ease;
|
||||||
|
right: -22px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1::before,
|
||||||
|
.ProseMirror h1::after {
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h2::before,
|
||||||
|
.ProseMirror h2::after
|
||||||
|
{
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h3::before,
|
||||||
|
.ProseMirror h3::after {
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h4::before,
|
||||||
|
.ProseMirror h4::after {
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h5::before,
|
||||||
|
.ProseMirror h5::after {
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h6::before,
|
||||||
|
.ProseMirror h6::after {
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1[data-collapsed="true"]::before,
|
||||||
|
.ProseMirror h2[data-collapsed="true"]::before,
|
||||||
|
.ProseMirror h3[data-collapsed="true"]::before,
|
||||||
|
.ProseMirror h4[data-collapsed="true"]::before,
|
||||||
|
.ProseMirror h5[data-collapsed="true"]::before,
|
||||||
|
.ProseMirror h6[data-collapsed="true"]::before {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror h1[data-collapsed="true"]::after,
|
||||||
|
.ProseMirror h2[data-collapsed="true"]::after,
|
||||||
|
.ProseMirror h3[data-collapsed="true"]::after,
|
||||||
|
.ProseMirror h4[data-collapsed="true"]::after,
|
||||||
|
.ProseMirror h5[data-collapsed="true"]::after,
|
||||||
|
.ProseMirror h6[data-collapsed="true"]::after {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ProseMirror h1:hover::before,
|
||||||
|
.ProseMirror h2:hover::before,
|
||||||
|
.ProseMirror h3:hover::before,
|
||||||
|
.ProseMirror h4:hover::before,
|
||||||
|
.ProseMirror h5:hover::before,
|
||||||
|
.ProseMirror h6:hover::before,
|
||||||
|
.ProseMirror h1:hover::after,
|
||||||
|
.ProseMirror h2:hover::after,
|
||||||
|
.ProseMirror h3:hover::after,
|
||||||
|
.ProseMirror h4:hover::after,
|
||||||
|
.ProseMirror h5:hover::after,
|
||||||
|
.ProseMirror h6:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror div.callout h1::before,
|
||||||
|
.ProseMirror div.callout h2::before,
|
||||||
|
.ProseMirror div.callout h3::before,
|
||||||
|
.ProseMirror div.callout h4::before,
|
||||||
|
.ProseMirror div.callout h5::before,
|
||||||
|
.ProseMirror div.callout h6::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-hidden-under] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* simplebar */
|
/* simplebar */
|
||||||
.simplebar-track {
|
.simplebar-track {
|
||||||
height: 9px !important;
|
height: 9px !important;
|
||||||
}
|
}
|
||||||
.simplebar-content:before {
|
.simplebar-content:before {
|
||||||
content: "" !important;
|
content: "" !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user