All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m44s
Replace ~250 render_to_sx calls across all services with sync sx_call, converting many async functions to sync where no other awaits remained. Make render_to_sx/render_to_sx_with_env private (_render_to_sx). Add (post-header-ctx) IO primitive and shared post/post-admin defmacros. Convert built-in post/post-admin layouts from Python to register_sx_layout with .sx defcomps. Remove dead post_admin_mobile_nav_sx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
7.6 KiB
Python
178 lines
7.6 KiB
Python
"""Blog editor panel rendering."""
|
|
from __future__ import annotations
|
|
|
|
|
|
def render_editor_panel(save_error: str | None = None, is_page: bool = False) -> str:
|
|
"""Build the WYSIWYG editor panel HTML for new post/page creation."""
|
|
import os
|
|
from quart import url_for as qurl, current_app
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
from shared.sx.helpers import sx_call
|
|
|
|
csrf = generate_csrf_token()
|
|
asset_url_fn = current_app.jinja_env.globals.get("asset_url", lambda p: "")
|
|
editor_css = asset_url_fn("scripts/editor.css")
|
|
editor_js = asset_url_fn("scripts/editor.js")
|
|
sx_editor_js = asset_url_fn("scripts/sx-editor.js")
|
|
|
|
upload_image_url = qurl("blog.editor_api.upload_image")
|
|
upload_media_url = qurl("blog.editor_api.upload_media")
|
|
upload_file_url = qurl("blog.editor_api.upload_file")
|
|
oembed_url = qurl("blog.editor_api.oembed_proxy")
|
|
snippets_url = qurl("blog.editor_api.list_snippets")
|
|
unsplash_key = os.environ.get("UNSPLASH_ACCESS_KEY", "")
|
|
|
|
title_placeholder = "Page title..." if is_page else "Post title..."
|
|
create_label = "Create Page" if is_page else "Create Post"
|
|
|
|
parts: list[str] = []
|
|
|
|
if save_error:
|
|
parts.append(sx_call("blog-editor-error", error=str(save_error)))
|
|
|
|
parts.append(sx_call("blog-editor-form",
|
|
csrf=csrf, title_placeholder=title_placeholder,
|
|
create_label=create_label,
|
|
))
|
|
|
|
parts.append(sx_call("blog-editor-styles", css_href=editor_css))
|
|
parts.append(sx_call("sx-editor-styles"))
|
|
|
|
init_js = (
|
|
"console.log('[EDITOR-DEBUG] init script running');\n"
|
|
"(function() {\n"
|
|
" console.log('[EDITOR-DEBUG] IIFE entered, mountEditor=', typeof window.mountEditor);\n"
|
|
" function init() {\n"
|
|
" var csrfToken = document.querySelector('input[name=\"csrf_token\"]').value;\n"
|
|
f" var uploadUrl = '{upload_image_url}';\n"
|
|
" var uploadUrls = {\n"
|
|
" image: uploadUrl,\n"
|
|
f" media: '{upload_media_url}',\n"
|
|
f" file: '{upload_file_url}',\n"
|
|
" };\n"
|
|
"\n"
|
|
" var fileInput = document.getElementById('feature-image-file');\n"
|
|
" var addBtn = document.getElementById('feature-image-add-btn');\n"
|
|
" var deleteBtn = document.getElementById('feature-image-delete-btn');\n"
|
|
" var preview = document.getElementById('feature-image-preview');\n"
|
|
" var emptyState = document.getElementById('feature-image-empty');\n"
|
|
" var filledState = document.getElementById('feature-image-filled');\n"
|
|
" var hiddenUrl = document.getElementById('feature-image-input');\n"
|
|
" var hiddenCaption = document.getElementById('feature-image-caption-input');\n"
|
|
" var captionInput = document.getElementById('feature-image-caption');\n"
|
|
" var uploading = document.getElementById('feature-image-uploading');\n"
|
|
"\n"
|
|
" function showFilled(url) {\n"
|
|
" preview.src = url;\n"
|
|
" hiddenUrl.value = url;\n"
|
|
" emptyState.classList.add('hidden');\n"
|
|
" filledState.classList.remove('hidden');\n"
|
|
" uploading.classList.add('hidden');\n"
|
|
" }\n"
|
|
"\n"
|
|
" function showEmpty() {\n"
|
|
" preview.src = '';\n"
|
|
" hiddenUrl.value = '';\n"
|
|
" hiddenCaption.value = '';\n"
|
|
" captionInput.value = '';\n"
|
|
" emptyState.classList.remove('hidden');\n"
|
|
" filledState.classList.add('hidden');\n"
|
|
" uploading.classList.add('hidden');\n"
|
|
" }\n"
|
|
"\n"
|
|
" function uploadFile(file) {\n"
|
|
" emptyState.classList.add('hidden');\n"
|
|
" uploading.classList.remove('hidden');\n"
|
|
" var fd = new FormData();\n"
|
|
" fd.append('file', file);\n"
|
|
" fetch(uploadUrl, {\n"
|
|
" method: 'POST',\n"
|
|
" body: fd,\n"
|
|
" headers: { 'X-CSRFToken': csrfToken },\n"
|
|
" })\n"
|
|
" .then(function(r) {\n"
|
|
" if (!r.ok) throw new Error('Upload failed (' + r.status + ')');\n"
|
|
" return r.json();\n"
|
|
" })\n"
|
|
" .then(function(data) {\n"
|
|
" var url = data.images && data.images[0] && data.images[0].url;\n"
|
|
" if (url) showFilled(url);\n"
|
|
" else { showEmpty(); alert('Upload succeeded but no image URL returned.'); }\n"
|
|
" })\n"
|
|
" .catch(function(e) {\n"
|
|
" showEmpty();\n"
|
|
" alert(e.message);\n"
|
|
" });\n"
|
|
" }\n"
|
|
"\n"
|
|
" addBtn.addEventListener('click', function() { fileInput.click(); });\n"
|
|
" preview.addEventListener('click', function() { fileInput.click(); });\n"
|
|
" deleteBtn.addEventListener('click', function(e) {\n"
|
|
" e.stopPropagation();\n"
|
|
" showEmpty();\n"
|
|
" });\n"
|
|
" fileInput.addEventListener('change', function() {\n"
|
|
" if (fileInput.files && fileInput.files[0]) {\n"
|
|
" uploadFile(fileInput.files[0]);\n"
|
|
" fileInput.value = '';\n"
|
|
" }\n"
|
|
" });\n"
|
|
" captionInput.addEventListener('input', function() {\n"
|
|
" hiddenCaption.value = captionInput.value;\n"
|
|
" });\n"
|
|
"\n"
|
|
" var excerpt = document.querySelector('textarea[name=\"custom_excerpt\"]');\n"
|
|
" function autoResize() {\n"
|
|
" excerpt.style.height = 'auto';\n"
|
|
" excerpt.style.height = excerpt.scrollHeight + 'px';\n"
|
|
" }\n"
|
|
" excerpt.addEventListener('input', autoResize);\n"
|
|
" autoResize();\n"
|
|
"\n"
|
|
" window.mountEditor('lexical-editor', {\n"
|
|
" initialJson: null,\n"
|
|
" csrfToken: csrfToken,\n"
|
|
" uploadUrls: uploadUrls,\n"
|
|
f" oembedUrl: '{oembed_url}',\n"
|
|
f" unsplashApiKey: '{unsplash_key}',\n"
|
|
f" snippetsUrl: '{snippets_url}',\n"
|
|
" });\n"
|
|
"\n"
|
|
" if (typeof SxEditor !== 'undefined') {\n"
|
|
" SxEditor.mount('sx-editor', {\n"
|
|
" initialSx: (document.getElementById('sx-content-input') || {}).value || null,\n"
|
|
" csrfToken: csrfToken,\n"
|
|
" uploadUrls: uploadUrls,\n"
|
|
f" oembedUrl: '{oembed_url}',\n"
|
|
" onChange: function(sx) {\n"
|
|
" document.getElementById('sx-content-input').value = sx;\n"
|
|
" }\n"
|
|
" });\n"
|
|
" }\n"
|
|
"\n"
|
|
" document.addEventListener('keydown', function(e) {\n"
|
|
" if ((e.ctrlKey || e.metaKey) && e.key === 's') {\n"
|
|
" e.preventDefault();\n"
|
|
" document.getElementById('post-new-form').requestSubmit();\n"
|
|
" }\n"
|
|
" });\n"
|
|
" }\n"
|
|
"\n"
|
|
" if (typeof window.mountEditor === 'function') {\n"
|
|
" init();\n"
|
|
" } else {\n"
|
|
" var _t = setInterval(function() {\n"
|
|
" if (typeof window.mountEditor === 'function') { clearInterval(_t); init(); }\n"
|
|
" }, 50);\n"
|
|
" }\n"
|
|
"})();\n"
|
|
)
|
|
parts.append(sx_call("blog-editor-scripts",
|
|
js_src=editor_js,
|
|
sx_editor_js_src=sx_editor_js,
|
|
init_js=init_js))
|
|
|
|
from shared.sx.parser import SxExpr
|
|
return sx_call("blog-editor-panel",
|
|
parts=SxExpr("(<> " + " ".join(parts) + ")")) if parts else ""
|