"""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 ""