904 lines
41 KiB
Plaintext
904 lines
41 KiB
Plaintext
/**
|
|
* Flex block tool for Editor.js.
|
|
* Allows users to create highly custom Flexbox layouts with alignment, direction, gaps, and custom column items.
|
|
* Supports instantiating other registered plugins inside columns dynamically.
|
|
*/
|
|
class FlexTool {
|
|
static get toolbox() {
|
|
return {
|
|
title: 'Flex Layout',
|
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line><line x1="15" y1="3" x2="15" y2="21"></line></svg>'
|
|
};
|
|
}
|
|
|
|
constructor({ data, api, readOnly }) {
|
|
this.api = api;
|
|
this.readOnly = readOnly;
|
|
this.data = {
|
|
cols: parseInt(data.cols) || 2,
|
|
direction: data.direction || 'row',
|
|
justify: data.justify || 'start',
|
|
align: data.align || 'stretch',
|
|
gap: data.gap || '3', // standard Bootstrap gap level
|
|
globalClass: data.globalClass || '',
|
|
globalId: data.globalId || '',
|
|
globalAttributes: data.globalAttributes || data.attributes || ''
|
|
};
|
|
this.activeInstances = {};
|
|
|
|
// Load items content 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}`] || '';
|
|
}
|
|
this.wrapper = undefined;
|
|
}
|
|
|
|
render() {
|
|
this.wrapper = document.createElement('div');
|
|
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-flex-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-boxes"></i> Custom Flexbox Layout Builder';
|
|
this.wrapper.appendChild(title);
|
|
|
|
// Container Flex Settings Row
|
|
const settingsRow = document.createElement('div');
|
|
settingsRow.className = 'form-row mb-3 pb-3 border-bottom';
|
|
|
|
// 1. Direction
|
|
const dirCol = document.createElement('div');
|
|
dirCol.className = 'col-md-3 col-sm-6 mb-2';
|
|
dirCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Direction</label>';
|
|
const dirSelect = document.createElement('select');
|
|
dirSelect.className = 'custom-select custom-select-sm';
|
|
const dirOpts = [
|
|
{ val: 'row', lbl: 'Row' },
|
|
{ val: 'row-reverse', lbl: 'Row Reverse' },
|
|
{ val: 'column', lbl: 'Column' },
|
|
{ val: 'column-reverse', lbl: 'Column Reverse' }
|
|
];
|
|
dirOpts.forEach(o => {
|
|
const opt = document.createElement('option');
|
|
opt.value = o.val;
|
|
opt.innerText = o.lbl;
|
|
opt.selected = this.data.direction === o.val;
|
|
dirSelect.appendChild(opt);
|
|
});
|
|
dirSelect.addEventListener('change', (e) => this.data.direction = e.target.value);
|
|
dirCol.appendChild(dirSelect);
|
|
settingsRow.appendChild(dirCol);
|
|
|
|
// 2. Justify Content
|
|
const justifyCol = document.createElement('div');
|
|
justifyCol.className = 'col-md-3 col-sm-6 mb-2';
|
|
justifyCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Justify Content</label>';
|
|
const justifySelect = document.createElement('select');
|
|
justifySelect.className = 'custom-select custom-select-sm';
|
|
const justifyOpts = [
|
|
{ val: 'start', lbl: 'Start' },
|
|
{ val: 'center', lbl: 'Center' },
|
|
{ val: 'end', lbl: 'End' },
|
|
{ val: 'between', lbl: 'Space Between' },
|
|
{ val: 'around', lbl: 'Space Around' },
|
|
{ val: 'evenly', lbl: 'Space Evenly' }
|
|
];
|
|
justifyOpts.forEach(o => {
|
|
const opt = document.createElement('option');
|
|
opt.value = o.val;
|
|
opt.innerText = o.lbl;
|
|
opt.selected = this.data.justify === o.val;
|
|
justifySelect.appendChild(opt);
|
|
});
|
|
justifySelect.addEventListener('change', (e) => this.data.justify = e.target.value);
|
|
justifyCol.appendChild(justifySelect);
|
|
settingsRow.appendChild(justifyCol);
|
|
|
|
// 3. Align Items
|
|
const alignCol = document.createElement('div');
|
|
alignCol.className = 'col-md-3 col-sm-6 mb-2';
|
|
alignCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Align Items</label>';
|
|
const alignSelect = document.createElement('select');
|
|
alignSelect.className = 'custom-select custom-select-sm';
|
|
const alignOpts = [
|
|
{ val: 'stretch', lbl: 'Stretch' },
|
|
{ val: 'start', lbl: 'Start' },
|
|
{ val: 'center', lbl: 'Center' },
|
|
{ val: 'end', lbl: 'End' }
|
|
];
|
|
alignOpts.forEach(o => {
|
|
const opt = document.createElement('option');
|
|
opt.value = o.val;
|
|
opt.innerText = o.lbl;
|
|
opt.selected = this.data.align === o.val;
|
|
alignSelect.appendChild(opt);
|
|
});
|
|
alignSelect.addEventListener('change', (e) => this.data.align = e.target.value);
|
|
alignCol.appendChild(alignSelect);
|
|
settingsRow.appendChild(alignCol);
|
|
|
|
// 4. Gap Level
|
|
const gapCol = document.createElement('div');
|
|
gapCol.className = 'col-md-3 col-sm-6 mb-2';
|
|
gapCol.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Gap Level</label>';
|
|
const gapSelect = document.createElement('select');
|
|
gapSelect.className = 'custom-select custom-select-sm';
|
|
const gapOpts = [
|
|
{ val: '0', lbl: 'None' },
|
|
{ val: '1', lbl: 'Extra Small' },
|
|
{ val: '2', lbl: 'Small' },
|
|
{ val: '3', lbl: 'Medium' },
|
|
{ val: '4', lbl: 'Large' },
|
|
{ val: '5', lbl: 'Extra Large' }
|
|
];
|
|
gapOpts.forEach(o => {
|
|
const opt = document.createElement('option');
|
|
opt.value = o.val;
|
|
opt.innerText = o.lbl;
|
|
opt.selected = this.data.gap === o.val;
|
|
gapSelect.appendChild(opt);
|
|
});
|
|
gapSelect.addEventListener('change', (e) => this.data.gap = e.target.value);
|
|
gapCol.appendChild(gapSelect);
|
|
settingsRow.appendChild(gapCol);
|
|
|
|
this.wrapper.appendChild(settingsRow);
|
|
|
|
// 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.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;
|
|
if (val > 12) val = 12;
|
|
this.data.cols = val;
|
|
this._renderColumnInputs();
|
|
});
|
|
colDiv.appendChild(input);
|
|
bgSettingsRow.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">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-flex-row';
|
|
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);
|
|
bgSettingsRow.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">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());
|
|
this.globalIdInput = idInput;
|
|
idDiv.appendChild(idInput);
|
|
bgSettingsRow.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);
|
|
bgSettingsRow.appendChild(attrDiv);
|
|
|
|
this.wrapper.appendChild(bgSettingsRow);
|
|
|
|
// 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';
|
|
|
|
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 = `Item ${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;
|
|
|
|
const types = [
|
|
{ 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 => {
|
|
// 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-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 (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 FlexTool:`, 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 Item ${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,
|
|
direction: this.data.direction,
|
|
justify: this.data.justify,
|
|
align: this.data.align,
|
|
gap: this.data.gap,
|
|
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 FlexTool:`, 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 FlexTool:`, 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['flex'] = {
|
|
class: FlexTool
|
|
};
|