mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-21 14:09:34 +01:00
feat: add permission handling
This commit is contained in:
@@ -3,7 +3,8 @@ import { NodeView, Decoration, DecorationSource } from "prosemirror-view";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import { PortalProviderAPI } from "./react-portal-provider";
|
||||
import { ReactNodeViewProps, ReactNodeViewOptions, GetPosNode, ForwardRef, ContentDOM } from "./types";
|
||||
import { Editor, NodeViewRendererProps } from "@tiptap/core";
|
||||
import { NodeViewRendererProps } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
export declare class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
||||
protected readonly editor: Editor;
|
||||
protected readonly getPos: GetPosNode;
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from "react";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import { ReactNodeViewOptions, GetPosNode, SelectionBasedReactNodeViewProps, ForwardRef } from "./types";
|
||||
import { ReactNodeView } from "./react-node-view";
|
||||
import { Editor, NodeViewRendererProps } from "@tiptap/core";
|
||||
import { NodeViewRendererProps } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
/**
|
||||
* A ReactNodeView that handles React components sensitive
|
||||
* to selection changes.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference types="react" />
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { Node as PMNode, Attrs } from "prosemirror-model";
|
||||
export interface ReactNodeProps {
|
||||
selected: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="react" />
|
||||
import { SelectionBasedReactNodeViewProps } from "../react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { NodeView } from "prosemirror-view";
|
||||
export declare function TableComponent(props: SelectionBasedReactNodeViewProps): JSX.Element;
|
||||
export declare function TableNodeView(editor: Editor): NodeView;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EditorOptions } from "@tiptap/core";
|
||||
import { DependencyList } from "react";
|
||||
import { Editor as EditorType } from "../types";
|
||||
export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => EditorType | null;
|
||||
import { Editor } from "../types";
|
||||
export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => Editor | null;
|
||||
|
||||
17
packages/editor/dist/cjs/hooks/useEditor.js
vendored
17
packages/editor/dist/cjs/hooks/useEditor.js
vendored
@@ -1,8 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useEditor = void 0;
|
||||
const core_1 = require("@tiptap/core");
|
||||
const react_1 = require("react");
|
||||
const types_1 = require("../types");
|
||||
function useForceUpdate() {
|
||||
const [, setValue] = (0, react_1.useState)(0);
|
||||
return () => setValue((value) => value + 1);
|
||||
@@ -13,7 +13,7 @@ const useEditor = (options = {}, deps = []) => {
|
||||
const editorRef = (0, react_1.useRef)(editor);
|
||||
(0, react_1.useEffect)(() => {
|
||||
let isMounted = true;
|
||||
const instance = new core_1.Editor(options);
|
||||
const instance = new types_1.Editor(options);
|
||||
setEditor(instance);
|
||||
instance.on("transaction", () => {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -31,10 +31,21 @@ const useEditor = (options = {}, deps = []) => {
|
||||
}, deps);
|
||||
(0, react_1.useEffect)(() => {
|
||||
editorRef.current = editor;
|
||||
if (editor && !editor.current)
|
||||
if (!editor)
|
||||
return;
|
||||
if (!editor.current) {
|
||||
Object.defineProperty(editor, "current", {
|
||||
get: () => editorRef.current,
|
||||
});
|
||||
}
|
||||
// if (!editor.executor) {
|
||||
// Object.defineProperty(editor, "executor", {
|
||||
// get: () => (id?: string) => {
|
||||
// console.log(id);
|
||||
// return editorRef.current;
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
}, [editor]);
|
||||
(0, react_1.useEffect)(() => {
|
||||
// this is required for the drag/drop to work properly
|
||||
|
||||
7
packages/editor/dist/cjs/hooks/usePermissionHandler.d.ts
vendored
Normal file
7
packages/editor/dist/cjs/hooks/usePermissionHandler.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { UnionCommands } from "@tiptap/core";
|
||||
export declare type Claims = "premium";
|
||||
export declare type PermissionHandlerOptions = {
|
||||
claims: Record<Claims, boolean>;
|
||||
onPermissionDenied: (claim: Claims, id: keyof UnionCommands) => void;
|
||||
};
|
||||
export declare function usePermissionHandler(options: PermissionHandlerOptions): void;
|
||||
32
packages/editor/dist/cjs/hooks/usePermissionHandler.js
vendored
Normal file
32
packages/editor/dist/cjs/hooks/usePermissionHandler.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.usePermissionHandler = void 0;
|
||||
const react_1 = require("react");
|
||||
const ClaimsMap = {
|
||||
premium: ["insertImage"],
|
||||
};
|
||||
function usePermissionHandler(options) {
|
||||
const { claims, onPermissionDenied } = options;
|
||||
(0, react_1.useEffect)(() => {
|
||||
function onPermissionRequested(ev) {
|
||||
const { detail: { id }, } = ev;
|
||||
for (const key in ClaimsMap) {
|
||||
const claim = key;
|
||||
const commands = ClaimsMap[claim];
|
||||
if (commands.indexOf(id) <= -1)
|
||||
continue;
|
||||
if (claims[claim])
|
||||
continue;
|
||||
onPermissionDenied(claim, id);
|
||||
ev.preventDefault();
|
||||
break;
|
||||
}
|
||||
ev.preventDefault();
|
||||
}
|
||||
window.addEventListener("permissionrequest", onPermissionRequested);
|
||||
return () => {
|
||||
window.removeEventListener("permissionrequest", onPermissionRequested);
|
||||
};
|
||||
}, [claims, onPermissionDenied]);
|
||||
}
|
||||
exports.usePermissionHandler = usePermissionHandler;
|
||||
3
packages/editor/dist/cjs/index.d.ts
vendored
3
packages/editor/dist/cjs/index.d.ts
vendored
@@ -4,12 +4,13 @@ import Toolbar from "./toolbar";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { AttachmentOptions } from "./extensions/attachment";
|
||||
import { EditorOptions } from "@tiptap/core";
|
||||
import { usePermissionHandler } from "./hooks/use-permission-handler";
|
||||
declare type TiptapOptions = EditorOptions & AttachmentOptions & {
|
||||
theme: Theme;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
declare const useTiptap: (options?: Partial<TiptapOptions>, deps?: import("react").DependencyList) => import("./types").Editor | null;
|
||||
export { useTiptap, Toolbar };
|
||||
export { useTiptap, Toolbar, usePermissionHandler };
|
||||
export * from "./types";
|
||||
export * from "./extensions/react";
|
||||
export * from "./toolbar";
|
||||
|
||||
10
packages/editor/dist/cjs/index.js
vendored
10
packages/editor/dist/cjs/index.js
vendored
@@ -28,7 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Toolbar = exports.useTiptap = void 0;
|
||||
exports.usePermissionHandler = exports.Toolbar = exports.useTiptap = void 0;
|
||||
require("./extensions");
|
||||
const extension_character_count_1 = __importDefault(require("@tiptap/extension-character-count"));
|
||||
const extension_placeholder_1 = __importDefault(require("@tiptap/extension-placeholder"));
|
||||
@@ -70,6 +70,8 @@ const outlinelistitem_1 = require("./extensions/outlinelistitem");
|
||||
const table_1 = require("./extensions/table");
|
||||
const toolbarstore_1 = require("./toolbar/stores/toolbarstore");
|
||||
const useEditor_1 = require("./hooks/useEditor");
|
||||
const usePermissionHandler_1 = require("./hooks/usePermissionHandler");
|
||||
Object.defineProperty(exports, "usePermissionHandler", { enumerable: true, get: function () { return usePermissionHandler_1.usePermissionHandler; } });
|
||||
prosemirror_view_1.EditorView.prototype.updateState = function updateState(state) {
|
||||
if (!this.docView)
|
||||
return; // This prevents the matchesNode error on hot reloads
|
||||
@@ -79,6 +81,9 @@ const useTiptap = (options = {}, deps = []) => {
|
||||
const { theme, isMobile, onDownloadAttachment, onOpenAttachmentPicker } = options, restOptions = __rest(options, ["theme", "isMobile", "onDownloadAttachment", "onOpenAttachmentPicker"]);
|
||||
const PortalProviderAPI = (0, react_2.usePortalProvider)();
|
||||
const setIsMobile = (0, toolbarstore_1.useToolbarStore)((store) => store.setIsMobile);
|
||||
(0, react_1.useEffect)(() => {
|
||||
setIsMobile(isMobile || false);
|
||||
}, [isMobile]);
|
||||
const defaultOptions = (0, react_1.useMemo)(() => ({
|
||||
extensions: [
|
||||
react_2.NodeViewSelectionNotifier,
|
||||
@@ -159,9 +164,6 @@ const useTiptap = (options = {}, deps = []) => {
|
||||
isMobile,
|
||||
]);
|
||||
const editor = (0, useEditor_1.useEditor)(Object.assign(Object.assign({}, defaultOptions), restOptions), deps);
|
||||
(0, react_1.useEffect)(() => {
|
||||
setIsMobile(isMobile || false);
|
||||
}, [isMobile]);
|
||||
return editor;
|
||||
};
|
||||
exports.useTiptap = useTiptap;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// <reference types="react" />
|
||||
import { ToolbarGroupDefinition, ToolButtonVariant } from "../types";
|
||||
import { FlexProps } from "rebass";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { NodeWithOffset } from "../utils/prosemirror";
|
||||
export declare type ToolbarGroupProps = FlexProps & {
|
||||
tools: ToolbarGroupDefinition;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="react" />
|
||||
import { ImageAlignmentOptions, ImageSizeOptions } from "../../extensions/image";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
export declare type ImagePropertiesProps = ImageSizeOptions & ImageAlignmentOptions & {
|
||||
editor: Editor;
|
||||
onClose: () => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference types="react" />
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
export declare type SearchReplacePopupProps = {
|
||||
editor: Editor;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="react" />
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../types";
|
||||
import { ToolbarLocation } from "./stores/toolbar-store";
|
||||
import { ToolbarDefinition } from "./types";
|
||||
declare type ToolbarProps = {
|
||||
|
||||
@@ -214,7 +214,8 @@ const uploadImageFromURLMobile = (editor) => ({
|
||||
type: "popup",
|
||||
component: ({ onClick }) => ((0, jsx_runtime_1.jsx)(imageupload_1.ImageUploadPopup, { onInsert: (image) => {
|
||||
var _a;
|
||||
(_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
(_a = editor
|
||||
.requestPermission("insertImage")) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
onClick === null || onClick === void 0 ? void 0 : onClick();
|
||||
}, onClose: () => {
|
||||
onClick === null || onClick === void 0 ? void 0 : onClick();
|
||||
@@ -229,13 +230,12 @@ const uploadImageFromURL = (editor) => ({
|
||||
title: "Attach from URL",
|
||||
icon: "link",
|
||||
onClick: () => {
|
||||
if (!editor)
|
||||
return;
|
||||
(0, popuppresenter_1.showPopup)({
|
||||
theme: editor.storage.theme,
|
||||
popup: (hide) => ((0, jsx_runtime_1.jsx)(imageupload_1.ImageUploadPopup, { onInsert: (image) => {
|
||||
var _a;
|
||||
(_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
(_a = editor
|
||||
.requestPermission("insertImage")) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
hide();
|
||||
}, onClose: hide })),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference types="react" />
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { MenuButton } from "../../components/menu/types";
|
||||
import { ToolProps } from "../types";
|
||||
export declare function menuButtonToTool(constructItem: (editor: Editor) => MenuButton): (props: ToolProps) => JSX.Element;
|
||||
|
||||
15
packages/editor/dist/cjs/types.d.ts
vendored
15
packages/editor/dist/cjs/types.d.ts
vendored
@@ -1,9 +1,20 @@
|
||||
import { Editor as TiptapEditor } from "@tiptap/core";
|
||||
export interface Editor extends TiptapEditor {
|
||||
import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
|
||||
export interface PermissionRequestEvent extends CustomEvent<{
|
||||
id: keyof UnionCommands;
|
||||
}> {
|
||||
}
|
||||
export declare class Editor extends TiptapEditor {
|
||||
/**
|
||||
* Use this to get the latest instance of the editor.
|
||||
* This is required to reduce unnecessary rerenders of
|
||||
* toolbar elements.
|
||||
*/
|
||||
current?: TiptapEditor;
|
||||
/**
|
||||
* Request permission before executing a command to make sure user
|
||||
* is allowed to perform the action.
|
||||
* @param id the command id to get permission for
|
||||
* @returns latest editor instance
|
||||
*/
|
||||
requestPermission(id: keyof UnionCommands): TiptapEditor | undefined;
|
||||
}
|
||||
|
||||
20
packages/editor/dist/cjs/types.js
vendored
20
packages/editor/dist/cjs/types.js
vendored
@@ -1,2 +1,22 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Editor = void 0;
|
||||
const core_1 = require("@tiptap/core");
|
||||
class Editor extends core_1.Editor {
|
||||
/**
|
||||
* Request permission before executing a command to make sure user
|
||||
* is allowed to perform the action.
|
||||
* @param id the command id to get permission for
|
||||
* @returns latest editor instance
|
||||
*/
|
||||
requestPermission(id) {
|
||||
const event = new CustomEvent("permissionrequest", {
|
||||
detail: { id },
|
||||
cancelable: true,
|
||||
});
|
||||
if (!window.dispatchEvent(event))
|
||||
return undefined;
|
||||
return this.current;
|
||||
}
|
||||
}
|
||||
exports.Editor = Editor;
|
||||
|
||||
@@ -3,7 +3,8 @@ import { NodeView, Decoration, DecorationSource } from "prosemirror-view";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import { PortalProviderAPI } from "./react-portal-provider";
|
||||
import { ReactNodeViewProps, ReactNodeViewOptions, GetPosNode, ForwardRef, ContentDOM } from "./types";
|
||||
import { Editor, NodeViewRendererProps } from "@tiptap/core";
|
||||
import { NodeViewRendererProps } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
export declare class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
||||
protected readonly editor: Editor;
|
||||
protected readonly getPos: GetPosNode;
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from "react";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import { ReactNodeViewOptions, GetPosNode, SelectionBasedReactNodeViewProps, ForwardRef } from "./types";
|
||||
import { ReactNodeView } from "./react-node-view";
|
||||
import { Editor, NodeViewRendererProps } from "@tiptap/core";
|
||||
import { NodeViewRendererProps } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
/**
|
||||
* A ReactNodeView that handles React components sensitive
|
||||
* to selection changes.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference types="react" />
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { Node as PMNode, Attrs } from "prosemirror-model";
|
||||
export interface ReactNodeProps {
|
||||
selected: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="react" />
|
||||
import { SelectionBasedReactNodeViewProps } from "../react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { NodeView } from "prosemirror-view";
|
||||
export declare function TableComponent(props: SelectionBasedReactNodeViewProps): JSX.Element;
|
||||
export declare function TableNodeView(editor: Editor): NodeView;
|
||||
|
||||
4
packages/editor/dist/es/hooks/useEditor.d.ts
vendored
4
packages/editor/dist/es/hooks/useEditor.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { EditorOptions } from "@tiptap/core";
|
||||
import { DependencyList } from "react";
|
||||
import { Editor as EditorType } from "../types";
|
||||
export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => EditorType | null;
|
||||
import { Editor } from "../types";
|
||||
export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => Editor | null;
|
||||
|
||||
15
packages/editor/dist/es/hooks/useEditor.js
vendored
15
packages/editor/dist/es/hooks/useEditor.js
vendored
@@ -1,5 +1,5 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Editor } from "../types";
|
||||
function useForceUpdate() {
|
||||
const [, setValue] = useState(0);
|
||||
return () => setValue((value) => value + 1);
|
||||
@@ -28,10 +28,21 @@ export const useEditor = (options = {}, deps = []) => {
|
||||
}, deps);
|
||||
useEffect(() => {
|
||||
editorRef.current = editor;
|
||||
if (editor && !editor.current)
|
||||
if (!editor)
|
||||
return;
|
||||
if (!editor.current) {
|
||||
Object.defineProperty(editor, "current", {
|
||||
get: () => editorRef.current,
|
||||
});
|
||||
}
|
||||
// if (!editor.executor) {
|
||||
// Object.defineProperty(editor, "executor", {
|
||||
// get: () => (id?: string) => {
|
||||
// console.log(id);
|
||||
// return editorRef.current;
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
}, [editor]);
|
||||
useEffect(() => {
|
||||
// this is required for the drag/drop to work properly
|
||||
|
||||
7
packages/editor/dist/es/hooks/usePermissionHandler.d.ts
vendored
Normal file
7
packages/editor/dist/es/hooks/usePermissionHandler.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { UnionCommands } from "@tiptap/core";
|
||||
export declare type Claims = "premium";
|
||||
export declare type PermissionHandlerOptions = {
|
||||
claims: Record<Claims, boolean>;
|
||||
onPermissionDenied: (claim: Claims, id: keyof UnionCommands) => void;
|
||||
};
|
||||
export declare function usePermissionHandler(options: PermissionHandlerOptions): void;
|
||||
28
packages/editor/dist/es/hooks/usePermissionHandler.js
vendored
Normal file
28
packages/editor/dist/es/hooks/usePermissionHandler.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEffect } from "react";
|
||||
const ClaimsMap = {
|
||||
premium: ["insertImage"],
|
||||
};
|
||||
export function usePermissionHandler(options) {
|
||||
const { claims, onPermissionDenied } = options;
|
||||
useEffect(() => {
|
||||
function onPermissionRequested(ev) {
|
||||
const { detail: { id }, } = ev;
|
||||
for (const key in ClaimsMap) {
|
||||
const claim = key;
|
||||
const commands = ClaimsMap[claim];
|
||||
if (commands.indexOf(id) <= -1)
|
||||
continue;
|
||||
if (claims[claim])
|
||||
continue;
|
||||
onPermissionDenied(claim, id);
|
||||
ev.preventDefault();
|
||||
break;
|
||||
}
|
||||
ev.preventDefault();
|
||||
}
|
||||
window.addEventListener("permissionrequest", onPermissionRequested);
|
||||
return () => {
|
||||
window.removeEventListener("permissionrequest", onPermissionRequested);
|
||||
};
|
||||
}, [claims, onPermissionDenied]);
|
||||
}
|
||||
3
packages/editor/dist/es/index.d.ts
vendored
3
packages/editor/dist/es/index.d.ts
vendored
@@ -4,12 +4,13 @@ import Toolbar from "./toolbar";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { AttachmentOptions } from "./extensions/attachment";
|
||||
import { EditorOptions } from "@tiptap/core";
|
||||
import { usePermissionHandler } from "./hooks/use-permission-handler";
|
||||
declare type TiptapOptions = EditorOptions & AttachmentOptions & {
|
||||
theme: Theme;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
declare const useTiptap: (options?: Partial<TiptapOptions>, deps?: import("react").DependencyList) => import("./types").Editor | null;
|
||||
export { useTiptap, Toolbar };
|
||||
export { useTiptap, Toolbar, usePermissionHandler };
|
||||
export * from "./types";
|
||||
export * from "./extensions/react";
|
||||
export * from "./toolbar";
|
||||
|
||||
9
packages/editor/dist/es/index.js
vendored
9
packages/editor/dist/es/index.js
vendored
@@ -49,6 +49,7 @@ import { OutlineListItem } from "./extensions/outline-list-item";
|
||||
import { Table } from "./extensions/table";
|
||||
import { useToolbarStore } from "./toolbar/stores/toolbar-store";
|
||||
import { useEditor } from "./hooks/use-editor";
|
||||
import { usePermissionHandler } from "./hooks/use-permission-handler";
|
||||
EditorView.prototype.updateState = function updateState(state) {
|
||||
if (!this.docView)
|
||||
return; // This prevents the matchesNode error on hot reloads
|
||||
@@ -58,6 +59,9 @@ const useTiptap = (options = {}, deps = []) => {
|
||||
const { theme, isMobile, onDownloadAttachment, onOpenAttachmentPicker } = options, restOptions = __rest(options, ["theme", "isMobile", "onDownloadAttachment", "onOpenAttachmentPicker"]);
|
||||
const PortalProviderAPI = usePortalProvider();
|
||||
const setIsMobile = useToolbarStore((store) => store.setIsMobile);
|
||||
useEffect(() => {
|
||||
setIsMobile(isMobile || false);
|
||||
}, [isMobile]);
|
||||
const defaultOptions = useMemo(() => ({
|
||||
extensions: [
|
||||
NodeViewSelectionNotifier,
|
||||
@@ -138,12 +142,9 @@ const useTiptap = (options = {}, deps = []) => {
|
||||
isMobile,
|
||||
]);
|
||||
const editor = useEditor(Object.assign(Object.assign({}, defaultOptions), restOptions), deps);
|
||||
useEffect(() => {
|
||||
setIsMobile(isMobile || false);
|
||||
}, [isMobile]);
|
||||
return editor;
|
||||
};
|
||||
export { useTiptap, Toolbar };
|
||||
export { useTiptap, Toolbar, usePermissionHandler };
|
||||
export * from "./types";
|
||||
export * from "./extensions/react";
|
||||
export * from "./toolbar";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// <reference types="react" />
|
||||
import { ToolbarGroupDefinition, ToolButtonVariant } from "../types";
|
||||
import { FlexProps } from "rebass";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { NodeWithOffset } from "../utils/prosemirror";
|
||||
export declare type ToolbarGroupProps = FlexProps & {
|
||||
tools: ToolbarGroupDefinition;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="react" />
|
||||
import { ImageAlignmentOptions, ImageSizeOptions } from "../../extensions/image";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
export declare type ImagePropertiesProps = ImageSizeOptions & ImageAlignmentOptions & {
|
||||
editor: Editor;
|
||||
onClose: () => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference types="react" />
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
export declare type SearchReplacePopupProps = {
|
||||
editor: Editor;
|
||||
};
|
||||
|
||||
2
packages/editor/dist/es/toolbar/toolbar.d.ts
vendored
2
packages/editor/dist/es/toolbar/toolbar.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="react" />
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../types";
|
||||
import { ToolbarLocation } from "./stores/toolbar-store";
|
||||
import { ToolbarDefinition } from "./types";
|
||||
declare type ToolbarProps = {
|
||||
|
||||
@@ -210,7 +210,8 @@ const uploadImageFromURLMobile = (editor) => ({
|
||||
type: "popup",
|
||||
component: ({ onClick }) => (_jsx(ImageUploadPopup, { onInsert: (image) => {
|
||||
var _a;
|
||||
(_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
(_a = editor
|
||||
.requestPermission("insertImage")) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
onClick === null || onClick === void 0 ? void 0 : onClick();
|
||||
}, onClose: () => {
|
||||
onClick === null || onClick === void 0 ? void 0 : onClick();
|
||||
@@ -225,13 +226,12 @@ const uploadImageFromURL = (editor) => ({
|
||||
title: "Attach from URL",
|
||||
icon: "link",
|
||||
onClick: () => {
|
||||
if (!editor)
|
||||
return;
|
||||
showPopup({
|
||||
theme: editor.storage.theme,
|
||||
popup: (hide) => (_jsx(ImageUploadPopup, { onInsert: (image) => {
|
||||
var _a;
|
||||
(_a = editor.current) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
(_a = editor
|
||||
.requestPermission("insertImage")) === null || _a === void 0 ? void 0 : _a.chain().focus().insertImage(image).run();
|
||||
hide();
|
||||
}, onClose: hide })),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference types="react" />
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { MenuButton } from "../../components/menu/types";
|
||||
import { ToolProps } from "../types";
|
||||
export declare function menuButtonToTool(constructItem: (editor: Editor) => MenuButton): (props: ToolProps) => JSX.Element;
|
||||
|
||||
15
packages/editor/dist/es/types.d.ts
vendored
15
packages/editor/dist/es/types.d.ts
vendored
@@ -1,9 +1,20 @@
|
||||
import { Editor as TiptapEditor } from "@tiptap/core";
|
||||
export interface Editor extends TiptapEditor {
|
||||
import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
|
||||
export interface PermissionRequestEvent extends CustomEvent<{
|
||||
id: keyof UnionCommands;
|
||||
}> {
|
||||
}
|
||||
export declare class Editor extends TiptapEditor {
|
||||
/**
|
||||
* Use this to get the latest instance of the editor.
|
||||
* This is required to reduce unnecessary rerenders of
|
||||
* toolbar elements.
|
||||
*/
|
||||
current?: TiptapEditor;
|
||||
/**
|
||||
* Request permission before executing a command to make sure user
|
||||
* is allowed to perform the action.
|
||||
* @param id the command id to get permission for
|
||||
* @returns latest editor instance
|
||||
*/
|
||||
requestPermission(id: keyof UnionCommands): TiptapEditor | undefined;
|
||||
}
|
||||
|
||||
19
packages/editor/dist/es/types.js
vendored
19
packages/editor/dist/es/types.js
vendored
@@ -1 +1,18 @@
|
||||
export {};
|
||||
import { Editor as TiptapEditor } from "@tiptap/core";
|
||||
export class Editor extends TiptapEditor {
|
||||
/**
|
||||
* Request permission before executing a command to make sure user
|
||||
* is allowed to perform the action.
|
||||
* @param id the command id to get permission for
|
||||
* @returns latest editor instance
|
||||
*/
|
||||
requestPermission(id) {
|
||||
const event = new CustomEvent("permissionrequest", {
|
||||
detail: { id },
|
||||
cancelable: true,
|
||||
});
|
||||
if (!window.dispatchEvent(event))
|
||||
return undefined;
|
||||
return this.current;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
ForwardRef,
|
||||
ContentDOM,
|
||||
} from "./types";
|
||||
import { Editor, NodeViewRendererProps } from "@tiptap/core";
|
||||
import { NodeViewRendererProps } from "@tiptap/core";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { ThemeProvider } from "emotion-theming";
|
||||
// @ts-ignore
|
||||
import { __serializeForClipboard } from "prosemirror-view";
|
||||
import { Editor } from "../../types";
|
||||
|
||||
export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
||||
private domRef!: HTMLElement;
|
||||
@@ -431,7 +432,7 @@ export function createNodeView<TProps extends ReactNodeViewProps>(
|
||||
return ({ node, getPos, editor }: NodeViewRendererProps) => {
|
||||
const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos());
|
||||
|
||||
return new ReactNodeView<TProps>(node, editor, _getPos, {
|
||||
return new ReactNodeView<TProps>(node, editor as Editor, _getPos, {
|
||||
...options,
|
||||
component,
|
||||
}).init();
|
||||
|
||||
@@ -16,9 +16,10 @@ import {
|
||||
ForwardRef,
|
||||
} from "./types";
|
||||
import { ReactNodeView } from "./react-node-view";
|
||||
import { Editor, NodeViewRendererProps } from "@tiptap/core";
|
||||
import { NodeViewRendererProps } from "@tiptap/core";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { ThemeProvider } from "emotion-theming";
|
||||
import { Editor } from "../../types";
|
||||
|
||||
/**
|
||||
* A ReactNodeView that handles React components sensitive
|
||||
@@ -251,7 +252,7 @@ export function createSelectionBasedNodeView<
|
||||
) {
|
||||
return ({ node, getPos, editor }: NodeViewRendererProps) => {
|
||||
const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos());
|
||||
return new SelectionBasedNodeView(node, editor, _getPos, {
|
||||
return new SelectionBasedNodeView(node, editor as Editor, _getPos, {
|
||||
...options,
|
||||
component,
|
||||
}).init();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { Node as PMNode, Attrs } from "prosemirror-model";
|
||||
|
||||
export interface ReactNodeProps {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
SelectionBasedReactNodeViewProps,
|
||||
} from "../react";
|
||||
import { Node as ProsemirrorNode } from "prosemirror-model";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { updateColumnsOnResize } from "@_ueberdosis/prosemirror-tables";
|
||||
import { NodeView } from "prosemirror-view";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Table as TiptapTable, TableOptions } from "@tiptap/extension-table";
|
||||
import { columnResizing, tableEditing } from "@_ueberdosis/prosemirror-tables";
|
||||
import { Editor } from "../../types";
|
||||
import { TableNodeView } from "./component";
|
||||
|
||||
export const Table = TiptapTable.extend<TableOptions>({
|
||||
@@ -12,7 +13,7 @@ export const Table = TiptapTable.extend<TableOptions>({
|
||||
columnResizing({
|
||||
handleWidth: this.options.handleWidth,
|
||||
cellMinWidth: this.options.cellMinWidth,
|
||||
View: TableNodeView(this.editor),
|
||||
View: TableNodeView(this.editor as Editor),
|
||||
// TODO: PR for @types/prosemirror-tables
|
||||
// @ts-ignore (incorrect type)
|
||||
lastColumnResizable: this.options.lastColumnResizable,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditorOptions, Editor } from "@tiptap/core";
|
||||
import { EditorOptions } from "@tiptap/core";
|
||||
import { DependencyList, useEffect, useRef, useState } from "react";
|
||||
import { Editor as EditorType } from "../types";
|
||||
import { Editor } from "../types";
|
||||
|
||||
function useForceUpdate() {
|
||||
const [, setValue] = useState(0);
|
||||
@@ -12,9 +12,9 @@ export const useEditor = (
|
||||
options: Partial<EditorOptions> = {},
|
||||
deps: DependencyList = []
|
||||
) => {
|
||||
const [editor, setEditor] = useState<EditorType | null>(null);
|
||||
const [editor, setEditor] = useState<Editor | null>(null);
|
||||
const forceUpdate = useForceUpdate();
|
||||
const editorRef = useRef<EditorType | null>(editor);
|
||||
const editorRef = useRef<Editor | null>(editor);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
@@ -42,10 +42,21 @@ export const useEditor = (
|
||||
useEffect(() => {
|
||||
editorRef.current = editor;
|
||||
|
||||
if (editor && !editor.current)
|
||||
if (!editor) return;
|
||||
|
||||
if (!editor.current) {
|
||||
Object.defineProperty(editor, "current", {
|
||||
get: () => editorRef.current,
|
||||
});
|
||||
}
|
||||
// if (!editor.executor) {
|
||||
// Object.defineProperty(editor, "executor", {
|
||||
// get: () => (id?: string) => {
|
||||
// console.log(id);
|
||||
// return editorRef.current;
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
43
packages/editor/src/hooks/use-permission-handler.ts
Normal file
43
packages/editor/src/hooks/use-permission-handler.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ChainedCommands, UnionCommands } from "@tiptap/core";
|
||||
import { useEffect } from "react";
|
||||
import { PermissionRequestEvent } from "../types";
|
||||
|
||||
export type Claims = "premium";
|
||||
export type PermissionHandlerOptions = {
|
||||
claims: Record<Claims, boolean>;
|
||||
onPermissionDenied: (claim: Claims, id: keyof UnionCommands) => void;
|
||||
};
|
||||
|
||||
const ClaimsMap: Record<Claims, (keyof UnionCommands)[]> = {
|
||||
premium: ["insertImage"],
|
||||
};
|
||||
|
||||
export function usePermissionHandler(options: PermissionHandlerOptions) {
|
||||
const { claims, onPermissionDenied } = options;
|
||||
|
||||
useEffect(() => {
|
||||
function onPermissionRequested(ev: Event) {
|
||||
const {
|
||||
detail: { id },
|
||||
} = ev as PermissionRequestEvent;
|
||||
|
||||
for (const key in ClaimsMap) {
|
||||
const claim = key as Claims;
|
||||
const commands = ClaimsMap[claim];
|
||||
|
||||
if (commands.indexOf(id) <= -1) continue;
|
||||
if (claims[claim]) continue;
|
||||
|
||||
onPermissionDenied(claim, id);
|
||||
ev.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
}
|
||||
window.addEventListener("permissionrequest", onPermissionRequested);
|
||||
return () => {
|
||||
window.removeEventListener("permissionrequest", onPermissionRequested);
|
||||
};
|
||||
}, [claims, onPermissionDenied]);
|
||||
}
|
||||
@@ -40,9 +40,10 @@ import {
|
||||
import { OutlineList } from "./extensions/outline-list";
|
||||
import { OutlineListItem } from "./extensions/outline-list-item";
|
||||
import { Table } from "./extensions/table";
|
||||
import { useIsMobile, useToolbarStore } from "./toolbar/stores/toolbar-store";
|
||||
import { useToolbarStore } from "./toolbar/stores/toolbar-store";
|
||||
import { useEditor } from "./hooks/use-editor";
|
||||
import { EditorOptions } from "@tiptap/core";
|
||||
import { usePermissionHandler } from "./hooks/use-permission-handler";
|
||||
|
||||
EditorView.prototype.updateState = function updateState(state) {
|
||||
if (!(this as any).docView) return; // This prevents the matchesNode error on hot reloads
|
||||
@@ -50,7 +51,10 @@ EditorView.prototype.updateState = function updateState(state) {
|
||||
};
|
||||
|
||||
type TiptapOptions = EditorOptions &
|
||||
AttachmentOptions & { theme: Theme; isMobile?: boolean };
|
||||
AttachmentOptions & {
|
||||
theme: Theme;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
const useTiptap = (
|
||||
options: Partial<TiptapOptions> = {},
|
||||
@@ -66,6 +70,10 @@ const useTiptap = (
|
||||
const PortalProviderAPI = usePortalProvider();
|
||||
const setIsMobile = useToolbarStore((store) => store.setIsMobile);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(isMobile || false);
|
||||
}, [isMobile]);
|
||||
|
||||
const defaultOptions = useMemo<Partial<EditorOptions>>(
|
||||
() => ({
|
||||
extensions: [
|
||||
@@ -158,14 +166,10 @@ const useTiptap = (
|
||||
deps
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(isMobile || false);
|
||||
}, [isMobile]);
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
export { useTiptap, Toolbar };
|
||||
export { useTiptap, Toolbar, usePermissionHandler };
|
||||
export * from "./types";
|
||||
export * from "./extensions/react";
|
||||
export * from "./toolbar";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ToolbarGroupDefinition, ToolButtonVariant } from "../types";
|
||||
import { findTool } from "../tools";
|
||||
import { Flex, FlexProps } from "rebass";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { MoreTools } from "./more-tools";
|
||||
import { getToolDefinition } from "../tool-definitions";
|
||||
import { NodeWithOffset } from "../utils/prosemirror";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ImageAlignmentOptions,
|
||||
ImageSizeOptions,
|
||||
} from "../../extensions/image";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { InlineInput } from "../../components/inline-input";
|
||||
|
||||
export type ImagePropertiesProps = ImageSizeOptions &
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Flex, Text } from "rebass";
|
||||
import { SearchStorage } from "../../extensions/search-replace";
|
||||
import { ToolButton } from "../components/tool-button";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
|
||||
export type SearchReplacePopupProps = { editor: Editor };
|
||||
export function SearchReplacePopup(props: SearchReplacePopupProps) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Theme, useTheme } from "@notesnook/theme";
|
||||
import { ThemeConfig } from "@notesnook/theme/dist/theme/types";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../types";
|
||||
import { Flex, FlexProps } from "rebass";
|
||||
import { findTool, ToolId } from "./tools";
|
||||
import { ThemeProvider } from "emotion-theming";
|
||||
|
||||
@@ -264,7 +264,12 @@ const uploadImageFromURLMobile = (editor: Editor): MenuItem => ({
|
||||
component: ({ onClick }) => (
|
||||
<ImageUploadPopup
|
||||
onInsert={(image) => {
|
||||
editor.current?.chain().focus().insertImage(image).run();
|
||||
editor
|
||||
.requestPermission("insertImage")
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertImage(image)
|
||||
.run();
|
||||
onClick?.();
|
||||
}}
|
||||
onClose={() => {
|
||||
@@ -283,13 +288,17 @@ const uploadImageFromURL = (editor: Editor): MenuItem => ({
|
||||
title: "Attach from URL",
|
||||
icon: "link",
|
||||
onClick: () => {
|
||||
if (!editor) return;
|
||||
showPopup({
|
||||
theme: editor.storage.theme,
|
||||
popup: (hide) => (
|
||||
<ImageUploadPopup
|
||||
onInsert={(image) => {
|
||||
editor.current?.chain().focus().insertImage(image).run();
|
||||
editor
|
||||
.requestPermission("insertImage")
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertImage(image)
|
||||
.run();
|
||||
hide();
|
||||
}}
|
||||
onClose={hide}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import React, { useState } from "react";
|
||||
import tinycolor from "tinycolor2";
|
||||
import { PopupWrapper } from "../../components/popup-presenter";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ToolProps } from "../types";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Box, Button, Flex } from "rebass";
|
||||
import { IconNames } from "../icons";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Editor } from "../../types";
|
||||
import { MenuButton } from "../../components/menu/types";
|
||||
import { ToolButton } from "../components/tool-button";
|
||||
import { ToolProps } from "../types";
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import { Editor as TiptapEditor } from "@tiptap/core";
|
||||
export interface Editor extends TiptapEditor {
|
||||
import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
|
||||
|
||||
export interface PermissionRequestEvent
|
||||
extends CustomEvent<{ id: keyof UnionCommands }> {}
|
||||
|
||||
export class Editor extends TiptapEditor {
|
||||
/**
|
||||
* Use this to get the latest instance of the editor.
|
||||
* This is required to reduce unnecessary rerenders of
|
||||
* toolbar elements.
|
||||
*/
|
||||
current?: TiptapEditor;
|
||||
|
||||
/**
|
||||
* Request permission before executing a command to make sure user
|
||||
* is allowed to perform the action.
|
||||
* @param id the command id to get permission for
|
||||
* @returns latest editor instance
|
||||
*/
|
||||
requestPermission(id: keyof UnionCommands): TiptapEditor | undefined {
|
||||
const event = new CustomEvent("permissionrequest", {
|
||||
detail: { id },
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
if (!window.dispatchEvent(event)) return undefined;
|
||||
|
||||
return this.current;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,3 +113,21 @@ What's next:
|
||||
|
||||
1. Keyboard shouldn't close on tool click
|
||||
2. Handle context toolbar menus on scroll
|
||||
|
||||
## Premium restriction
|
||||
|
||||
1. `isAuthorized` method on the editor
|
||||
2. `execCommand` method on the editor that emits an event (onBeforeExecCommand)
|
||||
|
||||
Requirements:
|
||||
|
||||
1. We will need to check the user status sometimes outside the editor commands
|
||||
2. For this reason, we shouldn't attach this to the editor
|
||||
3. Or if we do, we shouldn't make it command specific.
|
||||
4. Secondly, we need a way to authorize user actions without spreading it all over the codebase.
|
||||
5. One option is to put it in the base buttons etc. and pass an "isPro" prop
|
||||
1. But I don't like this because not all buttons need it.
|
||||
6. What we should do it emit an event before each command
|
||||
1. We can have a `requestPermission` method on the editor that sends an id
|
||||
2. This can be used to allow/deny user actions
|
||||
7. And also have a function to check authorization for non-command actions.
|
||||
|
||||
Reference in New Issue
Block a user