diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index 7a7348aba..4974979f6 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/desktop", - "version": "3.3.5", + "version": "3.3.6-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/desktop", - "version": "3.3.5", + "version": "3.3.6-beta.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 194205643..f3c1d3163 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "@notesnook/desktop", "productName": "Notesnook", "description": "Your private note taking space", - "version": "3.3.5", + "version": "3.3.6-beta.0", "appAppleId": "1544027013", "private": true, "main": "./dist/cjs/index.js", diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 4f90177c9..8ec3c05cb 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/web", - "version": "3.3.5", + "version": "3.3.6-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/web", - "version": "3.3.5", + "version": "3.3.6-beta.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/apps/web/package.json b/apps/web/package.json index 28a451344..7533b7516 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@notesnook/web", "description": "Your private note taking space", - "version": "3.3.5", + "version": "3.3.6-beta.0", "private": true, "main": "./src/app.js", "homepage": "https://notesnook.com/", diff --git a/apps/web/src/common/drop-handler.ts b/apps/web/src/common/drop-handler.ts index 89048061f..183c8ab0a 100644 --- a/apps/web/src/common/drop-handler.ts +++ b/apps/web/src/common/drop-handler.ts @@ -30,13 +30,18 @@ export async function handleDrop( item: | ItemReference | Context - | { type: "trash" | "notebooks" | "favorites" | undefined } + | { type: "trash" | "notebooks" | "favorites" | "archive" | undefined } ) { if (!item.type) return; const noteIds = getDragData(dataTransfer, "note"); const notebookIds = getDragData(dataTransfer, "notebook"); - const { setColor, favorite, delete: trashNotes } = useNoteStore.getState(); + const { + setColor, + favorite, + delete: trashNotes, + archive + } = useNoteStore.getState(); switch (item.type) { case "notebook": if (noteIds.length > 0) { @@ -83,5 +88,8 @@ export async function handleDrop( await useNoteStore.getState().refresh(); } break; + case "archive": + archive(true, ...noteIds); + break; } } diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index 3463e4526..5e3e2712f 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -118,7 +118,7 @@ function countCharacters(text: string) { function countParagraphs(fragment: Fragment) { let count = 0; fragment.nodesBetween(0, fragment.size, (node) => { - if (node.type.name === "paragraph") { + if (node.type.name === "paragraph" && node.content.size > 0) { count++; } return true; @@ -737,7 +737,7 @@ function toIEditor(editor: Editor): IEditor { function getSelectedParagraphs(editor: Editor, selection: Selection): number { let count = 0; editor.state.doc.nodesBetween(selection.from, selection.to, (node) => { - if (node.type.name === "paragraph") { + if (node.type.name === "paragraph" && node.content.size > 0) { count++; } return true; diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx index 6f8c67637..5a7476e6e 100644 --- a/apps/web/src/components/navigation-menu/index.tsx +++ b/apps/web/src/components/navigation-menu/index.tsx @@ -506,6 +506,8 @@ function RouteItem({ ? "trash" : item.path === "/favorites" ? "favorites" + : item.path == "/archive" + ? "archive" : undefined }); }} diff --git a/apps/web/src/dialogs/feature-dialog.tsx b/apps/web/src/dialogs/feature-dialog.tsx index 41164fd43..67117c72b 100644 --- a/apps/web/src/dialogs/feature-dialog.tsx +++ b/apps/web/src/dialogs/feature-dialog.tsx @@ -91,18 +91,7 @@ const features: Record = { ) } ] - : [ - { - title: "Notesnook Circle", - subtitle: - "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom. As a member you get exclusive discounts and offers from our partners. Check it out in Settings > Notesnook Circle." - }, - { - title: "Collapsible headings", - subtitle: - "You can now collapse and expand headings in your notes. This makes it easier to manage large notes and focus on specific sections." - } - ], + : [], cta: { title: strings.gotIt(), icon: Checkmark, diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts index 8ab3cc5c4..bd6875833 100644 --- a/packages/core/src/api/user-manager.ts +++ b/packages/core/src/api/user-manager.ts @@ -666,6 +666,15 @@ class UserManager { usesFallback: await this.usesFallbackPWHash(old_password) }); + // retrieve user keys before deriving a new encryption key + const oldUserKeys = { + attachmentsKey: await this.getAttachmentsKey(), + monographPasswordsKey: await this.getMonographPasswordsKey(), + inboxKeys: (await this.hasInboxKeys()) + ? await this.getInboxKeys() + : undefined + } as const; + await this.db.storage().deriveCryptoKey({ password: new_password, salt @@ -678,27 +687,33 @@ class UserManager { const userEncryptionKey = await this.getEncryptionKey(); if (userEncryptionKey) { const updateUserPayload: Partial = {}; - const attachmentsKey = await this.getAttachmentsKey(); - if (attachmentsKey) { + if (oldUserKeys.attachmentsKey) { user.attachmentsKey = await this.db .storage() - .encrypt(userEncryptionKey, JSON.stringify(attachmentsKey)); + .encrypt( + userEncryptionKey, + JSON.stringify(oldUserKeys.attachmentsKey) + ); updateUserPayload.attachmentsKey = user.attachmentsKey; } - const monographPasswordsKey = await this.getMonographPasswordsKey(); - if (monographPasswordsKey) { + if (oldUserKeys.monographPasswordsKey) { user.monographPasswordsKey = await this.db .storage() - .encrypt(userEncryptionKey, JSON.stringify(monographPasswordsKey)); + .encrypt( + userEncryptionKey, + JSON.stringify(oldUserKeys.monographPasswordsKey) + ); updateUserPayload.monographPasswordsKey = user.monographPasswordsKey; } - const inboxKeys = await this.getInboxKeys(); - if (inboxKeys) { + if (oldUserKeys.inboxKeys) { user.inboxKeys = { - public: inboxKeys.publicKey, + public: oldUserKeys.inboxKeys.publicKey, private: await this.db .storage() - .encrypt(userEncryptionKey, JSON.stringify(inboxKeys.privateKey)) + .encrypt( + userEncryptionKey, + JSON.stringify(oldUserKeys.inboxKeys.privateKey) + ) }; updateUserPayload.inboxKeys = user.inboxKeys; } diff --git a/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap b/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap index 36139c74a..36f948208 100644 --- a/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap +++ b/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap @@ -3,3 +3,13 @@ exports[`collapse heading > heading collapsed 1`] = `"

