chore: Editor refactor (#3286)
* cleanup * add context * EventEmitter allows removal of toolbar props from extensions * Move to 'packages' of extensions Remove EmojiTrigger extension * types * iteration * fix render flashing * fix: Missing nodes in collection descriptions
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { ToastType } from "../types";
|
||||
|
||||
function findPlaceholderLink(doc: Node, href: string) {
|
||||
let result: { pos: number; node: Node } | undefined;
|
||||
@@ -38,7 +37,7 @@ const createAndInsertLink = async function (
|
||||
options: {
|
||||
dictionary: any;
|
||||
onCreateLink: (title: string) => Promise<string>;
|
||||
onShowToast: (message: string, code: string) => void;
|
||||
onShowToast: (message: string) => void;
|
||||
}
|
||||
) {
|
||||
const { dispatch, state } = view;
|
||||
@@ -79,10 +78,7 @@ const createAndInsertLink = async function (
|
||||
)
|
||||
);
|
||||
|
||||
// let the user know
|
||||
if (onShowToast) {
|
||||
onShowToast(options.dictionary.createLinkError, ToastType.Error);
|
||||
}
|
||||
onShowToast(options.dictionary.createLinkError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import uploadPlaceholderPlugin, {
|
||||
findPlaceholder,
|
||||
} from "../lib/uploadPlaceholder";
|
||||
import findAttachmentById from "../queries/findAttachmentById";
|
||||
import { ToastType } from "../types";
|
||||
|
||||
export type Options = {
|
||||
dictionary: any;
|
||||
@@ -17,7 +16,7 @@ export type Options = {
|
||||
uploadFile?: (file: File) => Promise<string>;
|
||||
onFileUploadStart?: () => void;
|
||||
onFileUploadStop?: () => void;
|
||||
onShowToast: (message: string, code: string) => void;
|
||||
onShowToast: (message: string) => void;
|
||||
};
|
||||
|
||||
const insertFiles = function (
|
||||
@@ -187,10 +186,7 @@ const insertFiles = function (
|
||||
view.dispatch(view.state.tr.deleteRange(from, to || from));
|
||||
}
|
||||
|
||||
onShowToast(
|
||||
error.message || dictionary.fileUploadError,
|
||||
ToastType.Error
|
||||
);
|
||||
onShowToast(error.message || dictionary.fileUploadError);
|
||||
})
|
||||
.finally(() => {
|
||||
complete++;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { keymap } from "prosemirror-keymap";
|
||||
import { MarkdownParser, TokenConfig } from "prosemirror-markdown";
|
||||
import { Schema } from "prosemirror-model";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { Editor } from "~/editor";
|
||||
import Mark from "../marks/Mark";
|
||||
import Node from "../nodes/Node";
|
||||
import Extension, { CommandFactory } from "./Extension";
|
||||
@@ -10,16 +11,32 @@ import makeRules from "./markdown/rules";
|
||||
import { MarkdownSerializer } from "./markdown/serializer";
|
||||
|
||||
export default class ExtensionManager {
|
||||
extensions: (Node | Mark | Extension)[];
|
||||
extensions: (Node | Mark | Extension)[] = [];
|
||||
|
||||
constructor(extensions: (Node | Mark | Extension)[] = [], editor?: any) {
|
||||
if (editor) {
|
||||
extensions.forEach((extension) => {
|
||||
constructor(
|
||||
extensions: (
|
||||
| Extension
|
||||
| typeof Node
|
||||
| typeof Mark
|
||||
| typeof Extension
|
||||
)[] = [],
|
||||
editor?: Editor
|
||||
) {
|
||||
extensions.forEach((ext) => {
|
||||
let extension;
|
||||
|
||||
if (typeof ext === "function") {
|
||||
extension = new ext(editor?.props);
|
||||
} else {
|
||||
extension = ext;
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
extension.bindEditor(editor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.extensions = extensions;
|
||||
this.extensions.push(extension);
|
||||
});
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
|
||||
@@ -15,7 +15,7 @@ import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { isInternalUrl } from "../../utils/urls";
|
||||
import findLinkNodes from "../queries/findLinkNodes";
|
||||
import { Dispatch } from "../types";
|
||||
import { EventType, Dispatch } from "../types";
|
||||
import Mark from "./Mark";
|
||||
|
||||
const LINK_INPUT_REGEX = /\[([^[]+)]\((\S+)\)$/;
|
||||
@@ -106,7 +106,7 @@ export default class Link extends Mark {
|
||||
return {
|
||||
"Mod-k": (state: EditorState, dispatch: Dispatch) => {
|
||||
if (state.selection.empty) {
|
||||
this.options.onKeyboardShortcut();
|
||||
this.editor.events.emit(EventType.linkMenuOpen);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,13 @@ import rust from "refractor/lang/rust";
|
||||
import sql from "refractor/lang/sql";
|
||||
import typescript from "refractor/lang/typescript";
|
||||
import yaml from "refractor/lang/yaml";
|
||||
import { Dictionary } from "~/hooks/useDictionary";
|
||||
|
||||
import toggleBlockType from "../commands/toggleBlockType";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import Prism, { LANGUAGES } from "../plugins/Prism";
|
||||
import isInCode from "../queries/isInCode";
|
||||
import { Dispatch, ToastType } from "../types";
|
||||
import { Dispatch } from "../types";
|
||||
import Node from "./Node";
|
||||
|
||||
const PERSISTENCE_KEY = "rme-code-language";
|
||||
@@ -67,6 +68,13 @@ const DEFAULT_LANGUAGE = "javascript";
|
||||
].forEach(refractor.register);
|
||||
|
||||
export default class CodeFence extends Node {
|
||||
constructor(options: {
|
||||
dictionary: Dictionary;
|
||||
onShowToast: (message: string) => void;
|
||||
}) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
get languageOptions() {
|
||||
return Object.entries(LANGUAGES);
|
||||
}
|
||||
@@ -194,10 +202,7 @@ export default class CodeFence extends Node {
|
||||
const node = view.state.doc.nodeAt(result.pos);
|
||||
if (node) {
|
||||
copy(node.textContent);
|
||||
this.options.onShowToast(
|
||||
this.options.dictionary.codeCopied,
|
||||
ToastType.Info
|
||||
);
|
||||
this.options.onShowToast(this.options.dictionary.codeCopied);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,17 @@ import nameToEmoji from "gemoji/name-to-emoji.json";
|
||||
import Token from "markdown-it/lib/token";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { NodeSpec, Node as ProsemirrorNode, NodeType } from "prosemirror-model";
|
||||
import { EditorState, TextSelection } from "prosemirror-state";
|
||||
import { EditorState, TextSelection, Plugin } from "prosemirror-state";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import { run } from "../plugins/BlockMenuTrigger";
|
||||
import isInCode from "../queries/isInCode";
|
||||
import emojiRule from "../rules/emoji";
|
||||
import { Dispatch } from "../types";
|
||||
import { Dispatch, EventType } from "../types";
|
||||
import Node from "./Node";
|
||||
|
||||
const OPEN_REGEX = /(?:^|\s):([0-9a-zA-Z_+-]+)?$/;
|
||||
const CLOSE_REGEX = /(?:^|\s):(([0-9a-zA-Z_+-]*\s+)|(\s+[0-9a-zA-Z_+-]+)|[^0-9a-zA-Z_+-]+)$/;
|
||||
|
||||
export default class Emoji extends Node {
|
||||
get name() {
|
||||
return "emoji";
|
||||
@@ -61,6 +66,57 @@ export default class Emoji extends Node {
|
||||
return [emojiRule];
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleClick: () => {
|
||||
this.editor.events.emit(EventType.emojiMenuClose);
|
||||
return false;
|
||||
},
|
||||
handleKeyDown: (view, event) => {
|
||||
// Prosemirror input rules are not triggered on backspace, however
|
||||
// we need them to be evaluted for the filter trigger to work
|
||||
// correctly. This additional handler adds inputrules-like handling.
|
||||
if (event.key === "Backspace") {
|
||||
// timeout ensures that the delete has been handled by prosemirror
|
||||
// and any characters removed, before we evaluate the rule.
|
||||
setTimeout(() => {
|
||||
const { pos } = view.state.selection.$from;
|
||||
return run(view, pos, pos, OPEN_REGEX, (state, match) => {
|
||||
if (match) {
|
||||
this.editor.events.emit(EventType.emojiMenuOpen, match[1]);
|
||||
} else {
|
||||
this.editor.events.emit(EventType.emojiMenuClose);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If the query is active and we're navigating the block menu then
|
||||
// just ignore the key events in the editor itself until we're done
|
||||
if (
|
||||
event.key === "Enter" ||
|
||||
event.key === "ArrowUp" ||
|
||||
event.key === "ArrowDown" ||
|
||||
event.key === "Tab"
|
||||
) {
|
||||
const { pos } = view.state.selection.$from;
|
||||
|
||||
return run(view, pos, pos, OPEN_REGEX, (state, match) => {
|
||||
// just tell Prosemirror we handled it and not to do anything
|
||||
return match ? true : null;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType }) {
|
||||
return (attrs: Record<string, string>) => (
|
||||
state: EditorState,
|
||||
@@ -100,6 +156,29 @@ export default class Emoji extends Node {
|
||||
|
||||
return tr;
|
||||
}),
|
||||
// main regex should match only:
|
||||
// :word
|
||||
new InputRule(OPEN_REGEX, (state, match) => {
|
||||
if (
|
||||
match &&
|
||||
state.selection.$from.parent.type.name === "paragraph" &&
|
||||
!isInCode(state)
|
||||
) {
|
||||
this.editor.events.emit(EventType.emojiMenuOpen, match[1]);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
// invert regex should match some of these scenarios:
|
||||
// :<space>word
|
||||
// :<space>
|
||||
// :word<space>
|
||||
// :)
|
||||
new InputRule(CLOSE_REGEX, (state, match) => {
|
||||
if (match) {
|
||||
this.editor.events.emit(EventType.emojiMenuClose);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import toggleBlockType from "../commands/toggleBlockType";
|
||||
import { Command } from "../lib/Extension";
|
||||
import headingToSlug, { headingToPersistenceKey } from "../lib/headingToSlug";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import { ToastType } from "../types";
|
||||
import Node from "./Node";
|
||||
|
||||
export default class Heading extends Node {
|
||||
@@ -180,10 +179,7 @@ export default class Heading extends Node {
|
||||
const urlWithoutHash = window.location.href.split("#")[0];
|
||||
copy(urlWithoutHash + hash);
|
||||
|
||||
this.options.onShowToast(
|
||||
this.options.dictionary.linkCopied,
|
||||
ToastType.Info
|
||||
);
|
||||
this.options.onShowToast(this.options.dictionary.linkCopied);
|
||||
};
|
||||
|
||||
keys({ type, schema }: { type: NodeType; schema: Schema }) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
isTableSelected,
|
||||
isRowSelected,
|
||||
getCellsInColumn,
|
||||
selectRow,
|
||||
selectTable,
|
||||
} from "prosemirror-utils";
|
||||
import { DecorationSet, Decoration } from "prosemirror-view";
|
||||
import Node from "./Node";
|
||||
@@ -72,7 +74,7 @@ export default class TableCell extends Node {
|
||||
grip.addEventListener("mousedown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.options.onSelectTable(state);
|
||||
this.editor.view.dispatch(selectTable(state.tr));
|
||||
});
|
||||
return grip;
|
||||
})
|
||||
@@ -97,7 +99,7 @@ export default class TableCell extends Node {
|
||||
grip.addEventListener("mousedown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.options.onSelectRow(index, state);
|
||||
this.editor.view.dispatch(selectRow(index)(state.tr));
|
||||
});
|
||||
return grip;
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import Token from "markdown-it/lib/token";
|
||||
import { NodeSpec } from "prosemirror-model";
|
||||
import { Plugin } from "prosemirror-state";
|
||||
import { isColumnSelected, getCellsInRow } from "prosemirror-utils";
|
||||
import {
|
||||
isColumnSelected,
|
||||
getCellsInRow,
|
||||
selectColumn,
|
||||
} from "prosemirror-utils";
|
||||
import { DecorationSet, Decoration } from "prosemirror-view";
|
||||
import Node from "./Node";
|
||||
|
||||
@@ -72,7 +76,7 @@ export default class TableHeadCell extends Node {
|
||||
grip.addEventListener("mousedown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.options.onSelectColumn(index, state);
|
||||
this.editor.view.dispatch(selectColumn(index)(state.tr));
|
||||
});
|
||||
return grip;
|
||||
})
|
||||
|
||||
2
shared/editor/packages/README.md
Normal file
2
shared/editor/packages/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Packages are preselected collections of extensions that form the different types
|
||||
of editors within Outline.
|
||||
44
shared/editor/packages/basic.ts
Normal file
44
shared/editor/packages/basic.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import Extension from "../lib/Extension";
|
||||
import Bold from "../marks/Bold";
|
||||
import Code from "../marks/Code";
|
||||
import Italic from "../marks/Italic";
|
||||
import Link from "../marks/Link";
|
||||
import Mark from "../marks/Mark";
|
||||
import Strikethrough from "../marks/Strikethrough";
|
||||
import Underline from "../marks/Underline";
|
||||
import Doc from "../nodes/Doc";
|
||||
import Emoji from "../nodes/Emoji";
|
||||
import HardBreak from "../nodes/HardBreak";
|
||||
import Image from "../nodes/Image";
|
||||
import Node from "../nodes/Node";
|
||||
import Paragraph from "../nodes/Paragraph";
|
||||
import Text from "../nodes/Text";
|
||||
import History from "../plugins/History";
|
||||
import MaxLength from "../plugins/MaxLength";
|
||||
import PasteHandler from "../plugins/PasteHandler";
|
||||
import Placeholder from "../plugins/Placeholder";
|
||||
import SmartText from "../plugins/SmartText";
|
||||
import TrailingNode from "../plugins/TrailingNode";
|
||||
|
||||
const basicPackage: (typeof Node | typeof Mark | typeof Extension)[] = [
|
||||
Doc,
|
||||
HardBreak,
|
||||
Paragraph,
|
||||
Emoji,
|
||||
Text,
|
||||
Image,
|
||||
Bold,
|
||||
Code,
|
||||
Italic,
|
||||
Underline,
|
||||
Link,
|
||||
Strikethrough,
|
||||
History,
|
||||
SmartText,
|
||||
TrailingNode,
|
||||
PasteHandler,
|
||||
Placeholder,
|
||||
MaxLength,
|
||||
];
|
||||
|
||||
export default basicPackage;
|
||||
52
shared/editor/packages/full.ts
Normal file
52
shared/editor/packages/full.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import Extension from "../lib/Extension";
|
||||
import Highlight from "../marks/Highlight";
|
||||
import Mark from "../marks/Mark";
|
||||
import TemplatePlaceholder from "../marks/Placeholder";
|
||||
import Attachment from "../nodes/Attachment";
|
||||
import BulletList from "../nodes/BulletList";
|
||||
import CheckboxItem from "../nodes/CheckboxItem";
|
||||
import CheckboxList from "../nodes/CheckboxList";
|
||||
import CodeBlock from "../nodes/CodeBlock";
|
||||
import CodeFence from "../nodes/CodeFence";
|
||||
import Embed from "../nodes/Embed";
|
||||
import Heading from "../nodes/Heading";
|
||||
import HorizontalRule from "../nodes/HorizontalRule";
|
||||
import ListItem from "../nodes/ListItem";
|
||||
import Node from "../nodes/Node";
|
||||
import Notice from "../nodes/Notice";
|
||||
import OrderedList from "../nodes/OrderedList";
|
||||
import Table from "../nodes/Table";
|
||||
import TableCell from "../nodes/TableCell";
|
||||
import TableHeadCell from "../nodes/TableHeadCell";
|
||||
import TableRow from "../nodes/TableRow";
|
||||
import BlockMenuTrigger from "../plugins/BlockMenuTrigger";
|
||||
import Folding from "../plugins/Folding";
|
||||
import Keys from "../plugins/Keys";
|
||||
import basicPackage from "./basic";
|
||||
|
||||
const fullPackage: (typeof Node | typeof Mark | typeof Extension)[] = [
|
||||
...basicPackage,
|
||||
CodeBlock,
|
||||
CodeFence,
|
||||
CheckboxList,
|
||||
CheckboxItem,
|
||||
BulletList,
|
||||
OrderedList,
|
||||
Embed,
|
||||
ListItem,
|
||||
Attachment,
|
||||
Notice,
|
||||
Heading,
|
||||
HorizontalRule,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHeadCell,
|
||||
TableRow,
|
||||
Highlight,
|
||||
TemplatePlaceholder,
|
||||
Folding,
|
||||
Keys,
|
||||
BlockMenuTrigger,
|
||||
];
|
||||
|
||||
export default fullPackage;
|
||||
@@ -7,6 +7,7 @@ import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Extension from "../lib/Extension";
|
||||
import { EventType } from "../types";
|
||||
|
||||
const MAX_MATCH = 500;
|
||||
const OPEN_REGEX = /^\/(\w+)?$/;
|
||||
@@ -65,7 +66,7 @@ export default class BlockMenuTrigger extends Extension {
|
||||
new Plugin({
|
||||
props: {
|
||||
handleClick: () => {
|
||||
this.options.onClose();
|
||||
this.editor.events.emit(EventType.blockMenuClose);
|
||||
return false;
|
||||
},
|
||||
handleKeyDown: (view, event) => {
|
||||
@@ -79,9 +80,9 @@ export default class BlockMenuTrigger extends Extension {
|
||||
const { pos } = view.state.selection.$from;
|
||||
return run(view, pos, pos, OPEN_REGEX, (state, match) => {
|
||||
if (match) {
|
||||
this.options.onOpen(match[1]);
|
||||
this.editor.events.emit(EventType.blockMenuOpen, match[1]);
|
||||
} else {
|
||||
this.options.onClose();
|
||||
this.editor.events.emit(EventType.blockMenuClose);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -125,7 +126,7 @@ export default class BlockMenuTrigger extends Extension {
|
||||
decorations.push(
|
||||
Decoration.widget(parent.pos, () => {
|
||||
button.addEventListener("click", () => {
|
||||
this.options.onOpen("");
|
||||
this.editor.events.emit(EventType.blockMenuOpen, "");
|
||||
});
|
||||
return button;
|
||||
})
|
||||
@@ -176,7 +177,7 @@ export default class BlockMenuTrigger extends Extension {
|
||||
state.selection.$from.parent.type.name === "paragraph" &&
|
||||
!isInTable(state)
|
||||
) {
|
||||
this.options.onOpen(match[1]);
|
||||
this.editor.events.emit(EventType.blockMenuOpen, match[1]);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
@@ -186,7 +187,7 @@ export default class BlockMenuTrigger extends Extension {
|
||||
// /word<space>
|
||||
new InputRule(CLOSE_REGEX, (state, match) => {
|
||||
if (match) {
|
||||
this.options.onClose();
|
||||
this.editor.events.emit(EventType.blockMenuClose);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { Plugin } from "prosemirror-state";
|
||||
import Extension from "../lib/Extension";
|
||||
import isInCode from "../queries/isInCode";
|
||||
import { run } from "./BlockMenuTrigger";
|
||||
|
||||
const OPEN_REGEX = /(?:^|\s):([0-9a-zA-Z_+-]+)?$/;
|
||||
const CLOSE_REGEX = /(?:^|\s):(([0-9a-zA-Z_+-]*\s+)|(\s+[0-9a-zA-Z_+-]+)|[^0-9a-zA-Z_+-]+)$/;
|
||||
|
||||
export default class EmojiTrigger extends Extension {
|
||||
get name() {
|
||||
return "emojimenu";
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleClick: () => {
|
||||
this.options.onClose();
|
||||
return false;
|
||||
},
|
||||
handleKeyDown: (view, event) => {
|
||||
// Prosemirror input rules are not triggered on backspace, however
|
||||
// we need them to be evaluted for the filter trigger to work
|
||||
// correctly. This additional handler adds inputrules-like handling.
|
||||
if (event.key === "Backspace") {
|
||||
// timeout ensures that the delete has been handled by prosemirror
|
||||
// and any characters removed, before we evaluate the rule.
|
||||
setTimeout(() => {
|
||||
const { pos } = view.state.selection.$from;
|
||||
return run(view, pos, pos, OPEN_REGEX, (state, match) => {
|
||||
if (match) {
|
||||
this.options.onOpen(match[1]);
|
||||
} else {
|
||||
this.options.onClose();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If the query is active and we're navigating the block menu then
|
||||
// just ignore the key events in the editor itself until we're done
|
||||
if (
|
||||
event.key === "Enter" ||
|
||||
event.key === "ArrowUp" ||
|
||||
event.key === "ArrowDown" ||
|
||||
event.key === "Tab"
|
||||
) {
|
||||
const { pos } = view.state.selection.$from;
|
||||
|
||||
return run(view, pos, pos, OPEN_REGEX, (state, match) => {
|
||||
// just tell Prosemirror we handled it and not to do anything
|
||||
return match ? true : null;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
inputRules() {
|
||||
return [
|
||||
// main regex should match only:
|
||||
// :word
|
||||
new InputRule(OPEN_REGEX, (state, match) => {
|
||||
if (
|
||||
match &&
|
||||
state.selection.$from.parent.type.name === "paragraph" &&
|
||||
!isInCode(state)
|
||||
) {
|
||||
this.options.onOpen(match[1]);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
// invert regex should match some of these scenarios:
|
||||
// :<space>word
|
||||
// :<space>
|
||||
// :word<space>
|
||||
// :)
|
||||
new InputRule(CLOSE_REGEX, (state, match) => {
|
||||
if (match) {
|
||||
this.options.onClose();
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ export default class Keys extends Extension {
|
||||
|
||||
keys(): Record<string, Command> {
|
||||
const onCancel = () => {
|
||||
if (this.options.onCancel) {
|
||||
this.options.onCancel();
|
||||
if (this.editor.props.onCancel) {
|
||||
this.editor.props.onCancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -32,15 +32,15 @@ export default class Keys extends Extension {
|
||||
"Mod-Escape": onCancel,
|
||||
"Shift-Escape": onCancel,
|
||||
"Mod-s": () => {
|
||||
if (this.options.onSave) {
|
||||
this.options.onSave();
|
||||
if (this.editor.props.onSave) {
|
||||
this.editor.props.onSave({ done: false });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
"Mod-Enter": (state: EditorState) => {
|
||||
if (!isInCode(state) && this.options.onSaveAndExit) {
|
||||
this.options.onSaveAndExit();
|
||||
if (!isInCode(state) && this.editor.props.onSave) {
|
||||
this.editor.props.onSave({ done: true });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -52,10 +52,6 @@ export default class Keys extends Extension {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
blur: this.options.onBlur,
|
||||
focus: this.options.onFocus,
|
||||
},
|
||||
// we can't use the keys bindings for this as we want to preventDefault
|
||||
// on the original keyboard event when handled
|
||||
handleKeyDown: (view, event) => {
|
||||
|
||||
@@ -3,9 +3,13 @@ import { EditorState, Transaction } from "prosemirror-state";
|
||||
import * as React from "react";
|
||||
import { DefaultTheme } from "styled-components";
|
||||
|
||||
export enum ToastType {
|
||||
Error = "error",
|
||||
Info = "info",
|
||||
export enum EventType {
|
||||
blockMenuOpen = "blockMenuOpen",
|
||||
blockMenuClose = "blockMenuClose",
|
||||
emojiMenuOpen = "emojiMenuOpen",
|
||||
emojiMenuClose = "emojiMenuClose",
|
||||
linkMenuOpen = "linkMenuOpen",
|
||||
linkMenuClose = "linkMenuClose",
|
||||
}
|
||||
|
||||
export type MenuItem = {
|
||||
|
||||
29
shared/utils/events.ts
Normal file
29
shared/utils/events.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* A tiny EventEmitter implementation for the browser.
|
||||
*/
|
||||
export default class EventEmitter {
|
||||
private listeners: { [name: string]: ((data: any) => unknown)[] } = {};
|
||||
|
||||
public addListener(name: string, callback: (data: any) => unknown) {
|
||||
if (!this.listeners[name]) {
|
||||
this.listeners[name] = [];
|
||||
}
|
||||
|
||||
this.listeners[name].push(callback);
|
||||
}
|
||||
|
||||
public removeListener(name: string, callback: (data: any) => unknown) {
|
||||
this.listeners[name] = this.listeners[name]?.filter(
|
||||
(cb) => cb !== callback
|
||||
);
|
||||
}
|
||||
|
||||
public on = this.addListener;
|
||||
public off = this.removeListener;
|
||||
|
||||
public emit(name: string, data?: any) {
|
||||
this.listeners[name]?.forEach((callback) => {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user