Files
cms-gov/resources/js/editor/editor.js

600 lines
17 KiB
JavaScript
Raw Normal View History

/**
* 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 += `<p>${escapeHtml(block.data.text)}</p>`;
break;
case 'header':
html += `<h${block.data.level}>${escapeHtml(block.data.text)}</h${block.data.level}>`;
break;
case 'list':
const listTag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += `<${listTag}>`;
block.data.items.forEach(item => {
html += `<li>${escapeHtml(item)}</li>`;
});
html += `</${listTag}>`;
break;
case 'quote':
html += `<blockquote><p>${escapeHtml(block.data.text)}</p>`;
if (block.data.caption) {
html += `<cite>${escapeHtml(block.data.caption)}</cite>`;
}
html += `</blockquote>`;
break;
case 'code':
html += `<pre><code>${escapeHtml(block.data.code)}</code></pre>`;
break;
case 'table':
html += '<table><tbody>';
block.data.content.forEach(row => {
html += '<tr>';
row.forEach(cell => {
html += `<td>${escapeHtml(cell)}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
break;
case 'delimiter':
html += '<hr>';
break;
case 'image':
html += `<figure><img src="${escapeHtml(block.data.file.url)}" alt="${escapeHtml(block.data.caption || '')}">`;
if (block.data.caption) {
html += `<figcaption>${escapeHtml(block.data.caption)}</figcaption>`;
}
html += '</figure>';
break;
case 'linkTool':
html += `<div class="link-tool"><a href="${escapeHtml(block.data.link)}" target="_blank">${escapeHtml(block.data.meta?.title || block.data.link)}</a></div>`;
break;
default:
// Unknown block type - ignore safely
console.warn('Unknown block type:', block.type);
}
});
return html;
}
// Escape HTML
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Handle form submit
const form = editorContainer.closest('form');
if (form) {
const submitHandler = async function(e) {
e.preventDefault();
try {
await saveEditorContent(false);
// Remove event listener to avoid infinite loop
form.removeEventListener('submit', submitHandler);
// Use requestSubmit if available (modern browsers), otherwise create submit button
if (form.requestSubmit) {
form.requestSubmit();
} else {
// Fallback: create temporary submit button
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.style.display = 'none';
form.appendChild(submitButton);
submitButton.click();
form.removeChild(submitButton);
}
} catch (error) {
console.error('Error saving before submit:', error);
alert('Error saat menyimpan konten. Silakan coba lagi.');
}
};
form.addEventListener('submit', submitHandler);
}
// Preview button handler
const previewBtn = document.getElementById('preview-btn');
if (previewBtn) {
previewBtn.addEventListener('click', async function() {
try {
const outputData = await editor.save();
const htmlContent = renderEditorJsToHtml(outputData.blocks);
// Open preview in new window
const previewWindow = window.open('', '_blank');
previewWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Preview</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }
img { max-width: 100%; height: auto; }
table { border-collapse: collapse; width: 100%; }
table td, table th { border: 1px solid #ddd; padding: 8px; }
</style>
</head>
<body>
${htmlContent}
</body>
</html>
`);
} catch (error) {
console.error('Error generating preview:', error);
alert('Error saat membuat preview.');
}
});
}
console.log('Editor.js initialized successfully');
// Fix toolbar z-index to stay below header
setTimeout(() => {
const style = document.createElement('style');
style.id = 'editorjs-zindex-fix';
style.textContent = `
.ce-toolbar,
.ce-inline-toolbar,
.ce-popover,
.ce-conversion-toolbar,
.ce-settings,
.ce-block-settings,
.ce-toolbar__plus,
.ce-toolbar__settings-btn,
.ce-popover__item,
.ce-popover__items,
.ce-settings__button,
.ce-toolbar__content,
.ce-toolbar__actions {
z-index: 10 !important;
}
header,
header[class*="sticky"],
header[class*="fixed"],
header.sticky,
header.fixed {
z-index: 99999 !important;
}
`;
// Remove existing fix if any
const existingFix = document.getElementById('editorjs-zindex-fix');
if (existingFix) {
existingFix.remove();
}
document.head.appendChild(style);
}, 100);
});