code:
import {BB} from '../../../bb/bb';
import {dialogCounter} from '../modals/modal-count';
import toolPaintImg from '/src/app/img/ui/tool-paint.svg';
import toolFillImg from '/src/app/img/ui/tool-fill.svg';
import toolGradientImg from '/src/app/img/ui/tool-gradient.svg';
import toolTextImg from '/src/app/img/ui/tool-text.svg';
import toolShapeImg from '/src/app/img/ui/tool-shape.svg';
import caretDownImg from '/src/app/img/ui/caret-down.svg';
import {LANG} from '../../../language/language';
import {TToolType} from '../../kl-types';

type TDropdownButton = {
    wrapper: HTMLElement;
    show: (b: boolean) => void;
    setIsSmall: (b: boolean) => void;
};

/**
 * Toolrow Dropdown. The button where you select: brush, fill, select, transform, etc.
 */
export class ToolDropdown {

    private readonly rootEl: HTMLElement;
    private readonly activeButton: HTMLElement;
    private readonly smallMargin: string = '6px 0';
    private readonly optionArr: TToolType[] = ['draw', 'fill', 'gradient', 'text', 'shape'];
    private readonly dropdownBtnArr: TDropdownButton[];
    private readonly arrowButton: HTMLElement;
    private isActive: boolean;
    private currentActiveIndex: number;
    private readonly titleArr: string[];
    private activeButtonIm: HTMLElement;
    private readonly imArr;

    private updateButton () {
        this.activeButton.title = this.titleArr[this.currentActiveIndex];
        this.activeButtonIm.style.backgroundImage = 'url(\'' + this.imArr[this.currentActiveIndex] + '\')';
    }

    // ---- public ----
    constructor (
        p: {
            onChange: (activeStr: TToolType) => void;
        }
    ) {
        this.imArr = [
            toolPaintImg,
            toolFillImg,
            toolGradientImg,
            toolTextImg,
            toolShapeImg,
        ];
        this.titleArr = [
            `${LANG('tool-brush')} [B]`,
            `${LANG('tool-paint-bucket')} [G]`,
            `${LANG('tool-gradient')} [G]`,
            `${LANG('tool-text')} [T]`,
            `${LANG('tool-shape')} [U]`,
        ];
        this.currentActiveIndex = 0;
        this.isActive = true;
        let isOpen = false;

        //preload images
        setTimeout(() => {
            for (let i = 1; i < this.imArr.length; i++) {
                const im = new Image();
                im.src = this.imArr[i];
            }
        }, 100);

        this.rootEl = BB.el({
            css: {
                position: 'relative',
                flexGrow: '1',
            },
        });

        let openTimeout: ReturnType<typeof setTimeout>;
        let isDragging = false;
        let startX: number;
        let startY: number;
        let pointerListener;
        if (BB.hasPointerEvents) {
            pointerListener = new BB.PointerListener({
                target: this.rootEl,
                onPointer: (event) => {
                    if (event.type === 'pointerdown') {
                        if (isOpen) {
                            return;
                        }

                        openTimeout = setTimeout(() => {
                            showDropdown();
                        }, 400);
                        isDragging = true;
                        startX = event.pageX;
                        startY = event.pageY;

                    } else if (event.type === 'pointermove') {
                        if (isDragging && !isOpen && BB.dist(startX, startY, event.pageX, event.pageY) > 5) {
                            clearTimeout(openTimeout);
                            showDropdown();
                        }

                    } else if (event.type === 'pointerup') {
                        clearTimeout(openTimeout);
                        if (isOpen && isDragging) {
                            const target = document.elementFromPoint(event.pageX, event.pageY);
                            for (let i = 0; i < this.dropdownBtnArr.length; i++) {
                                if (target === this.dropdownBtnArr[i].wrapper) {
                                    closeDropdown();
                                    this.isActive = true;
                                    this.currentActiveIndex = i;
                                    this.updateButton();
                                    p.onChange(this.optionArr[this.currentActiveIndex]);
                                    break;
                                }
                            }
                        }
                        isDragging = false;
                    }
                },
            });
        }

        this.activeButton = BB.el({
            parent: this.rootEl,
            className: 'toolspace-row-button nohighlight toolspace-row-button-activated',
            title: this.titleArr[this.currentActiveIndex],
            onClick: (e) => {
                if (this.isActive && !isOpen) {
                    e.preventDefault();
                    e.stopPropagation();
                    showDropdown();
                    return;
                }

                this.isActive = true;
                p.onChange(this.optionArr[this.currentActiveIndex]);
                if (isOpen) {
                    closeDropdown();
                }
            },
            css: {
                padding: '10px 0',
                pointerEvents: 'auto',
                height: '100%',
                boxSizing: 'border-box',
            },
        });

        this.activeButtonIm = BB.el({
            parent: this.activeButton,
            className: 'dark-invert',
            css: {
                backgroundRepeat: 'no-repeat',
                backgroundPosition: 'center',
                backgroundSize: 'contain',
                width: 'calc(100% - 7px)',
                height: '100%',
                pointerEvents: 'none',
                opacity: '0.75',
            },
        });

        this.arrowButton = BB.el({
            parent: this.activeButton,
            className: 'dark-invert',
            css: {
                position: 'absolute',
                right: '1px',
                bottom: '1px',
                width: '18px',
                height: '18px',
                //background: '#aaa',
                //borderRadius: '2px',
                cursor: 'pointer',

                backgroundImage: 'url(\'' + caretDownImg + '\')',
                backgroundRepeat: 'no-repeat',
                backgroundPosition: 'center',
                backgroundSize: '60%',
            },
            title: 'More Tools',
            onClick: (e) => {
                e.preventDefault();
                e.stopPropagation();
                showDropdown();
            },
        });

        const overlay = BB.el({
            css: {
                position: 'absolute',
                //background: 'rgba(255,0,0,0.5)',
                left: '0',
                top: '0',
                right: '0',
                bottom: '0',
            },
        });
        const overlayPointerListener = new BB.PointerListener({
            target: overlay,
            onPointer: (e) => {
                if (e.type === 'pointerdown') {
                    e.eventPreventDefault();
                    closeDropdown();
                }
            },
        });

        const dropdownWrapper = BB.el({
            className: 'tool-dropdown-wrapper',
            css: {
                position: 'absolute',
                width: '100%',
                height: (100 * (this.optionArr.length - 1)) + '%',
                top: '100%',
                left: '0',
                zIndex: '1',
                boxSizing: 'border-box',
                cursor: 'pointer',
                transition: 'height 0.1s ease-in-out, opacity 0.1s ease-in-out',
                borderBottomLeftRadius: '5px',
                borderBottomRightRadius: '5px',
                overflow: 'hidden',
            },
        });

        this.dropdownBtnArr = [];

        const createDropdownButton = (
            p: {
                index: number;
                id: TToolType;
                image: string;
                title: string;
                onClick: (index: number) => void;
            }
        ): TDropdownButton => {
            const wrapper = BB.el({
                parent: dropdownWrapper,
                className: 'tool-dropdown-button',
                title: p.title,
                css: {
                    padding: '10px 0',
                    height: (100 / (this.optionArr.length - 1)) + '%',
                    boxSizing: 'border-box',
                },
                onClick: (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    p.onClick(p.index);
                },
            });

            BB.el({
                parent: wrapper,
                className: 'dark-invert',
                css: {
                    backgroundImage: 'url(\'' + p.image + '\')',
                    backgroundRepeat: 'no-repeat',
                    backgroundPosition: 'center',
                    backgroundSize: 'contain',
                    height: '100%',
                    pointerEvents: 'none',
                    opacity: '0.75',
                },
            });

            // --- interface ---
            const show = (b: boolean) => {
                wrapper.style.display = b ? 'block' : 'none';
            };

            const setIsSmall = (b: boolean) => {
                wrapper.style.padding = b ? this.smallMargin : '10px 0';
            };

            return {
                wrapper,
                show,
                setIsSmall,
            };
        };

        const onClickDropdownBtn = (index: number) => {
            closeDropdown();

            this.isActive = true;
            this.currentActiveIndex = index;

            this.updateButton();

            p.onChange(this.optionArr[this.currentActiveIndex]);
        };

        for (let i = 0; i < this.optionArr.length; i++) {
            this.dropdownBtnArr.push(createDropdownButton({
                index: i,
                id: this.optionArr[i],
                image: this.imArr[i],
                title: this.titleArr[i],
                onClick: onClickDropdownBtn,
            }));
        }

        const showDropdown = () => {
            dialogCounter.increase(0.5);
            isOpen = true;

            for (let i = 0; i < this.optionArr.length; i++) {
                this.dropdownBtnArr[i].show(this.currentActiveIndex !== i);
            }

            this.arrowButton.style.display = 'none';
            this.rootEl.style.zIndex = '1';
            document.body.append(overlay);
            this.rootEl.append(dropdownWrapper);
        };

        const closeDropdown = () => {
            dialogCounter.decrease(0.5);
            isOpen = false;
            this.arrowButton.style.removeProperty('display');
            this.rootEl.style.removeProperty('z-index');
            overlay.remove();
            dropdownWrapper.remove();
        };

        this.updateButton();
    }

    // ---- interface ----

    setIsSmall (b: boolean): void {
        this.activeButton.style.padding = b ? this.smallMargin : '10px 0';
        for (let i = 0; i < this.optionArr.length; i++) {
            this.dropdownBtnArr[i].setIsSmall(b);
        }
        if (b) {
            this.arrowButton.style.width = '14px';
            this.arrowButton.style.height = '14px';
        } else {
            this.arrowButton.style.width = '18px';
            this.arrowButton.style.height = '18px';
        }
    }

    setActive (activeStr: TToolType): void {
        if (this.optionArr.includes(activeStr)) {
            this.isActive = true;
            for (let i = 0; i < this.optionArr.length; i++) {
                if (this.optionArr[i] === activeStr) {
                    this.currentActiveIndex = i;
                    break;
                }
            }
            this.activeButton.classList.add('toolspace-row-button-activated');
            this.updateButton();
        } else {
            this.isActive = false;
            this.activeButton.classList.remove('toolspace-row-button-activated');
        }
    }

    getActive (): TToolType {
        return this.optionArr[this.currentActiveIndex];
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }
}


code:
import {BB} from '../../../bb/bb';
import collapseImg from '/src/app/img/ui/ui-collapse.svg';
import {LANG} from '../../../language/language';
import {TUiLayout} from '../../kl-types';

/**
 * button that allows to collapse toolspace (for mobile)
 */
export class ToolspaceCollapser {

