12

I'm trying to come up with an effective way of defining a nested flexbox and allow it to be resized. I think it's almost there:

http://jsfiddle.net/6j10L3x2/1/

I'm using three custom elements purely to make the mark-up more declarative:

flex, flex-item, flex-resizer

A flex represents the container. A flex-item presents an element within the container, and flex-resizer represents a resizer widget which can be placed between two flex-items to add resizing functionality between them.

This all appears to work really well. However, it only handles items sized with flex-grow. If flex-shrink or flex-basis is defined, then the calculations simply don't work.

Can anyone suggest a way to amend this to allow it work for all cases? I realise that there is some ambiguity in regards to how the space should be shared between items with various flex configurations, but any input would be welcome.

Any alternative approaches would be welcome also. Thanks.

Barguast
  • 5,926
  • 9
  • 43
  • 73
  • It works well until overflow is changed to auto in a flex-item which has content that overflow; I think you should change 'scrollHeight' and 'scrollWidth' to 'offsetHeight' and 'offsetWidth' repectively for manageResize() works as expected when there is content inside a flex-item. – fender0ne Aug 27 '20 at 23:16

2 Answers2

10

Wow. I am impressed how you resize the flexbox elements with vanilla javascript using 'flexGrow', excelent idea and code.

I have improve your code in a few ways and it is working very well.

What I did?

1.- I simplified the HTML:

  • Do not use a flex element inside a flex-item.

  • Use a flex or flex-item element, always!, inside another flex element.

2.- Solved! Splitter's jump when the visible flex-item size is smaller that its content size.

3.- I'd added different cursors to signal a state's change (setupResizerEvents, onMouseUp) to improve usability.

4.- I've added code to prevent the cursor from flickering when dragging.

5.- use of offsetWidth and offsetHeight in manageResize() versus scrollWidth and scrollHeight to avoid splitter's jump on resize when a flex-item content overflow (overflow: auto).

Here is the code:

function manageResize(md, sizeProp, posProp) {
    var r = md.target;

    var prev = r.previousElementSibling;
    var next = r.nextElementSibling;
    if (!prev || !next) {
        return;
    }

    md.preventDefault();

    var prevSize = prev[sizeProp];
    var nextSize = next[sizeProp];
    var sumSize = prevSize + nextSize;
    var prevGrow = Number(prev.style.flexGrow);
    var nextGrow = Number(next.style.flexGrow);
    var sumGrow = prevGrow + nextGrow;
    var lastPos = md[posProp];

    function onMouseMove(mm) {
        var pos = mm[posProp];
        var d = pos - lastPos;
        prevSize += d;
        nextSize -= d;
        if (prevSize < 0) {
            nextSize += prevSize;
            pos -= prevSize;
            prevSize = 0;
        }
        if (nextSize < 0) {
            prevSize += nextSize;
            pos += nextSize;
            nextSize = 0;
        }

        var prevGrowNew = sumGrow * (prevSize / sumSize);
        var nextGrowNew = sumGrow * (nextSize / sumSize);

        prev.style.flexGrow = prevGrowNew;
        next.style.flexGrow = nextGrowNew;

        lastPos = pos;
    }

    function onMouseUp(mu) {
        // Change cursor to signal a state's change: stop resizing.
        const html = document.querySelector('html');
        html.style.cursor = 'default';

        if (posProp === 'pageX') {
            r.style.cursor = 'ew-resize'; 
        } else {
            r.style.cursor = 'ns-resize';
        }
        
        window.removeEventListener("mousemove", onMouseMove);
        window.removeEventListener("mouseup", onMouseUp);
    }

    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup", onMouseUp);
}

function setupResizerEvents() {
    document.body.addEventListener("mousedown", function (md) {

        // Used to avoid cursor's flickering
        const html = document.querySelector('html');
        
        var target = md.target;
        if (target.nodeType !== 1 || target.tagName !== "FLEX-RESIZER") {
            return;
        }
        var parent = target.parentNode;
        var h = parent.classList.contains("h");
        var v = parent.classList.contains("v");
        if (h && v) {
            return;
        } else if (h) {
            // Change cursor to signal a state's change: begin resizing on H.
            target.style.cursor = 'col-resize';
            html.style.cursor = 'col-resize'; // avoid cursor's flickering

            // use offsetWidth versus scrollWidth (and clientWidth) to avoid splitter's jump on resize when a flex-item content overflow (overflow: auto).
            manageResize(md, "offsetWidth", "pageX");
            
        } else if (v) {
            // Change cursor to signal a state's change: begin resizing on V.
            target.style.cursor = 'row-resize';
            html.style.cursor = 'row-resize'; // avoid cursor's flickering

            manageResize(md, "offsetHeight", "pageY");
        }
    });
}

