Skip to content

refactor: Remirror link extension #393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 3 additions & 326 deletions ui/src/components/nodes/Rich.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { ResizableBox } from "react-resizable";
import { useApolloClient } from "@apollo/client";

import { useStore } from "zustand";
import { RepoContext } from "../../lib/store";

import ReactFlow, {
addEdge,
Expand Down Expand Up @@ -48,7 +47,6 @@ import {
DropCursorExtension,
ImageExtension,
ItalicExtension,
LinkExtension as RemirrorLinkExtension,
PlaceholderExtension,
ShortcutHandlerProps,
SubExtension,
Expand Down Expand Up @@ -130,296 +128,16 @@ import {
TaskListExtension,
} from "./extensions/list";

import { NewPodButtons, level2fontsize } from "./utils";

class LinkExtension extends RemirrorLinkExtension {
createInputRules(): InputRule[] {
return [
markInputRule({
regexp: /\[([^\]]+)\]\(([^)]+)\)/,
type: this.type,
getAttributes: (matches: string[]) => {
const [_, text, href] = matches;
return { text: text, href: href };
},
}),
markInputRule({
regexp: /(?:==|__)([^*_]+)(?:==|__)$/,
type: this.type,
ignoreWhitespace: true,
}),
];
}
}

function useLinkShortcut() {
const [linkShortcut, setLinkShortcut] = useState<
ShortcutHandlerProps | undefined
>();
const [isEditing, setIsEditing] = useState(false);

useExtensionEvent(
LinkExtension,
"onShortcut",
useCallback(
(props) => {
if (!isEditing) {
setIsEditing(true);
}

return setLinkShortcut(props);
},
[isEditing]
)
);

return { linkShortcut, isEditing, setIsEditing };
}

function useFloatingLinkState() {
const chain = useChainedCommands();
const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut();
const { to, empty } = useCurrentSelection();

const url = (useAttrs().link()?.href as string) ?? "";
const [href, setHref] = useState<string>(url);

// A positioner which only shows for links.
const linkPositioner = React.useMemo(
() => createMarkPositioner({ type: "link" }),
[]
);

const onRemove = useCallback(() => {
return chain.removeLink().focus().run();
}, [chain]);

const updateReason = useUpdateReason();

React.useLayoutEffect(() => {
if (!isEditing) {
return;
}

if (updateReason.doc || updateReason.selection) {
setIsEditing(false);
}
}, [isEditing, setIsEditing, updateReason.doc, updateReason.selection]);

useEffect(() => {
setHref(url);
}, [url]);

const submitHref = useCallback(() => {
setIsEditing(false);
const range = linkShortcut ?? undefined;

if (href === "") {
chain.removeLink();
} else {
chain.updateLink({ href, auto: false }, range);
}

chain.focus(range?.to ?? to).run();
}, [setIsEditing, linkShortcut, chain, href, to]);

const cancelHref = useCallback(() => {
setIsEditing(false);
}, [setIsEditing]);

const clickEdit = useCallback(() => {
if (empty) {
chain.selectLink();
}

setIsEditing(true);
}, [chain, empty, setIsEditing]);

return React.useMemo(
() => ({
href,
setHref,
linkShortcut,
linkPositioner,
isEditing,
clickEdit,
onRemove,
submitHref,
cancelHref,
}),
[
href,
linkShortcut,
linkPositioner,
isEditing,
clickEdit,
onRemove,
submitHref,
cancelHref,
]
);
}

const DelayAutoFocusInput = ({
autoFocus,
...rest
}: React.HTMLProps<HTMLInputElement>) => {
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (!autoFocus) {
return;
}

const frame = window.requestAnimationFrame(() => {
inputRef.current?.focus();
});

return () => {
window.cancelAnimationFrame(frame);
};
}, [autoFocus]);

return <input ref={inputRef} {...rest} />;
};

function useUpdatePositionerOnMove() {
// Update (all) the positioners whenever there's a move (pane) on reactflow,
// so that the toolbar moves with the Rich pod and content.
const { forceUpdatePositioners, emptySelection } = useCommands();
const store = useContext(RepoContext);
if (!store) throw new Error("Missing BearContext.Provider in the tree");
const moved = useStore(store, (state) => state.moved);
const clicked = useStore(store, (state) => state.clicked);
useEffect(() => {
forceUpdatePositioners();
}, [moved]);
useEffect(() => {
emptySelection();
}, [clicked]);
return;
}

