# Source: collapsible-headings.js

    /** Define a new zero dependency custom web component ECMA module that can be used as an HTML tag
     *
     * Version: See the class code
     *
     */
    /** Copyright (c) 2024-2025 Julian Knight (Totally Information)
     * https://it.knightnet.org.uk, https://github.com/TotallyInformation
     *
     * Licensed under the Apache License, Version 2.0 (the 'License');
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     * http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an 'AS IS' BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /** TODO
     * - Make deep dynamic additions work (currently only works on initial load)
     * - Add a host attrib or class to allow sections to be pre-hidden (add to the hx tag?)
     *   - sub-option to store collapsed sections in browser storage (probably needs ID's to be added to all heads and wrappers)
     * - Maybe add another option to make headings auto-numbered?
     * - Add option for an icon
     * - How to ignore outer div (as delivered by uib-element) - maybe something with observer?
     */

    import TiBaseComponent from '../libs/ti-base-component'

    /** Only use a template if you want to isolate the code and CSS */
    const template = document.createElement('template')
    template.innerHTML = /*html*/`
        <style>
            :host {
                display: block;   /* default is inline */
                contain: content; /* performance boost */
            }
        </style>
        <slot></slot>
    `

    /** Namespace
     * @namespace Beta
     */

    /**
     * @class
     * @augments TiBaseComponent
     * @description Wraps around HTML text with headings h1-h4 making the headings a collapsible hierarchy.
     *
     * @element collapsible-headings
     * @memberOf Beta

     * METHODS FROM BASE: (see TiBaseComponent)
     * STANDARD METHODS:
      * @function attributeChangedCallback Called when an attribute is added, removed, updated or replaced
      * @function connectedCallback Called when the element is added to a document
      * @function constructor Construct the component
      * @function disconnectedCallback Called when the element is removed from a document

     * OTHER METHODS:
      * None

     * CUSTOM EVENTS:
      * "component-template:connected" - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
      * "component-template:ready" - Alias for connected. The instance can handle property & attribute changes
      * "component-template:disconnected" - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
      * "component-template:attribChanged" - When a watched attribute changes. `evt.details.data` contains the details of the change.
      * NOTE that listeners can be attached either to the `document` or to the specific element instance.

     * Standard watched attributes (common across all my components):
     * @property {string|boolean} inherit-style - Optional. Load external styles into component (only useful if using template). If present but empty, will default to './index.css'. Optionally give a URL to load.
     * @property {string} name - Optional. HTML name attribute. Included in output _meta prop.

     * Other watched attributes:
     * @property {string} levels - Optional. Default='h2, h3, h4, h5'. A single string detailing the heading levels to make collapsible.

     * PROPS FROM BASE: (see TiBaseComponent)
     * OTHER STANDARD PROPS:
      * @property {string} componentVersion Static. The component version string (date updated). Also has a getter that returns component and base version strings.

     * Other props:
      * @property {string} level Default='h2, h3, h4, h5'. A single string detailing the heading levels to make collapsible.
      * By default, all attributes are also created as properties

     * @slot Container contents

     * See https://github.com/runem/web-component-analyzer?tab=readme-ov-file#-how-to-document-your-components-using-jsdoc
     */
    class CollapsibleHeadings extends TiBaseComponent {
        /** Component version */
        static componentVersion = '2025-02-25'

        levels = 'h2, h3, h4, h5'

        /** Makes HTML attribute change watched
         * @returns {Array<string>} List of all of the html attribs (props) listened to
         */
        static get observedAttributes() {
            return [
                // Standard watched attributes:
                'inherit-style', 'name',
                // Other watched attributes:
                'levels',
            ]
        }

        /** NB: Attributes not available here - use connectedCallback to reference */
        constructor() {
            super()
            // Only attach the shadow dom if code and style isolation is needed - comment out if shadow dom not required
            if (template && template.content) this._construct(template.content.cloneNode(true))
        }

        /** Runs when an instance is added to the DOM */
        connectedCallback() {
            this._connect() // Keep at start.

            // Get a reference to the slot tag
            this.shadowSlot = this.shadowRoot.querySelector('slot')

            // Create a mutation observer that processes changes to the content of the slot
            this.observer = new MutationObserver((mutations) => {
                this.processSlotContent(mutations)
            })
            this.observer.observe(this, { childList: true, subtree: true, })

            // Cannot use the slotchange event as it doesn't fire except for top-level changes to the slot, not deep changes.

            // Do a first pass to process the initial load
            this.processSlotContent()

            this._ready() // Keep at end. Let everyone know that a new instance of the component has been connected & is ready
        }

        /** Runs when an instance is removed from the DOM */
        disconnectedCallback() {
            // Remove the observer
            this.observer.disconnect()

            this._disconnect() // Keep at end.
        }

        /** Runs when an observed attribute changes - Note: values are always strings
         * @param {string} attrib Name of watched attribute that has changed
         * @param {string} oldVal The previous attribute value
         * @param {string} newVal The new attribute value
         */
        attributeChangedCallback(attrib, oldVal, newVal) {
            /** Optionally ignore attrib changes until instance is fully connected
             * Otherwise this can fire BEFORE everthing is fully connected.
             */
            // if (!this.connected) return

            // Don't bother if the new value same as old
            if ( oldVal === newVal ) return
            // Create a property from the value - WARN: Be careful with name clashes
            this[attrib] = newVal

            // Add other dynamic attribute processing here.
            // If attribute processing doesn't need to be dynamic, process in connectedCallback as that happens earlier in the lifecycle

            // Keep at end. Let everyone know that an attribute has changed for this instance of the component
            this._event('attribChanged', { attribute: attrib, newVal: newVal, oldVal: oldVal, })
        }

        /** Walk through slot content.
         * Called once when connected and then every time slot content changes
         * param {*} records Mutated records
         * param {*} observer Reference to the observer object
         * @param {MutationRecord[]=} [mutations] Optional. The mutation records to process
         */
        processSlotContent(mutations) {
            // console.log('>> Processing slot content >>', mutations, this)
            // mutations.forEach((mutation) => {
            //     console.log('>> Mutation >>', mutation)
            //     mutation.addedNodes.forEach(node => {
            //         console.log('>> Mutation:addedNode >>', node)
            //         if (node.nodeType === Node.ELEMENT_NODE) {
            //             // this.processElement(/** @type {HTMLElement} */ (node))
            //         }
            //     })
            // })

            // Get the contents of the slot
            const assignedNodes = this.shadowSlot.assignedNodes({ flatten: true, })
            // console.log('>> Assigned nodes >>', assignedNodes)
            // NB: assignedElements would be better but not supported in Safari 12.1/12.2

            assignedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    this.processElement(/** @type {HTMLElement} */ (node))
                }
            })
        }

        /** Process each found element node. Adds pointer cursor to the heading and wraps content in a div that can be collapsed.
         * @param {HTMLElement} element The HTML element to process
         */
        processElement(element) {
            // console.log('>> Processing element >>', element)

            if (!element.matches(this.levels) || element.dataset.collapsibleProcessed) return

            const level = parseInt(element.tagName[1])
            const nextElements = []

            let sibling = element.nextElementSibling
            while (sibling) {
                if (sibling.matches(this.levels) && parseInt(sibling.tagName[1]) <= level) {
                    break
                }
                nextElements.push(sibling)
                sibling = sibling.nextElementSibling
            }

            // Create a collapsible section for content under the heading
            const collapsibleSection = document.createElement('div')
            collapsibleSection.classList.add('collapsible-content')
            // @ts-ignore
            collapsibleSection.dataset.level = level

            nextElements.forEach(el => collapsibleSection.appendChild(el))

            element.insertAdjacentElement('afterend', collapsibleSection)
            element.style.cursor = 'pointer'

            // Add click event to toggle collapsible section
            element.addEventListener('click', function () {
                const isCollapsed = collapsibleSection.style.display === 'none'
                collapsibleSection.style.display = isCollapsed ? 'block' : 'none'
            })

            // Mark this heading as processed
            element.dataset.collapsibleProcessed = 'true'
        }
    } // ---- end of Class ---- //

    // Make the class the default export so it can be used elsewhere
    export default CollapsibleHeadings

    /** Self register the class to global
     * Enables new data lists to be dynamically added via JS
     * and lets the static methods be called
     */
    window['CollapsibleHeadings'] = CollapsibleHeadings

    // Self-register the HTML tag
    customElements.define('collapsible-headings', CollapsibleHeadings)