Refactor Editor components to be injected by associated extension (#6093)

This commit is contained in:
Tom Moor
2023-10-31 21:55:55 -04:00
committed by GitHub
parent 44198732d3
commit df6d8c12cc
25 changed files with 371 additions and 354 deletions

View File

@@ -26,15 +26,17 @@ import styled, { css, DefaultTheme, ThemeProps } from "styled-components";
import insertFiles from "@shared/editor/commands/insertFiles";
import Styles from "@shared/editor/components/Styles";
import { EmbedDescriptor } from "@shared/editor/embeds";
import Extension, { CommandFactory } from "@shared/editor/lib/Extension";
import Extension, {
CommandFactory,
WidgetProps,
} from "@shared/editor/lib/Extension";
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
import { MarkdownSerializer } from "@shared/editor/lib/markdown/serializer";
import textBetween from "@shared/editor/lib/textBetween";
import Mark from "@shared/editor/marks/Mark";
import { richExtensions, withComments } from "@shared/editor/nodes";
import { basicExtensions as extensions } from "@shared/editor/nodes";
import Node from "@shared/editor/nodes/Node";
import ReactNode from "@shared/editor/nodes/ReactNode";
import { SuggestionsMenuType } from "@shared/editor/plugins/Suggestions";
import { EventType } from "@shared/editor/types";
import { UserPreferences } from "@shared/types";
import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper";
@@ -43,21 +45,13 @@ import Flex from "~/components/Flex";
import { PortalContext } from "~/components/Portal";
import { Dictionary } from "~/hooks/useDictionary";
import Logger from "~/utils/Logger";
import BlockMenu from "./components/BlockMenu";
import ComponentView from "./components/ComponentView";
import EditorContext from "./components/EditorContext";
import EmojiMenu from "./components/EmojiMenu";
import FindAndReplace from "./components/FindAndReplace";
import { SearchResult } from "./components/LinkEditor";
import LinkToolbar from "./components/LinkToolbar";
import MentionMenu from "./components/MentionMenu";
import SelectionToolbar from "./components/SelectionToolbar";
import WithTheme from "./components/WithTheme";
const extensions = withComments(richExtensions);
export { default as Extension } from "@shared/editor/lib/Extension";
export type Props = {
/** An optional identifier for the editor context. It is used to persist local settings */
id?: string;
@@ -124,8 +118,6 @@ export type Props = {
href: string,
event: MouseEvent | React.MouseEvent<HTMLButtonElement>
) => void;
/** Callback when user hovers on any link in the document */
onHoverLink?: (element: HTMLAnchorElement | null) => boolean;
/** Callback when user presses any key with document focused */
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
/** Collection of embed types to render in the document */
@@ -148,12 +140,8 @@ type State = {
isEditorFocused: boolean;
/** If the toolbar for a text selection is visible */
selectionToolbarOpen: boolean;
/** If a suggestions menu is visible */
suggestionsMenuOpen: SuggestionsMenuType | false;
/** If the insert link toolbar is visible */
linkToolbarOpen: boolean;
/** The query for the suggestion menu */
query: string;
};
/**
@@ -182,10 +170,8 @@ export class Editor extends React.PureComponent<
state: State = {
isRTL: false,
isEditorFocused: false,
suggestionsMenuOpen: false,
selectionToolbarOpen: false,
linkToolbarOpen: false,
query: "",
};
isBlurred = true;
@@ -204,6 +190,7 @@ export class Editor extends React.PureComponent<
[name: string]: NodeViewConstructor;
};
widgets: { [name: string]: (props: WidgetProps) => React.ReactElement };
nodes: { [name: string]: NodeSpec };
marks: { [name: string]: MarkSpec };
commands: Record<string, CommandFactory>;
@@ -214,14 +201,6 @@ export class Editor extends React.PureComponent<
public constructor(props: Props & ThemeProps<DefaultTheme>) {
super(props);
this.events.on(EventType.LinkToolbarOpen, this.handleOpenLinkToolbar);
this.events.on(
EventType.SuggestionsMenuOpen,
this.handleOpenSuggestionsMenu
);
this.events.on(
EventType.SuggestionsMenuClose,
this.handleCloseSuggestionsMenu
);
}
/**
@@ -279,7 +258,6 @@ export class Editor extends React.PureComponent<
if (
!this.isBlurred &&
!this.state.isEditorFocused &&
!this.state.suggestionsMenuOpen &&
!this.state.linkToolbarOpen &&
!this.state.selectionToolbarOpen
) {
@@ -290,7 +268,6 @@ export class Editor extends React.PureComponent<
if (
this.isBlurred &&
(this.state.isEditorFocused ||
this.state.suggestionsMenuOpen ||
this.state.linkToolbarOpen ||
this.state.selectionToolbarOpen)
) {
@@ -310,6 +287,7 @@ export class Editor extends React.PureComponent<
this.nodes = this.createNodes();
this.marks = this.createMarks();
this.schema = this.createSchema();
this.widgets = this.createWidgets();
this.plugins = this.createPlugins();
this.rulePlugins = this.createRulePlugins();
this.keymaps = this.createKeymaps();
@@ -378,6 +356,10 @@ export class Editor extends React.PureComponent<
});
}
private createWidgets() {
return this.extensions.widgets;
}
private createNodes() {
return this.extensions.nodes;
}
@@ -702,8 +684,6 @@ export class Editor extends React.PureComponent<
this.setState((state) => ({
...state,
selectionToolbarOpen: true,
suggestionsMenuOpen: false,
query: "",
}));
};
@@ -720,9 +700,7 @@ export class Editor extends React.PureComponent<
private handleOpenLinkToolbar = () => {
this.setState((state) => ({
...state,
suggestionsMenuOpen: false,
linkToolbarOpen: true,
query: "",
}));
};
@@ -733,37 +711,6 @@ export class Editor extends React.PureComponent<
}));
};
private handleOpenSuggestionsMenu = (data: {
type: SuggestionsMenuType;
query: string;
}) => {
this.setState((state) => ({
...state,
suggestionsMenuOpen: data.type,
query: data.query,
}));
};
private handleCloseSuggestionsMenu = (
type: SuggestionsMenuType,
insertNewLine?: boolean
) => {
if (insertNewLine) {
const transaction = this.view.state.tr.split(
this.view.state.selection.to
);
this.view.dispatch(transaction);
this.view.focus();
}
if (type && this.state.suggestionsMenuOpen !== type) {
return;
}
this.setState((state) => ({
...state,
suggestionsMenuOpen: false,
}));
};
public render() {
const { dir, readOnly, canUpdate, grow, style, className, onKeyDown } =
this.props;
@@ -792,84 +739,31 @@ export class Editor extends React.PureComponent<
ref={this.elementRef}
/>
{this.view && (
<>
<SelectionToolbar
rtl={isRTL}
readOnly={readOnly}
canComment={this.props.canComment}
isTemplate={this.props.template === true}
onOpen={this.handleOpenSelectionToolbar}
onClose={this.handleCloseSelectionToolbar}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onCreateLink={this.props.onCreateLink}
/>
{this.commands.find && <FindAndReplace readOnly={readOnly} />}
</>
<SelectionToolbar
rtl={isRTL}
readOnly={readOnly}
canComment={this.props.canComment}
isTemplate={this.props.template === true}
onOpen={this.handleOpenSelectionToolbar}
onClose={this.handleCloseSelectionToolbar}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onCreateLink={this.props.onCreateLink}
/>
)}
{!readOnly && this.view && (
<>
{this.marks.link && (
<LinkToolbar
isActive={this.state.linkToolbarOpen}
onCreateLink={this.props.onCreateLink}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onClose={this.handleCloseLinkToolbar}
/>
)}
{this.nodes.emoji && (
<EmojiMenu
rtl={isRTL}
isActive={
this.state.suggestionsMenuOpen ===
SuggestionsMenuType.Emoji
}
search={this.state.query}
onClose={(insertNewLine) =>
this.handleCloseSuggestionsMenu(
SuggestionsMenuType.Emoji,
insertNewLine
)
}
/>
)}
{this.nodes.mention && (
<MentionMenu
rtl={isRTL}
isActive={
this.state.suggestionsMenuOpen ===
SuggestionsMenuType.Mention
}
search={this.state.query}
onClose={(insertNewLine) =>
this.handleCloseSuggestionsMenu(
SuggestionsMenuType.Mention,
insertNewLine
)
}
/>
)}
<BlockMenu
rtl={isRTL}
isActive={
this.state.suggestionsMenuOpen === SuggestionsMenuType.Block
}
search={this.state.query}
onClose={(insertNewLine) =>
this.handleCloseSuggestionsMenu(
SuggestionsMenuType.Block,
insertNewLine
)
}
uploadFile={this.props.uploadFile}
onLinkToolbarOpen={this.handleOpenLinkToolbar}
onFileUploadStart={this.props.onFileUploadStart}
onFileUploadStop={this.props.onFileUploadStop}
embeds={this.props.embeds}
/>
</>
{!readOnly && this.view && this.marks.link && (
<LinkToolbar
isActive={this.state.linkToolbarOpen}
onCreateLink={this.props.onCreateLink}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onClose={this.handleCloseLinkToolbar}
/>
)}
{this.widgets &&
Object.values(this.widgets).map((Widget, index) => (
<Widget key={String(index)} rtl={isRTL} />
))}
</Flex>
</EditorContext.Provider>
</PortalContext.Provider>