    private readonly rootEl: HTMLElement;
    private directionStr: TUiLayout;
    private readonly icon: HTMLElement;
    private stateIsOpen: boolean;

    private update (): void {
        if (this.directionStr === 'left') {
            this.icon.style.transform = this.stateIsOpen ? 'rotate(180deg)' : '';
        } else {
            this.icon.style.transform = this.stateIsOpen ? '' : 'rotate(180deg)';
        }
    }

    // ---- public ----
    constructor (
        p: {
            onChange: () => void;
        }
    ) {
        this.stateIsOpen = true;
        this.directionStr = 'right';

        this.rootEl = BB.el({
            className: 'kl-toolspace-toggle',
            css: {
                width: '36px',
                height: '36px',
                background: 'rgba(100, 100, 100, 0.9)',
                color: '#fff',
                position: 'absolute',
                top: '0',
                textAlign: 'center',
                lineHeight: '36px',
                cursor: 'pointer',
                userSelect: 'none',
                padding: '6px',
                boxSizing: 'border-box',
            },
            title: LANG('toggle-show-tools'),
            onClick: (e) => {
                e.preventDefault();
                this.stateIsOpen = !this.stateIsOpen;
                this.update();
                p.onChange();
            },
        });

        this.icon = BB.el({
            parent: this.rootEl,
            css: {
                backgroundImage: `url(${collapseImg})`,
                width: '100%',
                height: '100%',
                backgroundSize: 'contain',
                backgroundRepeat: 'no-repeat',
                backgroundPosition: 'center',
                userSelect: 'none',
            },
        });
        this.rootEl.oncontextmenu = () => {
            return false;
        };
    }

    // ---- interface ----

    isOpen (): boolean {
        return this.stateIsOpen;
    }

    setDirection (dirStr: TUiLayout): void {
        this.directionStr = dirStr;
        this.update();
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }

}

repo_name: Doggermelon/klecks

license: mit


code:
import {BB} from '../../../bb/bb';
import {LANG} from '../../../language/language';
import {dialogCounter} from '../modals/modal-count';

/**
 * Scroll buttons at the top and bottom of toolspace
 * Show up when window is very small.
 * Allow user to scroll up and down toolspace.
 */
export class ToolspaceScroller {

    private readonly toolspace: HTMLElement;
    private readonly upBtn: HTMLElement;
    private readonly downBtn: HTMLElement;
    private downInterval: any;
    private upInterval: any;

    private update (): void {
        if (this.toolspace.scrollHeight > this.toolspace.offsetHeight + 3) { // small buffer where it's not worth it
            if (!this.upInterval) {
                this.upBtn.style.display = this.toolspace.scrollTop === 0 ? 'none' : 'block';
            }
            if (!this.downInterval) {
                this.downBtn.style.display = (
                    this.toolspace.scrollTop + this.toolspace.offsetHeight + 1 >= this.toolspace.scrollHeight ?
                        'none' : 'block'
                );
            }
        } else {
            this.upBtn.style.display = 'none';
            this.downBtn.style.display = 'none';
        }
    }

    // --- public ---
    constructor (p: {toolspace: HTMLElement; uiState: 'left' | 'right'}) {
        this.toolspace = p.toolspace;
        this.upBtn = BB.el({
            parent: this.toolspace,
            title: LANG('scroll'),
            className: 'kl-scroller',
            css: {
                top: '0',
                transform: 'rotate(180deg)',
            },
        });
        this.downBtn = BB.el({
            parent: this.toolspace,
            title: LANG('scroll'),
            className: 'kl-scroller',
            css: {
                bottom: '0',
            },
        });
        this.updateUiState(p.uiState);

        const upListener = new BB.PointerListener({
            target: this.upBtn,
            onPointer: (e) => {
                if (e.type === 'pointerdown') {
                    this.upInterval = setInterval(() => {
                        this.toolspace.scrollBy(0, -13);
                        this.update();
                    }, 20);
                }
                if (e.type === 'pointerup') {
                    clearInterval(this.upInterval);
                    setTimeout(() => { // prevent ff pressing anything underneath
                        this.upInterval = null;
                        this.update();
                    }, 50);
                }
            },
        });
        const downListener = new BB.PointerListener({
            target: this.downBtn,
            onPointer: (e) => {
                if (e.type === 'pointerdown') {
                    this.downInterval = setInterval(() => {
                        this.toolspace.scrollBy(0, 13);
                        this.update();
                    }, 20);
                }
                if (e.type === 'pointerup') {
                    clearInterval(this.downInterval);
                    setTimeout(() => { // prevent ff pressing anything underneath
                        this.downInterval = null;
                        this.update();
                    }, 50);
                }
            },
        });

        const wheelListener = (e: WheelEvent) => {
            this.toolspace.scrollBy(0, Math.round(0.7 * e.deltaY));
            this.update();
        };
        this.upBtn.addEventListener('wheel', wheelListener);
        this.downBtn.addEventListener('wheel', wheelListener);

        this.update();

        const observer = new MutationObserver(() => this.update());
        observer.observe(
            this.toolspace,
            {
                attributes: true,
                childList: true,
                subtree: true,
            }
        );
        window.addEventListener('resize', () => this.update());

        // hide if in dialog because that can have its own scrollbar
        dialogCounter.subscribe((v) => {
            // ignores the 0.5 by tool dropdown
            this.upBtn.style.opacity = v >= 1 ? '0' : '';
            this.downBtn.style.opacity = v >= 1 ? '0' : '';
        });
    }

    updateUiState (uiState: 'left' | 'right'): void {
        BB.css(this.upBtn, {
            left: uiState === 'left' ? '0' : '',
            right: uiState === 'right' ? '0' : '',
        });
        BB.css(this.downBtn, {
            left: uiState === 'left' ? '0' : '',
            right: uiState === 'right' ? '0' : '',
        });
    }
}


code:
import {BB} from '../../../bb/bb';
import {Select} from './select';
import {LANG} from '../../../language/language';
import {PointerListener} from '../../../bb/input/pointer-listener';

/**
 * Ui to select stabilizer level. 4 options. returned as 0-3
 */
export class ToolspaceStabilizerRow {

    private readonly rootEl: HTMLElement;
    private readonly pointerListener: PointerListener;

    // ---- public ----
    constructor (
        p: {
            smoothing: number; // initial level [0,3]
            onSelect: (level: number) => void; // [0-3], when level changes
        }
    ) {
        this.rootEl = BB.el({
            tagName: 'label',
            className: 'kl-stabilizer',
            content: LANG('stabilizer') + ' ',
            title: LANG('stabilizer-title'),
        });

        const strengthSelect = new Select({
            optionArr: [
                ['0', '0'],
                ['1', '1'],
                ['2', '2'],
                ['3', '3'],
                ['4', '4'],
                ['5', '5'],
            ],
            initValue: '' + p.smoothing,
            onChange: function (val) {
                p.onSelect(parseInt(val));
            },
        });
        this.rootEl.append(strengthSelect.getElement());

        this.pointerListener = new BB.PointerListener({
            target: this.rootEl,
            onWheel: function (e) {
                strengthSelect.setDeltaValue(e.deltaY);
            },
        });
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }
}


code:
import {BB} from '../../../bb/bb';
import {ToolDropdown} from './tool-dropdown';
import toolHandImg from '/src/app/img/ui/tool-hand.svg';
import toolZoomInImg from '/src/app/img/ui/tool-zoom-in.svg';
import toolZoomOutImg from '/src/app/img/ui/tool-zoom-out.svg';
import toolUndoImg from '/src/app/img/ui/tool-undo.svg';
import {LANG} from '../../../language/language';
import {PointerListener} from '../../../bb/input/pointer-listener';
import {TToolType} from '../../kl-types';

type TBaseToolRowButton = {
    el: HTMLElement;
};

type TToolRowButton = TBaseToolRowButton & {
    pointerListener: PointerListener;
    setIsSmall: (b: boolean) => void;
};

type TToolRowTriangleButton = TBaseToolRowButton & {
    setIsEnabledLeft: (b: boolean) => void;
    setIsEnabledRight: (b: boolean) => void;
    leftPointerListener: PointerListener;
    rightPointerListener: PointerListener;
};

/**
 * Row of buttons in toolspace. image-operations (draw, hand), zoom, undo/redo
 * Need to do syncing. So tool is correct, and zoom/undo/redo buttons are properly enabled/disabled
 * heights: 54px tall, 36px small -> via setIsSmall
 */
export class ToolspaceToolRow {

    private readonly rootEl: HTMLElement;
    private readonly toolDropdown: ToolDropdown;
    private readonly handButton: TToolRowButton;
    private readonly zoomInButton: TToolRowButton;
    private readonly zoomOutButton: TToolRowButton;
    private readonly undoButton: TToolRowButton;
    private readonly redoButton: TToolRowButton;
    private readonly zoomInNOutButton: TToolRowTriangleButton;
    private readonly undoNRedoButton: TToolRowTriangleButton;
    private currentActiveStr: TToolType;
    private readonly onActivate: (activeStr: TToolType) => void;

    // ---- public ----

