nexxtway/react-rainbow

View on GitHub
src/libs/ResizeSensor/index.js

Summary

Maintainability
B
6 hrs
Test Coverage
/* eslint-disable no-param-reassign, no-plusplus */
import EventQueue from './eventQueue';

/**
 * Based on Marc J. Schmidt library: https://github.com/marcj/css-element-queries/blob/master
 */

/**
 * Get element size
 * @param {HTMLElement} element - element to return the size.
 * @returns {Object} {width, height}
 */
function getElementSize(element) {
    const rect = element.getBoundingClientRect();
    return {
        width: Math.round(rect.width),
        height: Math.round(rect.height),
    };
}

function createResizeSensor() {
    const resizeSensor = document.createElement('div');
    resizeSensor.dir = 'ltr';
    resizeSensor.className = 'resize-sensor';
    const style =
        'position: absolute; left: -10px; top: -10px; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;';
    const styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;';

    resizeSensor.style.cssText = style;
    resizeSensor.innerHTML =
        `<div class="resize-sensor-expand" style="${style}">` +
        `<div style="${styleChild}"></div>` +
        '</div>' +
        `<div class="resize-sensor-shrink" style="${style}">` +
        `<div style="${styleChild} width: 200%; height: 200%"></div>` +
        '</div>';

    return resizeSensor;
}

/**
 *
 * @param {HTMLElement} element - element to listen resize.
 * @param {Function}    resizeListener - resize event listener.
 */
function attachResizeEvent(element, resizeListener) {
    if (!element) {
        return;
    }
    if (element.resizedAttached) {
        element.resizedAttached.add(() => resizeListener());
        return;
    }

    element.resizedAttached = new EventQueue();
    element.resizedAttached.add(() => resizeListener());

    const resizeSensor = createResizeSensor();
    element.resizeSensor = resizeSensor;
    element.appendChild(resizeSensor);

    const position = (window.getComputedStyle(element) || element.style).getPropertyValue(
        'position',
    );

    if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
        element.style.position = 'relative';
    }

    const expand = resizeSensor.childNodes[0];
    const expandChild = expand.childNodes[0];
    const shrink = resizeSensor.childNodes[1];

    let dirty;
    let rafId;
    let size = getElementSize(element);
    let lastWidth = size.width;
    let lastHeight = size.height;
    let initialHiddenCheck = true;
    let resetRafId;

    const resetExpandShrink = () => {
        expandChild.style.width = '100000px';
        expandChild.style.height = '100000px';

        expand.scrollLeft = 100000;
        expand.scrollTop = 100000;

        shrink.scrollLeft = 100000;
        shrink.scrollTop = 100000;
    };

    const reset = () => {
        // Check if element is hidden
        if (initialHiddenCheck) {
            if (!expand.scrollTop && !expand.scrollLeft) {
                // reset
                resetExpandShrink();

                // Check in next frame
                if (!resetRafId) {
                    resetRafId = requestAnimationFrame(() => {
                        resetRafId = 0;
                        reset();
                    });
                }

                return;
            }

            initialHiddenCheck = false;
        }

        resetExpandShrink();
    };
    resizeSensor.resetSensor = reset;

    const onResized = () => {
        rafId = 0;

        if (!dirty) {
            return;
        }

        lastWidth = size.width;
        lastHeight = size.height;

        if (element.resizedAttached) {
            element.resizedAttached.call(size);
        }
    };

    const onScroll = () => {
        size = getElementSize(element);
        dirty = size.width !== lastWidth || size.height !== lastHeight;

        if (dirty && !rafId) {
            rafId = requestAnimationFrame(onResized);
        }

        reset();
    };

    const addEvent = (elem, name, callback) => {
        elem.addEventListener(name, callback);
    };

    addEvent(expand, 'scroll', onScroll);
    addEvent(shrink, 'scroll', onScroll);

    // Fix for custom Elements
    requestAnimationFrame(reset);
}

function detach(elem, listener) {
    if (!elem) {
        return;
    }
    if (elem.resizedAttached && typeof listener === 'function') {
        elem.resizedAttached.remove(listener);
        if (elem.resizedAttached.length()) {
            return;
        }
    }
    if (elem.resizeSensor) {
        if (elem.contains(elem.resizeSensor)) {
            elem.removeChild(elem.resizeSensor);
        }
        delete elem.resizeSensor;
        delete elem.resizedAttached;
    }
}

export default class ResizeSensor {
    constructor(element, resizeListener) {
        this.targetElement = element;
        this.resizeListener = resizeListener;

        attachResizeEvent(this.targetElement, this.resizeListener);
    }

    detach() {
        detach(this.targetElement, this.resizeListener);
    }

    reset() {
        this.targetElement.resizeSensor.resetSensor();
    }
}