diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index 1873f4be3..83a1cde0d 100644 --- a/packages/editor/src/extensions/heading/heading.ts +++ b/packages/editor/src/extensions/heading/heading.ts @@ -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; diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 59234251b..1362eb640 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -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; } +