chore: Convert LinkToolbar to functional component

Co-authored-by: Ítalo Sousa <italusousa@gmail.com>
This commit is contained in:
Tom Moor
2023-01-05 21:11:28 -05:00
parent a065a8426f
commit ec2da746dc
3 changed files with 111 additions and 120 deletions

View File

@@ -1,6 +1,5 @@
import { NodeSelection } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import { EditorView } from "prosemirror-view";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
@@ -9,10 +8,10 @@ import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener";
import useMediaQuery from "~/hooks/useMediaQuery";
import useViewportHeight from "~/hooks/useViewportHeight";
import { useEditor } from "./EditorContext";
type Props = {
active?: boolean;
view: EditorView;
children: React.ReactNode;
forwardedRef?: React.RefObject<HTMLDivElement> | null;
};
@@ -27,13 +26,13 @@ const defaultPosition = {
function usePosition({
menuRef,
isSelectingText,
props,
active,
}: {
menuRef: React.RefObject<HTMLDivElement>;
isSelectingText: boolean;
props: Props;
active?: boolean;
}) {
const { view, active } = props;
const { view } = useEditor();
const { selection } = view.state;
const { width: menuWidth, height: menuHeight } = useComponentSize(menuRef);
const viewportHeight = useViewportHeight();
@@ -155,14 +154,14 @@ function usePosition({
}
const FloatingToolbar = React.forwardRef(
(props: Props, forwardedRef: React.RefObject<HTMLDivElement>) => {
const menuRef = forwardedRef || React.createRef<HTMLDivElement>();
(props: Props, ref: React.RefObject<HTMLDivElement>) => {
const menuRef = ref || React.createRef<HTMLDivElement>();
const [isSelectingText, setSelectingText] = React.useState(false);
const position = usePosition({
menuRef,
isSelectingText,
props,
active: props.active,
});
useEventListener("mouseup", () => {

View File

@@ -2,152 +2,147 @@ import { EditorView } from "prosemirror-view";
import * as React from "react";
import createAndInsertLink from "@shared/editor/commands/createAndInsertLink";
import { creatingUrlPrefix } from "@shared/utils/urls";
import { Dictionary } from "~/hooks/useDictionary";
import useDictionary from "~/hooks/useDictionary";
import useEventListener from "~/hooks/useEventListener";
import useToasts from "~/hooks/useToasts";
import { useEditor } from "./EditorContext";
import FloatingToolbar from "./FloatingToolbar";
import LinkEditor, { SearchResult } from "./LinkEditor";
type Props = {
isActive: boolean;
view: EditorView;
dictionary: Dictionary;
onCreateLink?: (title: string) => Promise<string>;
onSearchLink?: (term: string) => Promise<SearchResult[]>;
onClickLink: (
href: string,
event: React.MouseEvent<HTMLButtonElement>
) => void;
onShowToast: (message: string) => void;
onClose: () => void;
};
function isActive(props: Props) {
const { view } = props;
const { selection } = view.state;
function isActive(view: EditorView, active: boolean): boolean {
try {
const { selection } = view.state;
const paragraph = view.domAtPos(selection.from);
return props.isActive && !!paragraph.node;
return active && !!paragraph.node;
} catch (err) {
return false;
}
}
export default class LinkToolbar extends React.Component<Props> {
menuRef = React.createRef<HTMLDivElement>();
export default function LinkToolbar({
onCreateLink,
onSearchLink,
onClickLink,
onClose,
...rest
}: Props) {
const dictionary = useDictionary();
const { view } = useEditor();
const { showToast } = useToasts();
const menuRef = React.useRef<HTMLDivElement>(null);
state = {
left: -1000,
top: undefined,
};
componentDidMount() {
window.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
window.removeEventListener("mousedown", this.handleClickOutside);
}
handleClickOutside = (event: Event) => {
useEventListener("mousedown", (event: Event) => {
if (
event.target instanceof HTMLElement &&
this.menuRef.current &&
this.menuRef.current.contains(event.target)
menuRef.current &&
menuRef.current.contains(event.target)
) {
return;
}
this.props.onClose();
};
handleOnCreateLink = async (title: string) => {
const { dictionary, onCreateLink, view, onClose, onShowToast } = this.props;
onClose();
this.props.view.focus();
});
if (!onCreateLink) {
return;
}
const handleOnCreateLink = React.useCallback(
async (title: string) => {
onClose();
view.focus();
const { dispatch, state } = view;
const { from, to } = state.selection;
if (from !== to) {
// selection must be collapsed
return;
}
if (!onCreateLink) {
return;
}
const href = `${creatingUrlPrefix}${title}`;
const { dispatch, state } = view;
const { from, to } = state.selection;
if (from !== to) {
// selection must be collapsed
return;
}
// Insert a placeholder link
dispatch(
view.state.tr
.insertText(title, from, to)
.addMark(
from,
to + title.length,
state.schema.marks.link.create({ href })
)
);
const href = `${creatingUrlPrefix}#${title}`;
createAndInsertLink(view, title, href, {
onCreateLink,
onShowToast,
dictionary,
});
};
// Insert a placeholder link
dispatch(
view.state.tr
.insertText(title, from, to)
.addMark(
from,
to + title.length,
state.schema.marks.link.create({ href })
)
);
handleOnSelectLink = ({
href,
title,
}: {
href: string;
title: string;
from: number;
to: number;
}) => {
const { view, onClose } = this.props;
createAndInsertLink(view, title, href, {
onCreateLink,
onShowToast: showToast,
dictionary,
});
},
[onCreateLink, onClose, view, dictionary, showToast]
);
onClose();
this.props.view.focus();
const handleOnSelectLink = React.useCallback(
({
href,
title,
}: {
href: string;
title: string;
from: number;
to: number;
}) => {
onClose();
view.focus();
const { dispatch, state } = view;
const { from, to } = state.selection;
if (from !== to) {
// selection must be collapsed
return;
}
const { dispatch, state } = view;
const { from, to } = state.selection;
if (from !== to) {
// selection must be collapsed
return;
}
dispatch(
view.state.tr
.insertText(title, from, to)
.addMark(
from,
to + title.length,
state.schema.marks.link.create({ href })
)
);
};
dispatch(
view.state.tr
.insertText(title, from, to)
.addMark(
from,
to + title.length,
state.schema.marks.link.create({ href })
)
);
},
[onClose, view]
);
render() {
const { onCreateLink, onClose, ...rest } = this.props;
const { selection } = this.props.view.state;
const active = isActive(this.props);
const { selection } = view.state;
const active = isActive(view, rest.isActive);
return (
<FloatingToolbar ref={this.menuRef} active={active} {...rest}>
{active && (
<LinkEditor
key={`${selection.from}-${selection.to}`}
from={selection.from}
to={selection.to}
onCreateLink={onCreateLink ? this.handleOnCreateLink : undefined}
onSelectLink={this.handleOnSelectLink}
onRemoveLink={onClose}
{...rest}
/>
)}
</FloatingToolbar>
);
}
return (
<FloatingToolbar ref={menuRef} active={active}>
{active && (
<LinkEditor
key={`${selection.from}-${selection.to}`}
from={selection.from}
to={selection.to}
onCreateLink={onCreateLink ? handleOnCreateLink : undefined}
onSelectLink={handleOnSelectLink}
onRemoveLink={onClose}
onShowToast={showToast}
onClickLink={onClickLink}
dictionary={dictionary}
view={view}
/>
)}
</FloatingToolbar>
);
}

View File

@@ -685,13 +685,10 @@ export class Editor extends React.PureComponent<
onShowToast={this.props.onShowToast}
/>
<LinkToolbar
view={this.view}
dictionary={dictionary}
isActive={this.state.linkMenuOpen}
onCreateLink={this.props.onCreateLink}
onSearchLink={this.props.onSearchLink}
onClickLink={this.props.onClickLink}
onShowToast={this.props.onShowToast}
onClose={this.handleCloseLinkMenu}
/>
<EmojiMenu