JavaScript Extension Development API for Oracle Visual Builder Cloud Service - Classic Applications

Source: core/js/api/router/AbstractRouter.js

define([
    'crossroads',
    'module',
    'core/js/api/ListenerBuilder',
    'core/js/api/router/RouterState',
    'pages/js/api/NavigationProvider'
], function(
        crossroads,
        module,
        ListenerBuilder,
        RouterState,
        NavigationProvider
        ) {

    'use strict';

    /**
     * Base Router class inherited by RT and DT routers.
     *
     * @returns {AbstractRouter}
     */
    var AbstractRouter = function() {
        AbcsLib.checkThis(this);
        var self = this;

        //observers
        self._observers = {};

        //initialization promise
        self._initializationPromiseFulfil;
        self._initializationPromise = new Promise(function(fulfil) {
            self._initializationPromiseFulfil = fulfil;
        });

        //stores temporary the last state
        self.__latestPageState;
    };

    AbstractRouter._LOGGER = Logger.get(module.id);

    AbstractRouter._NAVIGATION_CHANGE_HANDLER = function(event) {
        var state = event.state;
        if (state !== null && state !== undefined) {
            // call the state restoration implementation
            this.__restoreRouterState(state);
        } else {
            this.__navigateTo(AbstractRouter._getCurrentHash());
        }
    };

    /**
     * Gets the promise which is fulfiled once the application routing is successfully initialized.
     *
     * @returns {Promise}
     */
    AbstractRouter.prototype.getInitializationPromise = function() {
        return this._initializationPromise;
    };

    /**
     * Starts the initialization process of the Routing service.
     */
    AbstractRouter.prototype.initialise = function() {
        // our routing is stateless
        crossroads.ignoreState = true;
        // initialize routers/routes and listeners
        this._initializeListeners();
        this.__initialiseRoutes();
        this.__postInitialise();
    };

    /**
     * Registers a RouterStateObserver.
     * <p>
     * Such an observer is called during the route's push-, replace-
     * state or before the state is used for restoration of UI context.
     *
     * @param {String} id identifier of the observer to be registered
     * @param {RouterStateObserver} observer observer to be registered into the router
     */
    AbstractRouter.prototype.registerRouterStateObserver = function(id, observer) {
        AbcsLib.checkParameterCount(arguments, 2, 0);
        AbcsLib.checkDefined(id, 'id');
        AbcsLib.checkDefined(observer, 'observer');

        var existingObserver = this._observers[id];
        if (existingObserver && existingObserver !== observer) {
            AbstractRouter._LOGGER.warn('RouterStateObserver with id "' + id + '" is already registered.');
            return false;
        }
        this._observers[id] = observer;
        return true;
    };

    /**
     * Unregisters previously registered RouterStateObserver from the Router.
     *
     * @param {String} id RouterStateObserver's identifier
     */
    AbstractRouter.prototype.unregisterRouterStateObserver = function(id) {
        AbcsLib.checkDefined(id, 'id');
        if (this._observers[id]) {
            delete this._observers[id];
            return true;
        }
        return false;
    };

    /**
     * Stores the new state of the router.
     * <p>
     * It changes the browser's address bar and saves the state.
     *
     * @param {RouterState} state state to store
     */
    AbstractRouter.prototype.__storeRouterState = function(state) {
        this.__latestPageState = AbcsLib.clone(state);
        for (var i in this._observers) {
            this._observers[i].pushState(this.__latestPageState);
        }
    };

    /**
     * Updates the lastest state of the router.
     * <p>
     * It can change the browser's address bar and replaces the latest state.
     *
     * @param {RouterState} state state to use as replacement
     */
    AbstractRouter.prototype.__updateRouterState = function(state) {
        for (var i in this._observers) {
            this._observers[i].updateState(AbcsLib.clone(state));
        }
    };

    // XXX - call the beforeRestoreState from the AbstractRouter directly
    AbstractRouter.prototype.__handleBeforeRestoreState = function(state, viewModel) {
        // call observers first
        for (var i in this._observers) {
            this._observers[i].beforeRestoreState(state, viewModel);
        }
    };

    /**
     * Restores the given router state into submodule's configurations.
     *
     * @param {String} state state to restore within ABCS
     */
    AbstractRouter.prototype.__restoreRouterState = function(/*state*/) {
        AbcsLib.throwMustBeOverriddenError('Implementation of the Router needs provide state restoration method.');
    };

    AbstractRouter.prototype.__initialiseRoutes = function() {
        AbcsLib.throwMustBeOverriddenError('Implementation of the Router needs to introduce/register its own routes.');
    };

    /**
     * Calls the crossroad routing to invoke callbacks registered for given path.
     *
     * @param {String} path to navigate to (calls callback bound to that path)
     */
    AbstractRouter.prototype.__navigateTo = function(path) {
        // treat the beggining hash if any
        path = path.indexOf('#') === 0 ? path.substring(1) : path;
        crossroads.parse(path);
        AbstractRouter._LOGGER.debug('Navigation changed to \'' + path + '\'');
    };

    AbstractRouter.__registerNewRouter = function(isTopLevelRouter) {
        var router = crossroads.create();
        router.ignoreState = true;

        //top level routers are piped with the main crossroad's one
        if (isTopLevelRouter) {
            crossroads.pipe(router);
        }

        return router;
    };

    AbstractRouter.__registerPagesRouter = function(isTopLevelRouter, pageActivatedCallback) {
        var pagesRouter = AbstractRouter.__registerNewRouter(isTopLevelRouter);
        pagesRouter.addRoute('{page}/:rest*:{?qs}').matched.add(pageActivatedCallback);
        pagesRouter.addRoute('{page}/:rest*:').matched.add(pageActivatedCallback);

        return pagesRouter;
    };

    AbstractRouter.__registerUrlChangeInterceptorRouter = function(router) {
        var urlChangeInterceptorRouter = AbstractRouter.__registerNewRouter(true);
        var hookRoute = urlChangeInterceptorRouter.addRoute(':rest*:');
        hookRoute.greedy = true;
        hookRoute.matched.add(function(uri) {
            var state = RouterState.create({url: uri || ''});
            for (var i in router._observers) {
                router._observers[i].pushState(AbcsLib.clone(state));
            }
        });

        return urlChangeInterceptorRouter;
    };

    AbstractRouter.prototype.__postInitialise = function() {
        if (window.history) {
            var initialState = this.pathToState(location.hash);
            for (var i in this._observers) {
                this._observers[i].pushState(AbcsLib.clone(initialState));
            }

            // do the initial navigation action
            var currentNavigationUrl = AbstractRouter._getCurrentHash();
            this.__navigateTo(AbstractRouter.__treatPath(currentNavigationUrl));

            // enable popstate and hashchange listening
            window.addEventListener('popstate', AbstractRouter._NAVIGATION_CHANGE_HANDLER.bind(this));
//            window.addEventListener('hashchange', AbstractRouter._NAVIGATION_CHANGE_HANDLER.bind(this));

            // confirm the Router initialization as successfully done
            AbstractRouter._LOGGER.info('Router was successfully initialized.');
            this._initializationPromiseFulfil();
        } else {
            AbstractRouter._LOGGER.error('Browser doesn\'t support HTML5 history API (window.history not found).');
        }
    };

    AbstractRouter.prototype.pathToState = function(path) {
        return RouterState.create({url: path});
    };

    AbstractRouter.__treatPath = function(path) {
        var targetUrl = '';
        if (path === undefined || path.length === 0) {
            targetUrl = '#/';
        } else if (path.indexOf('#') === 0) {
            targetUrl = path;
        } else if (path.indexOf('/') !== 0) {
            targetUrl = '#/' + path;
        } else {
            targetUrl = '#' + path;
        }

        return targetUrl;
    };

    AbstractRouter.prototype._initializeListeners = function() {
        var self = this;
        var navigation = NavigationProvider.getNavigation();
        navigation.addListener(new ListenerBuilder(navigation.EVENT_PAGE_DEACTIVATED, function(pageId, pageViewModel) {
            // update the current state during the page leave
            if (pageViewModel) {
                var latestState = RouterState.create({
                    url: location.hash ? location.hash.substring(2) : '',
                    pageId: pageId,
                    pageModelVersion: pageViewModel ? pageViewModel[navigation.PAGE_VIEWMODEL_VERSION_PROPERTY] : undefined,
                    contextualData: pageViewModel.getContextualData() ? pageViewModel.getContextualData().toJSON() : undefined,
                    bookmarkData: pageViewModel.getBookmark() ? pageViewModel.getBookmark().toJSON() : undefined,
                    complete: true
                });
                self.__updateRouterState(latestState);
            }
        }).build());
    };

    AbstractRouter._getCurrentHash = function() {
        return document.location.hash;
    };

    return AbstractRouter;

});