import { schema } from "components/Blocks/TextBlock/schema"; import { EditorState, TextSelection } from "prosemirror-state"; import { useUIState } from "src/useUIState"; import { ToolbarButton } from "."; import { useEffect, useState } from "react"; import { Separator } from "components/Layout"; import { MarkType } from "prosemirror-model"; import { setEditorState, useEditorStates } from "src/state/useEditorState"; import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; import { Input } from "components/Input"; import { useReplicache } from "src/replicache"; import { CheckTiny } from "components/Icons/CheckTiny"; import { LinkSmall } from "components/Icons/LinkSmall"; export function LinkButton(props: { setToolbarState: (s: "link") => void }) { let focusedBlock = useUIState((s) => s.focusedEntity); let focusedEditor = useEditorStates((s) => focusedBlock ? s.editorStates[focusedBlock.entityID] : null, ); let isLink; if (focusedEditor) { let { to, from, $cursor } = focusedEditor.editor.selection as TextSelection; if ($cursor) isLink = !!schema.marks.link.isInSet($cursor.marks()); if (to !== from) isLink = !!rangeHasMark( focusedEditor.editor, schema.marks.link, from, to, ); } return ( { e.preventDefault(); props.setToolbarState("link"); }} disabled={ !focusedEditor || (focusedEditor?.editor.selection.empty && !isLink) } tooltipContent={
Inline Link
} >
); } export function InlineLinkToolbar(props: { onClose: () => void }) { let focusedBlock = useUIState((s) => s.focusedEntity); let focusedEditor = useEditorStates((s) => focusedBlock ? s.editorStates[focusedBlock.entityID] : null, ); let { undoManager } = useReplicache(); useEffect(() => { if (focusedEditor) { let isLink; let { to, from, $cursor } = focusedEditor.editor .selection as TextSelection; if ($cursor) isLink = !!schema.marks.link.isInSet($cursor.marks()); if (to !== from) isLink = !!rangeHasMark( focusedEditor.editor, schema.marks.link, from, to, ); if (isLink) return; } if (focusedEditor?.editor.selection.empty) props.onClose(); }, [focusedEditor, props]); let content = ""; let start: number | null = null; let end: number | null = null; if (focusedEditor) { let { to, from, $cursor } = focusedEditor.editor.selection as TextSelection; if (to !== from) { start = from; end = to; } else { let markRange = findMarkRange(focusedEditor.editor, schema.marks.link); start = markRange.start; end = markRange.end; } if ($cursor) { let link = $cursor.marks().find((f) => f.type === schema.marks.link); if (link) { content = link.attrs.href; } } } let [linkValue, setLinkValue] = useState(content); let setLink = () => { let href = !linkValue.startsWith("http") && !linkValue.startsWith("mailto") && !linkValue.startsWith("tel:") ? `https://${linkValue}` : linkValue; let editor = focusedEditor?.editor; if (!editor || start === null || !end || !focusedBlock) return; let tr = editor.tr; tr.addMark(start, end, schema.marks.link.create({ href })); tr.setSelection(TextSelection.create(tr.doc, tr.selection.to)); let oldState = editor; let newState = editor.apply(tr); undoManager.add({ undo: () => { if (!focusedEditor?.view?.hasFocus()) focusedEditor?.view?.focus(); setEditorState(focusedBlock.entityID, { editor: oldState, }); }, redo: () => { if (!focusedEditor?.view?.hasFocus()) focusedEditor?.view?.focus(); setEditorState(focusedBlock.entityID, { editor: newState, }); }, }); setEditorState(focusedBlock?.entityID, { editor: newState, }); props.onClose(); }; return (
setLinkValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); setLink(); } if (e.key === "Escape") { props.onClose(); } }} /> {/* TODO: to avoid all sort of messiness, editing any portion of link will edit the entire range that includes the link rather than just the link text that is selected. ALSO TODO: if there is already a link mark, the input should be prefilled with the link value and the check mark should be a garbage can to remove the link. if the user changes the link, then the button reverts to a check mark. */}
); } function findMarkRange(state: EditorState, markType: MarkType) { const { from, $from } = state.selection; // Find the start of the mark let start = from; let startPos = $from; while ( startPos.parent.inlineContent && startPos.nodeBefore && startPos.nodeBefore.marks.some((mark) => mark.type === markType) ) { start -= startPos.nodeBefore.nodeSize; startPos = state.doc.resolve(start); } // Find the end of the mark let end = from; let endPos = $from; while ( endPos.parent.inlineContent && endPos.nodeAfter && endPos.nodeAfter.marks.some((mark) => mark.type === markType) ) { end += endPos.nodeAfter.nodeSize; endPos = state.doc.resolve(end); } return { start, end }; }