MFPanels / 1.7 / js / mfpanel.js
file last updated : 2023-06-06
function MFPanels(_useCookies) {
    /*
    Important: All divs involved should have box-sizing:border-box set at the css (so that padding is included in the widths). If this is not set then the class will fix it manually
    Note, if the available space for a panel is less than the padding on that panel then the panel might fall underneath where it is expected.
     */
    if ( _useCookies==undefined ) _useCookies=false;
    var _paneldragger = null, _paneldragMin, _paneldragMax;
    //-----------------------------------------------------------------------------------------
    function doResetPanels() {
        if ( _useCookies ) {
            document.cookie = "urlDivSizes=; Max-Age=0;path="+document.location.pathname;
            console.log('reset urlDivSizes cookies setting');
        }
        else {
            localStorage.removeItem('urlDivSizes');
            console.log('reset urlDivSizes localStorage setting');
        }
    }
    //-----------------------------------------------------------------------------------------
    function getUniqueID(prefix) {
        var n = 1;
        while (document.getElementById(prefix + n)) n++;
        return prefix + n;
    }
    //-----------------------------------------------------------------------------------------
    function doSaveDimensions() {
        // Get the divs on the page
        const divs = document.querySelectorAll('div');

        // Create an object to store width and height values for each div on this URL
        const urlDivSizes = {};

        // Loop through each div and save its width and height to the object
        divs.forEach((div, index) => {
            const divId = div.id; // `div-${index}`; // Generate a unique ID for each div
            const width = div.style.width;
            const height = div.style.height;

            if ( width.endsWith("%") || height.endsWith("%") ) {
                // Add the div width and height to the object for this URL
                if (!urlDivSizes[divId]) {
                    urlDivSizes[divId] = {};
                }
                urlDivSizes[divId] = {width, height};
            }
        });

        // Save the urlDivSizes object to local storage
        let v=JSON.stringify(urlDivSizes);
        if ( _useCookies ) document.cookie = "urlDivSizes="+encodeURIComponent(v)+";expires=0;path="+document.location.pathname+";";
        else localStorage.setItem('urlDivSizes', v);
        //console.log('saved '+JSON.stringify(urlDivSizes));
    }
    //-----------------------------------------------------------------------------------------
    function getElemInfo(elem) {
        let result=elem.tagName;
        if ( elem.id ) result+='#'+elem.id;
        if ( elem.className ) result+='.'+elem.className;
        return '['+result+']';
    }
    //-----------------------------------------------------------------------------------------
    function doRestoreDimensions() {
        // Retrieve the urlDivSizes object from local storage
        let urlDivSizesString;
        if ( _useCookies ) {
            //var match = document.cookie.split(";");
            let match = document.cookie.match(RegExp('(?:^|;\\s*)' + 'urlDivSizes' + '=([^;]*)'));
            urlDivSizesString = match ? decodeURIComponent(match[0].substr(match[0].indexOf("=")+1)) : null;
        }
        else urlDivSizesString = localStorage.getItem('urlDivSizes');
        if ( !urlDivSizesString ) return;
        let urlDivSizes = urlDivSizesString ? JSON.parse(urlDivSizesString) : {};

        // Loop through each div and set its width and height from saved dimensions (if available)
        let sum=0;
        Object.keys(urlDivSizes).forEach((divId) => {
            let { width, height } = urlDivSizes[divId];
            let panel = document.getElementById(divId);
            if (panel) {
                let container = panel.parentElement;
                // let containerStyles = window.getComputedStyle(container); // Get the computed styles of the container
                // let containerWidth = container.offsetWidth - parseFloat(containerStyles.paddingLeft) - parseFloat(containerStyles.paddingRight); // Calculate the container width without padding
                // let containerHeight = container.offsetHeight - parseFloat(containerStyles.paddingTop) - parseFloat(containerStyles.paddingBottom); // Calculate the container width without padding
                // console.log('- restoreDimensions '+getElemInfo(panel)+' = w:'+width+' / h:'+height);
                //
                // let w0=panel.offsetWidth; // getBoundingClientRect().width;
                // let h0=panel.offsetHeight; // getBoundingClientRect().height;

                panel.style.width = width;
                panel.style.height = height;

                // let w1=panel.offsetWidth; // getBoundingClientRect().width;
                // let h1=panel.offsetHeight; // getBoundingClientRect().height;

                // let nextp = panel.nextElementSibling;
                // let adjustW = (w1 - w0);
                // let adjustH = (h1 - h0);
                // if (nextp) {
                //     console.log('- restoreDimensions nextpanel '+getElemInfo(nextp));
                //     nextp.style.width = ((nextp.offsetWidth - adjustW) / containerWidth * 100) + '%';
                //     nextp.style.height = ((nextp.offsetHeight - adjustH)/ containerHeight * 100) + '%';
                //     console.log('- restoreDimensions nextpanel.width = '+nextp.style.width);
                // }
            }
        });
    }
    //-----------------------------------------------------------------------------------------
    function doRepositionDividers() {
        let draggers = document.getElementsByClassName('paneldragger');
        for (let d = 0; d < draggers.length; d++) {
            let panel = document.getElementById(draggers[d].getAttribute('data-elem'));
            let container=panel.parentElement;
            if (/\bdragger-h\b/.test(draggers[d].classList)) {
                let w=parseInt(draggers[d].getBoundingClientRect().width);
                draggers[d].style.top = (panel.getBoundingClientRect().top) + 'px';
                draggers[d].style.left = (panel.getBoundingClientRect().left + panel.getBoundingClientRect().width - w/2) + 'px';
                draggers[d].style.height = (panel.getBoundingClientRect().height) + 'px';
            }
            if (/\bdragger-v\b/.test(draggers[d].classList)) {
                let h=parseInt(draggers[d].getBoundingClientRect().height);
                draggers[d].style.left = (panel.getBoundingClientRect().left) + 'px';
                draggers[d].style.top = (panel.getBoundingClientRect().top + panel.getBoundingClientRect().height - h/2) + 'px';
                draggers[d].style.width = (+panel.getBoundingClientRect().width) + 'px';
            }
        }
    }
    //-----------------------------------------------------------------------------------------
    function isnull(tx) {
        if ( ! tx ) return "[null]";
        return tx;
    }
    //-----------------------------------------------------------------------------------------
    function removePX(x){ // Return "a" from inside "apx"
        if ( x=='' ) return parseInt(0);
        let x2=''+x;
        return parseInt(x2.replace(/px/, ''),10);
    }
    //-----------------------------------------------------------------------------------------
    function getComputedStyleX(elem,attr,doRemovePX,nullsAreZero) {
        // get the computed style setting for an element.
        // Note, this can return the value (inc 'px'), or [auto] or [null] or ""
        // go directly to elem.getBoundingClientRect().left|top|width|height|bottom|right

        if ( doRemovePX==undefined ) doRemovePX=true;
        if ( nullsAreZero==undefined ) nullsAreZero=false;
        if ( typeof(elem)=='string' ) {
            if (startsWith(elem, '.')) {
                return getComputedStyleX(dge(elem,0), attr, doRemovePX,nullsAreZero);
            }
            else {
                elem=dge(elem);
            }
        }
        if ( !elem ) return "Null given in getComputedStyleX(NULL,'"+attr+"')";

        let style = window.getComputedStyle(elem);
        let result = style.getPropertyValue(attr);
        if ( doRemovePX && result.indexOf('px')>-1 ) {
            result=parseFloat(removePX(result));
            if ( !result && nullsAreZero ) return 0;
            else return isnull(result);
        }
        if ( !result && nullsAreZero ) return 0;
        return isnull(result);
    }
    //-----------------------------------------------------------------------------------------
    function hasClassName(elem, classNm) {
        const regex = new RegExp('\\b' + classNm + '\\b');
        return regex.test(elem.classList);
    }
    //-----------------------------------------------------------------------------------------
    function setBoxBorder(container) {

        let bs=getComputedStyleX(container, 'box-sizing');
        if ( bs!=='border-box' ) {
            console.warn('mfpanel fixing box-sizing for div.'+getElemInfo(container)+' (from '+bs+')'); // Ideally this is done by the developer and therefore not required
            container.style.boxSizing='border-box';
        }

        var panels=container.children;
        for(let p=0; p<panels.length; p++) {
            let bs=getComputedStyleX(panels[p], 'box-sizing');
            if ( bs!=='border-box' ) {
                console.warn('mfpanel fixing box-sizing for div.'+getElemInfo(panels[p])+' (from '+bs+')'); // Ideally this is done by the developer and therefore not required
                panels[p].style.boxSizing='border-box';
            }
        }
    }
    //-----------------------------------------------------------------------------------------
    function setNextAtt(draggers) {
        // Filter the elements to include only the ones with the required classes
        var elements = Array.from(draggers).filter(function(element) {
            return element.classList.contains('paneldragger') && (element.classList.contains('dragger-v') || element.classList.contains('dragger-h'));
        });

        // Iterate through each element
        elements.forEach(function(element) {
            var rect = element.getBoundingClientRect();
            var currentPosition = element.classList.contains('dragger-h') ? rect.top : rect.left;

            var closestLeftElement = null;
            var closestRightElement = null;
            var closestAboveElement = null;
            var closestBelowElement = null;

            elements.forEach(function(el) {
                if (el === element) return;
                var elRect = el.getBoundingClientRect();
                var elPosition = element.classList.contains('dragger-h') ? elRect.top : elRect.left;

                if (elPosition === currentPosition) {
                    if (el.classList.contains('dragger-h')) {
                        if (elRect.left < rect.left && (!closestLeftElement || elRect.left > closestLeftElement.getBoundingClientRect().left)) {
                            closestLeftElement = el;
                        } else if (elRect.left > rect.left && (!closestRightElement || elRect.left < closestRightElement.getBoundingClientRect().left)) {
                            closestRightElement = el;
                        }
                    } else {
                        if (elRect.top < rect.top && (!closestAboveElement || elRect.top > closestAboveElement.getBoundingClientRect().top)) {
                            closestAboveElement = el;
                        } else if (elRect.top > rect.top && (!closestBelowElement || elRect.top < closestBelowElement.getBoundingClientRect().top)) {
                            closestBelowElement = el;
                        }
                    }
                }
            });

            if ( closestLeftElement ) element.setAttribute('data-dragger-left', closestLeftElement.id);
            if ( closestRightElement ) element.setAttribute('data-dragger-right', closestRightElement.id);
            if ( closestAboveElement ) element.setAttribute('data-dragger-above', closestAboveElement.id);
            if ( closestBelowElement ) element.setAttribute('data-dragger-below', closestBelowElement.id);
        });
    }
    //-----------------------------------------------------------------------------------------
    window.addEventListener('resize', function () {
        doRepositionDividers();
    });
    window.addEventListener('scroll', function () {
        doRepositionDividers();
    });

    return {
        init: function(restoreSizesFg) {
            let container, panel, w, dragger, nextp, i, h, d, pw0, pw1, ph0, ph1, adjuster, pl, pr, pt, pb;

            if ( restoreSizesFg==undefined ) restoreSizesFg=false;

            if ( 1 ) {
                let panels = document.getElementsByClassName('resizable-e');
                adjuster=0;
                for (i = 0; i < panels.length; i++) {
                    panel = panels[i];
                    if (!hasClassName(panel, 'noselect')) {

                        container = panel.parentElement;
                        nextp = panel.nextElementSibling;
                        setBoxBorder(container);

                        let containerStyles = window.getComputedStyle(container);
                        let containerWidth = container.offsetWidth - parseFloat(containerStyles.paddingLeft) - parseFloat(containerStyles.paddingRight);

                        console.log('\n- init container '+getElemInfo(container)+ ' width = '+container.style.width+' / '+getComputedStyleX(container, 'width')+' = '+getElemInfo(container)+'.width is '+container.getBoundingClientRect().width);
                        console.log('- init panel is '+getElemInfo(panel));
                        if (!panel.id) panel.id = getUniqueID('left');
                        dragger = document.createElement('div');
                        dragger.className = 'paneldragger dragger-h';

                        dragger.style.top = panel.getBoundingClientRect().top + 'px';
                        dragger.style.height = (container.getBoundingClientRect().height) + 'px';
                        dragger.setAttribute('data-elem', panel.id);
                        dragger.id = getUniqueID('dragger');
                        document.body.appendChild(dragger);

                        w = parseInt(dragger.getBoundingClientRect().width);
                        pw0 = panel.offsetWidth;
                        dragger.style.left = (panel.getBoundingClientRect().left + panel.getBoundingClientRect().width - w / 2) + 'px';
                        panel.setAttribute('data-dragger', dragger.id);

                        panel.style.width = ((panel.offsetWidth / containerWidth) * 100) + '%';
                        console.log('- init panel.'+getElemInfo(panel)+'.width='+panel.style.width);

                        if (nextp) {
                            if ( !nextp.id ) nextp.id=getUniqueID("left");
                            pw1 = panel.getBoundingClientRect().width;
                            pw1 = panel.offsetWidth;
                            adjuster = pw1 - pw0;

                            nextp.style.width = (((nextp.offsetWidth - adjuster )/ containerWidth) * 100) + '%';
                            console.log('- init nextpanel'+getElemInfo(nextp)+'.width = '+nextp.style.width);
                        }
                    }
                }


                panels = document.getElementsByClassName('resizable-s');
                adjuster=0;
                for (i = 0; i < panels.length; i++) {
                    panel = panels[i];
                    let panelStyles = window.getComputedStyle(panel);
                    if (!hasClassName(panel, 'noselect')) {

                        container = panel.parentElement;
                        setBoxBorder(container);
                        if (!panel.id) panel.id = getUniqueID('top');
                        dragger = document.createElement('div');
                        dragger.className = 'paneldragger dragger-v';

                        dragger.style.left = panel.getBoundingClientRect().left + 'px';
                        dragger.style.width = container.getBoundingClientRect().width + 'px';
                        dragger.setAttribute('data-elem', panel.id);
                        dragger.id = getUniqueID('dragger');
                        document.body.appendChild(dragger);

                        h = parseInt(dragger.getBoundingClientRect().height);
                        ph0 = panel.offsetHeight;
                        dragger.style.top = (panel.getBoundingClientRect().top + panel.getBoundingClientRect().height - h / 2) + 'px';
                        panel.setAttribute('data-dragger', dragger.id);

                        panel.style.height = ((panel.getBoundingClientRect().height) / container.getBoundingClientRect().height * 100) + '%';
                        console.log('- init panel.'+getElemInfo(panel)+'.height='+panel.style.height);
                        nextp = panel.nextElementSibling;

                        if (nextp) {
                            if ( !nextp.id ) nextp.id=getUniqueID("top");
                            ph1 = panel.getBoundingClientRect().height;
                            adjuster = ph1 - ph0;
                            nextp.style.height = ((nextp.getBoundingClientRect().height - adjuster) / container.getBoundingClientRect().height * 100) + '%';
                        }
                    }
                }
            }


            let draggers = document.getElementsByClassName('paneldragger');


            // Set up attributes pointing to the neighbour draggers
            setNextAtt(draggers);


            let gap=30;
            for (d = 0; d < draggers.length; d++) {
                draggers[d].addEventListener('mousedown', function (e) {
                    let dragger = e.target;
                    if (/\bdragger-h\b/.test(dragger.classList)) {
                        _paneldragger = dragger;
                        let x = _paneldragger.getBoundingClientRect().left;
                        _paneldragger.setAttribute('data-startdrag', x);
                        _paneldragger.setAttribute('data-dragging', 1);

                        let leftElem = document.getElementById(_paneldragger.getAttribute('data-elem'));
                        let rightElem = leftElem.nextElementSibling;
                        let padding = getComputedStyleX(leftElem, 'padding-left', true, true) + getComputedStyleX(leftElem, 'padding-right', true, true);


                        let dragLeftId = _paneldragger.getAttribute('data-dragger-left');
                        if ( dragLeftId ) _paneldragMin = document.getElementById(dragLeftId).getBoundingClientRect().right + gap;
                        else _paneldragMin = leftElem.getBoundingClientRect().left + gap;

                        let dragRightId = _paneldragger.getAttribute('data-dragger-right');
                        if ( dragRightId ) _paneldragMax = document.getElementById(dragRightId).getBoundingClientRect().left - gap;
                        else _paneldragMax = rightElem.getBoundingClientRect().right - gap;

                        console.log('prev='+dragLeftId+', next='+dragRightId+', min='+_paneldragMin+', max='+_paneldragMax+', padding='+padding);
                    }
                    if (/\bdragger-v\b/.test(dragger.classList)) {
                        _paneldragger = dragger;
                        let y = _paneldragger.getBoundingClientRect().top;
                        _paneldragger.setAttribute('data-startdrag', y);
                        _paneldragger.setAttribute('data-dragging', 1);

                        let topElem = document.getElementById(_paneldragger.getAttribute('data-elem'));
                        let bottomElem = topElem.nextElementSibling;
                        let padding = getComputedStyleX(topElem, 'padding-top', true, true) + getComputedStyleX(topElem, 'padding-bottom', true, true);

                        let dragTopId = _paneldragger.getAttribute('data-dragger-above');
                        if ( dragTopId ) _paneldragMin = document.getElementById(dragTopId).getBoundingClientRect().bottom + gap;
                        else _paneldragMin = topElem.getBoundingClientRect().top + gap;

                        let dragBottomId = _paneldragger.getAttribute('data-dragger-below');
                        if ( dragBottomId ) _paneldragMax = document.getElementById(dragBottomId).getBoundingClientRect().top - gap;
                        else _paneldragMax = bottomElem.getBoundingClientRect().bottom - gap;
                    }
                });
            }

            if ( restoreSizesFg ) {
                doRestoreDimensions();
                doRepositionDividers();
            }

            window.addEventListener('mousemove', function (e) {
                if (_paneldragger) {
                    if (/\bdragger-h\b/.test(_paneldragger.classList)) {
                        let x = parseInt(e.clientX);
                        if (x > _paneldragMin && x < _paneldragMax) {
                            if (_paneldragger.getAttribute('data-dragging') == "1" && x > 0) {
                                let w=parseInt(_paneldragger.getBoundingClientRect().width);
                                _paneldragger.style.left = (x - w/2) + 'px';
                            }
                        }
                        let selection = window.getSelection();
                        if (selection.rangeCount > 0) selection.removeAllRanges();
                    }
                    if (/\bdragger-v\b/.test(_paneldragger.classList)) {
                        let y = parseInt(e.clientY);
                        if (y > _paneldragMin && y < _paneldragMax) {
                            if (_paneldragger.getAttribute('data-dragging') == "1" && y > 0) {
                                let h=parseInt(_paneldragger.getBoundingClientRect().height);
                                _paneldragger.style.top = (y - h/2) + 'px';
                            }
                        }
                        let selection = window.getSelection();
                        if (selection.rangeCount > 0) selection.removeAllRanges();

                    }
                }
            });

            window.addEventListener('mouseup', function (e) {
                if (_paneldragger) {
                    if (/\bdragger-h\b/.test(_paneldragger.classList)) {
                        // Horizontal moves
                        let leftElem = document.getElementById(_paneldragger.getAttribute('data-elem'));
                        let rightElem = leftElem.nextElementSibling;

                        let container=leftElem.parentElement; // getPanelsContainer(elem);
                        let containerStyles = window.getComputedStyle(container); // Get the computed styles of the container
                        let containerWidth = container.offsetWidth - parseFloat(containerStyles.paddingLeft) - parseFloat(containerStyles.paddingRight); // Calculate the container width without padding
                        _paneldragger.setAttribute('data-dragging', 0);

                        let x0 = _paneldragger.getAttribute('data-startdrag');
                        _paneldragger.removeAttribute('data-startdrag');

                        let x = _paneldragger.getBoundingClientRect().left;
                        let adjustPct = ((x - x0) * 100) / containerWidth;

                        if (/px/.test(leftElem.style.width)) console.error('PANEL ' + getElemInfo(leftElem) + ' width must be expressed in % and not px');

                        let leftwPct0 = parseFloat(leftElem.style.width.replace(/%/, ''));
                        let rightwPct0 = parseFloat(rightElem.style.width.replace(/%/, ''));

                        let leftwPct  = leftwPct0  + adjustPct;
                        let rightwPct = rightwPct0 - adjustPct;

                        leftElem.style.width = leftwPct + '%';
                        rightElem.style.width = rightwPct + '%';

                        // console.log('mouseup adjustment set : (('+x+' - '+x0+' ) * 100) / '+containerWidth+' = '+Math.round(adjustPct,4)+'% to '+leftElem.style.width+' ;container='+getElemInfo(container));
                        // console.log('mouseup - drop : right: from '+rightwPct0+'% to '+getElemInfo(rightElem)+'.width='+rightElem.style.width);
                        //
                        // console.log('mouseup - panel adjustment = '+adjustPct+'%');
                        // console.log('mouseup - pair width before = '+(leftwPct0+rightwPct0)+' ('+leftwPct0+'% and '+rightwPct0+'%)');
                        // console.log('mouseup - pair width after = '+(parseFloat(leftElem.style.width)+parseFloat(rightElem.style.width))+' ('+leftElem.style.width+'% and '+rightElem.style.width+'%)');

                        _paneldragger = null;
                        doRepositionDividers();
                    }
                    else if (/\bdragger-v\b/.test(_paneldragger.classList)) {
                        // Vertical moves
                        let topElem = document.getElementById(_paneldragger.getAttribute('data-elem'));
                        let bottomElem = topElem.nextElementSibling;

                        let container=topElem.parentElement; // getPanelsContainer(topElem);
                        let containerStyles = window.getComputedStyle(container); // Get the computed styles of the container
                        let containerHeight = container.offsetHeight - parseFloat(containerStyles.paddingTop) - parseFloat(containerStyles.paddingBottom); // Calculate the container height without padding
                        _paneldragger.setAttribute('data-dragging', 0);

                        let y0 = _paneldragger.getAttribute('data-startdrag');
                        _paneldragger.removeAttribute('data-startdrag');

                        let y = _paneldragger.getBoundingClientRect().top;
                        let adjustPct = ((y - y0) * 100) / containerHeight;

                        if (/px/.test(topElem.style.height)) console.error('PANEL ' + getElemInfo(topElem) + ' height must be expressed in % and not px');

                        let tophPct0 = parseFloat(topElem.style.height.replace(/%/, ''));
                        let bottomhPct0 = parseFloat(bottomElem.style.height.replace(/%/, ''));

                        let tophPct  = tophPct0  + adjustPct;
                        let bottomhPct = bottomhPct0 - adjustPct;

                        topElem.style.height = tophPct + '%';
                        bottomElem.style.height = bottomhPct + '%';

                        _paneldragger = null;
                        doRepositionDividers();
                    }
                }
                doSaveDimensions();

            });
        },
        reset: function() {
            doResetPanels()
        }
    };
};


window.addEventListener('load', function () {
    let useCookies=false;

    // Preset _MFPanelsUseCookies to true to have storage use cookies that can be
    // read by server side processing if desired.
    // e.g. let _MFPanelsUseCookies = true;
    if ( typeof _MFPanelsUseCookies !== 'undefined' ) useCookies = _MFPanelsUseCookies;
    new MFPanels(useCookies).init(true);

    // Preset _MFPanelsInitCallback to a function to be called once init is complete,
    // e.g. let _MFPanelsInitCallback=doAlert;
    if ( typeof _MFPanelsInitCallback !== 'undefined' ) _MFPanelsInitCallback();

});

About

License

Latest Release

Version 1.122024-05-08