Skip to content

fix: guard TOC navigation against re-entry (#63)#69

Merged
eXeLearningProject merged 6 commits into
mainfrom
63-fix-toc-navigation-loop
Jun 4, 2026
Merged

fix: guard TOC navigation against re-entry (#63)#69
eXeLearningProject merged 6 commits into
mainfrom
63-fix-toc-navigation-loop

Conversation

@erseco
Copy link
Copy Markdown
Collaborator

@erseco erseco commented May 9, 2026

Summary

Speculative fix for #63. Tagging this draft because I have not been able to reproduce the issue locally on Moodle 5.0, but the code path I'm patching is the only thing in module.js that matches every detail of the bug report.

Root-cause hypothesis

`exescorm_activate_item()` (the function that loads the SCO matching the clicked TOC entry) calls `exescorm_tree_node.closeAll()` and later `exescorm_tree_node.openAll()` to collapse and re-expand the whole YUI TreeView. Those calls mutate node selection state and YUI fires additional `select` events while the tree settles, which the `tree.after('select', ...)` handler then interprets as a fresh navigation request and calls `exescorm_activate_item()` again with a different node (typically a sibling).

That maps cleanly onto every detail Ignacio described:

  • "in many cases you don't see the section you clicked on" → second `activate_item()` call replaces the iframe src.
  • "the iframe content is loaded more than once: a page seems to load and then the content is refreshed" → exactly two iframe creations per click.
  • "clicking 'a 2 1' loads 'a 2 2'" → next sibling is the most likely target of a spurious `select` after a closeAll/openAll on a parent.

It also explains why I can't reproduce on Moodle 5.0: YUI TreeView's exact event ordering changed between versions, and the spurious `select` may simply not fire on the build I tested.

Fix

Single-flag re-entry guard:

  • `exescorm_navigating = true` set right before `closeAll()` (covers the `select()`/`closeAll()`/`openAll()` window).
  • `tree.after('select', ...)` returns early when the flag is set, so only the genuine user click drives navigation.
  • `exescorm_navigating = false` cleared right after `openAll()`.

17 added lines, no behavioural change for the non-buggy path.

Test plan

  • Reproduce on Moodle 4.5 first: install the example SCORM ZIP, open it, click "a 2 1" and verify the iframe stops at `html/a-2-1.html` (no second load to `a-2-2.html`).
  • Click each TOC entry once and verify the iframe loads exactly once per click (DevTools → Network).
  • Click a parent that has children (e.g. "a 2"): the parent's content loads once and the tree stays expanded.
  • Repeat the previous three checks on Moodle 5.0 to make sure the guard doesn't break the working flow.

Sibling repo

`mod_exeweb` doesn't share this code path (its viewer renders the package as a single iframe with no TOC), so no parallel PR is needed there. Per `AGENTS.md`'s twin-plugin checklist, audited `mod_exeweb/amd/src/editor_modal.js` and confirmed.


Moodle Playground Preview

The changes in this pull request can be previewed and tested using a Moodle Playground instance.

Preview in Moodle Playground

⚠️ The embedded eXeLearning editor is not included in this preview. You can install it from Modules > eXeLearning SCORM > Configure using the "Download & Install Editor" button. All other module features (ELPX upload, viewer, preview) work normally.

When the user clicks a TOC entry, `exescorm_activate_item()` calls
`exescorm_tree_node.closeAll()` and later `openAll()` to collapse and
re-expand the entire YUI TreeView. Those operations mutate node
selection state and YUI fires additional `select` events while the
tree settles, which the `tree.after('select', ...)` handler then
interprets as a fresh navigation request and calls
`exescorm_activate_item()` again with a *different* node (typically a
sibling). The visible symptoms reported in #63 — the iframe loading
twice and "click X, see Y" — line up with that pattern.

Add an `exescorm_navigating` flag set right before the first state
change and cleared after `openAll()`. The select handler returns early
when the flag is set, so only the original user click drives the
navigation.

The fix is defensive: I have not been able to reproduce locally on
Moodle 5.0, but every signal in the report (intermittent, multiple
iframe loads, sibling shown instead of clicked item) points at
re-entry through this code path. If the symptom persists after
upgrading we'll need to capture a console trace inside the failing
session.
@erseco erseco requested a review from ignaciogros May 9, 2026 17:16
@erseco erseco self-assigned this May 9, 2026
@erseco erseco added the bug Something isn't working label May 9, 2026
Copy link
Copy Markdown
Collaborator

@ignaciogros ignaciogros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigation menu works correctly, including keyboard support. Thank you!

But there are still some issues with the pagination buttons:

“Previous”, “Next”, and “Level up” work as expected, but “Previous within this level” and “Next within this level” do not.

Example:
Publish the sample SCORM and go to section “b”. The expected behavior when clicking “Next within this level” would be to navigate to section “c”, but this does not happen (in my tests, nothing happens). Additionally, when being on “b”, “Previous within this level” is disabled. This may be because index.html is not at the same level. In other cases, those buttons behave like “Previous” and “Next”, without respecting the hierarchical level.

If this becomes too problematic, I suggest temporarily hiding these buttons and releasing the next version with only “Previous” and “Next”.

There is also a JavaScript error when loading the page, but it does not appear to affect functionality:
[moodle-exe-bridge] Missing __MOODLE_EXE_CONFIG__

Copy link
Copy Markdown
Collaborator

@ignaciogros ignaciogros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @erseco,

Could you please take a look at it? Navigation may be problematic. I think that if it is not possible to resolve this soon, it might be a good idea to hide the #nav_skipprev and #nav_skipnext buttons.

What do you think?

Thank you.

exescorm_update_siblings grouped entries by treating each scoesnav key as a
candidate parentscoid. Top-level sections have no parentscoid at all (they are
the roots of the displayed organization; the organization node has no
launchable URL and is absent from scoesnav), so they never matched and never
received prevsibling/nextsibling. As a result exescorm_skipprev/exescorm_skipnext
returned null for them and the "Previous within this level" / "Next within this
level" buttons did nothing at the top level (they still worked at deeper levels,
where parentscoid is set).

Group every entry by its parentscoid value instead, bucketing the parentless
top-level roots together under a sentinel key, then link each group as siblings.
Deeper levels are unaffected (same groups as before) and exescorm_get_siblings
still only fills in undefined prevsibling/nextsibling, preserving server-computed
links.

Verified on Moodle 4.5 with the example SCORM from #63: on section "b",
"Next within this level" now navigates to "c" and "Previous within this level"
to "a"; deeper-level navigation and the TOC re-entry guard are unchanged.
@erseco
Copy link
Copy Markdown
Collaborator Author

erseco commented Jun 3, 2026

@ignaciogros thanks for the thorough testing — you were right, and I've now fixed the "within this level" buttons (#nav_skipprev / #nav_skipnext) rather than hiding them.

Root cause

This was a pre-existing bug, separate from the re-entry guard. exescorm_skipprev/exescorm_skipnext rely on prevsibling/nextsibling metadata in scoes_nav. That metadata is filled in by exescorm_update_siblings() in module.js, which grouped entries by treating each existing scoes_nav key as a candidate parentscoid.

The catch: top-level sections (a, b, c) have no parentscoid at all — they are the roots of the displayed organization, and the organization node itself has no launchable URL so it isn't in scoes_nav. They therefore never matched any group and never got prevsibling/nextsibling. That's exactly why:

  • on b, "Next within this level" did nothing (no nextsiblingskipnext returned null), and
  • "Previous within this level" was disabled (no prevsibling).

Deeper levels (e.g. a 1 / a 2 / a 3) did work because those children carry a parentscoid, which is why the behaviour looked inconsistent.

Fix

exescorm_update_siblings() now groups every entry by its parentscoid value, bucketing the parentless top-level roots together under a sentinel key, then links each group as siblings. Deeper levels produce the same groups as before, and exescorm_get_siblings() still only fills in undefined prevsibling/nextsibling, so server-computed links are preserved. The #63 re-entry guard is untouched.

Verified on Moodle 4.5 (Docker, your sample SCORM)

  • On b"Next within this level" now navigates to c, "Previous within this level" to a.
  • a correctly has no previous-sibling target and c no next-sibling target (first/last at that level).
  • Deeper-level "within this level" navigation still works.
  • TOC menu (the Wrong content displayed when clicking on navigation links #63 fix) still loads the correct page once per click — no regression.

The [moodle-exe-bridge] Missing __MOODLE_EXE_CONFIG__ console line is unrelated to this code path (harmless, separate component) — happy to track it in its own issue.

Could you re-test when you have a moment? Re-requesting your review. Thanks!

@erseco erseco requested a review from ignaciogros June 3, 2026 18:47
Copy link
Copy Markdown
Collaborator

@ignaciogros ignaciogros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @erseco.

It works much better now, but I think there are still some issues.

Please download the example SCORM ZIP and go to page "b1" (or another page that is the first one of its level). You'll see that the "Previous within this level" button is enabled, when there's no previous page in that level.

It happens the same with some of the last pages of a level: go to "a 2 2", and you'll see that the "Next within this level" button es enabled, but if you go to "b 2", it's not.

The "Previous/Next within this level" buttons (#nav_skipprev / #nav_skipnext)
stayed enabled at the first/last item of a level, and inconsistently so
(e.g. "a 2 2" enabled but "b 2" disabled). Their disabled state was derived
from exescorm_skipprev()/exescorm_skipnext(), which climb to the parent when
the current item has no same-level sibling (plain Previous/Next reuse that
fallback) — so at a level boundary the button resolved to a parent-level node
and looked navigable.

Gate the disabled state of both buttons on the authoritative prevsibling /
nextsibling metadata in scoes_nav, so they are disabled exactly when there is
no previous/next sibling at the current level. When a sibling exists the
behaviour is unchanged; Previous/Next/Up and the #63 re-entry guard are
untouched.

Verified on Moodle 5.0.5 with the sample SCORM: all 12 TOC nodes now show the
correct within-level button states, the buttons navigate to the right sibling,
and each TOC click still loads its page exactly once (no #63 regression).
@erseco
Copy link
Copy Markdown
Collaborator Author

erseco commented Jun 4, 2026

@ignaciogros thanks again — you nailed it. Fixed in 05d077e and re-tested on Moodle 5.0.5 (Docker) with your sample SCORM.

Root cause

The disabled state of the "within this level" buttons (#nav_skipprev / #nav_skipnext) was derived from exescorm_skipprev() / exescorm_skipnext(). Those functions climb to the parent when the current item has no same-level sibling — plain "Previous"/"Next" reuse them as their climbing engine — so at a level boundary the button resolved to a parent-level node and stayed enabled. That's also why it was inconsistent: on a 2 2 the parent (a 2) had a reachable next sibling so it looked navigable, while on b 2 it didn't.

Fix

The disabled state of each "within this level" button is now gated on the authoritative prevsibling / nextsibling metadata in scoes_nav, i.e. the button is disabled exactly when there is no previous/next sibling at the current level. When a sibling exists the behaviour is unchanged; Previous / Next / Level up and the #63 re-entry guard are untouched.

Verified (your sample SCORM, all 12 TOC nodes)

  • b 1, a 2 1, b 1, c 1 (first of their level) → "Previous within this level" now disabled.
  • a 2 2, a 3, b 2, c, c 2 (last of their level) → "Next within this level" now disabled (consistently this time).
  • When enabled, the buttons jump to the correct sibling: a 2a 3 / a 1, bc.
  • Every TOC click still loads its page exactly once — no Wrong content displayed when clicking on navigation links #63 regression.

Before / after, on b 1 (first of its level — "Previous within this level" = « ):

Before (wrongly enabled) After (correctly disabled)
« solid blue / clickable « greyed out

Re-requesting your review. Thanks!

@erseco erseco requested a review from ignaciogros June 4, 2026 08:05
Copy link
Copy Markdown
Collaborator

@ignaciogros ignaciogros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@eXeLearningProject eXeLearningProject merged commit 064f73b into main Jun 4, 2026
1 check passed
@eXeLearningProject eXeLearningProject deleted the 63-fix-toc-navigation-loop branch June 4, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants