feat: add Raw HTML, Accordion, and YouTube plugin support to the editor configuration
This commit is contained in:
@@ -922,11 +922,23 @@ function initSISEditor(holderId, hiddenInputId, initialData, skipSubmitHandler =
|
||||
builtInTools.image = {
|
||||
class: ImageTool,
|
||||
config: {
|
||||
endpoints: { byFile: '/api/manage/media/upload' },
|
||||
endpoints: {
|
||||
byFile: '/api/manage/media/upload',
|
||||
byUrl: '/api/manage/media/fetchUrl'
|
||||
},
|
||||
uploader: {
|
||||
uploadByUrl(url) {
|
||||
return Promise.resolve({
|
||||
success: 1,
|
||||
file: { url: url }
|
||||
});
|
||||
}
|
||||
},
|
||||
field: 'file',
|
||||
types: 'image/*'
|
||||
}
|
||||
};
|
||||
window.SISEditorPlugins.image = builtInTools.image;
|
||||
}
|
||||
if (typeof AttachesTool !== 'undefined') {
|
||||
builtInTools.attaches = {
|
||||
|
||||
@@ -150,20 +150,21 @@ class FlexTool {
|
||||
|
||||
this.wrapper.appendChild(settingsRow);
|
||||
|
||||
// Layout Columns Number Input
|
||||
const countGroup = document.createElement('div');
|
||||
countGroup.className = 'form-group mb-3';
|
||||
countGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1 d-block">Number of Flex Items (1 - 12)</label>';
|
||||
// Background Settings Row
|
||||
const bgSettingsRow = document.createElement('div');
|
||||
bgSettingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||
|
||||
// 1. Flex Items (Cols)
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'col-md-2 mb-2';
|
||||
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex Items</label>';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.className = 'form-control form-control-sm';
|
||||
input.style.width = '100px';
|
||||
input.min = '1';
|
||||
input.max = '12';
|
||||
input.value = this.data.cols;
|
||||
if (this.readOnly) input.disabled = true;
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
let val = parseInt(e.target.value) || 2;
|
||||
if (val < 1) val = 1;
|
||||
@@ -171,9 +172,38 @@ class FlexTool {
|
||||
this.data.cols = val;
|
||||
this._renderColumnInputs();
|
||||
});
|
||||
colDiv.appendChild(input);
|
||||
bgSettingsRow.appendChild(colDiv);
|
||||
|
||||
countGroup.appendChild(input);
|
||||
this.wrapper.appendChild(countGroup);
|
||||
// 2. Global CSS Class
|
||||
const classDiv = document.createElement('div');
|
||||
classDiv.className = 'col-md-5 mb-2';
|
||||
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex CSS Class(es)</label>';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.placeholder = 'e.g. my-custom-flex';
|
||||
classInput.value = this.data.globalClass || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||
classDiv.appendChild(classInput);
|
||||
bgSettingsRow.appendChild(classDiv);
|
||||
|
||||
// 3. Global HTML ID
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.className = 'col-md-5 mb-2';
|
||||
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex HTML ID</label>';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.placeholder = 'e.g. flex-section-1';
|
||||
idInput.value = this.data.globalId || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||
idDiv.appendChild(idInput);
|
||||
bgSettingsRow.appendChild(idDiv);
|
||||
|
||||
this.wrapper.appendChild(bgSettingsRow);
|
||||
|
||||
// Columns inputs container
|
||||
this.inputsContainer = document.createElement('div');
|
||||
@@ -260,8 +290,8 @@ class FlexTool {
|
||||
|
||||
if (window.SISEditorPlugins) {
|
||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||
// Avoid self-nesting flex to prevent recursion issues
|
||||
if (key !== 'flex' && !types.some(t => t.value === key)) {
|
||||
// Allow nesting any registered plugins (no exclusions)
|
||||
if (!types.some(t => t.value === key)) {
|
||||
types.push({ value: key, label: `[Plugin] ${key}` });
|
||||
}
|
||||
});
|
||||
@@ -311,24 +341,150 @@ class FlexTool {
|
||||
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
||||
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
||||
try {
|
||||
const pluginClass = window.SISEditorPlugins[type].class;
|
||||
let parsedData = {};
|
||||
if (this.data[`col${i}`]) {
|
||||
try {
|
||||
parsedData = typeof this.data[`col${i}`] === 'string' ? JSON.parse(this.data[`col${i}`]) : this.data[`col${i}`];
|
||||
} catch (e) {
|
||||
parsedData = { html: this.data[`col${i}`] }; // fallback
|
||||
}
|
||||
}
|
||||
const instance = new pluginClass({
|
||||
data: parsedData,
|
||||
api: this.api,
|
||||
readOnly: this.readOnly
|
||||
});
|
||||
const pluginEntry = window.SISEditorPlugins[type];
|
||||
const pluginClass = pluginEntry.class;
|
||||
const pluginConfig = pluginEntry.config || {};
|
||||
let parsedData = {};
|
||||
if (this.data[`col${i}`]) {
|
||||
try {
|
||||
parsedData = typeof this.data[`col${i}`] === 'string' ? JSON.parse(this.data[`col${i}`]) : this.data[`col${i}`];
|
||||
} catch (e) {
|
||||
parsedData = { html: this.data[`col${i}`] }; // fallback
|
||||
}
|
||||
}
|
||||
const instance = new pluginClass({
|
||||
data: parsedData,
|
||||
api: this.api,
|
||||
readOnly: this.readOnly,
|
||||
config: pluginConfig,
|
||||
block: {
|
||||
id: 'nested-flex-' + i + '-' + Math.random().toString(36).substring(7),
|
||||
name: type,
|
||||
holder: typeContainer,
|
||||
isEmpty: false,
|
||||
selected: false,
|
||||
stretched: false,
|
||||
tunes: {}
|
||||
}
|
||||
});
|
||||
this.activeInstances[i] = instance;
|
||||
|
||||
const element = instance.render();
|
||||
typeContainer.appendChild(element);
|
||||
|
||||
// Fix for Editor.js Image plugin 404 errors (spinner hangs forever)
|
||||
if (type === 'image') {
|
||||
// Custom UI override for nested images
|
||||
const injectCustomButtons = () => {
|
||||
if (element.querySelector('.sis-custom-image-controls')) return;
|
||||
|
||||
const fileBtn = element.querySelector('.cdx-button');
|
||||
if (fileBtn) {
|
||||
fileBtn.style.display = 'none'; // Hide native button
|
||||
}
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light';
|
||||
controls.style.display = 'flex';
|
||||
controls.style.flexDirection = 'column';
|
||||
controls.style.gap = '8px';
|
||||
|
||||
controls.innerHTML = `
|
||||
<div class="text-muted small text-center mb-1"><i class="fas fa-cog"></i> Image Controls</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm w-100" id="btn-upload-img-${i}"><i class="fas fa-upload"></i> Upload Image</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm w-100" id="btn-fetch-url-${i}"><i class="fas fa-link"></i> Fetch URL</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm w-100" id="btn-media-lib-${i}"><i class="fas fa-images"></i> Media Library</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 mt-2" id="btn-clear-img-${i}"><i class="fas fa-trash"></i> Remove Image</button>
|
||||
`;
|
||||
|
||||
const uploadBtn = controls.querySelector(`#btn-upload-img-${i}`);
|
||||
if (uploadBtn && fileBtn) {
|
||||
uploadBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
fileBtn.click();
|
||||
});
|
||||
}
|
||||
|
||||
const urlBtn = controls.querySelector(`#btn-fetch-url-${i}`);
|
||||
if (urlBtn) {
|
||||
urlBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const url = prompt('Enter Image URL:');
|
||||
if (url && url.trim() !== '') {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url.trim() },
|
||||
caption: '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mediaBtn = controls.querySelector(`#btn-media-lib-${i}`);
|
||||
if (mediaBtn) {
|
||||
mediaBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (typeof openMediaPickerModal === 'function') {
|
||||
openMediaPickerModal((url) => {
|
||||
if (url) {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url },
|
||||
caption: '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Media Library is not yet configured on this system.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clearBtn = controls.querySelector(`#btn-clear-img-${i}`);
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm('Are you sure you want to remove this image?')) {
|
||||
this.data[`col${i}`] = '';
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
element.appendChild(controls);
|
||||
};
|
||||
injectCustomButtons();
|
||||
|
||||
const handleImageError = () => {
|
||||
element.classList.remove('image-tool--loading');
|
||||
element.classList.add('image-tool--empty');
|
||||
const preloader = element.querySelector('.image-tool__image-preloader');
|
||||
if (preloader) preloader.style.display = 'none';
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.tagName === 'IMG') {
|
||||
node.addEventListener('error', handleImageError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe(element, { childList: true, subtree: true });
|
||||
|
||||
const existingImg = element.querySelector('img');
|
||||
if (existingImg) existingImg.addEventListener('error', handleImageError);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to render plugin ${type} inside FlexTool:`, err);
|
||||
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
||||
@@ -553,7 +709,9 @@ class FlexTool {
|
||||
direction: this.data.direction,
|
||||
justify: this.data.justify,
|
||||
align: this.data.align,
|
||||
gap: this.data.gap
|
||||
gap: this.data.gap,
|
||||
globalClass: this.data.globalClass || '',
|
||||
globalId: this.data.globalId || ''
|
||||
};
|
||||
for (let i = 1; i <= this.data.cols; i++) {
|
||||
const type = this.data[`type${i}`] || 'html';
|
||||
|
||||
@@ -44,34 +44,60 @@ class GridTool {
|
||||
title.innerHTML = '<i class="fas fa-columns"></i> Multi-Column Grid Layout';
|
||||
this.wrapper.appendChild(title);
|
||||
|
||||
// Layout Columns Number Input
|
||||
const selectorGroup = document.createElement('div');
|
||||
selectorGroup.className = 'form-group mb-3';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
label.innerText = 'Number of Columns (1 - 12)';
|
||||
selectorGroup.appendChild(label);
|
||||
// Layout Settings Row
|
||||
const settingsRow = document.createElement('div');
|
||||
settingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.className = 'form-control form-control-sm';
|
||||
input.style.width = '100px';
|
||||
input.min = '1';
|
||||
input.max = '12';
|
||||
input.value = this.data.cols;
|
||||
if (this.readOnly) input.disabled = true;
|
||||
|
||||
input.addEventListener('input', (e) => {
|
||||
// 1. Columns
|
||||
const colDiv = document.createElement('div');
|
||||
colDiv.className = 'col-md-2 mb-2';
|
||||
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Columns</label>';
|
||||
const colInput = document.createElement('input');
|
||||
colInput.type = 'number';
|
||||
colInput.className = 'form-control form-control-sm';
|
||||
colInput.min = '1';
|
||||
colInput.max = '12';
|
||||
colInput.value = this.data.cols;
|
||||
if (this.readOnly) colInput.disabled = true;
|
||||
colInput.addEventListener('input', (e) => {
|
||||
let val = parseInt(e.target.value) || 2;
|
||||
if (val < 1) val = 1;
|
||||
if (val > 12) val = 12;
|
||||
this.data.cols = val;
|
||||
this._renderColumnInputs();
|
||||
});
|
||||
colDiv.appendChild(colInput);
|
||||
settingsRow.appendChild(colDiv);
|
||||
|
||||
selectorGroup.appendChild(input);
|
||||
this.wrapper.appendChild(selectorGroup);
|
||||
// 2. Global CSS Class
|
||||
const classDiv = document.createElement('div');
|
||||
classDiv.className = 'col-md-5 mb-2';
|
||||
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Grid CSS Class(es)</label>';
|
||||
const classInput = document.createElement('input');
|
||||
classInput.type = 'text';
|
||||
classInput.className = 'form-control form-control-sm';
|
||||
classInput.placeholder = 'e.g. my-custom-grid';
|
||||
classInput.value = this.data.globalClass || '';
|
||||
if (this.readOnly) classInput.disabled = true;
|
||||
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||
classDiv.appendChild(classInput);
|
||||
settingsRow.appendChild(classDiv);
|
||||
|
||||
// 3. Global HTML ID
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.className = 'col-md-5 mb-2';
|
||||
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Grid HTML ID</label>';
|
||||
const idInput = document.createElement('input');
|
||||
idInput.type = 'text';
|
||||
idInput.className = 'form-control form-control-sm';
|
||||
idInput.placeholder = 'e.g. grid-section-1';
|
||||
idInput.value = this.data.globalId || '';
|
||||
if (this.readOnly) idInput.disabled = true;
|
||||
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||
idDiv.appendChild(idInput);
|
||||
settingsRow.appendChild(idDiv);
|
||||
|
||||
this.wrapper.appendChild(settingsRow);
|
||||
|
||||
// Columns inputs container
|
||||
this.inputsContainer = document.createElement('div');
|
||||
@@ -160,8 +186,8 @@ class GridTool {
|
||||
// Dynamically add other registered plugins from window.SISEditorPlugins
|
||||
if (window.SISEditorPlugins) {
|
||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||
// Avoid self-nesting grid to prevent recursion issues
|
||||
if (key !== 'grid' && !types.some(t => t.value === key)) {
|
||||
// Allow nesting any registered plugins (no exclusions)
|
||||
if (!types.some(t => t.value === key)) {
|
||||
types.push({ value: key, label: `[Plugin] ${key}` });
|
||||
}
|
||||
});
|
||||
@@ -211,7 +237,9 @@ class GridTool {
|
||||
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
||||
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
||||
try {
|
||||
const pluginClass = window.SISEditorPlugins[type].class;
|
||||
const pluginEntry = window.SISEditorPlugins[type];
|
||||
const pluginClass = pluginEntry.class;
|
||||
const pluginConfig = pluginEntry.config || {};
|
||||
let parsedData = {};
|
||||
if (this.data[`col${i}`]) {
|
||||
try {
|
||||
@@ -223,12 +251,136 @@ class GridTool {
|
||||
const instance = new pluginClass({
|
||||
data: parsedData,
|
||||
api: this.api,
|
||||
readOnly: this.readOnly
|
||||
readOnly: this.readOnly,
|
||||
config: pluginConfig,
|
||||
block: {
|
||||
id: 'nested-grid-' + i + '-' + Math.random().toString(36).substring(7),
|
||||
name: type,
|
||||
holder: typeContainer,
|
||||
isEmpty: false,
|
||||
selected: false,
|
||||
stretched: false,
|
||||
tunes: {}
|
||||
}
|
||||
});
|
||||
this.activeInstances[i] = instance;
|
||||
|
||||
const element = instance.render();
|
||||
typeContainer.appendChild(element);
|
||||
|
||||
// Fix for Editor.js Image plugin 404 errors (spinner hangs forever)
|
||||
if (type === 'image') {
|
||||
// Custom UI override for nested images
|
||||
const injectCustomButtons = () => {
|
||||
if (element.querySelector('.sis-custom-image-controls')) return;
|
||||
|
||||
const fileBtn = element.querySelector('.cdx-button');
|
||||
if (fileBtn) {
|
||||
fileBtn.style.display = 'none'; // Hide native button
|
||||
}
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light';
|
||||
controls.style.display = 'flex';
|
||||
controls.style.flexDirection = 'column';
|
||||
controls.style.gap = '8px';
|
||||
|
||||
controls.innerHTML = `
|
||||
<div class="text-muted small text-center mb-1"><i class="fas fa-cog"></i> Image Controls</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm w-100" id="btn-upload-img-${i}"><i class="fas fa-upload"></i> Upload Image</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm w-100" id="btn-fetch-url-${i}"><i class="fas fa-link"></i> Fetch URL</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm w-100" id="btn-media-lib-${i}"><i class="fas fa-images"></i> Media Library</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 mt-2" id="btn-clear-img-${i}"><i class="fas fa-trash"></i> Remove Image</button>
|
||||
`;
|
||||
|
||||
const uploadBtn = controls.querySelector(`#btn-upload-img-${i}`);
|
||||
if (uploadBtn && fileBtn) {
|
||||
uploadBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
fileBtn.click();
|
||||
});
|
||||
}
|
||||
|
||||
const urlBtn = controls.querySelector(`#btn-fetch-url-${i}`);
|
||||
if (urlBtn) {
|
||||
urlBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const url = prompt('Enter Image URL:');
|
||||
if (url && url.trim() !== '') {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url.trim() },
|
||||
caption: '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mediaBtn = controls.querySelector(`#btn-media-lib-${i}`);
|
||||
if (mediaBtn) {
|
||||
mediaBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (typeof openMediaPickerModal === 'function') {
|
||||
openMediaPickerModal((url) => {
|
||||
if (url) {
|
||||
this.data[`col${i}`] = {
|
||||
file: { url: url },
|
||||
caption: '',
|
||||
withBorder: false,
|
||||
withBackground: false,
|
||||
stretched: false
|
||||
};
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Media Library is not yet configured on this system.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clearBtn = controls.querySelector(`#btn-clear-img-${i}`);
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm('Are you sure you want to remove this image?')) {
|
||||
this.data[`col${i}`] = '';
|
||||
this._renderTypeSpecificInput(i, container);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
element.appendChild(controls);
|
||||
};
|
||||
injectCustomButtons();
|
||||
|
||||
const handleImageError = () => {
|
||||
element.classList.remove('image-tool--loading');
|
||||
element.classList.add('image-tool--empty');
|
||||
const preloader = element.querySelector('.image-tool__image-preloader');
|
||||
if (preloader) preloader.style.display = 'none';
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.tagName === 'IMG') {
|
||||
node.addEventListener('error', handleImageError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
observer.observe(element, { childList: true, subtree: true });
|
||||
|
||||
const existingImg = element.querySelector('img');
|
||||
if (existingImg) existingImg.addEventListener('error', handleImageError);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to render plugin ${type} inside GridTool:`, err);
|
||||
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
||||
@@ -449,7 +601,9 @@ class GridTool {
|
||||
|
||||
save(blockContent) {
|
||||
const savedData = {
|
||||
cols: this.data.cols
|
||||
cols: this.data.cols,
|
||||
globalClass: this.data.globalClass || '',
|
||||
globalId: this.data.globalId || ''
|
||||
};
|
||||
for (let i = 1; i <= this.data.cols; i++) {
|
||||
const type = this.data[`type${i}`] || 'html';
|
||||
|
||||
@@ -317,6 +317,9 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="font-weight-bold mb-0"> <i class="fas fa-cubes"></i> Page Content (Block Editor) </label>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-success mr-1" id="exportJsonBtn">
|
||||
<i class="fas fa-file-export"></i> Export JSON
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info mr-1" id="importJsonBtn">
|
||||
<i class="fas fa-file-import"></i> Import JSON
|
||||
</button>
|
||||
@@ -509,6 +512,43 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Export JSON Logic
|
||||
var exportBtn = document.getElementById('exportJsonBtn');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', function () {
|
||||
if (window.sisEditor && typeof window.sisEditor.save === 'function') {
|
||||
window.sisEditor.save().then(function (outputData) {
|
||||
var jsonString = JSON.stringify(outputData, null, 2);
|
||||
var blob = new Blob([jsonString], { type: 'application/json' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
|
||||
// Get page title for filename if possible, default to editor_backup
|
||||
var titleInput = document.getElementById('title') || document.querySelector('input[name="title"]');
|
||||
var filename = 'editor_backup.json';
|
||||
if (titleInput && titleInput.value.trim()) {
|
||||
filename = titleInput.value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_') + '_backup.json';
|
||||
}
|
||||
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
setTimeout(function () {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}).catch(function (error) {
|
||||
console.error('[SIS Editor] Export failed:', error);
|
||||
alert('Failed to export editor data: ' + error.message);
|
||||
});
|
||||
} else {
|
||||
alert('Editor instance is not initialized or ready.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Import JSON Modal Logic
|
||||
var importBtn = document.getElementById('importJsonBtn');
|
||||
var importModal = $('#importJsonModal');
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
<!-- Add Meta Description for SEO -->
|
||||
<meta name="description" th:if="${page.metaDescription != null}" th:content="${page.metaDescription}" />
|
||||
|
||||
<!-- Load lightweight Bootstrap Grid CSS to support grid/flex block layouts without overriding theme styles -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap-grid.min.css" />
|
||||
|
||||
<!-- Customizer CSS overrides -->
|
||||
<style th:inline="css">
|
||||
body {
|
||||
@@ -345,50 +348,64 @@
|
||||
|
||||
<!-- 15. Grid / Columns Block -->
|
||||
<th:block th:if="${block['type'] == 'grid'}">
|
||||
<div class="container my-4">
|
||||
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
|
||||
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}">
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
|
||||
<div
|
||||
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i])} ? ${block.data['id' + i]} : null"
|
||||
th:class="${block.data['width' + i] != null and block.data['width' + i] > 0} ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (${block.data['cols'] == 1} ? 'col-12 mb-4' : (${block.data['cols'] == 2} ? 'col-md-6 col-12 mb-4' : (${block.data['cols'] == 3} ? 'col-lg-4 col-md-6 col-12 mb-4' : (${block.data['cols'] == 4} ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))"
|
||||
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i])} ? ${block.data['class' + i]} : ''"
|
||||
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i]) ? block.data['id' + i] : null}"
|
||||
th:class="${(block.data['width' + i] != null and block.data['width' + i] gt 0) ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i]) ? block.data['class' + i] : ''}"
|
||||
>
|
||||
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, data=${block.data})"></div>
|
||||
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- 16. Flex Layout Block -->
|
||||
<th:block th:if="${block['type'] == 'flex'}">
|
||||
<div class="container my-4">
|
||||
<div
|
||||
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
|
||||
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}">
|
||||
<div class="container py-4">
|
||||
<div
|
||||
style="display: flex; flex-wrap: wrap;"
|
||||
th:styleappend="'flex-direction: ' + ${block.data['direction']} + '; justify-content: ' + (${block.data['justify'] == 'start' ? 'flex-start' : (block.data['justify'] == 'end' ? 'flex-end' : (block.data['justify'] == 'between' ? 'space-between' : (block.data['justify'] == 'around' ? 'space-around' : (block.data['justify'] == 'evenly' ? 'space-evenly' : 'center'))))}) + '; align-items: ' + (${block.data['align'] == 'start' ? 'flex-start' : (block.data['align'] == 'end' ? 'flex-end' : block.data['align'])} ) + '; gap: ' + (${block.data['gap'] == '0' ? '0' : (block.data['gap'] == '1' ? '0.25rem' : (block.data['gap'] == '2' ? '0.5rem' : (block.data['gap'] == '3' ? '1rem' : (block.data['gap'] == '4' ? '1.5rem' : '2rem'))))}) + ';'"
|
||||
th:styleappend="${'flex-direction: ' + (block.data['direction'] != null ? block.data['direction'] : 'row') + '; justify-content: ' + (block.data['justify'] == 'start' ? 'flex-start' : (block.data['justify'] == 'end' ? 'flex-end' : (block.data['justify'] == 'between' ? 'space-between' : (block.data['justify'] == 'around' ? 'space-around' : (block.data['justify'] == 'evenly' ? 'space-evenly' : 'center'))))) + '; align-items: ' + (block.data['align'] == 'start' ? 'flex-start' : (block.data['align'] == 'end' ? 'flex-end' : (block.data['align'] != null ? block.data['align'] : 'stretch'))) + '; gap: ' + (block.data['gap'] == '0' ? '0' : (block.data['gap'] == '1' ? '0.25rem' : (block.data['gap'] == '2' ? '0.5rem' : (block.data['gap'] == '3' ? '1rem' : (block.data['gap'] == '4' ? '1.5rem' : (block.data['gap'] == '5' ? '2rem' : '1rem')))))) + ';'}"
|
||||
>
|
||||
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
|
||||
<div
|
||||
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i])} ? ${block.data['id' + i]} : null"
|
||||
th:class="${block.data['width' + i] != null and block.data['width' + i] > 0} ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (${block.data['cols'] == 1} ? 'col-12 mb-4' : (${block.data['cols'] == 2} ? 'col-md-6 col-12 mb-4' : (${block.data['cols'] == 3} ? 'col-lg-4 col-md-6 col-12 mb-4' : (${block.data['cols'] == 4} ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))"
|
||||
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i])} ? ${block.data['class' + i]} : ''"
|
||||
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i]) ? block.data['id' + i] : null}"
|
||||
th:class="${(block.data['width' + i] != null and block.data['width' + i] gt 0) ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i]) ? block.data['class' + i] : ''}"
|
||||
>
|
||||
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, data=${block.data})"></div>
|
||||
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- Shared Cell Content Renderer Fragment -->
|
||||
<th:block th:fragment="renderCell(type, i, data)">
|
||||
<th:block th:fragment="renderCell(type, i, blockData)">
|
||||
<!-- Type 1: HTML / Text -->
|
||||
<th:block th:if="${type == null or type == 'html'}" th:utext="${data['col' + i]}"></th:block>
|
||||
<th:block th:if="${type == null or type == 'html'}" th:utext="${blockData != null ? blockData['col' + i] : ''}"></th:block>
|
||||
|
||||
<!-- Type 2: Image -->
|
||||
<th:block th:if="${type == 'image'}">
|
||||
<div class="sis-grid-image-wrapper text-center">
|
||||
<img th:if="${data['imgUrl' + i] != null and !#strings.isEmpty(data['imgUrl' + i])}" th:src="${data['imgUrl' + i]}" class="img-fluid rounded shadow-sm" alt="Grid Image" />
|
||||
<!-- Native Image Plugin structure -->
|
||||
<th:block th:if="${blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['file'] != null}">
|
||||
<img th:src="${blockData['col' + i]['file']['url']}" class="img-fluid rounded shadow-sm" th:alt="${blockData['col' + i]['caption']}" />
|
||||
</th:block>
|
||||
<!-- Custom imgUrl fallback -->
|
||||
<th:block th:unless="${blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['file'] != null}">
|
||||
<img th:if="${blockData != null and blockData['imgUrl' + i] != null and !#strings.isEmpty(blockData['imgUrl' + i])}" th:src="${blockData['imgUrl' + i]}" class="img-fluid rounded shadow-sm" alt="Grid Image" />
|
||||
</th:block>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
@@ -397,7 +414,7 @@
|
||||
<div class="sis-youtube-container text-center">
|
||||
<div
|
||||
class="sis-yt-thumb-wrapper"
|
||||
th:data-embed-url="${data['ytUrl' + i]}"
|
||||
th:data-embed-url="${blockData != null ? blockData['ytUrl' + i] : ''}"
|
||||
style="
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
@@ -412,7 +429,7 @@
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
"
|
||||
th:styleappend="${(data['ytUrl' + i] != null and #strings.contains(data['ytUrl' + i], 'youtu.be/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringAfter(data['ytUrl' + i], 'youtu.be/') + '/hqdefault.jpg' + ');' : ((data['ytUrl' + i] != null and #strings.contains(data['ytUrl' + i], 'v=')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(data['ytUrl' + i], 'v='), '&') + '/hqdefault.jpg' + ');' : ((data['ytUrl' + i] != null and #strings.contains(data['ytUrl' + i], 'embed/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(data['ytUrl' + i], 'embed/'), '?') + '/hqdefault.jpg' + ');' : ''))}"
|
||||
th:styleappend="${blockData != null and blockData['ytUrl' + i] != null ? ((#strings.contains(blockData['ytUrl' + i], 'youtu.be/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringAfter(blockData['ytUrl' + i], 'youtu.be/') + '/hqdefault.jpg' + ');' : ((#strings.contains(blockData['ytUrl' + i], 'v=')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(blockData['ytUrl' + i], 'v='), '&') + '/hqdefault.jpg' + ');' : ((#strings.contains(blockData['ytUrl' + i], 'embed/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(blockData['ytUrl' + i], 'embed/'), '?') + '/hqdefault.jpg' + ');' : ''))) : ''}"
|
||||
>
|
||||
<div class="sis-yt-play-btn" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: transform 0.2s ease">
|
||||
<svg width="68" height="48" viewBox="0 0 68 48">
|
||||
@@ -428,24 +445,24 @@
|
||||
<th:block th:if="${type == 'accordion'}">
|
||||
<details class="sis-accordion-item mb-3">
|
||||
<summary class="sis-accordion-title font-weight-bold p-3 bg-light border rounded" style="cursor: pointer; user-select: none">
|
||||
<span th:utext="${data['accTitle' + i]}">Accordion Title</span>
|
||||
<span th:utext="${blockData != null ? blockData['accTitle' + i] : ''}">Accordion Title</span>
|
||||
</summary>
|
||||
<div class="sis-accordion-body p-3 border border-top-0 rounded-bottom" th:utext="${data['accContent' + i]}"></div>
|
||||
<div class="sis-accordion-body p-3 border border-top-0 rounded-bottom" th:utext="${blockData != null ? blockData['accContent' + i] : ''}"></div>
|
||||
</details>
|
||||
</th:block>
|
||||
|
||||
<!-- Nested Plugin: html-snippet -->
|
||||
<th:block th:if="${type == 'html-snippet' and data['col' + i] != null and data['col' + i]['id'] != null}">
|
||||
<div th:replace="~{fragments/snippets :: snippet(id=${data['col' + i]['id']})}"></div>
|
||||
<th:block th:if="${type == 'html-snippet' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['id'] != null}">
|
||||
<div th:replace="~{fragments/snippets :: snippet(id=${blockData['col' + i]['id']})}"></div>
|
||||
</th:block>
|
||||
|
||||
<!-- Nested Plugin: timeline -->
|
||||
<th:block th:if="${type == 'timeline' and data['col' + i] != null and data['col' + i]['items'] != null}">
|
||||
<th:block th:if="${type == 'timeline' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['items'] != null}">
|
||||
<section class="sis-timeline">
|
||||
<div class="sis-timeline__grid">
|
||||
<div class="sis-timeline__text-col" style="width: 100%;">
|
||||
<div class="sis-timeline__line"></div>
|
||||
<th:block th:each="item : ${data['col' + i]['items']}">
|
||||
<th:block th:each="item : ${blockData['col' + i]['items']}">
|
||||
<div class="sis-timeline__entry">
|
||||
<div class="sis-timeline__dot"></div>
|
||||
<div class="sis-timeline__entry-content">
|
||||
@@ -461,8 +478,50 @@
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<!-- Nested Plugin: grid (Recursive) -->
|
||||
<th:block th:if="${type == 'grid' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['cols'] != null}">
|
||||
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
|
||||
th:class="${blockData['col' + i]['globalClass'] != null and !#strings.isEmpty(blockData['col' + i]['globalClass']) ? blockData['col' + i]['globalClass'] : null}">
|
||||
<div class="container-fluid p-0">
|
||||
<div class="row">
|
||||
<th:block th:each="nestedI : ${#numbers.sequence(1, blockData['col' + i]['cols'])}">
|
||||
<div
|
||||
th:id="${blockData['col' + i]['id' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['id' + nestedI]) ? blockData['col' + i]['id' + nestedI] : null}"
|
||||
th:class="${(blockData['col' + i]['width' + nestedI] != null and blockData['col' + i]['width' + nestedI] gt 0) ? ('col-lg-' + blockData['col' + i]['width' + nestedI] + ' col-md-6 col-12 mb-4') : (blockData['col' + i]['cols'] == 1 ? 'col-12 mb-4' : (blockData['col' + i]['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||
th:classappend="${blockData['col' + i]['class' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['class' + nestedI]) ? blockData['col' + i]['class' + nestedI] : ''}"
|
||||
>
|
||||
<div th:replace=":: renderCell(type=${blockData['col' + i]['type' + nestedI]}, i=${nestedI}, blockData=${blockData['col' + i]})"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<!-- Nested Plugin: flex (Recursive) -->
|
||||
<th:block th:if="${type == 'flex' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['cols'] != null}">
|
||||
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
|
||||
th:class="${blockData['col' + i]['globalClass'] != null and !#strings.isEmpty(blockData['col' + i]['globalClass']) ? blockData['col' + i]['globalClass'] : null}">
|
||||
<div class="container-fluid p-0">
|
||||
<div
|
||||
style="display: flex; flex-wrap: wrap;"
|
||||
th:styleappend="${'flex-direction: ' + (blockData['col' + i]['direction'] != null ? blockData['col' + i]['direction'] : 'row') + '; justify-content: ' + (blockData['col' + i]['justify'] == 'start' ? 'flex-start' : (blockData['col' + i]['justify'] == 'end' ? 'flex-end' : (blockData['col' + i]['justify'] == 'between' ? 'space-between' : (blockData['col' + i]['justify'] == 'around' ? 'space-around' : (blockData['col' + i]['justify'] == 'evenly' ? 'space-evenly' : 'center'))))) + '; align-items: ' + (blockData['col' + i]['align'] == 'start' ? 'flex-start' : (blockData['col' + i]['align'] == 'end' ? 'flex-end' : (blockData['col' + i]['align'] != null ? blockData['col' + i]['align'] : 'stretch'))) + '; gap: ' + (blockData['col' + i]['gap'] == '0' ? '0' : (blockData['col' + i]['gap'] == '1' ? '0.25rem' : (blockData['col' + i]['gap'] == '2' ? '0.5rem' : (blockData['col' + i]['gap'] == '3' ? '1rem' : (blockData['col' + i]['gap'] == '4' ? '1.5rem' : (blockData['col' + i]['gap'] == '5' ? '2rem' : '1rem')))))) + ';'}"
|
||||
>
|
||||
<th:block th:each="nestedI : ${#numbers.sequence(1, blockData['col' + i]['cols'])}">
|
||||
<div
|
||||
th:id="${blockData['col' + i]['id' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['id' + nestedI]) ? blockData['col' + i]['id' + nestedI] : null}"
|
||||
th:class="${(blockData['col' + i]['width' + nestedI] != null and blockData['col' + i]['width' + nestedI] gt 0) ? ('col-lg-' + blockData['col' + i]['width' + nestedI] + ' col-md-6 col-12 mb-4') : (blockData['col' + i]['cols'] == 1 ? 'col-12 mb-4' : (blockData['col' + i]['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||
th:classappend="${blockData['col' + i]['class' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['class' + nestedI]) ? blockData['col' + i]['class' + nestedI] : ''}"
|
||||
>
|
||||
<div th:replace=":: renderCell(type=${blockData['col' + i]['type' + nestedI]}, i=${nestedI}, blockData=${blockData['col' + i]})"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</th:block>
|
||||
</th:block>
|
||||
<style>
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
|
||||
Reference in New Issue
Block a user