Files
Foundry-VTT-Docker/resources/app/client/ui/tabs.js
2025-01-04 00:34:03 +01:00

156 lines
4.6 KiB
JavaScript

/**
* @typedef {object} TabsConfiguration
* @property {string} [group] The name of the tabs group
* @property {string} navSelector The CSS selector used to target the navigation element for these tabs
* @property {string} contentSelector The CSS selector used to target the content container for these tabs
* @property {string} initial The tab name of the initially active tab
* @property {Function|null} [callback] An optional callback function that executes when the active tab is changed
*/
/**
* A controller class for managing tabbed navigation within an Application instance.
* @see {@link Application}
* @param {TabsConfiguration} config The Tabs Configuration to use for this tabbed container
*
* @example Configure tab-control for a set of HTML elements
* ```html
* <!-- Example HTML -->
* <nav class="tabs" data-group="primary-tabs">
* <a class="item" data-tab="tab1" data-group="primary-tabs">Tab 1</li>
* <a class="item" data-tab="tab2" data-group="primary-tabs">Tab 2</li>
* </nav>
*
* <section class="content">
* <div class="tab" data-tab="tab1" data-group="primary-tabs">Content 1</div>
* <div class="tab" data-tab="tab2" data-group="primary-tabs">Content 2</div>
* </section>
* ```
* Activate tab control in JavaScript
* ```js
* const tabs = new Tabs({navSelector: ".tabs", contentSelector: ".content", initial: "tab1"});
* tabs.bind(html);
* ```
*/
class Tabs {
constructor({group, navSelector, contentSelector, initial, callback}={}) {
/**
* The name of the tabs group
* @type {string}
*/
this.group = group;
/**
* The value of the active tab
* @type {string}
*/
this.active = initial;
/**
* A callback function to trigger when the tab is changed
* @type {Function|null}
*/
this.callback = callback ?? null;
/**
* The CSS selector used to target the tab navigation element
* @type {string}
*/
this._navSelector = navSelector;
/**
* A reference to the HTML navigation element the tab controller is bound to
* @type {HTMLElement|null}
*/
this._nav = null;
/**
* The CSS selector used to target the tab content element
* @type {string}
*/
this._contentSelector = contentSelector;
/**
* A reference to the HTML container element of the tab content
* @type {HTMLElement|null}
*/
this._content = null;
}
/* -------------------------------------------- */
/**
* Bind the Tabs controller to an HTML application
* @param {HTMLElement} html
*/
bind(html) {
// Identify navigation element
this._nav = html.querySelector(this._navSelector);
if ( !this._nav ) return;
// Identify content container
if ( !this._contentSelector ) this._content = null;
else if ( html.matches(this._contentSelector )) this._content = html;
else this._content = html.querySelector(this._contentSelector);
// Initialize the active tab
this.activate(this.active);
// Register listeners
this._nav.addEventListener("click", this._onClickNav.bind(this));
}
/* -------------------------------------------- */
/**
* Activate a new tab by name
* @param {string} tabName
* @param {boolean} triggerCallback
*/
activate(tabName, {triggerCallback=false}={}) {
// Validate the requested tab name
const group = this._nav.dataset.group;
const items = this._nav.querySelectorAll("[data-tab]");
if ( !items.length ) return;
const valid = Array.from(items).some(i => i.dataset.tab === tabName);
if ( !valid ) tabName = items[0].dataset.tab;
// Change active tab
for ( let i of items ) {
i.classList.toggle("active", i.dataset.tab === tabName);
}
// Change active content
if ( this._content ) {
const tabs = this._content.querySelectorAll(".tab[data-tab]");
for ( let t of tabs ) {
if ( t.dataset.group && (t.dataset.group !== group) ) continue;
t.classList.toggle("active", t.dataset.tab === tabName);
}
}
// Store the active tab
this.active = tabName;
// Optionally trigger the callback function
if ( triggerCallback ) this.callback?.(null, this, tabName);
}
/* -------------------------------------------- */
/**
* Handle click events on the tab navigation entries
* @param {MouseEvent} event A left click event
* @private
*/
_onClickNav(event) {
const tab = event.target.closest("[data-tab]");
if ( !tab ) return;
event.preventDefault();
const tabName = tab.dataset.tab;
if ( tabName !== this.active ) this.activate(tabName, {triggerCallback: true});
}
}