feat: add keep last line in view extension

This commit is contained in:
thecodrr
2022-07-06 14:02:28 +05:00
parent e5d4f78d7e
commit c19b767ffb
20 changed files with 227 additions and 20 deletions

View File

@@ -0,0 +1 @@
export * from "./keep-in-view";

View File

@@ -0,0 +1 @@
export * from "./keep-in-view";

View File

@@ -0,0 +1,3 @@
import { Editor, Extension } from "@tiptap/core";
export declare const KeepInView: Extension<any, any>;
export declare function keepLastLineInView(editor: Editor): void;

View File

@@ -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;
};

View File

@@ -0,0 +1 @@
export * from "./keep-in-view";

View File

@@ -0,0 +1 @@
export * from "./keep-in-view";

View File

@@ -0,0 +1,3 @@
import { Editor, Extension } from "@tiptap/core";
export declare const KeepInView: Extension<any, any>;
export declare function keepLastLineInView(editor: Editor): void;

View File

@@ -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;
};

View File

@@ -0,0 +1,3 @@
import { Editor, Extension } from "@tiptap/core";
export declare const Scroll: Extension<any, any>;
export declare function keepLastLineInView(editor: Editor): void;

View File

@@ -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;
};

View File

@@ -1,4 +1,3 @@
/// <reference types="react" />
import "./extensions";
import Toolbar from "./toolbar";
import { Theme } from "@notesnook/theme";

View File

@@ -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) {

View File

@@ -14,8 +14,8 @@ export declare type ToolButtonProps = ButtonProps & {
};
export declare const ToolButton: React.NamedExoticComponent<ButtonProps & {
icon: IconNames;
iconColor?: keyof import("@notesnook/theme/dist/theme/colorscheme/static").StaticColors | "primary" | "placeholder" | "background" | "bgTransparent" | "accent" | "bgSecondary" | "bgSecondaryText" | "bgSecondaryHover" | "border" | "hover" | "fontSecondary" | "fontTertiary" | "text" | "overlay" | "secondary" | "icon" | "disabled" | "checked" | "red" | "orange" | "yellow" | "green" | "blue" | "purple" | "gray" | undefined;
iconSize?: number | "small" | "medium" | "big" | undefined;
iconColor?: "background" | "border" | "text" | "blue" | "gray" | "green" | "orange" | "purple" | "red" | "yellow" | "checked" | "disabled" | "placeholder" | "icon" | "overlay" | "primary" | "bgSecondary" | keyof import("@notesnook/theme/dist/theme/colorscheme/static").StaticColors | "bgTransparent" | "accent" | "bgSecondaryText" | "bgSecondaryHover" | "hover" | "fontSecondary" | "fontTertiary" | "secondary" | undefined;
iconSize?: number | "small" | "big" | "medium" | undefined;
toggled: boolean;
buttonRef?: React.MutableRefObject<HTMLButtonElement | null | undefined> | undefined;
variant?: ToolButtonVariant | undefined;

View File

@@ -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>;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1 @@
export * from "./keep-in-view";

View File

@@ -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;
};

View File

@@ -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) {

View File

@@ -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;
}