Skip to content

Single page Application rewrite#1383

Open
AsherMaximum wants to merge 20 commits into
crocodilestick:mainfrom
AsherMaximum:single-page
Open

Single page Application rewrite#1383
AsherMaximum wants to merge 20 commits into
crocodilestick:mainfrom
AsherMaximum:single-page

Conversation

@AsherMaximum
Copy link
Copy Markdown

@AsherMaximum AsherMaximum commented May 29, 2026

Overhaul of the page navigation system.

Each page now only reloads the main content section of the application, displaying a loading spinner while the contents are fetched in the background:

image

 
The sidebar is resizeable:

image

 
Shelves and Magic Shelves can be collapsed. State is saved to browser session:

image

 
And pages to display all the shelves or magic shelves added as well:

image

 
Changed spinner for restart dialog to a css spinner, and added polling to check when server comes back online:

image image

Additional changes:

  • URLs cleaned up to remove unneccessary parameters, and all sidebar entries now are highlighted when they are navigated to
  • Back button hooks into browser's back button
    • Each navigation is added to browser history

Adds the SPA shell-and-fragment architecture so navigation between pages
replaces only the #main-content area without a full browser reload.

Core pieces:
- cwa_spa_shell.html: serves the outer chrome once per session; all
  subsequent navigations swap the content area only
- partial-nav.js: intercepts same-domain link clicks and form submits,
  fetches the route as a fragment (X-CWA-Fragment header), injects the
  response into #main-content, and updates pushState / document.title /
  sidebar active state; excluded paths (/admin, /opds, /read/<int>,
  /download/<int>, auth routes, etc.) fall through to normal browser nav
- All content templates adopt a conditional parent_template so they render
  as standalone fragments when fetched by partial-nav, or extend the full
  layout on direct load; book_table.html, shelf_edit.html, tasks.html,
  and detail.html were converted as part of this work
- fragment.html: the fragment-mode parent; imports all shared macros
  (modal_dialogs, etc.) that layout.html provides, so fragment-served
  templates have access to delete_book and friends without 500s
- Removes the bookDetailsModal pop-up; book covers now navigate via SPA
  to /book/<id> as a real page swap, not a Bootstrap modal XHR load
A collection of correctness fixes so the DOM produced by a fragment
swap is indistinguishable from what a direct full-page load produces.

- Re-runnable init hooks: caliBlur, main.js, and sidebar state are
  wrapped in window.cwaInit callbacks invoked after each swap, not only
  on DOMContentLoaded
- Body class sync: fragment.html emits <meta name="x-cwa-body-class">;
  partial-nav removes the previous page's body class and applies the
  new one so body.<page> selectors in caliBlur.css activate correctly
- Header block: fragment.html renders {% block header %} inline so
  per-page <style> tags (e.g. .book-detail-card) reach the swapped DOM
- HTML5 parser fix: DOMParser reparented leading <meta>/<style> nodes
  into doc.head; they are now collected and prepended to the injected
  content explicitly
- Container structure: removed an extra .container-fluid wrapper that
  broke caliBlur's deeply-specific CSS child-chain selectors on
  list/detail pages, causing all section headings to lose their styling
- Pagination block: added to fragment.html so book-list pages include
  page-number controls under SPA navigation
- Body class seeding: layout.html emits x-cwa-body-class in <head> so
  partial-nav correctly strips full-render page classes (e.g. body.admin)
  on the next SPA swap, not just spa_shell
- Books-table re-init: table.js initialisation extracted into a cwaInit
  hook so the books-table re-initialises on every fragment swap, not
  only on page load
- Script attribute preservation: executeScripts copies all attributes
  when cloning inline scripts; fixes details.js template carriers
  (<script type="text/template" id="template-shelf-add">) whose id
  selectors returned empty after the attribute strip, causing
  underscore's template() to throw on the book-detail page
Two classes of routes bypass the normal SPA flow and required special
handling.

Excluded paths (/admin/*, /opds/*, login, etc.) are served as full
layout-extending pages. partial-nav.js was unconditionally bootstrapping
a second fragment fetch on load, re-injecting the full chrome into
#main-content (sidebar-inside-content) and replacing DOM nodes that
DOMContentLoaded handlers had already bound to — that is why the
Restart-confirmation OK button silently did nothing after an admin page
load. Bootstrap is now gated on window.__cwaInitialPath, which only
cwa_spa_shell.html sets; excluded pages skip the bootstrap fetch
entirely while still supporting SPA-eligible link clicks.

Unauthenticated visitors hitting a protected route previously received
the SPA shell, then saw the loading overlay while the fragment fetch
followed the login_required redirect across the exclusion boundary —
a visible flash before ending up at /login. spa_before_request now
skips the shell when current_user is not authenticated (and
g.allow_anonymous is false), so Flask-Login's redirect to /login is
the very first response.
Fixes and polish discovered while exercising the SPA in practice.

Navigation and routing:
- Sidebar active state: route page values ("newest", "ratings",
  "formats", "book_table") did not match sidebar config entries
  ("root", "rating", "format", "list"); normalised the mapping and
  added data-page attributes on each <li> for direct matching; falls
  back to anchor pathname for per-shelf items; resolves the URL before
  pushState so the fallback matches the final destination
- Search redirect: /search returns 302 → /search/stored/; partial-nav
  was treating this as a cross-boundary handoff and triggering a full
  reload; same-boundary redirected fragments are now injected directly
  with replaceState to the final URL
- Mobile sidebar: fragment swaps left the .navbar-collapse open on
  ≤768px screens; navigateTo now closes any open collapse before
  arming the loading overlay
- Document title: detail.html unified to parent_template conditional;
  fragment.html emits <meta name="x-cwa-title"> and partial-nav reads
  it after each swap to keep document.title in sync

Feature fixes:
- Shelf operations: delete-shelf button rebound as a delegated handler
  so it survives fragment swaps; #GeneralDeleteModal block rendered in
  fragment.html so the confirm dialog is present on SPA loads;
  create/edit magic-shelf now redirects to the new/saved shelf rather
  than bouncing to the index; create-shelf active highlight fixed with
  an !important rule to win over caliBlur's .create-shelf a colour
- Sequential script load: magic-shelf's query-builder was loaded async
  and the inline init script fired before the library resolved;
  executeScripts now chains external scripts via onload and
  injectFragment returns a promise so cwaInit.runAll waits for all
  scripts before running init hooks

Pre-existing bug also fixed here:
- Flash fade-out: caliBlur's cssAnimation keyframe only collapsed
  width/height, leaving padding and background visible as a pill;
  zeroing padding/border/margin and setting visibility:hidden cleans
  up the close animation
The '&nbsp' is not rendered by the browser; removing
Advanced search results had many duplicates, resulting in incorrect count, and each page of books not being the total amount given due to duplicate removal after results
The mouseup-anywhere handler in caliBlur.js called $.hide() to dismiss
any open dropdown. For Bootstrap-managed dropdowns this created two
problems: the handler fired before Bootstrap's click handler could add
.open, so the inline display:none beat the .open .dropdown-menu rule
and the menu opened invisibly; and after Bootstrap cleared .open the
inline style lingered, so the menu was invisible on every subsequent
toggle.

Close Bootstrap-managed dropdowns via removeClass('open') +
aria-expanded update instead of inline display:none. Non-Bootstrap
custom menus keep the .hide() fallback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant