My aggregated monorepo of OCaml code, automaintained

Fix SPA navigation breaking custom element initialization

Two bugs in navigateTo() prevented x-ocaml cells from working after
SPA sidebar navigation:

1. innerHTML assignment — replaced with document.adoptNode() + appendChild
so custom elements get properly connected and connectedCallback fires

2. Script cloneNode — cloned script elements are marked "parser-inserted"
and browsers refuse to execute them. Use document.createElement('script')
instead to create fresh executable script elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+43 -11
+41 -8
odoc-jons-plugins/src/odoc_jons_plugins_js.ml
··· 103 103 var html = await response.text(); 104 104 var doc = parser.parseFromString(html, 'text/html'); 105 105 106 - // Swap content 106 + // Swap content — use DOM node adoption instead of innerHTML so that 107 + // custom elements (e.g. <x-ocaml>) get properly connected and their 108 + // connectedCallback fires. 107 109 var newContent = doc.querySelector('.odoc-content'); 108 - if (newContent) { 109 - document.querySelector('.odoc-content').innerHTML = newContent.innerHTML; 110 + var oldContent = document.querySelector('.odoc-content'); 111 + if (newContent && oldContent) { 112 + oldContent.textContent = ''; 113 + while (newContent.firstChild) { 114 + oldContent.appendChild(document.adoptNode(newContent.firstChild)); 115 + } 110 116 } 111 117 112 118 // Update title ··· 137 143 return new URL(existingUrl, window.location.href).href === abs; 138 144 }); 139 145 if (!already) { 140 - var clone = el.cloneNode(true); 141 - clone.setAttribute(attr, abs); 146 + var node; 142 147 if (el.tagName === 'SCRIPT') { 148 + // Must create a fresh script element — cloneNode creates a 149 + // "parser-inserted" script that browsers refuse to execute. 150 + node = document.createElement('script'); 151 + node.src = abs; 143 152 var p = new Promise(function(resolve) { 144 - clone.onload = resolve; 145 - clone.onerror = resolve; 153 + node.onload = resolve; 154 + node.onerror = resolve; 146 155 }); 147 156 newScriptLoadPromises.push(p); 157 + } else { 158 + node = el.cloneNode(true); 159 + node.setAttribute(attr, abs); 148 160 } 149 - document.head.appendChild(clone); 161 + document.head.appendChild(node); 150 162 } 151 163 }); 152 164 ··· 258 270 navigateTo(targetUrl); 259 271 }); 260 272 273 + // Sidebar toggle 274 + function initSidebarToggle() { 275 + var btn = document.querySelector('.jon-shell-sidebar-toggle'); 276 + if (!btn) return; 277 + var sidebar = document.querySelector('.jon-shell-sidebar'); 278 + if (!sidebar) { btn.style.display = 'none'; return; } 279 + 280 + // Restore preference 281 + if (localStorage.getItem('jon-shell-sidebar') === 'hidden') { 282 + document.body.classList.add('sidebar-hidden'); 283 + } 284 + 285 + btn.addEventListener('click', function() { 286 + document.body.classList.toggle('sidebar-hidden'); 287 + var hidden = document.body.classList.contains('sidebar-hidden'); 288 + localStorage.setItem('jon-shell-sidebar', hidden ? 'hidden' : 'visible'); 289 + }); 290 + } 291 + 261 292 // Initialize 262 293 (function() { 263 294 // Mark header for SPA detection ··· 272 303 if (inlineData) { 273 304 initSidebar(inlineData); 274 305 } 306 + 307 + initSidebarToggle(); 275 308 })(); 276 309 |}
+2 -3
test/e2e/spa-navigation.spec.js
··· 20 20 test('sidebar navigation to interactive page initializes cells', async ({ 21 21 page, 22 22 }) => { 23 - // Known bug: SPA navigation uses innerHTML which breaks custom element 24 - // instantiation. Mark as expected failure. 25 - test.fail(); 23 + // Fixed: SPA navigation now uses document.adoptNode() instead of innerHTML 24 + // so custom elements get properly connected. 26 25 27 26 // Start on a non-interactive page within /reference/ 28 27 await page.goto('/reference/odoc-interactive-extension/index.html');