Compare commits
2 Commits
decoupling
...
exorcism
| Author | SHA1 | Date | |
|---|---|---|---|
| a98354c0f0 | |||
| df8b19ccb8 |
@@ -55,6 +55,104 @@
|
||||
(button :type "submit"
|
||||
:class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer" create-label))))
|
||||
|
||||
;; Edit form — pre-populated version for /<slug>/admin/edit/
|
||||
(defcomp ~blog-editor-edit-form (&key csrf updated-at title-val excerpt-val
|
||||
feature-image feature-image-caption
|
||||
sx-content-val lexical-json
|
||||
has-sx title-placeholder
|
||||
status already-emailed
|
||||
newsletter-options footer-extra)
|
||||
(let* ((sel-cls "text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600")
|
||||
(active "px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent")
|
||||
(inactive "px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600"))
|
||||
(form :id "post-edit-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "updated_at" :value updated-at)
|
||||
(input :type "hidden" :id "lexical-json-input" :name "lexical" :value "")
|
||||
(input :type "hidden" :id "sx-content-input" :name "sx_content" :value (or sx-content-val ""))
|
||||
(input :type "hidden" :id "feature-image-input" :name "feature_image" :value (or feature-image ""))
|
||||
(input :type "hidden" :id "feature-image-caption-input" :name "feature_image_caption" :value (or feature-image-caption ""))
|
||||
(div :id "feature-image-container" :class "relative mt-[16px] mb-[24px] group"
|
||||
(div :id "feature-image-empty" :class (if feature-image "hidden" "")
|
||||
(button :type "button" :id "feature-image-add-btn"
|
||||
:class "text-[14px] text-stone-400 hover:text-stone-600 transition-colors cursor-pointer"
|
||||
"+ Add feature image"))
|
||||
(div :id "feature-image-filled" :class (str "relative " (if feature-image "" "hidden"))
|
||||
(img :id "feature-image-preview" :src (or feature-image "") :alt ""
|
||||
:class "w-full max-h-[448px] object-cover rounded-[8px] cursor-pointer")
|
||||
(button :type "button" :id "feature-image-delete-btn"
|
||||
:class "absolute top-[8px] right-[8px] w-[32px] h-[32px] rounded-full bg-black/50 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-black/70 cursor-pointer text-[14px]"
|
||||
:title "Remove feature image"
|
||||
(i :class "fa-solid fa-trash-can"))
|
||||
(input :type "text" :id "feature-image-caption" :value (or feature-image-caption "")
|
||||
:placeholder "Add a caption..."
|
||||
:class "mt-[8px] w-full text-[14px] text-stone-500 bg-transparent border-none outline-none placeholder:text-stone-300 focus:text-stone-700"))
|
||||
(div :id "feature-image-uploading"
|
||||
:class "hidden flex items-center gap-[8px] mt-[8px] text-[14px] text-stone-400"
|
||||
(i :class "fa-solid fa-spinner fa-spin") " Uploading...")
|
||||
(input :type "file" :id "feature-image-file"
|
||||
:accept "image/jpeg,image/png,image/gif,image/webp,image/svg+xml" :class "hidden"))
|
||||
(input :type "text" :name "title" :value (or title-val "") :placeholder title-placeholder
|
||||
:class "w-full text-[36px] font-bold bg-transparent border-none outline-none placeholder:text-stone-300 mb-[8px] leading-tight")
|
||||
(textarea :name "custom_excerpt" :rows "1" :placeholder "Add an excerpt..."
|
||||
:class "w-full text-[18px] text-stone-500 bg-transparent border-none outline-none placeholder:text-stone-300 resize-none mb-[24px] leading-relaxed"
|
||||
(or excerpt-val ""))
|
||||
;; Editor tabs
|
||||
(div :class "flex gap-[4px] mb-[8px] border-b border-stone-200"
|
||||
(button :type "button" :id "editor-tab-sx"
|
||||
:class (if has-sx active inactive)
|
||||
:onclick "document.getElementById('sx-editor').style.display='block';document.getElementById('lexical-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-koenig').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'"
|
||||
"SX Editor")
|
||||
(button :type "button" :id "editor-tab-koenig"
|
||||
:class (if has-sx inactive active)
|
||||
:onclick "document.getElementById('lexical-editor').style.display='block';document.getElementById('sx-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-sx').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'"
|
||||
"Koenig (Legacy)"))
|
||||
(div :id "sx-editor" :class "relative w-full bg-transparent"
|
||||
:style (if has-sx "" "display:none"))
|
||||
(div :id "lexical-editor" :class "relative w-full bg-transparent"
|
||||
:style (if has-sx "display:none" ""))
|
||||
;; Initial lexical JSON
|
||||
(script :id "lexical-initial-data" :type "application/json" lexical-json)
|
||||
;; Footer: status + publish mode + newsletter + save + badges
|
||||
(div :class "flex flex-wrap items-center gap-[16px] mt-[32px] pt-[16px] border-t border-stone-200"
|
||||
(select :id "status-select" :name "status" :class sel-cls
|
||||
(option :value "draft" :selected (= status "draft") "Draft")
|
||||
(option :value "published" :selected (= status "published") "Published"))
|
||||
(select :id "publish-mode-select" :name "publish_mode"
|
||||
:class (str sel-cls (if (= status "published") "" " hidden")
|
||||
(if already-emailed " opacity-50 pointer-events-none" ""))
|
||||
:disabled (if already-emailed true nil)
|
||||
(option :value "web" :selected true "Web only")
|
||||
(option :value "email" "Email only")
|
||||
(option :value "both" "Web + Email"))
|
||||
(select :id "newsletter-select" :name "newsletter_slug"
|
||||
:class (str sel-cls " hidden")
|
||||
:disabled (if already-emailed true nil)
|
||||
newsletter-options)
|
||||
(button :type "submit"
|
||||
:class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer"
|
||||
"Save")
|
||||
(when footer-extra footer-extra)))))
|
||||
|
||||
;; Publish-mode show/hide script for edit form
|
||||
(defcomp ~blog-editor-publish-js (&key already-emailed)
|
||||
(script
|
||||
"(function() {"
|
||||
" var statusSel = document.getElementById('status-select');"
|
||||
" var modeSel = document.getElementById('publish-mode-select');"
|
||||
" var nlSel = document.getElementById('newsletter-select');"
|
||||
(str " var alreadyEmailed = " (if already-emailed "true" "false") ";")
|
||||
" function sync() {"
|
||||
" var isPublished = statusSel.value === 'published';"
|
||||
" if (isPublished && !alreadyEmailed) { modeSel.classList.remove('hidden'); } else { modeSel.classList.add('hidden'); }"
|
||||
" var needsEmail = isPublished && !alreadyEmailed && (modeSel.value === 'email' || modeSel.value === 'both');"
|
||||
" if (needsEmail) { nlSel.classList.remove('hidden'); } else { nlSel.classList.add('hidden'); }"
|
||||
" }"
|
||||
" statusSel.addEventListener('change', sync);"
|
||||
" modeSel.addEventListener('change', sync);"
|
||||
" sync();"
|
||||
"})();"))
|
||||
|
||||
(defcomp ~blog-editor-styles (&key css-href)
|
||||
(<> (link :rel "stylesheet" :href css-href)
|
||||
(style
|
||||
|
||||
@@ -1271,7 +1271,7 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) ->
|
||||
"\n"
|
||||
" if (typeof SxEditor !== 'undefined') {\n"
|
||||
" SxEditor.mount('sx-editor', {\n"
|
||||
" initialSx: window.__SX_INITIAL__ || null,\n"
|
||||
" initialSx: (document.getElementById('sx-content-input') || {}).value || null,\n"
|
||||
" csrfToken: csrfToken,\n"
|
||||
" uploadUrls: uploadUrls,\n"
|
||||
f" oembedUrl: '{oembed_url}',\n"
|
||||
@@ -1678,10 +1678,11 @@ def _raw_html_sx(html: str) -> str:
|
||||
|
||||
|
||||
def _post_edit_content_sx(ctx: dict) -> str:
|
||||
"""Build WYSIWYG editor panel natively (replaces _types/post_edit/_main_panel.html)."""
|
||||
"""Build WYSIWYG editor panel as SX expression (edit page)."""
|
||||
from quart import url_for as qurl, current_app, g, request as qrequest
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
esc = escape
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
ghost_post = ctx.get("ghost_post", {}) or {}
|
||||
save_success = ctx.get("save_success", False)
|
||||
@@ -1706,181 +1707,81 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
|
||||
feature_image = ghost_post.get("feature_image") or ""
|
||||
feature_image_caption = ghost_post.get("feature_image_caption") or ""
|
||||
title_val = esc(ghost_post.get("title") or "")
|
||||
excerpt_val = esc(ghost_post.get("custom_excerpt") or "")
|
||||
updated_at = esc(ghost_post.get("updated_at") or "")
|
||||
title_val = ghost_post.get("title") or ""
|
||||
excerpt_val = ghost_post.get("custom_excerpt") or ""
|
||||
updated_at = ghost_post.get("updated_at") or ""
|
||||
status = ghost_post.get("status") or "draft"
|
||||
lexical_json = ghost_post.get("lexical") or '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'
|
||||
sx_content = ghost_post.get("sx_content") or ""
|
||||
has_sx = bool(sx_content)
|
||||
|
||||
already_emailed = bool(ghost_post and ghost_post.get("email") and (ghost_post["email"] if isinstance(ghost_post["email"], dict) else {}).get("status"))
|
||||
# For ORM objects the email may be an object
|
||||
email_obj = ghost_post.get("email")
|
||||
if email_obj and not isinstance(email_obj, dict):
|
||||
already_emailed = bool(getattr(email_obj, "status", None))
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
# Error banner
|
||||
if save_error:
|
||||
parts.append(
|
||||
f'<div class="max-w-[768px] mx-auto mt-[16px] rounded-[8px] border border-red-300 bg-red-50 px-[16px] py-[12px] text-[14px] text-red-700">'
|
||||
f'<strong>Save failed:</strong> {esc(save_error)}</div>'
|
||||
)
|
||||
|
||||
# Hidden inputs
|
||||
fi_hidden = f' hidden' if not feature_image else ''
|
||||
fi_visible = f' hidden' if feature_image else ''
|
||||
|
||||
title_placeholder = "Page title..." if is_page else "Post title..."
|
||||
|
||||
form_parts: list[str] = []
|
||||
form_parts.append(f'<input type="hidden" name="csrf_token" value="{csrf}">')
|
||||
form_parts.append(f'<input type="hidden" name="updated_at" value="{updated_at}">')
|
||||
form_parts.append('<input type="hidden" id="lexical-json-input" name="lexical" value="">')
|
||||
form_parts.append(f'<input type="hidden" id="sx-content-input" name="sx_content" value="{esc(sx_content)}">')
|
||||
form_parts.append(f'<input type="hidden" id="feature-image-input" name="feature_image" value="{esc(feature_image)}">')
|
||||
form_parts.append(f'<input type="hidden" id="feature-image-caption-input" name="feature_image_caption" value="{esc(feature_image_caption)}">')
|
||||
|
||||
# Feature image section
|
||||
form_parts.append(
|
||||
f'<div id="feature-image-container" class="relative mt-[16px] mb-[24px] group">'
|
||||
f'<div id="feature-image-empty" class="{"hidden" if feature_image else ""}">'
|
||||
f'<button type="button" id="feature-image-add-btn" class="text-[14px] text-stone-400 hover:text-stone-600 transition-colors cursor-pointer">+ Add feature image</button>'
|
||||
f'</div>'
|
||||
f'<div id="feature-image-filled" class="relative {"" if feature_image else "hidden"}">'
|
||||
f'<img id="feature-image-preview" src="{esc(feature_image)}" alt="" class="w-full max-h-[448px] object-cover rounded-[8px] cursor-pointer">'
|
||||
f'<button type="button" id="feature-image-delete-btn" class="absolute top-[8px] right-[8px] w-[32px] h-[32px] rounded-full bg-black/50 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-black/70 cursor-pointer text-[14px]" title="Remove feature image"><i class="fa-solid fa-trash-can"></i></button>'
|
||||
f'<input type="text" id="feature-image-caption" value="{esc(feature_image_caption)}" placeholder="Add a caption..." class="mt-[8px] w-full text-[14px] text-stone-500 bg-transparent border-none outline-none placeholder:text-stone-300 focus:text-stone-700">'
|
||||
f'</div>'
|
||||
f'<div id="feature-image-uploading" class="hidden flex items-center gap-[8px] mt-[8px] text-[14px] text-stone-400"><i class="fa-solid fa-spinner fa-spin"></i> Uploading...</div>'
|
||||
f'<input type="file" id="feature-image-file" accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml" class="hidden">'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
# Title
|
||||
form_parts.append(
|
||||
f'<input type="text" name="title" value="{title_val}" placeholder="{title_placeholder}"'
|
||||
f' class="w-full text-[36px] font-bold bg-transparent border-none outline-none placeholder:text-stone-300 mb-[8px] leading-tight">'
|
||||
)
|
||||
|
||||
# Excerpt
|
||||
form_parts.append(
|
||||
f'<textarea name="custom_excerpt" rows="1" placeholder="Add an excerpt..."'
|
||||
f' class="w-full text-[18px] text-stone-500 bg-transparent border-none outline-none placeholder:text-stone-300 resize-none mb-[24px] leading-relaxed">{excerpt_val}</textarea>'
|
||||
)
|
||||
|
||||
# Editor tabs: SX (primary) and Koenig (legacy)
|
||||
has_sx = bool(sx_content)
|
||||
sx_active = 'text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent'
|
||||
sx_inactive = 'text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'
|
||||
form_parts.append(
|
||||
'<div class="flex gap-[4px] mb-[8px] border-b border-stone-200">'
|
||||
f'<button type="button" id="editor-tab-sx" class="px-[12px] py-[6px] text-[13px] font-medium {sx_active if has_sx else sx_inactive}"'
|
||||
""" onclick="document.getElementById('sx-editor').style.display='block';document.getElementById('lexical-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-koenig').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'" """
|
||||
'>SX Editor</button>'
|
||||
f'<button type="button" id="editor-tab-koenig" class="px-[12px] py-[6px] text-[13px] font-medium {sx_inactive if has_sx else sx_active}"'
|
||||
""" onclick="document.getElementById('lexical-editor').style.display='block';document.getElementById('sx-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-sx').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'" """
|
||||
'>Koenig (Legacy)</button>'
|
||||
'</div>'
|
||||
)
|
||||
# SX editor mount point
|
||||
form_parts.append(f'<div id="sx-editor" class="relative w-full bg-transparent" style="{"" if has_sx else "display:none"}"></div>')
|
||||
# Koenig editor mount point
|
||||
form_parts.append(f'<div id="lexical-editor" class="relative w-full bg-transparent" style="{"display:none" if has_sx else ""}"></div>')
|
||||
|
||||
# Initial lexical JSON
|
||||
form_parts.append(f'<script id="lexical-initial-data" type="application/json">{lexical_json}</script>')
|
||||
|
||||
# Status + publish footer
|
||||
draft_sel = ' selected' if status == 'draft' else ''
|
||||
pub_sel = ' selected' if status == 'published' else ''
|
||||
mode_hidden = ' hidden' if status != 'published' else ''
|
||||
mode_disabled = ' opacity-50 pointer-events-none' if already_emailed else ''
|
||||
mode_dis_attr = ' disabled' if already_emailed else ''
|
||||
|
||||
nl_options = '<option value="">Select newsletter\u2026</option>'
|
||||
# Newsletter options as SX fragment
|
||||
nl_parts = ['(option :value "" "Select newsletter\u2026")']
|
||||
for nl in newsletters:
|
||||
nl_slug = esc(getattr(nl, "slug", ""))
|
||||
nl_name = esc(getattr(nl, "name", ""))
|
||||
nl_options += f'<option value="{nl_slug}">{nl_name}</option>'
|
||||
nl_slug = sx_serialize(getattr(nl, "slug", ""))
|
||||
nl_name = sx_serialize(getattr(nl, "name", ""))
|
||||
nl_parts.append(f"(option :value {nl_slug} {nl_name})")
|
||||
nl_opts_sx = SxExpr("(<> " + " ".join(nl_parts) + ")")
|
||||
|
||||
footer_extra = ''
|
||||
# Footer extra badges as SX fragment
|
||||
badge_parts: list[str] = []
|
||||
if save_success:
|
||||
footer_extra += ' <span class="text-[14px] text-green-600">Saved.</span>'
|
||||
badge_parts.append('(span :class "text-[14px] text-green-600" "Saved.")')
|
||||
publish_requested = qrequest.args.get("publish_requested") if hasattr(qrequest, 'args') else None
|
||||
if publish_requested:
|
||||
footer_extra += ' <span class="text-[14px] text-blue-600">Publish requested \u2014 an admin will review.</span>'
|
||||
badge_parts.append('(span :class "text-[14px] text-blue-600" "Publish requested \u2014 an admin will review.")')
|
||||
if post.get("publish_requested"):
|
||||
footer_extra += ' <span class="inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800">Publish requested</span>'
|
||||
badge_parts.append('(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800" "Publish requested")')
|
||||
if already_emailed:
|
||||
nl_name = ""
|
||||
newsletter = ghost_post.get("newsletter")
|
||||
if newsletter:
|
||||
nl_name = getattr(newsletter, "name", "") if not isinstance(newsletter, dict) else newsletter.get("name", "")
|
||||
suffix = f" to {esc(nl_name)}" if nl_name else ""
|
||||
footer_extra += f' <span class="inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800">Emailed{suffix}</span>'
|
||||
suffix = f" to {nl_name}" if nl_name else ""
|
||||
badge_parts.append(f'(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800" "Emailed{suffix}")')
|
||||
footer_extra_sx = SxExpr("(<> " + " ".join(badge_parts) + ")") if badge_parts else None
|
||||
|
||||
form_parts.append(
|
||||
f'<div class="flex flex-wrap items-center gap-[16px] mt-[32px] pt-[16px] border-t border-stone-200">'
|
||||
f'<select id="status-select" name="status" class="text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600">'
|
||||
f'<option value="draft"{draft_sel}>Draft</option>'
|
||||
f'<option value="published"{pub_sel}>Published</option></select>'
|
||||
f'<select id="publish-mode-select" name="publish_mode" class="text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600{mode_hidden}{mode_disabled}"{mode_dis_attr}>'
|
||||
f'<option value="web" selected>Web only</option><option value="email">Email only</option><option value="both">Web + Email</option></select>'
|
||||
f'<select id="newsletter-select" name="newsletter_slug" class="text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600 hidden"{mode_dis_attr}>{nl_options}</select>'
|
||||
f'<button type="submit" class="px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer">Save</button>'
|
||||
f'{footer_extra}</div>'
|
||||
)
|
||||
parts: list[str] = []
|
||||
|
||||
form_html = '<form id="post-edit-form" method="post" class="max-w-[768px] mx-auto pb-[48px]">' + "".join(form_parts) + '</form>'
|
||||
parts.append(form_html)
|
||||
# Error banner
|
||||
if save_error:
|
||||
parts.append(sx_call("blog-editor-error", error=save_error))
|
||||
|
||||
# Publish-mode show/hide JS
|
||||
already_emailed_js = 'true' if already_emailed else 'false'
|
||||
parts.append(
|
||||
'<script>'
|
||||
'(function() {'
|
||||
" var statusSel = document.getElementById('status-select');"
|
||||
" var modeSel = document.getElementById('publish-mode-select');"
|
||||
" var nlSel = document.getElementById('newsletter-select');"
|
||||
f' var alreadyEmailed = {already_emailed_js};'
|
||||
' function sync() {'
|
||||
" var isPublished = statusSel.value === 'published';"
|
||||
" if (isPublished && !alreadyEmailed) { modeSel.classList.remove('hidden'); } else { modeSel.classList.add('hidden'); }"
|
||||
" var needsEmail = isPublished && !alreadyEmailed && (modeSel.value === 'email' || modeSel.value === 'both');"
|
||||
" if (needsEmail) { nlSel.classList.remove('hidden'); } else { nlSel.classList.add('hidden'); }"
|
||||
' }'
|
||||
" statusSel.addEventListener('change', sync);"
|
||||
" modeSel.addEventListener('change', sync);"
|
||||
' sync();'
|
||||
'})();'
|
||||
'</script>'
|
||||
)
|
||||
# Form (sx_content_val populates #sx-content-input; JS reads from there)
|
||||
parts.append(sx_call("blog-editor-edit-form",
|
||||
csrf=csrf,
|
||||
updated_at=str(updated_at),
|
||||
title_val=title_val,
|
||||
excerpt_val=excerpt_val,
|
||||
feature_image=feature_image,
|
||||
feature_image_caption=feature_image_caption,
|
||||
sx_content_val=sx_content,
|
||||
lexical_json=lexical_json,
|
||||
has_sx=has_sx,
|
||||
title_placeholder=title_placeholder,
|
||||
status=status,
|
||||
already_emailed=already_emailed,
|
||||
newsletter_options=nl_opts_sx,
|
||||
footer_extra=footer_extra_sx,
|
||||
))
|
||||
|
||||
# Editor CSS + styles + SX editor styles
|
||||
from shared.sx.helpers import sx_call
|
||||
parts.append(f'<link rel="stylesheet" href="{esc(editor_css)}">')
|
||||
# Publish-mode JS
|
||||
parts.append(sx_call("blog-editor-publish-js", already_emailed=already_emailed))
|
||||
|
||||
# Editor CSS + styles
|
||||
parts.append(sx_call("blog-editor-styles", css_href=editor_css))
|
||||
parts.append(sx_call("sx-editor-styles"))
|
||||
parts.append(
|
||||
'<style>'
|
||||
'#lexical-editor { display: flow-root; }'
|
||||
'#lexical-editor [data-kg-card="html"] * { float: none !important; }'
|
||||
'#lexical-editor [data-kg-card="html"] table { width: 100% !important; }'
|
||||
'</style>'
|
||||
)
|
||||
|
||||
# Initial sx content for SX editor
|
||||
sx_initial_escaped = sx_content.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n').replace('\r', '')
|
||||
parts.append(f"<script>window.__SX_INITIAL__ = '{sx_initial_escaped}' || null;</script>")
|
||||
|
||||
# Editor JS + SX editor JS + init
|
||||
parts.append(f'<script src="{esc(editor_js)}"></script>')
|
||||
parts.append(f'<script src="{esc(sx_editor_js)}"></script>')
|
||||
parts.append(
|
||||
'<script>'
|
||||
# Editor JS + init
|
||||
init_js = (
|
||||
'(function() {'
|
||||
# Font size overrides for Koenig
|
||||
" function applyEditorFontSize() {"
|
||||
" document.documentElement.style.fontSize = '62.5%';"
|
||||
" document.body.style.fontSize = '1.6rem';"
|
||||
@@ -1956,7 +1857,7 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
' });'
|
||||
" if (typeof SxEditor !== 'undefined') {"
|
||||
" SxEditor.mount('sx-editor', {"
|
||||
" initialSx: window.__SX_INITIAL__ || null,"
|
||||
" initialSx: (document.getElementById('sx-content-input') || {}).value || null,"
|
||||
' csrfToken: csrfToken,'
|
||||
' uploadUrls: uploadUrls,'
|
||||
f" oembedUrl: '{oembed_url}',"
|
||||
@@ -1976,10 +1877,13 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
||||
" if (typeof window.mountEditor === 'function') { clearInterval(_t); init(); }"
|
||||
' }, 50); }'
|
||||
'})();'
|
||||
'</script>'
|
||||
)
|
||||
parts.append(sx_call("blog-editor-scripts",
|
||||
js_src=editor_js,
|
||||
sx_editor_js_src=sx_editor_js,
|
||||
init_js=init_js))
|
||||
|
||||
return _raw_html_sx("".join(parts))
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
@@ -2249,7 +2249,9 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
root.className = (root.className || "") + " sx-editor";
|
||||
// Clear any previous mount
|
||||
root.innerHTML = "";
|
||||
root.className = (root.className || "").replace(/\bsx-editor\b/g, "").trim() + " sx-editor";
|
||||
|
||||
var container = el("div", { className: "sx-blocks-container" });
|
||||
root.appendChild(container);
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
var RE_WS = /\s+/y;
|
||||
var RE_COMMENT = /;[^\n]*/y;
|
||||
var RE_STRING = /"(?:[^"\\]|\\.)*"/y;
|
||||
var RE_STRING = /"(?:[^"\\]|\\[\s\S])*"/y;
|
||||
var RE_NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/y;
|
||||
var RE_KEYWORD = /:[a-zA-Z_][a-zA-Z0-9_>:\-]*/y;
|
||||
var RE_SYMBOL = /[a-zA-Z_~*+\-><=/!?&][a-zA-Z0-9_~*+\-><=/!?.:&]*/y;
|
||||
|
||||
@@ -254,7 +254,7 @@ def search_desktop_sx(ctx: dict) -> str:
|
||||
)
|
||||
|
||||
|
||||
def post_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
def post_header_sx(ctx: dict, *, oob: bool = False, child: str = "") -> str:
|
||||
"""Build the post-level header row as sx call string."""
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
@@ -273,6 +273,7 @@ def post_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
link_label_content=SxExpr(label_sx),
|
||||
nav=SxExpr(nav_sx) if nav_sx else None,
|
||||
child_id="post-header-child",
|
||||
child=SxExpr(child) if child else None,
|
||||
oob=oob, external=True,
|
||||
)
|
||||
|
||||
@@ -378,6 +379,7 @@ def sx_call(component_name: str, **kwargs: Any) -> str:
|
||||
return "(" + " ".join(parts) + ")"
|
||||
|
||||
|
||||
|
||||
def components_for_request() -> str:
|
||||
"""Return defcomp/defmacro source for definitions the client doesn't have yet.
|
||||
|
||||
|
||||
@@ -104,16 +104,18 @@ def _post_full(ctx: dict, **kw: Any) -> str:
|
||||
|
||||
def _post_oob(ctx: dict, **kw: Any) -> str:
|
||||
post_hdr = post_header_sx(ctx, oob=True)
|
||||
return post_hdr
|
||||
# Also replace #post-header-child (empty — clears any nested admin rows)
|
||||
child_oob = oob_header_sx("post-header-child", "", "")
|
||||
return "(<> " + post_hdr + " " + child_oob + ")"
|
||||
|
||||
|
||||
def _post_admin_full(ctx: dict, **kw: Any) -> str:
|
||||
slug = ctx.get("post", {}).get("slug", "")
|
||||
selected = kw.get("selected", "")
|
||||
root_hdr = root_header_sx(ctx)
|
||||
post_hdr = post_header_sx(ctx)
|
||||
admin_hdr = post_admin_header_sx(ctx, slug, selected=selected)
|
||||
return "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
||||
post_hdr = post_header_sx(ctx, child=admin_hdr)
|
||||
return "(<> " + root_hdr + " " + post_hdr + ")"
|
||||
|
||||
|
||||
def _post_admin_oob(ctx: dict, **kw: Any) -> str:
|
||||
|
||||
Reference in New Issue
Block a user