/** * 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: '' }; } 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 = ' 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 = ''; 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 = ''; 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 = ''; 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 = ''; 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 = `