@nativescript/core
Version: 
A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.
422 lines • 16.6 kB
JavaScript
import { CoreTypes } from '../../../core-types';
import { Trace } from '../../../trace';
import { CSSType, View } from '../../core/view';
import { GridLayout } from '../grid-layout';
import { Animation } from '../../animation';
import { isNumber } from '../../../utils/types';
import { _findRootLayoutById, _pushIntoRootLayoutStack, _removeFromRootLayoutStack, _geRootLayoutFromStack } from './root-layout-stack';
let RootLayoutBase = class RootLayoutBase extends GridLayout {
    constructor() {
        super(...arguments);
        this._popupViews = [];
    }
    initNativeView() {
        super.initNativeView();
        _pushIntoRootLayoutStack(this);
    }
    disposeNativeView() {
        super.disposeNativeView();
        _removeFromRootLayoutStack(this);
    }
    _onLivesync(context) {
        let handled = false;
        if (this._popupViews.length > 0) {
            this.closeAll();
            handled = true;
        }
        if (super._onLivesync(context)) {
            handled = true;
        }
        return handled;
    }
    /**
     * Ability to add any view instance to composite views like layers.
     *
     * @param view
     * @param options
     * @returns
     */
    open(view, options = {}) {
        return new Promise((resolve, reject) => {
            if (!(view instanceof View)) {
                return reject(new Error(`Invalid open view: ${view}`));
            }
            if (this.hasChild(view)) {
                return reject(new Error(`View ${view} has already been added to the root layout`));
            }
            const toOpen = [];
            const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
            // Keep track of the views locally to be able to use their options later
            this._popupViews.push({ view: view, options: options });
            // Always begin with view invisible when adding dynamically
            view.opacity = 0;
            // Add view to view tree before adding shade cover
            // Before being added to view tree, shade cover calculates the index to be inserted based on existing popup views
            this.insertChild(view, this.getChildrenCount());
            if (options.shadeCover) {
                // perf optimization note: we only need 1 layer of shade cover
                // we just update properties if needed by additional overlaid views
                if (this._shadeCover) {
                    // overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
                    toOpen.push(this.updateShadeCover(this._shadeCover, options.shadeCover));
                }
                else {
                    toOpen.push(this.openShadeCover(options.shadeCover));
                }
            }
            toOpen.push(new Promise((res, rej) => {
                setTimeout(() => {
                    // only apply initial state and animate after the first tick - ensures safe areas and other measurements apply correctly
                    this.applyInitialState(view, enterAnimationDefinition);
                    this.getEnterAnimation(view, enterAnimationDefinition)
                        .play()
                        .then(() => {
                        this.applyDefaultState(view);
                        view.notify({ eventName: 'opened', object: view });
                        res();
                    }, (err) => {
                        rej(new Error(`Error playing enter animation: ${err}`));
                    });
                });
            }));
            Promise.all(toOpen).then(() => {
                resolve();
            }, (err) => {
                reject(err);
            });
        });
    }
    /**
     * Ability to remove any view instance from composite views.
     * Optional animation parameter to overwrite close animation declared when opening popup.
     *
     * @param view
     * @param exitTo
     * @returns
     */
    close(view, exitTo) {
        return new Promise((resolve, reject) => {
            if (!(view instanceof View)) {
                return reject(new Error(`Invalid close view: ${view}`));
            }
            if (!this.hasChild(view)) {
                return reject(new Error(`Unable to close popup. View ${view} not found`));
            }
            const toClose = [];
            const popupIndex = this.getPopupIndex(view);
            const poppedView = this._popupViews[popupIndex];
            const cleanupAndFinish = () => {
                view.notify({ eventName: 'closed', object: view });
                this.removeChild(view);
                resolve();
            };
            // use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
            const exitAnimationDefinition = exitTo || poppedView?.options?.animation?.exitTo;
            // Remove view from tracked popupviews
            if (popupIndex > -1) {
                this._popupViews.splice(popupIndex, 1);
            }
            toClose.push(new Promise((res, rej) => {
                if (exitAnimationDefinition) {
                    this.getExitAnimation(view, exitAnimationDefinition)
                        .play()
                        .then(res, (err) => {
                        rej(new Error(`Error playing exit animation: ${err}`));
                    });
                }
                else {
                    res();
                }
            }));
            if (this._shadeCover) {
                // Update shade cover with the topmost popupView options (if not specifically told to ignore)
                if (this._popupViews.length) {
                    if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
                        const shadeCoverOptions = this._popupViews[this._popupViews.length - 1].options?.shadeCover;
                        if (shadeCoverOptions) {
                            toClose.push(this.updateShadeCover(this._shadeCover, shadeCoverOptions));
                        }
                    }
                }
                else {
                    // Remove shade cover animation if this is the last opened popup view
                    toClose.push(this.closeShadeCover(poppedView?.options?.shadeCover));
                }
            }
            Promise.all(toClose).then(() => {
                cleanupAndFinish();
            }, (err) => {
                reject(err);
            });
        });
    }
    closeAll() {
        const toClose = [];
        const views = this._popupViews.map((popupView) => popupView.view);
        // Close all views at the same time and wait for all of them
        for (const view of views) {
            toClose.push(this.close(view));
        }
        return Promise.all(toClose);
    }
    getShadeCover() {
        return this._shadeCover;
    }
    openShadeCover(options = {}) {
        return new Promise((resolve) => {
            const childrenCount = this.getChildrenCount();
            let indexToAdd;
            if (this._popupViews.length) {
                const { view } = this._popupViews[0];
                const index = this.getChildIndex(view);
                indexToAdd = index > -1 ? index : childrenCount;
            }
            else {
                indexToAdd = childrenCount;
            }
            if (this._shadeCover) {
                if (Trace.isEnabled()) {
                    Trace.write(`RootLayout shadeCover already open.`, Trace.categories.Layout, Trace.messageType.warn);
                }
                resolve();
            }
            else {
                // Create the one and only shade cover
                const shadeCover = this.createShadeCover();
                shadeCover.on('loaded', () => {
                    this._initShadeCover(shadeCover, options);
                    this.updateShadeCover(shadeCover, options).then(() => {
                        resolve();
                    });
                });
                this._shadeCover = shadeCover;
                // Insert shade cover at index right below the first popup view
                this.insertChild(this._shadeCover, indexToAdd);
            }
        });
    }
    closeShadeCover(shadeCoverOptions = {}) {
        return new Promise((resolve) => {
            // if shade cover is displayed and the last popup is closed, also close the shade cover
            if (this._shadeCover) {
                return this._closeShadeCover(this._shadeCover, shadeCoverOptions).then(() => {
                    if (this._shadeCover) {
                        this._shadeCover.off('loaded');
                        if (this._shadeCover.parent) {
                            this.removeChild(this._shadeCover);
                        }
                    }
                    this._shadeCover = null;
                    // cleanup any platform specific details related to shade cover
                    this._cleanupPlatformShadeCover();
                    resolve();
                });
            }
            resolve();
        });
    }
    topmost() {
        return this._popupViews.length ? this._popupViews[this._popupViews.length - 1].view : null;
    }
    /**
     * This method causes the requested view to overlap its siblings by bring it to front.
     *
     * @param view
     * @param animated
     * @returns
     */
    bringToFront(view, animated = false) {
        return new Promise((resolve, reject) => {
            if (!(view instanceof View)) {
                return reject(new Error(`Invalid bringToFront view: ${view}`));
            }
            if (!this.hasChild(view)) {
                return reject(new Error(`View ${view} is not a child of the root layout`));
            }
            const popupIndex = this.getPopupIndex(view);
            if (popupIndex < 0) {
                return reject(new Error(`View ${view} is not a child of the root layout`));
            }
            if (popupIndex == this._popupViews.length - 1) {
                return reject(new Error(`View ${view} is already the topmost view in the rootlayout`));
            }
            // keep the popupViews array in sync with the stacking of the views
            const currentView = this._popupViews[popupIndex];
            this._popupViews.splice(popupIndex, 1);
            this._popupViews.push(currentView);
            const exitAnimation = this.getViewExitState(view);
            if (animated && exitAnimation) {
                this.getExitAnimation(view, exitAnimation)
                    .play()
                    .then(() => {
                    this._bringToFront(view);
                    const initialState = this.getViewInitialState(currentView.view);
                    if (initialState) {
                        this.applyInitialState(view, initialState);
                        this.getEnterAnimation(view, initialState)
                            .play()
                            .then(() => {
                            this.applyDefaultState(view);
                        })
                            .catch((ex) => {
                            reject(new Error(`Error playing enter animation: ${ex}`));
                        });
                    }
                    else {
                        this.applyDefaultState(view);
                    }
                })
                    .catch((ex) => {
                    this._bringToFront(view);
                    reject(new Error(`Error playing exit animation: ${ex}`));
                });
            }
            else {
                this._bringToFront(view);
            }
            // update shadeCover to reflect topmost's shadeCover options
            const shadeCoverOptions = currentView?.options?.shadeCover;
            if (shadeCoverOptions) {
                this.updateShadeCover(this._shadeCover, shadeCoverOptions);
            }
            resolve();
        });
    }
    getPopupIndex(view) {
        return this._popupViews.findIndex((popupView) => popupView.view === view);
    }
    getViewInitialState(view) {
        const popupIndex = this.getPopupIndex(view);
        if (popupIndex === -1) {
            return;
        }
        const initialState = this._popupViews[popupIndex]?.options?.animation?.enterFrom;
        if (!initialState) {
            return;
        }
        return initialState;
    }
    getViewExitState(view) {
        const popupIndex = this.getPopupIndex(view);
        if (popupIndex === -1) {
            return;
        }
        const exitAnimation = this._popupViews[popupIndex]?.options?.animation?.exitTo;
        if (!exitAnimation) {
            return;
        }
        return exitAnimation;
    }
    applyInitialState(targetView, enterFrom) {
        const animationOptions = {
            ...defaultTransitionAnimation,
            ...(enterFrom || {}),
        };
        targetView.translateX = animationOptions.translateX;
        targetView.translateY = animationOptions.translateY;
        targetView.scaleX = animationOptions.scaleX;
        targetView.scaleY = animationOptions.scaleY;
        targetView.rotate = animationOptions.rotate;
        targetView.opacity = animationOptions.opacity;
    }
    applyDefaultState(targetView) {
        targetView.translateX = 0;
        targetView.translateY = 0;
        targetView.scaleX = 1;
        targetView.scaleY = 1;
        targetView.rotate = 0;
        targetView.opacity = 1;
    }
    getEnterAnimation(targetView, enterFrom) {
        const animationOptions = {
            ...defaultTransitionAnimation,
            ...(enterFrom || {}),
        };
        return new Animation([
            {
                target: targetView,
                translate: { x: 0, y: 0 },
                scale: { x: 1, y: 1 },
                rotate: 0,
                opacity: 1,
                duration: animationOptions.duration,
                curve: animationOptions.curve,
            },
        ]);
    }
    getExitAnimation(targetView, exitTo) {
        return new Animation([this.getExitAnimationDefinition(targetView, exitTo)]);
    }
    getExitAnimationDefinition(targetView, exitTo) {
        return {
            target: targetView,
            ...defaultTransitionAnimation,
            ...(exitTo || {}),
            translate: { x: isNumber(exitTo.translateX) ? exitTo.translateX : defaultTransitionAnimation.translateX, y: isNumber(exitTo.translateY) ? exitTo.translateY : defaultTransitionAnimation.translateY },
            scale: { x: isNumber(exitTo.scaleX) ? exitTo.scaleX : defaultTransitionAnimation.scaleX, y: isNumber(exitTo.scaleY) ? exitTo.scaleY : defaultTransitionAnimation.scaleY },
        };
    }
    createShadeCover() {
        const shadeCover = new GridLayout();
        shadeCover.verticalAlignment = 'bottom';
        return shadeCover;
    }
    updateShadeCover(shade, shadeOptions = {}) {
        if (shadeOptions.tapToClose !== undefined && shadeOptions.tapToClose !== null) {
            shade.off('tap');
            if (shadeOptions.tapToClose) {
                shade.on('tap', () => {
                    this.closeAll();
                });
            }
        }
        return this._updateShadeCover(shade, shadeOptions);
    }
    hasChild(view) {
        return this.getChildIndex(view) >= 0;
    }
    _bringToFront(view) { }
    _initShadeCover(view, shadeOption) { }
    _updateShadeCover(view, shadeOption) {
        return new Promise(() => { });
    }
    _closeShadeCover(view, shadeOptions) {
        return new Promise(() => { });
    }
    _cleanupPlatformShadeCover() { }
};
RootLayoutBase = __decorate([
    CSSType('RootLayout')
], RootLayoutBase);
export { RootLayoutBase };
export function getRootLayout() {
    return _geRootLayoutFromStack(0);
}
export function getRootLayoutById(id) {
    return _findRootLayoutById(id);
}
export const defaultTransitionAnimation = {
    translateX: 0,
    translateY: 0,
    scaleX: 1,
    scaleY: 1,
    rotate: 0,
    opacity: 1,
    duration: 300,
    curve: CoreTypes.AnimationCurve.easeIn,
};
export const defaultShadeCoverTransitionAnimation = {
    ...defaultTransitionAnimation,
    opacity: 0, // default to fade in/out
};
export const defaultShadeCoverOptions = {
    opacity: 0.5,
    color: '#000000',
    tapToClose: true,
    animation: {
        enterFrom: defaultShadeCoverTransitionAnimation,
        exitTo: defaultShadeCoverTransitionAnimation,
    },
    ignoreShadeRestore: false,
};
//# sourceMappingURL=root-layout-common.js.map