Home Reference Source

js/template/ButtonTemplate.js

import Template from '~/template/Template';
import { HandleUnhandledPromise } from '~/helpers/ErrorManager';
import ActionControllerDelegate from '~/delegate/ActionControllerDelegate';

import { fromEvent } from 'rxjs';
import { filter } from 'rxjs/operators';

import tippy from 'tippy.js/dist/tippy.all.min.js';

/**
 * @typedef {Object} ButtonColor
 * @property {Object} green - Green background
 * @property {Object} accent - Accent colored background
 * @property {Object} blue - Blue background
 * @property {Object} plain - No special coloring just blue text
 * @property {Object} accentBorder - Accent border and foreground
 * @property {Object} blackOnWhite - white button
 * @property {Object} highContrast - always maintains max contrast
 * @property {Object} minimalGreen - green version of high contrast.
 * @property {Object} red - red version of high contrast.
 * @property {Object} activeBlue - non-transitioning theme color
 * @property {Object} activeAxtell - non-transitioning theme color of axtell
 */
export const ButtonColor = {
    green: 'green',
    accent: 'accent',
    blue: 'blue',
    plain: 'plain',
    accentBorder: 'accent-border',
    blackOnWhite: 'black-on-white',
    highContrast: 'high-contrast',
    red: 'red',
    minimalGreen: 'minimal-green',
    activeBlue: 'active-blue',
    activeAxtell: 'active-axtell'
};

/**
 * @typedef {Object} ButtonStyle
 * @property {Object} normal - Normal default style
 * @property {Object} plain - A small caps amd smaller style
 * @property {Object} minimal - Modern style w/ border and slight bevel effect
 */
export const ButtonStyle = {
    normal: '',
    plain: 'plain',
    minimal: 'minimal',
};

/**
 * A button
 */
export default class ButtonTemplate extends Template {
    /**
     * @param {Object} opts
     * @param {string} options.text - The text of the button
     * @param {?Element} options.icon - The icon node
     * @param {ButtonColor} options.color
     * @param {ButtonStyle} [options.style=default]
     */
    constructor({ text, icon, color, style = ButtonStyle.normal }) {
        let node;

        if (icon) text = " " + text;

        node = (
            <button class={`button button--color-${color} button--align-center button--style-${style}`}>
                { icon || <DocumentFragment/> }
                { " " }
            </button>
        );

        super(node);

        /**
         * The label of the button. Reactive
         * @type {string}
         */
        this.text = null;
        node.appendChild(<span>{ this.defineLinkedText('text', text) }</span>);

        /**
         * If should be full width
         * @type {Boolean}
         */
        this.isWide = null;
        this.defineLinkedClass('isWide', 'button--size-wide');

        /**
         * If should be 'small'
         * @type {Boolean}
         */
        this.isSmall = null;
        this.defineLinkedClass('isSmall', 'button--size-small');

        /**
         * If should have little bit of padding on top
         * @type {Boolean}
         */
        this.hasPaddedTop = null;
        this.defineLinkedClass('hasPaddedTop', 'button--padding-top');

        /**
         * If the button is active. Only applies to those colors which have this preference set.
         * @type {Boolean}
         */
        this.isActive = null;
        this.defineLinkedClass('isActive', 'button--active');

        /**
         * If should have little padding on sides
         * @type {Boolean}
         */
        this.hasPaddedHorizontal = null;
        this.defineLinkedClass('hasPaddedHorizontal', 'button--padding-horizontal');

        /**
         * If has shadow
         * @type {Boolean}
         */
        this.hasShadow = null;
        this.defineLinkedClass('hasShadow', '!button--shadow-none');

        /** @type {ActionControllerDelegate} */
        this.delegate = new ActionControllerDelegate();

        this._isDisabled = false;

        this._observeClick = fromEvent(node, 'click')
            .pipe(
                filter(() => !this._isDisabled));

        node.addEventListener("click", () => {
            if (this._isDisabled) return;
            this.trigger().catch(HandleUnhandledPromise);
        });

        this._message = null;

    }

    /**
     * Observes the click of the button
     * @return {Observable}
     */
    observeClick() {
        return this._observeClick;
    }

    /**
     * Sets if disabled. Will stop click events from firing
     * @param {Boolean} isDisabled
     * @param {?string} message optional message to display
     */
    setIsDisabled(isDisabled, message = null) {
        this.underlyingNode.title = "";
        this._message?.destroy(true);

        if (isDisabled) {
            this._isDisabled = true;
            this.underlyingNode.classList.add('button--color-disabled');

            if (message) {
                this.underlyingNode.title = message;
                this._message = tippy.one(this.underlyingNode, {
                    duration: [200, 150],
                    size: 'small'
                })
            }
        } else {
            this._isDisabled = false;
            this.underlyingNode.classList.remove('button--color-disabled');
        }
    }

    /**
     * Called when loading
     */
    async trigger() {
        await this.delegate.didSetStateTo(this, true);
    }
}