Tiptap3 migration - WIP

This commit is contained in:
Philipinho
2025-08-02 19:09:06 -07:00
parent 1615e0f4ad
commit 2adc6a60d2
35 changed files with 983 additions and 883 deletions
@@ -1,9 +1,5 @@
import { import { BubbleMenu, BubbleMenuProps } from "@tiptap/react/menus";
BubbleMenu, import { isNodeSelection, useEditor } from "@tiptap/react";
BubbleMenuProps,
isNodeSelection,
useEditor,
} from "@tiptap/react";
import { FC, useEffect, useRef, useState } from "react"; import { FC, useEffect, useRef, useState } from "react";
import { import {
IconBold, IconBold,
@@ -114,14 +110,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
} }
return isTextSelected(editor); return isTextSelected(editor);
}, },
tippyOptions: { options: {
moveTransition: "transform 0.15s ease-out", placement: "top",
onCreate: (instance) => { offset: 8,
instance.popper.firstChild?.addEventListener("blur", (event) => {
event.preventDefault();
event.stopImmediatePropagation();
});
},
onHide: () => { onHide: () => {
setIsNodeSelectorOpen(false); setIsNodeSelectorOpen(false);
setIsTextAlignmentOpen(false); setIsTextAlignmentOpen(false);
@@ -1,8 +1,5 @@
import { import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
BubbleMenu as BaseBubbleMenu, import { findParentNode, posToDOMRect } from "@tiptap/react";
findParentNode,
posToDOMRect,
} from "@tiptap/react";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { Node as PMNode } from "prosemirror-model"; import { Node as PMNode } from "prosemirror-model";
import { import {
@@ -55,20 +52,17 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
}, },
[editor], [editor],
); );
return ( return (
<BaseBubbleMenu <BaseBubbleMenu
editor={editor} editor={editor}
pluginKey={`callout-menu}`} pluginKey={`callout-menu}`}
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
getReferenceClientRect, // getReferenceClientRect,
offset: [0, 10], placement: "right-end",
placement: "bottom", // offset: 233,
zIndex: 99, // zIndex: 99,
popperOptions: { flip: false,
modifiers: [{ name: "flip", enabled: false }],
},
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
@@ -90,6 +90,7 @@ export default function CodeBlockView(props: NodeViewProps) {
node.textContent.length > 0 node.textContent.length > 0
} }
> >
{/* @ts-ignore */}
<NodeViewContent as="code" className={`language-${language}`} /> <NodeViewContent as="code" className={`language-${language}`} />
</pre> </pre>
@@ -1,16 +1,12 @@
import { import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
BubbleMenu as BaseBubbleMenu, import { findParentNode, posToDOMRect } from "@tiptap/react";
findParentNode, import { useCallback } from "react";
posToDOMRect, import { Node as PMNode } from "prosemirror-model";
} from '@tiptap/react';
import { useCallback } from 'react';
import { sticky } from 'tippy.js';
import { Node as PMNode } from 'prosemirror-model';
import { import {
EditorMenuProps, EditorMenuProps,
ShouldShowProps, ShouldShowProps,
} from '@/features/editor/components/table/types/types.ts'; } from "@/features/editor/components/table/types/types.ts";
import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx'; import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
export function DrawioMenu({ editor }: EditorMenuProps) { export function DrawioMenu({ editor }: EditorMenuProps) {
const shouldShow = useCallback( const shouldShow = useCallback(
@@ -19,14 +15,14 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
return false; return false;
} }
return editor.isActive('drawio') && editor.getAttributes('drawio')?.src; return editor.isActive("drawio") && editor.getAttributes("drawio")?.src;
}, },
[editor] [editor],
); );
const getReferenceClientRect = useCallback(() => { const getReferenceClientRect = useCallback(() => {
const { selection } = editor.state; const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === 'drawio'; const predicate = (node: PMNode) => node.type.name === "drawio";
const parent = findParentNode(predicate)(selection); const parent = findParentNode(predicate)(selection);
if (parent) { if (parent) {
@@ -39,9 +35,9 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
const onWidthChange = useCallback( const onWidthChange = useCallback(
(value: number) => { (value: number) => {
editor.commands.updateAttributes('drawio', { width: `${value}%` }); editor.commands.updateAttributes("drawio", { width: `${value}%` });
}, },
[editor] [editor],
); );
return ( return (
@@ -49,29 +45,26 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
editor={editor} editor={editor}
pluginKey={`drawio-menu}`} pluginKey={`drawio-menu}`}
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
getReferenceClientRect, //getReferenceClientRect,
offset: [0, 8], placement: "bottom",
zIndex: 99, offset: 8,
popperOptions: { // zIndex: 99,
modifiers: [{ name: 'flip', enabled: false }], flip: false,
},
plugins: [sticky],
sticky: 'popper',
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center', alignItems: "center",
}} }}
> >
{editor.getAttributes('drawio')?.width && ( {editor.getAttributes("drawio")?.width && (
<NodeWidthResize <NodeWidthResize
onChange={onWidthChange} onChange={onWidthChange}
value={parseInt(editor.getAttributes('drawio').width)} value={parseInt(editor.getAttributes("drawio").width)}
/> />
)} )}
</div> </div>
@@ -66,6 +66,7 @@ export default function DrawioView(props: NodeViewProps) {
const fileName = "diagram.drawio.svg"; const fileName = "diagram.drawio.svg";
const drawioSVGFile = await svgStringToFile(svgString, fileName); const drawioSVGFile = await svgStringToFile(svgString, fileName);
//@ts-ignore
const pageId = editor.storage?.pageId; const pageId = editor.storage?.pageId;
let attachment: IAttachment = null; let attachment: IAttachment = null;
@@ -1,16 +1,12 @@
import { import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
BubbleMenu as BaseBubbleMenu, import { findParentNode, posToDOMRect } from "@tiptap/react";
findParentNode, import { useCallback } from "react";
posToDOMRect, import { Node as PMNode } from "prosemirror-model";
} from '@tiptap/react';
import { useCallback } from 'react';
import { sticky } from 'tippy.js';
import { Node as PMNode } from 'prosemirror-model';
import { import {
EditorMenuProps, EditorMenuProps,
ShouldShowProps, ShouldShowProps,
} from '@/features/editor/components/table/types/types.ts'; } from "@/features/editor/components/table/types/types.ts";
import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx'; import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
export function ExcalidrawMenu({ editor }: EditorMenuProps) { export function ExcalidrawMenu({ editor }: EditorMenuProps) {
const shouldShow = useCallback( const shouldShow = useCallback(
@@ -19,14 +15,16 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
return false; return false;
} }
return editor.isActive('excalidraw') && editor.getAttributes('excalidraw')?.src; return (
editor.isActive("excalidraw") && editor.getAttributes("excalidraw")?.src
);
}, },
[editor] [editor],
); );
const getReferenceClientRect = useCallback(() => { const getReferenceClientRect = useCallback(() => {
const { selection } = editor.state; const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === 'excalidraw'; const predicate = (node: PMNode) => node.type.name === "excalidraw";
const parent = findParentNode(predicate)(selection); const parent = findParentNode(predicate)(selection);
if (parent) { if (parent) {
@@ -39,9 +37,9 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
const onWidthChange = useCallback( const onWidthChange = useCallback(
(value: number) => { (value: number) => {
editor.commands.updateAttributes('excalidraw', { width: `${value}%` }); editor.commands.updateAttributes("excalidraw", { width: `${value}%` });
}, },
[editor] [editor],
); );
return ( return (
@@ -49,29 +47,26 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
editor={editor} editor={editor}
pluginKey={`excalidraw-menu}`} pluginKey={`excalidraw-menu}`}
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
getReferenceClientRect, //getReferenceClientRect,
offset: [0, 8], placement: "bottom",
zIndex: 99, offset: 8,
popperOptions: { // zIndex: 99,
modifiers: [{ name: 'flip', enabled: false }], flip: false,
},
plugins: [sticky],
sticky: 'popper',
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center', alignItems: "center",
}} }}
> >
{editor.getAttributes('excalidraw')?.width && ( {editor.getAttributes("excalidraw")?.width && (
<NodeWidthResize <NodeWidthResize
onChange={onWidthChange} onChange={onWidthChange}
value={parseInt(editor.getAttributes('excalidraw').width)} value={parseInt(editor.getAttributes("excalidraw").width)}
/> />
)} )}
</div> </div>
@@ -98,6 +98,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
const fileName = "diagram.excalidraw.svg"; const fileName = "diagram.excalidraw.svg";
const excalidrawSvgFile = await svgStringToFile(svgString, fileName); const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
// @ts-ignore
const pageId = editor.storage?.pageId; const pageId = editor.storage?.pageId;
let attachment: IAttachment = null; let attachment: IAttachment = null;
@@ -1,10 +1,6 @@
import { import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
BubbleMenu as BaseBubbleMenu, import { findParentNode, posToDOMRect } from "@tiptap/react";
findParentNode,
posToDOMRect,
} from "@tiptap/react";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { sticky } from "tippy.js";
import { Node as PMNode } from "prosemirror-model"; import { Node as PMNode } from "prosemirror-model";
import { import {
EditorMenuProps, EditorMenuProps,
@@ -85,15 +81,12 @@ export function ImageMenu({ editor }: EditorMenuProps) {
editor={editor} editor={editor}
pluginKey={`image-menu}`} pluginKey={`image-menu}`}
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
getReferenceClientRect, // getReferenceClientRect,
offset: [0, 8], placement: "bottom",
zIndex: 99, offset: 8,
popperOptions: { //zIndex: 99,
modifiers: [{ name: "flip", enabled: false }], flip: false,
},
plugins: [sticky],
sticky: "popper",
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
@@ -1,4 +1,5 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react"; import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { offset } from "@floating-ui/dom";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts"; import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts";
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx"; import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
@@ -50,16 +51,13 @@ export function LinkMenu({ editor, appendTo }: EditorMenuProps) {
editor={editor} editor={editor}
pluginKey={`link-menu}`} pluginKey={`link-menu}`}
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
appendTo: () => { onHide: () => {
return appendTo?.current;
},
onHidden: () => {
setShowEdit(false); setShowEdit(false);
}, },
placement: "bottom", placement: "bottom",
offset: [0, 5], offset: 5,
zIndex: 101, // zIndex: 101,
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
@@ -106,6 +106,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
setRenderItems(items); setRenderItems(items);
// update editor storage // update editor storage
//@ts-ignore
props.editor.storage.mentionItems = items; props.editor.storage.mentionItems = items;
} }
}, [suggestion, isLoading]); }, [suggestion, isLoading]);
@@ -73,6 +73,7 @@ const mentionRenderItems = () => {
// destroy component if space is greater 3 without a match // destroy component if space is greater 3 without a match
if ( if (
whitespaceCount > 3 && whitespaceCount > 3 &&
//@ts-ignore
props.editor.storage.mentionItems.length === 0 props.editor.storage.mentionItems.length === 0
) { ) {
popup?.[0]?.destroy(); popup?.[0]?.destroy();
@@ -73,6 +73,8 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
if (!editor) return; if (!editor) return;
const { results, resultIndex } = editor.storage.searchAndReplace; const { results, resultIndex } = editor.storage.searchAndReplace;
//TODO: check type error
//@ts-ignore
const position: Range = results[resultIndex]; const position: Range = results[resultIndex];
if (!position) return; if (!position) return;
@@ -159,6 +159,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run(); editor.chain().focus().deleteRange(range).run();
// @ts-ignore
const pageId = editor.storage?.pageId; const pageId = editor.storage?.pageId;
if (!pageId) return; if (!pageId) return;
@@ -186,6 +187,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run(); editor.chain().focus().deleteRange(range).run();
// @ts-ignore
const pageId = editor.storage?.pageId; const pageId = editor.storage?.pageId;
if (!pageId) return; if (!pageId) return;
@@ -211,6 +213,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run(); editor.chain().focus().deleteRange(range).run();
// @ts-ignore
const pageId = editor.storage?.pageId; const pageId = editor.storage?.pageId;
if (!pageId) return; if (!pageId) return;
@@ -1,4 +1,4 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react"; import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { import {
@@ -57,12 +57,13 @@ export const TableCellMenu = React.memo(
editor={editor} editor={editor}
pluginKey="table-cell-menu" pluginKey="table-cell-menu"
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
appendTo: () => { //appendTo: () => {
return appendTo?.current; // return appendTo?.current;
}, // },
offset: [0, 15], placement: "bottom",
zIndex: 99, offset: 15,
//zIndex: 99,
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
@@ -1,8 +1,5 @@
import { import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
BubbleMenu as BaseBubbleMenu, import { posToDOMRect, findParentNode } from "@tiptap/react";
posToDOMRect,
findParentNode,
} from "@tiptap/react";
import { Node as PMNode } from "@tiptap/pm/model"; import { Node as PMNode } from "@tiptap/pm/model";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
@@ -17,9 +14,11 @@ import {
IconColumnRemove, IconColumnRemove,
IconRowInsertBottom, IconRowInsertBottom,
IconRowInsertTop, IconRowInsertTop,
IconRowRemove, IconTableColumn, IconTableRow, IconRowRemove,
IconTableColumn,
IconTableRow,
IconTrashX, IconTrashX,
} from '@tabler/icons-react'; } from "@tabler/icons-react";
import { isCellSelection } from "@docmost/editor-ext"; import { isCellSelection } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -91,38 +90,15 @@ export const TableMenu = React.memo(
editor={editor} editor={editor}
pluginKey="table-menu" pluginKey="table-menu"
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
getReferenceClientRect: getReferenceClientRect, placement: "bottom",
offset: [0, 15], offset: 15,
zIndex: 99, //zIndex: 99,
popperOptions: {
modifiers: [
{
name: "preventOverflow",
enabled: true,
options: {
altAxis: true,
boundary: "clippingParents",
padding: 8,
},
},
{
name: "flip",
enabled: true,
options: {
boundary: editor.options.element,
fallbackPlacements: ["top", "bottom"],
padding: { top: 35, left: 8, right: 8, bottom: -Infinity },
},
},
],
},
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
<ActionIcon.Group> <ActionIcon.Group>
<Tooltip position="top" label={t("Add left column")} <Tooltip position="top" label={t("Add left column")}>
>
<ActionIcon <ActionIcon
onClick={addColumnLeft} onClick={addColumnLeft}
variant="default" variant="default"
@@ -188,8 +164,7 @@ export const TableMenu = React.memo(
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip position="top" label={t("Toggle header row")} <Tooltip position="top" label={t("Toggle header row")}>
>
<ActionIcon <ActionIcon
onClick={toggleHeaderRow} onClick={toggleHeaderRow}
variant="default" variant="default"
@@ -200,8 +175,7 @@ export const TableMenu = React.memo(
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip position="top" label={t("Toggle header column")} <Tooltip position="top" label={t("Toggle header column")}>
>
<ActionIcon <ActionIcon
onClick={toggleHeaderColumn} onClick={toggleHeaderColumn}
variant="default" variant="default"
@@ -1,10 +1,6 @@
import { import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
BubbleMenu as BaseBubbleMenu, import { findParentNode, posToDOMRect } from "@tiptap/react";
findParentNode,
posToDOMRect,
} from "@tiptap/react";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { sticky } from "tippy.js";
import { Node as PMNode } from "prosemirror-model"; import { Node as PMNode } from "prosemirror-model";
import { import {
EditorMenuProps, EditorMenuProps,
@@ -85,15 +81,12 @@ export function VideoMenu({ editor }: EditorMenuProps) {
editor={editor} editor={editor}
pluginKey={`video-menu}`} pluginKey={`video-menu}`}
updateDelay={0} updateDelay={0}
tippyOptions={{ options={{
getReferenceClientRect, //getReferenceClientRect,
offset: [0, 8], placement: "bottom",
zIndex: 99, offset: 8,
popperOptions: { //zIndex: 99,
modifiers: [{ name: "flip", enabled: false }], flip: false,
},
plugins: [sticky],
sticky: "popper",
}} }}
shouldShow={shouldShow} shouldShow={shouldShow}
> >
@@ -1,9 +1,13 @@
import { StarterKit } from "@tiptap/starter-kit"; import { StarterKit } from "@tiptap/starter-kit";
import { Placeholder } from "@tiptap/extension-placeholder";
import { TextAlign } from "@tiptap/extension-text-align"; import { TextAlign } from "@tiptap/extension-text-align";
import { TaskList } from "@tiptap/extension-task-list"; import {
import { TaskItem } from "@tiptap/extension-task-item"; TaskList,
import { Underline } from "@tiptap/extension-underline"; TaskItem,
} from "@tiptap/extension-list";
import {
Placeholder,
CharacterCount,
} from "@tiptap/extensions";
import { Superscript } from "@tiptap/extension-superscript"; import { Superscript } from "@tiptap/extension-superscript";
import SubScript from "@tiptap/extension-subscript"; import SubScript from "@tiptap/extension-subscript";
import { Highlight } from "@tiptap/extension-highlight"; import { Highlight } from "@tiptap/extension-highlight";
@@ -12,7 +16,7 @@ import { TextStyle } from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color"; import { Color } from "@tiptap/extension-color";
import SlashCommand from "@/features/editor/extensions/slash-command"; import SlashCommand from "@/features/editor/extensions/slash-command";
import { Collaboration } from "@tiptap/extension-collaboration"; import { Collaboration } from "@tiptap/extension-collaboration";
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor"; import { CollaborationCaret } from "@tiptap/extension-collaboration-caret";
import { HocuspocusProvider } from "@hocuspocus/provider"; import { HocuspocusProvider } from "@hocuspocus/provider";
import { import {
Comment, Comment,
@@ -73,7 +77,6 @@ import MentionView from "@/features/editor/components/mention/mention-view.tsx";
import i18n from "@/i18n.ts"; import i18n from "@/i18n.ts";
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts"; import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
import EmojiCommand from "./emoji-command"; import EmojiCommand from "./emoji-command";
import { CharacterCount } from "@tiptap/extension-character-count";
import { countWords } from "alfaaz"; import { countWords } from "alfaaz";
const lowlight = createLowlight(common); const lowlight = createLowlight(common);
@@ -90,7 +93,7 @@ lowlight.register("scala", scala);
export const mainExtensions = [ export const mainExtensions = [
StarterKit.configure({ StarterKit.configure({
history: false, undoRedo: false,
dropcursor: { dropcursor: {
width: 3, width: 3,
color: "#70CFF8", color: "#70CFF8",
@@ -101,6 +104,8 @@ export const mainExtensions = [
spellcheck: false, spellcheck: false,
}, },
}, },
link: false,
trailingNode: false,
}), }),
Placeholder.configure({ Placeholder.configure({
placeholder: ({ node }) => { placeholder: ({ node }) => {
@@ -122,7 +127,6 @@ export const mainExtensions = [
TaskItem.configure({ TaskItem.configure({
nested: true, nested: true,
}), }),
Underline,
LinkExtension.configure({ LinkExtension.configure({
openOnClick: false, openOnClick: false,
}), }),
@@ -221,17 +225,17 @@ export const mainExtensions = [
SearchAndReplace.extend({ SearchAndReplace.extend({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
'Mod-f': () => { "Mod-f": () => {
const event = new CustomEvent("openFindDialogFromEditor", {}); const event = new CustomEvent("openFindDialogFromEditor", {});
document.dispatchEvent(event); document.dispatchEvent(event);
return true; return true;
}, },
'Escape': () => { Escape: () => {
const event = new CustomEvent("closeFindDialogFromEditor", {}); const event = new CustomEvent("closeFindDialogFromEditor", {});
document.dispatchEvent(event); document.dispatchEvent(event);
return true; return true;
}, },
} };
}, },
}).configure(), }).configure(),
] as any; ] as any;
@@ -242,7 +246,7 @@ export const collabExtensions: CollabExtensions = (provider, user) => [
Collaboration.configure({ Collaboration.configure({
document: provider.document, document: provider.document,
}), }),
CollaborationCursor.configure({ CollaborationCaret.configure({
provider, provider,
user: { user: {
name: user.name, name: user.name,
+24 -15
View File
@@ -75,7 +75,7 @@ export default function PageEditor({
const [isLocalSynced, setLocalSynced] = useState(false); const [isLocalSynced, setLocalSynced] = useState(false);
const [isRemoteSynced, setRemoteSynced] = useState(false); const [isRemoteSynced, setRemoteSynced] = useState(false);
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom( const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
yjsConnectionStatusAtom yjsConnectionStatusAtom,
); );
const menuContainerRef = useRef(null); const menuContainerRef = useRef(null);
const documentName = `page.${pageId}`; const documentName = `page.${pageId}`;
@@ -100,6 +100,7 @@ export default function PageEditor({
// Track when collaborative provider is ready and synced // Track when collaborative provider is ready and synced
const [collabReady, setCollabReady] = useState(false); const [collabReady, setCollabReady] = useState(false);
/*
useEffect(() => { useEffect(() => {
if ( if (
remoteProvider?.status === WebSocketStatus.Connected && remoteProvider?.status === WebSocketStatus.Connected &&
@@ -109,6 +110,7 @@ export default function PageEditor({
setCollabReady(true); setCollabReady(true);
} }
}, [remoteProvider?.status, isLocalSynced, isRemoteSynced]); }, [remoteProvider?.status, isLocalSynced, isRemoteSynced]);
*/
useEffect(() => { useEffect(() => {
if (!providersRef.current) { if (!providersRef.current) {
@@ -119,8 +121,8 @@ export default function PageEditor({
url: collaborationURL, url: collaborationURL,
document: ydoc, document: ydoc,
token: collabQuery?.token, token: collabQuery?.token,
connect: true, //connect: true,
preserveConnection: false, //preserveConnection: false,
onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => { onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => {
const payload = jwtDecode(collabQuery?.token); const payload = jwtDecode(collabQuery?.token);
const now = Date.now().valueOf() / 1000; const now = Date.now().valueOf() / 1000;
@@ -137,11 +139,11 @@ export default function PageEditor({
}); });
} }
}, },
onStatus: (status) => { //onStatus: (status) => {
if (status.status === "connected") { // if (status.status === "connected") {
setYjsConnectionStatus(status.status); // setYjsConnectionStatus(status.status);
} // }
}, // },
}); });
remote.on("synced", () => setRemoteSynced(true)); remote.on("synced", () => setRemoteSynced(true));
remote.on("disconnect", () => { remote.on("disconnect", () => {
@@ -176,13 +178,14 @@ export default function PageEditor({
*/ */
// Only connect/disconnect on tab/idle, not destroy // Only connect/disconnect on tab/idle, not destroy
/*
useEffect(() => { useEffect(() => {
if (!providersReady || !providersRef.current) return; if (!providersReady || !providersRef.current) return;
const remoteProvider = providersRef.current.remote; const remoteProvider = providersRef.current.remote;
if ( if (
isIdle && isIdle &&
documentState === "hidden" && documentState === "hidden" &&
remoteProvider.status === WebSocketStatus.Connected remoteProvider === WebSocketStatus.Connected
) { ) {
remoteProvider.disconnect(); remoteProvider.disconnect();
setIsCollabReady(false); setIsCollabReady(false);
@@ -197,6 +200,7 @@ export default function PageEditor({
setTimeout(() => setIsCollabReady(true), 500); setTimeout(() => setIsCollabReady(true), 500);
} }
}, [isIdle, documentState, providersReady, resetIdle]); }, [isIdle, documentState, providersReady, resetIdle]);
*/
const extensions = useMemo(() => { const extensions = useMemo(() => {
if (!remoteProvider || !currentUser?.user) return mainExtensions; if (!remoteProvider || !currentUser?.user) return mainExtensions;
@@ -217,7 +221,7 @@ export default function PageEditor({
scrollMargin: 80, scrollMargin: 80,
handleDOMEvents: { handleDOMEvents: {
keydown: (_view, event) => { keydown: (_view, event) => {
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') { if ((event.ctrlKey || event.metaKey) && event.code === "KeyS") {
event.preventDefault(); event.preventDefault();
return true; return true;
} }
@@ -252,6 +256,7 @@ export default function PageEditor({
if (editor) { if (editor) {
// @ts-ignore // @ts-ignore
setEditor(editor); setEditor(editor);
// @ts-ignore
editor.storage.pageId = pageId; editor.storage.pageId = pageId;
} }
}, },
@@ -262,7 +267,7 @@ export default function PageEditor({
debouncedUpdateContent(editorJson); debouncedUpdateContent(editorJson);
}, },
}, },
[pageId, editable, remoteProvider] [pageId, editable, remoteProvider],
); );
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => { const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
@@ -300,7 +305,7 @@ export default function PageEditor({
return () => { return () => {
document.removeEventListener( document.removeEventListener(
"ACTIVE_COMMENT_EVENT", "ACTIVE_COMMENT_EVENT",
handleActiveCommentEvent handleActiveCommentEvent,
); );
}; };
}, []); }, []);
@@ -311,6 +316,7 @@ export default function PageEditor({
setAsideState({ tab: "", isAsideOpen: false }); setAsideState({ tab: "", isAsideOpen: false });
}, [pageId]); }, [pageId]);
/*
useEffect(() => { useEffect(() => {
if (remoteProvider?.status === WebSocketStatus.Connecting) { if (remoteProvider?.status === WebSocketStatus.Connecting) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@@ -319,9 +325,10 @@ export default function PageEditor({
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
} }
}, [remoteProvider?.status]); }, [remoteProvider?.status]);
*/
const isSynced = isLocalSynced && isRemoteSynced; const isSynced = isLocalSynced && isRemoteSynced;
/*
useEffect(() => { useEffect(() => {
const collabReadyTimeout = setTimeout(() => { const collabReadyTimeout = setTimeout(() => {
if ( if (
@@ -334,6 +341,7 @@ export default function PageEditor({
}, 500); }, 500);
return () => clearTimeout(collabReadyTimeout); return () => clearTimeout(collabReadyTimeout);
}, [isRemoteSynced, isLocalSynced, remoteProvider?.status]); }, [isRemoteSynced, isLocalSynced, remoteProvider?.status]);
*/
useEffect(() => { useEffect(() => {
// Only honor user default page edit mode preference and permissions // Only honor user default page edit mode preference and permissions
@@ -351,8 +359,9 @@ export default function PageEditor({
}, [userPageEditMode, editor, editable]); }, [userPageEditMode, editor, editable]);
const hasConnectedOnceRef = useRef(false); const hasConnectedOnceRef = useRef(false);
const [showStatic, setShowStatic] = useState(true); const [showStatic, setShowStatic] = useState(false);
/*
useEffect(() => { useEffect(() => {
if ( if (
!hasConnectedOnceRef.current && !hasConnectedOnceRef.current &&
@@ -361,7 +370,7 @@ export default function PageEditor({
hasConnectedOnceRef.current = true; hasConnectedOnceRef.current = true;
setShowStatic(false); setShowStatic(false);
} }
}, [remoteProvider?.status]); }, [remoteProvider?.status]);*/
if (showStatic) { if (showStatic) {
return ( return (
@@ -67,7 +67,7 @@ function HistoryList({ pageId }: Props) {
mainEditorTitle mainEditorTitle
.chain() .chain()
.clearContent() .clearContent()
.setContent(activeHistoryData.title, true) .setContent(activeHistoryData.title, { emitUpdate: true })
.run(); .run();
mainEditor mainEditor
.chain() .chain()
@@ -26,7 +26,7 @@ export class CollaborationGateway {
) { ) {
this.redisConfig = parseRedisUrl(this.environmentService.getRedisUrl()); this.redisConfig = parseRedisUrl(this.environmentService.getRedisUrl());
this.hocuspocus = HocuspocusServer.configure({ this.hocuspocus = new Hocuspocus({
debounce: 10000, debounce: 10000,
maxDebounce: 45000, maxDebounce: 45000,
unloadImmediately: false, unloadImmediately: false,
@@ -65,6 +65,6 @@ export class CollaborationGateway {
} }
async destroy(): Promise<void> { async destroy(): Promise<void> {
await this.hocuspocus.destroy(); //await this.hocuspocus.destroy();
} }
} }
@@ -1,8 +1,5 @@
import { StarterKit } from '@tiptap/starter-kit'; import { StarterKit } from '@tiptap/starter-kit';
import { TextAlign } from '@tiptap/extension-text-align'; import { TextAlign } from '@tiptap/extension-text-align';
import { TaskList } from '@tiptap/extension-task-list';
import { TaskItem } from '@tiptap/extension-task-item';
import { Underline } from '@tiptap/extension-underline';
import { Superscript } from '@tiptap/extension-superscript'; import { Superscript } from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript'; import SubScript from '@tiptap/extension-subscript';
import { Highlight } from '@tiptap/extension-highlight'; import { Highlight } from '@tiptap/extension-highlight';
@@ -10,6 +7,7 @@ import { Typography } from '@tiptap/extension-typography';
import { TextStyle } from '@tiptap/extension-text-style'; import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color'; import { Color } from '@tiptap/extension-color';
import { Youtube } from '@tiptap/extension-youtube'; import { Youtube } from '@tiptap/extension-youtube';
import { TaskList, TaskItem } from '@tiptap/extension-list';
import { import {
Callout, Callout,
Comment, Comment,
@@ -51,7 +49,6 @@ export const tiptapExtensions = [
TaskItem.configure({ TaskItem.configure({
nested: true, nested: true,
}), }),
Underline,
LinkExtension, LinkExtension,
Superscript, Superscript,
SubScript, SubScript,
@@ -69,7 +69,7 @@ export class AuthenticationExtension implements Extension {
} }
if (userSpaceRole === SpaceRole.READER) { if (userSpaceRole === SpaceRole.READER) {
data.connection.readOnly = true; data.connectionConfig.readOnly = true;
this.logger.debug(`User granted readonly access to page: ${pageId}`); this.logger.debug(`User granted readonly access to page: ${pageId}`);
} }
+35 -38
View File
@@ -20,53 +20,50 @@
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^7.1.0", "@braintree/sanitize-url": "^7.1.0",
"@docmost/editor-ext": "workspace:*", "@docmost/editor-ext": "workspace:*",
"@hocuspocus/extension-redis": "^2.15.2", "@floating-ui/dom": "^1.7.3",
"@hocuspocus/provider": "^2.15.2", "@hocuspocus/extension-redis": "^3.2.2",
"@hocuspocus/server": "^2.15.2", "@hocuspocus/provider": "^3.2.2",
"@hocuspocus/transformer": "^2.15.2", "@hocuspocus/server": "^3.2.2",
"@hocuspocus/transformer": "^3.2.2",
"@joplin/turndown": "^4.0.74", "@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56", "@joplin/turndown-plugin-gfm": "^1.0.56",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@tiptap/core": "^2.10.3", "@tiptap/core": "^3.0.9",
"@tiptap/extension-code-block": "^2.10.3", "@tiptap/extension-code-block": "^3.0.9",
"@tiptap/extension-code-block-lowlight": "^2.10.3", "@tiptap/extension-collaboration": "^3.0.9",
"@tiptap/extension-collaboration": "^2.10.3", "@tiptap/extension-collaboration-caret": "^3.0.9",
"@tiptap/extension-collaboration-cursor": "^2.10.3", "@tiptap/extension-color": "^3.0.9",
"@tiptap/extension-color": "^2.10.3", "@tiptap/extension-document": "^3.0.9",
"@tiptap/extension-document": "^2.10.3", "@tiptap/extension-heading": "^3.0.9",
"@tiptap/extension-heading": "^2.10.3", "@tiptap/extension-highlight": "^3.0.9",
"@tiptap/extension-highlight": "^2.10.3", "@tiptap/extension-history": "^3.0.9",
"@tiptap/extension-history": "^2.10.3", "@tiptap/extension-image": "^3.0.9",
"@tiptap/extension-image": "^2.10.3", "@tiptap/extension-link": "^3.0.9",
"@tiptap/extension-link": "^2.10.3", "@tiptap/extension-list": "^3.0.9",
"@tiptap/extension-list-item": "^2.10.3", "@tiptap/extension-list-item": "^3.0.9",
"@tiptap/extension-list-keymap": "^2.10.3", "@tiptap/extension-list-keymap": "^3.0.9",
"@tiptap/extension-placeholder": "^2.10.3", "@tiptap/extension-placeholder": "^3.0.9",
"@tiptap/extension-subscript": "^2.10.3", "@tiptap/extension-subscript": "^3.0.9",
"@tiptap/extension-superscript": "^2.10.3", "@tiptap/extension-superscript": "^3.0.9",
"@tiptap/extension-table": "^2.10.3", "@tiptap/extension-table": "^3.0.9",
"@tiptap/extension-table-cell": "^2.10.3", "@tiptap/extension-text": "^3.0.9",
"@tiptap/extension-table-header": "^2.10.3", "@tiptap/extension-text-align": "^3.0.9",
"@tiptap/extension-table-row": "^2.10.3", "@tiptap/extension-text-style": "^3.0.9",
"@tiptap/extension-task-item": "^2.10.3", "@tiptap/extension-typography": "^3.0.9",
"@tiptap/extension-task-list": "^2.10.3", "@tiptap/extension-youtube": "^3.0.9",
"@tiptap/extension-text": "^2.10.3", "@tiptap/html": "^3.0.9",
"@tiptap/extension-text-align": "^2.10.3", "@tiptap/pm": "^3.0.9",
"@tiptap/extension-text-style": "^2.10.3", "@tiptap/react": "^3.0.9",
"@tiptap/extension-typography": "^2.10.3", "@tiptap/starter-kit": "^3.0.9",
"@tiptap/extension-underline": "^2.10.3", "@tiptap/suggestion": "^3.0.9",
"@tiptap/extension-youtube": "^2.10.3",
"@tiptap/html": "^2.10.3",
"@tiptap/pm": "^2.10.3",
"@tiptap/react": "^2.10.3",
"@tiptap/starter-kit": "^2.10.3",
"@tiptap/suggestion": "^2.10.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"bytes": "^3.1.2", "bytes": "^3.1.2",
"core": "link:highlight.js/lib/core",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.2.6", "dompurify": "^3.2.6",
"fractional-indexing-jittered": "^1.0.0", "fractional-indexing-jittered": "^1.0.0",
"highlight.js": "^11.11.1",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"linkifyjs": "^4.3.2", "linkifyjs": "^4.3.2",
@@ -1,81 +0,0 @@
import CodeBlockLowlight, {
CodeBlockLowlightOptions,
} from "@tiptap/extension-code-block-lowlight";
import { ReactNodeViewRenderer } from "@tiptap/react";
export interface CustomCodeBlockOptions extends CodeBlockLowlightOptions {
view: any;
}
const TAB_CHAR = "\u00A0\u00A0";
export const CustomCodeBlock = CodeBlockLowlight.extend<CustomCodeBlockOptions>(
{
selectable: true,
addOptions() {
return {
...this.parent?.(),
view: null,
};
},
addKeyboardShortcuts() {
return {
...this.parent?.(),
Tab: () => {
if (this.editor.isActive("codeBlock")) {
this.editor
.chain()
.command(({ tr }) => {
tr.insertText(TAB_CHAR);
return true;
})
.run();
return true;
}
},
"Mod-a": () => {
if (this.editor.isActive("codeBlock")) {
const { state } = this.editor;
const { $from } = state.selection;
let codeBlockNode = null;
let codeBlockPos = null;
let depth = 0;
for (depth = $from.depth; depth > 0; depth--) {
const node = $from.node(depth);
if (node.type.name === "codeBlock") {
codeBlockNode = node;
codeBlockPos = $from.start(depth) - 1;
break;
}
}
if (codeBlockNode && codeBlockPos !== null) {
const codeBlockStart = codeBlockPos;
const codeBlockEnd = codeBlockPos + codeBlockNode.nodeSize;
const contentStart = codeBlockStart + 1;
const contentEnd = codeBlockEnd - 1;
this.editor.commands.setTextSelection({
from: contentStart,
to: contentEnd,
});
return true;
}
}
return false;
},
};
},
addNodeView() {
return ReactNodeViewRenderer(this.options.view);
},
}
);
@@ -0,0 +1,106 @@
import type { CodeBlockOptions } from '@tiptap/extension-code-block'
import CodeBlock from '@tiptap/extension-code-block'
import { LowlightPlugin } from './lowlight-plugin.js'
import { ReactNodeViewRenderer } from '@tiptap/react';
export interface CodeBlockLowlightOptions extends CodeBlockOptions {
/**
* The lowlight instance.
*/
lowlight: any,
view: any;
}
const TAB_CHAR = "\u00A0\u00A0";
/**
* This extension allows you to highlight code blocks with lowlight.
* @see https://tiptap.dev/api/nodes/code-block-lowlight
*/
export const CustomCodeBlock = CodeBlock.extend<CodeBlockLowlightOptions>({
selectable: true,
addOptions() {
return {
...this.parent?.(),
lowlight: {},
languageClassPrefix: 'language-',
exitOnTripleEnter: true,
exitOnArrowDown: true,
defaultLanguage: null,
HTMLAttributes: {},
view: null,
}
},
addKeyboardShortcuts() {
return {
...this.parent?.(),
Tab: () => {
if (this.editor.isActive("codeBlock")) {
this.editor
.chain()
.command(({ tr }) => {
tr.insertText(TAB_CHAR);
return true;
})
.run();
return true;
}
},
"Mod-a": () => {
if (this.editor.isActive("codeBlock")) {
const { state } = this.editor;
const { $from } = state.selection;
let codeBlockNode = null;
let codeBlockPos = null;
let depth = 0;
for (depth = $from.depth; depth > 0; depth--) {
const node = $from.node(depth);
if (node.type.name === "codeBlock") {
codeBlockNode = node;
codeBlockPos = $from.start(depth) - 1;
break;
}
}
if (codeBlockNode && codeBlockPos !== null) {
const codeBlockStart = codeBlockPos;
const codeBlockEnd = codeBlockPos + codeBlockNode.nodeSize;
const contentStart = codeBlockStart + 1;
const contentEnd = codeBlockEnd - 1;
this.editor.commands.setTextSelection({
from: contentStart,
to: contentEnd,
});
return true;
}
}
return false;
},
};
},
addNodeView() {
return ReactNodeViewRenderer(this.options.view);
},
addProseMirrorPlugins() {
return [
...(this.parent?.() || []),
LowlightPlugin({
name: this.name,
lowlight: this.options.lowlight,
defaultLanguage: this.options.defaultLanguage,
}),
]
},
})
@@ -0,0 +1 @@
export { CustomCodeBlock } from "./custom-code-block";
@@ -0,0 +1,159 @@
import { findChildren } from '@tiptap/core'
import type { Node as ProsemirrorNode } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
// @ts-ignore
import highlight from 'highlight.js/lib/core'
function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] {
return nodes
.map(node => {
const classes = [...className, ...(node.properties ? node.properties.className : [])]
if (node.children) {
return parseNodes(node.children, classes)
}
return {
text: node.value,
classes,
}
})
.flat()
}
function getHighlightNodes(result: any) {
// `.value` for lowlight v1, `.children` for lowlight v2
return result.value || result.children || []
}
function registered(aliasOrLanguage: string) {
return Boolean(highlight.getLanguage(aliasOrLanguage))
}
function getDecorations({
doc,
name,
lowlight,
defaultLanguage,
}: {
doc: ProsemirrorNode
name: string
lowlight: any
defaultLanguage: string | null | undefined
}) {
const decorations: Decoration[] = []
findChildren(doc, node => node.type.name === name).forEach(block => {
let from = block.pos + 1
const language = block.node.attrs.language || defaultLanguage
const languages = lowlight.listLanguages()
const nodes =
language && (languages.includes(language) || registered(language) || lowlight.registered?.(language))
? getHighlightNodes(lowlight.highlight(language, block.node.textContent))
: getHighlightNodes(lowlight.highlightAuto(block.node.textContent))
parseNodes(nodes).forEach(node => {
const to = from + node.text.length
if (node.classes.length) {
const decoration = Decoration.inline(from, to, {
class: node.classes.join(' '),
})
decorations.push(decoration)
}
from = to
})
})
return DecorationSet.create(doc, decorations)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function isFunction(param: any): param is Function {
return typeof param === 'function'
}
export function LowlightPlugin({
name,
lowlight,
defaultLanguage,
}: {
name: string
lowlight: any
defaultLanguage: string | null | undefined
}) {
if (!['highlight', 'highlightAuto', 'listLanguages'].every(api => isFunction(lowlight[api]))) {
throw Error('You should provide an instance of lowlight to use the code-block-lowlight extension')
}
const lowlightPlugin: Plugin<any> = new Plugin({
key: new PluginKey('lowlight'),
state: {
init: (_, { doc }) =>
getDecorations({
doc,
name,
lowlight,
defaultLanguage,
}),
apply: (transaction, decorationSet, oldState, newState) => {
const oldNodeName = oldState.selection.$head.parent.type.name
const newNodeName = newState.selection.$head.parent.type.name
const oldNodes = findChildren(oldState.doc, node => node.type.name === name)
const newNodes = findChildren(newState.doc, node => node.type.name === name)
if (
transaction.docChanged &&
// Apply decorations if:
// selection includes named node,
([oldNodeName, newNodeName].includes(name) ||
// OR transaction adds/removes named node,
newNodes.length !== oldNodes.length ||
// OR transaction has changes that completely encapsulte a node
// (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example.
transaction.steps.some(step => {
// @ts-ignore
return (
// @ts-ignore
step.from !== undefined &&
// @ts-ignore
step.to !== undefined &&
oldNodes.some(node => {
// @ts-ignore
return (
// @ts-ignore
node.pos >= step.from &&
// @ts-ignore
node.pos + node.node.nodeSize <= step.to
)
})
)
}))
) {
return getDecorations({
doc: transaction.doc,
name,
lowlight,
defaultLanguage,
})
}
return decorationSet.map(transaction.mapping, transaction.doc)
},
},
props: {
decorations(state) {
return lowlightPlugin.getState(state)
},
},
})
return lowlightPlugin
}
@@ -27,6 +27,7 @@ export const Details = Node.create<DetailsOptions>({
content: "detailsSummary detailsContent", content: "detailsSummary detailsContent",
defining: true, defining: true,
isolating: true, isolating: true,
// @ts-ignore
allowGapCursor: false, allowGapCursor: false,
addOptions() { addOptions() {
return { return {
@@ -31,6 +31,9 @@ import {
import { Node as PMNode, Mark } from "@tiptap/pm/model"; import { Node as PMNode, Mark } from "@tiptap/pm/model";
declare module "@tiptap/core" { declare module "@tiptap/core" {
interface Storage {
searchAndReplace: SearchAndReplaceStorage;
}
interface Commands<ReturnType> { interface Commands<ReturnType> {
search: { search: {
/** /**
@@ -189,7 +192,7 @@ const replace = (
const marksSet = new Set<Mark>(); const marksSet = new Set<Mark>();
state.doc.nodesBetween(from, to, (node) => { state.doc.nodesBetween(from, to, (node) => {
if (node.isText && node.marks) { if (node.isText && node.marks) {
node.marks.forEach(mark => marksSet.add(mark)); node.marks.forEach((mark) => marksSet.add(mark));
} }
}); });
@@ -220,7 +223,7 @@ const replaceAll = (
const marksSet = new Set<Mark>(); const marksSet = new Set<Mark>();
tr.doc.nodesBetween(from, to, (node) => { tr.doc.nodesBetween(from, to, (node) => {
if (node.isText && node.marks) { if (node.isText && node.marks) {
node.marks.forEach(mark => marksSet.add(mark)); node.marks.forEach((mark) => marksSet.add(mark));
} }
}); });
@@ -352,10 +355,17 @@ export const SearchAndReplace = Extension.create<
// The results will be recalculated by the plugin, but we need to ensure // The results will be recalculated by the plugin, but we need to ensure
// the index doesn't exceed the new bounds // the index doesn't exceed the new bounds
setTimeout(() => { setTimeout(() => {
const newResultsLength = editor.storage.searchAndReplace.results.length; const newResultsLength =
if (newResultsLength > 0 && editor.storage.searchAndReplace.resultIndex >= newResultsLength) { editor.storage.searchAndReplace.results.length;
if (
newResultsLength > 0 &&
editor.storage.searchAndReplace.resultIndex >= newResultsLength
) {
// Keep the same position if possible, otherwise go to the last result // Keep the same position if possible, otherwise go to the last result
editor.storage.searchAndReplace.resultIndex = Math.min(resultIndex, newResultsLength - 1); editor.storage.searchAndReplace.resultIndex = Math.min(
resultIndex,
newResultsLength - 1,
);
} }
}, 0); }, 0);
+8 -5
View File
@@ -1,8 +1,9 @@
import { TableCell as TiptapTableCell } from "@tiptap/extension-table-cell"; import { TableCell as TiptapTableCell } from "@tiptap/extension-table";
export const TableCell = TiptapTableCell.extend({ export const TableCell = TiptapTableCell.extend({
name: "tableCell", name: "tableCell",
content: "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+", content:
"(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
addAttributes() { addAttributes() {
return { return {
@@ -16,19 +17,21 @@ export const TableCell = TiptapTableCell.extend({
} }
return { return {
style: `background-color: ${attributes.backgroundColor}`, style: `background-color: ${attributes.backgroundColor}`,
'data-background-color': attributes.backgroundColor, "data-background-color": attributes.backgroundColor,
}; };
}, },
}, },
backgroundColorName: { backgroundColorName: {
default: null, default: null,
parseHTML: (element) => element.getAttribute('data-background-color-name') || null, parseHTML: (element) =>
element.getAttribute("data-background-color-name") || null,
renderHTML: (attributes) => { renderHTML: (attributes) => {
if (!attributes.backgroundColorName) { if (!attributes.backgroundColorName) {
return {}; return {};
} }
return { return {
'data-background-color-name': attributes.backgroundColorName.toLowerCase(), "data-background-color-name":
attributes.backgroundColorName.toLowerCase(),
}; };
}, },
}, },
+1 -1
View File
@@ -1,4 +1,4 @@
import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table-header"; import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table";
export const TableHeader = TiptapTableHeader.extend({ export const TableHeader = TiptapTableHeader.extend({
name: "tableHeader", name: "tableHeader",
+1 -1
View File
@@ -1,4 +1,4 @@
import TiptapTableRow from "@tiptap/extension-table-row"; import { TableRow as TiptapTableRow } from "@tiptap/extension-table";
export const TableRow = TiptapTableRow.extend({ export const TableRow = TiptapTableRow.extend({
allowGapCursor: false, allowGapCursor: false,
+9 -5
View File
@@ -1,4 +1,4 @@
import Table from "@tiptap/extension-table"; import { Table } from "@tiptap/extension-table";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
const LIST_TYPES = ["bulletList", "orderedList", "taskList"]; const LIST_TYPES = ["bulletList", "orderedList", "taskList"];
@@ -17,13 +17,17 @@ function isInList(editor: Editor): boolean {
} }
function handleListIndent(editor: Editor): boolean { function handleListIndent(editor: Editor): boolean {
return editor.commands.sinkListItem("listItem") || return (
editor.commands.sinkListItem("taskItem"); editor.commands.sinkListItem("listItem") ||
editor.commands.sinkListItem("taskItem")
);
} }
function handleListOutdent(editor: Editor): boolean { function handleListOutdent(editor: Editor): boolean {
return editor.commands.liftListItem("listItem") || return (
editor.commands.liftListItem("taskItem"); editor.commands.liftListItem("listItem") ||
editor.commands.liftListItem("taskItem")
);
} }
export const CustomTable = Table.extend({ export const CustomTable = Table.extend({
+3 -3
View File
@@ -1,10 +1,10 @@
// @ts-nocheck
import { Editor, findParentNode, isTextSelection } from "@tiptap/core"; import { Editor, findParentNode, isTextSelection } from "@tiptap/core";
import { Selection, Transaction } from "@tiptap/pm/state"; import { EditorState, Selection, Transaction } from '@tiptap/pm/state';
import { CellSelection, TableMap } from "@tiptap/pm/tables"; import { CellSelection, TableMap } from "@tiptap/pm/tables";
import { Node, ResolvedPos } from "@tiptap/pm/model"; import { Node, ResolvedPos } from "@tiptap/pm/model";
import Table from "@tiptap/extension-table"; import { Table } from "@tiptap/extension-table";
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"; import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url";
import { EditorView } from '@tiptap/pm/view';
export const isRectSelected = (rect: any) => (selection: CellSelection) => { export const isRectSelected = (rect: any) => (selection: CellSelection) => {
const map = TableMap.get(selection.$anchorCell.node(-1)); const map = TableMap.get(selection.$anchorCell.node(-1));
+475 -527
View File
File diff suppressed because it is too large Load Diff