feat: add permission handling

This commit is contained in:
thecodrr
2022-07-04 13:00:13 +05:00
parent b3a5444c91
commit 9f2e57b61a
54 changed files with 352 additions and 81 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/// <reference types="react" />
import { Editor } from "@tiptap/core";
import { Editor } from "../../types";
export declare type SearchReplacePopupProps = {
editor: Editor;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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]);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/// <reference types="react" />
import { Editor } from "@tiptap/core";
import { Editor } from "../../types";
export declare type SearchReplacePopupProps = {
editor: Editor;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Editor } from "@tiptap/core";
import { Editor } from "../../types";
import { Node as PMNode, Attrs } from "prosemirror-model";
export interface ReactNodeProps {

View File

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

View File

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

View File

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

View 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]);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { Editor } from "@tiptap/core";
import React, { useState } from "react";
import tinycolor from "tinycolor2";
import { PopupWrapper } from "../../components/popup-presenter";

View File

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

View File

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

View File

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

View File

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