nw-skeleton

Source: app-wrapper/js/helper/system/themeHelper.js

/**
 * @fileOverview ThemeHelper class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */


/**
 * Object that contains theme definition values
 * @typedef  {Object}   ThemeDefinition
 *
 * @property {string}       name                Name of the theme
 * @property {string}       path                Absolute path to theme directory
 * @property {string}       extends             Name of base theme that current theme extends
 * @property {string[]}     initCssFiles        Array of initial theme css files
 * @property {string[]}     cssFiles            Array of theme css files
 * @property {string[]}     overrideCssFiles    Array of theme css override files
 * @property {string[]}     initJsFiles         Array of initial theme js files
 * @property {string[]}     jsFiles             Array of theme js files
 */

const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const AppBaseClass = require('../../lib/appBase').AppBaseClass;

var _appWrapper;
var appState;

/**
 * ThemeHelper class - handles and manages themes and theme operations
 *
 * @class
 * @extends {appWrapper.AppBaseClass}
 * @memberof appWrapper.helpers.systemHelpers
 */
class ThemeHelper extends AppBaseClass {

    /**
     * Creates ThemeHelper instance
     *
     * @constructor
     * @return {ThemeHelper}              Instance of ThemeHelper class
     */
    constructor() {
        super();

        _appWrapper = window.getAppWrapper();
        appState = _appWrapper.getAppState();

        return this;
    }

    /**
     * Reads and initializes theme files based on configuration.
     *
     * @async
     * @return {undefined}
     */
    async initializeThemes () {
        appState.availableThemes = [];
        let themeCount = 0;

        this.log('Initializing themes...', 'group', []);

        let appWrapperBaseThemeDir = this.getConfig('wrapper.themeBaseDir');
        if (appWrapperBaseThemeDir){
            appWrapperBaseThemeDir = path.resolve(appWrapperBaseThemeDir);
        }

        let appThemeBaseDir = this.getConfig('appConfig.themeBaseDir');
        if (appThemeBaseDir){
            appThemeBaseDir = path.resolve(appThemeBaseDir);
        }

        if(appWrapperBaseThemeDir){
            themeCount += await this.initializeTypeThemes(appWrapperBaseThemeDir, 'wrapper');
        }
        if(appThemeBaseDir){
            themeCount += await this.initializeTypeThemes(appThemeBaseDir, 'app');
        }
        themeCount += await this.initializeModuleThemes();

        if (themeCount == appState.availableThemes.length){
            this.log('Initialized {1} themes.', 'info', [themeCount]);
        } else {
            this.log('Initialized {1} themes of {2} total ({3} overrides).', 'info', [appState.availableThemes.length, themeCount, (themeCount - appState.availableThemes.length)]);
        }

        appState.availableThemes = _.sortBy(appState.availableThemes, 'name');

        this.log('Initializing themes...', 'groupend', []);
    }

    /**
     * Initializes themes group from directory passed as argument
     *
     * @async
     * @param  {string} baseThemeDir Base theme group dir
     * @param  {string} type         Type of theme ('wrapper' or 'app')
     * @return {Number}              Number of initialized themes from the group
     */
    async initializeTypeThemes(baseThemeDir, type){
        let themeCount = 0;
        if (!type){
            type = '';
        }
        if (_appWrapper.fileManager.isDir(baseThemeDir)){
            let themeDirs = fs.readdirSync(baseThemeDir);
            if (themeDirs && themeDirs.length){
                this.log('Initializing {1} themes from {2} directories...', 'group', [type, themeDirs.length]);
                for(let i=0; i<themeDirs.length; i++){
                    let themePath = path.join(baseThemeDir, themeDirs[i]);
                    if (_appWrapper.fileManager.isDir(themePath)){
                        let themeConfig = await _appWrapper.fileManager.loadFile(path.join(themePath, 'theme.js'), true);
                        if (themeConfig){
                            if (themeConfig.name){
                                if (!themeConfig.path){
                                    themeConfig.path = themePath;
                                }
                                if (await this.registerTheme(themeConfig)){
                                    themeCount++;
                                }
                            } else {
                                this.log('Theme definition from "{1}" has no "name" property!', 'error', [themePath]);
                            }
                        } else {
                            this.log('Problem loading theme definition from "{1}"!', 'error', [themePath]);
                        }
                    } else {
                        this.log('Omitting path "{1}", not a directory', 'debug', [themePath]);
                    }
                }
                this.log('Initializing {1} themes from {2} directories...', 'groupend', [type, themeDirs.length]);
            }
        } else {
            this.log('Directory for {1} themes not found.', 'info', [type]);
        }
        return themeCount;
    }

