Fix SX history, OOB header swaps, cross-service nav components
- Always re-fetch on popstate (drop LRU cache) for fresh content on back/forward - Save/restore scroll position via pushState - Add id="root-header-child" to ~app-body so OOB swaps can target it - Fix OOB renderers: nest root-row inside root-header-child swap instead of separate OOB that clobbers it - Fix 3+ header rows dropped: wrap all headers in single fragment instead of concatenating outside (<> ...) - Strip <script data-components> from text/sx responses before renderToString - Fall back to location.assign for cross-origin pushState (SecurityError) - Move blog/sx/nav.sx to shared/sx/templates/ so all services have nav components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,12 @@ from shared.sx.helpers import (
|
|||||||
load_service_components(os.path.dirname(os.path.dirname(__file__)))
|
load_service_components(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
def _ctx_csrf(ctx: dict) -> str:
|
||||||
|
"""Get CSRF token from context, handling Jinja callable globals."""
|
||||||
|
val = ctx.get("csrf_token", "")
|
||||||
|
return val() if callable(val) else val
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# OOB header helper — delegates to shared
|
# OOB header helper — delegates to shared
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -269,7 +275,7 @@ def _blog_card_sx(post: dict, ctx: dict) -> str:
|
|||||||
if user:
|
if user:
|
||||||
kwargs["liked"] = post.get("is_liked", False)
|
kwargs["liked"] = post.get("is_liked", False)
|
||||||
kwargs["like_url"] = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
kwargs["like_url"] = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
||||||
kwargs["csrf_token"] = ctx.get("csrf_token", "")
|
kwargs["csrf_token"] = _ctx_csrf(ctx)
|
||||||
|
|
||||||
if tags:
|
if tags:
|
||||||
kwargs["tags"] = tags
|
kwargs["tags"] = tags
|
||||||
@@ -692,7 +698,7 @@ def _post_main_panel_sx(ctx: dict) -> str:
|
|||||||
like_url = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
like_url = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
||||||
like_sx = sx_call("blog-detail-like",
|
like_sx = sx_call("blog-detail-like",
|
||||||
like_url=like_url,
|
like_url=like_url,
|
||||||
hx_headers=f'{{"X-CSRFToken": "{ctx.get("csrf_token", "")}"}}',
|
hx_headers=f'{{"X-CSRFToken": "{_ctx_csrf(ctx)}"}}',
|
||||||
heart="\u2764\ufe0f" if liked else "\U0001f90d",
|
heart="\u2764\ufe0f" if liked else "\U0001f90d",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -790,7 +796,7 @@ def _settings_main_panel_sx(ctx: dict) -> str:
|
|||||||
def _cache_main_panel_sx(ctx: dict) -> str:
|
def _cache_main_panel_sx(ctx: dict) -> str:
|
||||||
from quart import url_for as qurl
|
from quart import url_for as qurl
|
||||||
|
|
||||||
csrf = ctx.get("csrf_token", "")
|
csrf = _ctx_csrf(ctx)
|
||||||
clear_url = qurl("settings.cache_clear")
|
clear_url = qurl("settings.cache_clear")
|
||||||
return sx_call("blog-cache-panel", clear_url=clear_url, csrf=csrf)
|
return sx_call("blog-cache-panel", clear_url=clear_url, csrf=csrf)
|
||||||
|
|
||||||
@@ -810,7 +816,7 @@ def _snippets_list_sx(ctx: dict) -> str:
|
|||||||
|
|
||||||
snippets = ctx.get("snippets") or []
|
snippets = ctx.get("snippets") or []
|
||||||
is_admin = ctx.get("is_admin", False)
|
is_admin = ctx.get("is_admin", False)
|
||||||
csrf = ctx.get("csrf_token", "")
|
csrf = _ctx_csrf(ctx)
|
||||||
user = getattr(g, "user", None)
|
user = getattr(g, "user", None)
|
||||||
user_id = getattr(user, "id", None)
|
user_id = getattr(user, "id", None)
|
||||||
|
|
||||||
@@ -881,7 +887,7 @@ def _menu_items_list_sx(ctx: dict) -> str:
|
|||||||
from quart import url_for as qurl
|
from quart import url_for as qurl
|
||||||
|
|
||||||
menu_items = ctx.get("menu_items") or []
|
menu_items = ctx.get("menu_items") or []
|
||||||
csrf = ctx.get("csrf_token", "")
|
csrf = _ctx_csrf(ctx)
|
||||||
|
|
||||||
if not menu_items:
|
if not menu_items:
|
||||||
return sx_call("blog-menu-items-empty")
|
return sx_call("blog-menu-items-empty")
|
||||||
@@ -919,7 +925,7 @@ def _tag_groups_main_panel_sx(ctx: dict) -> str:
|
|||||||
|
|
||||||
groups = ctx.get("groups") or []
|
groups = ctx.get("groups") or []
|
||||||
unassigned_tags = ctx.get("unassigned_tags") or []
|
unassigned_tags = ctx.get("unassigned_tags") or []
|
||||||
csrf = ctx.get("csrf_token", "")
|
csrf = _ctx_csrf(ctx)
|
||||||
|
|
||||||
create_url = qurl("blog.tag_groups_admin.create")
|
create_url = qurl("blog.tag_groups_admin.create")
|
||||||
form_sx = sx_call("blog-tag-groups-create-form",
|
form_sx = sx_call("blog-tag-groups-create-form",
|
||||||
@@ -979,7 +985,7 @@ def _tag_groups_edit_main_panel_sx(ctx: dict) -> str:
|
|||||||
group = ctx.get("group")
|
group = ctx.get("group")
|
||||||
all_tags = ctx.get("all_tags") or []
|
all_tags = ctx.get("all_tags") or []
|
||||||
assigned_tag_ids = ctx.get("assigned_tag_ids") or set()
|
assigned_tag_ids = ctx.get("assigned_tag_ids") or set()
|
||||||
csrf = ctx.get("csrf_token", "")
|
csrf = _ctx_csrf(ctx)
|
||||||
|
|
||||||
g_id = getattr(group, "id", None) or group.get("id") if group else None
|
g_id = getattr(group, "id", None) or group.get("id") if group else None
|
||||||
g_name = getattr(group, "name", "") if hasattr(group, "name") else (group.get("name", "") if group else "")
|
g_name = getattr(group, "name", "") if hasattr(group, "name") else (group.get("name", "") if group else "")
|
||||||
@@ -1046,12 +1052,12 @@ async def render_home_page(ctx: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def render_home_oob(ctx: dict) -> str:
|
async def render_home_oob(ctx: dict) -> str:
|
||||||
root_hdr = root_header_sx(ctx, oob=True)
|
root_hdr = root_header_sx(ctx)
|
||||||
post_oob = _oob_header_sx("root-header-child", "post-header-child",
|
post_hdr = _post_header_sx(ctx)
|
||||||
_post_header_sx(ctx))
|
rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||||
|
header_oob = _oob_header_sx("root-header-child", "post-header-child", rows)
|
||||||
content = _home_main_panel_sx(ctx)
|
content = _home_main_panel_sx(ctx)
|
||||||
oobs = "(<> " + root_hdr + " " + post_oob + ")"
|
return oob_page_sx(oobs=header_oob, content=content)
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Blog index ----
|
# ---- Blog index ----
|
||||||
@@ -1068,15 +1074,15 @@ async def render_blog_page(ctx: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def render_blog_oob(ctx: dict) -> str:
|
async def render_blog_oob(ctx: dict) -> str:
|
||||||
root_hdr = root_header_sx(ctx, oob=True)
|
root_hdr = root_header_sx(ctx)
|
||||||
blog_oob = _oob_header_sx("root-header-child", "blog-header-child",
|
blog_hdr = _blog_header_sx(ctx)
|
||||||
_blog_header_sx(ctx))
|
rows = "(<> " + root_hdr + " " + blog_hdr + ")"
|
||||||
|
header_oob = _oob_header_sx("root-header-child", "blog-header-child", rows)
|
||||||
content = _blog_main_panel_sx(ctx)
|
content = _blog_main_panel_sx(ctx)
|
||||||
aside = _blog_aside_sx(ctx)
|
aside = _blog_aside_sx(ctx)
|
||||||
filter_sx = _blog_filter_sx(ctx)
|
filter_sx = _blog_filter_sx(ctx)
|
||||||
nav = ctx.get("nav_sx", "") or ""
|
nav = ctx.get("nav_sx", "") or ""
|
||||||
oobs = "(<> " + root_hdr + " " + blog_oob + ")"
|
return oob_page_sx(oobs=header_oob, content=content, aside=aside,
|
||||||
return oob_page_sx(oobs=oobs, content=content, aside=aside,
|
|
||||||
filter=filter_sx, menu=nav)
|
filter=filter_sx, menu=nav)
|
||||||
|
|
||||||
|
|
||||||
@@ -1136,7 +1142,9 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) ->
|
|||||||
|
|
||||||
# Editor JS + init script
|
# Editor JS + init script
|
||||||
init_js = (
|
init_js = (
|
||||||
|
"console.log('[EDITOR-DEBUG] init script running');\n"
|
||||||
"(function() {\n"
|
"(function() {\n"
|
||||||
|
" console.log('[EDITOR-DEBUG] IIFE entered, mountEditor=', typeof window.mountEditor);\n"
|
||||||
" function applyEditorFontSize() {\n"
|
" function applyEditorFontSize() {\n"
|
||||||
" document.documentElement.style.fontSize = '62.5%';\n"
|
" document.documentElement.style.fontSize = '62.5%';\n"
|
||||||
" document.body.style.fontSize = '1.6rem';\n"
|
" document.body.style.fontSize = '1.6rem';\n"
|
||||||
@@ -1282,11 +1290,12 @@ async def render_new_post_page(ctx: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def render_new_post_oob(ctx: dict) -> str:
|
async def render_new_post_oob(ctx: dict) -> str:
|
||||||
root_hdr = root_header_sx(ctx, oob=True)
|
root_hdr = root_header_sx(ctx)
|
||||||
blog_oob = _blog_header_sx(ctx, oob=True)
|
blog_hdr = _blog_header_sx(ctx)
|
||||||
|
rows = "(<> " + root_hdr + " " + blog_hdr + ")"
|
||||||
|
header_oob = _oob_header_sx("root-header-child", "blog-header-child", rows)
|
||||||
content = ctx.get("editor_html", "")
|
content = ctx.get("editor_html", "")
|
||||||
oobs = "(<> " + root_hdr + " " + blog_oob + ")"
|
return oob_page_sx(oobs=header_oob, content=content)
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Post detail ----
|
# ---- Post detail ----
|
||||||
@@ -1303,12 +1312,13 @@ async def render_post_page(ctx: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def render_post_oob(ctx: dict) -> str:
|
async def render_post_oob(ctx: dict) -> str:
|
||||||
root_hdr = root_header_sx(ctx, oob=True)
|
root_hdr = root_header_sx(ctx) # non-OOB (nested inside root-header-child)
|
||||||
post_oob = _oob_header_sx("root-header-child", "post-header-child",
|
post_hdr = _post_header_sx(ctx)
|
||||||
_post_header_sx(ctx))
|
rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||||
|
post_oob = _oob_header_sx("root-header-child", "post-header-child", rows)
|
||||||
content = _post_main_panel_sx(ctx)
|
content = _post_main_panel_sx(ctx)
|
||||||
menu = ctx.get("nav_sx", "") or ""
|
menu = ctx.get("nav_sx", "") or ""
|
||||||
oobs = "(<> " + root_hdr + " " + post_oob + ")"
|
oobs = post_oob
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=menu)
|
return oob_page_sx(oobs=oobs, content=content, menu=menu)
|
||||||
|
|
||||||
|
|
||||||
@@ -1318,7 +1328,7 @@ async def render_post_admin_page(ctx: dict) -> str:
|
|||||||
root_hdr = root_header_sx(ctx)
|
root_hdr = root_header_sx(ctx)
|
||||||
post_hdr = _post_header_sx(ctx)
|
post_hdr = _post_header_sx(ctx)
|
||||||
admin_hdr = _post_admin_header_sx(ctx)
|
admin_hdr = _post_admin_header_sx(ctx)
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr
|
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
||||||
content = _post_admin_main_panel_sx(ctx)
|
content = _post_admin_main_panel_sx(ctx)
|
||||||
menu = ctx.get("nav_sx", "") or ""
|
menu = ctx.get("nav_sx", "") or ""
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
||||||
@@ -1341,7 +1351,7 @@ async def render_post_data_page(ctx: dict) -> str:
|
|||||||
root_hdr = root_header_sx(ctx)
|
root_hdr = root_header_sx(ctx)
|
||||||
post_hdr = _post_header_sx(ctx)
|
post_hdr = _post_header_sx(ctx)
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="data")
|
admin_hdr = _post_admin_header_sx(ctx, selected="data")
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr
|
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
||||||
content = _raw_html_sx(ctx.get("data_html", ""))
|
content = _raw_html_sx(ctx.get("data_html", ""))
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1358,7 +1368,7 @@ async def render_post_entries_page(ctx: dict) -> str:
|
|||||||
root_hdr = root_header_sx(ctx)
|
root_hdr = root_header_sx(ctx)
|
||||||
post_hdr = _post_header_sx(ctx)
|
post_hdr = _post_header_sx(ctx)
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="entries")
|
admin_hdr = _post_admin_header_sx(ctx, selected="entries")
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr
|
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
||||||
content = _raw_html_sx(ctx.get("entries_html", ""))
|
content = _raw_html_sx(ctx.get("entries_html", ""))
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1382,7 +1392,7 @@ async def render_post_edit_page(ctx: dict) -> str:
|
|||||||
root_hdr = root_header_sx(ctx)
|
root_hdr = root_header_sx(ctx)
|
||||||
post_hdr = _post_header_sx(ctx)
|
post_hdr = _post_header_sx(ctx)
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="edit")
|
admin_hdr = _post_admin_header_sx(ctx, selected="edit")
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr
|
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
||||||
content = _raw_html_sx(ctx.get("edit_html", ""))
|
content = _raw_html_sx(ctx.get("edit_html", ""))
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1399,7 +1409,7 @@ async def render_post_settings_page(ctx: dict) -> str:
|
|||||||
root_hdr = root_header_sx(ctx)
|
root_hdr = root_header_sx(ctx)
|
||||||
post_hdr = _post_header_sx(ctx)
|
post_hdr = _post_header_sx(ctx)
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="settings")
|
admin_hdr = _post_admin_header_sx(ctx, selected="settings")
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")" + admin_hdr
|
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
||||||
content = _raw_html_sx(ctx.get("settings_html", ""))
|
content = _raw_html_sx(ctx.get("settings_html", ""))
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1423,13 +1433,13 @@ async def render_settings_page(ctx: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def render_settings_oob(ctx: dict) -> str:
|
async def render_settings_oob(ctx: dict) -> str:
|
||||||
root_hdr = root_header_sx(ctx, oob=True)
|
root_hdr = root_header_sx(ctx)
|
||||||
settings_oob = _oob_header_sx("root-header-child", "root-settings-header-child",
|
settings_hdr = _settings_header_sx(ctx)
|
||||||
_settings_header_sx(ctx))
|
rows = "(<> " + root_hdr + " " + settings_hdr + ")"
|
||||||
|
header_oob = _oob_header_sx("root-header-child", "root-settings-header-child", rows)
|
||||||
content = _settings_main_panel_sx(ctx)
|
content = _settings_main_panel_sx(ctx)
|
||||||
menu = _settings_nav_sx(ctx)
|
menu = _settings_nav_sx(ctx)
|
||||||
oobs = "(<> " + root_hdr + " " + settings_oob + ")"
|
return oob_page_sx(oobs=header_oob, content=content, menu=menu)
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=menu)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Cache ----
|
# ---- Cache ----
|
||||||
@@ -1442,7 +1452,7 @@ async def render_cache_page(ctx: dict) -> str:
|
|||||||
"cache-row", "cache-header-child",
|
"cache-row", "cache-header-child",
|
||||||
qurl("settings.cache"), "refresh", "Cache", ctx,
|
qurl("settings.cache"), "refresh", "Cache", ctx,
|
||||||
)
|
)
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + cache_hdr
|
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + cache_hdr + ")"
|
||||||
content = _cache_main_panel_sx(ctx)
|
content = _cache_main_panel_sx(ctx)
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1471,7 +1481,7 @@ async def render_snippets_page(ctx: dict) -> str:
|
|||||||
"snippets-row", "snippets-header-child",
|
"snippets-row", "snippets-header-child",
|
||||||
qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx,
|
qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx,
|
||||||
)
|
)
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + snippets_hdr
|
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + snippets_hdr + ")"
|
||||||
content = _snippets_main_panel_sx(ctx)
|
content = _snippets_main_panel_sx(ctx)
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1500,7 +1510,7 @@ async def render_menu_items_page(ctx: dict) -> str:
|
|||||||
"menu_items-row", "menu_items-header-child",
|
"menu_items-row", "menu_items-header-child",
|
||||||
qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx,
|
qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx,
|
||||||
)
|
)
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + mi_hdr
|
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + mi_hdr + ")"
|
||||||
content = _menu_items_main_panel_sx(ctx)
|
content = _menu_items_main_panel_sx(ctx)
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1529,7 +1539,7 @@ async def render_tag_groups_page(ctx: dict) -> str:
|
|||||||
"tag-groups-row", "tag-groups-header-child",
|
"tag-groups-row", "tag-groups-header-child",
|
||||||
qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx,
|
qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx,
|
||||||
)
|
)
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + tg_hdr
|
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + tg_hdr + ")"
|
||||||
content = _tag_groups_main_panel_sx(ctx)
|
content = _tag_groups_main_panel_sx(ctx)
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
@@ -1559,7 +1569,7 @@ async def render_tag_group_edit_page(ctx: dict) -> str:
|
|||||||
"tag-groups-row", "tag-groups-header-child",
|
"tag-groups-row", "tag-groups-header-child",
|
||||||
qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx,
|
qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx,
|
||||||
)
|
)
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + ")" + tg_hdr
|
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + tg_hdr + ")"
|
||||||
content = _tag_groups_edit_main_panel_sx(ctx)
|
content = _tag_groups_edit_main_panel_sx(ctx)
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
this._advance(m[0].length);
|
this._advance(m[0].length);
|
||||||
var raw = m[0].slice(1, -1);
|
var raw = m[0].slice(1, -1);
|
||||||
return raw.replace(/\\n/g, "\n").replace(/\\t/g, "\t")
|
return raw.replace(/\\n/g, "\n").replace(/\\t/g, "\t")
|
||||||
.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
.replace(/\\"/g, '"').replace(/\\[/]/g, "/").replace(/\\\\/g, "\\");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyword
|
// Keyword
|
||||||
@@ -1115,8 +1115,14 @@
|
|||||||
}
|
}
|
||||||
var open = "<" + tag + attrs.join("") + ">";
|
var open = "<" + tag + attrs.join("") + ">";
|
||||||
if (VOID_ELEMENTS[tag]) return open;
|
if (VOID_ELEMENTS[tag]) return open;
|
||||||
|
var isRawText = (tag === "script" || tag === "style");
|
||||||
var inner = [];
|
var inner = [];
|
||||||
for (var ci = 0; ci < children.length; ci++) inner.push(renderStr(children[ci], env));
|
for (var ci = 0; ci < children.length; ci++) {
|
||||||
|
var child = children[ci];
|
||||||
|
if (isRawText && typeof child === "string") inner.push(child);
|
||||||
|
else if (isRawText && isSym(child)) inner.push(String(sxEval(child, env)));
|
||||||
|
else inner.push(renderStr(child, env));
|
||||||
|
}
|
||||||
return open + inner.join("") + "</" + tag + ">";
|
return open + inner.join("") + "</" + tag + ">";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1413,7 +1419,7 @@
|
|||||||
var PROCESSED = "_sxBound";
|
var PROCESSED = "_sxBound";
|
||||||
var VERBS = ["get", "post", "put", "delete", "patch"];
|
var VERBS = ["get", "post", "put", "delete", "patch"];
|
||||||
var DEFAULT_SWAP = "outerHTML";
|
var DEFAULT_SWAP = "outerHTML";
|
||||||
var HISTORY_MAX = 20;
|
|
||||||
|
|
||||||
function dispatch(el, name, detail) {
|
function dispatch(el, name, detail) {
|
||||||
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
|
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
|
||||||
@@ -1627,7 +1633,12 @@
|
|||||||
// Check for text/sx content type
|
// Check for text/sx content type
|
||||||
var ct = resp.headers.get("Content-Type") || "";
|
var ct = resp.headers.get("Content-Type") || "";
|
||||||
if (ct.indexOf("text/sx") >= 0) {
|
if (ct.indexOf("text/sx") >= 0) {
|
||||||
try { text = Sx.renderToString(text); }
|
try {
|
||||||
|
// Strip and load any <script type="text/sx" data-components> blocks
|
||||||
|
text = text.replace(/<script[^>]*type="text\/sx"[^>]*data-components[^>]*>([\s\S]*?)<\/script>/gi,
|
||||||
|
function (_, defs) { Sx.loadComponents(defs); return ""; });
|
||||||
|
text = Sx.renderToString(text.trim());
|
||||||
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error("sx.js render error:", err);
|
console.error("sx.js render error:", err);
|
||||||
return;
|
return;
|
||||||
@@ -1696,10 +1707,15 @@
|
|||||||
|
|
||||||
// History
|
// History
|
||||||
var pushUrl = el.getAttribute("sx-push-url");
|
var pushUrl = el.getAttribute("sx-push-url");
|
||||||
if (pushUrl === "true") {
|
if (pushUrl === "true" || (pushUrl && pushUrl !== "false")) {
|
||||||
history.pushState({ sxUrl: url }, "", url);
|
var pushTarget = pushUrl === "true" ? url : pushUrl;
|
||||||
} else if (pushUrl && pushUrl !== "false") {
|
try {
|
||||||
history.pushState({ sxUrl: pushUrl }, "", pushUrl);
|
history.pushState({ sxUrl: pushTarget, scrollY: window.scrollY }, "", pushTarget);
|
||||||
|
} catch (e) {
|
||||||
|
// Cross-origin pushState not allowed — full navigation
|
||||||
|
location.assign(pushTarget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(el, "sx:afterSwap", { target: target });
|
dispatch(el, "sx:afterSwap", { target: target });
|
||||||
@@ -1905,39 +1921,12 @@
|
|||||||
|
|
||||||
// ---- History manager --------------------------------------------------
|
// ---- History manager --------------------------------------------------
|
||||||
|
|
||||||
var _historyCache = {};
|
|
||||||
var _historyCacheKeys = [];
|
|
||||||
|
|
||||||
function _cacheCurrentPage() {
|
|
||||||
var key = location.href;
|
|
||||||
var main = document.getElementById("main-panel");
|
|
||||||
if (!main) return;
|
|
||||||
_historyCache[key] = main.innerHTML;
|
|
||||||
// LRU eviction
|
|
||||||
var idx = _historyCacheKeys.indexOf(key);
|
|
||||||
if (idx >= 0) _historyCacheKeys.splice(idx, 1);
|
|
||||||
_historyCacheKeys.push(key);
|
|
||||||
while (_historyCacheKeys.length > HISTORY_MAX) {
|
|
||||||
delete _historyCache[_historyCacheKeys.shift()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("popstate", function (e) {
|
window.addEventListener("popstate", function (e) {
|
||||||
var url = location.href;
|
var url = location.href;
|
||||||
// Try cache first
|
var main = document.getElementById("main-panel");
|
||||||
if (_historyCache[url]) {
|
if (!main) { location.reload(); return; }
|
||||||
var main = document.getElementById("main-panel");
|
|
||||||
if (main) {
|
|
||||||
main.innerHTML = _historyCache[url];
|
|
||||||
Sx.processScripts(main);
|
|
||||||
Sx.hydrate(main);
|
|
||||||
SxEngine.process(main);
|
|
||||||
dispatch(document.body, "sx:afterSettle", { target: main });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fetch fresh
|
|
||||||
var histOpts = {
|
var histOpts = {
|
||||||
headers: { "SX-Request": "true", "SX-History-Restore": "true" }
|
headers: { "SX-Request": "true", "SX-History-Restore": "true" }
|
||||||
};
|
};
|
||||||
@@ -1948,24 +1937,31 @@
|
|||||||
histOpts.credentials = "include";
|
histOpts.credentials = "include";
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
fetch(url, histOpts).then(function (resp) {
|
fetch(url, histOpts).then(function (resp) {
|
||||||
return resp.text();
|
return resp.text();
|
||||||
}).then(function (text) {
|
}).then(function (text) {
|
||||||
var ct = "";
|
// Strip and load any <script type="text/sx" data-components> blocks
|
||||||
// Response content-type is lost here, check for sx
|
var hadScript = false;
|
||||||
|
text = text.replace(/<script[^>]*type="text\/sx"[^>]*data-components[^>]*>([\s\S]*?)<\/script>/gi,
|
||||||
|
function (_, defs) { hadScript = true; Sx.loadComponents(defs); return ""; });
|
||||||
|
if (hadScript) text = text.trim();
|
||||||
if (text.charAt(0) === "(") {
|
if (text.charAt(0) === "(") {
|
||||||
try { text = Sx.renderToString(text); } catch (e) { /* not sx */ }
|
try { text = Sx.renderToString(text); } catch (e) {}
|
||||||
}
|
}
|
||||||
var parser = new DOMParser();
|
var parser = new DOMParser();
|
||||||
var doc = parser.parseFromString(text, "text/html");
|
var doc = parser.parseFromString(text, "text/html");
|
||||||
var newMain = doc.getElementById("main-panel");
|
var newMain = doc.getElementById("main-panel");
|
||||||
var main = document.getElementById("main-panel");
|
if (newMain) {
|
||||||
if (main && newMain) {
|
|
||||||
main.innerHTML = newMain.innerHTML;
|
main.innerHTML = newMain.innerHTML;
|
||||||
|
_activateScripts(main);
|
||||||
Sx.processScripts(main);
|
Sx.processScripts(main);
|
||||||
Sx.hydrate(main);
|
Sx.hydrate(main);
|
||||||
SxEngine.process(main);
|
SxEngine.process(main);
|
||||||
dispatch(document.body, "sx:afterSettle", { target: main });
|
dispatch(document.body, "sx:afterSettle", { target: main });
|
||||||
|
window.scrollTo(0, e.state && e.state.scrollY || 0);
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
}
|
}
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
location.reload();
|
location.reload();
|
||||||
@@ -2057,10 +2053,7 @@
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache current page before navigation
|
|
||||||
document.addEventListener("sx:beforeRequest", function () {
|
|
||||||
if (typeof SxEngine._cacheCurrentPage === "function") SxEngine._cacheCurrentPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,9 +209,13 @@ def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
|
|||||||
|
|
||||||
|
|
||||||
def oob_header_sx(parent_id: str, child_id: str, row_sx: str) -> str:
|
def oob_header_sx(parent_id: str, child_id: str, row_sx: str) -> str:
|
||||||
"""Wrap a header row sx in an OOB swap."""
|
"""Wrap a header row sx in an OOB swap.
|
||||||
|
|
||||||
|
child_id is accepted for call-site compatibility but no longer used —
|
||||||
|
the child placeholder is created by ~menu-row-sx itself.
|
||||||
|
"""
|
||||||
return sx_call("oob-header-sx",
|
return sx_call("oob-header-sx",
|
||||||
parent_id=parent_id, child_id=child_id,
|
parent_id=parent_id,
|
||||||
row=SxExpr(row_sx),
|
row=SxExpr(row_sx),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
(header :class "z-50"
|
(header :class "z-50"
|
||||||
(div :id "root-header-summary"
|
(div :id "root-header-summary"
|
||||||
:class "flex items-start gap-2 p-1 bg-sky-500"
|
:class "flex items-start gap-2 p-1 bg-sky-500"
|
||||||
(div :class "flex flex-col w-full items-center"
|
(div :id "root-header-child" :class "flex flex-col w-full items-center"
|
||||||
(when header-rows header-rows)))))
|
(when header-rows header-rows)))))
|
||||||
(div :id "root-menu" :sx-swap-oob "outerHTML" :class "md:hidden"
|
(div :id "root-menu" :sx-swap-oob "outerHTML" :class "md:hidden"
|
||||||
(when menu menu))))
|
(when menu menu))))
|
||||||
@@ -107,13 +107,12 @@
|
|||||||
(div :id child-id :class "flex flex-col w-full items-center"
|
(div :id child-id :class "flex flex-col w-full items-center"
|
||||||
(when child child))))))
|
(when child child))))))
|
||||||
|
|
||||||
(defcomp ~oob-header-sx (&key parent-id child-id row)
|
(defcomp ~oob-header-sx (&key parent-id row)
|
||||||
(div :id parent-id :sx-swap-oob "outerHTML" :class "w-full"
|
(div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center"
|
||||||
(div :class "w-full" row
|
row))
|
||||||
(div :id child-id))))
|
|
||||||
|
|
||||||
(defcomp ~header-child-sx (&key id inner)
|
(defcomp ~header-child-sx (&key id inner)
|
||||||
(div :id (or id "root-header-child") :class "w-full" inner))
|
(div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner))
|
||||||
|
|
||||||
(defcomp ~error-content (&key errnum message image)
|
(defcomp ~error-content (&key errnum message image)
|
||||||
(div :class "text-center p-8 max-w-lg mx-auto"
|
(div :class "text-center p-8 max-w-lg mx-auto"
|
||||||
|
|||||||
Reference in New Issue
Block a user