superdesk/superdesk-client-core

View on GitHub
scripts/core/editor3/components/HighlightsPopupPositioner.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import React from 'react';

interface IProps {
    children: any;
    editorNode: any;
}

// do calculations on a cloned element to avoid scroll position jumping to the top on chrome
// when resetting `max-height` to an empty value
function getNextPosition(originalElement, editorNode) {
    const element = originalElement.cloneNode(true) as HTMLElement;
    const mainFlexElement = element.firstElementChild as HTMLElement;

    originalElement.insertAdjacentElement('beforebegin', element);

    if (editorNode == null || element == null || mainFlexElement == null) {
        return;
    }

    const selectionRect = JSON.parse(editorNode.dataset.editorSelectionRect);

    if (selectionRect == null) {
        return;
    }

    // reset calculated values so new calculation can be performed
    element.style.bottom = '';
    mainFlexElement.style['max-height'] = '';

    element.style.left = (editorNode.getBoundingClientRect().left - 360) + 'px';

    const paddingBlockStart = 4;
    const paddingBlockEnd = 4;
    const viewportHeight = $(window).innerHeight();
    const remainingSpaceAtTheBottomOfSelectedText = viewportHeight - selectionRect.top;

    element.style.bottom = (
        remainingSpaceAtTheBottomOfSelectedText > element.offsetHeight
            ? Math.max(remainingSpaceAtTheBottomOfSelectedText - element.offsetHeight, paddingBlockEnd)
            : paddingBlockEnd
    ) + 'px';
    mainFlexElement.style['max-height'] = (
        viewportHeight
        - parseInt(element.style.bottom, 10)
        - paddingBlockStart
    ) + 'px';

    const result = {
        left: element.style.left,
        bottom: element.style.bottom,
        maxHeight: mainFlexElement.style['max-height'],
    };

    element.remove();

    return result;
}

export class HighlightsPopupPositioner extends React.Component<IProps> {
    static propTypes: any;
    static defaultProps: any;

    animationTimer: any;
    highlightsPopupRootElement: any;
    highlightsPopupMainFlexElement: any;

    constructor(props) {
        super(props);
        this.animationTimer = null;
        this.position = this.position.bind(this);
        this.positionOnNextAnimationFrame = this.positionOnNextAnimationFrame.bind(this);
    }
    positionOnNextAnimationFrame() {
        if (this.animationTimer != null) {
            window.cancelAnimationFrame(this.animationTimer);
        }

        const {position} = this;

        this.animationTimer = window.requestAnimationFrame(() => {
            this.animationTimer = null;
            position();
        });
    }
    position() {
        const {editorNode} = this.props;

        const element = this.highlightsPopupRootElement;
        const mainFlexElement = this.highlightsPopupMainFlexElement;

        if (editorNode == null || element == null || mainFlexElement == null) {
            return;
        }

        const selectionRect = JSON.parse(editorNode.dataset.editorSelectionRect);

        if (selectionRect == null) {
            return;
        }

        const nextPosition = getNextPosition(element, editorNode);

        element.style.left = nextPosition.left;
        element.style.bottom = nextPosition.bottom;
        mainFlexElement.style['max-height'] = nextPosition.maxHeight;
    }
    componentDidMount() {
        this.position();

        window.addEventListener('keydown', this.positionOnNextAnimationFrame);
        window.addEventListener('resize', this.positionOnNextAnimationFrame);
        window.addEventListener('click', this.positionOnNextAnimationFrame);
    }
    componentDidUpdate() {
        this.position();
    }
    componentWillUnmount() {
        window.removeEventListener('keydown', this.positionOnNextAnimationFrame);
        window.removeEventListener('resize', this.positionOnNextAnimationFrame);
        window.removeEventListener('click', this.positionOnNextAnimationFrame);
    }
    render() {
        return (
            <div
                className={'editor-popup editor-popup--open '}
                ref={(el) => {
                    this.highlightsPopupRootElement = el;
                }}
            >
                <div
                    className="editor-popup__main editor-popup__main--floating"
                    ref={(el) => {
                        this.highlightsPopupMainFlexElement = el;
                    }}
                >
                    {this.props.children}
                </div>
            </div>
        );
    }
}