diff --git a/docs/help/contents/keyboard-shortcuts.md b/docs/help/contents/keyboard-shortcuts.md index 9a5a85818..35759807e 100644 --- a/docs/help/contents/keyboard-shortcuts.md +++ b/docs/help/contents/keyboard-shortcuts.md @@ -65,7 +65,7 @@ The following keyboard shortcuts will help you navigate Notesnook faster. | Insert math block | Ctrl ⇧ M | Ctrl ⇧ M | ⌘ ⇧ M | | Toggle ordered list | Ctrl ⇧ 7 | Ctrl ⇧ 7 | ⌘ ⇧ 7 | | Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O | -| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space | +| Toggle callout/heading/outline list expand | Ctrl Space | Ctrl Space | ⌘ Space | | Open search | Ctrl F | Ctrl F | ⌘ F | | Open search and replace | Ctrl Alt F | Ctrl Alt F | ⌘ ⌥ F | | Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S | diff --git a/packages/common/src/utils/keybindings.ts b/packages/common/src/utils/keybindings.ts index 73c1296a7..2fa6b1508 100644 --- a/packages/common/src/utils/keybindings.ts +++ b/packages/common/src/utils/keybindings.ts @@ -323,9 +323,9 @@ export const tiptapKeys = { category: "Editor", type: "tiptap" }, - toggleOutlineListExpand: { + toggleNodeExpand: { keys: "Mod-Space", - description: "Toggle outline list expand", + description: "Toggle callout/heading/outline list expand", category: "Editor", type: "tiptap" }, diff --git a/packages/editor/src/extensions/callout/__tests__/__snapshots__/callout.test.ts.snap b/packages/editor/src/extensions/callout/__tests__/__snapshots__/callout.test.ts.snap new file mode 100644 index 000000000..8b5989479 --- /dev/null +++ b/packages/editor/src/extensions/callout/__tests__/__snapshots__/callout.test.ts.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ctrl+space should collapse/expand callout when cursor is in first heading 1`] = `""`; + +exports[`ctrl+space should collapse/expand callout when cursor is in first heading 2`] = `"

INFO

This is the callout content.

"`; diff --git a/packages/editor/src/extensions/callout/__tests__/callout.test.ts b/packages/editor/src/extensions/callout/__tests__/callout.test.ts new file mode 100644 index 000000000..473a358e6 --- /dev/null +++ b/packages/editor/src/extensions/callout/__tests__/callout.test.ts @@ -0,0 +1,60 @@ +/* +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 . +*/ + +import { test, expect } from "vitest"; +import { createEditor, h } from "../../../../test-utils/index.js"; +import { Callout } from "../callout.js"; +import { Heading } from "../../heading/heading.js"; + +test("ctrl+space should collapse/expand callout when cursor is in first heading", async () => { + const el = h("div", [ + h("div", [h("h4", ["INFO"]), h("p", ["This is the callout content."])], { + class: "callout", + "data-callout-type": "info" + }) + ]); + const { editor } = createEditor({ + extensions: { + callout: Callout, + heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }) + }, + initialContent: el.outerHTML + }); + + editor.commands.setTextSelection(3); + const collapseEvent = new KeyboardEvent("keydown", { + key: " ", + code: "Space", + ctrlKey: true, + bubbles: true + }); + editor.view.dom.dispatchEvent(collapseEvent); + + expect(editor.getHTML()).toMatchSnapshot(); + + const expandEvent = new KeyboardEvent("keydown", { + key: " ", + code: "Space", + ctrlKey: true, + bubbles: true + }); + editor.view.dom.dispatchEvent(expandEvent); + + expect(editor.getHTML()).toMatchSnapshot(); +}); diff --git a/packages/editor/src/extensions/callout/callout.ts b/packages/editor/src/extensions/callout/callout.ts index 86d8b0f03..ce815c5b1 100644 --- a/packages/editor/src/extensions/callout/callout.ts +++ b/packages/editor/src/extensions/callout/callout.ts @@ -18,14 +18,11 @@ along with this program. If not, see . */ import { getParentAttributes, - isClickWithinBounds + isClickWithinBounds, + findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror.js"; -import { - InputRule, - Node, - findParentNodeClosestToPos, - mergeAttributes -} from "@tiptap/core"; +import { tiptapKeys } from "@notesnook/common"; +import { InputRule, Node, mergeAttributes } from "@tiptap/core"; import { Paragraph } from "../paragraph/index.js"; import { Heading } from "../heading/index.js"; import { TextSelection } from "@tiptap/pm/state"; @@ -220,6 +217,35 @@ export const Callout = Node.create({ ]; }, + addKeyboardShortcuts() { + return { + [tiptapKeys.toggleNodeExpand.keys]: ({ editor }) => { + const { selection } = editor.state; + const { $from, empty } = selection; + + if (!empty) return false; + if ($from.parent.type.name !== Heading.name) return false; + + const callout = findParentNodeOfTypeClosestToPos($from, this.type); + if (!callout) return false; + + const firstChild = callout.node.firstChild; + if (!firstChild || firstChild.type.name !== Heading.name) return false; + + const headingStart = callout.start; + const headingEnd = headingStart + firstChild.nodeSize; + if ($from.pos < headingStart || $from.pos > headingEnd) return false; + + const isCollapsed = callout.node.attrs.collapsed; + + return editor.commands.command(({ tr }) => { + tr.setNodeAttribute(callout.pos, "collapsed", !isCollapsed); + return true; + }); + } + }; + }, + addNodeView() { return ({ node, getPos, editor, HTMLAttributes }) => { const container = document.createElement("div"); 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 61e8cb4cc..37e8bb512 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 @@ -6,6 +6,10 @@ exports[`collapse heading > heading uncollapsed 1`] = `"

Main Heading

exports[`converting collapsed heading to lower level should unhide higher level headings 1`] = `"

Level 1 (to be changed)

Level 2

Paragraph under level 2

"`; +exports[`ctrl+space should collapse/expand heading 1`] = `"

Heading

Paragraph

"`; + +exports[`ctrl+space should collapse/expand heading 2`] = `"

Heading

Paragraph

"`; + 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

"`; diff --git a/packages/editor/src/extensions/heading/__tests__/heading.test.ts b/packages/editor/src/extensions/heading/__tests__/heading.test.ts index 420e01ffc..6b32f2279 100644 --- a/packages/editor/src/extensions/heading/__tests__/heading.test.ts +++ b/packages/editor/src/extensions/heading/__tests__/heading.test.ts @@ -144,3 +144,34 @@ test("converting collapsed heading to lower level should unhide higher level hea expect(editor.getHTML()).toMatchSnapshot(); }); + +test("ctrl+space should collapse/expand heading", async () => { + const el = h("div", [h("h1", ["Heading"]), h("p", ["Paragraph"])]); + const { editor } = createEditor({ + extensions: { + heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }) + }, + initialContent: el.outerHTML + }); + + editor.commands.setTextSelection(3); + const collapseEvent = new KeyboardEvent("keydown", { + key: " ", + code: "Space", + ctrlKey: true, + bubbles: true + }); + editor.view.dom.dispatchEvent(collapseEvent); + + expect(editor.getHTML()).toMatchSnapshot(); + + const expandEvent = new KeyboardEvent("keydown", { + key: " ", + code: "Space", + ctrlKey: true, + bubbles: true + }); + editor.view.dom.dispatchEvent(expandEvent); + + expect(editor.getHTML()).toMatchSnapshot(); +}); diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index d5c2720fa..e9b341afc 100644 --- a/packages/editor/src/extensions/heading/heading.ts +++ b/packages/editor/src/extensions/heading/heading.ts @@ -126,6 +126,37 @@ export const Heading = TiptapHeading.extend({ }), {} ), + [tiptapKeys.toggleNodeExpand.keys]: ({ editor }) => { + const { selection } = editor.state; + const { $from, empty } = selection; + + if (!empty) return false; + if ($from.parent.type.name !== this.name) return false; + + const headingPos = $from.before(); + const headingNode = editor.state.doc.nodeAt(headingPos); + if (!headingNode || headingNode.type.name !== this.name) return false; + + // the first callout heading's collapsibility is handled by callout itself + const callout = findParentNodeClosestToPos( + $from, + (node) => node.type.name === Callout.name + ); + if (callout?.node.firstChild === headingNode) return false; + + const isCollapsed = headingNode.attrs.collapsed; + + return editor.commands.command(({ tr }) => { + tr.setNodeAttribute(headingPos, "collapsed", !isCollapsed); + toggleNodesUnderPos( + tr, + headingPos, + headingNode.attrs.level, + !isCollapsed + ); + return true; + }); + }, Enter: ({ editor }) => { const { state, commands } = editor; const { $from } = state.selection; diff --git a/packages/editor/src/extensions/outline-list-item/__tests__/__snapshots__/outline-list-item.test.ts.snap b/packages/editor/src/extensions/outline-list-item/__tests__/__snapshots__/outline-list-item.test.ts.snap index 3aebd38b5..d81bf5cfb 100644 --- a/packages/editor/src/extensions/outline-list-item/__tests__/__snapshots__/outline-list-item.test.ts.snap +++ b/packages/editor/src/extensions/outline-list-item/__tests__/__snapshots__/outline-list-item.test.ts.snap @@ -129,4 +129,8 @@ exports[`outline list item > code block in outline list item 1`] = ` } `; +exports[`outline list item > ctrl+space should collapse/expand outline list item 1`] = `""`; + +exports[`outline list item > ctrl+space should collapse/expand outline list item 2`] = `""`; + exports[`outline list item > inline image as first child in the old outline list item 1`] = `""`; diff --git a/packages/editor/src/extensions/outline-list-item/__tests__/outline-list-item.test.ts b/packages/editor/src/extensions/outline-list-item/__tests__/outline-list-item.test.ts index 673040c39..49a2622ec 100644 --- a/packages/editor/src/extensions/outline-list-item/__tests__/outline-list-item.test.ts +++ b/packages/editor/src/extensions/outline-list-item/__tests__/outline-list-item.test.ts @@ -98,4 +98,39 @@ describe("outline list item", () => { expect(editor.getHTML()).toMatchSnapshot(); }); + + test("ctrl+space should collapse/expand outline list item", async () => { + const el = outlineList( + outlineListItem(["item"], outlineList(outlineListItem(["sub item"]))) + ); + const { editor } = createEditor({ + initialContent: el.outerHTML, + extensions: { + outlineList: OutlineList, + outlineListItem: OutlineListItem, + paragraph: Paragraph + } + }); + + editor.commands.setTextSelection(3); + const collapseEvent = new KeyboardEvent("keydown", { + key: " ", + code: "Space", + ctrlKey: true, + bubbles: true + }); + editor.view.dom.dispatchEvent(collapseEvent); + + expect(editor.getHTML()).toMatchSnapshot(); + + const expandEvent = new KeyboardEvent("keydown", { + key: " ", + code: "Space", + ctrlKey: true, + bubbles: true + }); + editor.view.dom.dispatchEvent(expandEvent); + + expect(editor.getHTML()).toMatchSnapshot(); + }); }); diff --git a/packages/editor/src/extensions/outline-list-item/outline-list-item.ts b/packages/editor/src/extensions/outline-list-item/outline-list-item.ts index b26574b5b..0eb2e6c07 100644 --- a/packages/editor/src/extensions/outline-list-item/outline-list-item.ts +++ b/packages/editor/src/extensions/outline-list-item/outline-list-item.ts @@ -83,7 +83,7 @@ export const OutlineListItem = Node.create({ addKeyboardShortcuts() { return { - [tiptapKeys.toggleOutlineListExpand.keys]: ({ editor }) => { + [tiptapKeys.toggleNodeExpand.keys]: ({ editor }) => { const { selection } = editor.state; const { $from, empty } = selection;