apps/config-app.js

import { MODULE } from "../module.js";
import { logger } from "../logger.js";

/**
 * HelpersSettingConfig extends {SettingsConfig}
 * 
 * Additional window for 5e Helper specific settings
 * Allows for Settings to be organized in 4 categories
 *  System Helpers
 *  NPC Features
 *  PC Features
 *  Combat Helpers
 * 
 * @todo "display" value which is true or false based on some other setting
 * @todo "reRender" grabs (possibly saves) values and rerenders the Config to change what is displayed dynamically
 */
export class HelpersSettingsConfig extends SettingsConfig{

  static registerSubMenu(subModule, settingsData, {groupLabels = HelpersSettingsConfig.defaultGroupLabels, menuIcon = 'fas fa-user-cog', /* parentMenu = null,*/ tab = 'misc'} = {}){

    const parentMenu = null;
    const subMenuId = randomID();
    const config = false;

    Object.values(settingsData).forEach( setting => mergeObject(setting, {subMenuId, config}) );

    const handler = {
      construct(target) {
        return new target({subModule, subMenuId, groupLabels, parentMenu}); 
      }
    }

    const proxy = new Proxy(HelpersSettingsConfig, handler);

    this._registerMenu({
      name : `${subModule}`,
      label : MODULE.format(`DND5EH.SubModule.${subModule}.label`),
      icon : menuIcon,
      type : proxy,
      parentMenu,
      tab
    })

    MODULE.applySettings(settingsData);

  }

  static _registerMenu(menuData) {
    logger.debug('Registering submenu:', menuData); 
    this._menus.set(menuData.name, menuData);
  }
  
  constructor({subModule = null, subMenuId = null, groupLabels = HelpersSettingsConfig.defaultGroupLabels, parentMenu = null} = {}){
    super();
    this.options.subModule = subModule;
    this.options.groupLabels = groupLabels;
    this.options.subMenuId = subMenuId;
    this.options.parentMenu = parentMenu;
  }

  static _menus = new Collection();

  static get menus() {
    return HelpersSettingsConfig._menus;
  }

  get menus() {
    return HelpersSettingsConfig.menus;
  }

  /**@override */
  static get defaultOptions(){
    return mergeObject(super.defaultOptions, {
      title : MODULE.localize("Helpers"),
      id : "helpers-client-settings",
      template : `${MODULE.data.path}/templates/ModularSettings.html`,
      width : 600,
      height : "auto",
      tabs : [
        {navSelector: ".tabs", contentSelector: ".content", initial: "general"}
      ],
    });
  }

  /** @override */
  _onClickSubmenu(event) {
    event.preventDefault();
    const menu = HelpersSettingsConfig.menus.get(event.currentTarget.dataset.key);
    if ( !menu ) return ui.notifications.error("No submenu found for the provided key");
    const app = new menu.type();
    return app.render(true);
  }

  _onClickReturn(event) {
    event.preventDefault();
    const menu = game.settings.menus.get('dnd5e-helpers.helperOptions');
    if ( !menu ) return ui.notifications.error("No parent menu found");
    const app = new menu.type();
    return app.render(true);
  }

  async _onSubmit(...args) {
    const formData = await super._onSubmit(...args);

    if( this.options.subMenuId ){
      /* submitting from a subMenu, re-render parent */
      await this._onClickReturn(...args);
    }

    return formData;
  }

  /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find('button[name="return"]').click(this._onClickReturn.bind(this));
  }

  static get defaultGroupLabels() {
    return {
      'system': { faIcon: 'fas fa-cog', tabLabel: 'DND5EH.groupLabel.system'},
      'npc-features': { faIcon: 'fas fa-address-book', tabLabel: 'DND5EH.groupLabel.npcFeatures'},
      'pc-features':{ faIcon: 'fas fa-address-book', tabLabel: 'DND5EH.groupLabel.pcFeatures'},
      'combat':{ faIcon: 'fas fa-dice-d20', tabLabel: 'DND5EH.groupLabel.combat'},
      'misc':{ faIcon: 'fas fa-list-alt', tabLabel: 'DND5EH.groupLabel.misc'},
    }
  }

  /**@override */
  getData(options){
    const canConfigure = game.user.can("SETTING_MODIFY");
    const settings = Array.from(game.settings.settings);

    options.title = MODULE.format(options.subMenuId ? `DND5EH.SubModule.${this.options.subModule}.label` : 'DND5EH.ConfigApp.title');
    let data = {
      tabs: duplicate(options.groupLabels),
      hasParent: !!options.subMenuId,
      parentMenu: options.parentMenu
    }

    const registerTabSetting = (tabName) => {
      /* this entry exists already or the setting does NOT have a group,
       * dont need to create another tab. Core settings do not have this field.
       */
      if(data.tabs[tabName].settings) return false;  
      
      /* it doesnt exist, so add a new entry */
      data.tabs[tabName].settings = [];
    }

    const registerTabMenu = (tabName) => {
      /* this entry exists already or the setting does NOT have a group,
       * dont need to create another tab. Core settings do not have this field.
       */
      if(data.tabs[tabName].menus) return false;  
      
      /* it doesnt exist, so add a new entry */
      data.tabs[tabName].menus = [];
    }

    for(let [_, setting] of settings.filter(([_, setting]) => setting.namespace == MODULE.data.name)){

      /* only add an actual setting if the menu ids match */
      if(setting.subMenuId == this.options.subMenuId) {

      if(!canConfigure && setting.scope !== "client") continue;
        setting.group = data.tabs[setting.group] ? setting.group : 'misc'

        /* ensure there is a tab to hold this setting */
        registerTabSetting(setting.group);

        let groupTab = data.tabs[setting.group] ?? false;
        if(groupTab) groupTab.settings.push({
          ...setting,
          type : setting.type instanceof Function ? setting.type.name : "String",
          isCheckbox : setting.type === Boolean,
          isSelect : setting.choices !== undefined,
          isRange : setting.type === Number && setting.range,
          value : MODULE.setting(setting.key),
          path: `${setting.namespace}.${setting.key}`
        });
      } 
    }

    /* check if we are the parent of any registered submenus and add those */
    const childMenus = this.menus.filter( menu => menu.parentMenu == this.options.subMenuId )
    childMenus.forEach( menu => {
      registerTabMenu(menu.tab);
      let groupTab = data.tabs[menu.tab] ?? false;
      if(groupTab) groupTab.menus.push(menu);
    })

    /* clean out tabs that have no entries */
    data.tabs = Object.entries(data.tabs).reduce( (acc, [name, val]) => {
      /* if we have any settings or any menus, keep the tab */
      if(!!val.settings || !!val.menus) acc[name] = val;
      return acc;
    },{})

    logger.debug("GET DATA | DATA | ", data);

    return {
      user : game.user, canConfigure, systemTitle : game.system.data.title, data
    }
  }

  /*
    Need to add a "reRender" state based onChange of specific elements
  */
}