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(() => {
|
||||
if (!isMobile() && !isTablet()) {
|
||||
editor.style.marginLeft = `-${negativeSpace}px`;
|
||||
editor.style.marginRight = `-${negativeSpace}px`;
|
||||
}
|
||||
editor.style.paddingLeft = `${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
|
||||
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 {
|
||||
InputRule,
|
||||
Node,
|
||||
@@ -235,37 +238,9 @@ export const Callout = Node.create({
|
||||
|
||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||
if (typeof pos !== "number") return;
|
||||
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
|
||||
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) {
|
||||
if (isClickWithinBounds(e, resolvedPos, "right")) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
@@ -283,6 +258,12 @@ export const Callout = Node.create({
|
||||
container.onmousedown = onClick;
|
||||
container.ontouchstart = onClick;
|
||||
|
||||
if (node.attrs.hiddenUnder) {
|
||||
container.dataset.hiddenUnder = node.attrs.hiddenUnder;
|
||||
} else {
|
||||
delete container.dataset.hiddenUnder;
|
||||
}
|
||||
|
||||
return {
|
||||
dom: container,
|
||||
contentDOM: container,
|
||||
@@ -294,6 +275,10 @@ export const Callout = Node.create({
|
||||
if (updatedNode.attrs.collapsed) container.classList.add("collapsed");
|
||||
else container.classList.remove("collapsed");
|
||||
|
||||
if (updatedNode.attrs.hiddenUnder)
|
||||
container.dataset.hiddenUnder = updatedNode.attrs.hiddenUnder;
|
||||
else delete container.dataset.hiddenUnder;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -554,7 +554,8 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
return (
|
||||
compareCaretPosition(prev.caretPosition, next.caretPosition) ||
|
||||
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 { textblockTypeInputRule } from "@tiptap/core";
|
||||
import {
|
||||
findParentNodeClosestToPos,
|
||||
textblockTypeInputRule
|
||||
} from "@tiptap/core";
|
||||
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$/;
|
||||
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() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
@@ -45,8 +83,30 @@ export const Heading = TiptapHeading.extend({
|
||||
};
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: COLLAPSIBLE_BLOCK_TYPES,
|
||||
attributes: {
|
||||
hiddenUnder: {
|
||||
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(
|
||||
return {
|
||||
...this.options.levels.reduce(
|
||||
(items, level) => ({
|
||||
...items,
|
||||
...{
|
||||
@@ -55,7 +115,33 @@ export const Heading = TiptapHeading.extend({
|
||||
}
|
||||
}),
|
||||
{}
|
||||
),
|
||||
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() {
|
||||
@@ -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);
|
||||
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.textContent = "";
|
||||
this._mathRenderElt.classList.add("math-render");
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
mergeAttributes,
|
||||
findParentNodeClosestToPos
|
||||
} 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 { keybindings, tiptapKeys } from "@notesnook/common";
|
||||
|
||||
@@ -129,36 +132,9 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
|
||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||
if (typeof pos !== "number") return;
|
||||
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
|
||||
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) {
|
||||
if (isClickWithinBounds(e, resolvedPos, "left")) {
|
||||
e.preventDefault();
|
||||
editor.commands.command(({ tr }) => {
|
||||
tr.setNodeAttribute(
|
||||
|
||||
@@ -112,6 +112,12 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -345,3 +345,52 @@ export function getDeletedNodes(
|
||||
}
|
||||
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);
|
||||
transition: transform 250ms ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ProseMirror div.callout > :first-child[dir="rtl"]::after {
|
||||
@@ -800,6 +801,154 @@ del.diffdel {
|
||||
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-track {
|
||||
height: 9px !important;
|
||||
|
||||
Reference in New Issue
Block a user