UNPKG

@mindfiredigital/page-builder

Version:
485 lines (484 loc) 18.1 kB
import { DragDropManager } from './DragDropManager.js'; import { DeleteElementHandler } from './DeleteElement.js'; // import { UserPortfolioTemplate } from './../templates/UserPortfolioTemplate.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 { // Add getters and setters for components to make it accessible outside the canvas class static getComponents() { return Canvas.components; } static setComponents(components) { Canvas.components = components; } static init() { Canvas.canvasElement = document.getElementById('canvas'); Canvas.sidebarElement = document.getElementById('sidebar'); Canvas.canvasElement.addEventListener('drop', Canvas.onDrop.bind(Canvas)); Canvas.canvasElement.addEventListener('dragover', event => event.preventDefault() ); Canvas.canvasElement.addEventListener('click', event => { const component = event.target; // console.log('this is my component,', component); // console.log('this is component id ', component.id); if (component) { CustomizationSidebar.showSidebar(component.id); } }); // Set canvas to relative positioning Canvas.canvasElement.style.position = 'relative'; // Initialize the HistoryManager with this canvas, jsonStorage and ComponentControlsManager Canvas.historyManager = new HistoryManager(Canvas.canvasElement); // Pass the canvas element here Canvas.jsonStorage = new JSONStorage(); Canvas.controlsManager = new ComponentControlsManager(Canvas); //Initialize grid manager and initialize drop-view Canvas.gridManager = new GridManager(); Canvas.gridManager.initializeDropPreview(Canvas.canvasElement); const dragDropManager = new DragDropManager( Canvas.canvasElement, Canvas.sidebarElement ); dragDropManager.enable(); // Load existing layout from local storage and render, if any const savedState = Canvas.jsonStorage.load(); if (savedState) { Canvas.restoreState(savedState); } } // Method to clear the canvas and remove all components static clearCanvas() { Canvas.canvasElement.innerHTML = ''; Canvas.components = []; Canvas.historyManager.captureState(); // Capture cleared state for undo functionality if needed // Reinitialize the drop-preview after clearing the canvas Canvas.gridManager.initializeDropPreview(Canvas.canvasElement); } /** * Generates the current state of the canvas for undo/redo purposes. * Maps each component into a structured object containing: * Type, content, position, dimensions, style, and classes. * @returns The array of component objects. */ static getState() { return Canvas.components.map(component => { const baseType = component.classList[0] .split(/\d/)[0] .replace('-component', ''); // Capture the image src for image components const imageSrc = component.querySelector('img') ? component.querySelector('img').src : null; // Capture all inline styles // Enhanced style capturing const computedStyles = window.getComputedStyle(component); const styles = {}; // Capture all potentially relevant CSS properties const stylesToCapture = [ 'position', 'top', 'left', 'right', 'bottom', 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height', 'margin', 'padding', 'background-color', 'background-image', 'border', 'border-radius', 'transform', 'opacity', 'z-index', 'display', 'flex-direction', 'justify-content', 'align-items', 'flex-wrap', 'font-size', 'font-weight', 'color', 'text-align', 'line-height', ]; stylesToCapture.forEach(prop => { styles[prop] = computedStyles.getPropertyValue(prop); }); // Capture data attributes const dataAttributes = {}; Array.from(component.attributes) .filter(attr => attr.name.startsWith('data-')) .forEach(attr => { dataAttributes[attr.name] = attr.value; }); 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, // Store all styles dynamically inlineStyle: component.getAttribute('style') || '', classes: Array.from(component.classList), dataAttributes: dataAttributes, imageSrc: imageSrc, // Store the image source if it's an image component }; }); } static restoreState(state) { Canvas.canvasElement.innerHTML = ''; Canvas.components = []; state.forEach(componentData => { const component = Canvas.createComponent(componentData.type); if (component) { // For custom components, only restore style and attributes, not innerHTML // This prevents content duplication if (!componentData.classes.includes('custom-component')) { component.innerHTML = componentData.content; } // Restore classes component.className = ''; // Clear existing classes componentData.classes.forEach(cls => { component.classList.add(cls); }); 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); } ); } // Add control buttons and listeners Canvas.controlsManager.addControlButtons(component); Canvas.addDraggableListeners(component); // Component-specific restoration if (component.classList.contains('container-component')) { ContainerComponent.restoreContainer(component); } // 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); } if (componentData.type === 'table') { TableComponent.restore(component); } if (componentData.type === 'link') { LinkComponent.restore(component); } // Append to the canvas and add to the components array Canvas.canvasElement.appendChild(component); Canvas.components.push(component); } }); // Reinitialize the drop-preview after restoring the state Canvas.gridManager.initializeDropPreview(Canvas.canvasElement); } static onDrop(event) { var _a; event.preventDefault(); if (event.target.classList.contains('container-component')) { return; } const componentType = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData('component-type'); // console.log(`Dropped component type: ${componentType}`); if (!componentType) { return; } const { gridX, gridY } = this.gridManager.mousePositionAtGridCorner( event, Canvas.canvasElement ); const component = Canvas.createComponent(componentType); if (component) { // Add unique class name const uniqueClass = Canvas.generateUniqueClass(componentType); component.id = uniqueClass; component.classList.add(uniqueClass); component.style.position = 'absolute'; if ( componentType === 'container' || componentType === 'twoCol' || componentType === 'threeCol' ) { // Specific logic for containers component.style.top = `${event.offsetY}px`; } else { // Position the component at the snapped grid corner component.style.position = 'absolute'; component.style.left = `${gridX}px`; component.style.top = `${gridY}px`; } // // Create label for showing class name on hover // const label = document.createElement('span'); // label.className = 'component-label'; // label.textContent = uniqueClass; // component.appendChild(label); Canvas.components.push(component); Canvas.canvasElement.appendChild(component); Canvas.addDraggableListeners(component); // Add drag functionality CustomizationSidebar.updateLayersView(); //On adding new component to the canvas it captures the current state. Canvas.historyManager.captureState(); } } // Reorder components in the Canvas model (in the components array) 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(); } static createComponent(type) { let element = null; // First try to create using component factory const componentFactoryFunction = Canvas.componentFactory[type]; if (componentFactoryFunction) { element = componentFactoryFunction(); } // If not in factory, check if it's a custom component with a tag name 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); //Adding these classnames, since these will have prime role in history management. element.classList.add(`${type}-component`, 'custom-component'); } else { console.warn(`Unknown component type: ${type}`); return null; } } if (element) { element.classList.add('editable-component'); if (type != 'container') { element.classList.add('component-resizer'); } const uniqueClass = Canvas.generateUniqueClass(type); element.setAttribute('id', uniqueClass); // Conditionally set contenteditable attribute if (type === 'image') { element.setAttribute('contenteditable', 'false'); // Image should not be editable } else { element.setAttribute('contenteditable', 'true'); // Other components are editable } // Create label for showing class name on hover const label = document.createElement('span'); label.className = 'component-label'; label.textContent = uniqueClass; element.appendChild(label); //Add control for each component Canvas.controlsManager.addControlButtons(element); CustomizationSidebar.updateLayersView(); } return element; } static generateUniqueClass( type, isContainerComponent = false, containerClass = null ) { if (isContainerComponent && containerClass) { // Handle container components let containerElement = Canvas.components.find(component => component.classList.contains(containerClass) ); if (!containerElement) { // If container is not found in Canvas.components, try searching in the whole document containerElement = document.querySelector(`.${containerClass}`); if (!containerElement) { console.warn(`Container with class ${containerClass} not found.`); return `${containerClass}-${type}1`; // Default fallback name if no container found } } const containerComponents = Array.from(containerElement.children); const typePattern = new RegExp(`${containerClass}-${type}(\\d+)`); // Find the highest existing number for this type in the container 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 { // Handle regular components const typePattern = new RegExp(`${type}(\\d+)`); let maxNumber = 0; // Find the highest existing number for this type across all components Canvas.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) { element.setAttribute('draggable', 'true'); element.style.cursor = 'grab'; let dragStartX = 0; let dragStartY = 0; let elementStartX = 0; let elementStartY = 0; element.addEventListener('dragstart', event => { if (event.dataTransfer) { // Capture starting positions const canvasRect = Canvas.canvasElement.getBoundingClientRect(); const rect = element.getBoundingClientRect(); // Capture starting coordinates dragStartX = event.clientX; dragStartY = event.clientY; // Current element position relative to canvas elementStartX = rect.left - canvasRect.left; elementStartY = rect.top - canvasRect.top; event.dataTransfer.effectAllowed = 'move'; element.style.cursor = 'grabbing'; } }); element.addEventListener('dragend', event => { event.preventDefault(); // const canvasRect = Canvas.canvasElement.getBoundingClientRect(); // Calculate movement delta const deltaX = event.clientX - dragStartX; const deltaY = event.clientY - dragStartY; // Calculate new position let newX = elementStartX + deltaX; let newY = elementStartY + deltaY; // Constrain within canvas boundaries const maxX = Canvas.canvasElement.offsetWidth - element.offsetWidth; const maxY = Canvas.canvasElement.offsetHeight - element.offsetHeight; 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(); }); } // Unused for now, remove it later static exportLayout() { return Canvas.components.map(component => { return { type: component.className, content: component.innerHTML, }; }); } } Canvas.components = []; Canvas.componentFactory = { button: () => new ButtonComponent().create(), header: () => new HeaderComponent().create(), image: () => new ImageComponent().create(), video: () => new VideoComponent(() => Canvas.historyManager.captureState()).create(), table: () => new TableComponent().create(2, 2), text: () => new TextComponent().create(), container: () => new ContainerComponent().create(), twoCol: () => new TwoColumnContainer().create(), threeCol: () => new ThreeColumnContainer().create(), // portfolio: () => new UserPortfolioTemplate().create(), landingpage: () => new LandingPageTemplate().create(), link: () => new LinkComponent().create(), }; const canvas = document.getElementById('canvas'); // Instantiate the DeleteElementHandler const deleteElementHandler = new DeleteElementHandler(); if (canvas) { // Attach click event listener to canvas elements canvas.addEventListener('click', event => { const target = event.target; if (target !== canvas) { deleteElementHandler.selectElement(target); } }); }