Main Heading

paragraph.

Subheading

subheading paragraph

Main heading 2

paragraph another

"`; exports[`collapse heading > heading uncollapsed 1`] = `"

Main Heading

paragraph.

Subheading

subheading paragraph

Main heading 2

paragraph another

"`; + +exports[`replacing collapsed heading with another heading level should not unhide content 1`] = `"

A collapsed heading

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (blockquote) should unhide content 1`] = `"

A collpased heading

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (bulletList) should unhide content 1`] = `"

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (codeBlock) should unhide content 1`] = `"
A collpased heading

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (paragraph) should unhide content 1`] = `"

A collpased heading

Hidden paragraph

"`; diff --git a/packages/editor/src/extensions/heading/__tests__/heading.test.ts b/packages/editor/src/extensions/heading/__tests__/heading.test.ts index 9fda63e91..8f6a8a46d 100644 --- a/packages/editor/src/extensions/heading/__tests__/heading.test.ts +++ b/packages/editor/src/extensions/heading/__tests__/heading.test.ts @@ -18,8 +18,9 @@ along with this program. If not, see . */ import { test, expect } from "vitest"; -import { createEditor } from "../../../../test-utils/index.js"; +import { createEditor, h } from "../../../../test-utils/index.js"; import { Heading } from "../heading.js"; +import { Editor } from "@tiptap/core"; test("collapse heading", () => { const { editor } = createEditor({ @@ -52,3 +53,59 @@ test("collapse heading", () => { expect(editor.getHTML()).toMatchSnapshot("heading uncollapsed"); }); + +test("replacing collapsed heading with another heading level should not unhide content", () => { + const el = h("div", [ + h("h1", ["A collapsed heading"], { "data-collapsed": "true" }), + h("p", ["Hidden paragraph"], { "data-hidden": "true" }) + ]); + const { editor } = createEditor({ + extensions: { + heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }) + }, + initialContent: el.outerHTML + }); + + editor.commands.setTextSelection(0); + editor.commands.setHeading({ level: 2 }); + + expect(editor.getHTML()).toMatchSnapshot(); +}); + +const nodes: { name: string; setNode: (editor: Editor) => void }[] = [ + { + name: "paragraph", + setNode: (editor) => editor.commands.setParagraph() + }, + { + name: "codeBlock", + setNode: (editor) => editor.commands.setCodeBlock() + }, + { + name: "bulletList", + setNode: (editor) => editor.commands.toggleList("bulletList", "listItem") + }, + { + name: "blockquote", + setNode: (editor) => editor.commands.toggleBlockquote() + } +]; +for (const { name, setNode } of nodes) { + test(`replacing collapsed heading with another node (${name}) should unhide content`, () => { + const el = h("div", [ + h("h1", ["A collpased heading"], { "data-collapsed": "true" }), + h("p", ["Hidden paragraph"], { "data-hidden": "true" }) + ]); + const { editor } = createEditor({ + extensions: { + heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }) + }, + initialContent: el.outerHTML + }); + + editor.commands.setTextSelection(0); + setNode(editor); + + expect(editor.getHTML()).toMatchSnapshot(); + }); +} diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index 465e8d471..45ac83846 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,50 +182,46 @@ 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 calloutAncestor = findParentNodeClosestToPos( - resolvedPos, - (node) => node.type.name === "callout" - ); - if (calloutAncestor) return; - + const forbiddenParents = ["callout"]; if ( - isClickWithinBounds( - e, - resolvedPos, - useToolbarStore.getState().isMobile ? "right" : "left" + findParentNodeClosestToPos(resolvedPos, (node) => + forbiddenParents.includes(node.type.name) ) ) { - 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; - - tr.setNodeAttribute(pos, "collapsed", shouldCollapse); - toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse); - } - return true; - }); + return; } + + 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); + toggleNodesUnderPos(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; @@ -253,17 +255,17 @@ export const Heading = TiptapHeading.extend({ } }); -function toggleNodesUnderHeading( +function toggleNodesUnderPos( tr: Transaction, - headingPos: number, + pos: number, headingLevel: number, isCollapsing: boolean ) { const { doc } = tr; - const headingNode = doc.nodeAt(headingPos); - if (!headingNode || headingNode.type.name !== "heading") return; + const node = doc.nodeAt(pos); + if (!node) return; - let nextPos = headingPos + headingNode.nodeSize; + let nextPos = pos + node.nodeSize; const cursorPos = tr.selection.from; let shouldMoveCursor = false; let insideCollapsedHeading = false; @@ -318,8 +320,8 @@ function toggleNodesUnderHeading( } if (shouldMoveCursor) { - const headingEndPos = headingPos + headingNode.nodeSize - 1; - tr.setSelection(Selection.near(tr.doc.resolve(headingEndPos))); + const endPos = pos + node.nodeSize - 1; + tr.setSelection(Selection.near(tr.doc.resolve(endPos))); } } @@ -364,26 +366,27 @@ const headingUpdatePlugin = new Plugin({ let modified = false; newDoc.descendants((newNode, pos) => { - if (newNode.type.name === "heading") { - if (pos >= oldDoc.content.size) return; + if (pos >= oldDoc.content.size) return; - const oldNode = oldDoc.nodeAt(pos); - if ( - oldNode && - oldNode.type.name === "heading" && - oldNode.attrs.level !== newNode.attrs.level - ) { - /** - * if the level of a collapsed heading is changed, - * we need to reset visibility of all the nodes under it as there - * might be a heading of same or higher level previously - * hidden under this heading - */ - if (newNode.attrs.collapsed) { - toggleNodesUnderHeading(tr, pos, oldNode.attrs.level, false); - toggleNodesUnderHeading(tr, pos, newNode.attrs.level, true); - modified = true; - } + const oldNode = oldDoc.nodeAt(pos); + if ( + oldNode && + oldNode.type.name === "heading" && + oldNode.attrs.level !== newNode.attrs.level + ) { + /** + * if the level of a collapsed heading is changed, + * we need to reset visibility of all the nodes under it as there + * might be a heading of same or higher level previously + * hidden under this heading + */ + if (newNode.type.name === "heading" && newNode.attrs.collapsed) { + toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false); + toggleNodesUnderPos(tr, pos, newNode.attrs.level, true); + modified = true; + } else if (newNode.type.name !== "heading" && oldNode.attrs.collapsed) { + toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false); + modified = true; } } }); diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 214ce04da..85e398942 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -316,6 +316,31 @@ img.ProseMirror-separator { .ProseMirror table p { margin: 0; } + +.ProseMirror td > h1:first-child, +.ProseMirror td > h2:first-child, +.ProseMirror td > h3:first-child, +.ProseMirror td > h4:first-child, +.ProseMirror td > h5:first-child, +.ProseMirror td > h6:first-child { + margin-top: 0; +} + +.ProseMirror td > ol, +.ProseMirror td > ul { + padding-left: 20px; + margin-top: 0; +} + +.ProseMirror td > blockquote { + margin-left: 0; + margin-top: 0; +} + +.ProseMirror td > blockquote > :first-child { + margin-top: 0; +} + /* .resize-cursor { @@ -739,6 +764,10 @@ p > *::selection { mask-size: cover; } +.simple-checklist > li.checked p { + opacity: 0.8; +} + /* Callout */ .ProseMirror div.callout { padding: 15px; @@ -866,150 +895,133 @@ 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; } +/* 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"] { display: none !important; } @@ -1028,3 +1040,4 @@ del.diffdel { pre[class*="language-"] { overflow: initial !important; } +