@mindfiredigital/page-builder
Version:
544 lines (539 loc) • 21.3 kB
JavaScript
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);
}
}
}