mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +01:00
editor: improve escaping from & deleting of nodes
This commit is contained in:
committed by
Abdullah Atta
parent
cafa5ff821
commit
58de07073d
@@ -299,43 +299,6 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
to: codeblock.pos + codeblock.node.nodeSize - 1
|
||||
});
|
||||
},
|
||||
// remove code block when at start of document or code block is empty
|
||||
Backspace: ({ editor }) => {
|
||||
const { empty, $anchor } = editor.state.selection;
|
||||
|
||||
const currentNode = $anchor.parent;
|
||||
const nextNode = editor.state.doc.nodeAt($anchor.pos + 1);
|
||||
const isCodeBlock = (node: ProsemirrorNode | null) =>
|
||||
node && node.type.name === this.name;
|
||||
const isAtStart = $anchor.pos === 1;
|
||||
|
||||
if (!empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isAtStart ||
|
||||
(isCodeBlock(currentNode) && !currentNode.textContent.length)
|
||||
) {
|
||||
return this.editor.commands.deleteNode(this.type);
|
||||
}
|
||||
// on android due to composition issues with various keyboards,
|
||||
// sometimes backspace is detected one node behind. We need to
|
||||
// manually handle this case.
|
||||
else if (
|
||||
nextNode &&
|
||||
isCodeBlock(nextNode) &&
|
||||
!nextNode.textContent.length
|
||||
) {
|
||||
return this.editor.commands.command(({ tr }) => {
|
||||
tr.delete($anchor.pos + 1, $anchor.pos + 1 + nextNode.nodeSize);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// exit node on triple enter
|
||||
Enter: ({ editor }) => {
|
||||
const { state } = editor;
|
||||
@@ -354,28 +317,6 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
if (indentation) return indentOnEnter(editor, $from, indentation);
|
||||
return false;
|
||||
},
|
||||
|
||||
// exit node on arrow up
|
||||
ArrowUp: ({ editor }) => {
|
||||
if (!this.options.exitOnArrowUp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { state } = editor;
|
||||
const { selection } = state;
|
||||
const { $anchor, empty } = selection;
|
||||
|
||||
if (!empty || $anchor.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAtStart = $anchor.pos === 1;
|
||||
if (!isAtStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.commands.insertContentAt(0, "<p></p>");
|
||||
},
|
||||
// exit node on arrow down
|
||||
ArrowDown: ({ editor }) => {
|
||||
if (!this.options.exitOnArrowDown) {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
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 { EditorState } from "prosemirror-state";
|
||||
import { NodeType } from "prosemirror-model";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { findParentNodeOfType } from "../../utils/prosemirror";
|
||||
|
||||
export function onArrowUpPressed(editor: Editor, name: string, type: NodeType) {
|
||||
const { selection } = editor.state;
|
||||
const { empty } = selection;
|
||||
|
||||
if (!empty || !isFirstOfType(type, editor.state)) return false;
|
||||
const parentList = getListFromListItem(type, editor.state);
|
||||
if (editor.state.doc.firstChild === parentList)
|
||||
return editor.commands.insertContentAt(0, "<p></p>");
|
||||
return false;
|
||||
}
|
||||
|
||||
const isFirstOfType = (type: NodeType, state: EditorState) => {
|
||||
const block = findParentNodeOfType(type)(state.selection);
|
||||
if (!block) return false;
|
||||
const { pos } = block;
|
||||
const resolved = state.doc.resolve(pos);
|
||||
return !resolved.nodeBefore;
|
||||
};
|
||||
|
||||
const getListFromListItem = (type: NodeType, state: EditorState) => {
|
||||
const block = findParentNodeOfType(type)(state.selection);
|
||||
if (!block) return undefined;
|
||||
const { pos } = block;
|
||||
const resolved = state.doc.resolve(pos);
|
||||
if (
|
||||
!resolved.parent.type.spec.group ||
|
||||
resolved.parent.type.spec.group?.indexOf("list") <= -1
|
||||
)
|
||||
return undefined;
|
||||
|
||||
return resolved.parent;
|
||||
};
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ListItem as TiptapListItem } from "@tiptap/extension-list-item";
|
||||
import { onArrowUpPressed } from "./commands";
|
||||
|
||||
export const ListItem = TiptapListItem.extend({
|
||||
addKeyboardShortcuts() {
|
||||
@@ -33,8 +32,7 @@ export const ListItem = TiptapListItem.extend({
|
||||
if ($from.parent.type.name === "codeblock") return false;
|
||||
|
||||
return this.parent?.()?.Tab(props) || false;
|
||||
},
|
||||
ArrowUp: ({ editor }) => onArrowUpPressed(editor, this.name, this.type)
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from "@tiptap/core";
|
||||
import { NodeType } from "prosemirror-model";
|
||||
import { findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror";
|
||||
import { onArrowUpPressed } from "../list-item/commands";
|
||||
import { OutlineList } from "../outline-list/outline-list";
|
||||
|
||||
export interface ListItemOptions {
|
||||
@@ -110,8 +109,7 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
return this.editor.commands.splitListItem(this.name);
|
||||
},
|
||||
Tab: () => this.editor.commands.sinkListItem(this.name),
|
||||
"Shift-Tab": () => this.editor.commands.liftListItem(this.name),
|
||||
ArrowUp: ({ editor }) => onArrowUpPressed(editor, this.name, this.type)
|
||||
"Shift-Tab": () => this.editor.commands.liftListItem(this.name)
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
21
packages/editor/src/extensions/quirks/index.ts
Normal file
21
packages/editor/src/extensions/quirks/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export * from "./quirks";
|
||||
export { Quirks as default } from "./quirks";
|
||||
115
packages/editor/src/extensions/quirks/quirks.ts
Normal file
115
packages/editor/src/extensions/quirks/quirks.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
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 { Editor, Extension, findParentNode } from "@tiptap/core";
|
||||
import { EditorState, Selection } from "@tiptap/pm/state";
|
||||
import { isAndroid } from "../../utils/platform";
|
||||
|
||||
export type QuirksOptions = {
|
||||
/**
|
||||
* List of node types that do not get removed on pressing Backspace
|
||||
* even when they are empty.
|
||||
*/
|
||||
irremovableNodesOnBackspace: string[];
|
||||
|
||||
/**
|
||||
* Nodes that should be easily escapable if at the beginning of the
|
||||
* document by pressing the ArrowUp key. Pressing the ArrowUp key
|
||||
* will create an empty paragraph before the node.
|
||||
*/
|
||||
escapableNodesIfAtDocumentStart: string[];
|
||||
};
|
||||
|
||||
export const Quirks = Extension.create<QuirksOptions>({
|
||||
name: "quirks",
|
||||
addOptions() {
|
||||
return {
|
||||
escapableNodesIfAtDocumentStart: [],
|
||||
irremovableNodesOnBackspace: []
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
// exit node on arrow up
|
||||
ArrowUp: ({ editor }) =>
|
||||
escapeNode(editor, this.options.escapableNodesIfAtDocumentStart),
|
||||
ArrowLeft: ({ editor }) =>
|
||||
escapeNode(editor, this.options.escapableNodesIfAtDocumentStart),
|
||||
ArrowRight: ({ editor }) =>
|
||||
escapeNode(editor, this.options.escapableNodesIfAtDocumentStart),
|
||||
|
||||
Backspace: ({ editor }) => {
|
||||
const { empty, $anchor } = editor.state.selection;
|
||||
|
||||
const nextNode = editor.state.doc.nodeAt($anchor.pos + 1);
|
||||
const node = findFromParentNodes(
|
||||
editor.state,
|
||||
this.options.irremovableNodesOnBackspace
|
||||
);
|
||||
|
||||
if (!empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node && !node.node.textContent.length) {
|
||||
return this.editor.commands.deleteNode(node.node.type);
|
||||
}
|
||||
// on android due to composition issues with various keyboards,
|
||||
// sometimes backspace is detected one node behind. We need to
|
||||
// manually handle this case.
|
||||
else if (
|
||||
isAndroid &&
|
||||
nextNode &&
|
||||
this.options.irremovableNodesOnBackspace.includes(
|
||||
nextNode.type.name
|
||||
) &&
|
||||
!nextNode.textContent.length
|
||||
) {
|
||||
return this.editor.commands.command(({ tr }) => {
|
||||
tr.delete($anchor.pos + 1, $anchor.pos + 1 + nextNode.nodeSize);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const findFromParentNodes = (state: EditorState, types: string[]) => {
|
||||
return findParentNode((node) => types.includes(node.type.name))(
|
||||
state.selection
|
||||
);
|
||||
};
|
||||
|
||||
function escapeNode(editor: Editor, escapableNodes: string[]) {
|
||||
const { state } = editor;
|
||||
const { selection } = state;
|
||||
const { $anchor, empty } = selection;
|
||||
const documentStartPos = Selection.atStart(editor.state.doc).$head.pos;
|
||||
const isAtStart = $anchor.pos === documentStartPos;
|
||||
console.log(isAtStart);
|
||||
if (!empty || !isAtStart || !findFromParentNodes(state, escapableNodes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.commands.insertContentAt(0, "<p></p>");
|
||||
}
|
||||
@@ -121,7 +121,7 @@ export function TaskItemComponent(
|
||||
toggle();
|
||||
}}
|
||||
onTouchEnd={(e) => {
|
||||
if (globalThis["keyboardShown"] || isiOS()) {
|
||||
if (globalThis["keyboardShown"] || isiOS) {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { onArrowUpPressed } from "../list-item/commands";
|
||||
import { TaskItem } from "@tiptap/extension-task-item";
|
||||
import { TaskItemComponent } from "./component";
|
||||
import { createNodeView } from "../react";
|
||||
@@ -73,8 +72,7 @@ export const TaskItemNode = TaskItem.extend({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
ArrowUp: ({ editor }) => onArrowUpPressed(editor, this.name, this.type)
|
||||
...this.parent?.()
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ import { DownloadOptions } from "./utils/downloader";
|
||||
import { Heading } from "./extensions/heading";
|
||||
import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
|
||||
import Blockquote from "./extensions/blockquote";
|
||||
import { Quirks } from "./extensions/quirks";
|
||||
import { LIST_NODE_TYPES } from "./utils/node-types";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
@@ -220,22 +222,6 @@ const useTiptap = (
|
||||
OrderedList.configure({ keepMarks: true, keepAttributes: true }),
|
||||
TaskItemNode.configure({ nested: true }),
|
||||
TaskListNode,
|
||||
ListKeymap.configure({
|
||||
listTypes: [
|
||||
{
|
||||
itemName: ListItem.name,
|
||||
wrapperNames: [BulletList.name, OrderedList.name]
|
||||
},
|
||||
{
|
||||
itemName: TaskItemNode.name,
|
||||
wrapperNames: [TaskListNode.name]
|
||||
},
|
||||
{
|
||||
itemName: OutlineListItem.name,
|
||||
wrapperNames: [OutlineList.name]
|
||||
}
|
||||
]
|
||||
}),
|
||||
Link.extend({
|
||||
inclusive: true
|
||||
}).configure({
|
||||
@@ -286,7 +272,38 @@ const useTiptap = (
|
||||
}),
|
||||
DateTime.configure({ dateFormat, timeFormat }),
|
||||
KeyMap,
|
||||
WebClipNode
|
||||
WebClipNode,
|
||||
|
||||
// Quirks handlers
|
||||
Quirks.configure({
|
||||
irremovableNodesOnBackspace: [
|
||||
CodeBlock.name,
|
||||
TaskListNode.name,
|
||||
Table.name
|
||||
],
|
||||
escapableNodesIfAtDocumentStart: [
|
||||
CodeBlock.name,
|
||||
Table.name,
|
||||
Blockquote.name,
|
||||
...LIST_NODE_TYPES
|
||||
]
|
||||
}),
|
||||
ListKeymap.configure({
|
||||
listTypes: [
|
||||
{
|
||||
itemName: ListItem.name,
|
||||
wrapperNames: [BulletList.name, OrderedList.name]
|
||||
},
|
||||
{
|
||||
itemName: TaskItemNode.name,
|
||||
wrapperNames: [TaskListNode.name]
|
||||
},
|
||||
{
|
||||
itemName: OutlineListItem.name,
|
||||
wrapperNames: [OutlineList.name]
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
onBeforeCreate: ({ editor }) => {
|
||||
editor.storage.portalProviderAPI = PortalProviderAPI;
|
||||
|
||||
@@ -17,17 +17,17 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function isiOS(): boolean {
|
||||
return (
|
||||
[
|
||||
"iPad Simulator",
|
||||
"iPhone Simulator",
|
||||
"iPod Simulator",
|
||||
"iPad",
|
||||
"iPhone",
|
||||
"iPod"
|
||||
].includes(navigator.platform) ||
|
||||
// iPad on iOS 13 detection
|
||||
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||
);
|
||||
}
|
||||
export const isiOS =
|
||||
[
|
||||
"iPad Simulator",
|
||||
"iPhone Simulator",
|
||||
"iPod Simulator",
|
||||
"iPad",
|
||||
"iPhone",
|
||||
"iPod"
|
||||
].includes(navigator.platform) ||
|
||||
// iPad on iOS 13 detection
|
||||
(navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
||||
|
||||
export const isAndroid =
|
||||
navigator.userAgent.toLowerCase().indexOf("android") > -1;
|
||||
|
||||
Reference in New Issue
Block a user