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 { Node as PMNode } from "prosemirror-model";
import { PortalProviderAPI } from "./react-portal-provider"; import { PortalProviderAPI } from "./react-portal-provider";
import { ReactNodeViewProps, ReactNodeViewOptions, GetPosNode, ForwardRef, ContentDOM } from "./types"; 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 { export declare class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
protected readonly editor: Editor; protected readonly editor: Editor;
protected readonly getPos: GetPosNode; protected readonly getPos: GetPosNode;

View File

@@ -2,7 +2,8 @@ import React from "react";
import { Node as PMNode } from "prosemirror-model"; import { Node as PMNode } from "prosemirror-model";
import { ReactNodeViewOptions, GetPosNode, SelectionBasedReactNodeViewProps, ForwardRef } from "./types"; import { ReactNodeViewOptions, GetPosNode, SelectionBasedReactNodeViewProps, ForwardRef } from "./types";
import { ReactNodeView } from "./react-node-view"; 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 * A ReactNodeView that handles React components sensitive
* to selection changes. * to selection changes.

View File

@@ -1,5 +1,5 @@
/// <reference types="react" /> /// <reference types="react" />
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { Node as PMNode, Attrs } from "prosemirror-model"; import { Node as PMNode, Attrs } from "prosemirror-model";
export interface ReactNodeProps { export interface ReactNodeProps {
selected: boolean; selected: boolean;

View File

@@ -1,6 +1,6 @@
/// <reference types="react" /> /// <reference types="react" />
import { SelectionBasedReactNodeViewProps } from "../react"; import { SelectionBasedReactNodeViewProps } from "../react";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { NodeView } from "prosemirror-view"; import { NodeView } from "prosemirror-view";
export declare function TableComponent(props: SelectionBasedReactNodeViewProps): JSX.Element; export declare function TableComponent(props: SelectionBasedReactNodeViewProps): JSX.Element;
export declare function TableNodeView(editor: Editor): NodeView; export declare function TableNodeView(editor: Editor): NodeView;

View File

@@ -1,4 +1,4 @@
import { EditorOptions } from "@tiptap/core"; import { EditorOptions } from "@tiptap/core";
import { DependencyList } from "react"; import { DependencyList } from "react";
import { Editor as EditorType } from "../types"; import { Editor } from "../types";
export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => EditorType | null; export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => Editor | null;

View File

@@ -1,8 +1,8 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.useEditor = void 0; exports.useEditor = void 0;
const core_1 = require("@tiptap/core");
const react_1 = require("react"); const react_1 = require("react");
const types_1 = require("../types");
function useForceUpdate() { function useForceUpdate() {
const [, setValue] = (0, react_1.useState)(0); const [, setValue] = (0, react_1.useState)(0);
return () => setValue((value) => value + 1); return () => setValue((value) => value + 1);
@@ -13,7 +13,7 @@ const useEditor = (options = {}, deps = []) => {
const editorRef = (0, react_1.useRef)(editor); const editorRef = (0, react_1.useRef)(editor);
(0, react_1.useEffect)(() => { (0, react_1.useEffect)(() => {
let isMounted = true; let isMounted = true;
const instance = new core_1.Editor(options); const instance = new types_1.Editor(options);
setEditor(instance); setEditor(instance);
instance.on("transaction", () => { instance.on("transaction", () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -31,10 +31,21 @@ const useEditor = (options = {}, deps = []) => {
}, deps); }, deps);
(0, react_1.useEffect)(() => { (0, react_1.useEffect)(() => {
editorRef.current = editor; editorRef.current = editor;
if (editor && !editor.current) if (!editor)
return;
if (!editor.current) {
Object.defineProperty(editor, "current", { Object.defineProperty(editor, "current", {
get: () => editorRef.current, get: () => editorRef.current,
}); });
}
// if (!editor.executor) {
// Object.defineProperty(editor, "executor", {
// get: () => (id?: string) => {
// console.log(id);
// return editorRef.current;
// },
// });
// }
}, [editor]); }, [editor]);
(0, react_1.useEffect)(() => { (0, react_1.useEffect)(() => {
// this is required for the drag/drop to work properly // 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 { Theme } from "@notesnook/theme";
import { AttachmentOptions } from "./extensions/attachment"; import { AttachmentOptions } from "./extensions/attachment";
import { EditorOptions } from "@tiptap/core"; import { EditorOptions } from "@tiptap/core";
import { usePermissionHandler } from "./hooks/use-permission-handler";
declare type TiptapOptions = EditorOptions & AttachmentOptions & { declare type TiptapOptions = EditorOptions & AttachmentOptions & {
theme: Theme; theme: Theme;
isMobile?: boolean; isMobile?: boolean;
}; };
declare const useTiptap: (options?: Partial<TiptapOptions>, deps?: import("react").DependencyList) => import("./types").Editor | null; 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 "./types";
export * from "./extensions/react"; export * from "./extensions/react";
export * from "./toolbar"; export * from "./toolbar";

View File

@@ -28,7 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.Toolbar = exports.useTiptap = void 0; exports.usePermissionHandler = exports.Toolbar = exports.useTiptap = void 0;
require("./extensions"); require("./extensions");
const extension_character_count_1 = __importDefault(require("@tiptap/extension-character-count")); const extension_character_count_1 = __importDefault(require("@tiptap/extension-character-count"));
const extension_placeholder_1 = __importDefault(require("@tiptap/extension-placeholder")); 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 table_1 = require("./extensions/table");
const toolbarstore_1 = require("./toolbar/stores/toolbarstore"); const toolbarstore_1 = require("./toolbar/stores/toolbarstore");
const useEditor_1 = require("./hooks/useEditor"); 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) { prosemirror_view_1.EditorView.prototype.updateState = function updateState(state) {
if (!this.docView) if (!this.docView)
return; // This prevents the matchesNode error on hot reloads 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 { theme, isMobile, onDownloadAttachment, onOpenAttachmentPicker } = options, restOptions = __rest(options, ["theme", "isMobile", "onDownloadAttachment", "onOpenAttachmentPicker"]);
const PortalProviderAPI = (0, react_2.usePortalProvider)(); const PortalProviderAPI = (0, react_2.usePortalProvider)();
const setIsMobile = (0, toolbarstore_1.useToolbarStore)((store) => store.setIsMobile); const setIsMobile = (0, toolbarstore_1.useToolbarStore)((store) => store.setIsMobile);
(0, react_1.useEffect)(() => {
setIsMobile(isMobile || false);
}, [isMobile]);
const defaultOptions = (0, react_1.useMemo)(() => ({ const defaultOptions = (0, react_1.useMemo)(() => ({
extensions: [ extensions: [
react_2.NodeViewSelectionNotifier, react_2.NodeViewSelectionNotifier,
@@ -159,9 +164,6 @@ const useTiptap = (options = {}, deps = []) => {
isMobile, isMobile,
]); ]);
const editor = (0, useEditor_1.useEditor)(Object.assign(Object.assign({}, defaultOptions), restOptions), deps); const editor = (0, useEditor_1.useEditor)(Object.assign(Object.assign({}, defaultOptions), restOptions), deps);
(0, react_1.useEffect)(() => {
setIsMobile(isMobile || false);
}, [isMobile]);
return editor; return editor;
}; };
exports.useTiptap = useTiptap; exports.useTiptap = useTiptap;

View File

@@ -1,7 +1,7 @@
/// <reference types="react" /> /// <reference types="react" />
import { ToolbarGroupDefinition, ToolButtonVariant } from "../types"; import { ToolbarGroupDefinition, ToolButtonVariant } from "../types";
import { FlexProps } from "rebass"; import { FlexProps } from "rebass";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { NodeWithOffset } from "../utils/prosemirror"; import { NodeWithOffset } from "../utils/prosemirror";
export declare type ToolbarGroupProps = FlexProps & { export declare type ToolbarGroupProps = FlexProps & {
tools: ToolbarGroupDefinition; tools: ToolbarGroupDefinition;

View File

@@ -1,6 +1,6 @@
/// <reference types="react" /> /// <reference types="react" />
import { ImageAlignmentOptions, ImageSizeOptions } from "../../extensions/image"; import { ImageAlignmentOptions, ImageSizeOptions } from "../../extensions/image";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
export declare type ImagePropertiesProps = ImageSizeOptions & ImageAlignmentOptions & { export declare type ImagePropertiesProps = ImageSizeOptions & ImageAlignmentOptions & {
editor: Editor; editor: Editor;
onClose: () => void; onClose: () => void;

View File

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

View File

@@ -1,6 +1,6 @@
/// <reference types="react" /> /// <reference types="react" />
import { Theme } from "@notesnook/theme"; import { Theme } from "@notesnook/theme";
import { Editor } from "@tiptap/core"; import { Editor } from "../types";
import { ToolbarLocation } from "./stores/toolbar-store"; import { ToolbarLocation } from "./stores/toolbar-store";
import { ToolbarDefinition } from "./types"; import { ToolbarDefinition } from "./types";
declare type ToolbarProps = { declare type ToolbarProps = {

View File

@@ -214,7 +214,8 @@ const uploadImageFromURLMobile = (editor) => ({
type: "popup", type: "popup",
component: ({ onClick }) => ((0, jsx_runtime_1.jsx)(imageupload_1.ImageUploadPopup, { onInsert: (image) => { component: ({ onClick }) => ((0, jsx_runtime_1.jsx)(imageupload_1.ImageUploadPopup, { onInsert: (image) => {
var _a; 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(); onClick === null || onClick === void 0 ? void 0 : onClick();
}, onClose: () => { }, onClose: () => {
onClick === null || onClick === void 0 ? void 0 : onClick(); onClick === null || onClick === void 0 ? void 0 : onClick();
@@ -229,13 +230,12 @@ const uploadImageFromURL = (editor) => ({
title: "Attach from URL", title: "Attach from URL",
icon: "link", icon: "link",
onClick: () => { onClick: () => {
if (!editor)
return;
(0, popuppresenter_1.showPopup)({ (0, popuppresenter_1.showPopup)({
theme: editor.storage.theme, theme: editor.storage.theme,
popup: (hide) => ((0, jsx_runtime_1.jsx)(imageupload_1.ImageUploadPopup, { onInsert: (image) => { popup: (hide) => ((0, jsx_runtime_1.jsx)(imageupload_1.ImageUploadPopup, { onInsert: (image) => {
var _a; 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(); hide();
}, onClose: hide })), }, onClose: hide })),
}); });

View File

@@ -1,5 +1,5 @@
/// <reference types="react" /> /// <reference types="react" />
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { MenuButton } from "../../components/menu/types"; import { MenuButton } from "../../components/menu/types";
import { ToolProps } from "../types"; import { ToolProps } from "../types";
export declare function menuButtonToTool(constructItem: (editor: Editor) => MenuButton): (props: ToolProps) => JSX.Element; 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"; import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
export interface Editor extends TiptapEditor { export interface PermissionRequestEvent extends CustomEvent<{
id: keyof UnionCommands;
}> {
}
export declare class Editor extends TiptapEditor {
/** /**
* Use this to get the latest instance of the editor. * Use this to get the latest instance of the editor.
* This is required to reduce unnecessary rerenders of * This is required to reduce unnecessary rerenders of
* toolbar elements. * toolbar elements.
*/ */
current?: TiptapEditor; 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"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); 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 { Node as PMNode } from "prosemirror-model";
import { PortalProviderAPI } from "./react-portal-provider"; import { PortalProviderAPI } from "./react-portal-provider";
import { ReactNodeViewProps, ReactNodeViewOptions, GetPosNode, ForwardRef, ContentDOM } from "./types"; 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 { export declare class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
protected readonly editor: Editor; protected readonly editor: Editor;
protected readonly getPos: GetPosNode; protected readonly getPos: GetPosNode;

View File

@@ -2,7 +2,8 @@ import React from "react";
import { Node as PMNode } from "prosemirror-model"; import { Node as PMNode } from "prosemirror-model";
import { ReactNodeViewOptions, GetPosNode, SelectionBasedReactNodeViewProps, ForwardRef } from "./types"; import { ReactNodeViewOptions, GetPosNode, SelectionBasedReactNodeViewProps, ForwardRef } from "./types";
import { ReactNodeView } from "./react-node-view"; 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 * A ReactNodeView that handles React components sensitive
* to selection changes. * to selection changes.

View File

@@ -1,5 +1,5 @@
/// <reference types="react" /> /// <reference types="react" />
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { Node as PMNode, Attrs } from "prosemirror-model"; import { Node as PMNode, Attrs } from "prosemirror-model";
export interface ReactNodeProps { export interface ReactNodeProps {
selected: boolean; selected: boolean;

View File

@@ -1,6 +1,6 @@
/// <reference types="react" /> /// <reference types="react" />
import { SelectionBasedReactNodeViewProps } from "../react"; import { SelectionBasedReactNodeViewProps } from "../react";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { NodeView } from "prosemirror-view"; import { NodeView } from "prosemirror-view";
export declare function TableComponent(props: SelectionBasedReactNodeViewProps): JSX.Element; export declare function TableComponent(props: SelectionBasedReactNodeViewProps): JSX.Element;
export declare function TableNodeView(editor: Editor): NodeView; export declare function TableNodeView(editor: Editor): NodeView;

View File

@@ -1,4 +1,4 @@
import { EditorOptions } from "@tiptap/core"; import { EditorOptions } from "@tiptap/core";
import { DependencyList } from "react"; import { DependencyList } from "react";
import { Editor as EditorType } from "../types"; import { Editor } from "../types";
export declare const useEditor: (options?: Partial<EditorOptions>, deps?: DependencyList) => EditorType | null; 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 { useEffect, useRef, useState } from "react";
import { Editor } from "../types";
function useForceUpdate() { function useForceUpdate() {
const [, setValue] = useState(0); const [, setValue] = useState(0);
return () => setValue((value) => value + 1); return () => setValue((value) => value + 1);
@@ -28,10 +28,21 @@ export const useEditor = (options = {}, deps = []) => {
}, deps); }, deps);
useEffect(() => { useEffect(() => {
editorRef.current = editor; editorRef.current = editor;
if (editor && !editor.current) if (!editor)
return;
if (!editor.current) {
Object.defineProperty(editor, "current", { Object.defineProperty(editor, "current", {
get: () => editorRef.current, get: () => editorRef.current,
}); });
}
// if (!editor.executor) {
// Object.defineProperty(editor, "executor", {
// get: () => (id?: string) => {
// console.log(id);
// return editorRef.current;
// },
// });
// }
}, [editor]); }, [editor]);
useEffect(() => { useEffect(() => {
// this is required for the drag/drop to work properly // 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 { Theme } from "@notesnook/theme";
import { AttachmentOptions } from "./extensions/attachment"; import { AttachmentOptions } from "./extensions/attachment";
import { EditorOptions } from "@tiptap/core"; import { EditorOptions } from "@tiptap/core";
import { usePermissionHandler } from "./hooks/use-permission-handler";
declare type TiptapOptions = EditorOptions & AttachmentOptions & { declare type TiptapOptions = EditorOptions & AttachmentOptions & {
theme: Theme; theme: Theme;
isMobile?: boolean; isMobile?: boolean;
}; };
declare const useTiptap: (options?: Partial<TiptapOptions>, deps?: import("react").DependencyList) => import("./types").Editor | null; 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 "./types";
export * from "./extensions/react"; export * from "./extensions/react";
export * from "./toolbar"; export * from "./toolbar";

View File

@@ -49,6 +49,7 @@ import { OutlineListItem } from "./extensions/outline-list-item";
import { Table } from "./extensions/table"; import { Table } from "./extensions/table";
import { useToolbarStore } from "./toolbar/stores/toolbar-store"; import { useToolbarStore } from "./toolbar/stores/toolbar-store";
import { useEditor } from "./hooks/use-editor"; import { useEditor } from "./hooks/use-editor";
import { usePermissionHandler } from "./hooks/use-permission-handler";
EditorView.prototype.updateState = function updateState(state) { EditorView.prototype.updateState = function updateState(state) {
if (!this.docView) if (!this.docView)
return; // This prevents the matchesNode error on hot reloads 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 { theme, isMobile, onDownloadAttachment, onOpenAttachmentPicker } = options, restOptions = __rest(options, ["theme", "isMobile", "onDownloadAttachment", "onOpenAttachmentPicker"]);
const PortalProviderAPI = usePortalProvider(); const PortalProviderAPI = usePortalProvider();
const setIsMobile = useToolbarStore((store) => store.setIsMobile); const setIsMobile = useToolbarStore((store) => store.setIsMobile);
useEffect(() => {
setIsMobile(isMobile || false);
}, [isMobile]);
const defaultOptions = useMemo(() => ({ const defaultOptions = useMemo(() => ({
extensions: [ extensions: [
NodeViewSelectionNotifier, NodeViewSelectionNotifier,
@@ -138,12 +142,9 @@ const useTiptap = (options = {}, deps = []) => {
isMobile, isMobile,
]); ]);
const editor = useEditor(Object.assign(Object.assign({}, defaultOptions), restOptions), deps); const editor = useEditor(Object.assign(Object.assign({}, defaultOptions), restOptions), deps);
useEffect(() => {
setIsMobile(isMobile || false);
}, [isMobile]);
return editor; return editor;
}; };
export { useTiptap, Toolbar }; export { useTiptap, Toolbar, usePermissionHandler };
export * from "./types"; export * from "./types";
export * from "./extensions/react"; export * from "./extensions/react";
export * from "./toolbar"; export * from "./toolbar";

View File

@@ -1,7 +1,7 @@
/// <reference types="react" /> /// <reference types="react" />
import { ToolbarGroupDefinition, ToolButtonVariant } from "../types"; import { ToolbarGroupDefinition, ToolButtonVariant } from "../types";
import { FlexProps } from "rebass"; import { FlexProps } from "rebass";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { NodeWithOffset } from "../utils/prosemirror"; import { NodeWithOffset } from "../utils/prosemirror";
export declare type ToolbarGroupProps = FlexProps & { export declare type ToolbarGroupProps = FlexProps & {
tools: ToolbarGroupDefinition; tools: ToolbarGroupDefinition;

View File

@@ -1,6 +1,6 @@
/// <reference types="react" /> /// <reference types="react" />
import { ImageAlignmentOptions, ImageSizeOptions } from "../../extensions/image"; import { ImageAlignmentOptions, ImageSizeOptions } from "../../extensions/image";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
export declare type ImagePropertiesProps = ImageSizeOptions & ImageAlignmentOptions & { export declare type ImagePropertiesProps = ImageSizeOptions & ImageAlignmentOptions & {
editor: Editor; editor: Editor;
onClose: () => void; onClose: () => void;

View File

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

View File

@@ -1,6 +1,6 @@
/// <reference types="react" /> /// <reference types="react" />
import { Theme } from "@notesnook/theme"; import { Theme } from "@notesnook/theme";
import { Editor } from "@tiptap/core"; import { Editor } from "../types";
import { ToolbarLocation } from "./stores/toolbar-store"; import { ToolbarLocation } from "./stores/toolbar-store";
import { ToolbarDefinition } from "./types"; import { ToolbarDefinition } from "./types";
declare type ToolbarProps = { declare type ToolbarProps = {

View File

@@ -210,7 +210,8 @@ const uploadImageFromURLMobile = (editor) => ({
type: "popup", type: "popup",
component: ({ onClick }) => (_jsx(ImageUploadPopup, { onInsert: (image) => { component: ({ onClick }) => (_jsx(ImageUploadPopup, { onInsert: (image) => {
var _a; 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(); onClick === null || onClick === void 0 ? void 0 : onClick();
}, onClose: () => { }, onClose: () => {
onClick === null || onClick === void 0 ? void 0 : onClick(); onClick === null || onClick === void 0 ? void 0 : onClick();
@@ -225,13 +226,12 @@ const uploadImageFromURL = (editor) => ({
title: "Attach from URL", title: "Attach from URL",
icon: "link", icon: "link",
onClick: () => { onClick: () => {
if (!editor)
return;
showPopup({ showPopup({
theme: editor.storage.theme, theme: editor.storage.theme,
popup: (hide) => (_jsx(ImageUploadPopup, { onInsert: (image) => { popup: (hide) => (_jsx(ImageUploadPopup, { onInsert: (image) => {
var _a; 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(); hide();
}, onClose: hide })), }, onClose: hide })),
}); });

View File

@@ -1,5 +1,5 @@
/// <reference types="react" /> /// <reference types="react" />
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { MenuButton } from "../../components/menu/types"; import { MenuButton } from "../../components/menu/types";
import { ToolProps } from "../types"; import { ToolProps } from "../types";
export declare function menuButtonToTool(constructItem: (editor: Editor) => MenuButton): (props: ToolProps) => JSX.Element; 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"; import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
export interface Editor extends TiptapEditor { export interface PermissionRequestEvent extends CustomEvent<{
id: keyof UnionCommands;
}> {
}
export declare class Editor extends TiptapEditor {
/** /**
* Use this to get the latest instance of the editor. * Use this to get the latest instance of the editor.
* This is required to reduce unnecessary rerenders of * This is required to reduce unnecessary rerenders of
* toolbar elements. * toolbar elements.
*/ */
current?: TiptapEditor; 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, ForwardRef,
ContentDOM, ContentDOM,
} from "./types"; } from "./types";
import { Editor, NodeViewRendererProps } from "@tiptap/core"; import { NodeViewRendererProps } from "@tiptap/core";
import { Theme } from "@notesnook/theme"; import { Theme } from "@notesnook/theme";
import { ThemeProvider } from "emotion-theming"; import { ThemeProvider } from "emotion-theming";
// @ts-ignore // @ts-ignore
import { __serializeForClipboard } from "prosemirror-view"; import { __serializeForClipboard } from "prosemirror-view";
import { Editor } from "../../types";
export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView { export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
private domRef!: HTMLElement; private domRef!: HTMLElement;
@@ -431,7 +432,7 @@ export function createNodeView<TProps extends ReactNodeViewProps>(
return ({ node, getPos, editor }: NodeViewRendererProps) => { return ({ node, getPos, editor }: NodeViewRendererProps) => {
const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos()); const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos());
return new ReactNodeView<TProps>(node, editor, _getPos, { return new ReactNodeView<TProps>(node, editor as Editor, _getPos, {
...options, ...options,
component, component,
}).init(); }).init();

View File

@@ -16,9 +16,10 @@ import {
ForwardRef, ForwardRef,
} from "./types"; } from "./types";
import { ReactNodeView } from "./react-node-view"; import { ReactNodeView } from "./react-node-view";
import { Editor, NodeViewRendererProps } from "@tiptap/core"; import { NodeViewRendererProps } from "@tiptap/core";
import { Theme } from "@notesnook/theme"; import { Theme } from "@notesnook/theme";
import { ThemeProvider } from "emotion-theming"; import { ThemeProvider } from "emotion-theming";
import { Editor } from "../../types";
/** /**
* A ReactNodeView that handles React components sensitive * A ReactNodeView that handles React components sensitive
@@ -251,7 +252,7 @@ export function createSelectionBasedNodeView<
) { ) {
return ({ node, getPos, editor }: NodeViewRendererProps) => { return ({ node, getPos, editor }: NodeViewRendererProps) => {
const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos()); const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos());
return new SelectionBasedNodeView(node, editor, _getPos, { return new SelectionBasedNodeView(node, editor as Editor, _getPos, {
...options, ...options,
component, component,
}).init(); }).init();

View File

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

View File

@@ -4,7 +4,7 @@ import {
SelectionBasedReactNodeViewProps, SelectionBasedReactNodeViewProps,
} from "../react"; } from "../react";
import { Node as ProsemirrorNode } from "prosemirror-model"; import { Node as ProsemirrorNode } from "prosemirror-model";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { updateColumnsOnResize } from "@_ueberdosis/prosemirror-tables"; import { updateColumnsOnResize } from "@_ueberdosis/prosemirror-tables";
import { NodeView } from "prosemirror-view"; import { NodeView } from "prosemirror-view";

View File

@@ -1,5 +1,6 @@
import { Table as TiptapTable, TableOptions } from "@tiptap/extension-table"; import { Table as TiptapTable, TableOptions } from "@tiptap/extension-table";
import { columnResizing, tableEditing } from "@_ueberdosis/prosemirror-tables"; import { columnResizing, tableEditing } from "@_ueberdosis/prosemirror-tables";
import { Editor } from "../../types";
import { TableNodeView } from "./component"; import { TableNodeView } from "./component";
export const Table = TiptapTable.extend<TableOptions>({ export const Table = TiptapTable.extend<TableOptions>({
@@ -12,7 +13,7 @@ export const Table = TiptapTable.extend<TableOptions>({
columnResizing({ columnResizing({
handleWidth: this.options.handleWidth, handleWidth: this.options.handleWidth,
cellMinWidth: this.options.cellMinWidth, cellMinWidth: this.options.cellMinWidth,
View: TableNodeView(this.editor), View: TableNodeView(this.editor as Editor),
// TODO: PR for @types/prosemirror-tables // TODO: PR for @types/prosemirror-tables
// @ts-ignore (incorrect type) // @ts-ignore (incorrect type)
lastColumnResizable: this.options.lastColumnResizable, 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 { DependencyList, useEffect, useRef, useState } from "react";
import { Editor as EditorType } from "../types"; import { Editor } from "../types";
function useForceUpdate() { function useForceUpdate() {
const [, setValue] = useState(0); const [, setValue] = useState(0);
@@ -12,9 +12,9 @@ export const useEditor = (
options: Partial<EditorOptions> = {}, options: Partial<EditorOptions> = {},
deps: DependencyList = [] deps: DependencyList = []
) => { ) => {
const [editor, setEditor] = useState<EditorType | null>(null); const [editor, setEditor] = useState<Editor | null>(null);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const editorRef = useRef<EditorType | null>(editor); const editorRef = useRef<Editor | null>(editor);
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
@@ -42,10 +42,21 @@ export const useEditor = (
useEffect(() => { useEffect(() => {
editorRef.current = editor; editorRef.current = editor;
if (editor && !editor.current) if (!editor) return;
if (!editor.current) {
Object.defineProperty(editor, "current", { Object.defineProperty(editor, "current", {
get: () => editorRef.current, get: () => editorRef.current,
}); });
}
// if (!editor.executor) {
// Object.defineProperty(editor, "executor", {
// get: () => (id?: string) => {
// console.log(id);
// return editorRef.current;
// },
// });
// }
}, [editor]); }, [editor]);
useEffect(() => { 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 { OutlineList } from "./extensions/outline-list";
import { OutlineListItem } from "./extensions/outline-list-item"; import { OutlineListItem } from "./extensions/outline-list-item";
import { Table } from "./extensions/table"; 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 { useEditor } from "./hooks/use-editor";
import { EditorOptions } from "@tiptap/core"; import { EditorOptions } from "@tiptap/core";
import { usePermissionHandler } from "./hooks/use-permission-handler";
EditorView.prototype.updateState = function updateState(state) { EditorView.prototype.updateState = function updateState(state) {
if (!(this as any).docView) return; // This prevents the matchesNode error on hot reloads 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 & type TiptapOptions = EditorOptions &
AttachmentOptions & { theme: Theme; isMobile?: boolean }; AttachmentOptions & {
theme: Theme;
isMobile?: boolean;
};
const useTiptap = ( const useTiptap = (
options: Partial<TiptapOptions> = {}, options: Partial<TiptapOptions> = {},
@@ -66,6 +70,10 @@ const useTiptap = (
const PortalProviderAPI = usePortalProvider(); const PortalProviderAPI = usePortalProvider();
const setIsMobile = useToolbarStore((store) => store.setIsMobile); const setIsMobile = useToolbarStore((store) => store.setIsMobile);
useEffect(() => {
setIsMobile(isMobile || false);
}, [isMobile]);
const defaultOptions = useMemo<Partial<EditorOptions>>( const defaultOptions = useMemo<Partial<EditorOptions>>(
() => ({ () => ({
extensions: [ extensions: [
@@ -158,14 +166,10 @@ const useTiptap = (
deps deps
); );
useEffect(() => {
setIsMobile(isMobile || false);
}, [isMobile]);
return editor; return editor;
}; };
export { useTiptap, Toolbar }; export { useTiptap, Toolbar, usePermissionHandler };
export * from "./types"; export * from "./types";
export * from "./extensions/react"; export * from "./extensions/react";
export * from "./toolbar"; export * from "./toolbar";

View File

@@ -1,7 +1,7 @@
import { ToolbarGroupDefinition, ToolButtonVariant } from "../types"; import { ToolbarGroupDefinition, ToolButtonVariant } from "../types";
import { findTool } from "../tools"; import { findTool } from "../tools";
import { Flex, FlexProps } from "rebass"; import { Flex, FlexProps } from "rebass";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { MoreTools } from "./more-tools"; import { MoreTools } from "./more-tools";
import { getToolDefinition } from "../tool-definitions"; import { getToolDefinition } from "../tool-definitions";
import { NodeWithOffset } from "../utils/prosemirror"; import { NodeWithOffset } from "../utils/prosemirror";

View File

@@ -6,7 +6,7 @@ import {
ImageAlignmentOptions, ImageAlignmentOptions,
ImageSizeOptions, ImageSizeOptions,
} from "../../extensions/image"; } from "../../extensions/image";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
import { InlineInput } from "../../components/inline-input"; import { InlineInput } from "../../components/inline-input";
export type ImagePropertiesProps = ImageSizeOptions & export type ImagePropertiesProps = ImageSizeOptions &

View File

@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { Flex, Text } from "rebass"; import { Flex, Text } from "rebass";
import { SearchStorage } from "../../extensions/search-replace"; import { SearchStorage } from "../../extensions/search-replace";
import { ToolButton } from "../components/tool-button"; import { ToolButton } from "../components/tool-button";
import { Editor } from "@tiptap/core"; import { Editor } from "../../types";
export type SearchReplacePopupProps = { editor: Editor }; export type SearchReplacePopupProps = { editor: Editor };
export function SearchReplacePopup(props: SearchReplacePopupProps) { export function SearchReplacePopup(props: SearchReplacePopupProps) {

View File

@@ -1,6 +1,6 @@
import { Theme, useTheme } from "@notesnook/theme"; import { Theme, useTheme } from "@notesnook/theme";
import { ThemeConfig } from "@notesnook/theme/dist/theme/types"; import { ThemeConfig } from "@notesnook/theme/dist/theme/types";
import { Editor } from "@tiptap/core"; import { Editor } from "../types";
import { Flex, FlexProps } from "rebass"; import { Flex, FlexProps } from "rebass";
import { findTool, ToolId } from "./tools"; import { findTool, ToolId } from "./tools";
import { ThemeProvider } from "emotion-theming"; import { ThemeProvider } from "emotion-theming";

View File

@@ -264,7 +264,12 @@ const uploadImageFromURLMobile = (editor: Editor): MenuItem => ({
component: ({ onClick }) => ( component: ({ onClick }) => (
<ImageUploadPopup <ImageUploadPopup
onInsert={(image) => { onInsert={(image) => {
editor.current?.chain().focus().insertImage(image).run(); editor
.requestPermission("insertImage")
?.chain()
.focus()
.insertImage(image)
.run();
onClick?.(); onClick?.();
}} }}
onClose={() => { onClose={() => {
@@ -283,13 +288,17 @@ const uploadImageFromURL = (editor: Editor): MenuItem => ({
title: "Attach from URL", title: "Attach from URL",
icon: "link", icon: "link",
onClick: () => { onClick: () => {
if (!editor) return;
showPopup({ showPopup({
theme: editor.storage.theme, theme: editor.storage.theme,
popup: (hide) => ( popup: (hide) => (
<ImageUploadPopup <ImageUploadPopup
onInsert={(image) => { onInsert={(image) => {
editor.current?.chain().focus().insertImage(image).run(); editor
.requestPermission("insertImage")
?.chain()
.focus()
.insertImage(image)
.run();
hide(); hide();
}} }}
onClose={hide} onClose={hide}

View File

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

View File

@@ -1,5 +1,4 @@
import { ToolProps } from "../types"; import { ToolProps } from "../types";
import { Editor } from "@tiptap/core";
import { Box, Button, Flex } from "rebass"; import { Box, Button, Flex } from "rebass";
import { IconNames } from "../icons"; import { IconNames } from "../icons";
import { useCallback, useMemo, useRef, useState } from "react"; 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 { MenuButton } from "../../components/menu/types";
import { ToolButton } from "../components/tool-button"; import { ToolButton } from "../components/tool-button";
import { ToolProps } from "../types"; import { ToolProps } from "../types";

View File

@@ -1,9 +1,30 @@
import { Editor as TiptapEditor } from "@tiptap/core"; import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
export interface Editor extends TiptapEditor {
export interface PermissionRequestEvent
extends CustomEvent<{ id: keyof UnionCommands }> {}
export class Editor extends TiptapEditor {
/** /**
* Use this to get the latest instance of the editor. * Use this to get the latest instance of the editor.
* This is required to reduce unnecessary rerenders of * This is required to reduce unnecessary rerenders of
* toolbar elements. * toolbar elements.
*/ */
current?: TiptapEditor; 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 1. Keyboard shouldn't close on tool click
2. Handle context toolbar menus on scroll 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.