* fix: Margin on floating toolbar fix: Flash of toolbar on wide screens * fix: Nesting of comment marks * fix: Post button not visible when there is a draft comment, makes it look like the comment is saved fix: Styling of link editor results now matches other menus fix: Allow small link editor in comments sidebar * fix: Cannot use arrow keys to navigate suggested links Added animation to link suggestions Added mixin for text ellipsis * fix: Link input appears non-rounded when no creation option * Accidental removal
150 lines
3.6 KiB
TypeScript
150 lines
3.6 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 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;
|
|
onCreateLink?: (title: string) => Promise<string>;
|
|
onSearchLink?: (term: string) => Promise<SearchResult[]>;
|
|
onClickLink: (
|
|
href: string,
|
|
event: React.MouseEvent<HTMLButtonElement>
|
|
) => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
function isActive(view: EditorView, active: boolean): boolean {
|
|
try {
|
|
const { selection } = view.state;
|
|
const paragraph = view.domAtPos(selection.from);
|
|
return active && !!paragraph.node;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
useEventListener("mousedown", (event: Event) => {
|
|
if (
|
|
event.target instanceof HTMLElement &&
|
|
menuRef.current &&
|
|
menuRef.current.contains(event.target)
|
|
) {
|
|
return;
|
|
}
|
|
onClose();
|
|
});
|
|
|
|
const handleOnCreateLink = React.useCallback(
|
|
async (title: string) => {
|
|
onClose();
|
|
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: showToast,
|
|
dictionary,
|
|
});
|
|
},
|
|
[onCreateLink, onClose, view, dictionary, showToast]
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
dispatch(
|
|
view.state.tr
|
|
.insertText(title, from, to)
|
|
.addMark(
|
|
from,
|
|
to + title.length,
|
|
state.schema.marks.link.create({ href })
|
|
)
|
|
);
|
|
},
|
|
[onClose, view]
|
|
);
|
|
|
|
const { selection } = view.state;
|
|
const active = isActive(view, rest.isActive);
|
|
|
|
return (
|
|
<FloatingToolbar ref={menuRef} active={active} width={336}>
|
|
{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}
|
|
onSearchLink={onSearchLink}
|
|
dictionary={dictionary}
|
|
view={view}
|
|
/>
|
|
)}
|
|
</FloatingToolbar>
|
|
);
|
|
}
|