    /**
     * Loads and initializes themes configured as node modules
     *
     * @async
     * @return {Number} Number of initialized theme modules
     */
    async initializeModuleThemes () {
        let themeModules = this.getConfig('themeModules');
        let modulesInitialized = 0;
        if (themeModules && themeModules.length){
            this.log('Initializing {1} theme modules...', 'group', [themeModules.length]);
            for (let i=0; i<themeModules.length; i++){
                let themeModuleData;
                let themeDefs;
                try {
                    themeModuleData = _appWrapper.app.localRequire(themeModules[i]);
                    if (themeModuleData){
                        if (themeModuleData.themes){
                            themeDefs = themeModuleData.themes;
                            if (themeDefs && themeDefs.length){
                                this.log('Loading theme module "{1}".', 'group', [themeModules[i]]);
                                for (let i=0; i<themeDefs.length; i++){
                                    if (themeDefs[i] && themeDefs[i].theme){
                                        let themeDef = themeDefs[i].theme;
                                        if (themeDef.name && themeDef.path){
                                            if (await this.registerTheme(themeDef)){
                                                modulesInitialized++;
                                            }
                                        }
                                    }
                                }
                                this.log('Loading theme module "{1}".', 'groupend', [themeModules[i]]);
                            } else {
                                this.addUserMessage('Theme module "{1}" "themes" key does not contain any themes.', 'warning', [themeModules[i]]);
                            }
                        } else {
                            this.log('Problem loading theme module "{1}" - "themes" key not found in exported value', 'error', [themeModules[i]]);
                            this.setAppError('Problem loading theme', 'Problem loading theme module "{1}" - "themes" key not found in exported value.', 'Check whether module "{1}" exports value under name "themes".', [themeModules[i]]);
                        }
                    } else {
                        this.log('Problem loading theme module "{1}" - module not found', 'error', [themeModules[i]]);
                        this.setAppError('Problem loading theme', 'Problem loading theme module "{1}" - module not found.', 'Verify config variable "themeModules" to fix this problem.', [themeModules[i]]);
                    }
                } catch (ex){
                    this.log('Problem loading theme module "{1}" - "{2}"', 'error', [themeModules[i], ex.message]);
                    this.setAppError('Problem loading theme', 'Problem loading theme module "{1}" - module not found.', 'Verify config variable "themeModules" to fix this problem.', [themeModules[i]]);
                }
            }
            this.log('Initializing {1} theme modules...', 'groupend', [themeModules.length]);
        }
        return modulesInitialized;
    }

    /**
     * Registers theme in the system, adding it to appState
     *
     * @async
     * @param  {ThemeDefinition}    themeDefinition     Theme definition
     * @return {Boolean}                                Result of theme initialization
     */
    async registerTheme(themeDefinition){
        let result = false;
        if (themeDefinition.name && themeDefinition.path){
            let oldThemesFound = _.filter(appState.availableThemes, { name: themeDefinition.name }).length;
            if (oldThemesFound){
                this.log('Registering theme override "{1}"...', 'debug', [themeDefinition.name]);
                _.remove(appState.availableThemes, { name: themeDefinition.name });
            } else {
                this.log('Registering theme "{1}"...', 'debug', [themeDefinition.name]);
            }
            appState.availableThemes.push(themeDefinition);
            result = true;
        }
        return result;
    }

    /**
     * Gets theme config for given theme
     *
     * @async
     * @param  {string} themeName           Name of the theme
     * @return {ThemeDefinition}            Theme definition
     */
    async getThemeConfig (themeName) {
        let themeConfig;
        if (themeName){
            themeConfig = _.find(appState.availableThemes, {name: themeName});
        }
        return themeConfig;
    }

