UNPKG

@mindfiredigital/page-builder

Version:
544 lines (539 loc) 21.3 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { TextComponent } from '../components/TextComponent.js'; import { HeaderComponent } from '../components/HeaderComponent.js'; import { TableComponent } from '../components/TableComponent.js'; import { Canvas } from '../canvas/Canvas.js'; import { ModalComponent } from '../components/ModalManager.js'; import { handleComponentClick } from './componentClickManager.js'; const PAGE_SIZES = { A4_P: { width: 794, height: 1123 }, A4_L: { width: 1123, height: 794 }, LETTER_P: { width: 816, height: 1056 }, }; const PAGE_SIZES_OPTIONS = [ { value: 'A4_P', label: 'A4 Portrait (794x1123 px)' }, { value: 'A4_L', label: 'A4 Landscape (1123x794 px)' }, { value: 'LETTER_P', label: 'Letter Portrait (816x1056 px)' }, { value: 'CUSTOM', label: 'Custom Size' }, ]; export class SidebarUtils { static createPageSizeSelect(container, canvasElement) { var _a, _b; const wrapper = document.createElement('div'); wrapper.classList.add('control-wrapper', 'vertical'); const label = document.createElement('label'); label.textContent = 'Page Size Preset'; const select = document.createElement('select'); select.id = 'page-size-select'; select.classList.add('form-input'); const currentMaxWidth = canvasElement.style.maxWidth.match(/\d+/) ? parseInt( ((_a = canvasElement.style.maxWidth.match(/\d+/)) === null || _a === void 0 ? void 0 : _a[0]) || '0' ) : canvasElement.offsetWidth; const currentMinHeight = canvasElement.style.minHeight.match(/\d+/) ? parseInt( ((_b = canvasElement.style.minHeight.match(/\d+/)) === null || _b === void 0 ? void 0 : _b[0]) || '0' ) : 0; let defaultValue = 'CUSTOM'; PAGE_SIZES_OPTIONS.forEach(option => { const opt = document.createElement('option'); opt.value = option.value; opt.textContent = option.label; select.appendChild(opt); if (option.value !== 'CUSTOM') { const size = PAGE_SIZES[option.value]; if ( size && Math.abs(size.width - currentMaxWidth) < 5 && Math.abs(size.height - currentMinHeight) < 5 ) { defaultValue = option.value; } } }); select.value = defaultValue; wrapper.appendChild(label); wrapper.appendChild(select); container.appendChild(wrapper); } static createAttributeControls( attribute, functionsPanel, handleInputTrigger ) { const box = document.createElement('div'); box.className = 'attribute-input-container'; let inputHtml = ''; switch (attribute.input_type) { case 'checkbox': const isChecked = attribute.default_value === 'true'; inputHtml = ` <div class="attribute-input-wrapper checkbox-wrapper"> <input type="checkbox" class="attribute-input" id="${attribute.key}" ${!attribute.editable ? 'disabled' : ''} ${isChecked ? 'checked' : ''} > </div> `; break; case 'number': inputHtml = ` <div class="attribute-input-wrapper"> <input type="number" class="attribute-input" id="${attribute.key}" ${!attribute.editable ? 'disabled readonly' : ''} value="${attribute.default_value || ''}" placeholder="Enter ${attribute.title.toLowerCase()}..." > </div> `; break; case 'text': default: inputHtml = ` <div class="attribute-input-wrapper"> <input type="text" class="attribute-input" id="${attribute.key}" ${!attribute.editable ? 'disabled readonly' : ''} value="${attribute.default_value || ''}" placeholder="Enter ${attribute.title.toLowerCase()}..." > </div> `; break; } box.innerHTML = ` <div class="attribute-header"> <label for="${attribute.key}" class="attribute-label">${attribute.title}</label> ${!attribute.editable ? '<span class="readonly-badge">Read Only</span>' : ''} </div> ${inputHtml} `; functionsPanel.appendChild(box); const inputElement = document.getElementById(attribute.key); if (attribute.editable !== false) { const eventConfigurator = document.createElement('div'); eventConfigurator.className = 'event-configurator'; eventConfigurator.innerHTML = ` <div class="event-trigger-section"> <div class="trigger-header"> <label class="trigger-label">Trigger Event:</label> </div> <div class="trigger-select-wrapper"> <select class="event-selector" id="event-selector-${attribute.key}"> <option value="input">On Input (Real-time)</option> <option value="change">On Change</option> <option value="blur">On Focus Lost</option> <option value="keyup">On Key Release</option> <option value="click">On Click</option> </select> <div class="select-arrow">▼</div> </div> </div> `; box.appendChild(eventConfigurator); const eventSelector = document.getElementById( `event-selector-${attribute.key}` ); const setupListener = eventToListen => { const eventTypes = ['input', 'change', 'blur', 'keyup', 'click']; eventTypes.forEach(eventType => { inputElement.removeEventListener(eventType, handleInputTrigger); }); inputElement.addEventListener(eventToListen, handleInputTrigger); box.setAttribute('data-trigger', eventToListen); }; eventSelector.addEventListener('change', () => { var _a; const selectedEvent = eventSelector.value; setupListener(selectedEvent); (_a = eventSelector.parentElement) === null || _a === void 0 ? void 0 : _a.classList.add('trigger-changed'); setTimeout(() => { var _a; (_a = eventSelector.parentElement) === null || _a === void 0 ? void 0 : _a.classList.remove('trigger-changed'); }, 300); }); const defaultTrigger = 'input'; eventSelector.value = defaultTrigger; setupListener(defaultTrigger); inputElement.addEventListener('focus', () => { box.classList.add('input-focused'); }); inputElement.addEventListener('blur', () => { box.classList.remove('input-focused'); }); } } static populateModalButton(component, functionsPanel, editable) { if (editable === false) return; const componentType = component.classList[0].replace('-component', ''); // For table cells, the attribute is stored on the .table-cell parent, // not on the .table-cell-content element that was clicked. const isTableCell = component.classList.contains('table-cell-content'); const attributeTarget = isTableCell ? component.closest('.table-cell') : component; // ── SET ATTRIBUTE BUTTON ───────────────────────────────────────────────── const modalButton = document.createElement('button'); modalButton.textContent = `Set ${componentType} Attribute`; modalButton.className = 'set-attribute-button'; functionsPanel.appendChild(modalButton); // ── DELETE ATTRIBUTE BUTTON ────────────────────────────────────────────── const deleteAttributeButton = document.createElement('button'); deleteAttributeButton.textContent = `Delete ${componentType} Attribute`; deleteAttributeButton.className = 'delete-attribute-button'; functionsPanel.appendChild(deleteAttributeButton); // Only show the delete button when an attribute is already bound. // Re-checked after "Set" so the button appears as soon as one is set. const refreshDeleteVisibility = () => { deleteAttributeButton.style.display = attributeTarget && attributeTarget.hasAttribute('data-attribute-key') ? 'block' : 'none'; }; refreshDeleteVisibility(); deleteAttributeButton.addEventListener('click', () => { if (!attributeTarget) return; // Remove the binding attributes from the correct target element attributeTarget.removeAttribute('data-attribute-key'); attributeTarget.removeAttribute('data-attribute-type'); if (isTableCell) { // For table cells: reset the text inside .table-cell-content // and clear inline styles set by updateCellContent const textContentOfCell = attributeTarget.querySelector( '.table-cell-content' ); if (textContentOfCell) { textContentOfCell.textContent = ''; } attributeTarget.style.color = ''; attributeTarget.style.fontSize = ''; attributeTarget.style.fontWeight = ''; } else if (component.classList.contains('header-component')) { // For headers: reset .component-text-content and clear Formula styles const textContent = component.querySelector('.component-text-content'); if (textContent) { textContent.textContent = 'Header'; } component.style.color = ''; component.style.fontWeight = ''; } else if (component.classList.contains('text-component')) { // For text: reset .component-text-content and clear Formula styles const textContent = component.querySelector('.component-text-content'); if (textContent) { textContent.textContent = 'Text'; } component.style.color = ''; component.style.fontSize = ''; component.style.fontWeight = ''; } // Persist the deletion and hide the button since nothing is bound anymore Canvas.dispatchDesignChange(); Canvas.historyManager.captureState(); refreshDeleteVisibility(); }); // ──────────────────────────────────────────────────────────────────────── modalButton.addEventListener('click', () => __awaiter(this, void 0, void 0, function* () { const modalComponent = new ModalComponent(); if (component.classList.contains('text-component')) { const textComponentInstance = new TextComponent(); yield handleComponentClick( modalComponent, TextComponent.textAttributeConfig, component, textComponentInstance.updateTextContent ); } else if (component.classList.contains('header-component')) { const headerComponentInstance = new HeaderComponent(); yield handleComponentClick( modalComponent, HeaderComponent.headerAttributeConfig, component, headerComponentInstance.updateHeaderContent ); } else if (isTableCell) { const tableComponentInstance = new TableComponent(); const cell = component.closest('.table-cell'); yield handleComponentClick( modalComponent, TableComponent.tableAttributeConfig, cell, tableComponentInstance.updateCellContent ); } // After setting, re-check whether delete button should now be visible refreshDeleteVisibility(); }) ); } static rgbToHex(rgb) { const result = rgb.match( /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.?\d*))?\)$/ ); if (!result) return rgb; const r = parseInt(result[1], 10); const g = parseInt(result[2], 10); const b = parseInt(result[3], 10); return `#${((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1).toUpperCase()}`; } static createControl( label, id, type, value, controlsContainer, attributes = {} ) { const wrapper = document.createElement('div'); wrapper.classList.add('control-wrapper'); const isNumber = type === 'number'; if (isNumber && attributes.unit) { const unit = attributes.unit; wrapper.innerHTML = ` <label for="${id}">${label}:</label> <div class="input-wrapper"> <input type="${type}" id="${id}" value="${value}"> <select id="${id}-unit"> <option value="px" ${unit === 'px' ? 'selected' : ''}>px</option> <option value="rem" ${unit === 'rem' ? 'selected' : ''}>rem</option> <option value="vh" ${unit === 'vh' ? 'selected' : ''}>vh</option> <option value="%" ${unit === '%' ? 'selected' : ''}>%</option> </select> </div> `; } else if (type === 'color') { wrapper.innerHTML = ` <label for="${id}">${label}:</label> <div class="input-wrapper"> <input type="color" id="${id}" value="${value}"> <input type="text" id="${id}-value" style="font-size: 0.8rem; width: 200px; margin-left: 8px;" value="${value}"> </div> `; } else { wrapper.innerHTML = ` <label for="${id}">${label}:</label> <div class="input-wrapper"> <input type="${type}" id="${id}" value="${value}"> </div> `; } const input = wrapper.querySelector('input'); const unitSelect = wrapper.querySelector(`#${id}-unit`); if (input) { Object.keys(attributes).forEach(key => { input.setAttribute(key, attributes[key].toString()); }); } const colorInput = wrapper.querySelector(`input[type="color"]#${id}`); const hexInput = wrapper.querySelector(`#${id}-value`); if (colorInput) { colorInput.addEventListener('input', () => { if (hexInput) { hexInput.value = colorInput.value; } }); } if (hexInput) { hexInput.addEventListener('input', () => { if (colorInput) { colorInput.value = hexInput.value; } }); } controlsContainer.appendChild(wrapper); if (unitSelect) { unitSelect.addEventListener('change', () => { const unit = unitSelect.value; const currentValue = parseInt(input.value); input.value = `${currentValue}${unit}`; }); } } static createSelectControl( label, id, currentValue, options, controlsContainer ) { const wrapper = document.createElement('div'); wrapper.classList.add('control-wrapper'); const selectOptions = options .map( option => `<option value="${option}" ${option === currentValue ? 'selected' : ''}>${option}</option>` ) .join(''); wrapper.innerHTML = ` <label for="${id}">${label}:</label> <div class="input-wrapper"> <select id="${id}">${selectOptions}</select> </div> `; controlsContainer.appendChild(wrapper); } static populateRowVisibilityControls(row, inputs) { const functionsPanel = document.getElementById('functions-panel'); const addIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M5 12h14M12 5v14"/></svg>`; const deleteIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/></svg>`; functionsPanel.innerHTML = ` <div id="visibility-rules-panel" class="rules-panel"> <h4 class="panel-title">Row Visibility Rules</h4> <div id="rules-list" class="rules-list"></div> <div class="rule-builder-form"> <h5 class="rule-builder-form-title">Add New Rule</h5> <select id="rule-input-key-select" class="form-row select"></select> <div class="form-row"> <select id="rule-operator-select"> <option value="equals">Equals</option> <option value="not_equals">Not Equals</option> <option value="greater_than">Greater Than</option> <option value="less_than">Less Than</option> <option value="contains">Contains</option> </select> <input type="text" id="rule-value-input" placeholder="Enter value"> </div> <div class="form-row"> <select id="rule-action-select"> <option value="show">Show Row</option> <option value="hide">Hide Row</option> </select> <button id="add-rule-btn" class="add-rule-btn"> ${addIcon} <span>Add Rule</span> </button> </div> </div> </div> `; const inputKeySelect = document.getElementById('rule-input-key-select'); if (inputKeySelect) { if (inputs) { inputs.forEach(attr => { if (attr.type === 'Input') { const option = document.createElement('option'); option.value = attr.key; option.textContent = attr.title; inputKeySelect.appendChild(option); } }); } } const rulesList = document.getElementById('rules-list'); const addRuleBtn = document.getElementById('add-rule-btn'); const ruleValueInput = document.getElementById('rule-value-input'); const ruleOperatorSelect = document.getElementById('rule-operator-select'); const ruleActionSelect = document.getElementById('rule-action-select'); const renderRules = () => { rulesList.innerHTML = ''; const rules = JSON.parse( row.getAttribute('data-visibility-rules') || '[]' ); rules.forEach((rule, index) => { const ruleItem = document.createElement('div'); ruleItem.className = 'rule-item'; ruleItem.innerHTML = ` <span class="rule-item-text"> If <strong class="text-blue-600">${rule.inputKey}</strong> ${rule.operator} '<strong class="text-green-600">${rule.value}</strong>', then <strong class="text-purple-600">${rule.action}</strong> </span> <button class="delete-rule-btn"> ${deleteIcon} </button> `; const deleteButton = ruleItem.querySelector('.delete-rule-btn'); deleteButton.addEventListener('click', () => { this.deleteRule(row, index); renderRules(); Canvas.dispatchDesignChange(); }); rulesList.appendChild(ruleItem); }); }; addRuleBtn.addEventListener('click', () => { const newRule = { inputKey: inputKeySelect.value, operator: ruleOperatorSelect.value, value: ruleValueInput.value, action: ruleActionSelect.value, }; this.addRule(row, newRule); renderRules(); Canvas.dispatchDesignChange(); }); renderRules(); } static addRule(row, rule) { try { const existingRules = JSON.parse( row.getAttribute('data-visibility-rules') || '[]' ); existingRules.push(rule); row.setAttribute('data-visibility-rules', JSON.stringify(existingRules)); } catch (e) { console.error('Failed to add rule:', e); } } static deleteRule(row, index) { try { const existingRules = JSON.parse( row.getAttribute('data-visibility-rules') || '[]' ); existingRules.splice(index, 1); row.setAttribute('data-visibility-rules', JSON.stringify(existingRules)); } catch (e) { console.error('Failed to delete rule:', e); } } }