diff --git a/packages/editor/dist/es/extensions/keepinview/index.d.ts b/packages/editor/dist/es/extensions/keepinview/index.d.ts new file mode 100644 index 000000000..7d6036306 --- /dev/null +++ b/packages/editor/dist/es/extensions/keepinview/index.d.ts @@ -0,0 +1 @@ +export * from "./keep-in-view"; diff --git a/packages/editor/dist/es/extensions/keepinview/index.js b/packages/editor/dist/es/extensions/keepinview/index.js new file mode 100644 index 000000000..7d6036306 --- /dev/null +++ b/packages/editor/dist/es/extensions/keepinview/index.js @@ -0,0 +1 @@ +export * from "./keep-in-view"; diff --git a/packages/editor/dist/es/extensions/keepinview/keepinview.d.ts b/packages/editor/dist/es/extensions/keepinview/keepinview.d.ts new file mode 100644 index 000000000..708121164 --- /dev/null +++ b/packages/editor/dist/es/extensions/keepinview/keepinview.d.ts @@ -0,0 +1,3 @@ +import { Editor, Extension } from "@tiptap/core"; +export declare const KeepInView: Extension; +export declare function keepLastLineInView(editor: Editor): void; diff --git a/packages/editor/dist/es/extensions/keepinview/keepinview.js b/packages/editor/dist/es/extensions/keepinview/keepinview.js new file mode 100644 index 000000000..65bff8f9f --- /dev/null +++ b/packages/editor/dist/es/extensions/keepinview/keepinview.js @@ -0,0 +1,46 @@ +import { Extension, posToDOMRect } from "@tiptap/core"; +export const KeepInView = Extension.create({ + addKeyboardShortcuts() { + return { + Enter: ({ editor }) => { + setTimeout(() => { + keepLastLineInView(editor); + }); + return false; + }, + }; + }, +}); +export function keepLastLineInView(editor) { + const THRESHOLD = 100; + const node = editor.state.selection.$from; + const { top } = posToDOMRect(editor.view, node.pos, node.pos + 1); + const isBelowThreshold = window.innerHeight - top < THRESHOLD; + if (isBelowThreshold) { + let { node: domNode } = editor.view.domAtPos(node.pos); + if (domNode.nodeType === Node.TEXT_NODE && domNode.parentNode) + domNode = domNode.parentNode; + if (domNode instanceof HTMLElement) { + const container = findScrollContainer(domNode); + if (container) { + container.scrollBy({ top: THRESHOLD, behavior: "smooth" }); + } + else + domNode.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } +} +const findScrollContainer = (element) => { + if (!element) { + return undefined; + } + let parent = element.parentElement; + while (parent) { + const { overflow } = parent.style; + if (overflow.split(" ").every((o) => o === "auto" || o === "scroll")) { + return parent; + } + parent = parent.parentElement; + } + return document.documentElement; +}; diff --git a/packages/editor/dist/es/extensions/scroll/index.d.ts b/packages/editor/dist/es/extensions/scroll/index.d.ts new file mode 100644 index 000000000..7d6036306 --- /dev/null +++ b/packages/editor/dist/es/extensions/scroll/index.d.ts @@ -0,0 +1 @@ +export * from "./keep-in-view"; diff --git a/packages/editor/dist/es/extensions/scroll/index.js b/packages/editor/dist/es/extensions/scroll/index.js new file mode 100644 index 000000000..7d6036306 --- /dev/null +++ b/packages/editor/dist/es/extensions/scroll/index.js @@ -0,0 +1 @@ +export * from "./keep-in-view"; diff --git a/packages/editor/dist/es/extensions/scroll/keepinview.d.ts b/packages/editor/dist/es/extensions/scroll/keepinview.d.ts new file mode 100644 index 000000000..708121164 --- /dev/null +++ b/packages/editor/dist/es/extensions/scroll/keepinview.d.ts @@ -0,0 +1,3 @@ +import { Editor, Extension } from "@tiptap/core"; +export declare const KeepInView: Extension; +export declare function keepLastLineInView(editor: Editor): void; diff --git a/packages/editor/dist/es/extensions/scroll/keepinview.js b/packages/editor/dist/es/extensions/scroll/keepinview.js new file mode 100644 index 000000000..65bff8f9f --- /dev/null +++ b/packages/editor/dist/es/extensions/scroll/keepinview.js @@ -0,0 +1,46 @@ +import { Extension, posToDOMRect } from "@tiptap/core"; +export const KeepInView = Extension.create({ + addKeyboardShortcuts() { + return { + Enter: ({ editor }) => { + setTimeout(() => { + keepLastLineInView(editor); + }); + return false; + }, + }; + }, +}); +export function keepLastLineInView(editor) { + const THRESHOLD = 100; + const node = editor.state.selection.$from; + const { top } = posToDOMRect(editor.view, node.pos, node.pos + 1); + const isBelowThreshold = window.innerHeight - top < THRESHOLD; + if (isBelowThreshold) { + let { node: domNode } = editor.view.domAtPos(node.pos); + if (domNode.nodeType === Node.TEXT_NODE && domNode.parentNode) + domNode = domNode.parentNode; + if (domNode instanceof HTMLElement) { + const container = findScrollContainer(domNode); + if (container) { + container.scrollBy({ top: THRESHOLD, behavior: "smooth" }); + } + else + domNode.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } +} +const findScrollContainer = (element) => { + if (!element) { + return undefined; + } + let parent = element.parentElement; + while (parent) { + const { overflow } = parent.style; + if (overflow.split(" ").every((o) => o === "auto" || o === "scroll")) { + return parent; + } + parent = parent.parentElement; + } + return document.documentElement; +}; diff --git a/packages/editor/dist/es/extensions/scroll/scroll.d.ts b/packages/editor/dist/es/extensions/scroll/scroll.d.ts new file mode 100644 index 000000000..c0695030e --- /dev/null +++ b/packages/editor/dist/es/extensions/scroll/scroll.d.ts @@ -0,0 +1,3 @@ +import { Editor, Extension } from "@tiptap/core"; +export declare const Scroll: Extension; +export declare function keepLastLineInView(editor: Editor): void; diff --git a/packages/editor/dist/es/extensions/scroll/scroll.js b/packages/editor/dist/es/extensions/scroll/scroll.js new file mode 100644 index 000000000..b5f96f60e --- /dev/null +++ b/packages/editor/dist/es/extensions/scroll/scroll.js @@ -0,0 +1,46 @@ +import { Extension, posToDOMRect } from "@tiptap/core"; +export const Scroll = Extension.create({ + addKeyboardShortcuts() { + return { + Enter: ({ editor }) => { + setTimeout(() => { + keepLastLineInView(editor); + }); + return false; + }, + }; + }, +}); +export function keepLastLineInView(editor) { + const THRESHOLD = 100; + const node = editor.state.selection.$from; + const { top } = posToDOMRect(editor.view, node.pos, node.pos + 1); + const isBelowThreshold = window.innerHeight - top < THRESHOLD; + if (isBelowThreshold) { + let { node: domNode } = editor.view.domAtPos(node.pos); + if (domNode.nodeType === Node.TEXT_NODE && domNode.parentNode) + domNode = domNode.parentNode; + if (domNode instanceof HTMLElement) { + const container = findScrollContainer(domNode); + if (container) { + container.scrollBy({ top: THRESHOLD, behavior: "smooth" }); + } + else + domNode.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } +} +const findScrollContainer = (element) => { + if (!element) { + return undefined; + } + let parent = element.parentElement; + while (parent) { + const { overflow } = parent.style; + if (overflow.split(" ").every((o) => o === "auto" || o === "scroll")) { + return parent; + } + parent = parent.parentElement; + } + return document.documentElement; +}; diff --git a/packages/editor/dist/es/index.d.ts b/packages/editor/dist/es/index.d.ts index 183bc0edb..22e35801e 100644 --- a/packages/editor/dist/es/index.d.ts +++ b/packages/editor/dist/es/index.d.ts @@ -1,4 +1,3 @@ -/// import "./extensions"; import Toolbar from "./toolbar"; import { Theme } from "@notesnook/theme"; diff --git a/packages/editor/dist/es/index.js b/packages/editor/dist/es/index.js index b02ef144d..08c44db11 100644 --- a/packages/editor/dist/es/index.js +++ b/packages/editor/dist/es/index.js @@ -46,6 +46,7 @@ import { MathInline, MathBlock } from "./extensions/math"; import { NodeViewSelectionNotifier, usePortalProvider, } from "./extensions/react"; import { OutlineList } from "./extensions/outline-list"; import { OutlineListItem } from "./extensions/outline-list-item"; +import { KeepInView } from "./extensions/keep-in-view"; import { Table } from "./extensions/table"; import { useToolbarStore } from "./toolbar/stores/toolbar-store"; import { useEditor } from "./hooks/use-editor"; @@ -126,6 +127,7 @@ const useTiptap = (options = {}, deps = []) => { Codemark, MathInline, MathBlock, + KeepInView, ], onBeforeCreate: ({ editor }) => { if (theme) { diff --git a/packages/editor/dist/es/toolbar/components/toolbutton.d.ts b/packages/editor/dist/es/toolbar/components/toolbutton.d.ts index d8d394a7e..c0b3fc2f9 100644 --- a/packages/editor/dist/es/toolbar/components/toolbutton.d.ts +++ b/packages/editor/dist/es/toolbar/components/toolbutton.d.ts @@ -14,8 +14,8 @@ export declare type ToolButtonProps = ButtonProps & { }; export declare const ToolButton: React.NamedExoticComponent | undefined; variant?: ToolButtonVariant | undefined; diff --git a/packages/editor/dist/es/toolbar/tooldefinitions.d.ts b/packages/editor/dist/es/toolbar/tooldefinitions.d.ts index a3d3e1997..e501747aa 100644 --- a/packages/editor/dist/es/toolbar/tooldefinitions.d.ts +++ b/packages/editor/dist/es/toolbar/tooldefinitions.d.ts @@ -1,5 +1,5 @@ import { ToolbarDefinition, ToolDefinition } from "./types"; import { ToolId } from "./tools"; export declare function getToolDefinition(id: ToolId): ToolDefinition; -export declare function getAllTools(): Record<"bold" | "italic" | "underline" | "strikethrough" | "code" | "codeRemove" | "subscript" | "superscript" | "numberedList" | "bulletList" | "highlight" | "textColor" | "openLink" | "linkSettings" | "imageSettings" | "rowProperties" | "insertRowBelow" | "insertRowAbove" | "moveRowDown" | "moveRowUp" | "deleteRow" | "columnProperties" | "insertColumnRight" | "insertColumnLeft" | "moveColumnRight" | "moveColumnLeft" | "deleteColumn" | "cellProperties" | "cellBorderColor" | "deleteTable" | "mergeCells" | "splitCells" | "attachmentSettings" | "embedSettings" | "tableSettings" | "math" | "fontFamily" | "fontSize" | "indent" | "outdent" | "clearformatting" | "addLink" | "editLink" | "removeLink" | "insertBlock" | "headings" | "alignment" | "textDirection" | "imageAlignCenter" | "imageAlignLeft" | "imageAlignRight" | "imageProperties" | "embedAlignCenter" | "embedAlignLeft" | "embedAlignRight" | "embedProperties" | "downloadAttachment" | "removeAttachment" | "cellBackgroundColor" | "cellTextColor" | "cellBorderWidth", ToolDefinition>; +export declare function getAllTools(): Record<"bulletList" | "fontSize" | "underline" | "bold" | "code" | "italic" | "subscript" | "superscript" | "textDirection" | "fontFamily" | "highlight" | "removeAttachment" | "downloadAttachment" | "deleteColumn" | "deleteRow" | "deleteTable" | "mergeCells" | "strikethrough" | "codeRemove" | "numberedList" | "textColor" | "openLink" | "linkSettings" | "imageSettings" | "rowProperties" | "insertRowBelow" | "insertRowAbove" | "moveRowDown" | "moveRowUp" | "columnProperties" | "insertColumnRight" | "insertColumnLeft" | "moveColumnRight" | "moveColumnLeft" | "cellProperties" | "cellBorderColor" | "splitCells" | "attachmentSettings" | "embedSettings" | "tableSettings" | "math" | "indent" | "outdent" | "clearformatting" | "addLink" | "editLink" | "removeLink" | "insertBlock" | "headings" | "alignment" | "imageAlignCenter" | "imageAlignLeft" | "imageAlignRight" | "imageProperties" | "embedAlignCenter" | "embedAlignLeft" | "embedAlignRight" | "embedProperties" | "cellBackgroundColor" | "cellTextColor" | "cellBorderWidth", ToolDefinition>; export declare function getDefaultPresets(): Record<"default" | "minimal", ToolbarDefinition>; diff --git a/packages/editor/dist/es/toolbar/utils/prosemirror.d.ts b/packages/editor/dist/es/toolbar/utils/prosemirror.d.ts index ef7a00a57..d072ce375 100644 --- a/packages/editor/dist/es/toolbar/utils/prosemirror.d.ts +++ b/packages/editor/dist/es/toolbar/utils/prosemirror.d.ts @@ -1,14 +1,14 @@ import { Editor } from "@tiptap/core"; -import { Node, Mark } from "prosemirror-model"; +import { Node as ProsemirrorNode, Mark } from "prosemirror-model"; import { Selection } from "prosemirror-state"; export declare type NodeWithOffset = { - node: Node; + node: ProsemirrorNode; from: number; to: number; }; export declare function findSelectedDOMNode(editor: Editor, types: string[]): HTMLElement | null; -export declare function findSelectedNode(editor: Editor, type: string): Node | null; -export declare function findMark(node: Node, type: string): Mark | undefined; +export declare function findSelectedNode(editor: Editor, type: string): ProsemirrorNode | null; +export declare function findMark(node: ProsemirrorNode, type: string): Mark | undefined; export declare function selectionToOffset(selection: Selection): NodeWithOffset; export declare function findListItemType(editor: Editor): string | null; export declare function isListActive(editor: Editor): boolean; diff --git a/packages/editor/dist/es/toolbar/utils/prosemirror.js b/packages/editor/dist/es/toolbar/utils/prosemirror.js index fd18498de..6aecf9e79 100644 --- a/packages/editor/dist/es/toolbar/utils/prosemirror.js +++ b/packages/editor/dist/es/toolbar/utils/prosemirror.js @@ -1,4 +1,4 @@ -import { findParentNode, } from "@tiptap/core"; +import { findParentNode } from "@tiptap/core"; export function findSelectedDOMNode(editor, types) { var _a; const { $anchor } = editor.state.selection; diff --git a/packages/editor/src/extensions/keep-in-view/index.ts b/packages/editor/src/extensions/keep-in-view/index.ts new file mode 100644 index 000000000..7d6036306 --- /dev/null +++ b/packages/editor/src/extensions/keep-in-view/index.ts @@ -0,0 +1 @@ +export * from "./keep-in-view"; diff --git a/packages/editor/src/extensions/keep-in-view/keep-in-view.ts b/packages/editor/src/extensions/keep-in-view/keep-in-view.ts new file mode 100644 index 000000000..36f9e8986 --- /dev/null +++ b/packages/editor/src/extensions/keep-in-view/keep-in-view.ts @@ -0,0 +1,51 @@ +import { Editor, Extension, posToDOMRect } from "@tiptap/core"; + +export const KeepInView = Extension.create({ + addKeyboardShortcuts() { + return { + Enter: ({ editor }) => { + setTimeout(() => { + keepLastLineInView(editor); + }); + return false; + }, + }; + }, +}); + +export function keepLastLineInView(editor: Editor) { + const THRESHOLD = 100; + + const node = editor.state.selection.$from; + const { top } = posToDOMRect(editor.view, node.pos, node.pos + 1); + const isBelowThreshold = window.innerHeight - top < THRESHOLD; + if (isBelowThreshold) { + let { node: domNode } = editor.view.domAtPos(node.pos); + if (domNode.nodeType === Node.TEXT_NODE && domNode.parentNode) + domNode = domNode.parentNode; + + if (domNode instanceof HTMLElement) { + const container = findScrollContainer(domNode); + if (container) { + container.scrollBy({ top: THRESHOLD, behavior: "smooth" }); + } else domNode.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } +} + +const findScrollContainer = (element: HTMLElement) => { + if (!element) { + return undefined; + } + + let parent = element.parentElement; + while (parent) { + const { overflow } = parent.style; + if (overflow.split(" ").every((o) => o === "auto" || o === "scroll")) { + return parent; + } + parent = parent.parentElement; + } + + return document.documentElement; +}; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 836c4c24b..7214f48ba 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -39,6 +39,7 @@ import { } from "./extensions/react"; import { OutlineList } from "./extensions/outline-list"; import { OutlineListItem } from "./extensions/outline-list-item"; +import { KeepInView } from "./extensions/keep-in-view"; import { Table } from "./extensions/table"; import { useToolbarStore } from "./toolbar/stores/toolbar-store"; import { useEditor } from "./hooks/use-editor"; @@ -140,6 +141,7 @@ const useTiptap = ( Codemark, MathInline, MathBlock, + KeepInView, ], onBeforeCreate: ({ editor }) => { if (theme) { diff --git a/packages/editor/src/toolbar/utils/prosemirror.ts b/packages/editor/src/toolbar/utils/prosemirror.ts index 8d230dff5..25e1cbcf0 100644 --- a/packages/editor/src/toolbar/utils/prosemirror.ts +++ b/packages/editor/src/toolbar/utils/prosemirror.ts @@ -1,14 +1,9 @@ -import { - Editor, - findParentNode, - findParentNodeClosestToPos, - isNodeSelection, -} from "@tiptap/core"; -import { Node, Mark } from "prosemirror-model"; -import { Selection } from "prosemirror-state"; +import { Editor, findParentNode, posToDOMRect } from "@tiptap/core"; +import { Node as ProsemirrorNode, Mark } from "prosemirror-model"; +import { Selection, Transaction } from "prosemirror-state"; export type NodeWithOffset = { - node: Node; + node: ProsemirrorNode; from: number; to: number; }; @@ -30,7 +25,10 @@ export function findSelectedDOMNode( return (editor.view.nodeDOM(pos) as HTMLElement) || null; } -export function findSelectedNode(editor: Editor, type: string): Node | null { +export function findSelectedNode( + editor: Editor, + type: string +): ProsemirrorNode | null { const { $anchor } = editor.state.selection; const selectedNode = editor.state.doc.nodeAt($anchor.pos); @@ -45,7 +43,10 @@ export function findSelectedNode(editor: Editor, type: string): Node | null { return editor.state.doc.nodeAt(pos); } -export function findMark(node: Node, type: string): Mark | undefined { +export function findMark( + node: ProsemirrorNode, + type: string +): Mark | undefined { const mark = node.marks.find((m) => m.type.name === type); return mark; }