    constructor (
        p: {
            onActivate: (activeStr: TToolType) => void; // clicking on tool button - activating it
            onZoomIn: () => void;
            onZoomOut: () => void;
            onUndo: () => void;
            onRedo: () => void;
        }
    ) {
        this.rootEl = BB.el( {
            className: 'kl-toolspace-row',
            css: {
                height: '54px',
                display: 'flex',
            },
        });

        this.onActivate = p.onActivate;
        this.currentActiveStr = 'draw'; // 'draw' | 'hand' | 'fill'

        const createButton = (p: {
            onClick: () => void;
            image: string;
            contain: boolean;
            doLighten?: boolean;
            doMirror?: boolean;
        }): TToolRowButton => {

            const smallMargin = p.doLighten ? '6px 0' : '8px 0';

            const el = BB.el({
                className: 'toolspace-row-button nohighlight',
                //title: p.title,
                onClick: p.onClick,
                css: {
                    padding: p.contain ? '10px 0' : '',
                },
            });
            const im = BB.el({
                className: 'dark-invert',
                css: {
                    backgroundImage: 'url(\'' + p.image + '\')',
                    backgroundRepeat: 'no-repeat',
                    backgroundPosition: 'center',
                    backgroundSize: p.contain ? 'contain' : '',
                    //filter: 'grayscale(1)',
                    height: '100%',
                    transform: p.doMirror ? 'scale(-1, 1)' : '',
                    pointerEvents: 'none',
                    opacity: p.doLighten ? '0.75' : '1',
                },
            });
            el.append(im);
            const pointerListener = new BB.PointerListener({ // because :hover causes problems w touch
                target: el,
                onEnterLeave: (isOver) => {
                    el.classList.toggle('toolspace-row-button-hover', isOver);
                },
            });
            const setIsSmall = (b: boolean) => {
                el.style.padding = p.contain ? (b ? smallMargin : '10px 0') : '';
            };
            return {
                el,
                pointerListener,
                setIsSmall,
            };
        };

        const createTriangleButton = (p: {
            onLeft: () => void;
            onRight: () => void;
            leftImage: string;
            rightImage: string | null;
        }): TToolRowTriangleButton => { // because IE and Edge don't support clip path

            const result = BB.el({
                css: {
                    flexGrow: '1',
                    position: 'relative',
                },
            });

            const svg = BB.createSvg({
                elementType: 'svg',
                width: '67px', // can't think of a way doing with percentage
                height: '54px',
                viewBox: '0 0 100 100',
                preserveAspectRatio: 'none',
            });
            BB.css(svg, {
                position: 'absolute',
                left: '0',
                top: '0',
            });

            const blurRadius = 10;
            const blurOffsetX = 2;
            const blurOffsetY = 2;

            const defs = BB.createSvg( { // inset shadow via svg
                elementType: 'defs',
                childrenArr: [
                    {
                        elementType: 'filter',
                        id: 'innershadow',
                        x0: '-50%',
                        y0: '-50%',
                        width: '200%',
                        height: '200%',
                        childrenArr: [
                            {
                                elementType: 'feGaussianBlur',
                                in: 'SourceAlpha',
                                stdDeviation: '' + blurRadius,
                                result: 'blur',
                            }, {
                                elementType: 'feOffset',
                                dx: '' + blurOffsetX,
                                dy: '' + blurOffsetY,
                            }, {
                                elementType: 'feComposite',
                                in2: 'SourceAlpha',
                                operator: 'arithmetic',
                                k2: '-1',
                                k3: '1',
                                result: 'shadowDiff',
                            },

                            {
                                elementType: 'feFlood',
                                'flood-color': '#000',
                                'flood-opacity': '0.2',
                            }, {
                                elementType: 'feComposite',
                                in2: 'shadowDiff',
                                operator: 'in',
                            }, {
                                elementType: 'feComposite',
                                in2: 'SourceGraphic',
                                operator: 'over',
                                result: 'firstfilter',
                            },

                            {
                                elementType: 'feGaussianBlur',
                                in: 'firstfilter',
                                stdDeviation: '' + blurRadius,
                                result: 'blur2',
                            }, {
                                elementType: 'feOffset',
                                dx: '' + blurOffsetX,
                                dy: '' + blurOffsetY,
                            }, {
                                elementType: 'feComposite',
                                in2: 'firstfilter',
                                operator: 'arithmetic',
                                k2: '-1',
                                k3: '1',
                                result: 'shadowDiff',
                            },

                            {
                                elementType: 'feFlood',
                                'flood-color': '#000',
                                'flood-opacity': '0.2',
                            }, {
                                elementType: 'feComposite',
                                in2: 'shadowDiff',
                                operator: 'in',
                            }, {
                                elementType: 'feComposite',
                                in2: 'firstfilter',
                                operator: 'over',
                            },
                        ],
                    },
                ],
            });

            const svgTriangleLeft = BB.createSvg({
                elementType: 'path',
                'vector-effect': 'non-scaling-stroke',
                d: 'M0,0 L 100,0 0,100 z',
                fill: 'rgba(0,0,0,0)',
                class: 'toolspace-svg-triangle-button',
            });
            svgTriangleLeft.onclick = () => {
                p.onLeft();
                svgTriangleLeft.classList.remove('toolspace-svg-triangle-button-hover');
            };

            const svgTriangleRight = BB.createSvg({
                elementType: 'path',
                'vector-effect': 'non-scaling-stroke',
                d: 'M100,100 L 100,0 0,100 z',
                fill: 'rgba(0,0,0,0)',
                class: 'toolspace-svg-triangle-button',
            });
            svgTriangleRight.onclick = () => {
                p.onRight();
                svgTriangleRight.classList.remove('toolspace-svg-triangle-button-hover');
            };

            // because :hover causes problems w touch
            const leftPointerListener = new BB.PointerListener({
                target: svgTriangleLeft,
                onEnterLeave: (isOver) => {
                    svgTriangleLeft.classList.toggle('toolspace-svg-triangle-button-hover', isOver);
                },
            });
            const rightPointerListener = new BB.PointerListener({
                target: svgTriangleRight,
                onEnterLeave: (isOver) => {
                    svgTriangleRight.classList.toggle('toolspace-svg-triangle-button-hover', isOver);
                },
            });

            svg.append(defs, svgTriangleLeft, svgTriangleRight);
            result.append(svg);

            const leftIm = BB.el({
                parent: result,
                className: 'dark-invert',
                css: {
                    backgroundImage: 'url(\'' + p.leftImage + '\')',
                    backgroundRepeat: 'no-repeat',
                    backgroundSize: 'contain',
                    width: '20px',
                    height: '20px',
                    position: 'absolute',
                    left: '10px',
                    top: '8px',
                    //transform: p.doMirror ? 'scale(-1, 1)' : '',
                    pointerEvents: 'none',
                },
            });
            result.append(leftIm);

            const rightIm = BB.el({
                parent: result,
                className: 'dark-invert',
                css: {
                    backgroundImage: 'url(\'' + (p.rightImage ? p.rightImage : p.leftImage) + '\')',
                    backgroundRepeat: 'no-repeat',
                    backgroundSize: 'contain',
                    width: '20px',
                    height: '20px',
                    position: 'absolute',
                    right: '10px',
                    bottom: '8px',
                    transform: p.rightImage ? '' : 'scale(-1, 1)',
                    pointerEvents: 'none',
                },
            });

            const setIsEnabledLeft = (b: boolean) => {
                svgTriangleLeft.classList.toggle('toolspace-row-button-disabled', !b);
                leftIm.classList.toggle('toolspace-row-button-disabled', !b);
            };

            const setIsEnabledRight = (b: boolean) => {
                svgTriangleRight.classList.toggle('toolspace-row-button-disabled', !b);
                rightIm.classList.toggle('toolspace-row-button-disabled', !b);
            };

            return {
                el: result,
                setIsEnabledLeft,
                setIsEnabledRight,
                leftPointerListener,
                rightPointerListener,
            };
        };

        this.toolDropdown = new ToolDropdown({
            onChange: (activeStr) => {
                this.setActive(activeStr, true);
            },
        });
        this.rootEl.append(this.toolDropdown.getElement());

        this.handButton = createButton({
            onClick: () => {
                this.setActive('hand', true);
            },
            image: toolHandImg,
            contain: true,
            doLighten: true,
        });
        this.handButton.el.classList.add('kl-tool-row-border-right');
        this.handButton.el.title = LANG('tool-hand');
        this.rootEl.append(this.handButton.el);

        this.zoomInNOutButton = createTriangleButton({
            onLeft: p.onZoomIn,
            onRight: p.onZoomOut,
            leftImage: toolZoomInImg,
            rightImage: toolZoomOutImg,
        });
        this.zoomInNOutButton.el.title = LANG('tool-zoom');
        this.rootEl.append(this.zoomInNOutButton.el);

        this.zoomInButton = createButton({
            onClick: p.onZoomIn,
            image: toolZoomInImg,
            contain: true,
        });
        this.zoomInButton.el.title = LANG('zoom-in');
        this.rootEl.append(this.zoomInButton.el);

        this.zoomOutButton = createButton({
            onClick: p.onZoomOut,
            image: toolZoomOutImg,
            contain: true,
        });
        this.zoomOutButton.el.title = LANG('zoom-out');
        this.rootEl.append(this.zoomOutButton.el);

        this.undoNRedoButton = createTriangleButton({
            onLeft: p.onUndo,
            onRight: p.onRedo,
            leftImage: toolUndoImg,
            rightImage: null,
        });
        this.undoNRedoButton.el.title = LANG('undo') + '/' + LANG('redo');
        this.undoNRedoButton.setIsEnabledLeft(false);
        this.undoNRedoButton.setIsEnabledRight(false);
        this.rootEl.append(this.undoNRedoButton.el);

        this.undoButton = createButton({
            onClick: p.onUndo,
            image: toolUndoImg,
            contain: true,
        });
        this.undoButton.el.title = LANG('undo');
        this.undoButton.el.classList.add('toolspace-row-button-disabled');
        this.rootEl.append(this.undoButton.el);

        this.redoButton = createButton({
            onClick: p.onRedo,
            image: toolUndoImg,
            contain: true,
            doMirror: true,
        });
        this.redoButton.el.title = LANG('redo');
        this.redoButton.el.classList.add('toolspace-row-button-disabled');
        this.rootEl.append(this.redoButton.el);

        this.zoomInButton.el.style.display = 'none';
        this.zoomOutButton.el.style.display = 'none';
        this.undoButton.el.style.display = 'none';
        this.redoButton.el.style.display = 'none';
    }

    // ---- interface ----

    getElement (): HTMLElement {
        return this.rootEl;
    }

    setIsSmall (b: boolean): void {
        BB.css(this.rootEl, {
            height: b ? '36px' : '54px',
        });

        this.toolDropdown.setIsSmall(b);
        this.handButton.setIsSmall(b);
        this.zoomInButton.setIsSmall(b);
        this.zoomOutButton.setIsSmall(b);
        this.undoButton.setIsSmall(b);
        this.redoButton.setIsSmall(b);

        if (b) {
            this.zoomInNOutButton.el.style.display = 'none';
            this.undoNRedoButton.el.style.display = 'none';
            this.zoomInButton.el.style.display = 'block';
            this.zoomOutButton.el.style.display = 'block';
            this.undoButton.el.style.display = 'block';
            this.redoButton.el.style.display = 'block';
        } else {
            this.zoomInNOutButton.el.style.display = 'block';
            this.undoNRedoButton.el.style.display = 'block';
            this.zoomInButton.el.style.display = 'none';
            this.zoomOutButton.el.style.display = 'none';
            this.undoButton.el.style.display = 'none';
            this.redoButton.el.style.display = 'none';
        }

    }

    setEnableZoomIn (b: boolean): void {
        this.zoomInButton.el.classList.toggle('toolspace-row-button-disabled', !b);
        this.zoomInNOutButton.setIsEnabledLeft(b);
    }

    setEnableZoomOut (b: boolean): void {
        this.zoomOutButton.el.classList.toggle('toolspace-row-button-disabled', !b);
        this.zoomInNOutButton.setIsEnabledRight(b);
    }

    setEnableUndo (b: boolean): void {
        this.undoButton.el.classList.toggle('toolspace-row-button-disabled', !b);
        this.undoNRedoButton.setIsEnabledLeft(b);
    }

    setEnableRedo (b: boolean): void {
        this.redoButton.el.classList.toggle('toolspace-row-button-disabled', !b);
        this.undoNRedoButton.setIsEnabledRight(b);
    }

    setActive (activeStr: TToolType, doEmit?: boolean): void {
        if (this.currentActiveStr === activeStr) {
            return;
        }

        this.currentActiveStr = activeStr;

        this.toolDropdown.setActive(this.currentActiveStr);
        this.handButton.el.classList.toggle('toolspace-row-button-activated', this.currentActiveStr === 'hand');

        if (doEmit) {
            this.onActivate(this.currentActiveStr);
        }
    }

    getActive (): TToolType {
        return this.currentActiveStr;
    }
}


code:
import {BB} from '../../../bb/bb';
import klecksLogoImg from '/src/app/img/klecks-logo.png';
import newImageImg from '/src/app/img/ui/new-image.svg';
import importImg from '/src/app/img/ui/import.svg';
import exportImg from '/src/app/img/ui/export.svg';
import shareImg from '/src/app/img/ui/share.svg';
import helpImg from '/src/app/img/ui/help.svg';
import {LANG} from '../../../language/language';
import {PointerListener} from '../../../bb/input/pointer-listener';

/**
 * Topmost row of buttons in toolspace (with the app logo)
 */
export class ToolspaceTopRow {

    private readonly rootEl: HTMLElement;

    // ---- public ----
    constructor (
        p: {
            logoImg: string;
            onLogo: () => void;
            onNew: () => void;
            onImport: () => void;
            onSave: () => void;
            onShare: () => void;
            onHelp: () => void;
        }
    ) {
        this.rootEl = BB.el({
            className: 'kl-toolspace-row',
            css: {
                height: '36px',
                display: 'flex',
            },
        });

        function createButton (
            p: {
                onClick: () => void;
                title: string;
                image: string;
                contain: boolean;
                extraPadding?: number;
                darkInvert?: boolean;
            }
        ): {
            el: HTMLElement;
            pointerListener: PointerListener;
        } {
            const padding = 6 + (p.extraPadding ? p.extraPadding : 0);
            const el = BB.el({
                className: 'toolspace-row-button nohighlight',
                title: p.title,
                onClick: p.onClick,
                css: {
                    padding: p.contain ? padding + 'px 0' : '',
                },
            });
            const im = BB.el({
                className: p.darkInvert ? 'dark-invert' : undefined,
                css: {
                    backgroundImage: 'url(\'' + p.image + '\')',
                    backgroundRepeat: 'no-repeat',
                    backgroundPosition: 'center',
                    backgroundSize: p.contain ? 'contain' : '',
                    //filter: 'grayscale(1)',
                    height: '100%',
                },
            });
            im.style.pointerEvents = 'none';
            el.append(im);
            const pointerListener = new BB.PointerListener({ // because :hover causes problems w touch
                target: el,
                onEnterLeave: function (isOver) {
                    el.classList.toggle('toolspace-row-button-hover', isOver);
                },
            });
            return {
                el,
                pointerListener,
            };
        }

        const logoButton = createButton({
            onClick: p.onLogo,
            title: LANG('home'),
            image: p.logoImg ? p.logoImg : klecksLogoImg,
            contain: true,
            darkInvert: true,
        });
        logoButton.el.classList.add('kl-tool-row-border-right');
        BB.css(logoButton.el, {
            width: '45px',
        });
        const newButton = createButton({
            onClick: p.onNew,
            title: LANG('file-new'),
            image: newImageImg,
            extraPadding: 1,
            contain: true,
        });
        const importButton = createButton({
            onClick: p.onImport,
            title: LANG('file-import'),
            image: importImg,
            extraPadding: 1,
            contain: true,
        });
        const saveButton = createButton({
            onClick: p.onSave,
            title: LANG('file-save'),
            image: exportImg,
            extraPadding: 1,
            contain: true,
        });

        let shareButton = null;
        if (BB.canShareFiles()) {
            shareButton = createButton({
                onClick: p.onShare,
                title: LANG('file-share'),
                image: shareImg,
                contain: true,
                darkInvert: true,
            });
        }
        const helpButton = createButton({
            onClick: p.onHelp,
            title: LANG('help'),
            image: helpImg,
            contain: true,
            darkInvert: true,
        });

        BB.append(this.rootEl, [
            logoButton.el,
            newButton.el,
            importButton.el,
            saveButton.el,
            shareButton ? shareButton.el : undefined,
            helpButton.el,
        ]);
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }
}


code:
import {BB} from '../../../bb/bb';

/**
 * Two buttons next to each other, each representing a tab. one at a time can be active.
 */
export class TwoTabs {

    private readonly rootEl: HTMLElement;
    private readonly leftTab: HTMLElement;
    private readonly rightTab: HTMLElement;
    private value: number;

    private update (): void {
        this.leftTab.classList.toggle('kl-2-tabs--active', this.value === 0);
        this.rightTab.classList.toggle('kl-2-tabs--active', this.value === 1);
    }

    // ---- public ----
    constructor (params: {
        left: string;
        right: string;
        init: number; //0, 1
        onChange: (val: number) => void;
    }) {
        this.value = params.init;

        this.rootEl = BB.el({
            className: 'kl-2-tabs',
        });

        this.leftTab = BB.el({
            parent: this.rootEl,
            content: params.left,
            className: 'kl-2-tabs__left',
        });
        this.leftTab.onpointerdown = () => false;

        this.rightTab = BB.el({
            parent: this.rootEl,
            content: params.right,
            className: 'kl-2-tabs__right',
        });
        this.rightTab.onpointerdown = () => false;

        this.update();

        this.leftTab.onclick = () => {
            if (this.value === 0) {
                return;
            }
            this.value = 0;
            this.update();
            params.onChange(this.value);
        };

        this.rightTab.onclick = () => {
            if (this.value === 1) {
                return;
            }
            this.value = 1;
            this.update();
            params.onChange(this.value);
        };
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }
}


code:
import {KeyListener} from '../../../../bb/input/key-listener';
import {dialogCounter} from '../modal-count';
import {BB} from '../../../../bb/bb';
import {LANG} from '../../../../language/language';
import './scroll-fix';
import cancelImg from '/src/app/img/ui/cancel.svg';

/**
 * popup that fill whole height, with some padding.
 * currently only used for iframe popups.
 */
export class DynamicModal {

    private readonly parent: HTMLElement;
    private readonly rootEl: HTMLElement;
    private readonly updatePos: () => void;
    private readonly keyListener: KeyListener;
    private readonly xButton: HTMLElement;
    private readonly bgEl: HTMLElement;
    private readonly onClose: ((result?: string) => void) | undefined;

    // ---- public ----
    constructor (
        p: {
            title?: HTMLElement;
            content?: HTMLElement;
            buttonArr?: string[];
            autoFocus?: string | false; // todo - not implemented
            clickOnEnter?: string; // button name - todo not implemented
            onClose?: (result?: string) => void;

            //size and position
            width: number;
            isMaxHeight: boolean; // todo - not implemented
        }
    ) {
        dialogCounter.increase();
        this.onClose = p.onClose;
        this.parent = document.body;
        this.rootEl = BB.el({
            parent: this.parent,
            className: 'g-root kl-d-modal-root',
            css: {
                position: 'fixed',
                left: '0',
                top: '0',
                bottom: '0',
                right: '0',
                overflow: 'auto',
                animationName: 'consoleIn',
                animationDuration: '0.3s',
                animationTimingFunction: 'ease-out',
            },
            onClick: BB.handleClick,
        });

        //background element registering clicks
        this.bgEl = BB.el({
            parent: this.rootEl,
            css: {
                position: 'absolute',
                left: '0',
                top: '0',
                bottom: '0',
                right: '0',
            },
            onClick: () => this.close(),
        });

        //the actual popup box
        const popupEl = BB.el({
            parent: this.rootEl,
            className: 'kl-d-modal',
            css: {
                position: 'absolute',
                width: (BB.isCssMinMaxSupported() ?
                    'min(calc(100% - 40px), ' + (p.width ? p.width : 400) + 'px)' :
                    (p.width ? p.width : 400) + 'px'),
                height: 'calc(100% - 40px)',
                borderRadius: '10px',
                overflow: 'hidden',
            },
        });

        //x and y position via script. flex not powerful enough imo
        this.updatePos = () => {
            const elW = popupEl.offsetWidth;
            const elH = popupEl.offsetHeight;

            BB.css(popupEl, {
                left: Math.max(0, (window.innerWidth - elW) / 2) + 'px',
                top: Math.max(20, (window.innerHeight - elH) / 2 - (elH * 0.20)) + 'px',
            });
        };

        this.updatePos();
        window.addEventListener('resize', this.updatePos);

        //title row in popup
        const titleHeight = 40;
        const titleEl = BB.el({
            parent: popupEl,
            css: {
                height: titleHeight + 'px',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                paddingLeft: (titleHeight / 2) + 'px',
            },
        });
        if (p.title) {
            titleEl.append(p.title);
        }
        this.xButton = BB.el({
            parent: titleEl,
            tagName: 'button',
            className: 'popup-x',
            content: `<img alt="${LANG('modal-close')}" height="20" src="${cancelImg}">`,
            title: LANG('modal-close'),
            onClick: () => this.close(),
            css: {
                width: titleHeight + 'px',
                height: titleHeight + 'px',
                lineHeight: titleHeight + 'px',
                background: 'none',
                boxShadow: 'none',
            },
            custom: {
                tabindex: '0',
            },
        });

        const contentEl = BB.el({
            parent: popupEl,
            css: {
                height: 'calc(100% - ' + titleHeight + 'px)',
            },
        });
        if (p.content) {
            contentEl.append(p.content);
        }

        this.keyListener = new BB.KeyListener({
            onDown: (keyStr, e) => {
                if (keyStr === 'esc') {
                    e.stopPropagation();
                    this.close();
                }
            },
        });
    }

    // ---- interface ----
    close (): void {
        dialogCounter.decrease();
        BB.destroyEl(this.rootEl);
        this.rootEl.remove();
        window.removeEventListener('resize', this.updatePos);
        this.keyListener.destroy();
        BB.destroyEl(this.xButton);
        BB.destroyEl(this.bgEl);
        this.onClose && this.onClose();
    }
}


code:
window.onscroll = (e) => {
    e.preventDefault();
};


code:
import {IKeyString} from '../../../../bb/bb-types';
import {dialogCounter} from '../modal-count';
import {BB} from '../../../../bb/bb';
import {LANG} from '../../../../language/language';
import './scroll-fix';
import cancelImg from '/src/app/img/ui/cancel.svg';
import checkImg from '/src/app/img/ui/check.svg';

export function showModal (
    p: {
        target: HTMLElement;
        div?: HTMLElement; // node with content
        message: string; // can be html
        callback?: (result: string) => void;
        buttons?: string[]; // "Ok", and "Cancel" will be automatically translated
        primaries?: string[];
        type?: 'error' | 'warning' | 'upload' | 'ok'; // todo
        closeFunc?: (f: () => void) => void; // returns a function you can call to close (Cancel) the dialog
        style?: IKeyString;
        clickOnEnter?: string; // name of button - will be clicked if enter key pressed
        autoFocus?: false | string; // name of  to automatically focus - default 'Ok' - false -> none
        ignoreBackground?: boolean; // default false; if true clicking on background doesn't close
    }
): {
    setIgnoreBackground: (b: boolean) => void;
} {
    dialogCounter.increase();
    let isClosed = false;
    let ignoreBackground = !!p.ignoreBackground;

    // need this extra layer because chrome mobile otherwise scrolls the page and then glitches as the address bar goes away
    const rootRootEl = BB.el({
        parent: document.body,
        css: {
            position: 'absolute',
            left: '0',
            top: '0',
            right: '0',
            bottom: '0',
            overflow: 'hidden',
        },
    });
    const rootEl = BB.el({
        parent: rootRootEl,
        className: 'kl-popup',
    });

    const scrollContent = BB.el({
        parent: rootEl,
        css: {
            width: '100%',
            minHeight: '100%',
            // padding: '10px 0',
            position: 'relative',
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'center',
            alignItems: 'center',
        },
    });

    const bgEl = BB.el({
        parent: scrollContent,
        onClick: () => {
            if (ignoreBackground) {
                return;
            }
            close('Cancel');
        },
        css: {
            position: 'absolute',
            left: '0',
            top: '0',
            zIndex: '0',
            width: '100%',
            height: '100%',
        },
    });

    const titleHeight = 40;
    const xButton = BB.el({
        tagName: 'button',
        className: 'popup-x',
        content: `<img alt="${LANG('modal-close')}" height="20" src="${cancelImg}">`,
        title: LANG('modal-close'),
        onClick: () => {
            close('Cancel');
        },
        css: {
            width: titleHeight + 'px',
            height: titleHeight + 'px',
            lineHeight: titleHeight + 'px',
            position: 'absolute',
            right: '0',
            top: '0',
            background: 'none',
            boxShadow: 'none',
        },
    });

    const boxClasses = ['kl-popup-box'];
    boxClasses.push('kl-popup-box--sm');
    const boxEl = BB.el({
        content: [
            xButton,
            BB.el({
                content:p.message,
                css: {
                    marginBottom: p.div ? '10px' : undefined,
                    marginRight: '15px',
                },
            }),
            p.div,
        ],
        className: boxClasses.join(' '),
        css: p.style ? p.style : undefined,
    });

    scrollContent.append(
        BB.el({
            css: {
                flex: '0.5',
            },
        }),
        boxEl,
        BB.el({
            css: {
                flex: '1',
            },
        })
    );

    if (p.type === 'error') {
        boxEl.classList.add('poperror');
    }
    if (p.type === 'ok') {
        boxEl.classList.add('popok');
    }
    if (p.type === 'warning') {
        boxEl.classList.add('popwarning');
    }
    if (p.type === 'upload') {
        boxEl.classList.add('popupload');
    }

    const keyListener = new BB.KeyListener({
        onDown: function (keyStr, e, comboStr): void {
            if (isClosed) {
                return;
            }
            if (clickOnEnterBtn && comboStr === 'enter' && !BB.isInputFocused()) {
                e.stopPropagation();
                setTimeout(() => {
                    clickOnEnterBtn && clickOnEnterBtn.click();
                }, 10);
            }
            if (comboStr === 'esc') {
                e.stopPropagation();
                close('Cancel');
            }
        },
    });
    // prevent ctrl scroll -> zooming page
    const wheelPrevent = (event: WheelEvent): void => {
        if (keyListener.isPressed('ctrl')) {
            event.preventDefault();
        }
    };
    rootEl.addEventListener('wheel', wheelPrevent);
    rootEl.onclick = BB.handleClick;

    let autoFocus: string | undefined;
    if (p.autoFocus) {
        autoFocus = p.autoFocus;
    } else if (p.autoFocus === false) {
        autoFocus = undefined;
    } else {
        autoFocus = 'Ok';
    }

    const buttonRowEl = p.buttons && p.buttons.length > 0 ? BB.el({
        parent: boxEl,
        css: {
            display: 'flex',
            flexWrap: 'wrap',
            justifyContent: 'flex-end',
            marginTop: '12px', // 8px already via buttons
            marginLeft: '-8px',
        },
    }) : undefined;
    let clickOnEnterBtn: HTMLButtonElement | undefined;
    const btnElArr: HTMLButtonElement[] = [];
    if (p.buttons) {
        p.buttons.forEach(buttonName => {
            const btnClasses = ['kl-popup__btn'];
            if (buttonName === 'Ok' || (p.primaries && p.primaries.includes(buttonName))) {
                btnClasses.push('kl-button-primary');
            }
            let iconUrl;
            let label = buttonName;
            if (buttonName === 'Ok') {
                label = LANG('modal-ok');
                iconUrl = checkImg;
            }
            if (buttonName === 'Cancel') {
                label = LANG('modal-cancel');
                iconUrl = cancelImg;
                btnClasses.push('kl-button-cancel');
            }
            let iconImg: HTMLElement | undefined = undefined;
            if (iconUrl) {
                iconImg = BB.el({
                    tagName: 'img',
                    custom: {
                        src: iconUrl,
                        height: '17',
                    },
                });
            }
            const btn = BB.el({
                parent: buttonRowEl,
                tagName: 'button',
                className: btnClasses.join(' '),
                content: [
                    iconImg,
                    BB.el({
                        className: 'kl-popup__btn__text',
                        content: label,
                    }),
                ],
                onClick: () => {
                    close(buttonName);
                },
            });
            btnElArr.push(btn);
            if (autoFocus === buttonName) {
                setTimeout(() => {
                    btn.focus();
                    rootEl.scrollTo(0, 0);
                }, 10);
                setTimeout(() => {
                    // safari needs a separate timeout
                    rootEl.scrollTo(0, 0);
                }, 20);
            }
            if (buttonName === p.clickOnEnter) {
                clickOnEnterBtn = btn;
            }
        });
    }

    function close (value: string): void {
        if (isClosed) {
            return;
        }

        isClosed = true;
        BB.clearSelection();
        BB.unfocusAnyInput();
        rootRootEl.remove();
        dialogCounter.decrease();
        BB.destroyEl(xButton);
        BB.destroyEl(bgEl);
        keyListener.destroy();
        rootEl.removeEventListener('wheel', wheelPrevent);
        // (disabled) eslint-disable-next-line no-null/no-null
        rootEl.onclick = null;
        btnElArr.forEach(item => BB.destroyEl(item));
        btnElArr.splice(0, btnElArr.length);

        if (p.callback) {
            p.callback(value);
        }
    }

    if (p.closeFunc) {
        p.closeFunc(function () {
            close('Cancel');
        });
    }

    return {
        setIgnoreBackground: (b: boolean) => {
            ignoreBackground = b;
        },
    } as const;
}


code:
import {BB} from '../../../bb/bb';
import {showModal} from './base/showModal';
import {CropCopy} from '../components/crop-copy';
import {LANG} from '../../../language/language';
import {StatusOverlay} from '../components/status-overlay';
import {ICropRect} from '../../../bb/bb-types';

export function clipboardDialog (
    parent: HTMLElement,
    fullCanvas: HTMLCanvasElement,
    cropCallback: (crop: ICropRect) => void,
    output: StatusOverlay,
    showCrop: boolean, // show crop button
): void {

    let clipboardItemIsSupported: boolean = false;
    try {
        clipboardItemIsSupported = !!ClipboardItem;
    } catch (e) { /* empty */ }

    const div = document.createElement('div');
    const isSmall = window.innerWidth < 550 || window.innerHeight < 550;

    const topWrapper = BB.el({
        content: LANG('crop-drag-to-crop') + (clipboardItemIsSupported ? '' : '
' + LANG('cropcopy-click-hold')),
        css: {
            textAlign: 'center',
        },
    });
    div.append(topWrapper);

    const cropCopy = new CropCopy({
        width: isSmall ? 340 : 540,
        height: isSmall ? 300 : 350,
        canvas: fullCanvas,
        isNotCopy: false,
    });
    BB.css(cropCopy.getEl(), {
        marginTop: '10px',
        marginLeft: '-20px',
    });
    div.append(cropCopy.getEl());

    function toClipboard () {
        const imgURL = cropCopy.getCroppedImage().toDataURL('image/png');
        setTimeout(async function () {
            try {
                const data = await fetch(imgURL);
                const blob = await data.blob();
                await (navigator.clipboard as any).write([
                    new ClipboardItem({
                        [blob.type]: blob,
                    } as any), // todo check is possible?
                ]);
                setTimeout(function () {
                    output.out(LANG('cropcopy-copied'), true);
                }, 200);
            } catch (err) {
                console.error((err as Error).name, (err as Error).message);
            }
        }, 0);
    }

    const keyListener = new BB.KeyListener({
        onDown: function (keyStr, KeyEvent, comboStr) {
            if (comboStr === 'ctrl+c') {
                toClipboard();
                closeFunc && closeFunc();
            }
        },
    });

    let closeFunc: () => void;
    function blur () {
        closeFunc && closeFunc();
    }
    window.addEventListener('blur', blur);

    const buttonArr = [];
    if (clipboardItemIsSupported) {
        buttonArr.push(LANG('cropcopy-btn-copy'));
    }
    if (showCrop) {
        buttonArr.push(LANG('cropcopy-btn-crop'));
    }
    buttonArr.push('Cancel');

    showModal({
        target: parent,
        message: '' + (showCrop ? `${LANG('cropcopy-title-copy')} / ${LANG('cropcopy-title-crop')}` : `${LANG('cropcopy-title-copy')}`) + '',
        div: div,
        style: isSmall ? {} : {
            width: '500px',
        },
        buttons: buttonArr,
        primaries: [LANG('cropcopy-btn-copy')],
        callback: function (result) {
            if (result === LANG('cropcopy-btn-copy')) {
                toClipboard();
            } else if (result === LANG('cropcopy-btn-crop')) {
                const rectObj = cropCopy.getRect();
                cropCallback({
                    left: Math.round(-rectObj.x),
                    right: Math.round(rectObj.x + rectObj.width - fullCanvas.width),
                    top: Math.round(-rectObj.y),
                    bottom: Math.round(rectObj.y + rectObj.height - fullCanvas.height),
                });
            }
            window.removeEventListener('blur', blur);
            cropCopy.destroy();
            keyListener.destroy();
        },
        clickOnEnter: LANG('cropcopy-btn-copy'),
        closeFunc: function (func) {
            closeFunc = func;
        },
    });
}


code:
import {BB} from '../../../bb/bb';
import {input} from '../components/input';
import {showModal} from './base/showModal';
import {LANG} from '../../../language/language';
import copyImg from '/src/app/img/ui/copy.svg';
import {IRGB} from '../../kl-types';
import {RGB} from '../../../bb/color/color';

type TInputRow = {
    update: () => void;
    destroy: () => void;
};

/**
 * dialog for manually inputting the color
 */
export class HexColorDialog {

    // ---- public ----
    constructor (
        p: {
            color: IRGB;
            onClose: (rgb: IRGB | undefined) => void;
        }
    ) {
        let lastValidRgb: RGB = new BB.RGB(p.color.r, p.color.g, p.color.b);

        const div = BB.el();

        const previewEl = BB.el({
            css: {
                width: '20px',
                height: '20px',
                marginBottom: '10px',
                boxShadow: 'inset 0 0 0 1px #fff, 0 0 0 1px #000',
                background: '#' + BB.ColorConverter.toHexString(lastValidRgb),
            },
        });
        div.append(previewEl);

        // --- Hex ---
        const hexRowEl = BB.el({
            css: {
                display: 'flex',
                alignItems: 'center',
                marginBottom: '15px',
            },
        });
        const hexLabel = BB.el({
            content: LANG('mci-hex'),
            css: {
                width: '60px',
            },
        });
        const hexInput = input({
            init: '#' + BB.ColorConverter.toHexString(lastValidRgb),
            css: {
                width: '80px',
            },
            callback: function () {
                let rgbObj = BB.ColorConverter.hexToRGB(hexInput.value);
                if (!rgbObj) {
                    rgbObj = lastValidRgb;
                    hexInput.value = '#' + BB.ColorConverter.toHexString(lastValidRgb);
                } else {
                    lastValidRgb = rgbObj;
                }
                previewEl.style.background = '#' + BB.ColorConverter.toHexString(rgbObj);

                for (let i = 0; i < rgbArr.length; i++) {
                    rgbArr[i].update();
                }
            },
        });
        const copyButton = BB.el({
            tagName: 'button',
            content: '<img src="' + copyImg + '" height="20" alt=""/>',
            title: LANG('mci-copy'),
            css: {
                marginLeft: '10px',
            },
            onClick: function () {
                hexInput.select();
                document.execCommand('copy');
            },
        });
        hexRowEl.append(hexLabel, hexInput, copyButton);
        div.append(hexRowEl);
        setTimeout(function () {
            hexInput.focus();
            hexInput.select();
        }, 0);

        // --- R G B ---
        function createRgbInputRow (labelStr: string, attributeStr: 'r' | 'g' | 'b'): TInputRow {

            const rowEl = BB.el({
                css: {
                    display: 'flex',
                    alignItems: 'center',
                    marginTop: '5px',
                },
            });
            const labelEl = BB.el({
                content: labelStr,
                css: {
                    width: '60px',
                },
            });

            const inputEl = input({
                init: lastValidRgb[attributeStr],
                min: 0,
                max: 255,
                type: 'number',
                css: {
                    width: '80px',
                },
                callback: function () {
                    if (inputEl.value === '' || parseFloat(inputEl.value) < 0 || parseFloat(inputEl.value) > 255) {
                        result.update();
                        return;
                    }
                    inputEl.value = '' + Math.round(parseFloat(inputEl.value));
                    lastValidRgb[attributeStr] = Number(inputEl.value);
                    previewEl.style.background = '#' + BB.ColorConverter.toHexString(lastValidRgb);
                    hexInput.value = '#' + BB.ColorConverter.toHexString(lastValidRgb);
                },
            });

            rowEl.append(labelEl, inputEl);
            div.append(rowEl);

            const result = {
                update: (): void => {
                    inputEl.value = '' + lastValidRgb[attributeStr];
                },
                destroy: (): void => {
                    BB.unsetEventHandler(inputEl, 'onchange');
                },
            };
            return result;
        }
        const rgbArr: TInputRow[] = [];
        rgbArr.push(createRgbInputRow(LANG('red'), 'r'));
        rgbArr.push(createRgbInputRow(LANG('green'), 'g'));
        rgbArr.push(createRgbInputRow(LANG('blue'), 'b'));

        showModal({
            target: document.body,
            message: `<b>${LANG('manual-color-input')}</b>`,
            div: div,
            autoFocus: false,
            clickOnEnter: 'Ok',
            buttons: ['Ok', 'Cancel'],
            callback: function (resultStr) {
                BB.destroyEl(copyButton);
                rgbArr.forEach(item => item.destroy());
                rgbArr.splice(0, rgbArr.length);

                p.onClose(resultStr === 'Ok' ? BB.ColorConverter.hexToRGB(hexInput.value) : undefined);
            },
        });
    }

}


code:
import {BB} from '../../../bb/bb';
import {KL} from '../../kl';
import {SaveReminder} from '../components/save-reminder';
import {base64ToBlob} from '../../storage/base-64-to-blob';
import {KlCanvas} from '../../canvas/kl-canvas';
import {LANG} from '../../../language/language';
import loadingImg from '/src/app/img/ui/loading.gif';

type TImgurUploadResponse = {
    // just a subset
    id: string;
    datetime: number;
    type: string;
    width: number;
    height: number;
    deletehash: string;
    name: string;
    link: string;
};

/**
 * uploads canvas, opens new tab with the upload progress & then opens the image page.
 */
async function upload (
    canvas: HTMLCanvasElement,
    title: string,
    description: string,
    type: 'png' | 'jpeg',
    imgurKey: string,
): Promise<TImgurUploadResponse> {
    const img = base64ToBlob(canvas.toDataURL('image/' + type));

    const w = window.open();

    if (!w) {
        throw new Error('could not create new tab');
    }

    const label = w.document.createElement('div');
    const gif = w.document.createElement('img');
    gif.src = loadingImg;
    label.append(gif);
    BB.css(gif, {
        filter: 'invert(1)',
    });
    BB.css(w.document.body, {
        backgroundColor: '#121211',
        backgroundImage: 'linear-gradient(#2b2b2b 0%, #121211 50%)',
        backgroundRepeat: 'no-repeat',
    });

    const labelText = w.document.createElement('div');
    labelText.style.marginTop = '10px';
    label.append(labelText);
    labelText.textContent = LANG('upload-uploading');

    w.document.body.append(label);
    BB.css(label, {
        marginLeft: 'auto',
        marginRight: 'auto',
        marginTop: '100px',
        fontFamily: 'Arial, sans-serif',
        fontSize: '20px',
        textAlign: 'center',
        transition: 'opacity 0.3s ease-in-out',
        opacity: '0',
        color: '#ccc',
    });
    setTimeout(function () {
        label.style.opacity = '1';
    }, 20);

    let response;
    try {
        const formData = new FormData();
        formData.append('title', title);
        formData.append('description', description);
        formData.append('image', img);
        response = await fetch('https://api.imgur.com/3/image',  {
            method: 'POST',
            headers: {
                Authorization: 'Client-ID ' + imgurKey,
            },
            body: formData,
        });

    } catch (e) {
        w.close();
        throw e;
    }
    if (!response.ok) {
        w.close();
        throw new Error();
    }
    const data: TImgurUploadResponse = (await response.json()).data;

    w.location.href = data.link.replace(/\.(jpg|png)/, '');

    return data;
}

export function imgurUpload (
    klCanvas: KlCanvas,
    klRootEl: HTMLElement,
    saveReminder: SaveReminder,
    imgurKey: string, // API key
): void {
    if (!imgurKey) {
        throw new Error('imgur key missing');
    }

    const inputTitle = BB.el({tagName: 'input'});
    inputTitle.type = 'text';
    inputTitle.value = LANG('upload-title-untitled');
    const inputDescription = BB.el({
        tagName: 'textarea',
        custom: {
            rows: '2',
        },
        css: {
            width: '100%',
            maxWidth: '100%',
        },
    });

    const labelTitle =  BB.el({
        textContent: LANG('upload-name') + ':',
    });
    const labelDescription = BB.el({
        textContent: LANG('upload-caption') + ':',
        css: {
            marginTop: '10px',
        },
    });

    const tos = BB.el({
        content: `<br/><a href="https://imgur.com/tos" target="_blank" rel="noopener noreferrer">${LANG('terms-of-service')}</a>`,
    });

    const typeRadio = new KL.RadioList({
        name: 'filetype',
        init: 'jpeg',
        items: [
            { label: 'JPG', value: 'jpeg' },
            { label: 'PNG', value: 'png' },
        ],
        ignoreFocus: true,
    });
    BB.css(typeRadio.getElement(), {
        marginBottom: '10px',
    });

    const outDiv = BB.el();
    const infoHint = BB.el({
        className: 'info-hint',
        textContent: LANG('upload-link-notice'),
    });
    outDiv.append(
        infoHint,
        typeRadio.getElement(),
        labelTitle,
        inputTitle,
        labelDescription,
        inputDescription,
        tos
    );
    KL.popup({
        target: klRootEl,
        message: `<b>${LANG('upload-title')}</b>`,
        type: 'upload',
        div: outDiv,
        buttons: [LANG('upload-submit'), 'Cancel'],
        clickOnEnter: LANG('upload-submit'),
        primaries: [LANG('upload-submit')],
        autoFocus: LANG('upload-submit'),
        callback: async function (val) {
            if (val === LANG('upload-submit') || val === 'Yes' || val === 'Ok') {
                try {
                    const result = await upload(
                        klCanvas.getCompleteCanvas(1),
                        inputTitle.value,
                        inputDescription.value,
                        typeRadio.getValue() as 'png' | 'jpeg',
                        imgurKey,
                    );

                    KL.popup({
                        target: klRootEl,
                        type: 'ok',
                        message: `
${LANG('upload-success')}

${LANG('upload-delete')}
imgur.com/delete/${result.deletehash}

`,
                        buttons: ['Ok'],
                    });
                    saveReminder.reset();

                } catch(e) {
                    KL.popup({
                        target: klRootEl,
                        type: 'error',
                        message: LANG('upload-failed'),
                        buttons: ['Ok'],
                    });
                }
            }
        },
    });
}


code:
export const licenses: {
    title: string;
    full: string;
}[] = [
    {
        title: 'glfx.js by Evan Wallace',
        full: `glfx.js
https://github.com/evanw/glfx.js

Copyright (C) 2011 by Evan Wallace

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`,
    },
    {
        title: 'FileSaver.js by Eli Grey',
        full: `FileSaver.js
https://github.com/eligrey/FileSaver.js

The MIT License

Copyright © 2016 Eli Grey.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
    },
    {
        title: 'ag-psd by Agamnentzar',
        full: `ag-psd
https://github.com/Agamnentzar/ag-psd

The MIT License (MIT)

Copyright (c) 2016 Agamnentzar

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Any image or brush files included in this repository are not covered by this license and belong to their copyright holders.`,
    },
    {
        title: 'MDN polyfills',
        full: `MDN Polyfills
Licenses`,
    },
    {
        title: 'Gonkees-Shaders by Gonkee',
        full: `Gonkees-Shaders
https://github.com/Gonkee/Gonkees-Shaders

MIT License

Copyright (c) 2018 Gonkee

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`,
    },
    {
        title: 'GLSL Simplex Noise by Inigo Quilez',
        full: `GLSL Simplex Noise

The MIT License
Copyright © 2013 Inigo Quilez

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

https://www.youtube.com/c/InigoQuilez
https://iquilezles.org`,
    },
];


code:
import {c} from '../../../../bb/base/c';
import {css as emotionCss} from '@emotion/css';
import {DynamicModal} from '../base/dynamic-modal';
import {BB} from '../../../../bb/bb';
import {LANG} from '../../../../language/language';

export function showLicensesDialog () {
    const licensesCss = emotionCss({
        display: 'flex',
        flexDirection: 'column',
        gap: '10px',
        'details > div': {
            padding: '10px',
            boxShadow: '0 0 0 1px',
            marginTop: '5px',
            fontFamily: 'monospace',
            background: '#aaa2',
            overflow: 'hidden',
        },
        'summary': {
            cursor: 'pointer',
            userSelect: 'none',
        },
    });

    const libraries = c('.' + licensesCss, LANG('loading'));
    const fonts = c('.' + licensesCss, LANG('loading'));

    const content = c(',flex,flexCol,gap-10', [
        libraries,
        fonts,
    ]);

    new DynamicModal({
        title: BB.el({
            content: LANG('licenses'),
        }),
        content: BB.el({
            content,
            css: {
                height: '100%',
                overflowY: 'auto',
                padding: '10px',
                boxSizing: 'border-box',
            },
        }),
        width: 800,
        isMaxHeight: true,
    });

    import('./licenses').then(result => {
        libraries.innerHTML = '';
        result.licenses.forEach(item => {
            libraries.append(c('details', [
                c('summary', item.title),
                c('', item.full.replace(/\n/g, '
')),
            ]));
        });
    });

    import('../../../../../fonts/font-licenses').then(result => {
        fonts.innerHTML = '';
        result.fontLicenses.forEach(item => {
            fonts.append(c('details', [
                c('summary', 'Font: ' + item.title),
                c('', item.full.replace(/\n/g, '
')),
            ]));
        });
    });
}


code:
// not the best

/**
 * how many modals are open
 */

export class DialogCounter {
    private listeners: ((v: number) => void)[] = [];
    private count = 0;

    private emit (): void {
        this.listeners.forEach(item => {
            item(this.count);
        });
    }

    increase (v?: number): void {
        if (v !== undefined) {
            this.count += v;
        } else {
            this.count++;
        }
        this.emit();
    }
    decrease (v?: number): void {
        if (v !== undefined) {
            this.count -= v;
        } else {
            this.count--;
        }
        this.emit();
    }
    get (): number {
        return this.count;
    }
    subscribe (listener: (v: number) => void): void {
        this.listeners.push(listener);
    }
}

export const dialogCounter = new DialogCounter();


code:
import {BB} from '../../../bb/bb';
import {Select} from '../components/select';
import {ColorOptions} from '../components/color-options';
import {showModal} from './base/showModal';
import {LANG} from '../../../language/language';
import {IRGB, IRGBA} from '../../kl-types';
import {ISize2D} from '../../../bb/bb-types';
import {table} from '../components/table';
import {ERASE_COLOR} from '../../brushes/erase-color';
import {theme} from '../../../theme/theme';

export function newImageDialog (
    p: {
        currentColor: IRGB; // current color
        secondaryColor: IRGB;
        maxCanvasSize: number;
        canvasWidth: number; // current canvas size
        canvasHeight: number; // current canvas size
        workspaceWidth: number;
        workspaceHeight: number;
        onConfirm: (width: number, height: number, color: IRGBA) => void;
        onCancel: () => void;
    }
): void {

    const currentColor = p.currentColor;
    const secondaryColor = p.secondaryColor;
    const maxCanvasSize = p.maxCanvasSize;
    const canvasWidth = p.canvasWidth;
    const canvasHeight = p.canvasHeight;
    const workspaceWidth = p.workspaceWidth;
    const workspaceHeight = p.workspaceHeight;
    const onConfirm = p.onConfirm;
    const onCancel = p.onCancel;

    function createRatioSize (ratioX: number, ratioY: number, width: number, height: number, padding: number): ISize2D {
        return BB.fitInto(
            ratioX,
            ratioY,
            Math.min(maxCanvasSize, width - padding), Math.min(maxCanvasSize, height - padding),
            1
        );
    }

    const newImDiv = BB.el();
    const widthInput = BB.el({tagName: 'input'});
    const unitStyle = {
        color: '#888',
        fontSize: '12px',
        marginLeft: '5px',
    };
    const widthUnit = BB.el({
        textContent: LANG('new-px'),
        css: unitStyle,
    });
    const heightInput = BB.el({tagName: 'input'});
    const heightUnit = BB.el({
        textContent: LANG('new-px'),
        css: unitStyle,
    });

    widthInput.setAttribute('data-ignore-focus', 'true');
    heightInput.setAttribute('data-ignore-focus', 'true');

    widthInput.type = 'number';
    widthInput.min = '1';
    widthInput.max = '' + maxCanvasSize;
    BB.css(widthInput, {
        width: '70px',
    });

    heightInput.type = 'number';
    heightInput.min = '1';
    heightInput.max = '' + maxCanvasSize;
    heightInput.style.width = '70px';
    widthInput.value = '' + canvasWidth;
    heightInput.value = '' + canvasHeight;
    widthInput.onclick = (): void => {
        widthInput.focus();
        updateRatio();
    };
    heightInput.onclick = (): void => {
        heightInput.focus();
        updateRatio();
    };

    const sizeTable = table([
        [
            LANG('width') + ': ',
            widthInput,
            widthUnit,
        ],
        [
            BB.el({css: {height: '5px'}}),
            '',
            '',
        ],
        [
            LANG('height') + ': ',
            heightInput,
            heightUnit,
        ],
    ]);
    BB.css(sizeTable, {
        marginBottom: '10px',
    });

    const ratioWrapper = BB.el({
        css: {
            marginTop: '5px',
            color: '#888',
        },
    });

    const templateWrapper = BB.el();
    //BB.appendTextDiv(templateWrapper, "Preset Resolutions: <br />");
    const presetFitBtn = BB.el({tagName: 'button'});
    templateWrapper.style.marginBottom = '10px';
    const presetCurrentBtn = BB.el({tagName: 'button'});
    const presetSquareBtn = BB.el({tagName: 'button'});
    const presetLandscapeBtn = BB.el({tagName: 'button'});
    const presetPortraitBtn = BB.el({tagName: 'button'});
    const presetOversizeBtn = BB.el({tagName: 'button'});

    presetCurrentBtn.textContent = LANG('new-current');
    presetFitBtn.textContent = LANG('new-fit');
    presetOversizeBtn.textContent = LANG('new-oversize');
    presetLandscapeBtn.textContent = LANG('new-landscape');
    presetPortraitBtn.textContent = LANG('new-portrait');
    presetSquareBtn.textContent = LANG('new-square');

    presetCurrentBtn.style.marginRight = '5px';
    presetFitBtn.style.marginRight = '5px';
    presetOversizeBtn.style.marginRight = '5px';
    presetLandscapeBtn.style.marginTop = '5px';
    presetLandscapeBtn.style.marginRight = '5px';
    presetPortraitBtn.style.marginTop = '5px';
    presetPortraitBtn.style.marginRight = '5px';

    templateWrapper.append(
        presetCurrentBtn,
        presetFitBtn,
        presetOversizeBtn,
        presetSquareBtn,
        presetLandscapeBtn,
        presetPortraitBtn
    );

    const templatePadding = 0;

    presetCurrentBtn.onclick = function (): void {
        widthInput.value = '' + canvasWidth;
        heightInput.value = '' + canvasHeight;
        updateRatio();
    };
    presetFitBtn.onclick = function (): void {
        widthInput.value = '' + workspaceWidth;
        heightInput.value = '' + workspaceHeight;
        updateRatio();
    };
    presetOversizeBtn.onclick = function (): void {
        widthInput.value = '' + (workspaceWidth + 500);
        heightInput.value = '' + (workspaceHeight + 500);
        updateRatio();
    };
    presetSquareBtn.onclick = function (): void {
        const sizeObj = createRatioSize(1, 1, workspaceWidth, workspaceHeight, templatePadding);
        widthInput.value = '' + Math.round(sizeObj.width);
        heightInput.value = '' + Math.round(sizeObj.height);
        updateRatio();
    };
    presetLandscapeBtn.onclick = function (): void {
        const sizeObj = createRatioSize(4, 3, workspaceWidth, workspaceHeight, templatePadding);
        widthInput.value = '' + Math.round(sizeObj.width);
        heightInput.value = '' + Math.round(sizeObj.height);
        updateRatio();
    };
    presetPortraitBtn.onclick = function (): void {
        const sizeObj = createRatioSize(3, 4, workspaceWidth, workspaceHeight, templatePadding);
        widthInput.value = '' + Math.round(sizeObj.width);
        heightInput.value = '' + Math.round(sizeObj.height);
        updateRatio();
    };

    const select = new Select({
        isFocusable: true,
        optionArr: [
            ['screen', LANG('new-screen')],
            ['16 9', LANG('new-video') + ' 16:9'],
            ['3 2', '3:2'],
            ['5 3', '5:3'],
            ['2 1', '2:1'],
            ['paper', LANG('new-din-paper') + ' √2:1'],
            ['9 16', '9:16'],
            ['2 3', '2:3'],
            ['3 5', '3:5'],
            ['1 2', '1:2'],
            ['1 1.4142135623730951', '1:√2'],
        ],
        onChange: function (val): void {
            if (val === 'screen') {
                widthInput.value = '' + window.screen.width;
                heightInput.value = '' + window.screen.height;
            } else if (val === 'paper') {
                const sizeObj = createRatioSize(Math.sqrt(2), 1, workspaceWidth, workspaceHeight, templatePadding);
                widthInput.value = '' + Math.round(sizeObj.width);
                heightInput.value = '' + Math.round(sizeObj.height);
            } else {
                const split = val.split(' ');
                const sizeObj = createRatioSize(parseFloat(split[0]), parseFloat(split[1]), workspaceWidth, workspaceHeight, templatePadding);
                widthInput.value = '' + Math.round(sizeObj.width);
                heightInput.value = '' + Math.round(sizeObj.height);
            }
            updateRatio();
            select.setValue(undefined);
        },
    });
    setTimeout(() => {
        // safari: not empty without also setting it to null via timeout
        select.setValue(undefined);
    }, 0);
    BB.css(select.getElement(), {
        width: '80px',
    });
    templateWrapper.append(select.getElement());

    let backgroundRGBA = {r: 255, g: 255, b: 255, a: 1};

    const colorOptionsArr = [
        {r: 255, g: 255, b: 255, a: 1},
        {r: 0, g: 0, b: 0, a: 1},
        {r: 0, g: 0, b: 0, a: 0},
        {
            r: currentColor.r,
            g: currentColor.g,
            b: currentColor.b,
            a: 1,
        },
        {
            r: secondaryColor.r,
            g: secondaryColor.g,
            b: secondaryColor.b,
            a: 1,
        },
    ];
    let initColorIndex = 0;
    if (theme.isDark()) {
        colorOptionsArr.forEach((item, index) => {
            if (item.r === ERASE_COLOR && item.g === ERASE_COLOR && item.b === ERASE_COLOR) {
                initColorIndex = index;
                backgroundRGBA = item;
            }
        });
    }

    const colorOptions = new ColorOptions({
        colorArr: colorOptionsArr,
        initialIndex: initColorIndex,
        onChange: function (rgbaObj): void {
            backgroundRGBA = rgbaObj!;
            preview.style.backgroundColor = 'rgba(' + rgbaObj!.r + ',' + rgbaObj!.g + ',' + rgbaObj!.b + ', ' + rgbaObj!.a + ')';
        },
    });

    const previewWrapper = BB.el({
        className: 'kl-transparent-preview',
        css: {
            boxSizing: 'border-box',
            width: '340px',
            height: '140px',
            display: 'table',
            padding: '10px',
            marginTop: '10px',
            marginLeft: '-20px',
        },
    });
    const preview = BB.el({
        className: 'kl-transparent-preview__canvas',
        css: {
            width: 200 + 'px',
            height: 100 + 'px',
            backgroundColor: 'rgba(' + backgroundRGBA.r + ',' + backgroundRGBA.g + ',' + backgroundRGBA.b + ', ' + backgroundRGBA.a + ')',
            marginLeft: 'auto',
            marginRight: 'auto',
            color: '#aaa',
            fontSize: '16px',
            fontWeight: 'bold',
            textAlign: 'center',
            verticalAlign: 'center',
            display: 'table',
            overflow: 'hidden',
        },
    });
    BB.el({
        parent: previewWrapper,
        content: preview,
        css: {
            display: 'table-cell',
            verticalAlign: 'middle',
        },
    });
    BB.el({
        parent: preview,
        css: {
            display: 'table-cell',
            verticalAlign: 'middle',
        },
    });

    function updateRatio (): void {
        widthInput.value = '' + Math.min(maxCanvasSize, parseInt(widthInput.value));
        heightInput.value = '' + Math.min(maxCanvasSize, parseInt(heightInput.value));

        function hcf (u: number, v: number): number {
            let U = u, V = v;
            // eslint-disable-next-line no-constant-condition
            while (true) {
                if (!(U %= V)) {
                    return V;
                }
                if (!(V %= U)) {
                    return U;
                }
            }
        }

        let w = parseInt(widthInput.value);
        let h = parseInt(heightInput.value);
        if (w < 1 || w > maxCanvasSize || h < 1 || h > maxCanvasSize) {
            if (w > maxCanvasSize) {
                w = maxCanvasSize;
            } else if (h > maxCanvasSize) {
                h = maxCanvasSize;
            }

            widthInput.value = '' + w;
            heightInput.value = '' + h;
        }

        //generated canvas size doesn't always match ratio. so check if a common ratio is very close
        const commonRatios = [
            [1, 2], [2, 1],
            [2, 3], [3, 2],
            [3, 4], [4, 3],
            [4, 5], [5, 4],
            [16, 9], [9, 16],
            [3, 2], [2, 3],
            [5, 3], [3, 5],
            [2, 1], [1, 2],
            [1.414, 1], [1, 1.414],
        ];
        const reducedArr = BB.reduce(w, h);
        let closestRatio = commonRatios[0];
        let closestDistance = Math.abs(commonRatios[0][0] / commonRatios[0][1] - reducedArr[0] / reducedArr[1]);
        for (let i = 0; i < commonRatios.length; i++) {
            if (Math.abs(commonRatios[i][0] / commonRatios[i][1] - reducedArr[0] / reducedArr[1]) < closestDistance) {
                closestRatio = commonRatios[i];
                closestDistance = Math.abs(commonRatios[i][0] / commonRatios[i][1] - reducedArr[0] / reducedArr[1]);
            }
        }
        //display ratio
        if (closestDistance > 0 && closestDistance < 0.005) {
            ratioWrapper.innerText = LANG('new-ratio') + ': ~' + closestRatio[0] + ':' + closestRatio[1];
        } else {
            ratioWrapper.innerText = LANG('new-ratio') + ': ' + reducedArr[0] + ':' + reducedArr[1];
        }

        const realw = w;
        const T = hcf(w, h);
        w /= T;
        h /= T;
        w *= 260;
        h *= 260;
        if (w > 260) {
            h = 260 / w * h;
            w = 260;
        }
        if (h > 100) {
            w = 100 / h * w;
            h = 100;

        }

        preview.style.width = w + 'px';
        preview.style.height = h + 'px';
        BB.createCheckerDataUrl(parseInt('' + (30 * (w / realw))), function (url) {
            previewWrapper.style.background = 'url(' + url + ')';
        }, theme.isDark());
    }
    theme.addIsDarkListener(updateRatio);

    widthInput.onchange = (): void => {
        if (widthInput.value === '' || parseInt(widthInput.value) < 0) {
            widthInput.value = '1';
        }
        updateRatio();
    };
    widthInput.onkeyup = (): void => {
        updateRatio();
    };
    heightInput.onchange = (): void => {
        if (heightInput.value === '' || parseFloat(heightInput.value) < 0) {
            heightInput.value = '1';
        }
        updateRatio();
    };
    heightInput.onkeyup = (): void => {
        updateRatio();
    };
    updateRatio();

    newImDiv.append(templateWrapper);
    const secondRow = BB.el({
        parent: newImDiv,
        css: {
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'flex-end',
        },
    });
    const secondRowLeft = BB.el({
        parent: secondRow,
    });
    secondRowLeft.append(sizeTable, ratioWrapper);
    secondRow.append(colorOptions.getElement());

    newImDiv.append(previewWrapper);

    showModal({
        target: document.body,
        message: `<b>${LANG('new-title')}</b>`,
        div: newImDiv,
        buttons: ['Ok', 'Cancel'],
        callback: function (result) {
            BB.unsetEventHandler(widthInput, 'onclick', 'onchange', 'onkeyup');
            BB.unsetEventHandler(widthInput, 'onclick', 'onchange', 'onkeyup');

            BB.unsetEventHandler(presetCurrentBtn, 'onclick');
            BB.unsetEventHandler(presetFitBtn, 'onclick');
            BB.unsetEventHandler(presetOversizeBtn, 'onclick');
            BB.unsetEventHandler(presetSquareBtn, 'onclick');
            BB.unsetEventHandler(presetLandscapeBtn, 'onclick');
            BB.unsetEventHandler(presetPortraitBtn, 'onclick');

            select.destroy();
            colorOptions.destroy();
            theme.removeIsDarkListener(updateRatio);

            if (
                result === 'Cancel'
                || parseInt(widthInput.value) <= 0
                || parseInt(heightInput.value) <= 0
                || isNaN(parseInt(widthInput.value))
                || isNaN(parseInt(heightInput.value))
            ) {
                onCancel();
                return;
            }
            onConfirm(
                parseInt(widthInput.value),
                parseInt(heightInput.value),
                backgroundRGBA
            );
        },
        clickOnEnter: 'Ok',
    });

}


code:
import {BB} from '../../../bb/bb';
import {DynamicModal} from './base/dynamic-modal';
import {LANG} from '../../../language/language';

export function showIframeModal (
    url: string,
    isEmbed: boolean,
) {

    // window very small, modal might look bad
    if (!isEmbed && (window.innerHeight < 500 || window.innerWidth < 700)) {
        window.open(url);
        return;
    }

    const iframe = BB.el({
        tagName: 'iframe',
        custom: {
            src: url,
        },
        css: {
            width: '100%',
            height: '100%',
            opacity: '0',
        },
    });
    setTimeout(() => {
        iframe.style.opacity = ''; // fallback
    }, 500);

    const titleEl = BB.el();

    let linkEl: HTMLElement | undefined;
    if (!isEmbed) {
        linkEl = BB.el({
            tagName: 'a',
            parent: titleEl,
            content: LANG('modal-new-tab'),
            custom: {
                href: 'help',
                target: '_blank',
            },
            onClick: function () {
                popup.close();
            },
        });
        iframe.onload = () => {
            if (linkEl && iframe.contentWindow) {
                BB.setAttributes(linkEl, {
                    href: '' + iframe.contentWindow.location,
                });
            }
            iframe.style.opacity = '';
        };
    }

    const popup = new DynamicModal({
        title: titleEl,
        content: iframe,
        width: 880,
        isMaxHeight: true,
        onClose: () => {
            if (linkEl) {
                iframe.src = 'about:blank';
                BB.destroyEl(linkEl);
                linkEl = undefined;
            }
        },
    });

}