editor: change heading collapse icon pos (#8953)

* editor: enable heading in table, change collapse icon pos, && disable empty heading collapse
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: migrate empty collapsed headings in parseHTML instead of plugin
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: remove migration for empty collapsed headings
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix heading collapse on mobile
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: use ontouchend instead of ontouchstart

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
01zulfi
2025-11-24 10:49:43 +05:00
committed by GitHub
parent cec05b6dfc
commit bdd78df452
2 changed files with 110 additions and 142 deletions

View File

@@ -23,10 +23,8 @@ import {
textblockTypeInputRule
} from "@tiptap/core";
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
import { isClickWithinBounds } from "../../utils/prosemirror.js";
import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state";
import { Node } from "@tiptap/pm/model";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
const COLLAPSIBLE_BLOCK_TYPES = [
"paragraph",
@@ -168,6 +166,14 @@ export const Heading = TiptapHeading.extend({
addNodeView() {
return ({ node, getPos, editor, HTMLAttributes }) => {
const heading = document.createElement(`h${node.attrs.level}`);
const contentWrapper = document.createElement("div");
const icon = document.createElement("span");
// providing a minWidth so that empty headings show the blinking cursor
contentWrapper.style.minWidth = "1px";
icon.className = "heading-collapse-icon";
icon.contentEditable = "false";
for (const attr in HTMLAttributes) {
heading.setAttribute(attr, HTMLAttributes[attr]);
@@ -176,15 +182,16 @@ export const Heading = TiptapHeading.extend({
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;
function onIconClick(e: MouseEvent | TouchEvent) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const pos = typeof getPos === "function" ? getPos() : 0;
if (typeof pos !== "number") return;
const resolvedPos = editor.state.doc.resolve(pos);
const forbiddenParents = ["callout", "table"];
const forbiddenParents = ["callout"];
if (
findParentNodeClosestToPos(resolvedPos, (node) =>
forbiddenParents.includes(node.type.name)
@@ -193,36 +200,28 @@ export const Heading = TiptapHeading.extend({
return;
}
if (
isClickWithinBounds(
e,
resolvedPos,
useToolbarStore.getState().isMobile ? "right" : "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;
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;
tr.setNodeAttribute(pos, "collapsed", shouldCollapse);
toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse);
}
return true;
});
}
tr.setNodeAttribute(pos, "collapsed", shouldCollapse);
toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse);
}
return true;
});
}
heading.onmousedown = onClick;
heading.ontouchstart = onClick;
icon.onmousedown = onIconClick;
icon.ontouchend = onIconClick;
heading.appendChild(contentWrapper);
heading.appendChild(icon);
return {
dom: heading,
contentDOM: heading,
contentDOM: contentWrapper,
update: (updatedNode) => {
if (updatedNode.type !== this.type) {
return false;

View File

@@ -870,163 +870,131 @@ del.diffdel {
text-decoration: none;
}
.ProseMirror h1,
.ProseMirror h2,
.ProseMirror h3,
.ProseMirror h4,
.ProseMirror h5,
.ProseMirror h1 ,
.ProseMirror h2 ,
.ProseMirror h3 ,
.ProseMirror h4 ,
.ProseMirror h5 ,
.ProseMirror h6 {
position: relative;
display: flex;
align-items: center;
}
.ProseMirror h1::before,
.ProseMirror h2::before,
.ProseMirror h3::before,
.ProseMirror h4::before,
.ProseMirror h5::before,
.ProseMirror h6::before {
position: absolute;
.ProseMirror h1 .heading-collapse-icon,
.ProseMirror h2 .heading-collapse-icon,
.ProseMirror h3 .heading-collapse-icon,
.ProseMirror h4 .heading-collapse-icon,
.ProseMirror h5 .heading-collapse-icon,
.ProseMirror h6 .heading-collapse-icon {
cursor: pointer;
content: "";
background-size: 18px;
width: 18px;
height: 18px;
margin-inline-start: 8px;
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;
user-select: none;
}
.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;
.ProseMirror h1 .heading-collapse-icon {
margin-top: 3.5px;
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 .heading-collapse-icon {
margin-top: 3px;
width: 16px;
height: 16px;
}
.ProseMirror h2::before,
.ProseMirror h2::after
{
top: 3px;
.ProseMirror h3 .heading-collapse-icon {
margin-top: 2.3px;
width: 15px;
height: 15px;
}
.ProseMirror h3::before,
.ProseMirror h3::after {
top: 0px;
.ProseMirror h4 .heading-collapse-icon {
margin-top: 1.8px;
width: 14px;
height: 14px;
}
.ProseMirror h4::before,
.ProseMirror h4::after {
top: -1px;
.ProseMirror h5 .heading-collapse-icon {
margin-top: 1.3px;
width: 13px;
height: 13px;
}
.ProseMirror h5::before,
.ProseMirror h5::after {
top: -2px;
.ProseMirror h6 .heading-collapse-icon {
margin-top: 0.3px;
width: 12px;
height: 12px;
}
.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 {
.ProseMirror h1[data-collapsed="true"] .heading-collapse-icon,
.ProseMirror h2[data-collapsed="true"] .heading-collapse-icon,
.ProseMirror h3[data-collapsed="true"] .heading-collapse-icon,
.ProseMirror h4[data-collapsed="true"] .heading-collapse-icon,
.ProseMirror h5[data-collapsed="true"] .heading-collapse-icon,
.ProseMirror h6[data-collapsed="true"] .heading-collapse-icon {
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 {
.ProseMirror h1[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
.ProseMirror h2[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
.ProseMirror h3[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
.ProseMirror h4[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
.ProseMirror h5[data-collapsed="true"][dir="rtl"] .heading-collapse-icon,
.ProseMirror h6[data-collapsed="true"][dir="rtl"] .heading-collapse-icon {
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 {
.ProseMirror h1:hover .heading-collapse-icon,
.ProseMirror h2:hover .heading-collapse-icon,
.ProseMirror h3:hover .heading-collapse-icon,
.ProseMirror h4:hover .heading-collapse-icon,
.ProseMirror h5:hover .heading-collapse-icon,
.ProseMirror h6:hover .heading-collapse-icon {
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 {
.ProseMirror div.callout h1 .heading-collapse-icon,
.ProseMirror div.callout h2 .heading-collapse-icon,
.ProseMirror div.callout h3 .heading-collapse-icon,
.ProseMirror div.callout h4 .heading-collapse-icon,
.ProseMirror div.callout h5 .heading-collapse-icon,
.ProseMirror div.callout h6 .heading-collapse-icon {
display: none;
}
.ProseMirror table h1::before,
.ProseMirror table h2::before,
.ProseMirror table h3::before,
.ProseMirror table h4::before,
.ProseMirror table h5::before,
.ProseMirror table h6::before,
.ProseMirror table h1::after,
.ProseMirror table h2::after,
.ProseMirror table h3::after,
.ProseMirror table h4::after,
.ProseMirror table h5::after,
.ProseMirror table h6::after {
display: none;
/* hide collapse icon when heading is empty (only contains trailing break) */
.ProseMirror h1:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
.ProseMirror h2:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
.ProseMirror h3:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
.ProseMirror h4:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
.ProseMirror h5:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon,
.ProseMirror h6:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon {
display: none !important;
}
@media screen and (max-width: 768px) {
.ProseMirror h1 .heading-collapse-icon,
.ProseMirror h2 .heading-collapse-icon,
.ProseMirror h3 .heading-collapse-icon,
.ProseMirror h4 .heading-collapse-icon,
.ProseMirror h5 .heading-collapse-icon,
.ProseMirror h6 .heading-collapse-icon {
opacity: 1 !important;
}
}
[data-hidden="true"] {
@@ -1047,3 +1015,4 @@ del.diffdel {
pre[class*="language-"] {
overflow: initial !important;
}