Single page Application rewrite#1383
Open
AsherMaximum wants to merge 20 commits into
Open
Conversation
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 ' ' is not rendered by the browser; removing
…f new shelves pages
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
The sidebar is resizeable:
Shelves and Magic Shelves can be collapsed. State is saved to browser session:
And pages to display all the shelves or magic shelves added as well:
Changed spinner for restart dialog to a css spinner, and added polling to check when server comes back online:
Additional changes: