/** * Editor.js Entry Point * Self-hosted Gutenberg-like editor */ import EditorJS from '@editorjs/editorjs'; import Header from '@editorjs/header'; import List from '@editorjs/list'; import Quote from '@editorjs/quote'; import Code from '@editorjs/code'; import Table from '@editorjs/table'; import Delimiter from '@editorjs/delimiter'; import ImageTool from '@editorjs/image'; import LinkTool from '@editorjs/link'; // Initialize Editor.js when DOM is ready document.addEventListener('DOMContentLoaded', function() { const editorContainer = document.getElementById('editorjs'); if (!editorContainer) { console.warn('Editor.js container not found'); return; } // Get hidden inputs const contentJsonInput = document.getElementById('content_json'); const contentHtmlInput = document.getElementById('content_html'); const contentInput = document.getElementById('content'); const excerptInput = document.getElementById('excerpt'); const featuredImageInput = document.getElementById('featured_image'); const statusInput = document.getElementById('status'); // Convert HTML to Editor.js format function convertHtmlToEditorJs(html) { if (!html || html.trim() === '') { return null; } // Create temporary div to parse HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = html.trim(); const blocks = []; let blockId = 0; // Process all child nodes function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim(); if (text) { blocks.push({ type: 'paragraph', data: { text: text }, id: `block-${blockId++}` }); } return; } if (node.nodeType !== Node.ELEMENT_NODE) { return; } const tagName = node.tagName.toLowerCase(); switch (tagName) { case 'p': const pText = node.textContent.trim(); if (pText) { blocks.push({ type: 'paragraph', data: { text: pText }, id: `block-${blockId++}` }); } break; case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': const level = parseInt(tagName.charAt(1)); const headerText = node.textContent.trim(); if (headerText) { blocks.push({ type: 'header', data: { text: headerText, level: level }, id: `block-${blockId++}` }); } break; case 'ul': case 'ol': const items = []; const listItems = node.querySelectorAll('li'); listItems.forEach(li => { const itemText = li.textContent.trim(); if (itemText) { items.push(itemText); } }); if (items.length > 0) { blocks.push({ type: 'list', data: { style: tagName === 'ol' ? 'ordered' : 'unordered', items: items }, id: `block-${blockId++}` }); } break; case 'blockquote': const quoteText = node.querySelector('p')?.textContent.trim() || node.textContent.trim(); const quoteCaption = node.querySelector('cite')?.textContent.trim() || ''; if (quoteText) { blocks.push({ type: 'quote', data: { text: quoteText, caption: quoteCaption }, id: `block-${blockId++}` }); } break; case 'pre': const codeText = node.querySelector('code')?.textContent || node.textContent.trim(); if (codeText) { blocks.push({ type: 'code', data: { code: codeText }, id: `block-${blockId++}` }); } break; case 'table': const rows = []; const tableRows = node.querySelectorAll('tr'); tableRows.forEach(tr => { const cells = []; const tableCells = tr.querySelectorAll('td, th'); tableCells.forEach(cell => { cells.push(cell.textContent.trim()); }); if (cells.length > 0) { rows.push(cells); } }); if (rows.length > 0) { blocks.push({ type: 'table', data: { content: rows }, id: `block-${blockId++}` }); } break; case 'hr': blocks.push({ type: 'delimiter', data: {}, id: `block-${blockId++}` }); break; case 'figure': case 'img': const img = node.tagName === 'img' ? node : node.querySelector('img'); if (img && img.src) { const caption = node.querySelector('figcaption')?.textContent.trim() || img.alt || ''; blocks.push({ type: 'image', data: { file: { url: img.src }, caption: caption }, id: `block-${blockId++}` }); } break; default: // For other elements, process children Array.from(node.childNodes).forEach(child => { processNode(child); }); break; } } // Process all direct children Array.from(tempDiv.childNodes).forEach(child => { processNode(child); }); // If no blocks created, create empty paragraph if (blocks.length === 0) { blocks.push({ type: 'paragraph', data: { text: '' }, id: `block-${blockId++}` }); } return { time: Date.now(), blocks: blocks, version: '2.31.0' }; } // Parse initial data let initialData = null; // Try to load from content_json first if (contentJsonInput && contentJsonInput.value) { try { initialData = JSON.parse(contentJsonInput.value); } catch (e) { console.error('Failed to parse initial JSON:', e); } } // If no JSON data, try to convert from HTML if (!initialData) { const htmlContent = (contentHtmlInput && contentHtmlInput.value) ? contentHtmlInput.value : (contentInput && contentInput.value) ? contentInput.value : ''; if (htmlContent) { console.log('Converting HTML to Editor.js format...'); initialData = convertHtmlToEditorJs(htmlContent); // Update content_json input with converted data if (initialData && contentJsonInput) { contentJsonInput.value = JSON.stringify(initialData); } } } // Initialize Editor.js const editor = new EditorJS({ holder: 'editorjs', data: initialData, placeholder: 'Mulai menulis konten...', tools: { header: { class: Header, config: { placeholder: 'Masukkan heading', levels: [1, 2, 3, 4, 5, 6], defaultLevel: 2, }, inlineToolbar: true, }, list: { class: List, inlineToolbar: true, config: { defaultStyle: 'unordered', }, }, quote: { class: Quote, inlineToolbar: true, shortcut: 'CMD+SHIFT+O', config: { quotePlaceholder: 'Masukkan kutipan', captionPlaceholder: 'Penulis kutipan', }, }, code: { class: Code, config: { placeholder: 'Masukkan kode', }, }, table: { class: Table, inlineToolbar: true, config: { rows: 2, cols: 2, }, }, delimiter: Delimiter, image: { class: ImageTool, config: { endpoints: { byFile: window.uploadEndpoint || '/admin/upload', }, field: 'image', types: 'image/jpeg,image/png,image/webp', captionPlaceholder: 'Masukkan caption gambar', buttonContent: 'Pilih gambar', uploader: { async uploadByFile(file) { const formData = new FormData(); formData.append('image', file); formData.append(window.csrfTokenName, window.csrfTokenValue); const response = await fetch(window.uploadEndpoint || '/admin/upload', { method: 'POST', headers: { [window.csrfHeaderName]: window.csrfTokenValue, }, body: formData, }); if (!response.ok) { throw new Error('Upload failed'); } const result = await response.json(); return { success: 1, file: { url: result.url, }, }; }, }, }, }, linkTool: { class: LinkTool, config: { endpoint: window.linkPreviewEndpoint || '/admin/link-preview', }, }, }, autofocus: false, onChange: async () => { // Autosave every 10 seconds clearTimeout(window.autosaveTimeout); window.autosaveTimeout = setTimeout(async () => { await saveEditorContent(true); }, 10000); }, }); // Save function async function saveEditorContent(isAutosave = false) { try { const outputData = await editor.save(); const jsonString = JSON.stringify(outputData); // Update hidden inputs if (contentJsonInput) { contentJsonInput.value = jsonString; } // Render to HTML (client-side preview) const htmlContent = renderEditorJsToHtml(outputData.blocks); if (contentHtmlInput) { contentHtmlInput.value = htmlContent; } // Extract excerpt from first paragraph const firstParagraph = outputData.blocks.find(b => b.type === 'paragraph'); if (firstParagraph && excerptInput) { const excerpt = firstParagraph.data.text.substring(0, 160).replace(/<[^>]*>/g, ''); excerptInput.value = excerpt; } // Autosave to server if (isAutosave && window.pageId) { const formData = new FormData(); formData.append('content_json', jsonString); formData.append('content_html', htmlContent); formData.append(window.csrfTokenName, window.csrfTokenValue); await fetch(`/admin/pages/autosave/${window.pageId}`, { method: 'POST', headers: { [window.csrfHeaderName]: window.csrfTokenValue, }, body: formData, }); // Show autosave indicator const indicator = document.getElementById('autosave-indicator'); if (indicator) { indicator.textContent = 'Disimpan otomatis'; indicator.classList.remove('hidden'); setTimeout(() => { indicator.classList.add('hidden'); }, 2000); } } return outputData; } catch (error) { console.error('Error saving editor content:', error); throw error; } } // Render Editor.js blocks to HTML function renderEditorJsToHtml(blocks) { let html = ''; blocks.forEach(block => { switch(block.type) { case 'paragraph': html += `
${escapeHtml(block.data.text)}
`; break; case 'header': html += ``; break; case 'code': html += `${escapeHtml(block.data.text)}
`; if (block.data.caption) { html += `${escapeHtml(block.data.caption)}`; } html += `
${escapeHtml(block.data.code)}`;
break;
case 'table':
html += '| ${escapeHtml(cell)} | `; }); html += '