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

Source: pages.dt/js/api/Pages.js

define([
    'core/js/api/Listenable',
    'core/js/api/ListenerBuilder',
    'core/js/api/StorageProvider',
    'core/js/api/utils/RandomUtils',
    'core.dt/js/api/BackendStorage',
    'core.dt/js/api/application/ApplicationSupport',
    'core.dt/js/api/undo/UndoRedoManager',
    'layout/js/api/LayoutConstants',
    'pages.dt/js/PageFactory',
    'pages.dt/js/PagesManager',
    'pages.dt/js/api/Navigation',
    'pages.dt/js/api/Page',
    'pages.dt/js/api/ViewGeneratorModes',
    'translations/js/api/I18n'
], function(
        Listenable,
        ListenerBuilder,
        StorageProvider,
        RandomUtils,
        BackendStorage,
        ApplicationSupport,
        undoRedoManager,
        LayoutConstants,
        pageFactory,
        PagesManager,
        navigation,
        Page,
        ViewGeneratorModes,
        I18n) {

    'use strict';

    /**
     * Basic application pages management support. This support is useful for obtaining
     * all {@link pages.dt/js/api/Pages.getPages pages} or the particular {@link pages.dt/js/api/Page page}
     * instance registered in the opened application.</p>
     *
     * <p>This is one of the base modules used in ABCS if you are going
     * to work with pages.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @private
     * @singleton
     * @exports pages.dt/js/api/Pages
     * @constructor
     */
    /*
     * You can use this object internally to:<br>
     * 1) get a list of application pages: getPages()/getPageIds()/getPage()<br>
     * 2) create a new page: createPage()<br>
     * 3) remove an existing page: removePage()<br>
     * 4) listen on the Pages object for following change events:
     *      pageCreated,
     *      pageRemoved,
     *      activePageUpdated,
     *      homepageSet.
     */
    var Pages = function() {
        AbcsLib.checkThis(this);
        AbcsLib.checkSingleton(Pages);
        Listenable.apply(this, arguments);

        this._metadata = undefined;
        this._metadataChangedListener = undefined;
        this._pageChangedListener = null;
        this._initializedListeners = false;
    };
    AbcsLib.mixin(Pages, Listenable);

    // we needs to have reference to the singleton internally
    var pages = AbcsLib.initSingleton(Pages);

    Pages._LOGGER = Logger.get('pages.dt/js/api/Pages');

    Pages._ERROR_FIND_PAGE_TOP_VIEW = 'Trying to find a page for PAGE TOP VIEW ID which is the same for every page. Use "activePage.getView()" instead.';

    /**
     * Active page changed event.
     * @event pages.dt/js/api/Pages#EVENT_ACTIVE_PAGE_UPDATED
     * @property {pages.dt/js/api/Page} page page to be activated
     */
    Pages.EVENT_ACTIVE_PAGE_UPDATED = 'activePageUpdated';

    /**
     * @event pages.dt/js/api/Pages#EVENT_PAGE_CREATED
     * @property {pages.dt/js/api/Page} page created page
     */
    Pages.EVENT_PAGE_CREATED = 'pageCreated';

    /**
     * @event pages.dt/js/api/Pages#EVENT_PAGE_REMOVED
     * @property {pages.dt/js/api/Page} page removed page
     */
    Pages.EVENT_PAGE_REMOVED = 'pageRemoved';

    /**
     * @event pages.dt/js/api/Pages#EVENT_PAGE_CHANGED
     * @property {pages.dt/js/api/Page} page changed page
     */
    Pages.EVENT_PAGE_CHANGED = 'pageChanged';

    /**
     * @event pages.dt/js/api/Pages#EVENT_HOMEPAGE_SET
     */
    Pages.EVENT_HOMEPAGE_SET = 'homepageSet';

    /**
     * @event pages.dt/js/api/Pages#EVENT_MODE_CHANGED
     * @property {pages.dt/js/api/ViewGeneratorModes} mode the newly set mode
     */
    Pages.EVENT_MODE_CHANGED = 'modeChanged';

    /**
     * Initializes listening on other modules.
     */
    Pages.prototype._initListeners = function() {
        // we need to guarantee that the listeners initialization will be called just once
        if (this._initializedListeners) {
            return;
        }

        var self = this;
        this._initializedListeners = true;
        // XXX - unfify these call with the rest of the class to invoke i.e. the same base methods
        // handles PagesManager notifications from the backend
        PagesManager.getInstance().addListener(new ListenerBuilder(PagesManager.EVENT_PAGE_REMOVED, function(event) {
            var page = self.pages[event.pageId];
            delete self.pages[event.pageId];
            self.fireEvent(Pages.EVENT_PAGE_REMOVED, {
                page: page,
                userId: event.userId
            });
            // case of the removed active page
            self._handleNavigationFromActivePage(event.pageId);
        }).event(PagesManager.EVENT_PAGE_CHANGED, function(event) {
            self._refreshChangedPage(event);
        }).event(PagesManager.EVENT_PAGES_METADATA_CHANGED, function(definition) {
            // reset currently stored metadata
            self._metadata = self._createMetadata(definition);
        }).build());
    };

    /**
     * Gets pages specific metadata.
     *
     * @return {Pages.Metadata} pages's metadata
     */
    Pages.prototype.getMetadata = function() {
        if (this._metadata === undefined) {
            throw new Error('Pages metadata can\'t be undefinded.');
        }

        return this._metadata;
    };

    /**
     * Initializes the Pages singleton for the given application context.
     * Do not use! Supposed to be called exclusively from ApplicationSupport.setActiveApplication()
     */
    Pages.prototype.setContext = function(activateHomePage) {
        var self = this;

        self.pages = self._createPages();
        self._metadata = self._createMetadata();

        // initialize navigation object
        navigation.__initialize(this);

        if (ApplicationSupport.isInGenerationMode()) {
            //special app-sources-generation mode (runs in node -- see the app-gen.js script)
            self.mode = ViewGeneratorModes.MODE_DEPLOYMENT;

        } else {
            //normal designer mode
            self.mode = ViewGeneratorModes.MODE_DESIGNER;

            if (activateHomePage) {
                navigation.setActivePageId(self.getHomepageId());
            }
        }

        // initialize listeners on PagesManager changes
        this._initListeners();
    };

    /**
     * Loads definition from the PagesManager for particular pageID and creates
     * corresponding page instance.
     *
     * @param {String} id page ID
     * @returns {Page} created page
     */
    Pages.prototype._createPage = function(id) {
        var definition = PagesManager.getInstance().getPageDefinition(id);
        var page = this._initializePage(definition.id, definition);
        return page;
    };

    /**
     * Initializes the Pages module since it loads all page definitions from the
     * pages manager and creates instances of their pages.
     *
     * @returns {Page[]} full pages list
     */
    Pages.prototype._createPages = function() {
        var self = this;
        var pages = {};
        PagesManager.getInstance().getIds().forEach(function(id) {
            var page = self._createPage(id);
            pages[page.getId()] = page;
        });
        return pages;
    };

    /**
     * Loads the metadata definition file from the pages manager and returns the created metadata instance.
     *
     * @returns {Pages.Metadata} metadata object
     */
    Pages.prototype._createMetadata = function(metadataDefinition) {
        // create metadata
        metadataDefinition = metadataDefinition || PagesManager.getInstance().getMetadataDefinition();
        var metadata = Pages.Metadata.fromDefintion(metadataDefinition);

        // enable listening on them
        metadata.addListener(this._getMetadataChangedListener());

        return metadata;
    };

    Pages.prototype._checkInitialised = function() {
        if (!this.isInitialised()) {
            throw new Error('No Pages.setContext() has been called!');
        }
    };

    /**
     * Gets information whether the Pages is initialized or not
     *
     * @return {Boolean} true if Pages is initialized, false otherwise
     */
    Pages.prototype.isInitialised = function() {
        return this.pages !== undefined;
    };

    Pages.prototype.getPagesIds = function() {
        return this.getPageIds();
    };

    /**
     * Gets list of existing page IDs in the application.
     *
     * @AbcsExtension unstable
     *
     * @see {@link module:pages.dt/js/api/Pages.getPages Pages.getPages}
     * @returns {String[]} list of all application's page IDs
     *
     * @example
     * <caption>
     * Get count of all available pages.
     * </caption>
     * // synchronous access used to make the sample more readable,
     * //     you should always use async define by getting Pages
     * var Pages = require('pages.dt/js/api/Pages');
     * var pageCount = Pages.getPageIds().length;
     *
     * @example
     * <caption>
     * Seeks and creates unique page ID across available pages.
     * </caption>
     * // synchronous access used to make the sample more readable,
     * //     you should always use async define by getting Pages
     * var Pages = require('pages.dt/js/api/Pages');
     *
     * var count = 2;
     * var customPageId = 'myUniqueId';
     * // checks whether the customPageId is unique across all page IDs and
     * //    increases its suffix number if not until unique page ID is found
     * while (Pages.getPageIds().indexOf(customPageId) !== -1) {
     *     customPageId = customPageId + '_' + count;
     *     count++;
     * }
     * console.log('This is unique page ID which can be used: ' + customPageId);
     */
    Pages.prototype.getPageIds = function() {
        AbcsLib.checkParameterCount(arguments, 0);

        this._checkInitialised();
        return Object.keys(this.pages);
    };

    Pages.prototype.pageExists = function (pageId) {
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkDefined(pageId, 'pageId');

        var ids = this.getPageIds();
        for (var i = 0; i < ids.length; i++) {
            if (ids[i] === pageId) {
                return true;
            }
        }
        return false;
    };

    /**
     * Gets the {@link pages.dt/js/api/Page Page} for the given ID.
     * <p>
     * If the ID is not available in the list of all application's pages,
     * <code>undefined</code> value is returned.
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @param {String} pageId ID of the page to obtain
     * @returns {pages.dt/js/api/Page} page instance for supplied ID if such
     *          {@link pages.dt/js/api/Page Page} exists, <code>undefined</code> otherwise
     *
     * @example <caption>
     * Gets display name of the home page and shows it inside a notification.
     * </caption>
     * // gets page's ID of the homepage
     * var homepageId = Pages.getHomepageId();
     * // gets the page instance by its ID
     * var page = Pages.getPage(homepageId);
     * // show notification in the ABCS's UI
     * Abcs.UI().showNotification(Abcs.UI().Notification.create({
     *      message: 'Your homepage is called \'' + page.getDisplayName() + '\'.'
     * });
     */
    Pages.prototype.getPage = function(pageId) {
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkDefined(pageId, 'pageId');

        this._checkInitialised();
        return this.pages[pageId];
    };

    /**
     * Gets list of existing pages in the ABCS application.
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {pages.dt/js/api/Page[]} list of all application's pages
     *
     * @example
     * <caption>
     * Gets IDs of all pages.
     * </caption>
     * // synchronous access used to make the sample more readable,
     * //     you should always use async define for getting Pages
     * var Pages = require('pages.dt/js/api/Pages');
     * var ids = [];
     * // gets all pages
     * var pages = Pages.getPages();
     * // iterates through pages and collects their IDs
     * pages.forEach(function(page) {
     *     ids.push(page.getId();
     * });
     * // prints the all pages IDs
     * console.log('Pages of your application are: ' + JSON.stringify(ids));
     */
    Pages.prototype.getPages = function() {
        AbcsLib.checkParameterCount(arguments, 0);

        var self = this;
        var pages = [];
        self.getPageIds().forEach(function(id) {
            pages.push(self.getPage(id));
        });
        return pages;
    };

    /**
     * Creates a new page
     * @param {string} pageId
     * @param {JSON} [definition] definition to be used by page creation
     * @return {Page}
     * @fires pages.dt/js/api/Pages#EVENT_PAGE_CREATED
     */
    Pages.prototype.createPage = function(pageId, definition) {
        this._checkInitialised();

        var self = this;
        if (!pageId) {
            // make rand string appendix (a-z0-9)
            pageId = 'newPage_' + RandomUtils.randomString(6);
        }
        var baseDefinition = StorageProvider.getStorage().getSync(BackendStorage.PATH_SYSTEM_TEMPLATES + '/newPageTemplate.json');

        var page = self._initializePage(pageId, $.extend(definition || {}, baseDefinition));

        self.pages[pageId] = page;

        page.saveChanges();
        self.fireEvent(Pages.EVENT_PAGE_CREATED, {page: page});

        Pages._LOGGER.debug('Created page ' + page.getId() + '.');

        return page;
    };

    /**
     * Removes the given pages.
     * <p>
     * <b>If the given page is the active page, the client need to navigate to
     * other page by himself.</b>
     *
     * @param {type} page page to be deleted
     * @fires pages.dt/js/api/Pages#EVENT_PAGE_REMOVED
     */
    Pages.prototype.removePage = function(page, cleanup) {
        var self = this;
        self._checkInitialised();

        page.removeListener(self._getPageChangedListener());

        var pageId = page.getId();

        undoRedoManager.groupChanges({
            run: function () {
                // handle if the page would be homepage or active page
                self._handleHomepageRemoval(pageId);
                self._handleNavigationFromActivePage(pageId);

                delete self.pages[pageId];

                PagesManager.getInstance().removePage(page);
                // Wipe out all page data if clean up was true (or omitted)
                if (cleanup === true || cleanup === undefined) {
                    AbcsLib.Translations.deleteTranslatedString(I18n.key(I18n.Type.Page, page.getId()));
                    AbcsLib.Translations.deleteTranslatedString(I18n.key(I18n.Type.MainMenu, page.getId()));
                }
                self.fireEvent(Pages.EVENT_PAGE_REMOVED, {page: page});
                Pages._LOGGER.debug('Removed page ' + page.getId() + '.');
            },
            description: AbcsLib.i18n('pagesDt.undoPageRemoveDescription', {
                page: page.getDisplayName().toString()
            }),
            afterUndoAction: function (undoFinished) {
                return undoFinished && Abcs.Pages().navigateToPage(pageId);
            }
        });
    };

    /**
     * Handles navigation from the page if the given page is active at the moment.
     *
     * @param {String} pageId ID of the examined page
     */
    Pages.prototype._handleNavigationFromActivePage = function(pageId) {
        if (navigation.getActivePageId() === pageId) {
            var nextPageId = this.getHomepageId() !== pageId ? this.getHomepageId() : this._getNextPageId(pageId);
            if (nextPageId !== undefined) {
                Abcs.Pages().navigateToPage(nextPageId);
            } else {
                Pages._LOGGER.warn('No other page we could switch as homepage exists.');
            }
        }
    };

    /**
     * Handles homepage switch if the passed page is homepage at the moment.
     *
     * @param {String} pageId ID of the examined page
     */
    Pages.prototype._handleHomepageRemoval = function(pageId) {
        if (this.getHomepageId() === pageId) {
            var nextPageId = this._getNextPageId(pageId);
            if (nextPageId !== undefined) {
                this.setHomepageId(nextPageId);
            } else {
                Pages._LOGGER.warn('No other page we could switch as active exists.');
            }
        }
    };

    Pages.prototype._navigateFromActivePage = function(pageId) {
        if (navigation.getActivePageId() === pageId) {
            var nextPageId = this._getNextPageId(pageId);
            if (nextPageId !== undefined) {
                Abcs.Pages().navigateToPage(nextPageId);
            } else {
                Pages._LOGGER.warn('No other page we could switch as active exists.');
            }
        }
    };

    Pages.prototype._getPageChangedListener = function () {
        var self = this;
        if (!self._pageChangedListener) {
            self._pageChangedListener = new ListenerBuilder(Page.__EVENT_PAGE_CHANGED, function(page, pageDefinitionChanged) {
                if (pageDefinitionChanged[0] === true) {
                    PagesManager.getInstance().storePage(page);
                }
                self.fireEvent(Pages.EVENT_PAGE_CHANGED, {
                    page: page,
                    origin: Pages.PageChangedOrigin.DESIGNER
                });
            }).event(Page.EVENT_PAGE_UPDATED, function (page) {
                self.fireEvent(Pages.EVENT_ACTIVE_PAGE_UPDATED, page);
            }).build();
        }
        return self._pageChangedListener;
    };

    Pages.prototype._getMetadataChangedListener = function () {
        var self = this;
        if (self._metadataChangedListener === undefined) {
            self._metadataChangedListener = new ListenerBuilder(Pages.Metadata.EVENT_METADATA_CHANGED, function() {
                PagesManager.getInstance().storeMetadata({
                    value: self._metadata.getDefinition()
                });
            }).build();
        }
        return self._metadataChangedListener;
    };

    /**
     * Gets the {@link pages.dt/js/api/Page Page}'s ID of the homepage.
     *
     * <p>Every ABCS application has defined single {@link pages.dt/js/api/Page Page}
     * which is used as the homepage. Homepage means that if the ABCS application is opened
     * without route to specific page, the homepage is opened first then.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @return {String} homepage's ID
     *
     * @example <caption>Gets display name of the home page and shows it inside a notification.</caption>
     * // gets page's ID of the homepage
     * var homepageId = Pages.getHomepageId();
     * // gets the page instance by its ID
     * var page = Pages.getPage(homepageId);
     * // show notification in the ABCS's UI
     * Abcs.UI().showNotification(Abcs.UI().Notification.create({
     *      message: 'Your homepage is called \'' + page.getDisplayName() + '\'.'
     * });
     */
    Pages.prototype.getHomepageId = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        return this.getMetadata().getHomepageId();
    };

    /**
     * Sets new homepage ID.
     *
     * @param {String} homepageId page ID to be set as homepage
     * @fires pages.dt/js/api/Pages#EVENT_HOMEPAGE_SET
     */
    Pages.prototype.setHomepageId = function (homepageId) {
        this._checkInitialised();

        if (this.getHomepageId() !== homepageId) {
            this._metadata.setHomepageId(homepageId);
            this.fireEvent(Pages.EVENT_HOMEPAGE_SET);
        }
    };

    /**
     * Attemps to propose other page than the one obtained in the parameter.
     *
     * @param {String} pageIdToOmit page ID to be omitted from the choice
     * @returns {String} another page  ID to choose (switch to etc.)
     */
    Pages.prototype._getNextPageId = function(pageIdToOmit) {
        for (var key in this.pages) {
            if (key !== pageIdToOmit) {
                return key;
            }
        }
    };

    Pages.prototype._refreshChangedPage = function (event) {
        var pageId = event.pageId;
        var userId = event.userId;
        var pageDefinition = event.pageDefinition;
        var page = this.pages[pageId];

        if (page) {
            var displayName = I18n.deserializeIfTranslatable(pageDefinition.displayName);
            var viewModelDefinition = page.getViewModelDefinition();
            page.__updateFromDefinition(
                    displayName,
                    pageDefinition.isModalPage,
                    pageDefinition.type,
                    pageDefinition.viewModel,
                    pageDefinition.view);
            if (pageId === navigation.getActivePageId()) {
                // multi-user change requires page update
                if (!event.localUpdate
                        // undo/redo change with changed page's view model
                        || (viewModelDefinition !== page.getViewModelDefinition())) {
                    navigation.switchActivePage(page);
                }
            }
            this.fireEvent(Pages.EVENT_PAGE_CHANGED, {
                page: page,
                userId: userId,
                origin: event.localUpdate
                        ? Pages.PageChangedOrigin.UNDO_REDO
                        : Pages.PageChangedOrigin.MULTI_USER
            });
        } else {
            page = this._initializePage(pageId, pageDefinition);
            this.pages[pageId] = page;
            this.fireEvent(Pages.EVENT_PAGE_CREATED, {
                page: page,
                userId: userId
            });
        }
    };

    /**
     * Switches application to different mode.
     *
     * @param {ViewGeneratorModes} mode to be switched to
     * @fires pages.dt/js/api/Pages#EVENT_MODE_CHANGED
     */
    Pages.prototype.setMode = function(mode) {
        if (this.mode !== mode) {
            this.mode = mode;
            this.fireEvent(Pages.EVENT_MODE_CHANGED, mode);
            navigation.getActivePage().refresh();
        }
    };

    /**
     * Gets currently chosen application mode.
     *
     * @return {ViewGeneratorModes} current mode
     */
    Pages.prototype.getMode = function() {
        return this.mode;
    };

    Pages.prototype._initializePage = function(pageId, definition) {
        var page = pageFactory.createPageFromDefinition(pageId, definition, this);

        //listen on the created page
        page.addListener(this._getPageChangedListener());

        return page;
    };

    Pages.prototype.findPageForView = function(view) {
        if (view.getId() === LayoutConstants.PAGE_TOP_VIEW_ID) {
            throw Pages._ERROR_FIND_PAGE_TOP_VIEW;
        }
        var result = null;
        var pageIds = this.getPageIds();
        for (var i = 0; i < pageIds.length; i++) {
            var pageId = pageIds[i];
            var page = this.pages[pageId];
            if (page.findSameView(view)) {
                result = page;
                break;
            }
        }
        return result;
    };

    /**
     * Pages metadata.
     *
     * @param {JSON} definitionJson definition JSON
     */
    Pages.Metadata = function(definitionJson) {
        AbcsLib.checkThis(this);
        AbcsLib.checkDefined(definitionJson, 'definitionJson');
        AbcsLib.checkDefined(definitionJson.homepage, 'definitionJson.homepage');
        Listenable.apply(this, arguments);

        this._homepageId = definitionJson.homepage;
    };
    AbcsLib.mixin(Pages.Metadata, Listenable);

    Pages.Metadata.EVENT_METADATA_CHANGED = 'metadataChanged';

    Pages.Metadata.fromDefintion = function(def) {
        return new Pages.Metadata(def);
    };

    Pages.Metadata.prototype.getDefinition = function() {
        var def = {};
        def.homepage = this._homepageId;
        return def;
    };

    Pages.Metadata.prototype.getHomepageId = function() {
        return this._homepageId;
    };

    Pages.Metadata.prototype.setHomepageId = function(homepage) {
        this._homepageId = homepage;
        this.fireEvent(Pages.Metadata.EVENT_METADATA_CHANGED);
    };

    Pages.PageChangedOrigin = function () {
        AbcsLib.throwStaticClassError();
    };

    Pages.PageChangedOrigin.UNDO_REDO = 'undoRedo';
    Pages.PageChangedOrigin.MULTI_USER = 'multiUser';
    Pages.PageChangedOrigin.DESIGNER = 'designer';

    return pages;

});