/** * Editor.js integration — lifecycle-managed page editor module. */ import EditorJS from '../../vendor/editorjs/editorjs.mjs'; import { postForm } from '../core/app-http.js'; import { warnOnce, resolveHost } from '../core/app-dom.js'; const parseData = (value) => { if (!value) { return { blocks: [] }; } try { const parsed = JSON.parse(value); if (parsed && typeof parsed === 'object') { if (!Array.isArray(parsed.blocks)) { parsed.blocks = []; } return parsed; } } catch { // ignore invalid JSON } return { blocks: [] }; }; const resolveTools = (form) => { const HeaderTool = window.Header || window.EditorJSHeader || null; const ListTool = window.EditorjsList || window.List || null; const ChecklistTool = window.Checklist || window.EditorJSChecklist || null; const TableTool = window.Table || window.EditorJSTable || null; const ColumnsTool = window.editorjsColumns || window.Columns || null; const MarkerTool = window.Marker || null; const tools = {}; if (HeaderTool) { tools.header = { class: HeaderTool, inlineToolbar: true, config: { placeholder: form.dataset.headerPlaceholder || 'Heading', levels: [1, 2, 3, 4], defaultLevel: 1, }, }; } if (ListTool) { tools.list = { class: ListTool, inlineToolbar: true, config: { defaultStyle: 'unordered', }, }; } if (ChecklistTool) { tools.checklist = { class: ChecklistTool, inlineToolbar: true, }; } if (TableTool) { tools.table = { class: TableTool, inlineToolbar: true, config: { rows: 2, cols: 2, }, }; } if (ColumnsTool) { const innerTools = { ...tools }; tools.columns = { class: ColumnsTool, config: { EditorJsLibrary: EditorJS, tools: innerTools, }, }; } if (MarkerTool) { tools.marker = { class: MarkerTool, shortcut: 'CMD+SHIFT+M', }; } return { tools, inlineTools: MarkerTool ? ['marker', 'bold', 'italic', 'link'] : ['bold', 'italic', 'link'], }; }; export function initPageEditor(root = document, config = {}) { const host = resolveHost(root); const formSelector = String(config.selector || '[data-page-editor]').trim() || '[data-page-editor]'; const form = host.matches?.(formSelector) ? host : host.querySelector(formSelector); if (!(form instanceof HTMLFormElement)) { return { destroy: () => {} }; } if (form.dataset.pageEditorBound === '1' && form._pageEditorApi) { return form._pageEditorApi; } const holderSelector = String(config.holderSelector || '[data-editor-holder]').trim() || '[data-editor-holder]'; const contentFieldSelector = String(config.contentFieldSelector || '#page-content').trim() || '#page-content'; const controlsRootSelector = String(config.controlsRootSelector || '').trim(); const toggleSelector = String(config.toggleSelector || '[data-editor-toggle]').trim() || '[data-editor-toggle]'; const saveSelector = String(config.saveSelector || '[data-editor-save]').trim() || '[data-editor-save]'; const holder = form.querySelector(holderSelector); if (!(holder instanceof HTMLElement)) { warnOnce('UI_EL_MISSING', 'Missing editor holder', { module: 'page-editor' }); return { destroy: () => {} }; } const contentField = form.querySelector(contentFieldSelector); if (!(contentField instanceof HTMLTextAreaElement)) { warnOnce('UI_EL_MISSING', `Missing page editor content field: ${contentFieldSelector}`, { module: 'page-editor' }); return { destroy: () => {} }; } const controlsRoot = controlsRootSelector !== '' ? document.querySelector(controlsRootSelector) : document; const controlsHost = controlsRoot && typeof controlsRoot.querySelector === 'function' ? controlsRoot : document; const toggleButton = controlsHost.querySelector(toggleSelector); const saveButton = controlsHost.querySelector(saveSelector); const canEdit = form.dataset.canEdit === '1'; let mode = form.dataset.editMode === 'edit' && canEdit ? 'edit' : 'view'; form.dataset.editMode = mode; form.dataset.pageEditorBound = '1'; const setToggleLabel = (nextMode) => { if (!(toggleButton instanceof HTMLElement)) { return; } const viewLabel = toggleButton.dataset.viewLabel || ''; const editLabel = toggleButton.dataset.editLabel || ''; const text = nextMode === 'edit' ? viewLabel : editLabel; const icon = toggleButton.querySelector('[data-editor-icon]'); if (icon instanceof HTMLElement) { icon.className = nextMode === 'edit' ? 'bi bi-eye-fill' : 'bi bi-pencil-square'; } toggleButton.setAttribute('aria-label', text); toggleButton.dataset.tooltip = text; }; setToggleLabel(mode); if (saveButton instanceof HTMLButtonElement) { saveButton.disabled = mode !== 'edit'; } const toolConfig = resolveTools(form); const editor = new EditorJS({ holder, data: parseData(contentField.value), readOnly: mode !== 'edit', autofocus: mode === 'edit', placeholder: form.dataset.placeholder || undefined, tools: toolConfig.tools, inlineToolbar: toolConfig.inlineTools, }); const toggleMode = async (nextMode) => { if (!canEdit || mode === nextMode) { return; } mode = nextMode; form.dataset.editMode = mode; setToggleLabel(mode); if (editor.readOnly && typeof editor.readOnly.toggle === 'function') { await editor.readOnly.toggle(mode !== 'edit'); } if (saveButton instanceof HTMLButtonElement) { saveButton.disabled = mode !== 'edit'; } }; const onToggleClick = () => { void toggleMode(mode === 'edit' ? 'view' : 'edit'); }; const onSubmit = async (event) => { if (form.dataset.submitting === '1') { return; } event.preventDefault(); const output = await editor.save(); contentField.value = JSON.stringify(output); form.dataset.submitting = '1'; try { const data = await postForm(form.action || window.location.pathname, new FormData(form)); if (data && typeof data === 'object' && typeof data.redirect === 'string' && data.redirect.trim() !== '') { window.location.href = data.redirect; return; } window.location.reload(); } catch { form.submit(); } }; if (toggleButton instanceof HTMLElement && canEdit) { toggleButton.addEventListener('click', onToggleClick); } if (canEdit) { form.addEventListener('submit', onSubmit); } let destroyed = false; const destroy = () => { if (destroyed) { return; } destroyed = true; if (toggleButton instanceof HTMLElement && canEdit) { toggleButton.removeEventListener('click', onToggleClick); } if (canEdit) { form.removeEventListener('submit', onSubmit); } if (typeof editor.destroy === 'function') { editor.destroy(); } delete form.dataset.pageEditorBound; delete form._pageEditorApi; }; const api = { destroy }; form._pageEditorApi = api; return api; }