📜
layout.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
let divCounter = 0; let isDragging = false; let currentDiv = null; let offset = { x: 0, y: 0 }; let sortable = null; // Wait for settings to be loaded document.addEventListener('DOMContentLoaded', function() { // Ensure settings are available if (!window.LAYOUT_SETTINGS) { console.error('Settings not loaded!'); return; } const settings = window.LAYOUT_SETTINGS; function initSortable() { const flowContainer = document.getElementById('flowContainer'); if (!flowContainer) { console.error('Flow container not found!'); return; } sortable = Sortable.create(flowContainer, { animation: settings.sortable.animation, delay: settings.sortable.delay, delayOnTouchStart: settings.sortable.delayOnTouchStart, ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', filter: function(evt) { const target = evt.target; return target.classList.contains('delete-btn') || target.closest('.delete-btn') || !target.closest('.layout-div').classList.contains('locked'); }, onStart: function(evt) { if (window.AppState.selectedDiv) { window.AppState.selectedDiv.classList.remove('selected'); window.AppState.selectedDiv = null; document.getElementById('propertiesPanel').classList.remove('visible'); } }, onEnd: function(evt) { console.log('Div reordered from index', evt.oldIndex, 'to', evt.newIndex); }, onClick: function(evt) { const clickedDiv = evt.item; if (clickedDiv && !evt.target.classList.contains('delete-btn') && !evt.target.closest('.delete-btn')) { selectDiv(clickedDiv); } } }); } function setPositioning(type) { if (!window.AppState.selectedDiv) return; const divId = window.AppState.selectedDiv.id; const props = window.AppState.divProperties.get(divId); if (type === 'locked') { window.AppState.selectedDiv.classList.remove('absolute'); window.AppState.selectedDiv.classList.add('locked'); const flowContainer = document.getElementById('flowContainer'); const currentIndex = parseInt(divId.split('-')[1]); let insertBefore = null; const flowDivs = Array.from(flowContainer.children); for (let flowDiv of flowDivs) { const flowIndex = parseInt(flowDiv.id.split('-')[1]); if (flowIndex > currentIndex) { insertBefore = flowDiv; break; } } if (insertBefore) { flowContainer.insertBefore(window.AppState.selectedDiv, insertBefore); } else { flowContainer.appendChild(window.AppState.selectedDiv); } window.AppState.selectedDiv.style.left = ''; window.AppState.selectedDiv.style.top = ''; document.getElementById('xSlider').disabled = true; document.getElementById('ySlider').disabled = true; document.getElementById('xDisplay').textContent = 'Flow Layout'; document.getElementById('yDisplay').textContent = 'Flow Layout'; document.getElementById('lockedBtn').classList.add('active'); document.getElementById('absoluteBtn').classList.remove('absolute-active'); const indicator = window.AppState.selectedDiv.querySelector('.lock-indicator'); if (indicator) indicator.textContent = '🔒 Flow'; props.positioning = 'locked'; } else if (type === 'absolute') { window.AppState.selectedDiv.classList.remove('locked'); window.AppState.selectedDiv.classList.add('absolute'); document.getElementById('canvas').appendChild(window.AppState.selectedDiv); window.AppState.selectedDiv.style.left = '100px'; window.AppState.selectedDiv.style.top = '100px'; document.getElementById('xSlider').disabled = false; document.getElementById('ySlider').disabled = false; document.getElementById('lockedBtn').classList.remove('active'); document.getElementById('absoluteBtn').classList.add('absolute-active'); const indicator = window.AppState.selectedDiv.querySelector('.lock-indicator'); if (indicator) indicator.textContent = '📍 Abs'; props.positioning = 'absolute'; props.x = { px: 100, percent: (100 / window.innerWidth) * 100 }; props.y = { px: 100, percent: (100 / window.innerHeight) * 100 }; window.updateSliderRanges(); window.updatePositionSliders(); } } function selectDiv(div) { setTimeout(() => { if (window.AppState.selectedDiv) { window.AppState.selectedDiv.classList.remove('selected'); } window.AppState.selectedDiv = div; div.classList.add('selected'); const panel = document.getElementById('propertiesPanel'); panel.classList.add('visible'); const divId = div.id; const props = window.AppState.divProperties.get(divId); if (props.positioning === 'locked') { document.getElementById('lockedBtn').classList.add('active'); document.getElementById('absoluteBtn').classList.remove('absolute-active'); document.getElementById('xSlider').disabled = true; document.getElementById('ySlider').disabled = true; document.getElementById('xDisplay').textContent = 'Flow Layout'; document.getElementById('yDisplay').textContent = 'Flow Layout'; } else { document.getElementById('lockedBtn').classList.remove('active'); document.getElementById('absoluteBtn').classList.add('absolute-active'); document.getElementById('xSlider').disabled = false; document.getElementById('ySlider').disabled = false; } window.updateSliderRanges(); window.updateSliderValues(); window.updateColorSelection(div); }, settings.animations.selectionDelay); } function addDiv() { divCounter++; const flowContainer = document.getElementById('flowContainer'); const newDiv = document.createElement('div'); newDiv.className = 'layout-div locked'; newDiv.id = `div-${divCounter}`; const widthPercent = settings.sliders.width.percent.default; const heightPercent = settings.sliders.height.percent.default + (Math.random() * 5); const paddingPercent = settings.sliders.padding.percent.default; // Use available width accounting for canvas padding const availableWidth = window.innerWidth - 40; const width = (widthPercent / 100) * availableWidth; const height = (heightPercent / 100) * window.innerHeight; const padding = (paddingPercent / 100) * Math.min(window.innerWidth, window.innerHeight); const colorIndex = Math.floor(Math.random() * settings.colors.length); newDiv.innerHTML = ` <div class="lock-indicator">🔒 Flow</div> <span>Div ${divCounter}</span> <button class="delete-btn" onclick="deleteDiv(event, 'div-${divCounter}')" title="Delete">×</button> `; newDiv.style.width = width + 'px'; newDiv.style.height = height + 'px'; newDiv.style.padding = padding + 'px'; newDiv.style.background = settings.colors[colorIndex]; window.AppState.divProperties.set(`div-${divCounter}`, { positioning: 'locked', name: `Div ${divCounter}`, width: { px: width, percent: widthPercent }, height: { px: height, percent: heightPercent }, padding: { px: padding, percent: paddingPercent }, x: { px: 0, percent: 0 }, y: { px: 0, percent: 0 } }); newDiv.addEventListener('mousedown', startDrag); newDiv.addEventListener('touchstart', startDrag, { passive: false }); newDiv.addEventListener('click', function(e) { if (!e.target.classList.contains('delete-btn') && !e.target.closest('.delete-btn')) { selectDiv(newDiv); } }); flowContainer.appendChild(newDiv); } function deleteDiv(event, divId) { event.stopPropagation(); const div = document.getElementById(divId); if (div) { if (window.AppState.selectedDiv === div) { window.AppState.selectedDiv = null; document.getElementById('propertiesPanel').classList.remove('visible'); } window.AppState.divProperties.delete(divId); div.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => { div.remove(); }, settings.animations.deleteAnimation); } } function startDrag(e) { if (e.currentTarget.classList.contains('locked')) { return; } if (e.target.classList.contains('delete-btn')) return; isDragging = false; let startTime = Date.now(); let startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; let startY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY; currentDiv = e.currentTarget; currentDiv.style.zIndex = settings.drag.zIndexActive; const rect = currentDiv.getBoundingClientRect(); offset.x = startX - rect.left; offset.y = startY - rect.top; const onMove = (e) => { const currentX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; const currentY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY; const distance = Math.sqrt(Math.pow(currentX - startX, 2) + Math.pow(currentY - startY, 2)); if (distance > settings.drag.threshold || Date.now() - startTime > settings.drag.timeThreshold) { isDragging = true; } if (isDragging) { drag(e); } }; const onEnd = (e) => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onEnd); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onEnd); if (!isDragging && Date.now() - startTime < settings.drag.clickTimeLimit) { selectDiv(currentDiv); } stopDrag(); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onEnd); e.preventDefault(); e.stopPropagation(); } function drag(e) { if (!isDragging || !currentDiv || currentDiv.classList.contains('locked')) return; const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY; const newX = clientX - offset.x; const newY = clientY - offset.y; const maxX = window.innerWidth - currentDiv.offsetWidth; const maxY = window.innerHeight - currentDiv.offsetHeight; const constrainedX = Math.max(0, Math.min(newX, maxX)); const constrainedY = Math.max(0, Math.min(newY, maxY)); currentDiv.style.left = constrainedX + 'px'; currentDiv.style.top = constrainedY + 'px'; const divId = currentDiv.id; const props = window.AppState.divProperties.get(divId); if (props) { props.x.px = constrainedX; props.x.percent = (constrainedX / window.innerWidth) * 100; props.y.px = constrainedY; props.y.percent = (constrainedY / window.innerHeight) * 100; } if (window.AppState.selectedDiv === currentDiv) { window.updatePositionSliders(); } e.preventDefault(); } function stopDrag() { if (currentDiv) { currentDiv.style.zIndex = ''; currentDiv = null; } isDragging = false; } function handleResize() { window.AppState.divProperties.forEach((props, divId) => { const div = document.getElementById(divId); if (div) { // Always recalculate from percentage values for consistency const availableWidth = window.innerWidth - 40; const newWidth = (props.width.percent / 100) * availableWidth; const newHeight = (props.height.percent / 100) * window.innerHeight; const newPadding = (props.padding.percent / 100) * Math.min(window.innerWidth, window.innerHeight); // Update DOM div.style.width = newWidth + 'px'; div.style.height = newHeight + 'px'; div.style.padding = newPadding + 'px'; // Update stored pixel values props.width.px = newWidth; props.height.px = newHeight; props.padding.px = newPadding; // Handle absolute positioned divs if (props.positioning === 'absolute') { const newX = (props.x.percent / 100) * window.innerWidth; const newY = (props.y.percent / 100) * window.innerHeight; // Ensure div stays within new viewport const maxX = window.innerWidth - newWidth; const maxY = window.innerHeight - newHeight; const constrainedX = Math.max(0, Math.min(newX, maxX)); const constrainedY = Math.max(0, Math.min(newY, maxY)); div.style.left = constrainedX + 'px'; div.style.top = constrainedY + 'px'; props.x.px = constrainedX; props.y.px = constrainedY; // Update percentages if position was constrained props.x.percent = (constrainedX / window.innerWidth) * 100; props.y.percent = (constrainedY / window.innerHeight) * 100; } } }); if (window.AppState.selectedDiv) { window.updateSliderRanges(); window.updateSliderValues(); } } // Initialize event listeners document.addEventListener('click', (e) => { if (!e.target.closest('.layout-div') && !e.target.closest('.properties-panel')) { if (window.AppState.selectedDiv) { window.AppState.selectedDiv.classList.remove('selected'); window.AppState.selectedDiv = null; document.getElementById('propertiesPanel').classList.remove('visible'); } } }); window.addEventListener('resize', handleResize); // Set initial unit to percentage window.AppState.currentUnit = '%'; document.getElementById('pxBtn').classList.remove('active'); document.getElementById('percentBtn').classList.add('active'); // Update HTML defaults to percentage mode document.getElementById('widthSlider').min = '5'; document.getElementById('widthSlider').max = '100'; document.getElementById('widthSlider').value = '100'; document.getElementById('widthDisplay').textContent = '100%'; document.getElementById('heightSlider').min = '5'; document.getElementById('heightSlider').max = '100'; document.getElementById('heightSlider').value = '15'; document.getElementById('heightDisplay').textContent = '15%'; document.getElementById('paddingSlider').min = '0'; document.getElementById('paddingSlider').max = '10'; document.getElementById('paddingSlider').value = '2'; document.getElementById('paddingDisplay').textContent = '2%'; // Initialize window.initColorGrid(); initSortable(); addDiv(); // Export functions to global scope window.addDiv = addDiv; window.deleteDiv = deleteDiv; window.setPositioning = setPositioning; }); // Fallback initialization if DOMContentLoaded already fired if (document.readyState === 'loading') { // Already handled above } else { // DOM is already loaded, trigger the initialization const event = new Event('DOMContentLoaded'); document.dispatchEvent(event); }