Source: alpha/simple-card.js

/** Define a new zero dependency custom web component ECMA module that can be used as card with optional header & footer
 *
 * 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: color const not really needed, convert to make direct changes to style (see theme changer for code)
 *       Use uib-brand.css rather than trying to do local css processing for light/dark
 *       Improve slot change handlers and remove on disconnect
 *       Allow varient for slots not just the card
 */

const color = {
    mode: 'light',
    fg: 'var(--text2)',
    bg: 'var(--surface4)',
    border: 'var(--text4)', // 'rgba(255,255,255, .25)',
    // footer:
}
if ( window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ) {
    color.mode = 'dark'
}

/** 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;
            border: 1px solid ${color.border};
            border-radius: 0.5rem;
            margin: 0.2rem;
            background-color: ${color.bg};
            color: ${color.fg};
            background-clip: border-box;
            box-sizing: border-box;
            box-shadow: var(--shadow2);
        }
        #header {display: none;}
        #main   {display: block; padding:1rem 0.5rem;}
        #footer {display: none}
    </style>
    <article>
        <slot name="header" id="header" role="region" aria-label="Card Header"></slot>
        <slot id="main" role="region" aria-label="Card Main Content"></slot>
        <slot name="footer" id="footer" role="region" aria-label="Card Footer"></slot>
    </article>
`

/** Namespace
 * @namespace Alpha
 */

/**
 * @class
 * @augments TiBaseComponent
 * @description Define a new zero dependency custom web component ECMA module that can be used as card with optional header & footer
 *
 * @element simple-card
 * @memberOf Alpha

 * 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:
  * "simple-card:connected" - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
  * "simple-card:ready" - Alias for connected. The instance can handle property & attribute changes
  * "simple-card:disconnected" - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
  * "simple-card: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} variant - Optional. Sets the cards colour variant

 * 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 {string} slot - Populates the cards default slot content (can contain HTML markup)
  * @property {string} header - Populates the cards header slot content (can contain HTML markup)
  * @property {string} footer - Populates the cards footer slot content (can contain HTML markup)

 * @slot Container contents
 * @slot header - Content to go in the header section of the card
 * @slot footer - Content to go in the footer section of the card

 * @example
  * <simple-card name="myComponent" inherit-style="./myComponent.css"></simple-card>

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

    /** 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:
            'variant',
        ]
    }

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

        // Check if header/footer slots get content and turn on border if so
        const slots = this.shadowRoot.querySelectorAll('slot')
        slots[0].addEventListener('slotchange', (e) => {
            const slot = slots[0]
            const nodes = slot.assignedNodes()
            if (nodes.length > 0) {
                Object.assign(slot.style, {
                    color: color.bg,
                    backgroundColor: color.fg,
                    border: `1px solid ${color.border}`,
                    borderRadius: '0.5rem 0.5rem 0 0',
                    // borderBottom: '1px solid silver',
                    display: 'block',
                    padding: '1rem 0.5rem',
                    fontWeight: 'bolder',
                    fontSize: '120%',
                })
            } else slot.style.display = 'none'
        })
        slots[2].addEventListener('slotchange', (e) => {
            const slot = slots[2]
            const nodes = slot.assignedNodes()
            if (nodes.length > 0) {
                Object.assign(slot.style, {
                    border: `1px solid ${color.border}`,
                    borderRadius: '0 0 0.5rem 0.5rem',
                    // borderTop: '1px solid silver',
                    display: 'block',
                    padding: '1rem 0.5rem',
                    fontStyle: 'italic',
                })
            } else slot.style.display = 'none'
        })

        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 - 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) {
        /** 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
        if (attrib === 'variant') {
            this._setVariant(newVal)
        }

        // 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, })
    }

    get variant() {
        return this.getAttribute('variant')
    }

    set variant(value) {
        this.setAttribute('variant', value)
    }

    /** Set colour variant to use as background colour
     * @param {string} value colour variant to use
     */
    _setVariant(value) {
        switch (value) {
            case 'information':
            case 'info': {
                this.style.setProperty('color', 'var(--text1)')
                this.style.setProperty('background-color', 'var(--info)')
                break
            }

            case 'success': {
                this.style.setProperty('color', 'var(--text1)')
                this.style.setProperty('background-color', 'var(--success)')
                break
            }

            case 'warn':
            case 'warning': {
                this.style.setProperty('color', 'var(--text1)')
                this.style.setProperty('background-color', 'var(--warning)')
                break
            }

            case 'error':
            case 'failure': {
                this.style.setProperty('color', 'var(--text1)')
                this.style.setProperty('background-color', 'var(--failure)')
                break
            }

            default: {
                this.style.removeProperty('color')
                this.style.removeProperty('background-color')
                break
            }
        }
    }
} // ---- end of Class ---- //

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

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

// Self-register the HTML tag
customElements.define('simple-card', SimpleCard)