    /**
     * Returns an array of theme css files for given type, with applied base (extend) theme files
     *
     * @async
     * @param  {ThemeDefinition} themeConfig    Theme config definition
     * @param  {string} type                    Type of css files ('initCssFiles', 'cssFiles' or 'overrideCssFiles')
     * @return {string[]}                       Array of css file paths
     */
    async getThemeTypeCssFiles(themeConfig, type) {
        let cssFiles = [];

        if (type){
            if (themeConfig && themeConfig.name){
                if (themeConfig[type]){
                    let baseThemeConfig;
                    if (themeConfig.extends){
                        baseThemeConfig = await this.getThemeConfig(themeConfig.extends);
                        if (!(baseThemeConfig && baseThemeConfig.name)){
                            this.log('Can not find theme "{1}" that is base theme for "{2}"', 'error', [themeConfig.extends, themeConfig.name]);
                        }
                    }

                    if (_.isArray(themeConfig[type]) && themeConfig[type].length){
                        cssFiles = _.map(themeConfig[type], (file) => {
                            return path.resolve(path.join(themeConfig.path, file));
                        });

                        if (baseThemeConfig && baseThemeConfig[type] && _.isArray(baseThemeConfig[type]) && baseThemeConfig[type].length){
                            let baseCssFiles = _.map(baseThemeConfig[type], (file) => {
                                return path.resolve(path.join(baseThemeConfig.path, file));
                            });
                            if (baseCssFiles && baseCssFiles.length){
                                cssFiles = _.union(baseCssFiles, cssFiles);
                            }
                        }
                    }
                } else {
                    this.log('Can not find type "{1}" for theme "{2}"!', 'error', [type, themeConfig.name]);
                }
            }
        } else {
            this.log('No type argument passed for theme css files!', 'error', []);
        }
        return cssFiles;
    }

    /**
     * Returns an array of theme js files for given type, with applied base (extend) theme files
     *
     * @async
     * @param  {ThemeDefinition} themeConfig    Theme config definition
     * @param  {string} type                    Type of js files
     * @return {string[]}                       Array of js file paths
     */
    async getThemeTypeJsFiles (themeConfig, type){
        let jsFiles = [];
        if (type){
            if (themeConfig && themeConfig.name){
                if (themeConfig[type]){
                    let baseThemeConfig;
                    if (themeConfig.extends){
                        baseThemeConfig = await this.getThemeConfig(themeConfig.extends);
                        if (!(baseThemeConfig && baseThemeConfig.name)){
                            this.log('Can not find theme "{1}" that is base theme for "{2}"', 'error', [themeConfig.extends, themeConfig.name]);
                        }
                    }
                    if (_.isArray(themeConfig[type]) && themeConfig[type].length){
                        let themeJsFiles = _.map(themeConfig[type], (file) => {
                            return path.resolve(path.join(themeConfig.path, file));
                        });
                        for (let i=0; i<themeJsFiles.length; i++){
                            let jsFilePath = themeJsFiles[i];
                            if (fs.existsSync(jsFilePath)){
                                jsFiles.push('file://' + jsFilePath);
                            } else {
                                this.log('Problem loading theme JS file "{1}" - file does not exist!', 'error', [jsFilePath]);
                            }
                        }
                    }
                    if (baseThemeConfig && baseThemeConfig[type] && _.isArray(baseThemeConfig[type]) && baseThemeConfig[type].length){
                        let baseThemeJsFiles = _.map(baseThemeConfig[type], (file) => {
                            return path.resolve(path.join(baseThemeConfig.path, file));
                        });
                        let baseJsFiles = [];
                        for (let i=0; i<baseThemeJsFiles.length; i++){
                            let jsFilePath = baseThemeJsFiles[i];
                            if (fs.existsSync(jsFilePath)){
                                baseJsFiles.push('file://' + jsFilePath);
                            } else {
                                this.log('Problem loading base theme JS file "{1}" - file does not exist!', 'error', [jsFilePath]);
                            }
                        }
                        jsFiles = _.union(baseJsFiles, jsFiles);
                    }
                } else {
                    this.log('Can not find type "{1}" for theme "{2}"!', 'error', [type, themeConfig.name]);
                }
            }
        } else {
            this.log('No type argument passed for theme js files!', 'error', []);
        }
        return jsFiles;
    }

    /**
     * Handler that reloads css files after appState.theme has been changed
     *
     * @return {undefined}
     */
    changeTheme () {
        _appWrapper.getHelper('staticFiles').reloadCss();
    }
}

exports.ThemeHelper = ThemeHelper;