Skip to content

Commit

Permalink
Add ProseMirror view's "separator hack".
Browse files Browse the repository at this point in the history
Safari and Chrome both have cursor drawing/selection bugs that
prevent users from making selections after non-contenteditable
inline nodes in some situations. To work around this, in these
browsers, we add an empty image element between the trailing non-
contenteditable node and the "trailing hack", which allows users
to place cursors there.
  • Loading branch information
smoores-dev committed Mar 15, 2024
1 parent 24c43c9 commit db1e891
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 25 deletions.
8 changes: 4 additions & 4 deletions demo/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,17 @@ const ListItem = forwardRef(function ListItem(

const Footnote = forwardRef(function Footnote(
{ nodeProps, ...props }: NodeViewComponentProps,
ref: Ref<HTMLButtonElement>
ref: Ref<HTMLSpanElement>
) {
return (
<button
<span
ref={ref}
{...props}
suppressContentEditableWarning
contentEditable="false"
>
{nodeProps.node.attrs.number}
</button>
<button>{nodeProps.node.attrs.number}</button>
</span>
);
});

Expand Down
34 changes: 17 additions & 17 deletions docs/assets/index-0b42cd6e.js → docs/assets/index-7c1043a3.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React-ProseMirror Demo</title>
<script type="module" crossorigin src="/react-prosemirror/assets/index-0b42cd6e.js"></script>
<script type="module" crossorigin src="/react-prosemirror/assets/index-7c1043a3.js"></script>
<link rel="stylesheet" href="/react-prosemirror/assets/index-523693cd.css">
</head>
<body>
Expand Down
7 changes: 5 additions & 2 deletions src/components/ChildNodeViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useReactKeys } from "../hooks/useReactKeys.js";
import { MarkView } from "./MarkView.js";
import { NativeWidgetView } from "./NativeWidgetView.js";
import { NodeView } from "./NodeView.js";
import { SeparatorHackView } from "./SeparatorHackView.js";
import { TextNodeView } from "./TextNodeView.js";
import { TrailingHackView } from "./TrailingHackView.js";
import { WidgetView } from "./WidgetView.js";
Expand Down Expand Up @@ -437,8 +438,10 @@ export function ChildNodeViews({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
/\n$/.test(lastChild.node.text!)
) {
// TODO: ProseMirror also adds an img hack after non-contenteditable views
childElements.push(<TrailingHackView key="trailing-hack" />);
childElements.push(
<SeparatorHackView key="trailing-hack-img" />,
<TrailingHackView key="trailing-hack-br" />
);
}

return <>{childElements}</>;
Expand Down
34 changes: 34 additions & 0 deletions src/components/SeparatorHackView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useContext, useLayoutEffect, useRef, useState } from "react";

import { browser } from "../browser.js";
import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
import { TrailingHackViewDesc } from "../viewdesc.js";

export function SeparatorHackView() {
const siblingDescriptors = useContext(ChildDescriptorsContext);
const ref = useRef<HTMLImageElement | null>(null);
const [shouldRender, setShouldRender] = useState(false);

// There's no risk of an infinite loop here, because
// we call setShouldRender conditionally
// eslint-disable-next-line react-hooks/exhaustive-deps
useLayoutEffect(() => {
const lastSibling = siblingDescriptors[siblingDescriptors.length - 1];
if (
(browser.safari || browser.chrome) &&
(lastSibling?.dom as HTMLElement)?.contentEditable == "false"
) {
setShouldRender(true);
return;
}

if (!ref.current) return;

const desc = new TrailingHackViewDesc(undefined, [], ref.current, null);
siblingDescriptors.push(desc);
});

return shouldRender ? (
<img ref={ref} className="ProseMirror-separator" />
) : null;
}
2 changes: 1 addition & 1 deletion src/components/TrailingHackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TrailingHackViewDesc } from "../viewdesc.js";

export function TrailingHackView() {
const siblingDescriptors = useContext(ChildDescriptorsContext);
const ref = useRef<HTMLBRElement | null>(null);
const ref = useRef<(HTMLBRElement & HTMLImageElement) | null>(null);

useLayoutEffect(() => {
if (!ref.current) return;
Expand Down

0 comments on commit db1e891

Please sign in to comment.