778 lines
32 KiB
Plaintext
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();
|
|
}
|