setupResizerEvents();
body {
    /* margin:0; */
    border: 10px solid #aaa;
}

flex {
    display: flex;
    overflow: hidden;
}

/* flex-item > flex {
    position: absolute;
    width: 100%;
    height: 100%;
} */

flex.h {
    flex-direction: row;
}

flex.v {
    flex-direction: column;
}

flex-item {
    /* display: flex; */
    /* position: relative; */
    /* overflow: hidden; */
    overflow: auto;
}

flex > flex-resizer {
    flex: 0 0 10px;
    /* background: white; */
    background-color: #aaa;
    background-repeat: no-repeat;
    background-position: center;
}

flex.h > flex-resizer {
    cursor: ew-resize;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='30'><path d='M2 0 v30 M5 0 v30 M8 0 v30' fill='none' stroke='black'/></svg>");
}

flex.v > flex-resizer {
    cursor: ns-resize;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='30' height='10'><path d='M0 2 h30 M0 5 h30 M0 8 h30' fill='none' stroke='black'/></svg>");
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>flex-splitter</title>
    <link rel="stylesheet" href="./src/styles.css">
    <script src="./src/index.js" defer></script>
</head>

<body>
    <flex class="v" style="flex: 1; height: 500px;">
        <flex-item style="flex: 1;">Flex 1</flex-item>
        <flex-resizer></flex-resizer>
        <flex class="h" style="flex: 1;">
            <flex-item style="flex: 1; background-color: aqua;">
      
      <!-- 
        The next section is an example to test the splitter when there is content inside a flex-item
      -->
        <section>
                    <div>
                        <label for="CursorCoor" style="display: block;">showCursorCoor: </label>
                        <textarea id="CursorCoor" rows="6" cols="50" wrap="soft" readonly></textarea>
                    </div>
                
                    <br />
                
                    <div>
                        <label for="boxInfo" style="display: block;">showBoxInfo: </label>
                        <textarea id="boxInfo" rows="6" cols="50" wrap="soft" readonly></textarea>
                    </div>
                </section>
        
      </flex-item>
            <flex-resizer></flex-resizer>
            <flex class="v" style="flex: 2; ">
                <flex-item style="flex: 1; background: pink;">Flex 3</flex-item>
                <flex-resizer></flex-resizer>
                <flex class="h" style="flex: 1">
                    <flex-item style="flex: 1; background: green;">Flex 4</flex-item>
                    <flex-resizer></flex-resizer>
                    <flex-item style="flex: 2;">Flex 5</flex-item>
                    <!-- <flex-resizer></flex-resizer> -->
                    <flex-item style="flex: 3; background: darkorange;">Flex 6</flex-item>
                </flex>
            </flex>
        </flex>
    </flex>
    
</body>
</html>

Or see it on Codesandbox:

Edit sad-butterfly-1fwo4

I hope it helps!

fender0ne
  • 281
  • 4
  • 13
  • why are you using flex html tags - are they valid html ? – joedotnot Jul 25 '20 at 19:37
  • @joedotnot I could use a simple
    element but I just follow the original code from the main question. Anyway, in this case the tag is an standalone autonomous custom element (see https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)
    – fender0ne Aug 17 '20 at 16:33
  • wow. This is amazing. Do you think you can rewrite this in VueJs? If you can and can also be bothered to do, I could start a new question? – matt Oct 24 '22 at 22:18
  • Is there any way to make the resizer look 1 px wide but still have the 'grab-able' area 10px wide? – matt Oct 26 '22 at 01:49
3

Note: There is also the new, basic resize CSS property, but it's only for bottom right corner dragging.


I did some research on this, and the first 3 framework-free, fully baked results I came across were, in order of appearances (untested):

  1. https://daybrush.com/moveable

    • "Moveable is Draggable, Resizable, Scalable, Rotatable, Warpable, Pinchable, Groupable, Snappable"
    • I love the look of things here, both visually and code-wise! Seems highly functional and extremely flexible as well.
    • See also: https://github.com/daybrush/moveable https://daybrush.com/moveable/release/latest/doc
    • UPDATE: I tried this one out, I actually would not recommend it. It's very complicated to use, poorly documented, and I'd rather write my own JS instead.
  2. https://split.js.org

  3. https://jspanel.de

I also found this: http://w2ui.com/web/home https://github.com/vitmalina/w2ui

Andrew
  • 5,839
  • 1
  • 51
  • 72