Files
Tom Moor 0f3f7b8da7 refactor: Remove useDictionary hook in favor of i18next t directly (#12282)
Plumbed `dictionary` props through editor components, menus, extensions,
and nodes. Replaces with `useTranslation()` in React contexts and direct
`t` imports from i18next elsewhere.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 20:24:50 -04:00

274 lines
7.4 KiB
TypeScript

import { t } from "i18next";
import { chainCommands, toggleMark } from "prosemirror-commands";
import type { Attrs } from "prosemirror-model";
import type { Command } from "prosemirror-state";
import { NodeSelection, Selection, TextSelection } from "prosemirror-state";
import type * as React from "react";
import { getMarkRange } from "../queries/getMarkRange";
import { toast } from "sonner";
import { sanitizeUrl } from "@shared/utils/urls";
import { getMarkRangeNodeSelection } from "../queries/getMarkRange";
import type { NodeAttrMark } from "@shared/editor/types";
const addLinkTextSelection =
(attrs: Attrs): Command =>
(state, dispatch) => {
if (!(state.selection instanceof TextSelection)) {
return false;
}
dispatch?.(
state.tr
.setSelection(TextSelection.create(state.doc, state.tr.selection.to))
.addMark(
state.selection.from,
state.selection.to,
state.schema.marks.link.create(attrs)
)
);
return true;
};
const addLinkNodeSelection =
(attrs: Attrs): Command =>
(state, dispatch) => {
if (!(state.selection instanceof NodeSelection)) {
return false;
}
const { selection } = state;
const existingMarks = selection.node.attrs.marks ?? [];
const newMark = {
type: "link",
attrs,
};
const updatedMarks = [...existingMarks, newMark];
dispatch?.(
state.tr.setNodeAttribute(selection.from, "marks", updatedMarks)
);
return true;
};
const openLinkTextSelection =
(
onClickLink:
| ((
url: string,
event?:
| KeyboardEvent
| MouseEvent
| React.MouseEvent<HTMLButtonElement>
) => void)
| undefined
): Command =>
(state) => {
if (!(state.selection instanceof TextSelection)) {
return false;
}
const range = getMarkRange(state.selection.$from, state.schema.marks.link);
if (range && range.mark && onClickLink) {
try {
const event = new KeyboardEvent("keydown", { metaKey: false });
onClickLink(sanitizeUrl(range.mark.attrs.href) ?? "", event);
} catch (_err) {
toast.error(t("Sorry, that type of link is not supported"));
}
return true;
}
return false;
};
const openLinkNodeSelection =
(
onClickLink:
| ((
url: string,
event?:
| KeyboardEvent
| MouseEvent
| React.MouseEvent<HTMLButtonElement>
) => void)
| undefined
): Command =>
(state) => {
if (!(state.selection instanceof NodeSelection)) {
return false;
}
if (!onClickLink) {
return false;
}
const marks = state.selection.node.attrs.marks ?? [];
const linkMark = marks.find((mark: NodeAttrMark) => mark.type === "link");
if (!linkMark) {
return false;
}
try {
const event = new KeyboardEvent("keydown", { metaKey: false });
onClickLink(sanitizeUrl(linkMark.attrs.href) ?? "", event);
} catch (_err) {
toast.error(t("Sorry, that type of link is not supported"));
}
return true;
};
const updateLinkTextSelection =
(attrs: Attrs): Command =>
(state, dispatch) => {
if (!(state.selection instanceof TextSelection)) {
return false;
}
const range = getMarkRange(state.selection.$from, state.schema.marks.link);
if (range && range.mark) {
const nextSelection =
Selection.findFrom(state.doc.resolve(range.to), 1, true) ??
TextSelection.create(state.tr.doc, 0);
dispatch?.(
state.tr
.setSelection(nextSelection)
.removeMark(range.from, range.to, state.schema.marks.link)
.addMark(range.from, range.to, state.schema.marks.link.create(attrs))
);
return true;
}
return false;
};
const updateLinkNodeSelection =
(attrs: Attrs): Command =>
(state, dispatch) => {
if (!(state.selection instanceof NodeSelection)) {
return false;
}
const markRange = getMarkRangeNodeSelection(
state.selection,
state.schema.marks.link
);
if (!markRange) {
return false;
}
const existingMarks = state.selection.node.attrs.marks ?? [];
const updatedMarks = existingMarks.map((mark: NodeAttrMark) =>
mark.type === "link"
? { ...mark, attrs: { ...mark.attrs, ...attrs } }
: mark
);
const nextValidSelection =
Selection.findFrom(state.doc.resolve(markRange.to), 1, true) ??
TextSelection.create(state.tr.doc, 0);
dispatch?.(
state.tr
.setSelection(nextValidSelection)
.setNodeAttribute(state.selection.from, "marks", updatedMarks)
);
return true;
};
const removeLinkTextSelection = (): Command => (state, dispatch) => {
if (!(state.selection instanceof TextSelection)) {
return false;
}
const range = getMarkRange(state.selection.$from, state.schema.marks.link);
if (range && range.mark) {
const nextSelection =
Selection.findFrom(state.doc.resolve(range.to), 1, true) ??
TextSelection.create(state.tr.doc, 0);
dispatch?.(
state.tr
.setSelection(nextSelection)
.removeMark(range.from, range.to, range.mark)
);
return true;
}
return false;
};
const removeLinkNodeSelection = (): Command => (state, dispatch) => {
if (!(state.selection instanceof NodeSelection)) {
return false;
}
const markRange = getMarkRangeNodeSelection(
state.selection,
state.schema.marks.link
);
if (!markRange) {
return false;
}
const existingMarks = state.selection.node.attrs.marks ?? [];
const updatedMarks = existingMarks.filter(
(mark: NodeAttrMark) => mark.type !== "link"
);
const nextValidSelection =
Selection.findFrom(state.doc.resolve(markRange.to), 1, true) ??
TextSelection.create(state.tr.doc, 0);
dispatch?.(
state.tr
.setSelection(nextValidSelection)
.setNodeAttribute(state.selection.from, "marks", updatedMarks)
);
return true;
};
const toggleLinkTextSelection =
(attrs: Attrs): Command =>
(state, dispatch) => {
if (!(state.selection instanceof TextSelection)) {
return false;
}
return toggleMark(state.schema.marks.link, attrs)(state, dispatch);
};
const toggleLinkNodeSelection =
(attrs: Attrs): Command =>
(state, dispatch) => {
if (!(state.selection instanceof NodeSelection)) {
return false;
}
const existingMarks = state.selection.node.attrs.marks ?? [];
const linkMark = existingMarks.find(
(mark: NodeAttrMark) => mark.type === "link"
);
if (linkMark) {
return removeLinkNodeSelection()(state, dispatch);
} else {
return addLinkNodeSelection(attrs)(state, dispatch);
}
};
export const toggleLink = (attrs: Attrs): Command =>
chainCommands(toggleLinkTextSelection(attrs), toggleLinkNodeSelection(attrs));
export const addLink = (attrs: Attrs): Command =>
chainCommands(addLinkTextSelection(attrs), addLinkNodeSelection(attrs));
export const openLink = (
onClickLink:
| ((
url: string,
event?: KeyboardEvent | MouseEvent | React.MouseEvent<HTMLButtonElement>
) => void)
| undefined
): Command =>
chainCommands(
openLinkTextSelection(onClickLink),
openLinkNodeSelection(onClickLink)
);
export const updateLink = (attrs: Attrs): Command =>
chainCommands(updateLinkTextSelection(attrs), updateLinkNodeSelection(attrs));
export const removeLink = (): Command =>
chainCommands(removeLinkTextSelection(), removeLinkNodeSelection());