CCViewBase.ts 6.03 KB
import { Emitter } from "simba-eventkit";
import { View, PureView, Subview, PureSubview, OnSubviewCreated } from "./ViewBase";

type PickKeysForType<T extends {}, U> = {
    [P in keyof T]: T[P] extends U ? P : never
}[keyof T];

class CCViewBase extends cc.Component {
    protected _emitter: Emitter = new Emitter;
    protected _showed = false;
    private _isHidden = false;
    get isHidden() { return this._isHidden || !this.node.active }

    onShow = this._emitter.createEvent<() => void>();
    onHide = this._emitter.createEvent<() => void>();

    private _posX: number;

    show(): void {
        if (!this.node.parent || !this.isHidden) return;
        if (this.isHidden) {
            this.node.active = true;
            this._isHidden = false;
            this.node.opacity = 255;
            if (this._posX !== undefined)
                this.node.x = this._posX;
            this.onShow.emit();
        }
    }

    hide(): void {
        if (!this.node.parent || this.isHidden) return;
        if (!this.isHidden) {
            this._isHidden = true;
            this._posX = this.node.x;
            this.node.opacity = 0;
            this.node.x = 10000;
            this.onHide.emit();
        }
    }
}

export class CCView extends CCViewBase implements View {
    readonly _type: "View" = "View";
    onClose = this._emitter.createEvent<(destroy?: boolean) => void>();

    open(parent: CCView) {
        this.node.parent = parent.node;
    }

    close(destroy?: boolean): void {
        if (!this.node.parent) return;
        if (destroy) {
            this.node.destroy();
        } else {
            this.node.removeFromParent(false);
        }
        this.onClose.emit(destroy);
    }
}

export class CCSubview extends CCViewBase implements Subview {
    readonly _type: "Subview" = "Subview";

    constructor() {
        super();

        OnSubviewCreated.emit(this);
    }

    onLoad() {
        this.name = this.node.name;
    }

    attachParent(parent: CCView) {
        this.node.parent = parent.node;
    }
}

abstract class PureViewBase<P> implements PureViewBase<P> {
    protected _prevProps: Readonly<P>;
    protected _props: Readonly<P>;
    private _tempProps: Readonly<P>;
    private _prepareUpdate = false;
    private _propBinder: { [key: string]: { target: object | Function, prop: string }[] } = {};

    abstract scheduleOnce(callback: Function, delay?: number): void;

    setProps(props: P) {
        this._tempProps = props;
        if (!this._props) {
            this._setProps();
        } else {
            if (!this._prepareUpdate) {
                this._prepareUpdate = true;
                this.scheduleOnce(() => { // delay one frame, update at most once per frame
                    this._setProps();
                });
            }
        }
    }

    protected _setProps() {
        this._prepareUpdate = false;
        this._prevProps = this._props;
        this._props = { ...this._tempProps };
        delete this._tempProps;
        this.onPropsReceive();
        if (!this._prevProps) {
            this.onPropsLoad(this._props);
        } else {
            let keys = new Set([...Object.keys(this._prevProps), ...Object.keys(this._props)]);
            for (let key of keys) {
                if (this._props[key] !== this._prevProps[key]) {
                    this.onPropChange(key as Extract<keyof P, string>);
                }
            }
        }
    }

    updateProps(props: Partial<P>) {
        if (!this._props) throw new Error("setProps first.");
        let fullProps = this._tempProps ? { ...this._tempProps, ...props } : { ...this._props, ...props };
        this.setProps(fullProps);
    }

    /**
    * onLoad() {
    *     this.bindProp("title", this.titleLabel, "string");
    *     this.bindProp("percent", (value) => this.percentLabel.string = value + "%");
    * }
     */
    protected bindProp<T extends {}, K extends keyof P>(key: Extract<K, string>, target: (v: P[K]) => void): void;
    protected bindProp<T extends {}, K extends keyof P>(key: Extract<K, string>, target: T, propName: Extract<PickKeysForType<T, P[K]>, string>): void;
    protected bindProp(key: string, target, prop?) {
        if (!this._propBinder[key]) this._propBinder[key] = [];
        this._propBinder[key].push({ target, prop });
    }

    protected onPropsReceive(): void {

    }

    protected onPropsLoad(props: Readonly<P>) {
        for (let key in props) {
            if (this._propBinder[key]) {
                for (const { target, prop } of this._propBinder[key]) {
                    if (typeof target === "function") {
                        target(this._props[key]);
                    } else {
                        target[prop] = this._props[key];
                    }
                }
            }
        }
    }

    protected onPropChange(key: Extract<keyof P, string>) {
        if (this._propBinder[key]) {
            for (const { target, prop } of this._propBinder[key]) {
                if (typeof target === "function") {
                    target(this._props[key]);
                } else {
                    target[prop] = this._props[key];
                }
            }
        }
    }
}

export class CCPureView<P> extends CCView {
    constructor() {
        super();
        (this as any)._propBinder = {};
    }
    onLoad() { }
}
export interface CCPureView<P> extends PureViewBase<P> { }

export class CCPureSubview<P> extends CCSubview {
    constructor() {
        super();
        (this as any)._propBinder = {};
    }
}
export interface CCPureSubview<P> extends PureViewBase<P> { }

function applyMixins(derivedCtor: any, constructors: any[]) {
    constructors.forEach((baseCtor) => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
            if (name !== "constructor") {
                Object.defineProperty(
                    derivedCtor.prototype,
                    name,
                    Object.getOwnPropertyDescriptor(baseCtor.prototype, name)!
                );
            }
        });
    });
}

applyMixins(CCPureView, [PureViewBase]);
applyMixins(CCPureSubview, [PureViewBase]);