/** * Abstract Base Class for Bridgeman Accessible Web Components * Provides helper methods for easily initializing components, composing complex components, and managing nested custom elements. */ export class ComposableElement extends HTMLElement { constructor() { super(); } /** * Helper method to load and parse a JSON configuration ("Data Island") from a script tag within the Light DOM of the component. * The script tag should have a `data-{name}-config` attribute where `{name}` is a unique identifier for the component (e.g., 'tablist' for 'ba-tablist'). * * @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist') * @returns {Object|null} The parsed JSON configuration object, or null if not found */ loadDataIslandConfig(name) { const configScript = this.querySelector(`script[data-${name}-config]`); if (!configScript) { console.error(`${name} configuration missing.`); return null; } const config = JSON.parse(configScript.textContent); return config; } /** * Helper method to include the necessary stylesheets and scripts for a component based on its configuration. * This method should be called within the `createTemplateInJS` of a component after loading it's "data island" configurations. * * @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist') * @param {Object} config - The JSON configuration object that includes the necessary information about styles and scripts to include * @param {ShadowRoot} shadow - The shadow DOM root to append the link and script tags to */ includeStylesheetsAndScripts(name, config, shadow) { if (config.componentsStyleHref) { // Create and append the link tag for the components library CSS (including component specific styles) const componentLibraryStyle = document.createElement('link'); componentLibraryStyle.rel = 'stylesheet'; componentLibraryStyle.type = 'text/css'; componentLibraryStyle.href = config.componentsStyleHref; shadow.appendChild(componentLibraryStyle); } // Create additional link tags for any extra CSS files to include const additionalStyles = config[`${name}ExtraStyles`] || []; additionalStyles.forEach(style => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = `/css/${style}.css`; shadow.appendChild(link); }); // Create and append script tags for any extra JavaScript files to include const additionalScripts = config[`${name}ExtraScripts`] || []; additionalScripts.forEach(script => { const scriptElem = document.createElement('script'); scriptElem.type = typeof script === 'object' && typeof script.module === 'boolean' && script.module ? 'module' : typeof script === 'string' && script.endsWith('.mjs') ? 'module' : 'application/javascript'; scriptElem.src = typeof script === 'object' ? script.script.startsWith('http') ? script.script : `/js/${script.script.endsWith('.mjs') ? script.script : script.script + '.js'}` : script.startsWith('http') ? script : `/js/${script.endsWith('.mjs') ? script : script + '.js'}`; shadow.appendChild(scriptElem); }); } /** * Helper method to initialize a component by loading its configuration and including necessary stylesheets and scripts. * This method should be called at the beginning of the `createTemplateInJS` of a component to set up everything needed for the component to function properly. * * @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist') * @param {ShadowRoot} shadow - The shadow DOM root to append the link and script tags to * @param {{ includeStylesAndScripts: boolean, stylesAndScriptsNameOverride?: string, subComponents?: Array<{ name: string, configKey: string, slotNames: Array }> }} options - Options for initializing the component * @returns {Object|null} The parsed JSON configuration object, or null if not found */ initializeComponent(name, shadow, { includeStylesAndScripts = true, stylesAndScriptsNameOverride, subComponents = [] } = {}) { const config = this.loadDataIslandConfig(name); if(config == null) { return null; } if (includeStylesAndScripts) { this.includeStylesheetsAndScripts(typeof stylesAndScriptsNameOverride !== 'undefined' ? stylesAndScriptsNameOverride : name, config, shadow); } subComponents.forEach(subComp => { const subCompElem = this.createSubComponent(subComp.name, config[subComp.configKey], subComp.slotNames); shadow.appendChild(subCompElem); }); return config; } /** * Generates a sub-component, attaches its configuration, and forwards requested slots. * * @param {string} name - The suffix of the component (e.g., 'tablist' for 'ba-tablist') * @param {Object} config - The JSON configuration object to pass to the sub-component * @param {Array} slotNames - An array of exact slot names to forward down * @returns {HTMLElement} The fully constructed sub-component ready to be appended */ createSubComponent(name, config, slotNames = []) { const customElem = document.createElement(`ba-${name}`); // Attach the Configuration Data Island const customElemConfig = document.createElement('script'); customElemConfig.type = 'application/json'; customElemConfig.setAttribute(`data-${name}-config`, ''); customElemConfig.textContent = JSON.stringify(config); customElem.appendChild(customElemConfig); // Forward the explicit slots slotNames.forEach(slotName => { const slotForwarder = document.createElement('slot'); slotForwarder.name = slotName; slotForwarder.slot = slotName; customElem.appendChild(slotForwarder); }); // Return the element so the child class can append it exactly where it belongs in its specific DOM structure return customElem; } }