# Source: data-list.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) 2022-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.
     */

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

    /**
     * TODO: Add optional footer ?
     *       topic (uib)
     */

    const styles = `
            data-list {
                --list-style: disc; /* Default list style type */
                --nested-indent: 40px; /* Default nested list indent - all browsers use 40px by default */
            }
            data-list ul {
                list-style: var(--list-style);
            }
            /* Nested list indentation */
            data-list ul ul {
                padding-inline-start: var(--nested-indent);
                margin-inline-start: 0;
            }
            data-list .nested-container {
                /* No list bullet on an li containing a nested lists unless the key text is shown */
                list-style: none;
            }
    `
    /** 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 */
    //             --list-style: disc; /* Default list style type */
    //             --nested-indent: 40px; /* Default nested list indent - all browsers use 40px by default */
    //         }
    //         ul {
    //             list-style: var(--list-style);
    //         }
    //         /* Nested list indentation */
    //         ul ul {
    //             padding-inline-start: var(--nested-indent);
    //             margin-inline-start: 0;
    //         }
    //         .nested-container {
    //             /* No list bullet on an li containing a nested lists unless the key text is shown */
    //             list-style: none;
    //         }
    //     </style>
    //     <slot></slot>
    //     <!-- <ul></ul> -->
    // `

    /** Namespace
     * @namespace Live
     */

    /**
     * @class
     * @augments TiBaseComponent
     * @description Define a new zero dependency custom web component ECMA module that can be used as an HTML tag
     *
     * @element data-list
     * @memberOf Live

     * 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:
      * @function entry Dynamically ament/add a single entry. The list is rebuilt.

     * CUSTOM EVENTS:
      * "data-list:connected" - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
      * "data-list:ready" - Alias for connected. The instance can handle property & attribute changes
      * "data-list:disconnected" - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
      * "data-list: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} type - 💫 Optional. The type of list to use, ul or ol. Default is ul.
      * @property {string} listvar - 💫 Optional. The global variable name to use for the list data. If not set, set the data property directly from JS.
      * @property {string} keyvalueseparator - Optional. The separator to use between key and value in the list items when input is an object. Default is ' :: '. Set to "NULL" to disable key display.
      * @property {string} liststyle - 💫 Optional. The style type to use for the list. Default is `disc` for `ul` and `decimal` for `ol`. May contain any valid CSS list-style string value.

     * 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:
      * By default, all attributes are also created as properties
      * @property {object} data - 💫 The data to use for the list. Either set directly or via the `listvar` attribute. If an object, the key/value separator is used to separate the key and value in the list items.

     NB: properties marked with 💫 are dynamic and have getters/setters. They will cause the list to rebuild.

     * @slot Container contents

     * @example
      * <data-list id="myComponent" type="ol" inherit-style="./myComponent.css"></data-list>

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

        /** The top-level list element - this is created in the shadow DOM */
        #list = null
        /** The list style type to use for the list - this is set as a CSS variable */
        #listStyle = null
        /** The data to use for the list - this is set via the `data` property */
        #entries = null
        /** The type of list to use, ul or ol @type {"ol"|"ul"} */
        #type = 'ul'

        // Optional global variable name to use for the list data
        listvar = null
        // The separator to use between key and value in the list items
        keyvalueseparator = ' :: '

        /** 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:
                'keyvalueseparator', 'liststyle', 'listvar', 'type', 'topic',
            ]
        }

        /** 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))

            this.prependStylesheet(styles)
        }

        /** Runs when an instance is added to the DOM
         * Runs AFTER the initial attributeChangedCallback's
         */
        connectedCallback() {
            // console.info(`[DataList] ${this.id} connectedCallback (${this.tagName})`)
            this._connect() // Keep at start.

            if ( this.listvar && window[this.listvar] ) {
                if ( window[this.listvar] ) {
                    this.data = window[this.listvar]
                    // window.dataList.instances[this.id] = window[newVal]
                    // console.log('>> listvar change >>', this.id, this.#entries, window.dataList.instances[this.id])
                    //window[newVal] = new Proxy()
                } else {
                    console.warn(`[DataList] ${this.id} window.${this.listvar} does not exist, ignoring.`)
                }

                return
            }

            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() {
            this._disconnect() // Keep at end.
        }

        /** Runs when an observed attribute changes - Note: values are always strings
         * NOTE: On initial startup, this is called for each watched attrib set in HTML.
         *       and BEFORE connectedCallback is called.
         * @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) {
            // console.info(`[DataList] ${this.id} attributeChangedCallback: ${attrib} changed from "${oldVal}" to "${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, })
        }

        /** Updates the list attributes based on the current type and style
         * @param {HTMLElement} listEl The list element to update
         * @param {"ol"|"ul"} listType The type of list to use, either 'ol' for ordered or 'ul' for unordered
         */
        updateListAttributes(listEl, listType) {
            if (listEl) {
                if (listEl) {
                    // Update CSS classes/attributes instead of recreating element
                    listEl.setAttribute('data-list-type', listType)

                    // Update ARIA attributes for accessibility
                    if (listType === 'ol') {
                        this.listStyle = 'decimal'
                        listEl.setAttribute('aria-label', 'Ordered list')
                    } else {
                        this.listStyle = 'disc'
                        listEl.setAttribute('aria-label', 'Unordered list')
                    }
                }
                if (this.#listStyle) {
                    // Update the CSS variable for the list style
                    listEl.style.setProperty('--list-style', this.#listStyle)
                }
            }
        }

        // Getter/setter for the `type` public property - maps to this.#type
        set type(val) {
            if (!['ul', 'ol'].includes(val)) {
                console.warn(`[DataList] ${this.id} Invalid type "${val}" specified, defaulting to "ul".`)
                val = 'ul'
            }

            this.#type = val

            this.updateListAttributes(this.#list, this.#type)
        }
        get type() {
            return this.#type
        }

        // Getter/setter for the `listStyle` public property - maps to this.#listStyle
        set liststyle(val) {
            this.#listStyle = val
            this.updateListAttributes(this.#list, this.#type)
        }
        get liststyle() {
            return this.#listStyle
        }

        // Getter/setter for the `data` public property - maps to this.#entries
        set data(val) {
            this.#entries = val

            // If the list already exists, delete it
            if (this.#list) this.#list.remove()

            // Dynamically build the content of the ul/ol
            // Object.keys(this.#entries).forEach( this.buildListItem.bind(this))
            this.buildList(this.#entries, this, this.#type, 1)

            this.#list = this.querySelector('ul')
            this.updateListAttributes(this.#list, this.#type)

            // Issue a custom event to notify that the data has changed
            this._event('dataChanged', this.#entries)
        }
        get data() {
            return this.#entries
        }

        /** Dynamically change/add list entries - rebuilds the list DOM element
         * @param {string|number} key Object key or array index to change/add
         * @param {string} val Updates/new list text (can be HTML)
         * @example
         * // Add a new entry to the list
         * dataList.entry('newKey', 'New list item text')
         * // Update an existing entry in the list
         * dataList.entry('existingKey', 'Updated list item text')
         */
        entry(key, val) {
            this.#entries[key] = val
            this.data = this.#entries
        }

        /** Builds a list from input data
         * @param {Array|object} listData Source data for the list, can be an array or an object
         * @param {HTMLElement|ShadowRoot} parentEl The parent element to append the list to
         * @param {"ol"|"ul"} type  The type of list to create, either 'ol' for ordered or 'ul' for unordered
         * @param {number} depth  Recursion depth. Defaults to 1. Used to limit recursion depth for nested objects/arrays.
         */
        buildList(listData, parentEl, type, depth) {
            if (depth === undefined) depth = 1
            // console.log(`[DataList] ${this.id} buildList: listData=${listData}, listEl=${parentEl}, depth=${depth}`)

            // Create a new list element (all lists are created as ul, numbering is done via CSS)
            const listEl = document.createElement('ul')
            if (depth > 1) {
                // If this is a nested list, add a class to the list element
                listEl.classList.add('nested-list')
                // and add a data-depth attribute to the list element
                listEl.setAttribute('data-depth', depth.toString())
            }

            Object.entries(listData).forEach(([key, value], i) => {
                // Is the key a number? Keep a record
                const keyIsNumeric = !isNaN(parseInt(key))

                // Is the value an object or array?
                if (typeof value === 'object' && value !== null) {
                    if (depth > 3) {
                        // If depth is too high, just show the key and value as a string
                        listEl.insertAdjacentHTML(
                            'beforeend',
                            this.buildLIhtml({ arrayType: Array.isArray(listData), key, value: JSON.stringify(value), i, })
                        )
                        console.warn(`[DataList] ${this.id} buildList: Depth limit reached for key "${key}", showing as string.`)
                        return
                    }

                    // Create a new list item for the key
                    const li = document.createElement('li')

                    // If key is not numeric and if this.keyvalueseparator is not 'NULL', add key as text
                    if (!keyIsNumeric && this.keyvalueseparator !== 'NULL') {
                        const keySpan = document.createElement('span')
                        keySpan.textContent = `${key}${this.keyvalueseparator}`
                        // keySpan.classList.add('nested-key')
                        li.appendChild(keySpan)
                    } else {
                        li.classList.add('nested-container')
                    }

                    // recurse to create a nested list
                    this.buildList(value, li, type, ++depth)
                    this.updateListAttributes(li, type)
                    // add the new list item to its parent element
                    listEl.appendChild(li)
                } else {
                    // Otherwise, just create a list item with the key and value
                    // console.log(`[DataList] ${this.id} buildList: key=${key}, value=${value}, i=${i}`)
                    listEl.insertAdjacentHTML(
                        'beforeend',
                        this.buildLIhtml({ arrayType: Array.isArray(listData), key, value, i, })
                    )
                }
            })

            // Attach the list as the last child of the parent element
            parentEl.appendChild(listEl)
        }

        /** Builds a list item HTML string based on the provided options.
         * @param {object} options Object containing options for building the list item
         * @param {boolean} options.arrayType True if the list is an array, false if it is an object
         * @param {string} options.key The key for the list item, used as the id and data-index
         * @param {string} options.value The value for the list item, displayed as the text
         * @param {number} options.i The index of the item in the list, used for data-index attribute
         * @returns {string} The HTML string for the list item
         */
        buildLIhtml({ arrayType, key, value, i, }) {
            // check if this.#entries is an array
            if (arrayType) {
                return `<li id="${this.id}-${key}" data-index="${i}">${value}</li>`
            }
            // If the keyvalueseparator is 'NULL', then don't show the key
            // otherwise, show the key followed by the separator
            return `<li id="${this.id}-${key}" data-index="${i}">${this.keyvalueseparator === 'NULL' ? '' : `${key}${this.keyvalueseparator}`}${value}</li>`
        }
    } // ---- end of Class ---- //

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

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

    // Self-register the HTML tag
    customElements.define('data-list', DataList)