Skip to content

Commit 7d00bb1

Browse files
committed
Made popup show state only based on selection
1 parent 5dc9879 commit 7d00bb1

2 files changed

Lines changed: 45 additions & 139 deletions

File tree

packages/math-block/src/inlineContent/InlineMathPreview.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ export const InlineMathPreview = (
2626
const { store } = useExtension(SourceInlineContentWithPreviewExtension, {
2727
editor,
2828
});
29-
const popupOpen = useExtensionState(SourceInlineContentWithPreviewExtension, {
30-
editor,
31-
selector: (state) => state.popupOpen === pos,
32-
});
29+
// The popup is open exactly when the selection is inside this inline content,
30+
// which is the same condition that marks it as selected.
3331
const selected = useExtensionState(SourceInlineContentWithPreviewExtension, {
3432
editor,
3533
selector: (state) => state.selected === pos,
@@ -50,7 +48,7 @@ export const InlineMathPreview = (
5048
return;
5149
}
5250

53-
store.setState((state) => ({ ...state, popupOpen: pos }));
51+
store.setState({ selected: pos });
5452

5553
event.preventDefault();
5654
event.stopPropagation();
@@ -68,7 +66,7 @@ export const InlineMathPreview = (
6866
<span
6967
ref={rootRef}
7068
className={"bn-inline-source-content"}
71-
data-open={popupOpen ? "true" : "false"}
69+
data-open={selected ? "true" : "false"}
7270
>
7371
<span
7472
className="bn-inline-source-preview"

packages/math-block/src/inlineContent/SourceInlineContentWithPreviewExtension.ts

Lines changed: 41 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { BlockNoteEditor, createExtension, createStore } from "@blocknote/core";
2-
import { Selection, TextSelection } from "prosemirror-state";
2+
import { Selection } from "prosemirror-state";
33

44
/**
55
* Inline-content counterpart of {@link SourceBlockWithPreviewExtension}. Drives
6-
* the source popup for inline content with a preview: toggling it with the
7-
* keyboard, navigating out of the (hidden) source, and preventing edits to it
8-
* while it's hidden.
6+
* the source popup for inline content with a preview.
97
*
10-
* Unlike the block version, its store holds a single `open` flag rather than a
11-
* block ID - at most one inline content can hold the cursor, and the render
12-
* component decides whether the open popup is its own by checking if the cursor
13-
* is inside its source.
8+
* Unlike the block version, the popup isn't toggled with a separate state flag:
9+
* it's open exactly when the selection is inside the inline content's source.
10+
* The store therefore only tracks which inline content (by its position) holds
11+
* the selection - moving the selection in opens its popup, moving it out closes
12+
* it. Since the source popup is always laid out (just hidden via opacity), the
13+
* cursor can navigate into and out of it with the arrow keys as usual.
1414
*/
1515
export const SourceInlineContentWithPreviewExtension = createExtension(
1616
({
@@ -25,149 +25,57 @@ export const SourceInlineContentWithPreviewExtension = createExtension(
2525
};
2626
}) => {
2727
const store = createStore<{
28-
popupOpen: number | undefined;
2928
selected: number | undefined;
3029
}>({
31-
popupOpen: undefined,
3230
selected: undefined,
3331
});
3432

35-
const handleArrow =
36-
(direction: "before" | "after") =>
37-
({ editor }: { editor: BlockNoteEditor<any, any, any> }) => {
38-
if (store.state.popupOpen) {
39-
return false;
40-
}
41-
42-
const { $from } = editor.prosemirrorState.selection;
43-
const node = $from.node();
44-
if (node.type.name !== inlineContentType) {
45-
return false;
46-
}
47-
48-
const view = editor.prosemirrorView!;
49-
const target = direction === "before" ? $from.before() : $from.after();
50-
const selection = Selection.near(
51-
view.state.doc.resolve(target),
52-
direction === "before" ? -1 : 1,
53-
);
54-
55-
view.dispatch(view.state.tr.setSelection(selection));
56-
57-
return true;
58-
};
33+
// Moves the selection out of (just after) the inline content, which closes
34+
// the popup via the selection-change handler below. Lets the keyboard
35+
// commit-and-exit the source the same way arrowing past its end does, and
36+
// keeps Enter from splitting the block while editing the source.
37+
const moveSelectionOut = ({
38+
editor,
39+
}: {
40+
editor: BlockNoteEditor<any, any, any>;
41+
}) => {
42+
const { $from } = editor.prosemirrorState.selection;
43+
const node = $from.node();
44+
if (node.type.name !== inlineContentType) {
45+
return false;
46+
}
47+
48+
const view = editor.prosemirrorView!;
49+
const selection = Selection.near(
50+
view.state.doc.resolve($from.after()),
51+
1,
52+
);
53+
view.dispatch(view.state.tr.setSelection(selection));
54+
55+
return true;
56+
};
5957

6058
return {
6159
key,
6260
store,
6361
runsBefore,
6462
keyboardShortcuts: {
65-
// Toggles the popup.
66-
Enter: ({ editor }) => {
67-
const { $from } = editor.prosemirrorState.selection;
68-
const node = $from.node();
69-
if (node.type.name !== inlineContentType) {
70-
return false;
71-
}
72-
73-
const view = editor.prosemirrorView!;
74-
view.dispatch(
75-
view.state.tr.setSelection(
76-
TextSelection.create(view.state.tr.doc, $from.end()),
77-
),
78-
);
79-
80-
store.setState((state) => ({
81-
...state,
82-
popupOpen: state.popupOpen ? undefined : $from.before(),
83-
}));
84-
85-
return true;
86-
},
87-
// Closes the popup.
88-
Escape: ({ editor }) => {
89-
const { $from } = editor.prosemirrorState.selection;
90-
const node = $from.node();
91-
if (node.type.name !== inlineContentType) {
92-
return false;
93-
}
94-
95-
const view = editor.prosemirrorView!;
96-
view.dispatch(
97-
view.state.tr.setSelection(
98-
TextSelection.create(view.state.tr.doc, $from.end()),
99-
),
100-
);
101-
102-
store.setState((state) => ({ ...state, popupOpen: undefined }));
103-
104-
return true;
105-
},
106-
// While the popup is closed, moves the selection straight out of the inline
107-
// content rather than navigating its hidden source.
108-
ArrowUp: handleArrow("before"),
109-
ArrowLeft: handleArrow("before"),
110-
ArrowDown: handleArrow("after"),
111-
ArrowRight: handleArrow("after"),
63+
Enter: moveSelectionOut,
64+
Escape: moveSelectionOut,
11265
},
113-
mount: ({ dom, signal }) => {
66+
mount: ({ signal }) => {
67+
// The popup is open exactly when the selection is inside the inline
68+
// content, so we just track which inline content (if any) holds it.
11469
const unsubscribeSelectionChange = editor.onSelectionChange(() => {
11570
const { $from } = editor.prosemirrorState.selection;
11671
const node = $from.node();
117-
const before = $from.before();
11872

119-
store.setState((state) => ({
120-
selected: node.type.name === inlineContentType ? before : undefined,
121-
popupOpen:
122-
state.popupOpen && state.popupOpen !== before
123-
? undefined
124-
: state.popupOpen,
125-
}));
73+
store.setState({
74+
selected:
75+
node.type.name === inlineContentType ? $from.before() : undefined,
76+
});
12677
});
12778
signal.addEventListener("abort", unsubscribeSelectionChange);
128-
129-
const handleKeyDown = (event: KeyboardEvent) => {
130-
const view = editor.prosemirrorView;
131-
if (!editor.isEditable || !view) {
132-
return;
133-
}
134-
135-
const isTypedChar =
136-
event.key.length === 1 && !event.ctrlKey && !event.metaKey;
137-
138-
// While the popup is open, the source is shown and editable, so edits
139-
// are let through. Boundary edits on the (possibly empty) source - an
140-
// empty inline node can't hold a text cursor, so typing/deleting at
141-
// the boundary needs special handling - are handled generically by the
142-
// editor's `InlineContentBoundaryEdit` extension.
143-
if (store.state.popupOpen !== undefined) {
144-
return;
145-
}
146-
147-
// While the popup is closed, prevents editing of the (hidden) source.
148-
// Handled here rather than in `keyboardShortcuts` as it needs to match
149-
// any text-input key, which a keymap can't express.
150-
if (
151-
editor.prosemirrorState.selection.$from.node().type.name !==
152-
inlineContentType
153-
) {
154-
return;
155-
}
156-
157-
if (
158-
isTypedChar ||
159-
event.key === "Backspace" ||
160-
event.key === "Delete" ||
161-
event.key === "Tab"
162-
) {
163-
event.preventDefault();
164-
event.stopImmediatePropagation();
165-
}
166-
};
167-
dom.addEventListener("keydown", handleKeyDown, {
168-
capture: true,
169-
signal,
170-
});
17179
},
17280
};
17381
},

0 commit comments

Comments
 (0)