154 lines
3.5 KiB
TypeScript
154 lines
3.5 KiB
TypeScript
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 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;
|
|
|
|
try {
|
|
const paragraph = view.domAtPos(selection.from);
|
|
return props.isActive && !!paragraph.node;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export default class LinkToolbar extends React.Component<Props> {
|
|
menuRef = React.createRef<HTMLDivElement>();
|
|
|
|
state = {
|
|
left: -1000,
|
|
top: undefined,
|
|
};
|
|
|
|
componentDidMount() {
|
|
window.addEventListener("mousedown", this.handleClickOutside);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener("mousedown", this.handleClickOutside);
|
|
}
|
|
|
|
handleClickOutside = (event: Event) => {
|
|
if (
|
|
event.target instanceof HTMLElement &&
|
|
this.menuRef.current &&
|
|
this.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 { dispatch, state } = view;
|
|
const { from, to } = state.selection;
|
|
if (from !== to) {
|
|
// selection must be collapsed
|
|
return;
|
|
}
|
|
|
|
const href = `${creatingUrlPrefix}${title}…`;
|
|
|
|
// Insert a placeholder link
|
|
dispatch(
|
|
view.state.tr
|
|
.insertText(title, from, to)
|
|
.addMark(
|
|
from,
|
|
to + title.length,
|
|
state.schema.marks.link.create({ href })
|
|
)
|
|
);
|
|
|
|
createAndInsertLink(view, title, href, {
|
|
onCreateLink,
|
|
onShowToast,
|
|
dictionary,
|
|
});
|
|
};
|
|
|
|
handleOnSelectLink = ({
|
|
href,
|
|
title,
|
|
}: {
|
|
href: string;
|
|
title: string;
|
|
from: number;
|
|
to: number;
|
|
}) => {
|
|
const { view, onClose } = this.props;
|
|
|
|
onClose();
|
|
this.props.view.focus();
|
|
|
|
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 })
|
|
)
|
|
);
|
|
};
|
|
|
|
render() {
|
|
const { onCreateLink, onClose, ...rest } = this.props;
|
|
const { selection } = this.props.view.state;
|
|
const active = isActive(this.props);
|
|
|
|
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>
|
|
);
|
|
}
|
|
}
|