Files
sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/Edit Page - SIS Vietnam - Manage_files/editor-config.js.download
T

778 lines
32 KiB
Plaintext

/**
* SIS Vietnam - Block Editor Configuration
*
* Built on Editor.js (https://editorjs.io)
*
* PLUGIN ARCHITECTURE:
* =====================
* This editor uses a plugin registry pattern. The built-in block types
* (Header, List, Quote, etc.) are registered by default.
*
* To add a CUSTOM BLOCK TYPE in the future:
*
* 1. Create a new JS file in /js/manage/editor-plugins/
* Example: /js/manage/editor-plugins/my-custom-block.js
*
* 2. In that file, register your tool BEFORE the editor initializes:
*
* window.SISEditorPlugins = window.SISEditorPlugins || {};
* window.SISEditorPlugins['myBlock'] = {
* class: MyBlockClass, // Your Editor.js Tool class
* inlineToolbar: true, // optional
* config: { ... } // optional tool-specific config
* };
*
* 3. Load the script in your template BEFORE editor-config.js:
* <script src="/js/manage/editor-plugins/my-custom-block.js"></script>
*
* 4. The editor will automatically pick up all registered plugins.
*/
// Global plugin registry — external plugins register here
window.SISEditorPlugins = window.SISEditorPlugins || {};
/**
* Global Media Library Modal Handler
* Callable from any Editor.js plugin via window.openSISMediaModal(callback)
*/
window.openSISMediaModal = function (callback) {
if (window.SISMediaPicker && typeof window.SISMediaPicker.open === 'function') {
window.SISMediaPicker.open(callback);
} else {
window.sisMediaSelectCallback = callback;
var modalEl = document.getElementById('sisMediaLibraryModal');
if (modalEl && typeof $ !== 'undefined' && $.fn && $.fn.modal) {
$(modalEl).modal('show');
}
}
};
/**
* Dynamic Monaco Editor Script Loader Helper
*/
window.loadMonacoEditor = function () {
if (window.monaco) return Promise.resolve(window.monaco);
if (window._monacoLoadingPromise) return window._monacoLoadingPromise;
window._monacoLoadingPromise = new Promise((resolve, reject) => {
if (typeof require !== 'undefined' && require.config) {
require(['vs/editor/editor.main'], function () {
resolve(window.monaco);
});
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js';
script.onload = () => {
window.require.config({
paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }
});
window.require(['vs/editor/editor.main'], () => {
resolve(window.monaco);
});
};
script.onerror = (err) => reject(err);
document.head.appendChild(script);
});
return window._monacoLoadingPromise;
};
class SISRawHtmlTool {
static get toolbox() {
return {
title: 'Raw HTML',
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>'
};
}
constructor({ data, api, readOnly }) {
this.api = api;
this.readOnly = readOnly;
this.data = {
html: (data && data.html) ? data.html : '',
stretched: !!(data && data.stretched)
};
this.textarea = null;
this.editorInstance = null;
}
render() {
const container = document.createElement('div');
container.style.width = '100%';
container.className = 'ce-raw-html-wrapper mb-3';
const stretchWrapper = document.createElement('div');
stretchWrapper.className = 'custom-control custom-switch mb-2';
this.stretchCheck = document.createElement('input');
this.stretchCheck.type = 'checkbox';
this.stretchCheck.className = 'custom-control-input';
this.stretchCheck.id = 'raw_stretch_' + Math.random().toString(36).substring(7);
this.stretchCheck.checked = !!(this.data && this.data.stretched);
const stretchLabel = document.createElement('label');
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
stretchLabel.htmlFor = this.stretchCheck.id;
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch block to Full Screen Width (Independent Breakout)';
this.stretchCheck.addEventListener('change', () => {
this.data.stretched = this.stretchCheck.checked;
});
stretchWrapper.appendChild(this.stretchCheck);
stretchWrapper.appendChild(stretchLabel);
container.appendChild(stretchWrapper);
// Header status bar
const headerBar = document.createElement('div');
headerBar.className = 'bg-dark text-white p-2 rounded-top d-flex justify-content-between align-items-center small font-weight-bold';
headerBar.innerHTML = '<span><i class="fab fa-html5 text-danger"></i> Raw HTML Code (Monaco IDE Editor)</span><span class="badge badge-info font-weight-normal">HTML5</span>';
container.appendChild(headerBar);
// Monaco Container
const editorContainer = document.createElement('div');
editorContainer.className = 'monaco-editor-box border border-dark rounded-bottom';
editorContainer.style.height = '350px';
editorContainer.style.width = '100%';
container.appendChild(editorContainer);
// Textarea fallback (hidden when Monaco is active)
this.textarea = document.createElement('textarea');
this.textarea.className = 'form-control d-none';
this.textarea.value = this.data.html || '';
container.appendChild(this.textarea);
if (this.readOnly) {
this.stretchCheck.disabled = true;
this.textarea.disabled = true;
}
// Initialize Monaco Editor
window.loadMonacoEditor().then((monaco) => {
if (!editorContainer || !document.body.contains(editorContainer)) return;
this.editorInstance = monaco.editor.create(editorContainer, {
value: this.data.html || '',
language: 'html',
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
tabSize: 2,
scrollBeyondLastLine: false,
readOnly: !!this.readOnly
});
this.editorInstance.onDidChangeModelContent(() => {
this.data.html = this.editorInstance.getValue();
if (this.textarea) this.textarea.value = this.data.html;
});
}).catch((err) => {
console.warn('[SIS Raw HTML Tool] Monaco Editor CDN fallback to Textarea:', err);
this.textarea.classList.remove('d-none');
this.textarea.style.minHeight = '300px';
this.textarea.style.backgroundColor = '#1e1e24';
this.textarea.style.color = '#66d9ef';
this.textarea.style.fontFamily = 'monospace';
editorContainer.style.display = 'none';
this.textarea.addEventListener('input', () => {
this.data.html = this.textarea.value;
});
});
return container;
}
save() {
const htmlVal = this.editorInstance ? this.editorInstance.getValue() : (this.textarea ? this.textarea.value : (this.data.html || ''));
return {
html: htmlVal,
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
};
}
}
class SISAccordionTool {
static get toolbox() {
return {
title: 'Accordion / Collapse',
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>'
};
}
constructor({ data, api, readOnly }) {
this.api = api;
this.readOnly = readOnly;
this.data = {
title: (data && data.title) ? data.title : '',
content: (data && data.content) ? data.content : '',
isOpen: !!(data && data.isOpen),
stretched: !!(data && data.stretched)
};
this.titleInput = null;
this.contentInput = null;
}
render() {
const container = document.createElement('div');
container.style.border = '1px solid #d1d3e2';
container.style.borderRadius = '6px';
container.style.padding = '12px';
container.style.background = '#ffffff';
container.style.marginBottom = '10px';
const headerLabel = document.createElement('label');
headerLabel.className = 'font-weight-bold text-primary small mb-1';
headerLabel.innerHTML = '<i class="fas fa-chevron-circle-down"></i> Accordion Title / Summary';
this.titleInput = document.createElement('input');
this.titleInput.type = 'text';
this.titleInput.className = 'form-control mb-2';
this.titleInput.placeholder = 'Enter accordion header/title...';
this.titleInput.value = this.data.title;
const bodyLabel = document.createElement('label');
bodyLabel.className = 'font-weight-bold text-secondary small mb-1';
bodyLabel.innerHTML = '<i class="fas fa-align-left"></i> Accordion Content / Details';
this.contentInput = document.createElement('textarea');
this.contentInput.className = 'form-control';
this.contentInput.style.minHeight = '100px';
this.contentInput.placeholder = 'Enter body content (HTML or text)...';
this.contentInput.value = this.data.content;
if (this.readOnly) {
this.titleInput.disabled = true;
this.contentInput.disabled = true;
}
this.titleInput.addEventListener('input', () => {
this.data.title = this.titleInput.value;
});
this.contentInput.addEventListener('input', () => {
this.data.content = this.contentInput.value;
});
container.appendChild(headerLabel);
container.appendChild(this.titleInput);
container.appendChild(bodyLabel);
container.appendChild(this.contentInput);
// Open by default toggle
const openWrapper = document.createElement('div');
openWrapper.className = 'custom-control custom-switch mt-2';
this.openCheck = document.createElement('input');
this.openCheck.type = 'checkbox';
this.openCheck.className = 'custom-control-input';
this.openCheck.id = 'acc_open_' + Math.random().toString(36).substring(7);
this.openCheck.checked = !!(this.data && this.data.isOpen);
const openLabel = document.createElement('label');
openLabel.className = 'custom-control-label small font-weight-bold text-primary';
openLabel.htmlFor = this.openCheck.id;
openLabel.innerHTML = '<i class="fas fa-folder-open"></i> Expand / Uncollapse by default after loading page';
this.openCheck.addEventListener('change', () => {
this.data.isOpen = this.openCheck.checked;
});
openWrapper.appendChild(this.openCheck);
openWrapper.appendChild(openLabel);
container.appendChild(openWrapper);
// Stretch toggle
const stretchWrapper = document.createElement('div');
stretchWrapper.className = 'custom-control custom-switch mt-1';
this.stretchCheck = document.createElement('input');
this.stretchCheck.type = 'checkbox';
this.stretchCheck.className = 'custom-control-input';
this.stretchCheck.id = 'acc_stretch_' + Math.random().toString(36).substring(7);
this.stretchCheck.checked = !!(this.data && this.data.stretched);
const stretchLabel = document.createElement('label');
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
stretchLabel.htmlFor = this.stretchCheck.id;
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch block to Full Screen Width (Independent Breakout)';
this.stretchCheck.addEventListener('change', () => {
this.data.stretched = this.stretchCheck.checked;
});
stretchWrapper.appendChild(this.stretchCheck);
stretchWrapper.appendChild(stretchLabel);
container.appendChild(stretchWrapper);
return container;
}
save() {
return {
title: this.titleInput ? this.titleInput.value : this.data.title,
content: this.contentInput ? this.contentInput.value : this.data.content,
isOpen: this.openCheck ? this.openCheck.checked : !!(this.data && this.data.isOpen),
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
};
}
}
class SISYouTubeTool {
static get toolbox() {
return {
title: 'YouTube Video',
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="red"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>'
};
}
constructor({ data, api, readOnly }) {
this.api = api;
this.readOnly = readOnly;
this.data = {
url: (data && data.url) ? data.url : '',
thumbnailUrl: (data && data.thumbnailUrl) ? data.thumbnailUrl : '',
caption: (data && data.caption) ? data.caption : '',
stretched: !!(data && data.stretched)
};
this.urlInput = null;
this.thumbnailInput = null;
this.captionInput = null;
this.previewContainer = null;
this.iframe = null;
this.isIframeActive = false;
}
extractVideoId(input) {
if (!input) return '';
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
var match = input.match(regExp);
if (match && match[2].length === 11) {
return match[2];
}
if (input.length === 11) {
return input;
}
return '';
}
getEmbedUrl(videoId) {
return videoId ? 'https://www.youtube.com/embed/' + videoId : '';
}
getThumbnailUrl(videoId) {
return videoId ? 'https://img.youtube.com/vi/' + videoId + '/hqdefault.jpg' : '';
}
render() {
const container = document.createElement('div');
container.style.border = '1px solid #e3e6f0';
container.style.borderRadius = '8px';
container.style.padding = '14px';
container.style.background = '#fff';
container.style.marginBottom = '12px';
const label = document.createElement('label');
label.className = 'font-weight-bold text-danger small mb-1';
label.innerHTML = '<i class="fab fa-youtube"></i> YouTube Video Link / URL / ID';
this.urlInput = document.createElement('input');
this.urlInput.type = 'text';
this.urlInput.className = 'form-control mb-2';
this.urlInput.placeholder = 'Paste YouTube URL (e.g. https://www.youtube.com/watch?v=sDE4ZZdNOOY)...';
this.urlInput.value = this.data.url;
// Custom Thumbnail Field
const thumbLabel = document.createElement('label');
thumbLabel.className = 'font-weight-bold text-secondary small mb-1';
thumbLabel.innerHTML = '<i class="fas fa-image"></i> Custom Thumbnail Image URL (Optional - Overrides YouTube Cover)';
this.thumbnailInput = document.createElement('input');
this.thumbnailInput.type = 'text';
this.thumbnailInput.className = 'form-control form-control-sm mb-2';
this.thumbnailInput.placeholder = 'Custom Thumbnail Image URL (leave blank to auto-use YouTube cover)...';
this.thumbnailInput.value = this.data.thumbnailUrl;
// Thumbnail Preview Container
this.previewContainer = document.createElement('div');
this.previewContainer.className = 'yt-thumbnail-preview';
this.previewContainer.style.position = 'relative';
this.previewContainer.style.paddingBottom = '56.25%';
this.previewContainer.style.height = '0';
this.previewContainer.style.overflow = 'hidden';
this.previewContainer.style.background = '#111 center/cover no-repeat';
this.previewContainer.style.borderRadius = '6px';
this.previewContainer.style.marginBottom = '8px';
this.previewContainer.style.cursor = 'pointer';
this.previewContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
// Play Button Overlay
const playBtn = document.createElement('div');
playBtn.className = 'yt-play-button-overlay';
playBtn.style.position = 'absolute';
playBtn.style.top = '50%';
playBtn.style.left = '50%';
playBtn.style.transform = 'translate(-50%, -50%)';
playBtn.style.transition = 'transform 0.2s ease';
playBtn.innerHTML = '<svg width="68" height="48" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="#ff0000"/><path d="M45 24L27 14v20z" fill="#ffffff"/></svg>';
this.previewContainer.appendChild(playBtn);
// Update Thumbnail Image
const updatePreview = () => {
const customThumb = this.thumbnailInput.value.trim();
const videoId = this.extractVideoId(this.urlInput.value);
const thumbUrl = customThumb || this.getThumbnailUrl(videoId);
if (thumbUrl) {
this.previewContainer.style.backgroundImage = 'url("' + thumbUrl + '")';
playBtn.style.display = 'block';
} else {
this.previewContainer.style.backgroundImage = 'none';
this.previewContainer.style.backgroundColor = '#222';
}
};
// Click thumbnail to play live video
this.previewContainer.addEventListener('click', () => {
const videoId = this.extractVideoId(this.urlInput.value);
if (videoId && !this.isIframeActive) {
this.isIframeActive = true;
this.previewContainer.innerHTML = '';
const iframe = document.createElement('iframe');
iframe.style.position = 'absolute';
iframe.style.top = '0';
iframe.style.left = '0';
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = '0';
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share');
iframe.setAttribute('allowfullscreen', 'true');
iframe.src = this.getEmbedUrl(videoId) + '?autoplay=1';
this.previewContainer.appendChild(iframe);
}
});
updatePreview();
this.captionInput = document.createElement('input');
this.captionInput.type = 'text';
this.captionInput.className = 'form-control form-control-sm mb-2';
this.captionInput.placeholder = 'Video caption (optional)...';
this.captionInput.value = this.data.caption;
if (this.readOnly) {
this.urlInput.disabled = true;
this.thumbnailInput.disabled = true;
this.captionInput.disabled = true;
}
this.urlInput.addEventListener('input', () => {
const videoId = this.extractVideoId(this.urlInput.value);
this.data.url = this.getEmbedUrl(videoId);
this.isIframeActive = false;
this.previewContainer.innerHTML = '';
this.previewContainer.appendChild(playBtn);
updatePreview();
});
this.thumbnailInput.addEventListener('input', () => {
this.data.thumbnailUrl = this.thumbnailInput.value.trim();
updatePreview();
});
this.captionInput.addEventListener('input', () => {
this.data.caption = this.captionInput.value;
});
container.appendChild(label);
container.appendChild(this.urlInput);
container.appendChild(thumbLabel);
container.appendChild(this.thumbnailInput);
container.appendChild(this.previewContainer);
container.appendChild(this.captionInput);
const stretchWrapper = document.createElement('div');
stretchWrapper.className = 'custom-control custom-switch mt-2';
this.stretchCheck = document.createElement('input');
this.stretchCheck.type = 'checkbox';
this.stretchCheck.className = 'custom-control-input';
this.stretchCheck.id = 'yt_stretch_' + Math.random().toString(36).substring(7);
this.stretchCheck.checked = !!(this.data && this.data.stretched);
const stretchLabel = document.createElement('label');
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
stretchLabel.htmlFor = this.stretchCheck.id;
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch block to Full Screen Width (Independent Breakout)';
this.stretchCheck.addEventListener('change', () => {
this.data.stretched = this.stretchCheck.checked;
});
stretchWrapper.appendChild(this.stretchCheck);
stretchWrapper.appendChild(stretchLabel);
container.appendChild(stretchWrapper);
return container;
}
save() {
const videoId = this.extractVideoId(this.urlInput ? this.urlInput.value : this.data.url);
return {
url: this.getEmbedUrl(videoId) || this.data.url,
thumbnailUrl: this.thumbnailInput ? this.thumbnailInput.value.trim() : (this.data.thumbnailUrl || ''),
caption: this.captionInput ? this.captionInput.value : this.data.caption,
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
};
}
}
// SISHeroBannerTool extracted to /js/manage/editor-plugins/hero-banner.js
/**
* Initialize the SIS Block Editor on a given holder element.
*
* @param {string} holderId - The DOM element ID for the editor container
* @param {string} hiddenInputId - The DOM element ID for the hidden input storing JSON
* @param {object|null} initialData - Pre-existing Editor.js JSON data to load
* @param {boolean} [skipSubmitHandler=false] - If true, prevents automatic form submission handling
* @returns {EditorJS} The editor instance
*/
function initSISEditor(holderId, hiddenInputId, initialData, skipSubmitHandler = false) {
if (typeof EditorJS === 'undefined') {
console.error('[SIS Editor] Editor.js library is not loaded!');
return null;
}
// === Register Built-in Tools (Defensive Check for Globals) ===
var builtInTools = {};
if (typeof Header !== 'undefined') {
builtInTools.header = {
class: Header,
inlineToolbar: ['link'],
tunes: ['textStyling'],
config: {
placeholder: 'Header text...',
levels: [1, 2, 3, 4, 5, 6],
defaultLevel: 2
}
};
}
builtInTools.paragraph = {
tunes: ['textStyling']
};
if (typeof NestedList !== 'undefined') {
builtInTools.list = {
class: NestedList,
inlineToolbar: true,
tunes: ['textStyling'],
config: { defaultStyle: 'unordered' }
};
} else if (typeof List !== 'undefined') {
builtInTools.list = {
class: List,
inlineToolbar: true,
tunes: ['textStyling'],
config: { defaultStyle: 'unordered' }
};
}
if (typeof Quote !== 'undefined') {
builtInTools.quote = {
class: Quote,
inlineToolbar: true,
tunes: ['textStyling'],
config: {
quotePlaceholder: 'Enter a quote',
captionPlaceholder: 'Quote\'s author'
}
};
}
if (typeof Delimiter !== 'undefined') builtInTools.delimiter = { class: Delimiter };
if (typeof Table !== 'undefined') {
builtInTools.table = {
class: Table,
inlineToolbar: true,
tunes: ['textStyling'],
config: { rows: 2, cols: 3 }
};
}
if (typeof CodeTool !== 'undefined') builtInTools.code = { class: CodeTool };
if (typeof Warning !== 'undefined') builtInTools.warning = { class: Warning, inlineToolbar: true, config: { titlePlaceholder: 'Title', messagePlaceholder: 'Message' } };
if (typeof Marker !== 'undefined') builtInTools.marker = { class: Marker };
if (typeof InlineCode !== 'undefined') builtInTools.inlineCode = { class: InlineCode };
if (typeof Underline !== 'undefined') builtInTools.underline = { class: Underline };
if (typeof ImageTool !== 'undefined') {
builtInTools.image = {
class: ImageTool,
config: {
endpoints: {
byFile: '/api/manage/media/upload',
byUrl: '/api/manage/media/fetchUrl'
},
uploader: {
uploadByUrl(url) {
return Promise.resolve({
success: 1,
file: { url: url }
});
},
selectFile() {
return new Promise((resolve, reject) => {
if (window.SISMediaPicker) {
SISMediaPicker.open((url, mediaObj, config) => {
if (url) {
resolve({
success: 1,
file: {
url: url,
style: config ? config.style : '',
cssClass: config ? config.cssClass : '',
alt: config ? config.alt : '',
customAttributes: config ? config.customAttributes : '',
aspectRatio: config ? config.aspectRatio : ''
}
});
} else {
reject('No image selected');
}
});
} else {
reject('SISMediaPicker not loaded');
}
});
}
},
field: 'file',
types: 'image/*'
}
};
window.SISEditorPlugins.image = builtInTools.image;
}
if (typeof AttachesTool !== 'undefined') {
builtInTools.attaches = {
class: AttachesTool,
config: { endpoint: '/api/manage/media/upload', field: 'file' }
};
}
// Custom SIS Tools (Always present)
builtInTools.raw = { class: SISRawHtmlTool };
builtInTools.accordion = { class: SISAccordionTool };
builtInTools.youtube = { class: SISYouTubeTool };
if (typeof SISHeroBannerTool !== 'undefined') {
builtInTools.hero = { class: SISHeroBannerTool };
}
if (typeof SISStickyNavTool !== 'undefined') {
builtInTools.stickyNav = { class: SISStickyNavTool };
}
if (typeof TextStylingTune !== 'undefined') {
builtInTools.textStyling = { class: TextStylingTune };
}
// === Merge built-in tools with any registered plugins ===
var allTools = Object.assign({}, builtInTools, window.SISEditorPlugins);
// Apply the textStyling tune to all block tools dynamically
for (var toolName in allTools) {
if (toolName !== 'textStyling') {
if (!allTools[toolName].tunes) {
allTools[toolName].tunes = ['textStyling'];
} else if (!allTools[toolName].tunes.includes('textStyling')) {
allTools[toolName].tunes.push('textStyling');
}
}
}
// === Parse initial data ===
var editorData = null;
if (initialData && typeof initialData === 'string') {
try {
editorData = JSON.parse(initialData);
} catch (e) {
console.warn('[SIS Editor] Could not parse initial data as JSON, starting empty.', e);
editorData = null;
}
} else if (initialData && typeof initialData === 'object') {
editorData = initialData;
}
// === Create the Editor ===
var editor = new EditorJS({
holder: holderId,
tools: allTools,
data: editorData || undefined,
placeholder: 'Click here to start writing your page content...',
autofocus: false,
onReady: function() {
console.log('[SIS Editor] Ready. Tools loaded:', Object.keys(allTools));
},
onChange: function(api, event) {
// Auto-save to hidden input on every change
api.saver.save().then(function(outputData) {
var hiddenInput = document.getElementById(hiddenInputId);
if (hiddenInput) {
hiddenInput.value = JSON.stringify(outputData);
}
});
}
});
// === Form submission handler ===
// Ensure the latest content is saved before form submit
var hiddenInput = document.getElementById(hiddenInputId);
var form = hiddenInput ? hiddenInput.closest('form') : document.querySelector('form');
if (form && !skipSubmitHandler) {
var submitHandler = function(event) {
event.preventDefault();
editor.save().then(function(outputData) {
var hiddenInput = document.getElementById(hiddenInputId);
if (hiddenInput) {
hiddenInput.value = JSON.stringify(outputData);
}
// Now submit the form
form.removeEventListener('submit', submitHandler);
form.submit();
}).catch(function(error) {
console.error('[SIS Editor] Save failed:', error);
});
};
form.addEventListener('submit', submitHandler);
}
return editor;
}
/**
* Setup IDE Code Editor styling and Tab indentation for custom CSS and JS textareas
*/
function setupCodeTextareas() {
var codeSelectors = '#customCss, #customJs, #postCustomCss, #postCustomJs, textarea[name="customCss"], textarea[name="customJs"]';
document.querySelectorAll(codeSelectors).forEach(function(textarea) {
if (!textarea || textarea.dataset.codeEditorInit) return;
textarea.dataset.codeEditorInit = 'true';
// Enable Tab key indentation (inserts 2 spaces)
textarea.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
var start = this.selectionStart;
var end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 2;
}
});
});
}
// Auto-run code textareas setup on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupCodeTextareas);
} else {
setupCodeTextareas();
}