Source

client/ui/elements/button.js

/**
 * 
 * The Button derives from [Component](kiss.ui.Component.html).
 * 
 * Its a standard clickable button with an icon.
 * 
 * @param {object} config
 * @param {function} [config.action] Action to perform when clicked. Shortcut for config.events.onclick
 * @param {string} [config.text]
 * @param {string} [config.tip] - Tip displayed when hovering the button
 * @param {string} [config.textAlign]
 * @param {string} [config.fontSize]
 * @param {string} [config.fontWeight]
 * @param {string} [config.color]
 * @param {string} [config.colorHover]
 * @param {string} [config.icon]
 * @param {string} [config.iconHover]
 * @param {string} [config.iconSize]
 * @param {string} [config.iconColor]
 * @param {string} [config.iconColorHover]
 * @param {string} [config.iconShadow]
 * @param {string} [config.iconShadowHover]
 * @param {string} [config.iconPosition] Use "top" to put the icon at the top
 * @param {string} [config.iconPadding]
 * @param {string} [config.iconMargin]
 * @param {string} [config.background]
 * @param {string} [config.backgroundColor]
 * @param {string} [config.backgroundColorHover]
 * @param {string} [config.boxShadow]
 * @param {string} [config.boxShadowHover]
 * @param {string} [config.cursor]
 * @param {string} [config.position]
 * @param {string} [config.top]
 * @param {string} [config.left]
 * @param {string} [config.right]
 * @param {string} [config.float]
 * @param {string} [config.display]
 * @param {string} [config.flex]
 * @param {string|number} [config.width]
 * @param {string|number} [config.minWidth]
 * @param {string|number} [config.maxWidth]
 * @param {string|number} [config.height]
 * @param {string|number} [config.minHeight]
 * @param {string|number} [config.maxHeight]
 * @param {string} [config.margin]
 * @param {string} [config.padding]
 * @param {string} [config.border]
 * @param {string} [config.borderStyle]
 * @param {string} [config.borderWidth]
 * @param {string} [config.borderColor]
 * @param {string} [config.borderColorHover]
 * @param {string} [config.borderRadius]
 * @param {string} [config.overflow]
 * @param {string} [config.overflowX]
 * @param {string} [config.overflowY]
 * @param {number} [config.zIndex]
 * @param {boolean} [config.draggable]
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-button class="a-button">
 *  <span class="button-icon font-awesome-icon-class"></span>
 *  <span class="button-text"></span>
 * </a-button>
 * ```
 */
