UNPKG

@mindfiredigital/page-builder

Version:
798 lines (797 loc) 30.8 kB
var _a; import { DragDropManager } from './DragDropManager.js'; import { DeleteElementHandler } from './DeleteElement.js'; import { LandingPageTemplate } from './../templates/LandingPageTemplate.js'; import { ButtonComponent, HeaderComponent, ImageComponent, VideoComponent, TextComponent, ContainerComponent, TwoColumnContainer, ThreeColumnContainer, TableComponent, LinkComponent, } from '../components/index.js'; import { HistoryManager } from '../services/HistoryManager.js'; import { JSONStorage } from '../services/JSONStorage.js'; import { ComponentControlsManager } from './ComponentControls.js'; import { CustomizationSidebar } from '../sidebar/CustomizationSidebar.js'; import { MultiColumnContainer } from '../services/MultiColumnContainer.js'; import { GridManager } from './GridManager.js'; export class Canvas { static getComponents() { return _a.components; } static setComponents(components) { _a.components = components; } static init(initialData = null, editable, basicComponentsConfig, layouMode) { this.editable = editable; this.layoutMode = layouMode; const tableComponent = basicComponentsConfig.find( component => component.name === 'table' ); this.tableAttributeConfig = tableComponent === null || tableComponent === void 0 ? void 0 : tableComponent.attributes; const textComponent = basicComponentsConfig.find( component => component.name === 'text' ); this.textAttributeConfig = textComponent === null || textComponent === void 0 ? void 0 : textComponent.attributes; const headerComponent = basicComponentsConfig.find( component => component.name === 'header' ); this.headerAttributeConfig = headerComponent === null || headerComponent === void 0 ? void 0 : headerComponent.attributes; const ImageComponent = basicComponentsConfig.find( component => component.name === 'image' ); this.ImageAttributeConfig = ImageComponent === null || ImageComponent === void 0 ? void 0 : ImageComponent.globalExecuteFunction; if ( tableComponent && tableComponent.attributes && tableComponent.attributes.length > 0 ) { } _a.canvasElement = document.getElementById('canvas'); if (_a.canvasElement) { if (this.layoutMode === 'absolute') { _a.canvasElement.classList.add('preview-printable'); } else { _a.canvasElement.classList.remove('preview-printable'); } } _a.sidebarElement = document.getElementById('sidebar'); window.addEventListener('table-design-change', () => { _a.dispatchDesignChange(); }); _a.canvasElement.addEventListener('drop', _a.onDrop.bind(_a)); _a.canvasElement.addEventListener('dragover', event => event.preventDefault() ); _a.canvasElement.addEventListener('click', event => { const selected = document.querySelector('.editable-component.selected'); if (selected) { selected.classList.remove('selected'); } const target = event.target; if (target !== _a.canvasElement) { _a.deleteElementHandler.selectElement(target); } }); _a.canvasElement.classList.add('preview-desktop'); _a.canvasElement.addEventListener('click', event => { const component = event.target; if (component) { CustomizationSidebar.showSidebar(component.id); } }); if (layouMode == 'grid') { _a.canvasElement.classList.add('grid-layout-active'); } _a.canvasElement.style.position = 'relative'; this.lastCanvasWidth = _a.canvasElement.offsetWidth; _a.historyManager = new HistoryManager(_a.canvasElement); _a.jsonStorage = new JSONStorage(); _a.controlsManager = new ComponentControlsManager(_a); _a.gridManager = new GridManager(); _a.gridManager.initializeDropPreview(_a.canvasElement); const dragDropManager = new DragDropManager( _a.canvasElement, _a.sidebarElement ); dragDropManager.enable(); if (initialData) { _a.restoreState(initialData); } else { const savedState = _a.jsonStorage.load(); if (savedState) { _a.restoreState(savedState); } } } /** * Dispatches a custom event indicating that the canvas design has changed. * The event detail contains the current design state. */ static dispatchDesignChange() { if (_a.canvasElement && this.editable !== false) { const currentDesign = _a.getState(); const event = new CustomEvent('design-change', { detail: currentDesign, bubbles: true, composed: true, }); _a.canvasElement.dispatchEvent(event); _a.jsonStorage.save(currentDesign); } } static clearCanvas() { _a.canvasElement.innerHTML = ''; _a.components = []; _a.historyManager.captureState(); _a.gridManager.initializeDropPreview(_a.canvasElement); _a.gridManager.initializeDropPreview(_a.canvasElement); _a.dispatchDesignChange(); } static getState() { const canvasElement = _a.canvasElement; const computedStyles = window.getComputedStyle(canvasElement); const canvasStyles = {}; [ 'background-color', 'min-height', 'padding', 'margin', 'height', 'width', ].forEach(prop => { const value = computedStyles.getPropertyValue(prop); if ( value && value !== 'initial' && value !== 'auto' && value !== 'none' ) { canvasStyles[prop] = value; } }); const canvasState = { id: 'canvas', type: 'canvas', content: '', position: { x: 0, y: 0 }, dimensions: { width: canvasElement.offsetWidth, height: canvasElement.offsetHeight, }, style: canvasStyles, inlineStyle: canvasElement.getAttribute('style') || '', classes: Array.from(canvasElement.classList), dataAttributes: {}, props: {}, }; const componentStates = _a.components.map(component => { const baseType = component.classList[0] .split(/\d/)[0] .replace('-component', ''); const imageElement = component.querySelector('img'); const imageSrc = imageElement ? imageElement.src : null; const videoElement = component.querySelector('video'); const videoSrc = videoElement ? videoElement.src : null; const computedStyles = window.getComputedStyle(component); const styles = {}; for (let i = 0; i < computedStyles.length; i++) { const prop = computedStyles[i]; const value = computedStyles.getPropertyValue(prop); // Exclude values that are not useful for static HTML if ( value && value !== 'initial' && value !== 'auto' && value !== 'none' && value !== '' ) { styles[prop] = value; } } const dataAttributes = {}; Array.from(component.attributes) .filter(attr => attr.name.startsWith('data-')) .forEach(attr => { dataAttributes[attr.name] = attr.value; }); let componentProps = {}; if (component.classList.contains('custom-component')) { const propsJson = component.getAttribute('data-component-props'); if (propsJson) { try { componentProps = JSON.parse(propsJson); } catch (e) { console.error('Error parsing data-component-props:', e); } } } return { id: component.id, type: baseType, content: component.innerHTML, position: { x: component.offsetLeft, y: component.offsetTop, }, dimensions: { width: component.offsetWidth, height: component.offsetHeight, }, style: styles, inlineStyle: component.getAttribute('style') || '', classes: Array.from(component.classList), dataAttributes: dataAttributes, imageSrc: imageSrc, videoSrc: videoSrc, props: componentProps, }; }); return [canvasState, ...componentStates]; } static restoreState(state) { const canvasDataIndex = state.findIndex( data => data.id === 'canvas' && data.type === 'canvas' ); if (canvasDataIndex !== -1) { const canvasData = state[canvasDataIndex]; const canvasElement = _a.canvasElement; if (canvasData.inlineStyle) { canvasElement.setAttribute('style', canvasData.inlineStyle); } canvasElement.className = ''; canvasData.classes.forEach(cls => { canvasElement.classList.add(cls); }); state.splice(canvasDataIndex, 1); } _a.canvasElement.innerHTML = ''; _a.components = []; state.forEach(componentData => { const customSettings = componentData.dataAttributes['data-custom-settings'] || null; const component = _a.createComponent( componentData.type, customSettings, componentData.content ); if (component) { if (!componentData.classes.includes('custom-component')) { component.innerHTML = componentData.content; } const deleteButton = component.querySelector('.component-controls'); if (deleteButton && this.editable === false) { deleteButton.remove(); } component.className = ''; componentData.classes.forEach(cls => { component.classList.add(cls); }); if (component.classList.contains('selected')) { component.classList.remove('selected'); } if (this.editable === false) { if (component.classList.contains('component-resizer')) { component.classList.remove('component-resizer'); } } if (componentData.type === 'video' && componentData.videoSrc) { const videoElement = component.querySelector('video'); const uploadText = component.querySelector('.upload-text'); videoElement.src = componentData.videoSrc; videoElement.style.display = 'block'; uploadText.style.display = 'none'; } // Restore inline styles if (componentData.inlineStyle) { component.setAttribute('style', componentData.inlineStyle); } // Restore computed styles if (componentData.computedStyle) { Object.keys(componentData.computedStyle).forEach(prop => { component.style.setProperty( prop, componentData.computedStyle[prop] ); }); } // Restore data attributes if (componentData.dataAttributes) { Object.entries(componentData.dataAttributes).forEach( ([key, value]) => { component.setAttribute(key, value); } ); } if (this.editable !== false) { // Add control buttons and listeners _a.controlsManager.addControlButtons(component); _a.addDraggableListeners(component); } // Component-specific restoration if (component.classList.contains('container-component')) { ContainerComponent.restoreContainer(component, this.editable); } // column-specific restoration if ( component.classList.contains('twoCol-component') || component.classList.contains('threeCol-component') ) { MultiColumnContainer.restoreColumn(component); } if (componentData.type === 'image') { ImageComponent.restoreImageUpload( component, componentData.imageSrc, this.editable ); } if (componentData.type === 'table') { TableComponent.restore(component, this.editable); } if (componentData.type === 'link') { LinkComponent.restore(component); } if (componentData.type === 'header') { HeaderComponent.restore(component); } if (componentData.type === 'text') { TextComponent.restore(component); } _a.canvasElement.appendChild(component); _a.components.push(component); } }); _a.gridManager.initializeDropPreview(_a.canvasElement); } static onDrop(event) { var _b, _c; event.preventDefault(); const target = event.target; // Check if target is a container or child of a container if ( target.classList.contains('container-component') || target.closest('.container-component') ) { return; } const componentType = (_b = event.dataTransfer) === null || _b === void 0 ? void 0 : _b.getData('component-type'); let customSettings = (_c = event.dataTransfer) === null || _c === void 0 ? void 0 : _c.getData('custom-settings'); if (!componentType) { return; } if (!customSettings || customSettings.trim() === '') { const draggableElement = document.querySelector( `[data-component="${componentType}"]` ); if (draggableElement) { if (window.customComponents && window.customComponents[componentType]) { const componentConfig = window.customComponents[componentType]; if (componentConfig.settings) { customSettings = JSON.stringify(componentConfig.settings); } } } } const { gridX, gridY } = this.gridManager.mousePositionAtGridCorner( event, _a.canvasElement ); if ( _a.layoutMode === 'absolute' && _a.canvasElement.classList.contains('preview-printable') ) { const style = window.getComputedStyle(_a.canvasElement); // Get the padding values (which define the margin area in CSS) const paddingTop = parseFloat(style.paddingTop); // CSS padding: 30px const paddingRight = parseFloat(style.paddingRight); // CSS padding: 30px const paddingLeft = parseFloat(style.paddingLeft); // CSS padding: 30px // Estimate minimum space needed for the new component const MIN_COMPONENT_WIDTH = 100; // Calculate the inner content boundaries (where dropping is allowed) const innerContentRightX = _a.canvasElement.offsetWidth - paddingRight - MIN_COMPONENT_WIDTH; // Check if the drop position (gridX, gridY) is in the restricted margin area if ( gridX < paddingLeft || // Too far left (in left margin) gridY < paddingTop || // Too far up (in top margin) gridX > innerContentRightX ) { // Drop is outside the editable area. Prevent component creation. console.warn('Component dropped into margin area. Drop prevented.'); // Display a quick visual feedback (optional) _a.canvasElement.classList.add('container-highlight'); setTimeout(() => { _a.canvasElement.classList.remove('container-highlight'); }, 300); return; } } const component = _a.createComponent(componentType, customSettings); if (component && this.editable !== false) { const uniqueClass = _a.generateUniqueClass(componentType); component.id = uniqueClass; component.classList.add(uniqueClass); if (_a.layoutMode === 'absolute') { component.style.position = 'absolute'; if ( componentType === 'container' || componentType === 'twoCol' || componentType === 'threeCol' ) { component.style.top = `${event.offsetY}px`; } else { component.style.position = 'absolute'; component.style.left = `${gridX}px`; component.style.top = `${gridY}px`; } _a.addDraggableListeners(component); } else if (_a.layoutMode === 'grid') { component.style.position = ''; if (component.hasAttribute('draggable')) { component.removeAttribute('draggable'); component.style.cursor = 'default'; } } _a.components.push(component); _a.canvasElement.appendChild(component); _a.historyManager.captureState(); } _a.dispatchDesignChange(); } static reorderComponent(fromIndex, toIndex) { if ( fromIndex < 0 || toIndex < 0 || fromIndex >= this.components.length || toIndex >= this.components.length ) { console.error('Invalid indices for reordering'); return; } const [movedComponent] = this.components.splice(fromIndex, 1); this.components.splice(toIndex, 0, movedComponent); const canvasContainer = document.getElementById('canvas-container'); if (canvasContainer) { canvasContainer.innerHTML = ''; this.components.forEach(component => { canvasContainer.appendChild(component); }); } this.historyManager.captureState(); _a.dispatchDesignChange(); } static createComponent(type, customSettings = null, props) { let element = null; const componentFactoryFunction = _a.componentFactory[type]; if (componentFactoryFunction) { element = componentFactoryFunction(); } else { const tagNameElement = document.querySelector( `[data-component='${type}']` ); const tagName = tagNameElement === null || tagNameElement === void 0 ? void 0 : tagNameElement.getAttribute('data-tag-name'); if (tagName) { element = document.createElement(tagName); element.classList.add(`${type}-component`, 'custom-component'); element.classList.add(`${type}-component`, 'custom-component'); element.setAttribute('data-component-type', type); } else { return null; } } if (element && this.editable !== false) { const resizeObserver = new ResizeObserver(entries => { // Add resize constraints for printable mode if ( _a.layoutMode === 'absolute' && _a.canvasElement.classList.contains('preview-printable') ) { const style = window.getComputedStyle(_a.canvasElement); const paddingLeft = parseFloat(style.paddingLeft); const paddingRight = parseFloat(style.paddingRight); const paddingTop = parseFloat(style.paddingTop); const elementLeft = parseFloat(element.style.left) || 0; const elementTop = parseFloat(element.style.top) || 0; const elementWidth = element.offsetWidth; const maxCanvasWidth = _a.canvasElement.offsetWidth; // Constrain width to not exceed right padding if (elementLeft + elementWidth > maxCanvasWidth - paddingRight) { const maxAllowedWidth = maxCanvasWidth - paddingLeft - paddingRight - elementLeft; element.style.width = `${Math.max(50, maxAllowedWidth)}px`; } // Constrain position if resized beyond left padding if (elementLeft < paddingLeft) { element.style.left = `${paddingLeft}px`; } // Constrain top position if (elementTop < paddingTop) { element.style.top = `${paddingTop}px`; } } _a.dispatchDesignChange(); }); resizeObserver.observe(element); element.classList.add('editable-component'); if (type != 'container') { if (_a.layoutMode !== 'grid') { element.classList.add('component-resizer'); } } if (type === 'image') { element.setAttribute('contenteditable', 'false'); } else { if (type !== 'header' && type !== 'text' && type !== 'table') { element.setAttribute('contenteditable', 'true'); } element.addEventListener('input', () => { _a.historyManager.captureState(); this.dispatchDesignChange(); }); } _a.controlsManager.addControlButtons(element); } if (element) { const uniqueClass = _a.generateUniqueClass(type); element.setAttribute('id', uniqueClass); const label = document.createElement('span'); label.className = 'component-label'; label.setAttribute('contenteditable', 'false'); label.textContent = uniqueClass; element.appendChild(label); } return element; } static generateUniqueClass( type, isContainerComponent = false, containerClass = null ) { if (isContainerComponent && containerClass) { let containerElement = _a.components.find(component => component.classList.contains(containerClass) ); if (!containerElement) { containerElement = document.querySelector(`.${containerClass}`); if (!containerElement) { return `${containerClass}-${type}1`; } } const containerComponents = Array.from(containerElement.children); const typePattern = new RegExp(`${containerClass}-${type}(\\d+)`); let maxNumber = 0; containerComponents.forEach(component => { component.classList.forEach(className => { const match = className.match(typePattern); if (match) { const number = parseInt(match[1]); maxNumber = Math.max(maxNumber, number); } }); }); return `${containerClass}-${type}${maxNumber + 1}`; } else { const typePattern = new RegExp(`${type}(\\d+)`); let maxNumber = 0; _a.components.forEach(component => { component.classList.forEach(className => { const match = className.match(typePattern); if (match) { const number = parseInt(match[1]); maxNumber = Math.max(maxNumber, number); } }); }); return `${type}${maxNumber + 1}`; } } // static addDraggableListeners(element: HTMLElement) { // element.setAttribute('draggable', 'true'); // element.style.cursor = 'grab'; // let dragStartX = 0; // let dragStartY = 0; // let elementStartX = 0; // let elementStartY = 0; // let canvasScrollStartX = 0; // let canvasScrollStartY = 0; // element.addEventListener('dragstart', (event: DragEvent) => { // event.stopPropagation(); // if (event.dataTransfer) { // // Store exact mouse position at drag start // dragStartX = event.clientX; // dragStartY = event.clientY; // // Store canvas scroll position at drag start // canvasScrollStartX = Canvas.canvasElement.scrollLeft; // canvasScrollStartY = Canvas.canvasElement.scrollTop; // // Get current element position relative to canvas // elementStartX = parseFloat(element.style.left) || 0; // elementStartY = parseFloat(element.style.top) || 0; // event.dataTransfer.effectAllowed = 'move'; // element.style.cursor = 'grabbing'; // } // }); // element.addEventListener('dragend', (event: DragEvent) => { // event.preventDefault(); // event.stopPropagation(); // // Get current canvas scroll position // const canvasScrollCurrentX = Canvas.canvasElement.scrollLeft; // const canvasScrollCurrentY = Canvas.canvasElement.scrollTop; // // Calculate scroll delta (how much the canvas scrolled during drag) // const scrollDeltaX = canvasScrollCurrentX - canvasScrollStartX; // const scrollDeltaY = canvasScrollCurrentY - canvasScrollStartY; // // Calculate mouse movement delta // const mouseDeltaX = event.clientX - dragStartX; // const mouseDeltaY = event.clientY - dragStartY; // // Calculate new position accounting for both mouse movement and scroll changes // let newX = elementStartX + mouseDeltaX + scrollDeltaX; // let newY = elementStartY + mouseDeltaY + scrollDeltaY; // // Alternative approach: Use the actual mouse position relative to canvas // // This is more accurate when dealing with scrolling // const canvasRect = Canvas.canvasElement.getBoundingClientRect(); // const actualMouseX = // event.clientX - canvasRect.left + Canvas.canvasElement.scrollLeft; // const actualMouseY = // event.clientY - canvasRect.top + Canvas.canvasElement.scrollTop; // // Calculate the offset between drag start mouse position and element position // const canvasRectStart = Canvas.canvasElement.getBoundingClientRect(); // const dragStartMouseX = // dragStartX - canvasRectStart.left + canvasScrollStartX; // const dragStartMouseY = // dragStartY - canvasRectStart.top + canvasScrollStartY; // const offsetX = elementStartX - dragStartMouseX; // const offsetY = elementStartY - dragStartMouseY; // // Use actual mouse position for more precise positioning // newX = actualMouseX + offsetX; // newY = actualMouseY + offsetY; // // Constrain within canvas boundaries (accounting for scroll area) // const elementRect = element.getBoundingClientRect(); // const maxX = Canvas.canvasElement.scrollWidth - elementRect.width; // const maxY = Canvas.canvasElement.scrollHeight - elementRect.height; // newX = Math.max(0, Math.min(newX, maxX)); // newY = Math.max(0, Math.min(newY, maxY)); // // Set new position // element.style.left = `${newX}px`; // element.style.top = `${newY}px`; // // Reset cursor // element.style.cursor = 'grab'; // // Capture the state after dragging // Canvas.historyManager.captureState(); // Canvas.dispatchDesignChange(); // }); // } static addDraggableListeners(element) { element.setAttribute('draggable', 'true'); element.style.cursor = 'grab'; let dragStartX = 0; let dragStartY = 0; let elementStartX = parseFloat(element.style.left) || 0; let elementStartY = parseFloat(element.style.top) || 0; let canvasScrollStartX = 0; let canvasScrollStartY = 0; element.addEventListener('dragstart', event => { event.stopPropagation(); if (event.dataTransfer) { // Store exact mouse position at drag start dragStartX = event.clientX; dragStartY = event.clientY; // Store canvas scroll position at drag start canvasScrollStartX = _a.canvasElement.scrollLeft; canvasScrollStartY = _a.canvasElement.scrollTop; // Re-read initial element position elementStartX = parseFloat(element.style.left) || 0; elementStartY = parseFloat(element.style.top) || 0; event.dataTransfer.effectAllowed = 'move'; element.style.cursor = 'grabbing'; } }); element.addEventListener('dragend', event => { event.preventDefault(); event.stopPropagation(); // Calculate the offset between drag start mouse position and element position const canvasRectStart = _a.canvasElement.getBoundingClientRect(); const dragStartMouseX = dragStartX - canvasRectStart.left + canvasScrollStartX; const dragStartMouseY = dragStartY - canvasRectStart.top + canvasScrollStartY; const offsetX = elementStartX - dragStartMouseX; const offsetY = elementStartY - dragStartMouseY; // Calculate actual mouse position relative to canvas const canvasRect = _a.canvasElement.getBoundingClientRect(); const actualMouseX = event.clientX - canvasRect.left + _a.canvasElement.scrollLeft; const actualMouseY = event.clientY - canvasRect.top + _a.canvasElement.scrollTop; // Use actual mouse position for more precise positioning let newX = actualMouseX + offsetX; let newY = actualMouseY + offsetY; // --- 🎯 FINAL MARGIN/SCROLL LOGIC FOR DRAGEND START --- if ( _a.layoutMode === 'absolute' && _a.canvasElement.classList.contains('preview-printable') ) { const style = window.getComputedStyle(_a.canvasElement); const paddingRight = parseFloat(style.paddingRight); const paddingLeft = parseFloat(style.paddingLeft); const paddingTop = parseFloat(style.paddingTop); // Get the current dimensions of the element being dragged const elementWidth = element.offsetWidth; // --- HORIZONTAL MARGIN CLAMPING (Fixed Width/A4) --- // 1. Constrain Left (must be greater than or equal to paddingLeft) newX = Math.max(newX, paddingLeft); // 2. Constrain Right // The canvas width is max-width: 794px. We calculate the max allowed left position (newX) const maxCanvasContentWidth = _a.canvasElement.offsetWidth; const innerContentRightX = maxCanvasContentWidth - paddingRight - elementWidth; newX = Math.min(newX, innerContentRightX); // --- VERTICAL MARGIN CLAMPING (Only top constraint needed for scrollable area) --- // 3. Constrain Top (must be greater than or equal to paddingTop) newY = Math.max(newY, paddingTop); // 4. IMPORTANT: DO NOT add a maximum Y constraint. This is what breaks scrolling. // We rely on the canvas's `scrollHeight` growing naturally. } else { // --- Existing general canvas boundary constraint (only runs if not in A4 mode) --- const elementRect = element.getBoundingClientRect(); const maxX = _a.canvasElement.scrollWidth - elementRect.width; const maxY = _a.canvasElement.scrollHeight - elementRect.height; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); } // --- 🎯 FINAL MARGIN/SCROLL LOGIC FOR DRAGEND END --- // Set new position element.style.left = `${newX}px`; element.style.top = `${newY}px`; // Reset cursor element.style.cursor = 'grab'; // Capture the state after dragging _a.historyManager.captureState(); _a.dispatchDesignChange(); }); } } _a = Canvas; Canvas.components = []; Canvas.componentFactory = { button: () => new ButtonComponent().create(), header: () => new HeaderComponent().create(1, 'Header', _a.headerAttributeConfig), image: () => new ImageComponent().create(undefined, _a.ImageAttributeConfig), video: () => new VideoComponent(() => _a.historyManager.captureState()).create(), table: () => new TableComponent().create(2, 2, undefined, _a.tableAttributeConfig), text: () => new TextComponent().create(_a.textAttributeConfig), container: () => new ContainerComponent().create(), twoCol: () => new TwoColumnContainer().create(), threeCol: () => new ThreeColumnContainer().create(), landingpage: () => new LandingPageTemplate().create(), link: () => new LinkComponent().create(), }; Canvas.deleteElementHandler = new DeleteElementHandler();