Skip to content
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
17 changes: 17 additions & 0 deletions packages/platform/src/editor/ScriptureReferencePlugin.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ describe("ScriptureReferencePlugin", () => {

expect(mockOnScrRefChange).not.toHaveBeenCalled();
});

it("should not echo the stale document's book when scrRef navigates to a different book while the old document is still loaded", async () => {
// Content stays on GEN (the old document hasn't been swapped in yet - simulates the
// window between a host navigating across books and the new book's USJ finishing its
// async load). scrRef.book already matches the content at mount, so no sync fires yet.
const { setScrRef } = await testEnvironment(scrRef, mockOnScrRefChange);

// Host navigates across books (here GEN -> EXO): scrRef now targets a different book (and
// a verse that still exists in the stale document, isolating this test to the book-sync
// listener rather than the separate cursor-placement/BCV path), but the editor still
// contains the OLD book's BookNode.
await setScrRef({ book: "EXO", chapterNum: 1, verseNum: 2 });

// The plugin must not echo the stale document's book back to the host combined with the
// new chapter/verse - that would corrupt the navigation (e.g. EXO 1:2 -> GEN 1:2).
expect(mockOnScrRefChange).not.toHaveBeenCalled();
});
});

describe("Selection Change", () => {
Expand Down
95 changes: 53 additions & 42 deletions packages/platform/src/editor/ScriptureReferencePlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,55 +60,66 @@ export default function ScriptureReferencePlugin({
onScrRefChangeRef.current = onScrRefChange;
}, [scrRef, onScrRefChange]);

// Book node: move cursor when a new BookNode appears after load; sync scrRef.book when the
// book code changes (initial tree + updates). Uses a second listener with skipInitialization:
// false so existing BookNodes from initial editor state are included (updateListener ran on
// every keystroke; this only runs when BookNodes are created/updated).
// Book node created: move cursor to the verse start once a new BookNode appears after the
// initial load (e.g. a different book's document has finished mounting). Re-registers on every
// chapterNum/verseNum change so the closure captures the latest target position; safe to
// re-register because skipInitialization: true means re-registering never replays existing
// BookNodes, only genuinely new "created" mutations going forward.
useEffect(
() =>
mergeRegister(
editor.registerMutationListener(
BookNode,
(nodeMutations) => {
editor.update(
() => {
for (const [nodeKey, mutation] of nodeMutations) {
const bookNode = $getNodeByKey<BookNode>(nodeKey);
if (bookNode && $isBookNode(bookNode) && mutation === "created") {
$moveCursorToVerseStart(chapterNum, verseNum, hasCursorMovedRef);
}
editor.registerMutationListener(
BookNode,
(nodeMutations) => {
editor.update(
() => {
for (const [nodeKey, mutation] of nodeMutations) {
const bookNode = $getNodeByKey<BookNode>(nodeKey);
if (bookNode && $isBookNode(bookNode) && mutation === "created") {
$moveCursorToVerseStart(chapterNum, verseNum, hasCursorMovedRef);
}
},
{ tag: CURSOR_CHANGE_TAG },
);
},
{ skipInitialization: true },
),
editor.registerMutationListener(
BookNode,
(nodeMutations) => {
editor.update(
() => {
for (const [nodeKey, mutation] of nodeMutations) {
if (mutation === "destroyed") continue;
const bookNode = $getNodeByKey<BookNode>(nodeKey);
if (!bookNode || !$isBookNode(bookNode)) continue;
const code = bookNode.getCode();
const current = scrRefRef.current;
if (code && code !== current.book) {
onScrRefChangeRef.current({ ...current, book: code });
}
}
},
{ tag: CURSOR_CHANGE_TAG },
);
},
{ skipInitialization: false },
),
}
},
{ tag: CURSOR_CHANGE_TAG },
);
},
{ skipInitialization: true },
),
[editor, chapterNum, verseNum],
);

// Book node sync: correct scrRef.book to match the currently loaded document's BookNode.
// Fires on every non-destroyed BookNode mutation (initial-mount replay + later swaps); the
// `code !== current.book` guard below is what makes stray firings harmless. Registered once
// per editor ([editor] only), NOT re-registered on chapterNum/verseNum change: the refs
// already keep values fresh, and re-registering would replay skipInitialization: false against
// the still-mounted (stale) document on every navigation, echoing the OLD book with the NEW
// chapter/verse back to the host.
useEffect(
() =>
editor.registerMutationListener(
BookNode,
(nodeMutations) => {
editor.update(
() => {
for (const [nodeKey, mutation] of nodeMutations) {
if (mutation === "destroyed") continue;
const bookNode = $getNodeByKey<BookNode>(nodeKey);
if (!bookNode || !$isBookNode(bookNode)) continue;
const code = bookNode.getCode();
const current = scrRefRef.current;
if (code && code !== current.book) {
onScrRefChangeRef.current({ ...current, book: code });
}
}
},
{ tag: CURSOR_CHANGE_TAG },
);
},
{ skipInitialization: false },
),
[editor],
);

// Scripture Reference changed
useEffect(() => {
if (hasSelectionChangedRef.current) hasSelectionChangedRef.current = false;
Expand Down
Loading