javascriptsplitresizablejsplitpanesplitpane

Creating Vertivcally/Horizontally Split-Div in Javascript


For a while now, I have been trying to create dynamic-resizable-split-panes in javascript (something like vs code). I successfully created the dynamic split panes. It creates the split panes by checking the parent element and its children and decides whether to create a split pane vertically or horizontally. What I mean is, if the parent already has vertically split panes and the new pane that has to be created is also vertical, it simply created the structure and appends it in the parent. Otherwise, it will wrap the active pane with a new parent and split the pane horizontally. (Same goes for the other way around). The problem I am facing is, how can I make them resizable like vs code. What I want is, on mouse drag the panes should be resized.

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dynamic Split Panes</title>

    <script src="jquery.js"></script>

    <link rel="stylesheet" href="main.css">
</head>
<body>
    <div id="container">
        <div class="ep">
            <div class="ec" id="ec"></div>
        </div>
    </div>

    <script src="main.js"></script>
</body>
</html>

CSS:

html,
body{
    height: 100%;
    width: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
}

#container{
    display: flex;
    width: 100%;
    height: 100%;
    background-color: black;
}

.ep{
    display: flex;
    flex: 1;
}

.ec{
    flex: auto;
    background-color: white;
}

.gutter-ver{
    height: 10px;
    width: 100%;
    background-color: red;
}

.gutter-hor{
    height: 100%;
    width: 10px;
    background-color: red;
}

.ui-resizable-ghost { 
    border: 1px dotted gray; 
    background-color: rgba(128, 128, 128, 0.544);
}

JavaScript:

var ec_active = document.querySelector(".ec");
var ec = null;
function createEC(dir){
    if(dir == "ver"){
        if(ec_active.parentElement.style.flexDirection == "row"){
            // `element` is the element you want to wrap
            var parent = ec_active.parentNode;
            var wrapper = document.createElement('div');
            wrapper.classList.add("ep");
            wrapper.style.flexDirection = "column";
            // set the wrapper as child (instead of the element)
            parent.replaceChild(wrapper, ec_active);
            // set element as child of wrapper
            wrapper.appendChild(ec_active);


            ec = document.createElement("div");
            ec.classList.add("ec");
            
            var gv = document.createElement("div");
            gv.classList.add("gutter-ver");

            ec_active.parentElement.appendChild(gv);
            ec_active.parentElement.appendChild(ec);
        }else if(ec_active.parentElement.style.flexDirection == "column"){
            ec = document.createElement("div");
            ec.classList.add("ec");
            
            var gv = document.createElement("div");
            gv.classList.add("gutter-ver");

            ec_active.parentElement.appendChild(gv);
            ec_active.parentElement.appendChild(ec);
        }else{
            ec = document.createElement("div");
            ec.classList.add("ec");
            
            var gv = document.createElement("div");
            gv.classList.add("gutter-ver");

            ec_active.parentElement.style.flexDirection = "column";

            ec_active.parentElement.appendChild(gv);
            ec_active.parentElement.appendChild(ec);
        }
    }else{
        if(ec_active.parentElement.style.flexDirection == "column"){
            // `element` is the element you want to wrap
            var parent = ec_active.parentNode;
            var wrapper = document.createElement('div');
            wrapper.classList.add("ep");
            wrapper.style.flexDirection = "row";
            // set the wrapper as child (instead of the element)
            parent.replaceChild(wrapper, ec_active);
            // set element as child of wrapper
            wrapper.appendChild(ec_active);


            ec = document.createElement("div");
            ec.classList.add("ec");
            
            var gv = document.createElement("div");
            gv.classList.add("gutter-hor");

            ec_active.parentElement.appendChild(gv);
            ec_active.parentElement.appendChild(ec);
        }else if(ec_active.parentElement.style.flexDirection == "row"){
            ec = document.createElement("div");
            ec.classList.add("ec");
            
            var gv = document.createElement("div");
            gv.classList.add("gutter-hor");

            ec_active.parentElement.appendChild(gv);
            ec_active.parentElement.appendChild(ec);

            // resizeEC(ec_active, ec);
        }else{
            ec = document.createElement("div");
            ec.classList.add("ec");
            
            var gv = document.createElement("div");
            gv.classList.add("gutter-hor");

            ec_active.parentElement.style.flexDirection = "row";

            ec_active.parentElement.appendChild(gv);
            ec_active.parentElement.appendChild(ec);

            // resizeEC(ec_active, ec);
        }
    }
}


document.addEventListener("click", (e)=>{
    ec_active = e.target;
});

function refreshGutters(){
    var gutters_hor = document.querySelectorAll(".gutter-hor");
    gutters_hor.forEach((gutter_hor)=>{
        gutter_hor.onmousedown = (e)=>{
            var elm_prev_gutter = gutter_hor.previousElementSibling;
            var elm_next_gutter = gutter_hor.nextElementSibling;

            var elm_prev_gutter_width = $(elm_prev_gutter).width();
            var elm_next_gutter_width = $(elm_next_gutter).width();
            
            init_pos = gutter_hor.getBoundingClientRect().x;
            init_mouse_pos = e.clientX;
            gutter_hor.onmousemove = (e)=>{
                if(e.clientX > init_mouse_pos){
                    gutter_hor.style.left = e.clientX;
                }
            };
        };
    });
}

I tried using the split.js library and also jquery-ui but wasn't able to achieve the desired result.


Solution

  • When mousedown on border you enter a certain “resizing” state, where you register mousemove handler on window to capture mouse dx/dy movement, also register on window a mouseup handler to cleanup.

    Upon entering this state on mousedown, you capture parent.getBoundingClientRect() so you can calculate dx/dy movement relative to parent dimensions. I’ll use horizontal dx movement as example.

    My method is to calc dx over parent width, to get % delta. I also use flex-grow number to layout sibling panes.

    Say you have 3 panes, each would have flex-grow: 33.33 all added up to 100 roughly. And you have % delta, now you can distribute this delta among two adjacent sibling panes. E.g. for each 1% delta, one adds 1, the other minuses 1, that’s the idea.

    This method is not pixel perfect, but precision is totally acceptable. I like it because it’s simpler considering other case like whole window resize. Of course if you want pixel perfection, you can calculate px width of each pane instead of flex-grow % change, but then you need to address window/parent resize, it’s your call, the basic idea is the same.

    When you’re done, by handling mouseup, don’t forget to cleanup all these temporary state variables and event handlers.