Files

807 lines
37 KiB
Plaintext

/**
* Grid block tool for Editor.js.
* Allows users to create responsive layouts with any number of columns and choose different content types, manual widths,
* and dynamically instantiates other registered editor plugins inside columns.
*/
class GridTool {
static get toolbox() {
return {
title: 'Grid / Columns',
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="9"></rect><rect x="14" y="15" width="7" height="6"></rect><rect x="3" y="15" width="7" height="6"></rect></svg>'
};
}
constructor({ data, api, readOnly }) {
this.api = api;
this.readOnly = readOnly;
this.data = {
cols: parseInt(data.cols) || 2,
globalClass: data.globalClass || '',
globalId: data.globalId || '',
globalAttributes: data.globalAttributes || data.attributes || ''
};
this.activeInstances = {};
// Load column contents dynamically
for (let i = 1; i <= 12; i++) {
this.data[`type${i}`] = data[`type${i}`] || 'html';
this.data[`col${i}`] = data[`col${i}`] || '';
this.data[`imgUrl${i}`] = data[`imgUrl${i}`] || '';
this.data[`ytUrl${i}`] = data[`ytUrl${i}`] || '';
this.data[`accTitle${i}`] = data[`accTitle${i}`] || '';
this.data[`accContent${i}`] = data[`accContent${i}`] || '';
this.data[`class${i}`] = data[`class${i}`] || '';
this.data[`id${i}`] = data[`id${i}`] || '';
this.data[`attr${i}`] = data[`attr${i}`] || data[`attributes${i}`] || '';
this.data[`width${i}`] = parseInt(data[`width${i}`]) || 0; // 0 means auto
this.data[`caption${i}`] = data[`caption${i}`] || '';
// Sub-plugin properties
this.data[`subType${i}`] = data[`subType${i}`] || 'none';
this.data[`colsub_${i}`] = data[`colsub_${i}`] || '';
this.data[`imgUrlsub_${i}`] = data[`imgUrlsub_${i}`] || '';
this.data[`ytUrlsub_${i}`] = data[`ytUrlsub_${i}`] || '';
this.data[`accTitlesub_${i}`] = data[`accTitlesub_${i}`] || '';
this.data[`accContentsub_${i}`] = data[`accContentsub_${i}`] || '';
this.data[`classsub_${i}`] = data[`classsub_${i}`] || '';
this.data[`idsub_${i}`] = data[`idsub_${i}`] || '';
this.data[`attrsub_${i}`] = data[`attrsub_${i}`] || '';
}
this.wrapper = undefined;
}
render() {
this.wrapper = document.createElement('div');
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-grid-tool-wrapper';
this.wrapper.style.fontFamily = 'inherit';
const title = document.createElement('div');
title.className = 'font-weight-bold text-primary small mb-3';
title.innerHTML = '<i class="fas fa-columns"></i> Multi-Column Grid Layout';
this.wrapper.appendChild(title);
// Layout Settings Row
const settingsRow = document.createElement('div');
settingsRow.className = 'form-row mb-3 pb-2 border-bottom';
// 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);
// 2. Global CSS Class
const classDiv = document.createElement('div');
classDiv.className = 'col-md-3 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());
this.globalClassInput = classInput;
classDiv.appendChild(classInput);
settingsRow.appendChild(classDiv);
// 3. Global HTML ID
const idDiv = document.createElement('div');
idDiv.className = 'col-md-3 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());
this.globalIdInput = idInput;
idDiv.appendChild(idInput);
settingsRow.appendChild(idDiv);
// 4. Global Custom Attributes
const attrDiv = document.createElement('div');
attrDiv.className = 'col-md-4 mb-2';
attrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</label>';
const attrInput = document.createElement('input');
attrInput.type = 'text';
attrInput.className = 'form-control form-control-sm';
attrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"';
attrInput.value = this.data.globalAttributes || '';
if (this.readOnly) attrInput.disabled = true;
attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim());
this.globalAttrInput = attrInput;
attrDiv.appendChild(attrInput);
settingsRow.appendChild(attrDiv);
this.wrapper.appendChild(settingsRow);
// Columns inputs container
this.inputsContainer = document.createElement('div');
this.wrapper.appendChild(this.inputsContainer);
this._renderColumnInputs();
return this.wrapper;
}
_renderColumnInputs() {
this.inputsContainer.innerHTML = '';
const row = document.createElement('div');
row.className = 'row d-flex flex-wrap';
const count = this.data.cols;
const colWidthClass = count === 1 ? 'col-12' : (count === 2 ? 'col-md-6' : (count === 3 ? 'col-md-4' : (count === 4 ? 'col-md-3' : 'col-md')));
for (let i = 1; i <= count; i++) {
const colDiv = document.createElement('div');
colDiv.className = `${colWidthClass} mb-3 p-2 border rounded bg-white`;
colDiv.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
const headerRow = document.createElement('div');
headerRow.className = 'd-flex justify-content-between align-items-center mb-2 pb-1 border-bottom';
const label = document.createElement('label');
label.className = 'small font-weight-bold text-dark mb-0';
label.innerText = `Col ${i}`;
headerRow.appendChild(label);
const controlsWrapper = document.createElement('div');
controlsWrapper.className = 'd-flex align-items-center gap-1';
// Width Selector
const widthSelect = document.createElement('select');
widthSelect.className = 'custom-select custom-select-sm mr-1';
widthSelect.style.width = '75px';
if (this.readOnly) widthSelect.disabled = true;
const widthOpts = [
{ val: 0, lbl: 'Auto' },
{ val: 1, lbl: '1/12' },
{ val: 2, lbl: '2/12' },
{ val: 3, lbl: '3/12' },
{ val: 4, lbl: '4/12' },
{ val: 5, lbl: '5/12' },
{ val: 6, lbl: '6/12' },
{ val: 7, lbl: '7/12' },
{ val: 8, lbl: '8/12' },
{ val: 9, lbl: '9/12' },
{ val: 10, lbl: '10/12' },
{ val: 11, lbl: '11/12' },
{ val: 12, lbl: '12/12' }
];
widthOpts.forEach(o => {
const opt = document.createElement('option');
opt.value = o.val;
opt.innerText = o.lbl;
opt.selected = this.data[`width${i}`] === o.val;
widthSelect.appendChild(opt);
});
widthSelect.addEventListener('change', (e) => {
this.data[`width${i}`] = parseInt(e.target.value) || 0;
});
controlsWrapper.appendChild(widthSelect);
// Column Type Select (Built-ins + Registered plugins dynamically)
const typeSelect = document.createElement('select');
typeSelect.className = 'custom-select custom-select-sm';
typeSelect.style.width = '100px';
if (this.readOnly) typeSelect.disabled = true;
// Base types
const types = [
{ value: 'html', label: 'HTML/Text' },
{ value: 'image', label: 'Image' },
{ value: 'youtube', label: 'YouTube' },
{ value: 'accordion', label: 'Accordion' }
];
// Dynamically add other registered plugins from window.SISEditorPlugins
if (window.SISEditorPlugins) {
Object.keys(window.SISEditorPlugins).forEach(key => {
// Allow nesting any registered plugins (no exclusions)
if (!types.some(t => t.value === key)) {
types.push({ value: key, label: `[Plugin] ${key}` });
}
});
}
types.forEach(t => {
const opt = document.createElement('option');
opt.value = t.value;
opt.innerText = t.label;
opt.selected = this.data[`type${i}`] === t.value;
typeSelect.appendChild(opt);
});
const contentDiv = document.createElement('div');
typeSelect.addEventListener('change', (e) => {
this.data[`type${i}`] = e.target.value;
this._renderTypeSpecificInput(i, contentDiv);
});
controlsWrapper.appendChild(typeSelect);
headerRow.appendChild(controlsWrapper);
colDiv.appendChild(headerRow);
colDiv.appendChild(contentDiv);
// --- Sub-Plugin (Sub-Content) Section ---
const subWrapper = document.createElement('div');
if (this.data[`subType${i}`] !== 'none') {
subWrapper.className = 'mt-3 pt-2 border-top';
}
const subHeader = document.createElement('div');
subHeader.className = 'd-flex justify-content-between align-items-center mb-2';
const subLabel = document.createElement('label');
subLabel.className = 'small font-weight-bold text-muted mb-0';
subLabel.style.fontSize = '10px';
subLabel.innerText = 'SUB-CONTENT / INJECT PLUGIN';
subHeader.appendChild(subLabel);
const subTypeSelect = document.createElement('select');
subTypeSelect.className = 'custom-select custom-select-sm';
subTypeSelect.style.width = '130px';
subTypeSelect.style.fontSize = '10px';
if (this.readOnly) subTypeSelect.disabled = true;
const subTypes = [
{ value: 'none', label: 'None' },
{ value: 'caption', label: 'Text Caption' },
{ value: 'html', label: 'HTML/Text' },
{ value: 'image', label: 'Image' },
{ value: 'youtube', label: 'YouTube' },
{ value: 'accordion', label: 'Accordion' }
];
if (window.SISEditorPlugins) {
Object.keys(window.SISEditorPlugins).forEach(key => {
if (!subTypes.some(t => t.value === key)) {
subTypes.push({ value: key, label: `[Plugin] ${key}` });
}
});
}
subTypes.forEach(t => {
const opt = document.createElement('option');
opt.value = t.value;
opt.innerText = t.label;
opt.selected = this.data[`subType${i}`] === t.value;
subTypeSelect.appendChild(opt);
});
subHeader.appendChild(subTypeSelect);
subWrapper.appendChild(subHeader);
const subContentDiv = document.createElement('div');
subWrapper.appendChild(subContentDiv);
colDiv.appendChild(subWrapper);
const renderSubInputs = () => {
subContentDiv.innerHTML = '';
const subType = this.data[`subType${i}`];
if (subType === 'none') {
subWrapper.className = '';
} else {
subWrapper.className = 'mt-3 pt-2 border-top';
}
if (subType === 'none') {
// Empty
} else if (subType === 'caption') {
const captionGroup = document.createElement('div');
captionGroup.className = 'form-group mb-2';
const captionInput = document.createElement('input');
captionInput.type = 'text';
captionInput.className = 'form-control form-control-sm';
captionInput.style.fontSize = '11px';
captionInput.placeholder = 'Optional column caption / text...';
captionInput.value = this.data[`caption${i}`] || '';
if (this.readOnly) captionInput.disabled = true;
captionInput.addEventListener('input', (e) => {
this.data[`caption${i}`] = e.target.value;
});
captionGroup.appendChild(captionInput);
subContentDiv.appendChild(captionGroup);
} else {
this._renderTypeSpecificInput(`sub_${i}`, subContentDiv, subType);
}
};
subTypeSelect.addEventListener('change', (e) => {
this.data[`subType${i}`] = e.target.value;
renderSubInputs();
});
row.appendChild(colDiv);
this._renderTypeSpecificInput(i, contentDiv);
renderSubInputs();
}
this.inputsContainer.appendChild(row);
}
_renderTypeSpecificInput(i, container, explicitType) {
container.innerHTML = '';
// Remove tracking of previous instance
if (this.activeInstances[i]) {
delete this.activeInstances[i];
}
const typeContainer = document.createElement('div');
typeContainer.className = 'mb-3';
container.appendChild(typeContainer);
const type = explicitType || this.data[`type${i}`] || 'html';
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
try {
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-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 (window.SISMediaPicker) {
SISMediaPicker.open((url, mediaObj, config) => {
if (url) {
this.data[`col${i}`] = {
file: { url: url },
caption: (config && config.alt) || '',
withBorder: false,
withBackground: false,
stretched: false
};
if (config) {
this.data[`imgStyle${i}`] = config.style || '';
this.data[`imgClass${i}`] = config.cssClass || '';
this.data[`imgAlt${i}`] = config.alt || '';
this.data[`imgAttrs${i}`] = config.customAttributes || '';
this.data[`imgAspectRatio${i}`] = config.aspectRatio || '';
}
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>`;
}
} else if (type === 'html') {
const textarea = document.createElement('textarea');
textarea.className = 'form-control form-control-sm';
textarea.style.minHeight = '140px';
textarea.style.fontSize = '12px';
textarea.placeholder = `HTML / Text for Column ${i}...`;
textarea.value = typeof this.data[`col${i}`] === 'object' ? JSON.stringify(this.data[`col${i}`]) : this.data[`col${i}`] || '';
if (this.readOnly) textarea.disabled = true;
textarea.addEventListener('input', (e) => {
this.data[`col${i}`] = e.target.value;
});
typeContainer.appendChild(textarea);
} else if (type === 'image') {
const inputGroup = document.createElement('div');
inputGroup.className = 'input-group input-group-sm mb-2';
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control form-control-sm';
input.placeholder = 'Paste image URL here...';
input.value = this.data[`imgUrl${i}`] || '';
if (this.readOnly) input.disabled = true;
const appendDiv = document.createElement('div');
appendDiv.className = 'input-group-append';
const chooseBtn = document.createElement('button');
chooseBtn.type = 'button';
chooseBtn.className = 'btn btn-outline-secondary';
chooseBtn.innerText = 'Choose';
if (this.readOnly) chooseBtn.disabled = true;
chooseBtn.addEventListener('click', () => {
if (window.SISMediaPicker) {
SISMediaPicker.open((selectedUrl, mediaObj, config) => {
input.value = selectedUrl;
this.data[`imgUrl${i}`] = selectedUrl;
if (config) {
this.data[`imgStyle${i}`] = config.style || '';
this.data[`imgClass${i}`] = config.cssClass || '';
this.data[`imgAlt${i}`] = config.alt || '';
this.data[`imgAttrs${i}`] = config.customAttributes || '';
this.data[`imgAspectRatio${i}`] = config.aspectRatio || '';
}
previewImg.src = selectedUrl;
previewBox.style.display = 'block';
});
} else {
alert('Media Picker modal helper is not loaded.');
}
});
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn btn-outline-primary';
uploadBtn.innerText = 'Upload';
if (this.readOnly) uploadBtn.disabled = true;
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
uploadBtn.disabled = true;
uploadBtn.innerText = '...';
try {
const response = await fetch('/api/manage/media/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.file && result.file.url) {
const url = result.file.url;
input.value = url;
this.data[`imgUrl${i}`] = url;
previewImg.src = url;
previewBox.style.display = 'block';
} else {
alert('Upload failed: ' + (result.message || 'Unknown error'));
}
} catch (err) {
console.error(err);
alert('Upload error occurred.');
} finally {
uploadBtn.disabled = false;
uploadBtn.innerText = 'Upload';
}
});
appendDiv.appendChild(chooseBtn);
appendDiv.appendChild(uploadBtn);
inputGroup.appendChild(input);
inputGroup.appendChild(appendDiv);
typeContainer.appendChild(inputGroup);
typeContainer.appendChild(fileInput);
const previewBox = document.createElement('div');
previewBox.className = 'border rounded p-1 text-center bg-light';
previewBox.style.display = this.data[`imgUrl${i}`] ? 'block' : 'none';
previewBox.style.maxHeight = '150px';
previewBox.style.overflow = 'hidden';
const previewImg = document.createElement('img');
previewImg.src = this.data[`imgUrl${i}`] || '';
previewImg.style.maxWidth = '100%';
previewImg.style.maxHeight = '140px';
previewImg.style.objectFit = 'contain';
previewBox.appendChild(previewImg);
typeContainer.appendChild(previewBox);
input.addEventListener('input', (e) => {
const val = e.target.value.trim();
this.data[`imgUrl${i}`] = val;
if (val) {
previewImg.src = val;
previewBox.style.display = 'block';
} else {
previewBox.style.display = 'none';
}
});
} else if (type === 'youtube') {
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control form-control-sm mb-2';
input.placeholder = 'Paste YouTube video URL...';
input.value = this.data[`ytUrl${i}`] || '';
if (this.readOnly) input.disabled = true;
const preview = document.createElement('div');
preview.className = 'small text-muted p-2 bg-light border rounded';
preview.innerText = 'Format: https://www.youtube.com/watch?v=XXXXXX';
input.addEventListener('input', (e) => {
this.data[`ytUrl${i}`] = e.target.value.trim();
});
typeContainer.appendChild(input);
typeContainer.appendChild(preview);
} else if (type === 'accordion') {
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.className = 'form-control form-control-sm mb-2';
titleInput.placeholder = 'Accordion Title...';
titleInput.value = this.data[`accTitle${i}`] || '';
if (this.readOnly) titleInput.disabled = true;
const contentTextarea = document.createElement('textarea');
contentTextarea.className = 'form-control form-control-sm';
contentTextarea.style.minHeight = '90px';
contentTextarea.style.fontSize = '12px';
contentTextarea.placeholder = 'Accordion Body HTML...';
contentTextarea.value = this.data[`accContent${i}`] || '';
if (this.readOnly) contentTextarea.disabled = true;
titleInput.addEventListener('input', (e) => {
this.data[`accTitle${i}`] = e.target.value;
});
contentTextarea.addEventListener('input', (e) => {
this.data[`accContent${i}`] = e.target.value;
});
typeContainer.appendChild(titleInput);
typeContainer.appendChild(contentTextarea);
}
// Advanced Settings (ID and Class)
const advancedHeader = document.createElement('div');
advancedHeader.className = 'font-weight-bold text-muted small mb-2';
advancedHeader.style.fontSize = '10px';
advancedHeader.innerText = 'ADVANCED SETTINGS';
container.appendChild(advancedHeader);
const advRow = document.createElement('div');
advRow.className = 'form-row';
const classCol = document.createElement('div');
classCol.className = 'col';
const classInput = document.createElement('input');
classInput.type = 'text';
classInput.className = 'form-control form-control-sm';
classInput.style.fontSize = '11px';
classInput.placeholder = 'CSS Class(es)...';
classInput.value = this.data[`class${i}`] || '';
if (this.readOnly) classInput.disabled = true;
classInput.addEventListener('input', (e) => {
this.data[`class${i}`] = e.target.value.trim();
});
classCol.appendChild(classInput);
const idCol = document.createElement('div');
idCol.className = 'col';
const idInput = document.createElement('input');
idInput.type = 'text';
idInput.className = 'form-control form-control-sm';
idInput.style.fontSize = '11px';
idInput.placeholder = 'HTML ID...';
idInput.value = this.data[`id${i}`] || '';
if (this.readOnly) idInput.disabled = true;
idInput.addEventListener('input', (e) => {
this.data[`id${i}`] = e.target.value.trim();
});
idCol.appendChild(idInput);
advRow.appendChild(classCol);
advRow.appendChild(idCol);
container.appendChild(advRow);
}
save(blockContent) {
const savedData = {
cols: this.data.cols,
globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''),
globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''),
globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || '')
};
for (let i = 1; i <= this.data.cols; i++) {
const type = this.data[`type${i}`] || 'html';
savedData[`type${i}`] = type;
// If it is a dynamically nested plugin, save its output data
if (this.activeInstances[i]) {
try {
const pluginData = this.activeInstances[i].save();
savedData[`col${i}`] = pluginData;
} catch (e) {
console.error(`Failed to save plugin ${type} inside GridTool:`, e);
savedData[`col${i}`] = this.data[`col${i}`];
}
} else {
savedData[`col${i}`] = this.data[`col${i}`] || '';
}
savedData[`imgUrl${i}`] = this.data[`imgUrl${i}`] || '';
savedData[`ytUrl${i}`] = this.data[`ytUrl${i}`] || '';
savedData[`accTitle${i}`] = this.data[`accTitle${i}`] || '';
savedData[`accContent${i}`] = this.data[`accContent${i}`] || '';
savedData[`class${i}`] = this.data[`class${i}`] || '';
savedData[`id${i}`] = this.data[`id${i}`] || '';
savedData[`attr${i}`] = this.data[`attr${i}`] || '';
savedData[`width${i}`] = this.data[`width${i}`] || 0;
savedData[`caption${i}`] = this.data[`caption${i}`] || '';
// Sub-plugin serialization
const subType = this.data[`subType${i}`] || 'none';
savedData[`subType${i}`] = subType;
if (this.activeInstances[`sub_${i}`]) {
try {
const subPluginData = this.activeInstances[`sub_${i}`].save();
savedData[`colsub_${i}`] = subPluginData;
} catch (e) {
console.error(`Failed to save sub-plugin ${subType} inside GridTool:`, e);
savedData[`colsub_${i}`] = this.data[`colsub_${i}`];
}
} else {
savedData[`colsub_${i}`] = this.data[`colsub_${i}`] || '';
}
savedData[`imgUrlsub_${i}`] = this.data[`imgUrlsub_${i}`] || '';
savedData[`ytUrlsub_${i}`] = this.data[`ytUrlsub_${i}`] || '';
savedData[`accTitlesub_${i}`] = this.data[`accTitlesub_${i}`] || '';
savedData[`accContentsub_${i}`] = this.data[`accContentsub_${i}`] || '';
savedData[`classsub_${i}`] = this.data[`classsub_${i}`] || '';
savedData[`idsub_${i}`] = this.data[`idsub_${i}`] || '';
savedData[`attrsub_${i}`] = this.data[`attrsub_${i}`] || '';
}
return savedData;
}
}
// Register the plugin globally
window.SISEditorPlugins = window.SISEditorPlugins || {};
window.SISEditorPlugins['grid'] = {
class: GridTool
};