Spaces:
Running
Running
| // ===== Mubashra Studio - Advanced Drag & Drop Editor ===== | |
| const CORRECT_PASSWORD = "inshaaAllah"; | |
| // DOM Elements | |
| const authOverlay = document.getElementById('authOverlay'); | |
| const adminWorkspace = document.getElementById('adminWorkspace'); | |
| const passwordInput = document.getElementById('passwordInput'); | |
| const loginBtn = document.getElementById('loginBtn'); | |
| const errorMsg = document.getElementById('errorMsg'); | |
| const blogCanvas = document.getElementById('blogCanvas'); | |
| const canvasWrapper = document.getElementById('canvasWrapper'); | |
| const canvasArea = document.getElementById('canvasArea'); | |
| const canvasPlaceholder = document.getElementById('canvasPlaceholder'); | |
| const blogTitleInput = document.getElementById('blogTitle'); | |
| const postBtn = document.getElementById('postBtn'); | |
| const previewBtn = document.getElementById('previewBtn'); | |
| // Sidebars | |
| const sidebarLeft = document.getElementById('sidebarLeft'); | |
| const sidebarRight = document.getElementById('sidebarRight'); | |
| const sidebarOverlay = document.getElementById('sidebarOverlay'); | |
| const menuToggle = document.getElementById('menuToggle'); | |
| const closeSidebarLeft = document.getElementById('closeSidebarLeft'); | |
| const closeSidebarRight = document.getElementById('closeSidebarRight'); | |
| const mobilePropsBtn = document.getElementById('mobilePropsBtn'); | |
| // Zoom Controls | |
| const zoomIn = document.getElementById('zoomIn'); | |
| const zoomOut = document.getElementById('zoomOut'); | |
| const zoomReset = document.getElementById('zoomReset'); | |
| const zoomLevelDisplay = document.getElementById('zoomLevel'); | |
| // Modal Elements | |
| const publishModal = document.getElementById('publishModal'); | |
| const closeModal = document.getElementById('closeModal'); | |
| const cancelPublish = document.getElementById('cancelPublish'); | |
| const confirmPublish = document.getElementById('confirmPublish'); | |
| const modalBlogTitle = document.getElementById('modalBlogTitle'); | |
| const coverUploadArea = document.getElementById('coverUploadArea'); | |
| const coverImageInput = document.getElementById('coverImageInput'); | |
| const uploadPlaceholder = document.getElementById('uploadPlaceholder'); | |
| const uploadPreview = document.getElementById('uploadPreview'); | |
| const coverPreviewImg = document.getElementById('coverPreviewImg'); | |
| const removeCover = document.getElementById('removeCover'); | |
| // Element buttons (both sidebar and mobile) | |
| const elementBtns = document.querySelectorAll('.element-btn'); | |
| const mobileToolBtns = document.querySelectorAll('.mobile-tool-btn[data-type]'); | |
| const layersList = document.getElementById('layersList'); | |
| const propertiesPanel = document.getElementById('propertiesPanel'); | |
| // Property Panels | |
| const textProperties = document.getElementById('textProperties'); | |
| const imageProperties = document.getElementById('imageProperties'); | |
| const positionProperties = document.getElementById('positionProperties'); | |
| // Property Inputs | |
| const fontFamily = document.getElementById('fontFamily'); | |
| const fontSize = document.getElementById('fontSize'); | |
| const fontWeight = document.getElementById('fontWeight'); | |
| const textColor = document.getElementById('textColor'); | |
| const textColorHex = document.getElementById('textColorHex'); | |
| const letterSpacing = document.getElementById('letterSpacing'); | |
| const lineHeight = document.getElementById('lineHeight'); | |
| const posX = document.getElementById('posX'); | |
| const posY = document.getElementById('posY'); | |
| const elWidth = document.getElementById('elWidth'); | |
| const elHeight = document.getElementById('elHeight'); | |
| const elRotation = document.getElementById('elRotation'); | |
| const rotationValue = document.getElementById('rotationValue'); | |
| const deleteElement = document.getElementById('deleteElement'); | |
| // Image Properties | |
| const replaceImage = document.getElementById('replaceImage'); | |
| const imageBorderRadius = document.getElementById('imageBorderRadius'); | |
| const borderRadiusValue = document.getElementById('borderRadiusValue'); | |
| const imageOpacity = document.getElementById('imageOpacity'); | |
| const opacityValue = document.getElementById('opacityValue'); | |
| // Image Upload Input | |
| const imageUpload = document.getElementById('imageUpload'); | |
| // State | |
| let selectedElement = null; | |
| let isDragging = false; | |
| let isResizing = false; | |
| let resizeDirection = ''; | |
| let dragStartX = 0; | |
| let dragStartY = 0; | |
| let initialX = 0; | |
| let initialY = 0; | |
| let initialWidth = 0; | |
| let initialHeight = 0; | |
| let elementCounter = 0; | |
| let currentZoom = 1; | |
| let coverImageData = null; | |
| const ZOOM_STEP = 0.1; | |
| const MIN_ZOOM = 0.25; | |
| const MAX_ZOOM = 2; | |
| // ===== Authentication ===== | |
| loginBtn.addEventListener('click', checkPassword); | |
| passwordInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') checkPassword(); | |
| }); | |
| function checkPassword() { | |
| if (passwordInput.value === CORRECT_PASSWORD) { | |
| authOverlay.classList.add('hidden'); | |
| adminWorkspace.classList.remove('hidden'); | |
| } else { | |
| errorMsg.textContent = "Incorrect password. Please try again."; | |
| passwordInput.value = ""; | |
| passwordInput.focus(); | |
| } | |
| } | |
| // ===== Mobile Sidebar Controls ===== | |
| menuToggle?.addEventListener('click', () => { | |
| sidebarLeft.classList.add('open'); | |
| sidebarOverlay.classList.add('visible'); | |
| }); | |
| closeSidebarLeft?.addEventListener('click', closeSidebars); | |
| closeSidebarRight?.addEventListener('click', closeSidebars); | |
| sidebarOverlay?.addEventListener('click', closeSidebars); | |
| mobilePropsBtn?.addEventListener('click', () => { | |
| sidebarRight.classList.add('open'); | |
| sidebarOverlay.classList.add('visible'); | |
| }); | |
| function closeSidebars() { | |
| sidebarLeft.classList.remove('open'); | |
| sidebarRight.classList.remove('open'); | |
| sidebarOverlay.classList.remove('visible'); | |
| } | |
| // ===== Zoom Controls ===== | |
| zoomIn?.addEventListener('click', () => { | |
| if (currentZoom < MAX_ZOOM) { | |
| currentZoom = Math.min(currentZoom + ZOOM_STEP, MAX_ZOOM); | |
| applyZoom(); | |
| } | |
| }); | |
| zoomOut?.addEventListener('click', () => { | |
| if (currentZoom > MIN_ZOOM) { | |
| currentZoom = Math.max(currentZoom - ZOOM_STEP, MIN_ZOOM); | |
| applyZoom(); | |
| } | |
| }); | |
| zoomReset?.addEventListener('click', () => { | |
| currentZoom = 1; | |
| applyZoom(); | |
| }); | |
| function applyZoom() { | |
| canvasWrapper.style.transform = `scale(${currentZoom})`; | |
| if (zoomLevelDisplay) { | |
| zoomLevelDisplay.textContent = `${Math.round(currentZoom * 100)}%`; | |
| } | |
| } | |
| // Mouse wheel zoom | |
| canvasArea?.addEventListener('wheel', (e) => { | |
| if (e.ctrlKey) { | |
| e.preventDefault(); | |
| if (e.deltaY < 0 && currentZoom < MAX_ZOOM) { | |
| currentZoom = Math.min(currentZoom + ZOOM_STEP, MAX_ZOOM); | |
| } else if (e.deltaY > 0 && currentZoom > MIN_ZOOM) { | |
| currentZoom = Math.max(currentZoom - ZOOM_STEP, MIN_ZOOM); | |
| } | |
| applyZoom(); | |
| } | |
| }, { passive: false }); | |
| // ===== Element Creation ===== | |
| elementBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const type = btn.dataset.type; | |
| createElement(type); | |
| closeSidebars(); | |
| }); | |
| }); | |
| mobileToolBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const type = btn.dataset.type; | |
| if (type) createElement(type); | |
| }); | |
| }); | |
| function createElement(type, x = 50, y = 50) { | |
| elementCounter++; | |
| const element = document.createElement('div'); | |
| element.classList.add('draggable-element'); | |
| element.id = `element-${elementCounter}`; | |
| element.dataset.type = type; | |
| element.style.left = `${x}px`; | |
| element.style.top = `${y}px`; | |
| // Create resize handles | |
| const handles = ['nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w']; | |
| handles.forEach(dir => { | |
| const handle = document.createElement('div'); | |
| handle.classList.add('resize-handle', dir); | |
| handle.dataset.direction = dir; | |
| element.appendChild(handle); | |
| }); | |
| // Create content based on type | |
| const content = document.createElement('div'); | |
| content.classList.add('element-content'); | |
| switch (type) { | |
| case 'h1': | |
| content.innerHTML = '<h1 contenteditable="true">Heading 1</h1>'; | |
| element.style.width = '300px'; | |
| element.style.minHeight = '60px'; | |
| break; | |
| case 'h2': | |
| content.innerHTML = '<h2 contenteditable="true">Heading 2</h2>'; | |
| element.style.width = '280px'; | |
| element.style.minHeight = '50px'; | |
| break; | |
| case 'h3': | |
| content.innerHTML = '<h3 contenteditable="true">Heading 3</h3>'; | |
| element.style.width = '250px'; | |
| element.style.minHeight = '40px'; | |
| break; | |
| case 'p': | |
| content.innerHTML = '<p contenteditable="true">Enter your paragraph text here. You can type anything you want.</p>'; | |
| element.style.width = '350px'; | |
| element.style.minHeight = '80px'; | |
| break; | |
| case 'image': | |
| // Trigger file upload | |
| const uploadHandler = (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| content.innerHTML = `<img src="${event.target.result}" alt="Uploaded Image" draggable="false">`; | |
| element.style.width = '300px'; | |
| element.style.height = '200px'; | |
| element.appendChild(content); | |
| blogCanvas.appendChild(element); | |
| setupElementEvents(element); | |
| selectElement(element); | |
| updateLayersList(); | |
| hidePlaceholder(); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| imageUpload.value = ''; | |
| imageUpload.removeEventListener('change', uploadHandler); | |
| }; | |
| imageUpload.addEventListener('change', uploadHandler); | |
| imageUpload.click(); | |
| return; | |
| case 'quote': | |
| content.innerHTML = '<blockquote contenteditable="true">"Add your inspiring quote here." - Author</blockquote>'; | |
| element.style.width = '350px'; | |
| element.style.minHeight = '100px'; | |
| break; | |
| case 'divider': | |
| content.innerHTML = '<div class="element-divider"></div>'; | |
| element.style.width = '300px'; | |
| element.style.height = '30px'; | |
| break; | |
| case 'button': | |
| content.innerHTML = '<span class="element-button" contenteditable="true">Click Me</span>'; | |
| element.style.width = '120px'; | |
| element.style.height = '50px'; | |
| break; | |
| } | |
| element.appendChild(content); | |
| blogCanvas.appendChild(element); | |
| setupElementEvents(element); | |
| selectElement(element); | |
| updateLayersList(); | |
| hidePlaceholder(); | |
| } | |
| function setupElementEvents(element) { | |
| const type = element.dataset.type; | |
| // Helper function to get pointer position | |
| const getPointerPos = (e) => { | |
| if (e.touches && e.touches.length > 0) { | |
| return { x: e.touches[0].clientX, y: e.touches[0].clientY }; | |
| } | |
| return { x: e.clientX, y: e.clientY }; | |
| }; | |
| // Mouse/Touch down for dragging | |
| const handlePointerDown = (e) => { | |
| // Check if it's a resize handle | |
| if (e.target.classList.contains('resize-handle')) { | |
| startResize(e); | |
| return; | |
| } | |
| selectElement(element); | |
| // For text elements, check if we should allow editing | |
| const editableEl = e.target.closest('[contenteditable="true"]'); | |
| if (editableEl && document.activeElement === editableEl) { | |
| return; // Allow text editing | |
| } | |
| // Start dragging | |
| startDrag(e, element); | |
| }; | |
| element.addEventListener('mousedown', handlePointerDown); | |
| element.addEventListener('touchstart', handlePointerDown, { passive: false }); | |
| // Double click/tap to edit text | |
| element.addEventListener('dblclick', () => { | |
| const editable = element.querySelector('[contenteditable="true"]'); | |
| if (editable) { | |
| editable.focus(); | |
| const selection = window.getSelection(); | |
| const range = document.createRange(); | |
| range.selectNodeContents(editable); | |
| selection.removeAllRanges(); | |
| selection.addRange(range); | |
| } | |
| }); | |
| // Resize handles - both mouse and touch | |
| const handles = element.querySelectorAll('.resize-handle'); | |
| handles.forEach(handle => { | |
| handle.addEventListener('mousedown', startResize); | |
| handle.addEventListener('touchstart', startResize, { passive: false }); | |
| }); | |
| // Update layers on text input | |
| const editables = element.querySelectorAll('[contenteditable="true"]'); | |
| editables.forEach(ed => { | |
| ed.addEventListener('input', () => { | |
| updateLayersList(); | |
| }); | |
| }); | |
| } | |
| // ===== Drag Functionality ===== | |
| function startDrag(e, element) { | |
| if (isResizing) return; | |
| e.preventDefault(); | |
| isDragging = true; | |
| selectedElement = element; | |
| element.classList.add('dragging'); | |
| const pos = getEventPos(e); | |
| dragStartX = pos.x; | |
| dragStartY = pos.y; | |
| initialX = element.offsetLeft; | |
| initialY = element.offsetTop; | |
| document.addEventListener('mousemove', drag); | |
| document.addEventListener('mouseup', stopDrag); | |
| document.addEventListener('touchmove', drag, { passive: false }); | |
| document.addEventListener('touchend', stopDrag); | |
| } | |
| function drag(e) { | |
| if (!isDragging || !selectedElement) return; | |
| e.preventDefault(); | |
| const pos = getEventPos(e); | |
| const dx = (pos.x - dragStartX) / currentZoom; | |
| const dy = (pos.y - dragStartY) / currentZoom; | |
| let newX = initialX + dx; | |
| let newY = initialY + dy; | |
| // Keep element within canvas bounds | |
| newX = Math.max(0, newX); | |
| newY = Math.max(0, newY); | |
| selectedElement.style.left = `${newX}px`; | |
| selectedElement.style.top = `${newY}px`; | |
| // Update position inputs | |
| if (posX) posX.value = Math.round(newX); | |
| if (posY) posY.value = Math.round(newY); | |
| } | |
| function stopDrag() { | |
| if (selectedElement) { | |
| selectedElement.classList.remove('dragging'); | |
| } | |
| isDragging = false; | |
| document.removeEventListener('mousemove', drag); | |
| document.removeEventListener('mouseup', stopDrag); | |
| document.removeEventListener('touchmove', drag); | |
| document.removeEventListener('touchend', stopDrag); | |
| } | |
| // ===== Resize Functionality ===== | |
| function startResize(e) { | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| isResizing = true; | |
| resizeDirection = e.target.dataset.direction; | |
| const element = e.target.closest('.draggable-element'); | |
| selectedElement = element; | |
| const pos = getEventPos(e); | |
| dragStartX = pos.x; | |
| dragStartY = pos.y; | |
| initialX = element.offsetLeft; | |
| initialY = element.offsetTop; | |
| initialWidth = element.offsetWidth; | |
| initialHeight = element.offsetHeight; | |
| document.addEventListener('mousemove', resize); | |
| document.addEventListener('mouseup', stopResize); | |
| document.addEventListener('touchmove', resize, { passive: false }); | |
| document.addEventListener('touchend', stopResize); | |
| } | |
| function resize(e) { | |
| if (!isResizing || !selectedElement) return; | |
| e.preventDefault(); | |
| const pos = getEventPos(e); | |
| const dx = (pos.x - dragStartX) / currentZoom; | |
| const dy = (pos.y - dragStartY) / currentZoom; | |
| let newWidth = initialWidth; | |
| let newHeight = initialHeight; | |
| let newX = initialX; | |
| let newY = initialY; | |
| if (resizeDirection.includes('e')) { | |
| newWidth = Math.max(50, initialWidth + dx); | |
| } | |
| if (resizeDirection.includes('w')) { | |
| newWidth = Math.max(50, initialWidth - dx); | |
| newX = initialX + (initialWidth - newWidth); | |
| } | |
| if (resizeDirection.includes('s')) { | |
| newHeight = Math.max(30, initialHeight + dy); | |
| } | |
| if (resizeDirection.includes('n')) { | |
| newHeight = Math.max(30, initialHeight - dy); | |
| newY = initialY + (initialHeight - newHeight); | |
| } | |
| selectedElement.style.width = `${newWidth}px`; | |
| selectedElement.style.height = `${newHeight}px`; | |
| selectedElement.style.left = `${newX}px`; | |
| selectedElement.style.top = `${newY}px`; | |
| if (elWidth) elWidth.value = Math.round(newWidth); | |
| if (elHeight) elHeight.value = Math.round(newHeight); | |
| if (posX) posX.value = Math.round(newX); | |
| if (posY) posY.value = Math.round(newY); | |
| } | |
| function stopResize() { | |
| isResizing = false; | |
| resizeDirection = ''; | |
| document.removeEventListener('mousemove', resize); | |
| document.removeEventListener('mouseup', stopResize); | |
| document.removeEventListener('touchmove', resize); | |
| document.removeEventListener('touchend', stopResize); | |
| } | |
| // Helper to get event position for both mouse and touch | |
| function getEventPos(e) { | |
| if (e.touches && e.touches.length > 0) { | |
| return { x: e.touches[0].clientX, y: e.touches[0].clientY }; | |
| } | |
| return { x: e.clientX, y: e.clientY }; | |
| } | |
| // ===== Selection ===== | |
| function selectElement(element) { | |
| if (selectedElement && selectedElement !== element) { | |
| selectedElement.classList.remove('selected'); | |
| } | |
| selectedElement = element; | |
| element.classList.add('selected'); | |
| showPropertiesFor(element); | |
| updateLayersList(); | |
| } | |
| function deselectAll() { | |
| if (selectedElement) { | |
| selectedElement.classList.remove('selected'); | |
| selectedElement = null; | |
| } | |
| hideAllPropertyPanels(); | |
| } | |
| blogCanvas.addEventListener('click', (e) => { | |
| if (e.target === blogCanvas || e.target === canvasPlaceholder) { | |
| deselectAll(); | |
| } | |
| }); | |
| // ===== Properties Panel ===== | |
| function showPropertiesFor(element) { | |
| const type = element.dataset.type; | |
| hideAllPropertyPanels(); | |
| if (positionProperties) positionProperties.classList.remove('hidden'); | |
| if (posX) posX.value = Math.round(element.offsetLeft); | |
| if (posY) posY.value = Math.round(element.offsetTop); | |
| if (elWidth) elWidth.value = Math.round(element.offsetWidth); | |
| if (elHeight) elHeight.value = Math.round(element.offsetHeight); | |
| const transform = element.style.transform; | |
| const rotateMatch = transform.match(/rotate\((\d+)deg\)/); | |
| if (elRotation) elRotation.value = rotateMatch ? parseInt(rotateMatch[1]) : 0; | |
| if (rotationValue) rotationValue.textContent = `${elRotation?.value || 0}°`; | |
| if (['h1', 'h2', 'h3', 'p', 'quote', 'button'].includes(type)) { | |
| if (textProperties) textProperties.classList.remove('hidden'); | |
| const editable = element.querySelector('[contenteditable="true"]') || | |
| element.querySelector('.element-button'); | |
| if (editable) { | |
| const style = window.getComputedStyle(editable); | |
| if (fontSize) fontSize.value = parseInt(style.fontSize); | |
| if (fontWeight) fontWeight.value = style.fontWeight; | |
| const color = rgbToHex(style.color); | |
| if (textColor) textColor.value = color; | |
| if (textColorHex) textColorHex.value = color; | |
| const ls = parseFloat(style.letterSpacing); | |
| if (letterSpacing) letterSpacing.value = isNaN(ls) ? 0 : ls; | |
| const lh = parseFloat(style.lineHeight) / parseFloat(style.fontSize); | |
| if (lineHeight) lineHeight.value = isNaN(lh) ? 1.5 : lh.toFixed(1); | |
| } | |
| } else if (type === 'image') { | |
| if (imageProperties) imageProperties.classList.remove('hidden'); | |
| const br = parseInt(element.style.borderRadius) || 0; | |
| if (imageBorderRadius) imageBorderRadius.value = br; | |
| if (borderRadiusValue) borderRadiusValue.textContent = `${br}px`; | |
| const op = parseFloat(element.style.opacity); | |
| const opacityPercent = isNaN(op) ? 100 : op * 100; | |
| if (imageOpacity) imageOpacity.value = opacityPercent; | |
| if (opacityValue) opacityValue.textContent = `${Math.round(opacityPercent)}%`; | |
| } | |
| if (propertiesPanel) { | |
| const emptyMsg = propertiesPanel.querySelector('.empty-properties'); | |
| if (emptyMsg) emptyMsg.style.display = 'none'; | |
| } | |
| } | |
| function hideAllPropertyPanels() { | |
| if (textProperties) textProperties.classList.add('hidden'); | |
| if (imageProperties) imageProperties.classList.add('hidden'); | |
| if (positionProperties) positionProperties.classList.add('hidden'); | |
| if (propertiesPanel) { | |
| const emptyMsg = propertiesPanel.querySelector('.empty-properties'); | |
| if (emptyMsg) emptyMsg.style.display = 'block'; | |
| } | |
| } | |
| // ===== Property Change Handlers ===== | |
| fontFamily?.addEventListener('change', () => { | |
| if (!selectedElement) return; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.fontFamily = fontFamily.value; | |
| }); | |
| fontSize?.addEventListener('input', () => { | |
| if (!selectedElement) return; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.fontSize = `${fontSize.value}px`; | |
| }); | |
| fontWeight?.addEventListener('change', () => { | |
| if (!selectedElement) return; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.fontWeight = fontWeight.value; | |
| }); | |
| textColor?.addEventListener('input', () => { | |
| if (!selectedElement) return; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.color = textColor.value; | |
| if (textColorHex) textColorHex.value = textColor.value; | |
| }); | |
| textColorHex?.addEventListener('input', () => { | |
| if (!selectedElement) return; | |
| const hex = textColorHex.value; | |
| if (/^#[0-9A-Fa-f]{6}$/.test(hex)) { | |
| if (textColor) textColor.value = hex; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.color = hex; | |
| } | |
| }); | |
| letterSpacing?.addEventListener('input', () => { | |
| if (!selectedElement) return; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.letterSpacing = `${letterSpacing.value}px`; | |
| }); | |
| lineHeight?.addEventListener('input', () => { | |
| if (!selectedElement) return; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.lineHeight = lineHeight.value; | |
| }); | |
| document.querySelectorAll('[data-align]').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| if (!selectedElement) return; | |
| const editable = getEditableElement(); | |
| if (editable) editable.style.textAlign = btn.dataset.align; | |
| document.querySelectorAll('[data-align]').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| }); | |
| }); | |
| posX?.addEventListener('input', () => { | |
| if (selectedElement) selectedElement.style.left = `${posX.value}px`; | |
| }); | |
| posY?.addEventListener('input', () => { | |
| if (selectedElement) selectedElement.style.top = `${posY.value}px`; | |
| }); | |
| elWidth?.addEventListener('input', () => { | |
| if (selectedElement) selectedElement.style.width = `${elWidth.value}px`; | |
| }); | |
| elHeight?.addEventListener('input', () => { | |
| if (selectedElement) selectedElement.style.height = `${elHeight.value}px`; | |
| }); | |
| elRotation?.addEventListener('input', () => { | |
| if (selectedElement) { | |
| selectedElement.style.transform = `rotate(${elRotation.value}deg)`; | |
| if (rotationValue) rotationValue.textContent = `${elRotation.value}°`; | |
| } | |
| }); | |
| imageBorderRadius?.addEventListener('input', () => { | |
| if (selectedElement) { | |
| selectedElement.style.borderRadius = `${imageBorderRadius.value}px`; | |
| selectedElement.style.overflow = 'hidden'; | |
| if (borderRadiusValue) borderRadiusValue.textContent = `${imageBorderRadius.value}px`; | |
| } | |
| }); | |
| imageOpacity?.addEventListener('input', () => { | |
| if (selectedElement) { | |
| selectedElement.style.opacity = imageOpacity.value / 100; | |
| if (opacityValue) opacityValue.textContent = `${imageOpacity.value}%`; | |
| } | |
| }); | |
| replaceImage?.addEventListener('change', (e) => { | |
| if (!selectedElement || selectedElement.dataset.type !== 'image') return; | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| const img = selectedElement.querySelector('img'); | |
| if (img) img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| replaceImage.value = ''; | |
| }); | |
| deleteElement?.addEventListener('click', () => { | |
| if (selectedElement) { | |
| selectedElement.remove(); | |
| selectedElement = null; | |
| hideAllPropertyPanels(); | |
| updateLayersList(); | |
| checkPlaceholder(); | |
| } | |
| }); | |
| // ===== Layers List ===== | |
| function updateLayersList() { | |
| const elements = blogCanvas.querySelectorAll('.draggable-element'); | |
| if (elements.length === 0) { | |
| layersList.innerHTML = '<p class="empty-layers">No elements yet</p>'; | |
| return; | |
| } | |
| layersList.innerHTML = ''; | |
| elements.forEach((el) => { | |
| const type = el.dataset.type; | |
| const icon = getIconForType(type); | |
| const label = getLabelForElement(el, type); | |
| const layerItem = document.createElement('div'); | |
| layerItem.classList.add('layer-item'); | |
| if (el === selectedElement) { | |
| layerItem.classList.add('active'); | |
| } | |
| layerItem.innerHTML = ` | |
| <span class="material-symbols-outlined">${icon}</span> | |
| <span>${label}</span> | |
| `; | |
| layerItem.addEventListener('click', () => { | |
| selectElement(el); | |
| closeSidebars(); | |
| }); | |
| layersList.appendChild(layerItem); | |
| }); | |
| } | |
| function getIconForType(type) { | |
| const icons = { | |
| 'h1': 'title', 'h2': 'format_h2', 'h3': 'format_h3', | |
| 'p': 'notes', 'image': 'image', 'quote': 'format_quote', | |
| 'divider': 'horizontal_rule', 'button': 'smart_button' | |
| }; | |
| return icons[type] || 'crop_square'; | |
| } | |
| function getLabelForElement(el, type) { | |
| const labels = { | |
| 'h1': 'Heading 1', 'h2': 'Heading 2', 'h3': 'Heading 3', | |
| 'p': 'Paragraph', 'image': 'Image', 'quote': 'Quote', | |
| 'divider': 'Divider', 'button': 'Button' | |
| }; | |
| const editable = el.querySelector('[contenteditable="true"]'); | |
| if (editable) { | |
| const text = editable.textContent.trim(); | |
| if (text.length > 15) return text.substring(0, 15) + '...'; | |
| return text || labels[type]; | |
| } | |
| return labels[type] || 'Element'; | |
| } | |
| // ===== Helper Functions ===== | |
| function getEditableElement() { | |
| if (!selectedElement) return null; | |
| return selectedElement.querySelector('[contenteditable="true"]') || | |
| selectedElement.querySelector('.element-button') || | |
| selectedElement.querySelector('blockquote'); | |
| } | |
| function rgbToHex(rgb) { | |
| if (!rgb) return '#000000'; | |
| if (rgb.startsWith('#')) return rgb; | |
| const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); | |
| if (!match) return '#000000'; | |
| const r = parseInt(match[1]).toString(16).padStart(2, '0'); | |
| const g = parseInt(match[2]).toString(16).padStart(2, '0'); | |
| const b = parseInt(match[3]).toString(16).padStart(2, '0'); | |
| return `#${r}${g}${b}`; | |
| } | |
| function hidePlaceholder() { | |
| if (canvasPlaceholder) canvasPlaceholder.classList.add('hidden'); | |
| } | |
| function checkPlaceholder() { | |
| const elements = blogCanvas.querySelectorAll('.draggable-element'); | |
| if (canvasPlaceholder) { | |
| canvasPlaceholder.classList.toggle('hidden', elements.length > 0); | |
| } | |
| } | |
| // ===== Keyboard Shortcuts ===== | |
| document.addEventListener('keydown', (e) => { | |
| if (!selectedElement) return; | |
| const isEditing = document.activeElement.getAttribute('contenteditable') === 'true' || | |
| document.activeElement.tagName === 'INPUT'; | |
| if ((e.key === 'Delete' || e.key === 'Backspace') && !isEditing) { | |
| selectedElement.remove(); | |
| selectedElement = null; | |
| hideAllPropertyPanels(); | |
| updateLayersList(); | |
| checkPlaceholder(); | |
| e.preventDefault(); | |
| } | |
| if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) && !isEditing) { | |
| const step = e.shiftKey ? 10 : 1; | |
| switch (e.key) { | |
| case 'ArrowUp': | |
| selectedElement.style.top = `${selectedElement.offsetTop - step}px`; | |
| break; | |
| case 'ArrowDown': | |
| selectedElement.style.top = `${selectedElement.offsetTop + step}px`; | |
| break; | |
| case 'ArrowLeft': | |
| selectedElement.style.left = `${selectedElement.offsetLeft - step}px`; | |
| break; | |
| case 'ArrowRight': | |
| selectedElement.style.left = `${selectedElement.offsetLeft + step}px`; | |
| break; | |
| } | |
| if (posX) posX.value = Math.round(selectedElement.offsetLeft); | |
| if (posY) posY.value = Math.round(selectedElement.offsetTop); | |
| e.preventDefault(); | |
| } | |
| }); | |
| // ===== Publish Modal ===== | |
| postBtn?.addEventListener('click', () => { | |
| const elements = blogCanvas.querySelectorAll('.draggable-element'); | |
| if (elements.length === 0) { | |
| alert('Canvas is empty! Add some elements before publishing.'); | |
| return; | |
| } | |
| // Show modal | |
| if (modalBlogTitle) modalBlogTitle.value = blogTitleInput?.value || ''; | |
| publishModal.classList.remove('hidden'); | |
| }); | |
| closeModal?.addEventListener('click', closePublishModal); | |
| cancelPublish?.addEventListener('click', closePublishModal); | |
| publishModal?.querySelector('.modal-backdrop')?.addEventListener('click', closePublishModal); | |
| function closePublishModal() { | |
| publishModal.classList.add('hidden'); | |
| coverImageData = null; | |
| if (uploadPlaceholder) uploadPlaceholder.classList.remove('hidden'); | |
| if (uploadPreview) uploadPreview.classList.add('hidden'); | |
| } | |
| // Cover Image Upload | |
| coverUploadArea?.addEventListener('click', () => { | |
| coverImageInput?.click(); | |
| }); | |
| coverImageInput?.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| coverImageData = event.target.result; | |
| if (coverPreviewImg) coverPreviewImg.src = coverImageData; | |
| if (uploadPlaceholder) uploadPlaceholder.classList.add('hidden'); | |
| if (uploadPreview) uploadPreview.classList.remove('hidden'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| removeCover?.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| coverImageData = null; | |
| if (coverImageInput) coverImageInput.value = ''; | |
| if (uploadPlaceholder) uploadPlaceholder.classList.remove('hidden'); | |
| if (uploadPreview) uploadPreview.classList.add('hidden'); | |
| }); | |
| // Confirm Publish | |
| confirmPublish?.addEventListener('click', () => { | |
| const title = modalBlogTitle?.value || blogTitleInput?.value || 'Untitled Blog'; | |
| // Clone canvas for saving | |
| const clone = blogCanvas.cloneNode(true); | |
| // Clean up cloned elements | |
| clone.querySelectorAll('.draggable-element').forEach(el => { | |
| el.classList.remove('selected', 'dragging'); | |
| el.querySelectorAll('.resize-handle').forEach(h => h.remove()); | |
| el.querySelectorAll('[contenteditable]').forEach(c => { | |
| c.removeAttribute('contenteditable'); | |
| }); | |
| el.style.cursor = 'default'; | |
| }); | |
| const placeholder = clone.querySelector('.canvas-placeholder'); | |
| if (placeholder) placeholder.remove(); | |
| // Create blog data | |
| const blogData = { | |
| id: Date.now(), | |
| title: title, | |
| date: new Date().toISOString(), | |
| content: clone.innerHTML, | |
| coverImage: coverImageData || null, | |
| backgroundColor: '#FFFFFF' | |
| }; | |
| // Save to localStorage | |
| // Save to localStorage | |
| const blogs = JSON.parse(localStorage.getItem('mubashra_blogs_v2')) || []; | |
| blogs.unshift(blogData); | |
| localStorage.setItem('mubashra_blogs_v2', JSON.stringify(blogs)); | |
| alert('Blog published successfully!'); | |
| // Clear canvas | |
| blogCanvas.querySelectorAll('.draggable-element').forEach(el => el.remove()); | |
| if (blogTitleInput) blogTitleInput.value = ''; | |
| selectedElement = null; | |
| hideAllPropertyPanels(); | |
| updateLayersList(); | |
| checkPlaceholder(); | |
| closePublishModal(); | |
| }); | |
| // Preview | |
| previewBtn?.addEventListener('click', () => { | |
| const elements = blogCanvas.querySelectorAll('.draggable-element'); | |
| if (elements.length === 0) { | |
| alert('Canvas is empty! Add some elements to preview.'); | |
| return; | |
| } | |
| const clone = blogCanvas.cloneNode(true); | |
| clone.querySelectorAll('.draggable-element').forEach(el => { | |
| el.classList.remove('selected', 'dragging'); | |
| el.querySelectorAll('.resize-handle').forEach(h => h.remove()); | |
| el.querySelectorAll('[contenteditable]').forEach(c => { | |
| c.removeAttribute('contenteditable'); | |
| }); | |
| el.style.cursor = 'default'; | |
| }); | |
| const placeholder = clone.querySelector('.canvas-placeholder'); | |
| if (placeholder) placeholder.remove(); | |
| const previewWindow = window.open('', '_blank'); | |
| previewWindow.document.write(` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Preview - ${blogTitleInput?.value || 'Untitled Blog'}</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: #F7FAF9; | |
| min-height: 100vh; | |
| padding: 1rem; | |
| } | |
| @media (min-width: 768px) { | |
| body { padding: 2rem; display: flex; justify-content: center; } | |
| } | |
| .blog-content { | |
| width: 100%; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 1.5rem; | |
| background: white; | |
| border-radius: 16px; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.12); | |
| } | |
| @media (min-width: 768px) { | |
| .blog-content { padding: 2rem; } | |
| } | |
| .draggable-element { | |
| position: relative !important; | |
| left: auto !important; | |
| top: auto !important; | |
| width: 100% !important; | |
| max-width: 100% !important; | |
| height: auto !important; | |
| min-height: auto !important; | |
| margin-bottom: 1.5rem; | |
| } | |
| .element-content { padding: 0; } | |
| h1, h2, h3, p { margin: 0; word-wrap: break-word; } | |
| h1 { font-size: clamp(1.75rem, 5vw, 2.5rem); font-weight: 700; font-family: 'Playfair Display', serif; margin-bottom: 1rem; line-height: 1.2; } | |
| h2 { font-size: clamp(1.5rem, 4vw, 2rem); font-weight: 600; margin-bottom: 0.75rem; line-height: 1.3; } | |
| h3 { font-size: clamp(1.25rem, 3vw, 1.5rem); font-weight: 600; margin-bottom: 0.5rem; line-height: 1.4; } | |
| p { font-size: clamp(0.95rem, 2.5vw, 1.1rem); line-height: 1.8; margin-bottom: 1rem; color: #4A5568; } | |
| blockquote { | |
| margin: 1.5rem 0; | |
| padding: 1rem 1.5rem; | |
| border-left: 4px solid #2D6A4F; | |
| background: #E8F3EE; | |
| font-style: italic; | |
| border-radius: 0 8px 8px 0; | |
| } | |
| img { | |
| width: 100%; | |
| max-width: 100%; | |
| height: auto !important; | |
| object-fit: cover; | |
| display: block; | |
| border-radius: 12px; | |
| margin: 1rem 0; | |
| } | |
| .element-divider { | |
| width: 100%; | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent, #2D6A4F, transparent); | |
| margin: 2rem 0; | |
| } | |
| .element-button { | |
| display: inline-block; | |
| padding: 0.75rem 1.5rem; | |
| background: #2D6A4F; | |
| color: white; | |
| font-weight: 600; | |
| border-radius: 8px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="blog-content">${clone.innerHTML}</div> | |
| </body> | |
| </html> | |
| `); | |
| previewWindow.document.close(); | |
| }); | |
| // Initialize | |
| updateLayersList(); | |