Skip to content

Dashboard grid system movement buggy on touch devices #148

@mStirner

Description

@mStirner

The grid system used for position the dashboard widgets ("vue3-drr-grid-layout") is terrible and buggy.
Replace it with custom css grid system.

Playground/example:

<!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;
            background-color: rgb(53, 49, 49)
        }

        .grid {
            display: grid;
            gap: 2px;
            border: 2px solid #000;
            position: relative;
            background-color: #000;
        }

        .grid div {
            background-color: #e0e0e0;
        }

        .draggable {
            width: 100px;
            height: 100px;
            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;
            cursor: grab;
        }

    </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;

                    // TODO calcualte here "enforcedGridSize" for indicator

                }
            });

            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
                    // TODO: Check if greather than grid size, and set it to grid size if greaiter
                    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>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions