KennethanCeyer/formulize

View on GitHub
src/ui/ui.manager.ts

Summary

Maintainability
F
1 wk
Test Coverage
import { convert, valid } from 'metric-parser';
import { FormulizeTokenHelper } from '../token.helper';
import { Tree } from 'metric-parser/dist/types/tree/simple.tree/type';
import { UIElementHelper } from './ui.element.helper';
import { ElementPosition, FormulizeData, Position } from './ui.interface';
import { UIHelper } from './ui.helper';
import { UIPipe } from './ui.pipe';

export abstract class UIManager extends UIPipe {
    protected prevCursorIndex = 0;
    protected prevPosition: Position = { x: 0, y: 0 };
    protected dragged: boolean;
    protected moved: boolean;

    public pick(position: Position = { x: 0, y: 0 }) {
        this.removeCursor();
        this.cursor = $(UIElementHelper.getCursorElement(this.options.id));
        this.cursor.appendTo(this.container);

        const closestUnitElem = this.findClosestUnit(position);
        if (closestUnitElem)
            this.cursor.insertAfter(closestUnitElem);
        else
            this.cursor.prependTo(this.container);

        this.removeDrag();
    }

    public setData(data: Tree): void {
        this.clear();
        const result = convert(data);
        if (!result.code)
            this.insertData(result.data);
    }

    public getData<T extends Tree>(extractor?: (data: T) => void): T {
        const expression = this.getExpression();
        const result = convert(expression);

        if (extractor)
            extractor(result.data);

        return result.data;
    }

    protected triggerUpdate(): void {
        this.validate();
        this.pipeTrigger('input', this.getData());
    }

    private getExpression(): FormulizeData[] {
        return this.container
            .find(`.${this.options.id}-item`)
            .toArray()
            .map(elem => this.pipeParse(elem))
            .map(value => UIHelper.getDataValue(value));
    }

    protected startDrag(position: Position): void {
        this.dragged = true;
        this.moved = false;
        this.prevPosition = position;
        this.pick(position);
        this.prevCursorIndex = this.cursorIndex;
    }

    protected endDrag(position: Position): void {
        this.dragged = false;

        if (this.moved)
            return;

        this.moved = false;
        this.pick(position);
    }

    protected moveDrag(position: Position): void {
        if (!this.dragged)
            return;

        if (!this.moved) {
            this.moved = UIHelper.isOverDistance(this.prevPosition, position, 5);
            return;
        }

        this.removeDrag();
        this.pick(position);

        if (this.prevCursorIndex === this.cursorIndex)
            return;

        const positions = [this.prevCursorIndex, this.cursorIndex];
        positions.sort();

        const dragElem = $(UIElementHelper.getDragElement(this.options.id));
        if (this.cursorIndex >= this.prevCursorIndex)
            dragElem.insertBefore(this.cursor);
        else
            dragElem.insertAfter(this.cursor);

        this.selectRange(positions[0], positions[1]);
    }

    private findClosestUnit(position: Position): HTMLElement {
        const unitPositions: ElementPosition[] = this.container
            .children(`*:not(".${this.options.id}-cursor")`)
            .toArray()
            .map(elem => ({
                elem,
                x: $(elem).position().left + $(elem).outerWidth(),
                y: $(elem).position().top
            }));

        const closestUnitPositions = unitPositions
            .filter(unitPosition => unitPosition.x <= position.x && unitPosition.y <= position.y)
            .map(unitPosition => {
                const diffX = Math.abs(position.x - unitPosition.x);
                const diffY = Math.abs(position.y - unitPosition.y);
                return {
                    ...unitPosition,
                    diff: { x: diffX, y: diffY }
                };
            })
            .filter(unitPosition => unitPosition);;
        const maxY = Math.max(...closestUnitPositions.map(unitPosition => unitPosition.y));
        const filteredUnitPositions = closestUnitPositions.filter(unitPosition => unitPosition.y === maxY).length
            ? closestUnitPositions.filter(unitPosition => unitPosition.y === maxY)
            : closestUnitPositions.filter(unitPosition => unitPosition.y <= position.y);
        filteredUnitPositions.sort((a, b) => a.diff.x - b.diff.x || a.diff.y - b.diff.y);

        const closestUnitPosition = filteredUnitPositions.shift();

        return closestUnitPosition
            ? closestUnitPosition.elem
            : undefined;
    }

    public selectAll(): void {
        this.removeDrag();
        const dragElem = $(UIElementHelper.getDragElement(this.options.id));
        dragElem.prependTo(this.container);
        this.container
            .children(`:not(".${this.options.id}-cursor")`)
            .appendTo(dragElem);
    }

    public selectRange(start: number, end: number): void {
        if (!this.dragElem.length)
            return;

        this.container
            .children(`:not(".${this.options.id}-cursor")`)
            .filter(`:gt("${start}")`)
            .filter(`:lt("${end - start}")`)
            .add(this.container.children(`:not(".${this.options.id}-cursor")`).eq(start))
            .appendTo(this.dragElem);
    }

    protected removeBefore(): void {
        if (this.dragElem.length) {
            this.cursor.insertBefore(this.dragElem);
            this.dragElem.remove();
            this.triggerUpdate();
            return;
        }

        const prevCursorElem = this.cursor.prev();
        if (!this.cursor.length || !prevCursorElem.length)
            return;

        if (
            prevCursorElem.hasClass(`${this.options.id}-unit`) &&
            prevCursorElem.text().length > 1
        ) {
            const text = prevCursorElem.text();
            UIElementHelper.setUnitValue(this.options.id, prevCursorElem.get(0), text.substring(0, text.length - 1));
        } else
            prevCursorElem.remove();

        this.triggerUpdate();
    }

    protected removeAfter(): void {
        if (this.dragElem.length) {
            this.cursor.insertAfter(this.dragElem);
            this.dragElem.remove();
            this.triggerUpdate();
            return;
        }

        const nextCursorElem = this.cursor.next();
        if (!this.cursor.length || !nextCursorElem.length)
            return;

        if (
            nextCursorElem.hasClass(`${this.options.id}-unit`) &&
            nextCursorElem.text().length > 1
        ) {
            const text = nextCursorElem.text();
            UIElementHelper.setUnitValue(this.options.id, nextCursorElem.get(0), text.substring(1, text.length));
        } else
            nextCursorElem.remove();

        this.triggerUpdate();
    }

    protected dragFirst(): void {
        this.cursor.prevAll().prependTo(this.dragElem);
        this.cursor.insertAfter(this.dragElem);
    }

    protected dragLast(): void {
        this.cursor.nextAll().appendTo(this.dragElem);
        this.cursor.insertBefore(this.dragElem);
    }

    protected dragLeft(): void {
        if (UIElementHelper.isDrag(this.options.id, this.cursor.prev().get(0))) {
            this.dragElem.prev().prependTo(this.dragElem);
            this.moveCursorAfter(this.dragElem.get(0));
            return;
        }

        if (UIElementHelper.isDrag(this.options.id, this.cursor.next().get(0))) {
            const lastDraggedElem = this.dragElem.children().last();
            lastDraggedElem.insertAfter(this.dragElem);

            if (!this.dragElem.children().length)
                this.removeDrag();

            return;
        }
    }

    protected dragRight(): void {
        if (UIElementHelper.isDrag(this.options.id, this.cursor.next().get(0))) {
            this.dragElem.next().appendTo(this.dragElem);
            this.moveCursorBefore(this.dragElem.get(0));
            return;
        }

        if (UIElementHelper.isDrag(this.options.id, this.cursor.prev().get(0))) {
            const firstDraggedElem = this.dragElem.children().first();
            firstDraggedElem.insertBefore(this.dragElem);

            if (!this.dragElem.children().length)
                this.removeDrag();

            return;
        }
    }

    private moveCursorBefore(elem: HTMLElement) {
        if (!$(elem).length)
            return;

        this.cursor.insertBefore($(elem));
    }

    private moveCursorAfter(elem: HTMLElement) {
        if (!$(elem).length)
            return;

        this.cursor.insertAfter($(elem));
    }

    protected moveLeftCursor(dragMode: boolean = false): void {
        const prevCursorElem = this.cursor.prev();

        if (!this.cursor.length || !dragMode) {
            this.moveCursorBefore(prevCursorElem.get(0));
            this.removeDrag();
            return;
        }

        if (!this.dragElem.length) {
            if (!prevCursorElem.length)
                return;

            const dragElem = $(UIElementHelper.getDragElement(this.options.id));
            dragElem.insertBefore(this.cursor);
            prevCursorElem.prependTo(this.dragElem);
            return;
        }

        this.dragLeft();
    }

    protected moveUpCursor(): void {
        if (!this.cursor.length)
            return;

        this.pick({
            x: this.cursor.position().left + this.cursor.outerWidth(),
            y: this.cursor.position().top - this.cursor.outerHeight() / 2
        });
    }

    protected moveRightCursor(dragMode: boolean = false): void {
        const nextCursorElem = this.cursor.next();

        if (!this.cursor.length || !dragMode) {
            this.moveCursorAfter(nextCursorElem.get(0));
            this.removeDrag();
            return;
        }

        if (!this.dragElem.length) {
            if (!nextCursorElem.length)
                return;

            const dragElem = $(UIElementHelper.getDragElement(this.options.id));
            dragElem.insertAfter(this.cursor);
            nextCursorElem.appendTo(this.dragElem);
            return;
        }

        this.dragRight();
    }

    protected moveDownCursor(): void {
        if (!this.cursor.length)
            return;

        this.pick({
            x: this.cursor.position().left + this.cursor.outerWidth(),
            y: this.cursor.position().top + this.cursor.outerHeight() * 1.5
        });
    }

    protected moveFirstCursor(dragMode: boolean = false): void {
        const firstCursorElem = this.container.children(':first');
        if (!this.cursor.length || !firstCursorElem.length || !dragMode) {
            this.removeDrag();
            this.moveCursorBefore(firstCursorElem.get(0));
            return;
        }

        if (!this.dragElem.length) {
            const dragElem = $(UIElementHelper.getDragElement(this.options.id));
            dragElem.insertAfter(this.cursor);
        }

        this.dragFirst();
    }

    protected moveLastCursor(dragMode: boolean = false): void {
        const lastCursorElem = this.container.children(':last');
        if (!this.cursor.length || !lastCursorElem.length || !dragMode) {
            this.removeDrag();
            this.moveCursorAfter(lastCursorElem.get(0));
            return;
        }

        if (!this.dragElem.length) {
            const dragElem = $(UIElementHelper.getDragElement(this.options.id));
            dragElem.insertBefore(this.cursor);
        }

        this.dragLast();
    }

    public clear(): void {
        this.removeCursor();
        this.removeUnit();
        this.triggerUpdate();
    }

    public blur(): void {
        if (!this.cursor)
            return;

        this.cursor.remove();
        this.removeDrag();
    }

    public removeDrag(): void {
        this.dragElem.children().insertBefore(this.dragElem);
        this.dragElem.remove();
        this.triggerUpdate();
    }

    public insert(data: FormulizeData, position?: Position): void {
        if (!data)
            return;

        const pipedData = this.pipeInsert(data);

        if (!this.cursor || !this.cursor.length || position)
            this.pick(position);

        if (typeof pipedData === 'string' || typeof pipedData === 'number') {
            this.insertValue(String(pipedData));
            return;
        }

        if (!UIHelper.isDOM(pipedData))
            return;

        const insertElem = $(pipedData);
        insertElem.addClass(`${this.options.id}-item`);
        insertElem.insertBefore(this.cursor);

        this.triggerUpdate();
    }

    public insertValue(value: string): void {
        if (!FormulizeTokenHelper.isValid(value))
            return;

        if (FormulizeTokenHelper.isNumeric(value)) {
            const unitElem = $(UIElementHelper.getUnitElement(this.options.id, value));

            if (this.dragElem.length) {
                this.cursor.insertBefore(this.dragElem);
                this.dragElem.remove();
            }

            if (this.cursor && this.cursor.length)
                this.cursor.before(unitElem);
            else
                this.container.append(unitElem);

            this.mergeUnit(unitElem[0]);

            this.triggerUpdate();
            return;
        }

        const operatorElem = $(UIElementHelper.getOperatorElement(this.options.id, value));
        if (this.cursor && this.cursor.length)
            this.cursor.before(operatorElem);
        else
            this.container.append(operatorElem);

        if (FormulizeTokenHelper.isBracket(value))
            operatorElem.addClass(`${this.options.id}-bracket`);
    }

    public insertData(data: string | string[] | any[]): void {
        const arrayData = typeof data === 'string'
            ? data.split('')
            : data;

        arrayData.forEach(value => this.insert(value));
        this.triggerUpdate();
    }

    public validate(extractor?: (valid: boolean) => void): boolean {
        const data = this.getData();

        if (!data)
            return;

        const isValid = valid(data);

        this.updateStatus(isValid);

        if (extractor)
            extractor(isValid);

        return isValid;
    }
}