diff --git a/packages/editor/dist/extensions/attachment/attachment.d.ts b/packages/editor/dist/extensions/attachment/attachment.d.ts index 0671bb5d4..be965cb5e 100644 --- a/packages/editor/dist/extensions/attachment/attachment.d.ts +++ b/packages/editor/dist/extensions/attachment/attachment.d.ts @@ -1,6 +1,7 @@ import { Node } from "@tiptap/core"; export declare type AttachmentType = "image" | "file"; export interface AttachmentOptions { + HTMLAttributes: Record; onDownloadAttachment: (attachment: Attachment) => boolean; onOpenAttachmentPicker: (type: AttachmentType) => boolean; } diff --git a/packages/editor/dist/extensions/attachment/attachment.js b/packages/editor/dist/extensions/attachment/attachment.js index f5f142e2c..52b7e9878 100644 --- a/packages/editor/dist/extensions/attachment/attachment.js +++ b/packages/editor/dist/extensions/attachment/attachment.js @@ -9,8 +9,8 @@ var __values = (this && this.__values) || function(o) { }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; -import { Node, mergeAttributes } from "@tiptap/core"; -import { findChildren, ReactNodeViewRenderer } from "../react"; +import { Node, mergeAttributes, findChildren } from "@tiptap/core"; +import { createNodeView } from "../react"; import { AttachmentComponent } from "./component"; export var AttachmentNode = Node.create({ name: "attachment", @@ -20,7 +20,7 @@ export var AttachmentNode = Node.create({ atom: true, addOptions: function () { return { - // HTMLAttributes: {}, + HTMLAttributes: {}, onDownloadAttachment: function () { return false; }, onOpenAttachmentPicker: function () { return false; }, }; @@ -46,10 +46,13 @@ export var AttachmentNode = Node.create({ }, renderHTML: function (_a) { var HTMLAttributes = _a.HTMLAttributes; - return ["span", mergeAttributes(HTMLAttributes)]; + return [ + "span", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + ]; }, addNodeView: function () { - return ReactNodeViewRenderer(AttachmentComponent); + return createNodeView(AttachmentComponent); }, addCommands: function () { var _this = this; diff --git a/packages/editor/dist/extensions/attachment/component.d.ts b/packages/editor/dist/extensions/attachment/component.d.ts index 14a25bf99..7fee9f4e1 100644 --- a/packages/editor/dist/extensions/attachment/component.d.ts +++ b/packages/editor/dist/extensions/attachment/component.d.ts @@ -1,3 +1,3 @@ -import { ImageProps } from "rebass"; -import { NodeViewProps } from "../react"; -export declare function AttachmentComponent(props: ImageProps & NodeViewProps): JSX.Element; +import { Attachment } from "./attachment"; +import { ReactNodeViewProps } from "../react"; +export declare function AttachmentComponent(props: ReactNodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/attachment/component.js b/packages/editor/dist/extensions/attachment/component.js index fb568a4c1..d2e408281 100644 --- a/packages/editor/dist/extensions/attachment/component.js +++ b/packages/editor/dist/extensions/attachment/component.js @@ -9,10 +9,8 @@ var __assign = (this && this.__assign) || function () { }; return __assign.apply(this, arguments); }; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Box, Flex, Text } from "rebass"; -import { NodeViewWrapper } from "../react"; -import { ThemeProvider } from "emotion-theming"; import { ToolButton } from "../../toolbar/components/tool-button"; import { useRef } from "react"; import { MenuPresenter } from "../../components/menu/menu"; @@ -20,52 +18,51 @@ import { Icon } from "../../toolbar/components/icon"; import { Icons } from "../../toolbar/icons"; export function AttachmentComponent(props) { var _a = props.node.attrs, hash = _a.hash, filename = _a.filename, size = _a.size; - var editor = props.editor, updateAttributes = props.updateAttributes; + var editor = props.editor; var elementRef = useRef(); var isActive = editor.isActive("attachment", { hash: hash }); // const [isToolbarVisible, setIsToolbarVisible] = useState(); - var theme = editor.storage.theme; // useEffect(() => { // setIsToolbarVisible(isActive); // }, [isActive]); - return (_jsx(NodeViewWrapper, __assign({ as: "span" }, { children: _jsxs(ThemeProvider, __assign({ theme: theme }, { children: [_jsxs(Box, __assign({ ref: elementRef, as: "span", contentEditable: false, variant: "body", sx: { - display: "inline-flex", - overflow: "hidden", - position: "relative", - zIndex: 1, - userSelect: "none", - alignItems: "center", - backgroundColor: "bgSecondary", - px: 1, - borderRadius: "default", - border: "1px solid var(--border)", - cursor: "pointer", - maxWidth: 250, - borderColor: isActive ? "primary" : "border", - ":hover": { - bg: "hover", - }, - }, title: filename }, { children: [_jsx(Icon, { path: Icons.attachment, size: 14 }), _jsx(Text, __assign({ as: "span", sx: { - ml: "small", - fontSize: "0.85rem", - whiteSpace: "nowrap", - textOverflow: "ellipsis", - overflow: "hidden", - }, className: "filename" }, { children: filename })), _jsx(Text, __assign({ as: "span", className: "size", sx: { - ml: 1, - fontSize: "subBody", - color: "fontTertiary", - flexShrink: 0, - } }, { children: formatBytes(size) }))] })), _jsx(MenuPresenter, __assign({ isOpen: isActive, onClose: function () { }, items: [], options: { - type: "autocomplete", - position: { - target: elementRef.current || undefined, - location: "top", - yOffset: -5, - isTargetAbsolute: true, - align: "end", - }, - } }, { children: _jsx(AttachmentToolbar, { editor: editor }) }))] })) }))); + return (_jsxs(_Fragment, { children: [_jsxs(Box, __assign({ ref: elementRef, as: "span", contentEditable: false, variant: "body", sx: { + display: "inline-flex", + overflow: "hidden", + position: "relative", + zIndex: 1, + userSelect: "none", + alignItems: "center", + backgroundColor: "bgSecondary", + px: 1, + borderRadius: "default", + border: "1px solid var(--border)", + cursor: "pointer", + maxWidth: 250, + borderColor: isActive ? "primary" : "border", + ":hover": { + bg: "hover", + }, + }, title: filename }, { children: [_jsx(Icon, { path: Icons.attachment, size: 14 }), _jsx(Text, __assign({ as: "span", sx: { + ml: "small", + fontSize: "0.85rem", + whiteSpace: "nowrap", + textOverflow: "ellipsis", + overflow: "hidden", + }, className: "filename" }, { children: filename })), _jsx(Text, __assign({ as: "span", className: "size", sx: { + ml: 1, + fontSize: "subBody", + color: "fontTertiary", + flexShrink: 0, + } }, { children: formatBytes(size) }))] })), _jsx(MenuPresenter, __assign({ isOpen: isActive, onClose: function () { }, items: [], options: { + type: "autocomplete", + position: { + target: elementRef.current || undefined, + location: "top", + yOffset: -5, + isTargetAbsolute: true, + align: "end", + }, + } }, { children: _jsx(AttachmentToolbar, { editor: editor }) }))] })); } function formatBytes(bytes, decimals) { if (decimals === void 0) { decimals = 1; } @@ -77,6 +74,7 @@ function formatBytes(bytes, decimals) { var i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]; } +// TODO make this functional function AttachmentToolbar(props) { var editor = props.editor; return (_jsx(Flex, __assign({ sx: { diff --git a/packages/editor/dist/extensions/codeblock/codeblock.js b/packages/editor/dist/extensions/codeblock/codeblock.js index fd275803a..93a4bd2d2 100644 --- a/packages/editor/dist/extensions/codeblock/codeblock.js +++ b/packages/editor/dist/extensions/codeblock/codeblock.js @@ -34,12 +34,12 @@ var __values = (this && this.__values) || function(o) { }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; +import { findParentNodeClosestToPos } from "@tiptap/core"; import { Node, textblockTypeInputRule, mergeAttributes } from "@tiptap/core"; import { Plugin, PluginKey, TextSelection, } from "prosemirror-state"; -import { findParentNodeClosestToPos } from "../react"; import { CodeblockComponent } from "./component"; import { HighlighterPlugin } from "./highlighter"; -import ReactNodeView from "../react/ReactNodeView"; +import { createNodeView } from "../react"; import detectIndent from "detect-indent"; import redent from "redent"; import stripIndent from "strip-indent"; @@ -443,7 +443,7 @@ export var CodeBlock = Node.create({ ]; }, addNodeView: function () { - return ReactNodeView.fromComponent(CodeblockComponent, { + return createNodeView(CodeblockComponent, { contentDOMFactory: function () { var content = document.createElement("div"); content.classList.add("node-content-wrapper"); diff --git a/packages/editor/dist/extensions/codeblock/component.d.ts b/packages/editor/dist/extensions/codeblock/component.d.ts index 8b6ae36e8..54e13f8bb 100644 --- a/packages/editor/dist/extensions/codeblock/component.d.ts +++ b/packages/editor/dist/extensions/codeblock/component.d.ts @@ -1,4 +1,4 @@ import "prism-themes/themes/prism-dracula.min.css"; import { CodeBlockAttributes } from "./code-block"; -import { ReactComponentProps } from "../react/types"; -export declare function CodeblockComponent(props: ReactComponentProps): JSX.Element; +import { ReactNodeViewProps } from "../react/types"; +export declare function CodeblockComponent(props: ReactNodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/codeblock/component.js b/packages/editor/dist/extensions/codeblock/component.js index bfcedb979..8bc024130 100644 --- a/packages/editor/dist/extensions/codeblock/component.js +++ b/packages/editor/dist/extensions/codeblock/component.js @@ -61,12 +61,11 @@ var __read = (this && this.__read) || function (o, n) { } return ar; }; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useEffect, useRef, useState } from "react"; import { isLanguageLoaded, loadLanguage } from "./loader"; import { refractor } from "refractor/lib/core"; import "prism-themes/themes/prism-dracula.min.css"; -import { ThemeProvider } from "emotion-theming"; import { Button, Flex, Text } from "rebass"; import Languages from "./languages.json"; import { PopupPresenter } from "../../components/menu/menu"; @@ -76,7 +75,6 @@ import { Icons } from "../../toolbar/icons"; export function CodeblockComponent(props) { var editor = props.editor, updateAttributes = props.updateAttributes, node = props.node, forwardRef = props.forwardRef; var _a = node === null || node === void 0 ? void 0 : node.attrs, language = _a.language, indentLength = _a.indentLength, indentType = _a.indentType, caretPosition = _a.caretPosition; - var theme = editor === null || editor === void 0 ? void 0 : editor.storage.theme; var _b = __read(useState(false), 2), isOpen = _b[0], setIsOpen = _b[1]; // const [caretPosition, setCaretPosition] = useState(); var toolbarRef = useRef(null); @@ -105,7 +103,7 @@ export function CodeblockComponent(props) { }); })(); }, [language]); - return (_jsxs(ThemeProvider, __assign({ theme: theme }, { children: [_jsxs(Flex, __assign({ sx: { + return (_jsxs(_Fragment, { children: [_jsxs(Flex, __assign({ sx: { flexDirection: "column", borderRadius: "default", overflow: "hidden", @@ -154,7 +152,7 @@ export function CodeblockComponent(props) { location: "top", yOffset: 5, }, - } }, { children: _jsx(LanguageSelector, { selectedLanguage: (languageDefinition === null || languageDefinition === void 0 ? void 0 : languageDefinition.filename) || "Plaintext", onLanguageSelected: function (language) { return updateAttributes({ language: language }); } }) }))] }))); + } }, { children: _jsx(LanguageSelector, { selectedLanguage: (languageDefinition === null || languageDefinition === void 0 ? void 0 : languageDefinition.filename) || "Plaintext", onLanguageSelected: function (language) { return updateAttributes({ language: language }); } }) }))] })); } function LanguageSelector(props) { var onLanguageSelected = props.onLanguageSelected, selectedLanguage = props.selectedLanguage; diff --git a/packages/editor/dist/extensions/embed/component.d.ts b/packages/editor/dist/extensions/embed/component.d.ts index 2725cc799..70c58dc23 100644 --- a/packages/editor/dist/extensions/embed/component.d.ts +++ b/packages/editor/dist/extensions/embed/component.d.ts @@ -1,2 +1,3 @@ -import { NodeViewProps } from "../react"; -export declare function EmbedComponent(props: NodeViewProps): JSX.Element; +import { EmbedAlignmentOptions, EmbedAttributes } from "./embed"; +import { ReactNodeViewProps } from "../react"; +export declare function EmbedComponent(props: ReactNodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/embed/component.js b/packages/editor/dist/extensions/embed/component.js index 31284d4c8..0f370c7d0 100644 --- a/packages/editor/dist/extensions/embed/component.js +++ b/packages/editor/dist/extensions/embed/component.js @@ -25,10 +25,8 @@ var __read = (this && this.__read) || function (o, n) { } return ar; }; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Box, Flex } from "rebass"; -import { NodeViewWrapper } from "../react"; -import { ThemeProvider } from "emotion-theming"; import { Resizable } from "re-resizable"; import { ToolButton } from "../../toolbar/components/tool-button"; import { useEffect, useRef, useState } from "react"; @@ -44,40 +42,36 @@ export function EmbedComponent(props) { useEffect(function () { setIsToolbarVisible(isActive); }, [isActive]); - return (_jsx(NodeViewWrapper, { children: _jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsx(Box, __assign({ sx: { - display: "flex", - justifyContent: align === "center" - ? "center" - : align === "left" - ? "start" - : "end", - } }, { children: _jsxs(Resizable, __assign({ size: { - height: height || "auto", - width: width || "auto", - }, maxWidth: "100%", onResizeStop: function (e, direction, ref, d) { - updateAttributes({ - width: ref.clientWidth, - height: ref.clientHeight, - }); - }, lockAspectRatio: true }, { children: [_jsx(Flex, __assign({ width: "100%", sx: { - position: "relative", - justifyContent: "end", - borderTop: "20px solid var(--bgSecondary)", - // borderLeft: "20px solid var(--bgSecondary)", - borderTopLeftRadius: "default", - borderTopRightRadius: "default", - borderColor: isActive ? "border" : "bgSecondary", - cursor: "pointer", - ":hover": { - borderColor: "border", - }, - } }, { children: isToolbarVisible && (_jsx(EmbedToolbar, { editor: editor, align: align, height: height || 0, width: width || 0, src: src })) })), _jsx(Box, __assign({ as: "iframe", ref: embedRef, src: src, width: "100%", height: "100%", sx: { - border: "none", - // border: isActive - // ? "2px solid var(--primary)" - // : "2px solid transparent", - // borderRadius: "default", - } }, props))] })) })) })) })); + return (_jsx(_Fragment, { children: _jsx(Box, __assign({ sx: { + display: "flex", + justifyContent: align === "center" ? "center" : align === "left" ? "start" : "end", + } }, { children: _jsxs(Resizable, __assign({ size: { + height: height || "auto", + width: width || "auto", + }, maxWidth: "100%", onResizeStop: function (e, direction, ref, d) { + updateAttributes({ + width: ref.clientWidth, + height: ref.clientHeight, + }); + }, lockAspectRatio: true }, { children: [_jsx(Flex, __assign({ width: "100%", sx: { + position: "relative", + justifyContent: "end", + borderTop: "20px solid var(--bgSecondary)", + // borderLeft: "20px solid var(--bgSecondary)", + borderTopLeftRadius: "default", + borderTopRightRadius: "default", + borderColor: isActive ? "border" : "bgSecondary", + cursor: "pointer", + ":hover": { + borderColor: "border", + }, + } }, { children: isToolbarVisible && (_jsx(EmbedToolbar, { editor: editor, align: align, height: height || 0, width: width || 0, src: src })) })), _jsx(Box, __assign({ as: "iframe", ref: embedRef, src: src, width: "100%", height: "100%", sx: { + border: "none", + // border: isActive + // ? "2px solid var(--primary)" + // : "2px solid transparent", + // borderRadius: "default", + } }, props))] })) })) })); } function EmbedToolbar(props) { var editor = props.editor, height = props.height, width = props.width, src = props.src; diff --git a/packages/editor/dist/extensions/embed/embed.js b/packages/editor/dist/extensions/embed/embed.js index c9470c0d6..3dcea2686 100644 --- a/packages/editor/dist/extensions/embed/embed.js +++ b/packages/editor/dist/extensions/embed/embed.js @@ -10,7 +10,7 @@ var __assign = (this && this.__assign) || function () { return __assign.apply(this, arguments); }; import { Node, mergeAttributes } from "@tiptap/core"; -import { ReactNodeViewRenderer } from "../react"; +import { createNodeView } from "../react"; import { EmbedComponent } from "./component"; export var EmbedNode = Node.create({ name: "embed", @@ -50,7 +50,7 @@ export var EmbedNode = Node.create({ ]; }, addNodeView: function () { - return ReactNodeViewRenderer(EmbedComponent); + return createNodeView(EmbedComponent); }, addCommands: function () { var _this = this; diff --git a/packages/editor/dist/extensions/image/component.d.ts b/packages/editor/dist/extensions/image/component.d.ts index 7f5f71e2e..af8c4ee3e 100644 --- a/packages/editor/dist/extensions/image/component.d.ts +++ b/packages/editor/dist/extensions/image/component.d.ts @@ -1,3 +1,3 @@ -import { ImageProps } from "rebass"; -import { NodeViewProps } from "../react"; -export declare function ImageComponent(props: ImageProps & NodeViewProps): JSX.Element; +import { ImageAlignmentOptions, ImageAttributes } from "./image"; +import { ReactNodeViewProps } from "../react"; +export declare function ImageComponent(props: ReactNodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/image/component.js b/packages/editor/dist/extensions/image/component.js index d12324292..88eb598db 100644 --- a/packages/editor/dist/extensions/image/component.js +++ b/packages/editor/dist/extensions/image/component.js @@ -25,10 +25,8 @@ var __read = (this && this.__read) || function (o, n) { } return ar; }; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Box, Flex, Image } from "rebass"; -import { NodeViewWrapper } from "../react"; -import { ThemeProvider } from "emotion-theming"; import { Resizable } from "re-resizable"; import { ToolButton } from "../../toolbar/components/tool-button"; import { useEffect, useRef, useState } from "react"; @@ -36,8 +34,7 @@ import { PopupPresenter } from "../../components/menu/menu"; import { Popup } from "../../toolbar/components/popup"; import { ImageProperties } from "../../toolbar/popups/image-properties"; export function ImageComponent(props) { - var _a = props.node - .attrs, src = _a.src, alt = _a.alt, title = _a.title, width = _a.width, height = _a.height, align = _a.align, float = _a.float; + var _a = props.node.attrs, src = _a.src, alt = _a.alt, title = _a.title, width = _a.width, height = _a.height, align = _a.align, float = _a.float; var editor = props.editor, updateAttributes = props.updateAttributes; var imageRef = useRef(); var isActive = editor.isActive("image", { src: src }); @@ -46,31 +43,31 @@ export function ImageComponent(props) { useEffect(function () { setIsToolbarVisible(isActive); }, [isActive]); - return (_jsx(NodeViewWrapper, { children: _jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsx(Box, __assign({ sx: { - display: float ? "block" : "flex", - justifyContent: float - ? "stretch" - : align === "center" - ? "center" - : align === "left" - ? "start" - : "end", - } }, { children: _jsxs(Resizable, __assign({ style: { - float: float ? (align === "left" ? "left" : "right") : "none", - }, size: { - height: height || "auto", - width: width || "auto", - }, maxWidth: "100%", onResizeStop: function (e, direction, ref, d) { - updateAttributes({ - width: ref.clientWidth, - height: ref.clientHeight, - }); - }, lockAspectRatio: true }, { children: [_jsx(Flex, __assign({ sx: { position: "relative", justifyContent: "end" } }, { children: isToolbarVisible && (_jsx(ImageToolbar, { editor: editor, float: float, align: align, height: height || 0, width: width || 0 })) })), _jsx(Image, __assign({ ref: imageRef, src: src, alt: alt, title: title, width: "100%", height: "100%", sx: { - border: isActive - ? "2px solid var(--primary)" - : "2px solid transparent", - borderRadius: "default", - } }, props))] })) })) })) })); + return (_jsx(_Fragment, { children: _jsx(Box, __assign({ sx: { + display: float ? "block" : "flex", + justifyContent: float + ? "stretch" + : align === "center" + ? "center" + : align === "left" + ? "start" + : "end", + } }, { children: _jsxs(Resizable, __assign({ style: { + float: float ? (align === "left" ? "left" : "right") : "none", + }, size: { + height: height || "auto", + width: width || "auto", + }, maxWidth: "100%", onResizeStop: function (e, direction, ref, d) { + updateAttributes({ + width: ref.clientWidth, + height: ref.clientHeight, + }); + }, lockAspectRatio: true }, { children: [_jsx(Flex, __assign({ sx: { position: "relative", justifyContent: "end" } }, { children: isToolbarVisible && (_jsx(ImageToolbar, { editor: editor, float: float, align: align, height: height || 0, width: width || 0 })) })), _jsx(Image, __assign({ ref: imageRef, src: src, alt: alt, title: title, width: "100%", height: "100%", sx: { + border: isActive + ? "2px solid var(--primary)" + : "2px solid transparent", + borderRadius: "default", + } }, props))] })) })) })); } function ImageToolbar(props) { var editor = props.editor, float = props.float, height = props.height, width = props.width; diff --git a/packages/editor/dist/extensions/image/image.js b/packages/editor/dist/extensions/image/image.js index a128e9a15..99181b0a8 100644 --- a/packages/editor/dist/extensions/image/image.js +++ b/packages/editor/dist/extensions/image/image.js @@ -26,7 +26,7 @@ var __read = (this && this.__read) || function (o, n) { return ar; }; import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core"; -import { ReactNodeViewRenderer } from "../react"; +import { createNodeView } from "../react"; import { ImageComponent } from "./component"; export var inputRegex = /(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/; export var ImageNode = Node.create({ @@ -81,7 +81,7 @@ export var ImageNode = Node.create({ ]; }, addNodeView: function () { - return ReactNodeViewRenderer(ImageComponent); + return createNodeView(ImageComponent); }, addCommands: function () { var _this = this; diff --git a/packages/editor/dist/extensions/react/ReactNodeView.d.ts b/packages/editor/dist/extensions/react/ReactNodeView.d.ts index bc677c9a6..da2e74f45 100644 --- a/packages/editor/dist/extensions/react/ReactNodeView.d.ts +++ b/packages/editor/dist/extensions/react/ReactNodeView.d.ts @@ -5,7 +5,7 @@ import { PortalProviderAPI } from "./ReactNodeViewPortals"; import { EventDispatcher } from "./event-dispatcher"; import { ReactComponentProps, ReactNodeViewOptions, GetPos, ForwardRef, ContentDOM } from "./types"; import { Editor, NodeViewRendererProps } from "@tiptap/core"; -export default class ReactNodeView

implements NodeView { +export declare class ReactNodeView

implements NodeView { protected readonly editor: Editor; protected readonly getPos: GetPos; protected readonly portalProviderAPI: PortalProviderAPI; diff --git a/packages/editor/dist/extensions/react/ReactNodeView.js b/packages/editor/dist/extensions/react/ReactNodeView.js index 630c3c6a3..782641179 100644 --- a/packages/editor/dist/extensions/react/ReactNodeView.js +++ b/packages/editor/dist/extensions/react/ReactNodeView.js @@ -226,7 +226,7 @@ var ReactNodeView = /** @class */ (function () { }; return ReactNodeView; }()); -export default ReactNodeView; +export { ReactNodeView }; function isiOS() { return ([ "iPad Simulator", diff --git a/packages/editor/dist/extensions/react/SelectionBasedReactNodeView.d.ts b/packages/editor/dist/extensions/react/SelectionBasedReactNodeView.d.ts index b842c5371..47515c369 100644 --- a/packages/editor/dist/extensions/react/SelectionBasedReactNodeView.d.ts +++ b/packages/editor/dist/extensions/react/SelectionBasedReactNodeView.d.ts @@ -50,5 +50,5 @@ export declare class SelectionBasedNodeView

extends Rea viewShouldUpdate(_nextNode: PMNode): boolean; destroy(): void; private onSelectionChange; - static fromComponent(component: React.ComponentType, options?: Omit, "component">): ({ node, getPos, editor }: NodeViewRendererProps) => SelectionBasedNodeView; + static fromComponent(component: React.ComponentType, options?: Omit, "component">): ({ node, getPos, editor }: NodeViewRendererProps) => any; } diff --git a/packages/editor/dist/extensions/react/index.d.ts b/packages/editor/dist/extensions/react/index.d.ts index 5c1bb8dd6..db315768f 100644 --- a/packages/editor/dist/extensions/react/index.d.ts +++ b/packages/editor/dist/extensions/react/index.d.ts @@ -1,8 +1,6 @@ -export * from "@tiptap/core"; -export { Editor } from "./Editor"; -export * from "./use-editor"; -export * from "./ReactRenderer"; -export * from "./ReactNodeViewRenderer"; -export * from "./EditorContent"; -export * from "./NodeViewWrapper"; -export * from "./NodeViewContent"; +export * from "./react-node-view"; +export * from "./types"; +export * from "./react-portal-provider"; +export * from "./selection-based-react-node-view"; +export * from "./plugin"; +export * from "./event-dispatcher"; diff --git a/packages/editor/dist/extensions/react/index.js b/packages/editor/dist/extensions/react/index.js index 5c1bb8dd6..db315768f 100644 --- a/packages/editor/dist/extensions/react/index.js +++ b/packages/editor/dist/extensions/react/index.js @@ -1,8 +1,6 @@ -export * from "@tiptap/core"; -export { Editor } from "./Editor"; -export * from "./use-editor"; -export * from "./ReactRenderer"; -export * from "./ReactNodeViewRenderer"; -export * from "./EditorContent"; -export * from "./NodeViewWrapper"; -export * from "./NodeViewContent"; +export * from "./react-node-view"; +export * from "./types"; +export * from "./react-portal-provider"; +export * from "./selection-based-react-node-view"; +export * from "./plugin"; +export * from "./event-dispatcher"; diff --git a/packages/editor/dist/extensions/react/reactnodeview.d.ts b/packages/editor/dist/extensions/react/reactnodeview.d.ts new file mode 100644 index 000000000..b4ca5a585 --- /dev/null +++ b/packages/editor/dist/extensions/react/reactnodeview.d.ts @@ -0,0 +1,50 @@ +import React from "react"; +import { NodeView, Decoration, DecorationSource } from "prosemirror-view"; +import { Node as PMNode } from "prosemirror-model"; +import { PortalProviderAPI } from "./react-portal-provider"; +import { EventDispatcher } from "./event-dispatcher"; +import { ReactNodeViewProps, ReactNodeViewOptions, GetPosNode, ForwardRef, ContentDOM } from "./types"; +import { Editor, NodeViewRendererProps } from "@tiptap/core"; +export declare class ReactNodeView

implements NodeView { + protected readonly editor: Editor; + protected readonly getPos: GetPosNode; + protected readonly portalProviderAPI: PortalProviderAPI; + protected readonly eventDispatcher: EventDispatcher; + protected readonly options: ReactNodeViewOptions

; + private domRef; + private contentDOMWrapper?; + contentDOM: HTMLElement | undefined; + node: PMNode; + constructor(node: PMNode, editor: Editor, getPos: GetPosNode, portalProviderAPI: PortalProviderAPI, eventDispatcher: EventDispatcher, options: ReactNodeViewOptions

); + /** + * This method exists to move initialization logic out of the constructor, + * so object can be initialized properly before calling render first time. + * + * Example: + * Instance properties get added to an object only after super call in + * constructor, which leads to some methods being undefined during the + * first render. + */ + init(): this; + private renderReactComponent; + createDomRef(): HTMLElement; + getContentDOM(): ContentDOM; + handleRef: (node: HTMLElement | null) => void; + private _handleRef; + render(props?: P, forwardRef?: ForwardRef): React.ReactElement | null; + updateAttributes(attributes: any, pos: number): void; + update(node: PMNode, _decorations: readonly Decoration[], _innerDecorations: DecorationSource): boolean; + ignoreMutation(mutation: MutationRecord | { + type: "selection"; + target: Element; + }): boolean; + viewShouldUpdate(nextNode: PMNode): boolean; + /** + * Copies the attributes from a ProseMirror Node to a DOM node. + * @param node The Prosemirror Node from which to source the attributes + */ + setDomAttrs(node: PMNode, element: HTMLElement): void; + get dom(): HTMLElement; + destroy(): void; +} +export declare function createNodeView(component: React.ComponentType, options?: Omit, "component">): ({ node, getPos, editor }: NodeViewRendererProps) => ReactNodeView; diff --git a/packages/editor/dist/extensions/react/reactnodeview.js b/packages/editor/dist/extensions/react/reactnodeview.js new file mode 100644 index 000000000..a32e5c447 --- /dev/null +++ b/packages/editor/dist/extensions/react/reactnodeview.js @@ -0,0 +1,240 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { ThemeProvider } from "emotion-theming"; +var ReactNodeView = /** @class */ (function () { + function ReactNodeView(node, editor, getPos, portalProviderAPI, eventDispatcher, options) { + var _this = this; + this.editor = editor; + this.getPos = getPos; + this.portalProviderAPI = portalProviderAPI; + this.eventDispatcher = eventDispatcher; + this.options = options; + this.handleRef = function (node) { return _this._handleRef(node); }; + this.node = node; + } + /** + * This method exists to move initialization logic out of the constructor, + * so object can be initialized properly before calling render first time. + * + * Example: + * Instance properties get added to an object only after super call in + * constructor, which leads to some methods being undefined during the + * first render. + */ + ReactNodeView.prototype.init = function () { + var _this = this; + this.domRef = this.createDomRef(); + // this.setDomAttrs(this.node, this.domRef); + var _a = this.getContentDOM() || { + dom: undefined, + contentDOM: undefined, + }, contentDOMWrapper = _a.dom, contentDOM = _a.contentDOM; + if (this.domRef && contentDOMWrapper) { + this.domRef.appendChild(contentDOMWrapper); + this.contentDOM = contentDOM ? contentDOM : contentDOMWrapper; + this.contentDOMWrapper = contentDOMWrapper || contentDOM; + } + // @see ED-3790 + // something gets messed up during mutation processing inside of a + // nodeView if DOM structure has nested plain "div"s, it doesn't see the + // difference between them and it kills the nodeView + this.domRef.classList.add("".concat(this.node.type.name, "-view-content-wrap")); + this.renderReactComponent(function () { + return _this.render(_this.options.props, _this.handleRef); + }); + return this; + }; + ReactNodeView.prototype.renderReactComponent = function (component) { + if (!this.domRef || !component) { + return; + } + this.portalProviderAPI.render(component, this.domRef); + }; + ReactNodeView.prototype.createDomRef = function () { + if (this.options.wrapperFactory) + return this.options.wrapperFactory(); + if (!this.node.isInline) { + return document.createElement("div"); + } + var htmlElement = document.createElement("span"); + return htmlElement; + }; + ReactNodeView.prototype.getContentDOM = function () { + var _a, _b; + if (!this.options.contentDOMFactory) + return; + if (this.options.contentDOMFactory === true) { + var content = document.createElement("div"); + content.classList.add("".concat(this.node.type.name.toLowerCase(), "-content-wrapper")); + content.style.whiteSpace = "inherit"; + // caret is not visible if content element width is 0px + content.style.minWidth = "20px"; + return { dom: content }; + } + return (_b = (_a = this.options).contentDOMFactory) === null || _b === void 0 ? void 0 : _b.call(_a); + }; + ReactNodeView.prototype._handleRef = function (node) { + var contentDOM = this.contentDOMWrapper || this.contentDOM; + // move the contentDOM node inside the inner reference after rendering + if (node && contentDOM && !node.contains(contentDOM)) { + node.appendChild(contentDOM); + } + }; + ReactNodeView.prototype.render = function (props, forwardRef) { + var _this = this; + if (props === void 0) { props = {}; } + if (!this.options.component) + return null; + var theme = this.editor.storage.theme; + var pos = this.getPos(); + return (_jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsx(this.options.component, __assign({}, props, { editor: this.editor, getPos: this.getPos, node: this.node, forwardRef: forwardRef, updateAttributes: function (attr) { return _this.updateAttributes(attr, pos); } })) }))); + }; + ReactNodeView.prototype.updateAttributes = function (attributes, pos) { + var _this = this; + this.editor.commands.command(function (_a) { + var tr = _a.tr; + tr.setNodeMarkup(pos, undefined, __assign(__assign({}, _this.node.attrs), attributes)); + return true; + }); + }; + ReactNodeView.prototype.update = function (node, _decorations, _innerDecorations + // _innerDecorations?: Array, + // validUpdate: (currentNode: PMNode, newNode: PMNode) => boolean = () => true + ) { + var _this = this; + // @see https://github.com/ProseMirror/prosemirror/issues/648 + var isValidUpdate = this.node.type === node.type; // && validUpdate(this.node, node); + if (!isValidUpdate) { + return false; + } + // if (this.domRef && !this.node.sameMarkup(node)) { + // this.setDomAttrs(node, this.domRef); + // } + // View should not process a re-render if this is false. + // We dont want to destroy the view, so we return true. + if (!this.viewShouldUpdate(node)) { + this.node = node; + return true; + } + this.node = node; + this.renderReactComponent(function () { + return _this.render(_this.options.props, _this.handleRef); + }); + return true; + }; + ReactNodeView.prototype.ignoreMutation = function (mutation) { + if (!this.dom || !this.contentDOM) { + return true; + } + // TODO if (typeof this.options.ignoreMutation === 'function') { + // return this.options.ignoreMutation({ mutation }) + // } + // a leaf/atom node is like a black box for ProseMirror + // and should be fully handled by the node view + if (this.node.isLeaf || this.node.isAtom) { + return true; + } + // ProseMirror should handle any selections + if (mutation.type === "selection") { + return false; + } + // try to prevent a bug on mobiles that will break node views on enter + // this is because ProseMirror can’t preventDispatch on enter + // this will lead to a re-render of the node view on enter + // see: https://github.com/ueberdosis/tiptap/issues/1214 + if (this.dom.contains(mutation.target) && + mutation.type === "childList" && + this.editor.isFocused) { + var changedNodes = __spreadArray(__spreadArray([], __read(Array.from(mutation.addedNodes)), false), __read(Array.from(mutation.removedNodes)), false); + // we’ll check if every changed node is contentEditable + // to make sure it’s probably mutated by ProseMirror + if (changedNodes.every(function (node) { return node.isContentEditable; })) { + return false; + } + } + // we will allow mutation contentDOM with attributes + // so we can for example adding classes within our node view + if (this.contentDOM === mutation.target && mutation.type === "attributes") { + return true; + } + // ProseMirror should handle any changes within contentDOM + if (this.contentDOM.contains(mutation.target)) { + return false; + } + return true; + }; + ReactNodeView.prototype.viewShouldUpdate = function (nextNode) { + if (this.options.shouldUpdate) + return this.options.shouldUpdate(this.node, nextNode); + return true; + }; + /** + * Copies the attributes from a ProseMirror Node to a DOM node. + * @param node The Prosemirror Node from which to source the attributes + */ + ReactNodeView.prototype.setDomAttrs = function (node, element) { + Object.keys(node.attrs || {}).forEach(function (attr) { + element.setAttribute(attr, node.attrs[attr]); + }); + }; + Object.defineProperty(ReactNodeView.prototype, "dom", { + get: function () { + return this.domRef; + }, + enumerable: false, + configurable: true + }); + ReactNodeView.prototype.destroy = function () { + if (!this.domRef) { + return; + } + this.portalProviderAPI.remove(this.domRef); + // @ts-ignore NEW PM API + this.domRef = undefined; + this.contentDOM = undefined; + }; + return ReactNodeView; +}()); +export { ReactNodeView }; +export function createNodeView(component, options) { + return function (_a) { + var node = _a.node, getPos = _a.getPos, editor = _a.editor; + var _getPos = function () { return (typeof getPos === "boolean" ? -1 : getPos()); }; + return new ReactNodeView(node, editor, _getPos, editor.storage.portalProviderAPI, editor.storage.eventDispatcher, __assign(__assign({}, options), { component: component })).init(); + }; +} diff --git a/packages/editor/dist/extensions/react/reactportalprovider.d.ts b/packages/editor/dist/extensions/react/reactportalprovider.d.ts new file mode 100644 index 000000000..b3df1ee34 --- /dev/null +++ b/packages/editor/dist/extensions/react/reactportalprovider.d.ts @@ -0,0 +1,38 @@ +import React from "react"; +import { EventDispatcher } from "./event-dispatcher"; +export declare type BasePortalProviderProps = { + render: (portalProviderAPI: PortalProviderAPI) => React.ReactChild | JSX.Element | null; +}; +export declare type Portals = Map; +export declare type PortalRendererState = { + portals: Portals; +}; +declare type MountedPortal = { + children: () => React.ReactChild | null; +}; +export declare class PortalProviderAPI extends EventDispatcher { + portals: Map; + context: any; + constructor(); + setContext: (context: any) => void; + render(children: () => React.ReactChild | JSX.Element | null, container: HTMLElement): void; + forceUpdate(): void; + remove(container: HTMLElement): void; +} +export declare class PortalProvider extends React.Component { + static displayName: string; + portalProviderAPI: PortalProviderAPI; + constructor(props: BasePortalProviderProps); + render(): React.ReactChild | JSX.Element | null; + componentDidUpdate(): void; +} +export declare class PortalRenderer extends React.Component<{ + portalProviderAPI: PortalProviderAPI; +}, PortalRendererState> { + constructor(props: { + portalProviderAPI: PortalProviderAPI; + }); + handleUpdate: (portals: Portals) => void; + render(): JSX.Element; +} +export {}; diff --git a/packages/editor/dist/extensions/react/reactportalprovider.js b/packages/editor/dist/extensions/react/reactportalprovider.js new file mode 100644 index 000000000..57b324c7a --- /dev/null +++ b/packages/editor/dist/extensions/react/reactportalprovider.js @@ -0,0 +1,111 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime"; +import React from "react"; +import { createPortal, unstable_renderSubtreeIntoContainer, unmountComponentAtNode, } from "react-dom"; +import { EventDispatcher } from "./event-dispatcher"; +var PortalProviderAPI = /** @class */ (function (_super) { + __extends(PortalProviderAPI, _super); + function PortalProviderAPI() { + var _this = _super.call(this) || this; + _this.portals = new Map(); + _this.setContext = function (context) { + _this.context = context; + }; + return _this; + } + PortalProviderAPI.prototype.render = function (children, container) { + this.portals.set(container, { + children: children, + }); + var wrappedChildren = children(); + unstable_renderSubtreeIntoContainer(this.context, wrappedChildren, container); + }; + // TODO: until https://product-fabric.atlassian.net/browse/ED-5013 + // we (unfortunately) need to re-render to pass down any updated context. + // selectively do this for nodeviews that opt-in via `hasAnalyticsContext` + PortalProviderAPI.prototype.forceUpdate = function () { }; + PortalProviderAPI.prototype.remove = function (container) { + this.portals.delete(container); + // There is a race condition that can happen caused by Prosemirror vs React, + // where Prosemirror removes the container from the DOM before React gets + // around to removing the child from the container + // This will throw a NotFoundError: The node to be removed is not a child of this node + // Both Prosemirror and React remove the elements asynchronously, and in edge + // cases Prosemirror beats React + try { + unmountComponentAtNode(container); + } + catch (error) { + // IGNORE console.error(error); + } + }; + return PortalProviderAPI; +}(EventDispatcher)); +export { PortalProviderAPI }; +var PortalProvider = /** @class */ (function (_super) { + __extends(PortalProvider, _super); + function PortalProvider(props) { + var _this = _super.call(this, props) || this; + _this.portalProviderAPI = new PortalProviderAPI(); + return _this; + } + PortalProvider.prototype.render = function () { + return this.props.render(this.portalProviderAPI); + }; + PortalProvider.prototype.componentDidUpdate = function () { + this.portalProviderAPI.forceUpdate(); + }; + PortalProvider.displayName = "PortalProvider"; + return PortalProvider; +}(React.Component)); +export { PortalProvider }; +var PortalRenderer = /** @class */ (function (_super) { + __extends(PortalRenderer, _super); + function PortalRenderer(props) { + var _this = _super.call(this, props) || this; + _this.handleUpdate = function (portals) { return _this.setState({ portals: portals }); }; + props.portalProviderAPI.setContext(_this); + props.portalProviderAPI.on("update", _this.handleUpdate); + _this.state = { portals: new Map() }; + return _this; + } + PortalRenderer.prototype.render = function () { + var portals = this.state.portals; + return (_jsx(_Fragment, { children: Array.from(portals.entries()).map(function (_a) { + var _b = __read(_a, 2), container = _b[0], children = _b[1]; + return createPortal(children, container); + }) })); + }; + return PortalRenderer; +}(React.Component)); +export { PortalRenderer }; diff --git a/packages/editor/dist/extensions/react/selectionbasedreactnodeview.d.ts b/packages/editor/dist/extensions/react/selectionbasedreactnodeview.d.ts new file mode 100644 index 000000000..0a84a0b06 --- /dev/null +++ b/packages/editor/dist/extensions/react/selectionbasedreactnodeview.d.ts @@ -0,0 +1,55 @@ +import React from "react"; +import { Node as PMNode } from "prosemirror-model"; +import { PortalProviderAPI } from "./react-portal-provider"; +import { EventDispatcher } from "./event-dispatcher"; +import { ReactNodeViewOptions, GetPosNode, SelectionBasedReactNodeViewProps, ForwardRef } from "./types"; +import { ReactNodeView } from "./react-node-view"; +import { Editor, NodeViewRendererProps } from "@tiptap/core"; +/** + * A ReactNodeView that handles React components sensitive + * to selection changes. + * + * If the selection changes, it will attempt to re-render the + * React component. Otherwise it does nothing. + * + * You can subclass `viewShouldUpdate` to include other + * props that your component might want to consider before + * entering the React lifecycle. These are usually props you + * compare in `shouldComponentUpdate`. + * + * An example: + * + * ``` + * viewShouldUpdate(nextNode) { + * if (nextNode.attrs !== this.node.attrs) { + * return true; + * } + * + * return super.viewShouldUpdate(nextNode); + * }``` + */ +export declare class SelectionBasedNodeView

extends ReactNodeView

{ + private oldSelection; + private selectionChangeState; + pos: number; + posEnd: number | undefined; + constructor(node: PMNode, editor: Editor, getPos: GetPosNode, portalProviderAPI: PortalProviderAPI, eventDispatcher: EventDispatcher, options: ReactNodeViewOptions

); + render(props?: P, forwardRef?: ForwardRef): React.ReactElement | null; + /** + * Update current node's start and end positions. + * + * Prefer `this.pos` rather than getPos(), because calling getPos is + * expensive, unless you know you're definitely going to render. + */ + private updatePos; + private getPositionsWithDefault; + isNodeInsideSelection: (from: number, to: number, pos?: number, posEnd?: number) => boolean; + isSelectionInsideNode: (from: number, to: number, pos?: number, posEnd?: number) => boolean; + private isSelectedNode; + insideSelection: () => boolean; + nodeInsideSelection: () => boolean; + viewShouldUpdate(_nextNode: PMNode): boolean; + destroy(): void; + private onSelectionChange; +} +export declare function createSelectionBasedNodeView(component: React.ComponentType, options?: Omit, "component">): ({ node, getPos, editor }: NodeViewRendererProps) => SelectionBasedNodeView; diff --git a/packages/editor/dist/extensions/react/selectionbasedreactnodeview.js b/packages/editor/dist/extensions/react/selectionbasedreactnodeview.js new file mode 100644 index 000000000..209d14fd0 --- /dev/null +++ b/packages/editor/dist/extensions/react/selectionbasedreactnodeview.js @@ -0,0 +1,177 @@ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { DecorationSet } from "prosemirror-view"; +import { NodeSelection } from "prosemirror-state"; +import { stateKey as SelectionChangePluginKey, } from "./plugin"; +import { ReactNodeView } from "./react-node-view"; +import { ThemeProvider } from "emotion-theming"; +/** + * A ReactNodeView that handles React components sensitive + * to selection changes. + * + * If the selection changes, it will attempt to re-render the + * React component. Otherwise it does nothing. + * + * You can subclass `viewShouldUpdate` to include other + * props that your component might want to consider before + * entering the React lifecycle. These are usually props you + * compare in `shouldComponentUpdate`. + * + * An example: + * + * ``` + * viewShouldUpdate(nextNode) { + * if (nextNode.attrs !== this.node.attrs) { + * return true; + * } + * + * return super.viewShouldUpdate(nextNode); + * }``` + */ +var SelectionBasedNodeView = /** @class */ (function (_super) { + __extends(SelectionBasedNodeView, _super); + function SelectionBasedNodeView(node, editor, getPos, portalProviderAPI, eventDispatcher, options) { + var _this = _super.call(this, node, editor, getPos, portalProviderAPI, eventDispatcher, options) || this; + _this.pos = -1; + _this.isNodeInsideSelection = function (from, to, pos, posEnd) { + var _a; + (_a = _this.getPositionsWithDefault(pos, posEnd), pos = _a.pos, posEnd = _a.posEnd); + if (typeof pos !== "number" || typeof posEnd !== "number") { + return false; + } + return from <= pos && to >= posEnd; + }; + _this.isSelectionInsideNode = function (from, to, pos, posEnd) { + var _a; + (_a = _this.getPositionsWithDefault(pos, posEnd), pos = _a.pos, posEnd = _a.posEnd); + if (typeof pos !== "number" || typeof posEnd !== "number") { + return false; + } + return pos < from && to < posEnd; + }; + _this.isSelectedNode = function (selection) { + if (selection instanceof NodeSelection) { + var _a = _this.editor.view.state.selection, from = _a.from, to = _a.to; + return (selection.node === _this.node || + // If nodes are not the same object, we check if they are referring to the same document node + (_this.pos === from && + _this.posEnd === to && + selection.node.eq(_this.node))); + } + return false; + }; + _this.insideSelection = function () { + var _a = _this.editor.view.state.selection, from = _a.from, to = _a.to; + return (_this.isSelectedNode(_this.editor.view.state.selection) || + _this.isSelectionInsideNode(from, to)); + }; + _this.nodeInsideSelection = function () { + var selection = _this.editor.view.state.selection; + var from = selection.from, to = selection.to; + return (_this.isSelectedNode(selection) || _this.isNodeInsideSelection(from, to)); + }; + _this.onSelectionChange = function () { + _this.update(_this.node, [], DecorationSet.empty); + }; + _this.updatePos(); + _this.oldSelection = editor.view.state.selection; + _this.selectionChangeState = SelectionChangePluginKey.getState(_this.editor.view.state); + _this.selectionChangeState.subscribe(_this.onSelectionChange); + return _this; + } + SelectionBasedNodeView.prototype.render = function (props, forwardRef) { + var _this = this; + if (props === void 0) { props = {}; } + if (!this.options.component) + return null; + var theme = this.editor.storage.theme; + var isSelected = this.insideSelection() || this.nodeInsideSelection(); + return (_jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsx(this.options.component, __assign({}, props, { editor: this.editor, getPos: this.getPos, node: this.node, forwardRef: forwardRef, selected: isSelected, updateAttributes: function (attr) { return _this.updateAttributes(attr, _this.pos); } })) }))); + }; + /** + * Update current node's start and end positions. + * + * Prefer `this.pos` rather than getPos(), because calling getPos is + * expensive, unless you know you're definitely going to render. + */ + SelectionBasedNodeView.prototype.updatePos = function () { + if (typeof this.getPos === "boolean") { + return; + } + this.pos = this.getPos(); + this.posEnd = this.pos + this.node.nodeSize; + }; + SelectionBasedNodeView.prototype.getPositionsWithDefault = function (pos, posEnd) { + return { + pos: typeof pos !== "number" ? this.pos : pos, + posEnd: typeof posEnd !== "number" ? this.posEnd : posEnd, + }; + }; + SelectionBasedNodeView.prototype.viewShouldUpdate = function (_nextNode) { + var selection = this.editor.view.state.selection; + // update selection + var oldSelection = this.oldSelection; + this.oldSelection = selection; + // update cached positions + var _a = this, oldPos = _a.pos, oldPosEnd = _a.posEnd; + this.updatePos(); + var from = selection.from, to = selection.to; + var oldFrom = oldSelection.from, oldTo = oldSelection.to; + if (this.node.type.spec.selectable) { + var newNodeSelection = selection instanceof NodeSelection && selection.from === this.pos; + var oldNodeSelection = oldSelection instanceof NodeSelection && oldSelection.from === this.pos; + if ((newNodeSelection && !oldNodeSelection) || + (oldNodeSelection && !newNodeSelection)) { + return true; + } + } + var movedInToSelection = this.isNodeInsideSelection(from, to) && + !this.isNodeInsideSelection(oldFrom, oldTo); + var movedOutOfSelection = !this.isNodeInsideSelection(from, to) && + this.isNodeInsideSelection(oldFrom, oldTo); + var moveOutFromOldSelection = this.isNodeInsideSelection(from, to, oldPos, oldPosEnd) && + !this.isNodeInsideSelection(from, to); + if (movedInToSelection || movedOutOfSelection || moveOutFromOldSelection) { + return true; + } + return false; + }; + SelectionBasedNodeView.prototype.destroy = function () { + this.selectionChangeState.unsubscribe(this.onSelectionChange); + _super.prototype.destroy.call(this); + }; + return SelectionBasedNodeView; +}(ReactNodeView)); +export { SelectionBasedNodeView }; +export function createSelectionBasedNodeView(component, options) { + return function (_a) { + var node = _a.node, getPos = _a.getPos, editor = _a.editor; + var _getPos = function () { return (typeof getPos === "boolean" ? -1 : getPos()); }; + return new SelectionBasedNodeView(node, editor, _getPos, editor.storage.portalProviderAPI, editor.storage.eventDispatcher, __assign(__assign({}, options), { component: component })).init(); + }; +} diff --git a/packages/editor/dist/extensions/react/types.d.ts b/packages/editor/dist/extensions/react/types.d.ts index 8a5f3b480..58d9a0b1b 100644 --- a/packages/editor/dist/extensions/react/types.d.ts +++ b/packages/editor/dist/extensions/react/types.d.ts @@ -4,6 +4,9 @@ import { Node as PMNode, Attrs } from "prosemirror-model"; export interface ReactNodeProps { selected: boolean; } +export declare type NodeWithAttrs = PMNode & { + attrs: T; +}; export declare type GetPos = GetPosNode | boolean; export declare type GetPosNode = () => number; export declare type ForwardRef = (node: HTMLElement | null) => void; @@ -13,19 +16,20 @@ export declare type ContentDOM = { dom: HTMLElement; contentDOM?: HTMLElement | null | undefined; } | undefined; -export declare type ReactComponentProps = { - getPos: GetPos; - node: PMNode & { - attrs: TAttributes; - }; +export declare type ReactNodeViewProps = { + getPos: GetPosNode; + node: NodeWithAttrs; editor: Editor; updateAttributes: UpdateAttributes; forwardRef?: ForwardRef; }; +export declare type SelectionBasedReactNodeViewProps = ReactNodeViewProps & { + selected: boolean; +}; export declare type ReactNodeViewOptions

= { props?: P; - component?: React.ComponentType

; + component?: React.ComponentType

; shouldUpdate?: ShouldUpdate; - contentDOMFactory?: () => ContentDOM; + contentDOMFactory?: (() => ContentDOM) | boolean; wrapperFactory?: () => HTMLElement; }; diff --git a/packages/editor/dist/extensions/react/useEditor.d.ts b/packages/editor/dist/extensions/react/useEditor.d.ts index 937ee877c..f325cd188 100644 --- a/packages/editor/dist/extensions/react/useEditor.d.ts +++ b/packages/editor/dist/extensions/react/useEditor.d.ts @@ -1,4 +1,3 @@ import { DependencyList } from 'react'; import { EditorOptions } from '@tiptap/core'; -import { Editor } from './Editor'; -export declare const useEditor: (options?: Partial, deps?: DependencyList) => Editor | null; +export declare const useEditor: (options?: Partial, deps?: DependencyList) => any; diff --git a/packages/editor/dist/extensions/taskitem/component.d.ts b/packages/editor/dist/extensions/taskitem/component.d.ts index 1714b484e..0a2a6f514 100644 --- a/packages/editor/dist/extensions/taskitem/component.d.ts +++ b/packages/editor/dist/extensions/taskitem/component.d.ts @@ -1,3 +1,3 @@ -import { ImageProps } from "rebass"; -import { NodeViewProps } from "../react"; -export declare function TaskItemComponent(props: ImageProps & NodeViewProps): JSX.Element; +import { ReactNodeViewProps } from "../react"; +import { TaskItemAttributes } from "./task-item"; +export declare function TaskItemComponent(props: ReactNodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/taskitem/component.js b/packages/editor/dist/extensions/taskitem/component.js index 91d8c7bcc..8ddb6047e 100644 --- a/packages/editor/dist/extensions/taskitem/component.js +++ b/packages/editor/dist/extensions/taskitem/component.js @@ -9,22 +9,6 @@ var __assign = (this && this.__assign) || function () { }; return __assign.apply(this, arguments); }; -var __read = (this && this.__read) || function (o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); - } - catch (error) { e = { error: error }; } - finally { - try { - if (r && !r.done && (m = i["return"])) m.call(i); - } - finally { if (e) throw e.error; } - } - return ar; -}; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); @@ -38,111 +22,60 @@ var __values = (this && this.__values) || function(o) { }; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Flex, Text } from "rebass"; -import { NodeViewWrapper, NodeViewContent } from "../react"; -import { ThemeProvider } from "emotion-theming"; import { Icon } from "../../toolbar/components/icon"; import { Icons } from "../../toolbar/icons"; import { findChildren, } from "@tiptap/core"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback } from "react"; import { TaskItemNode } from "./task-item"; export function TaskItemComponent(props) { - var _a; + var editor = props.editor, updateAttributes = props.updateAttributes, node = props.node, getPos = props.getPos, forwardRef = props.forwardRef; var checked = props.node.attrs.checked; - var _b = __read(useState({ checked: 0, total: 0 }), 2), stats = _b[0], setStats = _b[1]; - var editor = props.editor, updateAttributes = props.updateAttributes, node = props.node, getPos = props.getPos; - var theme = editor.storage.theme; var toggle = useCallback(function () { if (!editor.isEditable) return false; updateAttributes({ checked: !checked }); - var tr = editor.state.tr; - var parentPos = getPos(); - toggleChildren(node, tr, !checked, parentPos); - editor.view.dispatch(tr); - return true; - }, [editor, getPos, node]); - var nestedTaskList = useMemo(function () { - return getChildren(node, getPos()).find(function (_a) { - var node = _a.node; - return node.type.name === "taskList"; + editor.commands.command(function (_a) { + var tr = _a.tr; + var parentPos = getPos(); + toggleChildren(node, tr, !checked, parentPos); + return true; }); - }, [node.childCount]); - var isNested = !!nestedTaskList; - var isCollapsed = nestedTaskList - ? (_a = editor.state.doc.nodeAt(nestedTaskList.pos)) === null || _a === void 0 ? void 0 : _a.attrs.collapsed - : false; - useEffect(function () { - if (!nestedTaskList) - return; - var pos = nestedTaskList.pos, node = nestedTaskList.node; - var children = findChildren(node, function (node) { return node.type.name === TaskItemNode.name; }); - var checked = children.filter(function (_a) { - var node = _a.node; - return node.attrs.checked; - }).length; - var total = children.length; - setStats({ checked: checked, total: total }); - }, []); - return (_jsx(NodeViewWrapper, { children: _jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsxs(Flex, __assign({ sx: { - // mb: isNested ? 0 : 2, - alignItems: "center", - ":hover > .dragHandle, :hover > .toggleSublist": { - opacity: 1, - }, - } }, { children: [_jsx(Icon, { className: "dragHandle", draggable: "true", contentEditable: false, "data-drag-handle": true, path: Icons.dragHandle, sx: { - opacity: 0, - alignSelf: "start", - mr: 2, - cursor: "grab", - ".icon:hover path": { - fill: "var(--checked) !important", - }, - }, size: 20 }), _jsx(Icon, { path: checked ? Icons.check : "", stroke: "1px", sx: { - border: "2px solid", - borderColor: checked ? "checked" : "icon", - borderRadius: "default", - alignSelf: "start", - mr: 2, - p: "1px", - cursor: "pointer", - ":hover": { - borderColor: "checked", - }, - ":hover .icon path": { - fill: "var(--checked) !important", - }, - }, onMouseDown: function (e) { - if (toggle()) - e.preventDefault(); - }, color: checked ? "checked" : "icon", size: 13 }), _jsx(NodeViewContent, { style: { - textDecorationLine: checked ? "line-through" : "none", - color: checked ? "var(--checked)" : "var(--text)", - flex: 1, - // marginBottom: isNested ? 0 : 5, - } }), isNested && (_jsxs(_Fragment, { children: [isCollapsed && (_jsxs(Text, __assign({ variant: "body", sx: { color: "fontTertiary", mr: 35 } }, { children: [stats.checked, "/", stats.total] }))), _jsx(Icon, { className: "toggleSublist", path: isCollapsed ? Icons.chevronDown : Icons.chevronUp, sx: { - opacity: isCollapsed ? 1 : 0, - position: "absolute", - right: 0, - alignSelf: "start", - mr: 2, - cursor: "pointer", - ".icon:hover path": { - fill: "var(--checked) !important", - }, - }, size: 20, onClick: function () { - editor - .chain() - .setNodeSelection(getPos()) - .command(function (_a) { - var tr = _a.tr; - var pos = nestedTaskList.pos, node = nestedTaskList.node; - tr.setNodeMarkup(pos, undefined, { - collapsed: !isCollapsed, - }); - return true; - }) - .run(); - } })] }))] })) })) })); + return true; + }, [editor, getPos, node, checked]); + return (_jsx(_Fragment, { children: _jsxs(Flex, __assign({ sx: { + ":hover > .dragHandle": { + opacity: 1, + }, + } }, { children: [_jsx(Icon, { className: "dragHandle", draggable: "true", contentEditable: false, "data-drag-handle": true, path: Icons.dragHandle, sx: { + opacity: 0, + alignSelf: "start", + mr: 2, + cursor: "grab", + ".icon:hover path": { + fill: "var(--checked) !important", + }, + }, size: 20 }), _jsx(Icon, { path: checked ? Icons.check : "", stroke: "1px", sx: { + border: "2px solid", + borderColor: checked ? "checked" : "icon", + borderRadius: "default", + alignSelf: "start", + mr: 2, + p: "1px", + cursor: "pointer", + ":hover": { + borderColor: "checked", + }, + ":hover .icon path": { + fill: "var(--checked) !important", + }, + }, onMouseDown: function (e) { + if (toggle()) + e.preventDefault(); + }, color: checked ? "checked" : "icon", size: 13 }), _jsx(Text, { as: "div", ref: forwardRef, sx: { + textDecorationLine: checked ? "line-through" : "none", + color: checked ? "var(--checked)" : "var(--text)", + flex: 1, + } })] })) })); } function toggleChildren(node, tr, toggleState, parentPos) { var e_1, _a; @@ -173,3 +106,9 @@ function getChildren(node, parentPos) { }); return children; } +function areAllChecked(node) { + var children = findChildren(node, function (node) { return node.type.name === TaskItemNode.name; }); + if (children.length <= 0) + return undefined; + return children.every(function (node) { return node.node.attrs.checked; }); +} diff --git a/packages/editor/dist/extensions/taskitem/taskitem.d.ts b/packages/editor/dist/extensions/taskitem/taskitem.d.ts index 9706b73d9..32494f7fa 100644 --- a/packages/editor/dist/extensions/taskitem/taskitem.d.ts +++ b/packages/editor/dist/extensions/taskitem/taskitem.d.ts @@ -1 +1,4 @@ +export declare type TaskItemAttributes = { + checked: boolean; +}; export declare const TaskItemNode: import("@tiptap/core").Node; diff --git a/packages/editor/dist/extensions/taskitem/taskitem.js b/packages/editor/dist/extensions/taskitem/taskitem.js index 960be81a7..95d8164a3 100644 --- a/packages/editor/dist/extensions/taskitem/taskitem.js +++ b/packages/editor/dist/extensions/taskitem/taskitem.js @@ -13,7 +13,7 @@ import { mergeAttributes } from "@tiptap/core"; import { onBackspacePressed } from "../list-item/commands"; import { TaskItem } from "@tiptap/extension-task-item"; import { TaskItemComponent } from "./component"; -import { ReactNodeViewRenderer } from "../react"; +import { createNodeView } from "../react"; export var TaskItemNode = TaskItem.extend({ draggable: true, addAttributes: function () { @@ -65,8 +65,14 @@ export var TaskItemNode = TaskItem.extend({ } }); }, addNodeView: function () { - return ReactNodeViewRenderer(TaskItemComponent, { - as: "li", + return createNodeView(TaskItemComponent, { + contentDOMFactory: true, + wrapperFactory: function () { return document.createElement("li"); }, + shouldUpdate: function (_a, _b) { + var prev = _a.attrs; + var next = _b.attrs; + return (prev.checked !== next.checked || prev.collapsed !== next.collapsed); + }, }); }, }); diff --git a/packages/editor/dist/extensions/tasklist/component.d.ts b/packages/editor/dist/extensions/tasklist/component.d.ts index 106fc805a..117e0bb94 100644 --- a/packages/editor/dist/extensions/tasklist/component.d.ts +++ b/packages/editor/dist/extensions/tasklist/component.d.ts @@ -1,2 +1,3 @@ -import { NodeViewProps } from "../react"; -export declare function TaskListComponent(props: NodeViewProps): JSX.Element; +import { ReactNodeViewProps } from "../react"; +import { TaskListAttributes } from "./task-list"; +export declare function TaskListComponent(props: ReactNodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/tasklist/component.js b/packages/editor/dist/extensions/tasklist/component.js index d071f21bf..8aa8f6fc9 100644 --- a/packages/editor/dist/extensions/tasklist/component.js +++ b/packages/editor/dist/extensions/tasklist/component.js @@ -25,70 +25,124 @@ var __read = (this && this.__read) || function (o, n) { } return ar; }; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; import { Box, Flex, Text } from "rebass"; -import { NodeViewWrapper, NodeViewContent } from "../react"; -import { findParentNodeClosestToPos, findChildren } from "@tiptap/core"; -import { ThemeProvider } from "emotion-theming"; +import { findChildren, getNodeType, } from "@tiptap/core"; import { Icon } from "../../toolbar/components/icon"; import { Icons } from "../../toolbar/icons"; import { useEffect, useMemo, useState } from "react"; import { Input } from "@rebass/forms"; import { TaskItemNode } from "../task-item"; +import { findParentNodeOfTypeClosestToPos } from "prosemirror-utils"; export function TaskListComponent(props) { - var editor = props.editor, getPos = props.getPos, node = props.node, updateAttributes = props.updateAttributes; - var _a = node.attrs, collapsed = _a.collapsed, title = _a.title; + var editor = props.editor, getPos = props.getPos, node = props.node, updateAttributes = props.updateAttributes, forwardRef = props.forwardRef; + var taskItemType = getNodeType(TaskItemNode.name, editor.schema); + var _a = node.attrs, title = _a.title, collapsed = _a.collapsed; var _b = __read(useState({ checked: 0, total: 0, percentage: 0 }), 2), stats = _b[0], setStats = _b[1]; - var theme = editor.storage.theme; var parentTaskItem = useMemo(function () { - var resolvedPos = editor.state.doc.resolve(getPos()); - return findParentNodeClosestToPos(resolvedPos, function (node) { return node.type.name === TaskItemNode.name; }); + var pos = editor.state.doc.resolve(getPos()); + return findParentNodeOfTypeClosestToPos(pos, taskItemType); }, []); var nested = !!parentTaskItem; useEffect(function () { if (!parentTaskItem) return; var node = parentTaskItem.node, pos = parentTaskItem.pos; - var allChecked = areAllChecked(node); - var tr = editor.state.tr; - tr.setNodeMarkup(pos, node.type, { checked: allChecked }); - editor.view.dispatch(tr); - }, [parentTaskItem]); + var allChecked = areAllChecked(node, pos, editor.state.doc); + // check parent item if all child items are checked. + editor.commands.command(function (_a) { + var tr = _a.tr; + tr.setNodeMarkup(pos, undefined, { checked: allChecked }); + return true; + }); + }, [node, parentTaskItem]); useEffect(function () { - if (nested) - return; var children = findChildren(node, function (node) { return node.type.name === TaskItemNode.name; }); var checked = children.filter(function (node) { return node.node.attrs.checked; }).length; var total = children.length; var percentage = Math.round((checked / total) * 100); setStats({ checked: checked, total: total, percentage: percentage }); }, [nested, node]); - return (_jsxs(NodeViewWrapper, __assign({ style: { display: collapsed ? "none" : "block" } }, { children: [_jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsx(Flex, __assign({ sx: { flexDirection: "column" } }, { children: nested ? null : (_jsxs(Flex, __assign({ sx: { - position: "relative", - bg: "bgSecondary", - py: 1, - borderRadius: "default", - mb: 2, - alignItems: "center", - justifyContent: "end", - overflow: "hidden", - } }, { children: [_jsx(Box, { sx: { - height: "100%", - width: "".concat(stats.percentage, "%"), - position: "absolute", - bg: "border", - zIndex: 0, - left: 0, - transition: "width 250ms ease-out", - } }), _jsx(Input, { value: title || "", variant: "clean", sx: { p: 0, px: 2, zIndex: 1, color: "fontTertiary" }, placeholder: "Untitled", onChange: function (e) { - updateAttributes({ title: e.target.value }); - } }), _jsxs(Flex, __assign({ sx: { flexShrink: 0, pr: 2 } }, { children: [_jsx(Icon, { path: Icons.checkbox, size: 15, color: "fontTertiary" }), _jsxs(Text, __assign({ variant: "body", sx: { ml: 1, color: "fontTertiary" } }, { children: [stats.checked, "/", stats.total] }))] }))] }))) })) })), _jsx(NodeViewContent, { as: "ul", style: { - paddingInlineStart: 0, - marginBlockStart: nested ? 10 : 0, - marginBlockEnd: 0, - } })] }))); + return (_jsxs(_Fragment, { children: [_jsx(Flex, __assign({ sx: { + flexDirection: "column", + ":hover > div > .toggleSublist": { opacity: 1 }, + } }, { children: nested ? (_jsxs(Flex, __assign({ sx: { + position: "absolute", + top: 0, + right: 0, + }, contentEditable: false }, { children: [collapsed && (_jsxs(Text, __assign({ variant: "body", sx: { color: "fontTertiary", mr: 35 } }, { children: [stats.checked, "/", stats.total] }))), _jsx(Icon, { className: "toggleSublist", path: collapsed ? Icons.chevronDown : Icons.chevronUp, sx: { + opacity: collapsed ? 1 : 0, + position: "absolute", + right: 0, + alignSelf: "start", + mr: 2, + cursor: "pointer", + ".icon:hover path": { + fill: "var(--checked) !important", + }, + }, size: 20, onClick: function () { + updateAttributes({ collapsed: !collapsed }); + } })] }))) : (_jsxs(Flex, __assign({ sx: { + position: "relative", + bg: "bgSecondary", + py: 1, + borderRadius: "default", + mb: 2, + alignItems: "center", + justifyContent: "end", + overflow: "hidden", + }, contentEditable: false }, { children: [_jsx(Box, { sx: { + height: "100%", + width: "".concat(stats.percentage, "%"), + position: "absolute", + bg: "border", + zIndex: 0, + left: 0, + transition: "width 250ms ease-out", + } }), _jsx(Input, { value: title || "", variant: "clean", sx: { p: 0, px: 2, zIndex: 1, color: "fontTertiary" }, placeholder: "Untitled", onChange: function (e) { + updateAttributes({ title: e.target.value }); + } }), _jsxs(Flex, __assign({ sx: { flexShrink: 0, pr: 2 } }, { children: [_jsx(Icon, { path: Icons.checkbox, size: 15, color: "fontTertiary" }), _jsxs(Text, __assign({ variant: "body", sx: { ml: 1, color: "fontTertiary" } }, { children: [stats.checked, "/", stats.total] }))] }))] }))) })), _jsx(Text, { as: "div", ref: forwardRef, sx: { + ul: { + display: collapsed ? "none" : "block", + paddingInlineStart: 0, + marginBlockStart: nested ? 10 : 0, + marginBlockEnd: 0, + }, + li: { + listStyleType: "none", + position: "relative", + }, + } })] })); } -function areAllChecked(node) { +function areAllChecked(node, pos, doc) { + var e_1, _a; var children = findChildren(node, function (node) { return node.type.name === TaskItemNode.name; }); - return children.every(function (node) { return node.node.attrs.checked; }); + try { + for (var children_1 = __values(children), children_1_1 = children_1.next(); !children_1_1.done; children_1_1 = children_1.next()) { + var child = children_1_1.value; + var childPos = pos + child.pos + 1; + var node_1 = doc.nodeAt(childPos); + if (!(node_1 === null || node_1 === void 0 ? void 0 : node_1.attrs.checked)) + return false; + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (children_1_1 && !children_1_1.done && (_a = children_1.return)) _a.call(children_1); + } + finally { if (e_1) throw e_1.error; } + } + return true; } diff --git a/packages/editor/dist/extensions/tasklist/tasklist.d.ts b/packages/editor/dist/extensions/tasklist/tasklist.d.ts index 1520f3f44..46bdb2668 100644 --- a/packages/editor/dist/extensions/tasklist/tasklist.d.ts +++ b/packages/editor/dist/extensions/tasklist/tasklist.d.ts @@ -1 +1,5 @@ +export declare type TaskListAttributes = { + title: string; + collapsed: boolean; +}; export declare const TaskListNode: import("@tiptap/core").Node; diff --git a/packages/editor/dist/extensions/tasklist/tasklist.js b/packages/editor/dist/extensions/tasklist/tasklist.js index 5e8377aa2..e0cb62d03 100644 --- a/packages/editor/dist/extensions/tasklist/tasklist.js +++ b/packages/editor/dist/extensions/tasklist/tasklist.js @@ -1,6 +1,6 @@ import { mergeAttributes } from "@tiptap/core"; import { TaskList } from "@tiptap/extension-task-list"; -import { ReactNodeViewRenderer } from "../react"; +import { createNodeView } from "../react"; import { TaskListComponent } from "./component"; export var TaskListNode = TaskList.extend({ addAttributes: function () { @@ -77,6 +77,14 @@ export var TaskListNode = TaskList.extend({ }; }, addNodeView: function () { - return ReactNodeViewRenderer(TaskListComponent); + var _this = this; + return createNodeView(TaskListComponent, { + contentDOMFactory: function () { + var content = document.createElement("ul"); + content.classList.add("".concat(_this.name.toLowerCase(), "-content-wrapper")); + content.style.whiteSpace = "inherit"; + return { dom: content }; + }, + }); }, }); diff --git a/packages/editor/dist/index.d.ts b/packages/editor/dist/index.d.ts index c4e18b360..fcadacac3 100644 --- a/packages/editor/dist/index.d.ts +++ b/packages/editor/dist/index.d.ts @@ -1,11 +1,11 @@ -import { EditorOptions } from "./extensions/react"; +import { EditorOptions } from "@tiptap/react"; import Toolbar from "./toolbar"; import { Theme } from "@notesnook/theme"; import { AttachmentOptions } from "./extensions/attachment"; -import { PortalProviderAPI } from "./extensions/react/ReactNodeViewPortals"; +import { PortalProviderAPI } from "./extensions/react"; declare const useTiptap: (options?: Partial, deps?: React.DependencyList) => import("./extensions/react").Editor | null; +}>, deps?: React.DependencyList) => import("@tiptap/react").Editor | null; export { useTiptap, Toolbar }; export * from "./extensions/react"; diff --git a/packages/editor/dist/index.js b/packages/editor/dist/index.js index f7d056319..809978069 100644 --- a/packages/editor/dist/index.js +++ b/packages/editor/dist/index.js @@ -23,7 +23,7 @@ var __rest = (this && this.__rest) || function (s, e) { import CharacterCount from "@tiptap/extension-character-count"; import Placeholder from "@tiptap/extension-placeholder"; import Underline from "@tiptap/extension-underline"; -import { useEditor } from "./extensions/react"; +import { useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { useMemo } from "react"; import { EditorView } from "prosemirror-view"; @@ -53,7 +53,7 @@ import { SearchReplace } from "./extensions/search-replace"; import { EmbedNode } from "./extensions/embed"; import { CodeBlock } from "./extensions/code-block"; import { ListItem } from "./extensions/list-item"; -import { EventDispatcher } from "./extensions/react/event-dispatcher"; +import { EventDispatcher } from "./extensions/react"; EditorView.prototype.updateState = function updateState(state) { if (!this.docView) return; // This prevents the matchesNode error on hot reloads diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index 64142a0fa..f7b203c2a 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -33,6 +33,7 @@ "@tiptap/extension-text-align": "^2.0.0-beta.29", "@tiptap/extension-text-style": "^2.0.0-beta.23", "@tiptap/extension-underline": "^2.0.0-beta.23", + "@tiptap/react": "^2.0.0-beta.109", "@tiptap/starter-kit": "^2.0.0-beta.185", "detect-indent": "^7.0.0", "detect-indentation": "^5.20.0", @@ -3909,6 +3910,23 @@ "@tiptap/core": "^2.0.0-beta.1" } }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "2.0.0-beta.56", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.56.tgz", + "integrity": "sha512-nZozwauICdaNPmDPrSn1JFd/9/2rLtK8i2vBOcqxWHObVROvu8ZlJspnrJv23vS6P7/ZO3e/QLVHpnn+1yVq3g==", + "dependencies": { + "prosemirror-state": "^1.3.4", + "prosemirror-view": "^1.23.6", + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0-beta.1" + } + }, "node_modules/@tiptap/extension-bullet-list": { "version": "2.0.0-beta.26", "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.26.tgz", @@ -4005,6 +4023,23 @@ "@tiptap/core": "^2.0.0-beta.1" } }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "2.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.51.tgz", + "integrity": "sha512-rEe7jADK9xr2n2LJsrGEN3Dz7sEGC1JT/7AdTdaZBxQRQvwxTjomqYGrt+LnX+v0MYggh6swMzj7upJosnKbBg==", + "dependencies": { + "prosemirror-state": "^1.3.4", + "prosemirror-view": "^1.23.6", + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0-beta.1" + } + }, "node_modules/@tiptap/extension-font-family": { "version": "2.0.0-beta.21", "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-2.0.0-beta.21.tgz", @@ -4347,6 +4382,25 @@ "@tiptap/core": "^2.0.0-beta.1" } }, + "node_modules/@tiptap/react": { + "version": "2.0.0-beta.109", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.109.tgz", + "integrity": "sha512-kx/I+9DbiKX+LPFYTQf1Mycbw4U77nRsuztMi5UyGoONnwVwVxOUN6sxdnsNX0uo/H0Rf5ZAtQn8vQBaTWPzsQ==", + "dependencies": { + "@tiptap/extension-bubble-menu": "^2.0.0-beta.56", + "@tiptap/extension-floating-menu": "^2.0.0-beta.51", + "prosemirror-view": "^1.23.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0-beta.1", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@tiptap/starter-kit": { "version": "2.0.0-beta.185", "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.185.tgz", @@ -28497,6 +28551,16 @@ "integrity": "sha512-pnO0I5sEQM3pmowjMGQ74adLzvc6HqGyLyqMizaGMicPu9uTYlSdId+qckYEEgPwPMaEShtv2Vg+ZHs7KVqfcg==", "requires": {} }, + "@tiptap/extension-bubble-menu": { + "version": "2.0.0-beta.56", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.56.tgz", + "integrity": "sha512-nZozwauICdaNPmDPrSn1JFd/9/2rLtK8i2vBOcqxWHObVROvu8ZlJspnrJv23vS6P7/ZO3e/QLVHpnn+1yVq3g==", + "requires": { + "prosemirror-state": "^1.3.4", + "prosemirror-view": "^1.23.6", + "tippy.js": "^6.3.7" + } + }, "@tiptap/extension-bullet-list": { "version": "2.0.0-beta.26", "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.26.tgz", @@ -28547,6 +28611,16 @@ "prosemirror-dropcursor": "^1.4.0" } }, + "@tiptap/extension-floating-menu": { + "version": "2.0.0-beta.51", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.51.tgz", + "integrity": "sha512-rEe7jADK9xr2n2LJsrGEN3Dz7sEGC1JT/7AdTdaZBxQRQvwxTjomqYGrt+LnX+v0MYggh6swMzj7upJosnKbBg==", + "requires": { + "prosemirror-state": "^1.3.4", + "prosemirror-view": "^1.23.6", + "tippy.js": "^6.3.7" + } + }, "@tiptap/extension-font-family": { "version": "2.0.0-beta.21", "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-2.0.0-beta.21.tgz", @@ -28726,6 +28800,16 @@ "integrity": "sha512-pMjFH/NpFWLd2XQQa5rG9rGVQ9mu3ygdtu6VGfJ3aAjzBiyLXDKhE4biIFWyFsr8zLpp7DjwbrmLV0UGvbG1WQ==", "requires": {} }, + "@tiptap/react": { + "version": "2.0.0-beta.109", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.109.tgz", + "integrity": "sha512-kx/I+9DbiKX+LPFYTQf1Mycbw4U77nRsuztMi5UyGoONnwVwVxOUN6sxdnsNX0uo/H0Rf5ZAtQn8vQBaTWPzsQ==", + "requires": { + "@tiptap/extension-bubble-menu": "^2.0.0-beta.56", + "@tiptap/extension-floating-menu": "^2.0.0-beta.51", + "prosemirror-view": "^1.23.6" + } + }, "@tiptap/starter-kit": { "version": "2.0.0-beta.185", "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.185.tgz", diff --git a/packages/editor/package.json b/packages/editor/package.json index 5d428a437..5481e3efe 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -29,6 +29,7 @@ "@tiptap/extension-text-align": "^2.0.0-beta.29", "@tiptap/extension-text-style": "^2.0.0-beta.23", "@tiptap/extension-underline": "^2.0.0-beta.23", + "@tiptap/react": "^2.0.0-beta.109", "@tiptap/starter-kit": "^2.0.0-beta.185", "detect-indent": "^7.0.0", "detect-indentation": "^5.20.0", diff --git a/packages/editor/src/extensions/attachment/attachment.ts b/packages/editor/src/extensions/attachment/attachment.ts index d9f4c42a4..7161dd380 100644 --- a/packages/editor/src/extensions/attachment/attachment.ts +++ b/packages/editor/src/extensions/attachment/attachment.ts @@ -1,11 +1,11 @@ -import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core"; -import { findChildren, ReactNodeViewRenderer } from "../react"; +import { Node, mergeAttributes, findChildren } from "@tiptap/core"; import { Attribute } from "@tiptap/core"; +import { createNodeView } from "../react"; import { AttachmentComponent } from "./component"; export type AttachmentType = "image" | "file"; export interface AttachmentOptions { - // HTMLAttributes: Record; + HTMLAttributes: Record; onDownloadAttachment: (attachment: Attachment) => boolean; onOpenAttachmentPicker: (type: AttachmentType) => boolean; } @@ -43,7 +43,7 @@ export const AttachmentNode = Node.create({ addOptions() { return { - // HTMLAttributes: {}, + HTMLAttributes: {}, onDownloadAttachment: () => false, onOpenAttachmentPicker: () => false, }; @@ -73,11 +73,14 @@ export const AttachmentNode = Node.create({ }, renderHTML({ HTMLAttributes }) { - return ["span", mergeAttributes(HTMLAttributes)]; + return [ + "span", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + ]; }, addNodeView() { - return ReactNodeViewRenderer(AttachmentComponent); + return createNodeView(AttachmentComponent); }, addCommands() { diff --git a/packages/editor/src/extensions/attachment/component.tsx b/packages/editor/src/extensions/attachment/component.tsx index 62ad9738e..4212080db 100644 --- a/packages/editor/src/extensions/attachment/component.tsx +++ b/packages/editor/src/extensions/attachment/component.tsx @@ -1,102 +1,97 @@ -import { Box, Flex, ImageProps, Text } from "rebass"; -import { NodeViewWrapper, NodeViewProps } from "../react"; +import { Box, Flex, Text } from "rebass"; import { Attachment } from "./attachment"; -import { ThemeProvider } from "emotion-theming"; -import { Theme } from "@notesnook/theme"; import { ToolButton } from "../../toolbar/components/tool-button"; import { Editor } from "@tiptap/core"; import { useRef } from "react"; import { MenuPresenter } from "../../components/menu/menu"; import { Icon } from "../../toolbar/components/icon"; import { Icons } from "../../toolbar/icons"; +import { ReactNodeViewProps } from "../react"; -export function AttachmentComponent(props: ImageProps & NodeViewProps) { - const { hash, filename, size } = props.node.attrs as Attachment; +export function AttachmentComponent(props: ReactNodeViewProps) { + const { hash, filename, size } = props.node.attrs; - const { editor, updateAttributes } = props; + const { editor } = props; const elementRef = useRef(); const isActive = editor.isActive("attachment", { hash }); // const [isToolbarVisible, setIsToolbarVisible] = useState(); - const theme = editor.storage.theme as Theme; // useEffect(() => { // setIsToolbarVisible(isActive); // }, [isActive]); return ( - - - + + + - - - {filename} - - - {formatBytes(size)} - - - {}} - items={[]} - options={{ - type: "autocomplete", - position: { - target: elementRef.current || undefined, - location: "top", - yOffset: -5, - isTargetAbsolute: true, - align: "end", - }, + {filename} + + - - - - + {formatBytes(size)} + + + {}} + items={[]} + options={{ + type: "autocomplete", + position: { + target: elementRef.current || undefined, + location: "top", + yOffset: -5, + isTargetAbsolute: true, + align: "end", + }, + }} + > + + + ); } @@ -116,6 +111,7 @@ type AttachmentToolbarProps = { editor: Editor; }; +// TODO make this functional function AttachmentToolbar(props: AttachmentToolbarProps) { const { editor } = props; diff --git a/packages/editor/src/extensions/code-block/code-block.ts b/packages/editor/src/extensions/code-block/code-block.ts index 4664a369c..8bf88c4bc 100644 --- a/packages/editor/src/extensions/code-block/code-block.ts +++ b/packages/editor/src/extensions/code-block/code-block.ts @@ -1,4 +1,4 @@ -import { Editor, NodeViewRendererProps } from "@tiptap/core"; +import { Editor, findParentNodeClosestToPos } from "@tiptap/core"; import { Node, textblockTypeInputRule, mergeAttributes } from "@tiptap/core"; import { Plugin, @@ -8,10 +8,9 @@ import { Selection, } from "prosemirror-state"; import { ResolvedPos, Node as ProsemirrorNode } from "prosemirror-model"; -import { findParentNodeClosestToPos, ReactNodeViewRenderer } from "../react"; import { CodeblockComponent } from "./component"; import { HighlighterPlugin } from "./highlighter"; -import ReactNodeView from "../react/ReactNodeView"; +import { createNodeView } from "../react"; import detectIndent from "detect-indent"; import redent from "redent"; import stripIndent from "strip-indent"; @@ -505,7 +504,7 @@ export const CodeBlock = Node.create({ }, addNodeView() { - return ReactNodeView.fromComponent(CodeblockComponent, { + return createNodeView(CodeblockComponent, { contentDOMFactory: () => { const content = document.createElement("div"); content.classList.add("node-content-wrapper"); diff --git a/packages/editor/src/extensions/code-block/component.tsx b/packages/editor/src/extensions/code-block/component.tsx index fe71cacf7..59d6629de 100644 --- a/packages/editor/src/extensions/code-block/component.tsx +++ b/packages/editor/src/extensions/code-block/component.tsx @@ -2,8 +2,6 @@ import { useEffect, useRef, useState } from "react"; import { isLanguageLoaded, loadLanguage } from "./loader"; import { refractor } from "refractor/lib/core"; import "prism-themes/themes/prism-dracula.min.css"; -import { Theme } from "@notesnook/theme"; -import { ThemeProvider } from "emotion-theming"; import { Button, Flex, Text } from "rebass"; import Languages from "./languages.json"; import { PopupPresenter } from "../../components/menu/menu"; @@ -11,14 +9,13 @@ import { Input } from "@rebass/forms"; import { Icon } from "../../toolbar/components/icon"; import { Icons } from "../../toolbar/icons"; import { CodeBlockAttributes } from "./code-block"; -import { ReactComponentProps } from "../react/types"; +import { ReactNodeViewProps } from "../react/types"; export function CodeblockComponent( - props: ReactComponentProps + props: ReactNodeViewProps ) { const { editor, updateAttributes, node, forwardRef } = props; const { language, indentLength, indentType, caretPosition } = node?.attrs; - const theme = editor?.storage.theme as Theme; const [isOpen, setIsOpen] = useState(false); // const [caretPosition, setCaretPosition] = useState(); @@ -45,7 +42,7 @@ export function CodeblockComponent( }, [language]); return ( - + <> updateAttributes({ language })} /> - + ); } diff --git a/packages/editor/src/extensions/embed/component.tsx b/packages/editor/src/extensions/embed/component.tsx index 1e3480b47..e9af8d4d7 100644 --- a/packages/editor/src/extensions/embed/component.tsx +++ b/packages/editor/src/extensions/embed/component.tsx @@ -1,5 +1,4 @@ import { Box, Flex } from "rebass"; -import { NodeViewWrapper, NodeViewProps } from "../react"; import { ThemeProvider } from "emotion-theming"; import { Theme } from "@notesnook/theme"; import { Resizable } from "re-resizable"; @@ -9,10 +8,12 @@ import { useEffect, useRef, useState } from "react"; import { PopupPresenter } from "../../components/menu/menu"; import { EmbedAlignmentOptions, EmbedAttributes } from "./embed"; import { EmbedPopup } from "../../toolbar/popups/embed-popup"; +import { ReactNodeViewProps } from "../react"; -export function EmbedComponent(props: NodeViewProps) { - const { src, width, height, align } = props.node.attrs as EmbedAttributes & - EmbedAlignmentOptions; +export function EmbedComponent( + props: ReactNodeViewProps +) { + const { src, width, height, align } = props.node.attrs; const { editor, updateAttributes } = props; const embedRef = useRef(); @@ -25,81 +26,75 @@ export function EmbedComponent(props: NodeViewProps) { }, [isActive]); return ( - - - + + { + updateAttributes({ + width: ref.clientWidth, + height: ref.clientHeight, + }); + }} + lockAspectRatio={true} > - { - updateAttributes({ - width: ref.clientWidth, - height: ref.clientHeight, - }); - }} - lockAspectRatio={true} - > - {/* + {/* */} - - {isToolbarVisible && ( - - )} - - - - - - + + {isToolbarVisible && ( + + )} + + + + + ); } diff --git a/packages/editor/src/extensions/embed/embed.ts b/packages/editor/src/extensions/embed/embed.ts index 0bd4f17af..431981fd9 100644 --- a/packages/editor/src/extensions/embed/embed.ts +++ b/packages/editor/src/extensions/embed/embed.ts @@ -1,6 +1,5 @@ -import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core"; -import { findChildren, ReactNodeViewRenderer } from "../react"; -import { Attribute } from "@tiptap/core"; +import { Node, mergeAttributes } from "@tiptap/core"; +import { createNodeView } from "../react"; import { EmbedComponent } from "./component"; export interface EmbedOptions { @@ -76,7 +75,7 @@ export const EmbedNode = Node.create({ }, addNodeView() { - return ReactNodeViewRenderer(EmbedComponent); + return createNodeView(EmbedComponent); }, addCommands() { diff --git a/packages/editor/src/extensions/image/component.tsx b/packages/editor/src/extensions/image/component.tsx index 9ae71d860..60af32062 100644 --- a/packages/editor/src/extensions/image/component.tsx +++ b/packages/editor/src/extensions/image/component.tsx @@ -1,5 +1,4 @@ import { Box, Flex, Image, ImageProps } from "rebass"; -import { NodeViewWrapper, NodeViewProps } from "../react"; import { ImageAlignmentOptions, ImageAttributes, @@ -14,10 +13,12 @@ import { useEffect, useRef, useState } from "react"; import { PopupPresenter } from "../../components/menu/menu"; import { Popup } from "../../toolbar/components/popup"; import { ImageProperties } from "../../toolbar/popups/image-properties"; +import { ReactNodeViewProps } from "../react"; -export function ImageComponent(props: ImageProps & NodeViewProps) { - const { src, alt, title, width, height, align, float } = props.node - .attrs as ImageAttributes & ImageAlignmentOptions; +export function ImageComponent( + props: ReactNodeViewProps +) { + const { src, alt, title, width, height, align, float } = props.node.attrs; const { editor, updateAttributes } = props; const imageRef = useRef(); @@ -30,67 +31,65 @@ export function ImageComponent(props: ImageProps & NodeViewProps) { }, [isActive]); return ( - - - + + { + updateAttributes({ + width: ref.clientWidth, + height: ref.clientHeight, + }); + }} + lockAspectRatio={true} > - + {isToolbarVisible && ( + + )} + + {alt} { - updateAttributes({ - width: ref.clientWidth, - height: ref.clientHeight, - }); - }} - lockAspectRatio={true} - > - - {isToolbarVisible && ( - - )} - - {alt} - - - - + {...props} + /> + + + ); } diff --git a/packages/editor/src/extensions/image/image.ts b/packages/editor/src/extensions/image/image.ts index 084af0cb1..67b7fa284 100644 --- a/packages/editor/src/extensions/image/image.ts +++ b/packages/editor/src/extensions/image/image.ts @@ -1,5 +1,5 @@ import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core"; -import { ReactNodeViewRenderer } from "../react"; +import { createNodeView } from "../react"; import { ImageComponent } from "./component"; export interface ImageOptions { @@ -98,7 +98,7 @@ export const ImageNode = Node.create({ }, addNodeView() { - return ReactNodeViewRenderer(ImageComponent); + return createNodeView(ImageComponent); }, addCommands() { diff --git a/packages/editor/src/extensions/react/Editor.ts b/packages/editor/src/extensions/react/Editor.ts deleted file mode 100644 index 06614c23f..000000000 --- a/packages/editor/src/extensions/react/Editor.ts +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { Editor as CoreEditor } from '@tiptap/core' -import { EditorContentProps, EditorContentState } from './EditorContent' - -export class Editor extends CoreEditor { - public contentComponent: React.Component | null = null -} diff --git a/packages/editor/src/extensions/react/EditorContent.tsx b/packages/editor/src/extensions/react/EditorContent.tsx deleted file mode 100644 index 712c3f29b..000000000 --- a/packages/editor/src/extensions/react/EditorContent.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { HTMLProps } from 'react' -import ReactDOM from 'react-dom' -import { Editor } from './Editor' -import { ReactRenderer } from './ReactRenderer' - -const Portals: React.FC<{ renderers: Map }> = ({ renderers }) => { - return ( - <> - {Array.from(renderers).map(([key, renderer]) => { - return ReactDOM.createPortal( - renderer.reactElement, - renderer.element, - key, - ) - })} - - ) -} - -export interface EditorContentProps extends HTMLProps { - editor: Editor | null, -} - -export interface EditorContentState { - renderers: Map -} - -export class PureEditorContent extends React.Component { - editorContentRef: React.RefObject - - constructor(props: EditorContentProps) { - super(props) - this.editorContentRef = React.createRef() - - this.state = { - renderers: new Map(), - } - } - - componentDidMount() { - this.init() - } - - componentDidUpdate() { - this.init() - } - - init() { - const { editor } = this.props - - if (editor && editor.options.element) { - if (editor.contentComponent) { - return - } - - const element = this.editorContentRef.current - - element.append(...editor.options.element.childNodes) - - editor.setOptions({ - element, - }) - - editor.contentComponent = this - - editor.createNodeViews() - } - } - - componentWillUnmount() { - const { editor } = this.props - - if (!editor) { - return - } - - if (!editor.isDestroyed) { - editor.view.setProps({ - nodeViews: {}, - }) - } - - editor.contentComponent = null - - if (!editor.options.element.firstChild) { - return - } - - const newElement = document.createElement('div') - - newElement.append(...editor.options.element.childNodes) - - editor.setOptions({ - element: newElement, - }) - } - - render() { - const { editor, ...rest } = this.props - - return ( - <> -

- - - ) - } -} - -export const EditorContent = React.memo(PureEditorContent) diff --git a/packages/editor/src/extensions/react/NodeViewContent.tsx b/packages/editor/src/extensions/react/NodeViewContent.tsx deleted file mode 100644 index a7142f2bd..000000000 --- a/packages/editor/src/extensions/react/NodeViewContent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { useReactNodeView } from "./useReactNodeView"; - -export interface NodeViewContentProps { - [key: string]: any; - as?: React.ElementType; -} - -export const NodeViewContent: React.FC = ({ - as, - ...props -}) => { - const Tag = as || "div"; - const { nodeViewContentRef } = useReactNodeView(); - - return ( - - ); -}; diff --git a/packages/editor/src/extensions/react/NodeViewWrapper.tsx b/packages/editor/src/extensions/react/NodeViewWrapper.tsx deleted file mode 100644 index 23fc8b438..000000000 --- a/packages/editor/src/extensions/react/NodeViewWrapper.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { useReactNodeView } from './useReactNodeView' - -export interface NodeViewWrapperProps { - [key: string]: any, - as?: React.ElementType, -} - -export const NodeViewWrapper: React.FC = React.forwardRef((props, ref) => { - const { onDragStart } = useReactNodeView() - const Tag = props.as || 'div' - - return ( - - ) -}) diff --git a/packages/editor/src/extensions/react/ReactNodeViewRenderer.tsx b/packages/editor/src/extensions/react/ReactNodeViewRenderer.tsx deleted file mode 100644 index df2859a87..000000000 --- a/packages/editor/src/extensions/react/ReactNodeViewRenderer.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import React from "react"; -import { - NodeView, - NodeViewProps, - NodeViewRenderer, - NodeViewRendererProps, - NodeViewRendererOptions, -} from "@tiptap/core"; -import { - Decoration, - NodeView as ProseMirrorNodeView, - DecorationSource, -} from "prosemirror-view"; -import { Node as ProseMirrorNode } from "prosemirror-model"; -import { Editor } from "./Editor"; -import { ReactRenderer } from "./ReactRenderer"; -import { - ReactNodeViewContext, - ReactNodeViewContextProps, -} from "./useReactNodeView"; - -export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions { - update: - | ((props: { - oldNode: ProseMirrorNode; - oldDecorations: Decoration[]; - newNode: ProseMirrorNode; - newDecorations: Decoration[]; - updateProps: () => void; - }) => boolean) - | null; - as?: string; - className?: string; -} - -class ReactNodeView extends NodeView< - React.FunctionComponent, - Editor, - ReactNodeViewRendererOptions -> { - renderer!: ReactRenderer; - - contentDOMElement!: HTMLElement | null; - - mount() { - const props: NodeViewProps = { - editor: this.editor, - node: this.node, - decorations: this.decorations, - selected: false, - extension: this.extension, - getPos: () => this.getPos(), - updateAttributes: (attributes = {}) => this.updateAttributes(attributes), - deleteNode: () => this.deleteNode(), - }; - - if (!(this.component as any).displayName) { - const capitalizeFirstChar = (string: string): string => { - return string.charAt(0).toUpperCase() + string.substring(1); - }; - - this.component.displayName = capitalizeFirstChar(this.extension.name); - } - - const ReactNodeViewProvider: React.FunctionComponent = (componentProps) => { - const Component = this.component; - const onDragStart = this.onDragStart.bind(this); - const nodeViewContentRef: ReactNodeViewContextProps["nodeViewContentRef"] = - (element) => { - if ( - element && - this.contentDOMElement && - element.firstChild !== this.contentDOMElement - ) { - element.appendChild(this.contentDOMElement); - } - }; - - return ( - - - - ); - }; - - ReactNodeViewProvider.displayName = "ReactNodeView"; - - this.contentDOMElement = this.node.isLeaf - ? null - : document.createElement(this.node.isInline ? "span" : "div"); - - if (this.contentDOMElement) { - // For some reason the whiteSpace prop is not inherited properly in Chrome and Safari - // With this fix it seems to work fine - // See: https://github.com/ueberdosis/tiptap/issues/1197 - this.contentDOMElement.style.whiteSpace = "inherit"; - } - - let as = this.node.isInline ? "span" : "div"; - - if (this.options.as) { - as = this.options.as; - } - - const { className = "" } = this.options; - - this.renderer = new ReactRenderer(ReactNodeViewProvider, { - editor: this.editor, - props, - as, - className: `node-${this.node.type.name} ${className}`.trim(), - }); - } - - get dom() { - if ( - this.renderer.element.firstElementChild && - !this.renderer.element.firstElementChild?.hasAttribute( - "data-node-view-wrapper" - ) - ) { - throw Error( - "Please use the NodeViewWrapper component for your node view." - ); - } - - return this.renderer.element; - } - - get contentDOM() { - if (this.node.isLeaf) { - return null; - } - - return this.contentDOMElement; - } - - update( - node: ProseMirrorNode, - readonlyDecorations: readonly Decoration[], - innerDecorations: DecorationSource - ) { - const decorations = [...readonlyDecorations]; - const updateProps = (props?: Record) => { - this.renderer.updateProps(props); - }; - - if (node.type !== this.node.type) { - return false; - } - - if (typeof this.options.update === "function") { - const oldNode = this.node; - const oldDecorations = this.decorations; - - this.node = node; - this.decorations = decorations; - - return this.options.update({ - oldNode, - oldDecorations, - newNode: node, - newDecorations: decorations, - updateProps: () => updateProps({ node, decorations }), - }); - } - - if (node === this.node && this.decorations === decorations) { - return true; - } - - this.node = node; - this.decorations = decorations; - - updateProps({ node, decorations }); - - return true; - } - - selectNode() { - this.renderer.updateProps({ - selected: true, - }); - } - - deselectNode() { - this.renderer.updateProps({ - selected: false, - }); - } - - destroy() { - this.renderer.destroy(); - this.contentDOMElement = null; - } -} - -export function ReactNodeViewRenderer( - component: any, - options?: Partial -): NodeViewRenderer { - return (props: NodeViewRendererProps) => { - // try to get the parent component - // this is important for vue devtools to show the component hierarchy correctly - // maybe it’s `undefined` because isn’t rendered yet - if (!(props.editor as Editor).contentComponent) { - return {}; - } - - return new ReactNodeView(component, props, options) as ProseMirrorNodeView; - }; -} diff --git a/packages/editor/src/extensions/react/ReactRenderer.tsx b/packages/editor/src/extensions/react/ReactRenderer.tsx deleted file mode 100644 index f5e6bb856..000000000 --- a/packages/editor/src/extensions/react/ReactRenderer.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react' -import { Editor } from '@tiptap/core' -import { Editor as ExtendedEditor } from './Editor' - -function isClassComponent(Component: any) { - return !!( - typeof Component === 'function' - && Component.prototype - && Component.prototype.isReactComponent - ) -} - -function isForwardRefComponent(Component: any) { - return !!( - typeof Component === 'object' - && Component.$$typeof?.toString() === 'Symbol(react.forward_ref)' - ) -} - -export interface ReactRendererOptions { - editor: Editor, - props?: Record, - as?: string, - className?: string, -} - -type ComponentType = - React.ComponentClass

| - React.FunctionComponent

| - React.ForwardRefExoticComponent & React.RefAttributes>; - -export class ReactRenderer { - id: string - - editor: ExtendedEditor - - component: any - - element: Element - - props: Record - - reactElement: React.ReactNode - - ref: R | null = null - - constructor(component: ComponentType, { - editor, - props = {}, - as = 'div', - className = '', - }: ReactRendererOptions) { - this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() - this.component = component - this.editor = editor as ExtendedEditor - this.props = props - this.element = document.createElement(as) - this.element.classList.add('react-renderer') - - if (className) { - this.element.classList.add(...className.split(' ')) - } - - this.render() - } - - render(): void { - const Component = this.component - const props = this.props - - if (isClassComponent(Component) || isForwardRefComponent(Component)) { - props.ref = (ref: R) => { - this.ref = ref - } - } - - this.reactElement = - - if (this.editor?.contentComponent) { - this.editor.contentComponent.setState({ - renderers: this.editor.contentComponent.state.renderers.set( - this.id, - this, - ), - }) - } - } - - updateProps(props: Record = {}): void { - this.props = { - ...this.props, - ...props, - } - - this.render() - } - - destroy(): void { - if (this.editor?.contentComponent) { - const { renderers } = this.editor.contentComponent.state - - renderers.delete(this.id) - - this.editor.contentComponent.setState({ - renderers, - }) - } - } -} diff --git a/packages/editor/src/extensions/react/index.ts b/packages/editor/src/extensions/react/index.ts index 5c1bb8dd6..db315768f 100644 --- a/packages/editor/src/extensions/react/index.ts +++ b/packages/editor/src/extensions/react/index.ts @@ -1,8 +1,6 @@ -export * from "@tiptap/core"; -export { Editor } from "./Editor"; -export * from "./use-editor"; -export * from "./ReactRenderer"; -export * from "./ReactNodeViewRenderer"; -export * from "./EditorContent"; -export * from "./NodeViewWrapper"; -export * from "./NodeViewContent"; +export * from "./react-node-view"; +export * from "./types"; +export * from "./react-portal-provider"; +export * from "./selection-based-react-node-view"; +export * from "./plugin"; +export * from "./event-dispatcher"; diff --git a/packages/editor/src/extensions/react/ReactNodeView.tsx b/packages/editor/src/extensions/react/react-node-view.tsx similarity index 75% rename from packages/editor/src/extensions/react/ReactNodeView.tsx rename to packages/editor/src/extensions/react/react-node-view.tsx index d21e9ce1c..c365c3d91 100644 --- a/packages/editor/src/extensions/react/ReactNodeView.tsx +++ b/packages/editor/src/extensions/react/react-node-view.tsx @@ -1,24 +1,21 @@ import React from "react"; -import { - NodeView, - EditorView, - Decoration, - DecorationSource, -} from "prosemirror-view"; +import { NodeView, Decoration, DecorationSource } from "prosemirror-view"; import { Node as PMNode } from "prosemirror-model"; -import { PortalProviderAPI } from "./ReactNodeViewPortals"; +import { PortalProviderAPI } from "./react-portal-provider"; import { EventDispatcher } from "./event-dispatcher"; import { - ReactComponentProps, + ReactNodeViewProps, ReactNodeViewOptions, - GetPos, + GetPosNode, ForwardRef, ContentDOM, } from "./types"; import { Editor, NodeViewRendererProps } from "@tiptap/core"; +import { Theme } from "@notesnook/theme"; +import { ThemeProvider } from "emotion-theming"; -export default class ReactNodeView

implements NodeView { +export class ReactNodeView

implements NodeView { private domRef!: HTMLElement; private contentDOMWrapper?: Node; @@ -28,7 +25,7 @@ export default class ReactNodeView

implements NodeView { constructor( node: PMNode, protected readonly editor: Editor, - protected readonly getPos: GetPos, + protected readonly getPos: GetPosNode, protected readonly portalProviderAPI: PortalProviderAPI, protected readonly eventDispatcher: EventDispatcher, protected readonly options: ReactNodeViewOptions

@@ -95,6 +92,17 @@ export default class ReactNodeView

implements NodeView { } getContentDOM(): ContentDOM { + if (!this.options.contentDOMFactory) return; + if (this.options.contentDOMFactory === true) { + const content = document.createElement("div"); + content.classList.add( + `${this.node.type.name.toLowerCase()}-content-wrapper` + ); + content.style.whiteSpace = "inherit"; + // caret is not visible if content element width is 0px + content.style.minWidth = `20px`; + return { dom: content }; + } return this.options.contentDOMFactory?.(); } @@ -114,30 +122,29 @@ export default class ReactNodeView

implements NodeView { forwardRef?: ForwardRef ): React.ReactElement | null { if (!this.options.component) return null; + const theme = this.editor.storage.theme as Theme; + const pos = this.getPos(); return ( - this.updateAttributes(attr)} - /> + + this.updateAttributes(attr, pos)} + /> + ); } - private updateAttributes(attributes: any) { + updateAttributes(attributes: any, pos: number) { this.editor.commands.command(({ tr }) => { - if (typeof this.getPos === "boolean") return false; - - const pos = this.getPos(); - tr.setNodeMarkup(pos, undefined, { ...this.node.attrs, ...attributes, }); - return true; }); } @@ -262,38 +269,40 @@ export default class ReactNodeView

implements NodeView { this.domRef = undefined; this.contentDOM = undefined; } - - static fromComponent( - component: React.ComponentType, - options?: Omit, "component"> - ) { - return ({ node, getPos, editor }: NodeViewRendererProps) => { - return new ReactNodeView( - node, - editor, - getPos, - editor.storage.portalProviderAPI, - editor.storage.eventDispatcher, - { - ...options, - component, - } - ).init(); - }; - } } -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 function createNodeView( + component: React.ComponentType, + options?: Omit, "component"> +) { + return ({ node, getPos, editor }: NodeViewRendererProps) => { + const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos()); + + return new ReactNodeView( + node, + editor, + _getPos, + editor.storage.portalProviderAPI, + editor.storage.eventDispatcher, + { + ...options, + component, + } + ).init(); + }; } + +// 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) +// ); +// } diff --git a/packages/editor/src/extensions/react/ReactNodeViewPortals.tsx b/packages/editor/src/extensions/react/react-portal-provider.tsx similarity index 98% rename from packages/editor/src/extensions/react/ReactNodeViewPortals.tsx rename to packages/editor/src/extensions/react/react-portal-provider.tsx index 6ef40ad23..71dccab09 100644 --- a/packages/editor/src/extensions/react/ReactNodeViewPortals.tsx +++ b/packages/editor/src/extensions/react/react-portal-provider.tsx @@ -67,7 +67,7 @@ export class PortalProviderAPI extends EventDispatcher { try { unmountComponentAtNode(container); } catch (error) { - console.error(error); + // IGNORE console.error(error); } } } diff --git a/packages/editor/src/extensions/react/SelectionBasedReactNodeView.tsx b/packages/editor/src/extensions/react/selection-based-react-node-view.tsx similarity index 75% rename from packages/editor/src/extensions/react/SelectionBasedReactNodeView.tsx rename to packages/editor/src/extensions/react/selection-based-react-node-view.tsx index 5f6d34422..b5ed9329c 100644 --- a/packages/editor/src/extensions/react/SelectionBasedReactNodeView.tsx +++ b/packages/editor/src/extensions/react/selection-based-react-node-view.tsx @@ -1,16 +1,24 @@ import React from "react"; -import { DecorationSet, EditorView } from "prosemirror-view"; +import { DecorationSet } from "prosemirror-view"; import { Node as PMNode } from "prosemirror-model"; import { Selection, NodeSelection } from "prosemirror-state"; -import { PortalProviderAPI } from "./ReactNodeViewPortals"; +import { PortalProviderAPI } from "./react-portal-provider"; import { stateKey as SelectionChangePluginKey, ReactNodeViewState, } from "./plugin"; import { EventDispatcher } from "./event-dispatcher"; -import { ReactComponentProps, GetPos, ReactNodeViewOptions } from "./types"; -import ReactNodeView from "./ReactNodeView"; +import { + ReactNodeViewProps, + ReactNodeViewOptions, + GetPosNode, + SelectionBasedReactNodeViewProps, + ForwardRef, +} from "./types"; +import { ReactNodeView } from "./react-node-view"; import { Editor, NodeViewRendererProps } from "@tiptap/core"; +import { Theme } from "@notesnook/theme"; +import { ThemeProvider } from "emotion-theming"; /** * A ReactNodeView that handles React components sensitive @@ -37,18 +45,18 @@ import { Editor, NodeViewRendererProps } from "@tiptap/core"; */ export class SelectionBasedNodeView< - P = ReactComponentProps + P extends SelectionBasedReactNodeViewProps > extends ReactNodeView

{ private oldSelection: Selection; private selectionChangeState: ReactNodeViewState; - pos: number | undefined; + pos: number = -1; posEnd: number | undefined; constructor( node: PMNode, editor: Editor, - getPos: GetPos, + getPos: GetPosNode, portalProviderAPI: PortalProviderAPI, eventDispatcher: EventDispatcher, options: ReactNodeViewOptions

@@ -64,6 +72,29 @@ export class SelectionBasedNodeView< this.selectionChangeState.subscribe(this.onSelectionChange); } + render( + props: P = {} as P, + forwardRef?: ForwardRef + ): React.ReactElement | null { + if (!this.options.component) return null; + const theme = this.editor.storage.theme as Theme; + const isSelected = this.insideSelection() || this.nodeInsideSelection(); + + return ( + + this.updateAttributes(attr, this.pos)} + /> + + ); + } + /** * Update current node's start and end positions. * @@ -208,23 +239,26 @@ export class SelectionBasedNodeView< private onSelectionChange = () => { this.update(this.node, [], DecorationSet.empty); }; - - static fromComponent( - component: React.ComponentType, - options?: Omit, "component"> - ) { - return ({ node, getPos, editor }: NodeViewRendererProps) => { - return new SelectionBasedNodeView( - node, - editor, - getPos, - editor.storage.portalProviderAPI, - editor.storage.eventDispatcher, - { - ...options, - component, - } - ).init(); - }; - } +} + +export function createSelectionBasedNodeView< + TProps extends SelectionBasedReactNodeViewProps +>( + component: React.ComponentType, + options?: Omit, "component"> +) { + return ({ node, getPos, editor }: NodeViewRendererProps) => { + const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos()); + return new SelectionBasedNodeView( + node, + editor, + _getPos, + editor.storage.portalProviderAPI, + editor.storage.eventDispatcher, + { + ...options, + component, + } + ).init(); + }; } diff --git a/packages/editor/src/extensions/react/types.ts b/packages/editor/src/extensions/react/types.ts index d4e5fe76a..2574ffa84 100644 --- a/packages/editor/src/extensions/react/types.ts +++ b/packages/editor/src/extensions/react/types.ts @@ -4,7 +4,7 @@ import { Node as PMNode, Attrs } from "prosemirror-model"; export interface ReactNodeProps { selected: boolean; } - +export type NodeWithAttrs = PMNode & { attrs: T }; export type GetPos = GetPosNode | boolean; export type GetPosNode = () => number; export type ForwardRef = (node: HTMLElement | null) => void; @@ -17,18 +17,23 @@ export type ContentDOM = } | undefined; -export type ReactComponentProps = { - getPos: GetPos; - node: PMNode & { attrs: TAttributes }; +export type ReactNodeViewProps = { + getPos: GetPosNode; + node: NodeWithAttrs; editor: Editor; updateAttributes: UpdateAttributes; forwardRef?: ForwardRef; }; +export type SelectionBasedReactNodeViewProps = + ReactNodeViewProps & { + selected: boolean; + }; + export type ReactNodeViewOptions

= { props?: P; - component?: React.ComponentType

; + component?: React.ComponentType

; shouldUpdate?: ShouldUpdate; - contentDOMFactory?: () => ContentDOM; + contentDOMFactory?: (() => ContentDOM) | boolean; wrapperFactory?: () => HTMLElement; }; diff --git a/packages/editor/src/extensions/react/useEditor.ts b/packages/editor/src/extensions/react/useEditor.ts deleted file mode 100644 index 32896f235..000000000 --- a/packages/editor/src/extensions/react/useEditor.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect, DependencyList } from 'react' -import { EditorOptions } from '@tiptap/core' -import { Editor } from './Editor' - -function useForceUpdate() { - const [, setValue] = useState(0) - - return () => setValue(value => value + 1) -} - -export const useEditor = (options: Partial = {}, deps: DependencyList = []) => { - const [editor, setEditor] = useState(null) - const forceUpdate = useForceUpdate() - - useEffect(() => { - const instance = new Editor(options) - - setEditor(instance) - - instance.on('transaction', () => { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - forceUpdate() - }) - }) - }) - - return () => { - instance.destroy() - } - }, deps) - - return editor -} diff --git a/packages/editor/src/extensions/react/useReactNodeView.ts b/packages/editor/src/extensions/react/useReactNodeView.ts deleted file mode 100644 index 6127ea4a7..000000000 --- a/packages/editor/src/extensions/react/useReactNodeView.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createContext, useContext } from 'react' - -export interface ReactNodeViewContextProps { - onDragStart: (event: DragEvent) => void, - nodeViewContentRef: (element: HTMLElement | null) => void, -} - -export const ReactNodeViewContext = createContext>({ - onDragStart: undefined, -}) - -export const useReactNodeView = () => useContext(ReactNodeViewContext) diff --git a/packages/editor/src/extensions/task-item/component.tsx b/packages/editor/src/extensions/task-item/component.tsx index 4b21279cb..751780232 100644 --- a/packages/editor/src/extensions/task-item/component.tsx +++ b/packages/editor/src/extensions/task-item/component.tsx @@ -1,165 +1,97 @@ -import { Box, Flex, Image, ImageProps, Text } from "rebass"; -import { NodeViewWrapper, NodeViewProps, NodeViewContent } from "../react"; -import { ThemeProvider } from "emotion-theming"; -import { Theme } from "@notesnook/theme"; +import { Flex, Text } from "rebass"; +import { ReactNodeViewProps } from "../react"; import { Icon } from "../../toolbar/components/icon"; import { Icons } from "../../toolbar/icons"; import { Node } from "prosemirror-model"; -import { Transaction, Selection } from "prosemirror-state"; +import { Transaction } from "prosemirror-state"; import { - findParentNodeClosestToPos, findChildren, + findParentNode, + getNodeType, NodeWithPos, } from "@tiptap/core"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { TaskItemNode } from "./task-item"; +import { useCallback, useEffect } from "react"; +import { TaskItemNode, TaskItemAttributes } from "./task-item"; -export function TaskItemComponent(props: ImageProps & NodeViewProps) { +export function TaskItemComponent( + props: ReactNodeViewProps +) { + const { editor, updateAttributes, node, getPos, forwardRef } = props; const { checked } = props.node.attrs; - const [stats, setStats] = useState({ checked: 0, total: 0 }); - const { editor, updateAttributes, node, getPos } = props; - const theme = editor.storage.theme as Theme; const toggle = useCallback(() => { if (!editor.isEditable) return false; updateAttributes({ checked: !checked }); - - const tr = editor.state.tr; - const parentPos = getPos(); - - toggleChildren(node, tr, !checked, parentPos); - - editor.view.dispatch(tr); + editor.commands.command(({ tr }) => { + const parentPos = getPos(); + toggleChildren(node, tr, !checked, parentPos); + return true; + }); return true; - }, [editor, getPos, node]); - - const nestedTaskList = useMemo(() => { - return getChildren(node, getPos()).find( - ({ node }) => node.type.name === "taskList" - ); - }, [node.childCount]); - - const isNested = !!nestedTaskList; - const isCollapsed: boolean = nestedTaskList - ? editor.state.doc.nodeAt(nestedTaskList.pos)?.attrs.collapsed - : false; - - useEffect(() => { - if (!nestedTaskList) return; - const { pos, node } = nestedTaskList; - const children = findChildren( - node, - (node) => node.type.name === TaskItemNode.name - ); - const checked = children.filter(({ node }) => node.attrs.checked).length; - const total = children.length; - setStats({ checked, total }); - }, []); + }, [editor, getPos, node, checked]); return ( - - - + .dragHandle": { + opacity: 1, + }, + }} + > + .dragHandle, :hover > .toggleSublist": { - opacity: 1, + opacity: 0, + alignSelf: "start", + mr: 2, + cursor: "grab", + ".icon:hover path": { + fill: "var(--checked) !important", }, }} - > - - { - if (toggle()) e.preventDefault(); - }} - color={checked ? "checked" : "icon"} - size={13} - /> + size={20} + /> + { + if (toggle()) e.preventDefault(); + }} + color={checked ? "checked" : "icon"} + size={13} + /> - - - {isNested && ( - <> - {isCollapsed && ( - - {stats.checked}/{stats.total} - - )} - { - editor - .chain() - .setNodeSelection(getPos()) - .command(({ tr }) => { - const { pos, node } = nestedTaskList; - tr.setNodeMarkup(pos, undefined, { - collapsed: !isCollapsed, - }); - return true; - }) - .run(); - }} - /> - - )} - - - + + + ); } @@ -190,3 +122,12 @@ function getChildren(node: Node, parentPos: number) { }); return children; } + +function areAllChecked(node: Node) { + const children = findChildren( + node, + (node) => node.type.name === TaskItemNode.name + ); + if (children.length <= 0) return undefined; + return children.every((node) => node.node.attrs.checked); +} diff --git a/packages/editor/src/extensions/task-item/task-item.ts b/packages/editor/src/extensions/task-item/task-item.ts index b18dbb4d6..48a2cbcda 100644 --- a/packages/editor/src/extensions/task-item/task-item.ts +++ b/packages/editor/src/extensions/task-item/task-item.ts @@ -1,8 +1,12 @@ -import { mergeAttributes } from "@tiptap/core"; +import { mergeAttributes, nodeInputRule } from "@tiptap/core"; import { onBackspacePressed } from "../list-item/commands"; import { TaskItem } from "@tiptap/extension-task-item"; import { TaskItemComponent } from "./component"; -import { ReactNodeViewRenderer } from "../react"; +import { createNodeView } from "../react"; + +export type TaskItemAttributes = { + checked: boolean; +}; export const TaskItemNode = TaskItem.extend({ draggable: true, @@ -57,8 +61,14 @@ export const TaskItemNode = TaskItem.extend({ }, addNodeView() { - return ReactNodeViewRenderer(TaskItemComponent, { - as: "li", + return createNodeView(TaskItemComponent, { + contentDOMFactory: true, + wrapperFactory: () => document.createElement("li"), + shouldUpdate: ({ attrs: prev }, { attrs: next }) => { + return ( + prev.checked !== next.checked || prev.collapsed !== next.collapsed + ); + }, }); }, }); diff --git a/packages/editor/src/extensions/task-list/component.tsx b/packages/editor/src/extensions/task-list/component.tsx index d61f29082..0f5ca7e6c 100644 --- a/packages/editor/src/extensions/task-list/component.tsx +++ b/packages/editor/src/extensions/task-list/component.tsx @@ -1,29 +1,30 @@ -import { Box, Flex, Image, ImageProps, Text } from "rebass"; -import { NodeViewWrapper, NodeViewProps, NodeViewContent } from "../react"; +import { Box, Flex, Text } from "rebass"; +import { ReactNodeViewProps } from "../react"; import { Node } from "prosemirror-model"; -import { Transaction, Selection } from "prosemirror-state"; -import { findParentNodeClosestToPos, findChildren } from "@tiptap/core"; -import { ThemeProvider } from "emotion-theming"; -import { Theme } from "@notesnook/theme"; +import { + findParentNodeClosestToPos, + findChildren, + getNodeType, +} from "@tiptap/core"; import { Icon } from "../../toolbar/components/icon"; import { Icons } from "../../toolbar/icons"; import { useEffect, useMemo, useState } from "react"; import { Input } from "@rebass/forms"; import { TaskItemNode } from "../task-item"; +import { TaskListAttributes } from "./task-list"; +import { findParentNodeOfTypeClosestToPos } from "prosemirror-utils"; -export function TaskListComponent(props: NodeViewProps) { - const { editor, getPos, node, updateAttributes } = props; - const { collapsed, title } = node.attrs; +export function TaskListComponent( + props: ReactNodeViewProps +) { + const { editor, getPos, node, updateAttributes, forwardRef } = props; + const taskItemType = getNodeType(TaskItemNode.name, editor.schema); + const { title, collapsed } = node.attrs; const [stats, setStats] = useState({ checked: 0, total: 0, percentage: 0 }); - const theme = editor.storage.theme as Theme; - const parentTaskItem = useMemo(() => { - const resolvedPos = editor.state.doc.resolve(getPos()); - return findParentNodeClosestToPos( - resolvedPos, - (node) => node.type.name === TaskItemNode.name - ); + const pos = editor.state.doc.resolve(getPos()); + return findParentNodeOfTypeClosestToPos(pos, taskItemType); }, []); const nested = !!parentTaskItem; @@ -31,14 +32,16 @@ export function TaskListComponent(props: NodeViewProps) { useEffect(() => { if (!parentTaskItem) return; const { node, pos } = parentTaskItem; - const allChecked = areAllChecked(node); - const tr = editor.state.tr; - tr.setNodeMarkup(pos, node.type, { checked: allChecked }); - editor.view.dispatch(tr); - }, [parentTaskItem]); + const allChecked = areAllChecked(node, pos, editor.state.doc); + + // check parent item if all child items are checked. + editor.commands.command(({ tr }) => { + tr.setNodeMarkup(pos, undefined, { checked: allChecked }); + return true; + }); + }, [node, parentTaskItem]); useEffect(() => { - if (nested) return; const children = findChildren( node, (node) => node.type.name === TaskItemNode.name @@ -50,69 +53,122 @@ export function TaskListComponent(props: NodeViewProps) { }, [nested, node]); return ( - - - - {nested ? null : ( - + div > .toggleSublist": { opacity: 1 }, + }} + > + {nested ? ( + + {collapsed && ( + + {stats.checked}/{stats.total} + + )} + - { + updateAttributes({ collapsed: !collapsed }); + }} + /> + + ) : ( + + - { - updateAttributes({ title: e.target.value }); - }} - /> - - - - {stats.checked}/{stats.total} - - + zIndex: 0, + left: 0, + transition: "width 250ms ease-out", + }} + /> + { + updateAttributes({ title: e.target.value }); + }} + /> + + + + {stats.checked}/{stats.total} + - )} - - - + )} + + - + ); } -function areAllChecked(node: Node) { +function areAllChecked(node: Node, pos: number, doc: Node) { const children = findChildren( node, (node) => node.type.name === TaskItemNode.name ); - return children.every((node) => node.node.attrs.checked); + + for (const child of children) { + const childPos = pos + child.pos + 1; + const node = doc.nodeAt(childPos); + if (!node?.attrs.checked) return false; + } + + return true; } diff --git a/packages/editor/src/extensions/task-list/task-list.ts b/packages/editor/src/extensions/task-list/task-list.ts index f78d87819..62dacfbab 100644 --- a/packages/editor/src/extensions/task-list/task-list.ts +++ b/packages/editor/src/extensions/task-list/task-list.ts @@ -1,8 +1,13 @@ import { mergeAttributes } from "@tiptap/core"; import { TaskList } from "@tiptap/extension-task-list"; -import { ReactNodeViewRenderer } from "../react"; +import { createNodeView } from "../react"; import { TaskListComponent } from "./component"; +export type TaskListAttributes = { + title: string; + collapsed: boolean; +}; + export const TaskListNode = TaskList.extend({ addAttributes() { return { @@ -80,6 +85,13 @@ export const TaskListNode = TaskList.extend({ }, addNodeView() { - return ReactNodeViewRenderer(TaskListComponent); + return createNodeView(TaskListComponent, { + contentDOMFactory: () => { + const content = document.createElement("ul"); + content.classList.add(`${this.name.toLowerCase()}-content-wrapper`); + content.style.whiteSpace = "inherit"; + return { dom: content }; + }, + }); }, }); diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index dd21640b1..566fbd876 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -1,9 +1,9 @@ import CharacterCount from "@tiptap/extension-character-count"; import Placeholder from "@tiptap/extension-placeholder"; import Underline from "@tiptap/extension-underline"; -import { EditorOptions, useEditor } from "./extensions/react"; +import { EditorOptions, useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { EditorView } from "prosemirror-view"; import Toolbar from "./toolbar"; import TextAlign from "@tiptap/extension-text-align"; @@ -32,8 +32,7 @@ import { SearchReplace } from "./extensions/search-replace"; import { EmbedNode } from "./extensions/embed"; import { CodeBlock } from "./extensions/code-block"; import { ListItem } from "./extensions/list-item"; -import { PortalProviderAPI } from "./extensions/react/ReactNodeViewPortals"; -import { EventDispatcher } from "./extensions/react/event-dispatcher"; +import { PortalProviderAPI, EventDispatcher } from "./extensions/react"; EditorView.prototype.updateState = function updateState(state) { if (!(this as any).docView) return; // This prevents the matchesNode error on hot reloads