Source: pages/page.js

/**
 * Base class for all Pages. Defines standard implementations if possible
 * @abstract
 * @extends FunctionsInterface
 */
class Page extends FunctionsInterface{
    /**
     * string is an internal name for content displayed on this page, content is the
     * associated content to the string
     * @type {Object<string, Content>}
     */
    content;
    /**
     * the id with wich the page div can be found in the dom
     * @type {string}
     */
    id;
    /**
     * is a reference to the element where the page content is displayed
     * @type {HTMLElement}
     */
    pageDiv;
    /**
     * when the page was added to the dom and all elements have their event listeners the page is set
     *  up and this value is true. Is set by {@link Page.setup}
     * @type {boolean}
     */
    isSetUp = false
    /**
     * if the setup method was called this value changes to true
     * @type {boolean}
     */
    setUpStarted = false;
    /**
     * when the page is ready to show this value changes to true, is set by {@link Page.initialize}
     * @type {boolean}
     */
    isInitialized = false;
    /**
     * if initialization method was called this value changes to true
     * @type {boolean}
     */
    initializationStarted = false;
    /**
     * is set to true if {@link Page.afterPyodideLoaded} was executed. Is used to determine if a page
     * missed the first call by {@link PageManager} after pyodide was loaded
     *  @type {boolean}
     */
    pyodideCallBackDone = false;

    constructor(content, id, title) {
        super();
        this.title = title;
        this.content = content
        this.id = id;
        this.pageDiv = document.getElementById(id);
    }

    /**
     * Execute before custom code in setup method of derived class
     * @returns {boolean} returns true if the setup can continue false otherwise */
    beforeSetup(){
        if (this.isSetUp || this.setUpStarted) return false;
        this.hide();
        this.setUpStarted = true;
        return true
    }

    /**
     * Execute after custom code in setup method of derived class
     */
    afterSetup(){
        this.addEventListeners();
        this.isSetUp = true;
    }

    /**
     * Page standard implementation of setup, usually should be overwritten in child
     * @returns {DocumentFragment}
     */
    setup(){
        this.beforeSetup();
        this.afterSetup();
        return new DocumentFragment()
    }

    /**
     * Execute before custom code in initialize method of derived class
     * @returns {boolean} returns true if the init can continue false otherwise  */
    beforeInit(){
        if(this.initializationStarted) return false;
        if (!this.isSetUp) {
            console.log("init called before setup, setup started... pageClass: " + this.constructor.name)
            this.setup();
        }

        this.initializationStarted = true; // avoid starting the init multiple times
        return true;
    }

    /**
     * Execute after custom code in initialize method of derived class
     */
    afterInit(){
        this.isInitialized = true;
        this.updateColor();
        this.updateLang();
        this.initializationStarted = false;
    }

    /**
     * Page standard implementation of initialize, should be overwritten in child
     */
    initialize(){
        // if the page has to do some things that take longer and may be async override this method in the child
        // the page manager then shows a loading page and waits until this.isInitialized = true
        if (!this.beforeInit()) return;
        this.afterInit();
    }

    /**
     * displays the page container by adding style=block
     * @param animate {boolean} slides in the page from the left (experimental)
     * @returns {Promise<boolean>}
     */
    show(animate=false){
        // page manager handles this with a loading page with the changePage() method
        // this asserts that a page never can be displayed without being setup and initialized properly

        // if a page is shown without being set up there will be errors
        if (!this.isSetUp){
            this.setup();
        }
        // content may not be displayed correctly or out of date if it is not initialized
        if (!this.isInitialized) {
            this.initialize()
        }
        // assert that a page is fully set up even if it was set up later and missed the promised based call to
        // afterPyodideLoaded
        if (!this.pyodideCallBackDone && state.pyodideReady) this.afterPyodideLoaded()

        let page = this.pageDiv;
        if (animate) {
            page.classList.remove('out-left', "in-right");
            page.classList.add("in-right");
        }
        page.style.display = "block";
        this.matomoEvent();
        return Promise.resolve(true)
    }

    matomoEvent(){
        pushPageViewMatomo(this.title)
    }

    /**
     * hides the page container by adding style=none
     */
    hide(){
        this.pageDiv.style.display = "none";
    }

    /**
     * slide the page out of view to the left (experimental)
     */
    slideOut(){
        // don't know at wich state slide out is used, remove both
        this.pageDiv.classList.remove('in-right', 'out-left');
        this.pageDiv.classList.add("out-left");
        setTimeout(()=> this.pageDiv.style.display = "none", 200)
    }

    /**
     * call addEventListeners on each Content that is registered in {@link Page.content}
     */
    addEventListeners() {
        for(let content of Object.values(this.content)) {
            content.addEventListeners();
        }
    }

    /**
     * call updateLang on each Content that is registered in {@link Page.content}
     */
    updateLang() {
        if (!this.isSetUp || !this.isInitialized) return;
        for(let content of Object.values(this.content)) {
            content.updateLang();
        }
    }

    /**
     * some containers need an update of the bs class that bootstrap displays them in the right color
     * @param bgClassName {string} <bgClassName>-dark standard value is bg, is passed to updateBsClass()
     */
    updateColor(bgClassName="bg") {
        if (!this.isSetUp || !this.isInitialized) return;
        for(let content of Object.values(this.content)) {
            content.updateColor();
        }
        this.updateBsClass(bgClassName);
    }

    /**
     * execute code that depends on Pyodide instance fully loaded
     */
    afterPyodideLoaded() {
        if (this.pyodideCallBackDone) return;
        this.pyodideCallBackDone = true;
        // override in child and call this implementation, if something depends on pyodide loaded
    }

    updateBsClass(className = "bg"){
        let element = document.getElementById(this.id);
        let colorScheme = colors.currentBsColorScheme;

        if (colorScheme === colors.bsColorSchemeLight) {
            switchBsClassToLight(className, element);
        } else if (colorScheme === colors.bsColorSchemeDark) {
            switchBsClassToDark(className, element);
        } else {
            throw Error("Only light or dark colorScheme");
        }
    }

    /**
     * change the opacity of this page
     * @param newOpacity {number} float value between 0 and 1
     */
    opacity(newOpacity = 1) {
        this.pageDiv.style.opacity = newOpacity.toString();
    }

    /**
     * If called forces the page manager to recall initialize on next page load, effects depend on
     * implementation of {@link Page.initialize}
     * */
    invalidate(){
        this.isInitialized = false;
        this.initializationStarted = false;
    }

    /**
     * If a page has Easter-Eggs load and setup everything in this method. This method is called by the
     * {@link PageManager} when the page finished loading to avoid loading "unnecessary" stuff before the page is
     * functional
     */
    setupEasterEggs(){

    }
}