/**
* This is a two-buttons toolbar when user click on a link. The first button
* edits the link, the second button opens the link.
*/
const LinkToolbar = () => {
const {
isEditing,
linkPositioner,
clickEdit,
onRemove,
submitHref,
href,
setHref,
cancelHref,
} = useFloatingLinkState();
useUpdatePositionerOnMove();
const { empty } = useCurrentSelection();

const handleClickEdit = useCallback(() => {
clickEdit();
}, [clickEdit]);

return (
<>
{!isEditing && empty && (
// By default, MUI's Popper creates a Portal, which is a ROOT html
// elements that prevents paning on reactflow canvas. Therefore, we
// disable the portal behavior.
<FloatingToolbar
disablePortal
sx={{
button: {
padding: 0,
border: "none",
borderRadius: "5px",
marginLeft: "5px",
},
paddingX: "4px",
border: "2px solid grey",
borderRadius: "5px",
alignItems: "center",
backgroundColor: "white",
}}
// The default positinoer will cause the toolbar only show on text
// selection. This linkPositioner allows the toolbar to be shown
// without any text selection
positioner={linkPositioner}
>
<CommandButton
commandName="updateLink"
aria-label="Edit link"
onSelect={handleClickEdit}
icon="pencilLine"
enabled
/>
<CommandButton
commandName="removeLink"
aria-label="Open link"
onSelect={() => {
window.open(href, "_blank");
}}
icon="externalLinkFill"
enabled
/>
</FloatingToolbar>
)}

<FloatingWrapper
positioner="always"
placement="bottom"
enabled={isEditing}
renderOutsideEditor
>
<DelayAutoFocusInput
style={{ zIndex: 20 }}
autoFocus
placeholder="Enter link..."
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setHref(event.target.value)
}
value={href}
onKeyPress={(event: React.KeyboardEvent<HTMLInputElement>) => {
const { code } = event;

if (code === "Enter") {
submitHref();
}
import { LinkExtension, LinkToolbar } from "./extensions/link";

if (code === "Escape") {
cancelHref();
}
}}
/>
</FloatingWrapper>
</>
);
};
import { NewPodButtons, level2fontsize } from "./utils";
import { RepoContext } from "../../lib/store";

/**
* This is the toolbar when user select some text. It allows user to change the
* markups of the text, e.g. bold, italic, underline, highlight, etc.
*/
const EditorToolbar = () => {
useUpdatePositionerOnMove();
const {
isEditing,
linkPositioner,
clickEdit,
onRemove,
submitHref,
href,
setHref,
cancelHref,
} = useFloatingLinkState();
const active = useActive();
const activeLink = active.link();
const handleClickEdit = useCallback(() => {
clickEdit();
}, [clickEdit]);

return (
<>
<FloatingToolbar
Expand All @@ -446,15 +164,6 @@ const EditorToolbar = () => {
<ToggleUnderlineButton />
<ToggleStrikeButton />
<ToggleCodeButton />
{!activeLink && (
<CommandButton
commandName="updateLink"
aria-label="Add link"
onSelect={handleClickEdit}
icon="link"
enabled
/>
)}
<SetHighlightButton color="lightpink" />
<SetHighlightButton color="yellow" />
<SetHighlightButton color="lightgreen" />
Expand All @@ -465,34 +174,6 @@ const EditorToolbar = () => {
{/* <IndentationButtonGroup /> */}
{/* <BaselineButtonGroup /> */}
</FloatingToolbar>

<FloatingWrapper
positioner="always"
placement="bottom"
enabled={isEditing}
renderOutsideEditor
>
<DelayAutoFocusInput
style={{ zIndex: 20 }}
autoFocus
placeholder="Enter link..."
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setHref(event.target.value)
}
value={href}
onKeyPress={(event: React.KeyboardEvent<HTMLInputElement>) => {
const { code } = event;

if (code === "Enter") {
submitHref();
}

if (code === "Escape") {
cancelHref();
}
}}
/>
</FloatingWrapper>
</>
);
};
Expand Down Expand Up @@ -589,10 +270,6 @@ const MyEditor = ({
new TextHighlightExtension(),
new SupExtension(),
new SubExtension(),
new LinkExtension({
autoLink: true,
autoLinkAllowedTLDs: ["dev", ...TOP_50_TLDS],
}),
new MarkdownExtension(),
new MyYjsExtension({ getProvider: () => provider, id }),
new MathInlineExtension(),
Expand Down
Loading