This gist allows to define a css grid system where elements can be positioned inside. E.g. Dashboard widget arrangement.
Add a dragable element, resize it, position where ever you want.
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Grid Draggable & Resizable Element</title> | |
| <style> | |
| *::selection { | |
| background-color: transparent; | |
| user-select: none; | |
| } | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| margin: 0; | |
| font-family: Arial, sans-serif; | |
| } | |
| .grid { | |
| display: grid; | |
| gap: 2px; | |
| border: 2px solid #000; | |
| position: relative; | |
| background-color: #000; | |
| } | |
| .grid div { | |
| background-color: #e0e0e0; | |
| } | |
| .draggable { | |
| width: 100px; | |
| height: 100px; | |
| cursor: grab; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: absolute; | |
| background-color: red !important; | |
| /*resize: both;*/ | |
| overflow: auto; | |
| box-sizing: border-box; | |
| } | |
| .draggable:active { | |
| cursor: grabbing; | |
| } | |
| .disabled { | |
| pointer-events: none; | |
| opacity: 0.6; | |
| } | |
| div.dashed-border{ | |
| border: 1px dashed #fff; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="grid" id="grid"></div> | |
| <div style="margin-top: 20px; text-align: center;"> | |
| <label>Columns: | |
| <input type="number" id="nCols" min="1" value="5"> | |
| </label> | |
| <label>Rows: | |
| <input type="number" id="nRows" min="1" value="5"> | |
| </label> | |
| <button id="updatePosition">Update Grid</button> | |
| <button id="addDraggable" disabled>Add Draggable</button> | |
| <label> | |
| <input type="checkbox" id="toggleLock"> Enable Dragging/Resizing | |
| </label> | |
| </div> | |
| <script> | |
| const updateButton = document.getElementById('updatePosition'); | |
| const addDraggableButton = document.getElementById('addDraggable'); | |
| const toggleLock = document.getElementById('toggleLock'); | |
| const nCols = document.getElementById('nCols'); | |
| const nRows = document.getElementById('nRows'); | |
| const grid = document.getElementById('grid'); | |
| const draggableElements = []; | |
| const setDefaultGrid = () => { | |
| updateGrid(5, 5); | |
| createDraggable(0, 0); | |
| }; | |
| toggleLock.addEventListener("click", (e) => { | |
| if (toggleLock.checked) { | |
| // enable dragging | |
| addDraggableButton.disabled = false; | |
| draggableElements.forEach(({element}) => { | |
| console.log("element", element) | |
| element.style.resize = "both"; | |
| element.classList.add("dashed-border"); | |
| }); | |
| } else { | |
| //disable dragging | |
| addDraggableButton.disabled = true; | |
| draggableElements.forEach(({element}) => { | |
| element.style.resize = "none"; | |
| element.classList.remove("dashed-border"); | |
| }); | |
| } | |
| }); | |
| const updateGrid = (cols, rows) => { | |
| grid.style.gridTemplateColumns = `repeat(${cols}, 100px)`; | |
| grid.style.gridTemplateRows = `repeat(${rows}, 100px)`; | |
| // Clear grid cells (but not draggable elements) | |
| const totalCells = cols * rows; | |
| while (grid.firstChild && !grid.firstChild.classList.contains('draggable')) { | |
| grid.removeChild(grid.firstChild); | |
| } | |
| for (let i = 0; i < totalCells; i++) { | |
| const cell = document.createElement('div'); | |
| grid.appendChild(cell); | |
| } | |
| // Reattach draggable elements | |
| draggableElements.forEach(({ element, left, top, width, height }) => { | |
| element.style.left = `${left}px`; | |
| element.style.top = `${top}px`; | |
| element.style.width = `${width}px`; | |
| element.style.height = `${height}px`; | |
| grid.appendChild(element); | |
| }); | |
| }; | |
| const createDraggable = (left = 0, top = 0) => { | |
| let offsetX = 0; | |
| let offsetY = 0; | |
| let isDragging = false; | |
| const draggable = document.createElement('div'); | |
| draggable.className = 'draggable'; | |
| draggable.textContent = 'Drag Me'; | |
| draggable.style.left = `${left}px`; | |
| draggable.style.top = `${top}px`; | |
| if(toggleLock.checked){ | |
| draggable.style.resize = "both"; | |
| // add only if resize checkbox is enabled | |
| // if this is called in "setDefaultGrid()" this indicates its dragable, which is by default false | |
| // thats the reason why is only added if resize is set & checkbox enabled | |
| draggable.classList.add("dashed-border"); | |
| }else{ | |
| draggable.style.resize = "none"; | |
| } | |
| const elementData = { | |
| element: draggable, | |
| left, | |
| top, | |
| width: 100, | |
| height: 100, | |
| }; | |
| draggableElements.push(elementData); | |
| grid.appendChild(draggable); | |
| draggable.addEventListener('mousedown', (e) => { | |
| if (toggleLock.checked) { | |
| isDragging = true; | |
| offsetX = e.clientX - draggable.offsetLeft; | |
| offsetY = e.clientY - draggable.offsetTop; | |
| } | |
| }); | |
| document.addEventListener('mousemove', (e) => { | |
| if (isDragging && toggleLock.checked) { | |
| let x = e.clientX - offsetX; | |
| let y = e.clientY - offsetY; | |
| // Snap to grid | |
| const gridSize = 100 + 2; // 100px cell + 5px gap | |
| x = Math.round(x / gridSize) * gridSize; | |
| y = Math.round(y / gridSize) * gridSize; | |
| // Constrain within grid boundaries | |
| x = Math.max(0, Math.min(x, grid.offsetWidth - draggable.offsetWidth)); | |
| y = Math.max(0, Math.min(y, grid.offsetHeight - draggable.offsetHeight)); | |
| draggable.style.left = `${x}px`; | |
| draggable.style.top = `${y}px`; | |
| elementData.left = x; | |
| elementData.top = y; | |
| } | |
| }); | |
| document.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| // Ensure resizing snaps to grid | |
| const enforceGridSize = () => { | |
| if (toggleLock.checked) { | |
| const gridSize = 100 + 2; // 100px cell + 5px gap | |
| let width = draggable.offsetWidth; | |
| let height = draggable.offsetHeight; | |
| // Calculate the closest valid grid-aligned size | |
| const newWidth = Math.round(width / gridSize) * gridSize - 2; | |
| const newHeight = Math.round(height / gridSize) * gridSize - 2; | |
| // Enforce minimum size and reset position to prevent shifting | |
| draggable.style.width = `${Math.max(gridSize - 5, newWidth)}px`; | |
| draggable.style.height = `${Math.max(gridSize - 5, newHeight)}px`; | |
| let left = parseInt(draggable.style.left, 10); | |
| let top = parseInt(draggable.style.top, 10); | |
| left = Math.round(left / gridSize) * gridSize; | |
| top = Math.round(top / gridSize) * gridSize; | |
| draggable.style.left = `${left}px`; | |
| draggable.style.top = `${top}px`; | |
| elementData.width = newWidth; | |
| elementData.height = newHeight; | |
| elementData.left = left; | |
| elementData.top = top; | |
| } | |
| }; | |
| draggable.addEventListener('mouseup', enforceGridSize); | |
| draggable.addEventListener('mouseleave', enforceGridSize); | |
| }; | |
| setDefaultGrid(); | |
| updateButton.addEventListener('click', () => { | |
| const cols = parseInt(nCols.value, 10); | |
| const rows = parseInt(nRows.value, 10); | |
| if (cols > 0 && rows > 0) { | |
| updateGrid(cols, rows); | |
| } else { | |
| alert('Columns and rows must be greater than 0.'); | |
| } | |
| }); | |
| addDraggableButton.addEventListener('click', () => { | |
| createDraggable(0, 0); | |
| }); | |
| </script> | |
| </body> | |
| </html> |