kiss.ui.Button = class Button extends kiss.ui.Component {
    /**
     * Its a Custom Web Component. Do not use the constructor directly with the **new** keyword.
     * Instead, use one of the 3 following methods:
     * 
     * Create the Web Component and call its **init** method:
     * ```
     * const myButton = document.createElement("a-button").init(config)
     * ```
     * 
     * Or use the shorthand for it:
     * ```
     * const myButton = createButton({
     *  text: "Click me!",
     *  icon: "fas fa-check",
     *  action: () => console.log("click!")
     * })
     * 
     * myButton.render()
     * ```
     * 
     * Or directly declare the config inside a container component:
     * ```
     * const myPanel = createPanel({
     *   title: "My panel",
     *   items: [
     *       {
     *           type: "button",
     *           text: "Click me!",
     *           icon: "fas fa-check",
     *           action: () => console.log("click!")
     *       }
     *   ]
     * })
     * myPanel.render()
     * ```
     */
    constructor() {
        super()
    }

    /**
     * Generates a Button from a JSON config
     * 
     * @ignore
     * @param {object} config - JSON config
     * @returns {HTMLElement}
     */
    init(config = {}) {
        super.init(config)

        // Template
        this.icon = config.icon || ""
        this.text = config.text || ""
        this.text = this.text.replaceAll("\n", "<br>")

        this.innerHTML = `
            ${ (config.icon) ? `<span id="button-icon-${this.id}" class="button-icon ${this.icon}"></span>` : "" }
            ${ (config.text) ? `<span id="button-text-${this.id}" class="button-text">${this.text}</span>` : "" }`.removeExtraSpaces()

        // Set properties
        this.buttonIcon = this.querySelector(".button-icon") || {}
        this.buttonText = this.querySelector(".button-text") || {}

        this._setProperties(config, [
            [
                ["draggable"],
                [this]
            ],
            [
                ["display", "flex", "position", "float", "top", "left", "right", "width", "minWidth", "maxWidth", "height", "minHeight", "maxHeight", "margin", "padding", "background", "backgroundColor", "borderColor", "borderRadius", "borderStyle", "borderWidth", "boxShadow", "cursor", "zIndex"],
                [this.style]
            ],
            [
                ["color", "fontSize", "fontWeight", "textAlign"],
                [this.buttonText.style]
            ],
            [
                ["iconSize=fontSize", "iconColor=color", "iconPadding=padding", "iconMargin=margin", "iconShadow=textShadow"],
                [this.buttonIcon.style]
            ]
        ])

        // Set the default display mode that will be restored by the show() method
        this.displayMode = "inline-flex"

        // Bind action to onclick
        if (config.action) this.onclick = config.action

        // Build hover events
        if (config.iconHover) this._createHoverListener("iconHover", config.iconHover, "_setHoverIcon", "_resetIcon")
        if (config.colorHover) this._createHoverListener("colorHover", config.colorHover, "_setHoverColor", "_resetColor")
        if (config.iconColorHover) this._createHoverListener("iconColorHover", config.iconColorHover, "_setHoverIconColor", "_resetIconColor")
        if (config.iconShadowHover) this._createHoverListener("iconShadowHover", config.iconShadowHover, "_setHoverIconShadow", "_resetIconShadow")
        if (config.boxShadowHover) this._createHoverListener("boxShadowHover", config.boxShadowHover, "_setHoverBoxShadow", "_resetBoxShadow")
        if (config.borderColorHover) this._createHoverListener("borderColorHover", config.borderColorHover, "_setHoverBorderColor", "_resetBorderColor")
        if (config.backgroundColorHover) this._createHoverListener("backgroundColorHover", config.backgroundColorHover, "_setHoverBackgroundColor", "_resetBackgroundColor")

        // Force the button's hover to reset after click
        // @note: this is necessary in situations where the "click" hides the view which contains the button, and sends the button to cache in its wrong "hover" state.
        this.onmouseup = () => {
            if (config.colorHover) this._resetColor()
            if (config.iconColorHover) this._resetIconColor()
            if (config.iconShadowHover) this._resetIconShadow()
            if (config.boxShadowHover) this._resetBoxShadow()
            if (config.borderColorHover) this._resetBorderColor()
            if (config.backgroundColorHover) this._resetBackgroundColor()
        }

        if (config.icon) {
            // Define the width of the icon area, to be able to center it properly
            // The width of the icon span should be equal to the height of the button in order to create a regular square and center icons within it
            this.buttonIcon.style.width = (config.height) ? this._computeSize("height") : "var(--button-height)"

            // Change flex flow if depending on the icon position
            this.iconPosition = config.iconPosition || "left"

            const flow = {
                top: "column",
                left: "row",
                bottom: "column-reverse",
                right: "row-reverse"
            }
            this.style.flexFlow = flow[this.iconPosition]
        }

        return this
    }

    /**
     * Change the text of the button
     * 
     * @param {string} text - The new button text
     * @returns this
     * 
     * @example
     * myButton.setText("Click here")
     */
    setText(text = "") {
        text = text.replaceAll("\n", "<br>")
        this.config.text = text
        this.buttonText.innerText = text
        return this
    }

    /**
     * Change the icon of the button
     * 
     * @param {string} iconClass - The new Font Awesome icon class
     * @returns this
     * 
     * @example
     * myButton.setIcon("fas fa-check")
     */
    setIcon(iconClass) {
        this.config.icon = iconClass
        this.icon.split(" ").forEach(className => this.buttonIcon.classList.remove(className))
        this.icon = iconClass
        this.icon.split(" ").forEach(className => this.buttonIcon.classList.add(className))
        return this
    }

    /**
     * Change the color of the text
     * 
     * @param {string} color - The new color, in hexa format
     * @returns this
     * 
     * @example
     * myButton.setColor("#00aaee")
     */
    setColor(color) {
        this.config.color = color
        if ((this.buttonText) && (this.buttonText.style)) this.buttonText.style.color = color
        return this
    }

    /**
     * Change the background color of the text
     * 
     * @param {string} color - The new color, in hexa format
     * @returns this
     * 
     * @example
     * myButton.setBackgroundColor("#00aaee")
     */
    setBackgroundColor(color) {
        this.config.backgroundColor = this.style.backgroundColor = color
        return this
    }

    /**
     * Change the color of the icon
     * 
     * @param {string} color - The new color, in hexa format
     * @returns this
     * 
     * @example
     * myButton.setIconColor("#00aaee")
     */
    setIconColor(color) {
        this.config.iconColor = color
        if (this.config.icon) this.buttonIcon.style.color = color
        return this
    }

    /**
     * Change the color of the border
     * 
     * @param {string} color - The new color, in hexa format
     * @returns this
     * 
     * @example
     * myButton.setBorderColor("#00aaee")
     */
    setBorderColor(color) {
        this.config.borderColor = color
        this.style.borderColor = color
        return this
    }

    /**
     * Manage HOVER for backgroundColor, color, and icon color
     * 
     * @private
     * @ignore
     */
    _createHoverListener(propertyName, propertyValue, setMethod, resetMethod) {
        this[propertyName] = propertyValue
        this.addEventListener("mouseenter", this[setMethod])
        this.addEventListener("mouseleave", this[resetMethod])
    }

    // Color
    _setHoverColor() {
        if (!this.buttonText) return
        this.currentColor = this.buttonText.style.color
        this.buttonText.style.color = this.colorHover
    }

    _resetColor() {
        this.buttonText.style.color = this.currentColor
    }

    // Icon
    _setHoverIcon() {
        if (!this.icon) return
        this.currentIcon = this.icon
        this.setIcon(this.config.iconHover)
    }

    _resetIcon() {
        this.setIcon(this.currentIcon)
    }

    // Icon color
    _setHoverIconColor() {
        if (!this.buttonIcon) return
        this.currentIconColor = this.buttonIcon.style.color
        this.buttonIcon.style.color = this.iconColorHover
    }

    _resetIconColor() {
        this.buttonIcon.style.color = this.currentIconColor
    }

    // Icon shadow
    _setHoverIconShadow() {
        if (!this.buttonIcon) return
        this.currentIconShadow = this.buttonIcon.style.textShadow
        this.buttonIcon.style.textShadow = this.iconShadowHover
    }

    _resetIconShadow() {
        this.buttonIcon.style.textShadow = this.currentIconShadow
    }    

    // Shadow
    _setHoverBoxShadow() {
        this.currentShadow = this.style.boxShadow
        this.style.boxShadow = this.boxShadowHover
    }

    _resetBoxShadow() {
        this.style.boxShadow = this.currentShadow
    }

    // Border color
    _setHoverBorderColor() {
        this.currentBorderColor = this.style.borderColor
        this.style.borderColor = this.borderColorHover
    }

    _resetBorderColor() {
        this.style.borderColor = this.currentBorderColor
    }

    // Background color
    _setHoverBackgroundColor() {
        this.currentBackgroundColor = this.style.backgroundColor
        this.style.backgroundColor = this.backgroundColorHover
    }

    _resetBackgroundColor() {
        this.style.backgroundColor = this.currentBackgroundColor
    }
}

// Create a Custom Element and add a shortcut to create it
customElements.define("a-button", kiss.ui.Button)

/**
 * Shorthand to create a new Button. See [kiss.ui.Button](kiss.ui.Button.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createButton = (config) => document.createElement("a-button").init(config)

;