mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
editor: add keybinding to collapse/expand headings & callouts
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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`] = `"<div data-callout-type="info" class="collapsed callout"><h4>INFO</h4><p>This is the callout content.</p></div>"`;
|
||||
|
||||
exports[`ctrl+space should collapse/expand callout when cursor is in first heading 2`] = `"<div data-callout-type="info" class="callout"><h4>INFO</h4><p>This is the callout content.</p></div>"`;
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -18,14 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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");
|
||||
|
||||
@@ -6,6 +6,10 @@ exports[`collapse heading > heading uncollapsed 1`] = `"<h1>Main Heading</h1><p>
|
||||
|
||||
exports[`converting collapsed heading to lower level should unhide higher level headings 1`] = `"<h3 data-collapsed="true">Level 1 (to be changed)</h3><h2 data-collapsed="true">Level 2</h2><p data-hidden="true">Paragraph under level 2</p>"`;
|
||||
|
||||
exports[`ctrl+space should collapse/expand heading 1`] = `"<h1 data-collapsed="true">Heading</h1><p data-hidden="true">Paragraph</p>"`;
|
||||
|
||||
exports[`ctrl+space should collapse/expand heading 2`] = `"<h1>Heading</h1><p>Paragraph</p>"`;
|
||||
|
||||
exports[`replacing collapsed heading with another heading level should not unhide content 1`] = `"<h2 data-collapsed="true">A collapsed heading</h2><p data-hidden="true">Hidden paragraph</p>"`;
|
||||
|
||||
exports[`replacing collapsed heading with another node (blockquote) should unhide content 1`] = `"<blockquote><h1 data-collapsed="true">A collpased heading</h1></blockquote><p>Hidden paragraph</p>"`;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`] = `"<ul data-type="outlineList"><li data-collapsed="true" data-type="outlineListItem"><p data-spacing="double">item</p><ul data-type="outlineList"><li data-type="outlineListItem"><p data-spacing="double">sub item</p></li></ul></li></ul>"`;
|
||||
|
||||
exports[`outline list item > ctrl+space should collapse/expand outline list item 2`] = `"<ul data-type="outlineList"><li data-type="outlineListItem"><p data-spacing="double">item</p><ul data-type="outlineList"><li data-type="outlineListItem"><p data-spacing="double">sub item</p></li></ul></li></ul>"`;
|
||||
|
||||
exports[`outline list item > inline image as first child in the old outline list item 1`] = `"<ul data-type="outlineList"><li data-type="outlineListItem"><p data-spacing="double">item 1</p></li><li data-type="outlineListItem"><p data-spacing="double"></p><img src="image.png" data-aspect-ratio="1"></li></ul>"`;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
[tiptapKeys.toggleOutlineListExpand.keys]: ({ editor }) => {
|
||||
[tiptapKeys.toggleNodeExpand.keys]: ({ editor }) => {
|
||||
const { selection } = editor.state;
|
||||
const { $from, empty } = selection;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user