Source: control2/hotspot/Hotspot.js

(function() {
    /**
     * @namespace hotspot
     * @memberof Help4.control2
     */
    Help4.control2.hotspot = {};

    /**
     * @enum {string}
     */
    Help4.control2.hotspot.ANIMATION_TYPES = {
        none: 'none',
        fadeinout: 'fadeinout',
        pulsingripples: 'pulsingripples',
        shake: 'shake',
        updown: 'updown'
    }

    const CSS_VARS = {
        spotlightOffset: {unit: 'px'},
        spotlightBlur: {unit: 'px'},
        spotlightOpacity: {}
    };

    /**
     * @typedef {Help4.control2.Control.Params} Help4.control2.hotspot.Hotspot.Params
     * @property {number} [borderSize = 0] - size of border in px
     * @property {boolean} [spotlight = false] - whether to activate spotlight
     * @property {number} [spotlightOffset = 0] - offset for spotlight in px
     * @property {number} [spotlightBlur = 0] - blur for spotlight in px
     * @property {number} [spotlightOpacity = 0] - opacity for spotlight
     * @property {boolean} [callout = false] - whether if it is a callout hotspot
     * @property {boolean} [instantHelp = false] - whether if it is an instant help hotspot
     */

    /**
     * Hotspot base class.
     * @abstract
     * @augments Help4.control2.Control
     * @property {number} borderSize - size of border in px
     * @property {boolean} spotlight - whether to activate spotlight
     * @property {number} spotlightOffset - offset for spotlight in px
     * @property {number} spotlightBlur - blur for spotlight in px
     * @property {number} spotlightOpacity - opacity for spotlight
     * @property {boolean} callout - whether if it is a callout hotspot
     * @property {boolean} instantHelp - whether if it is an instant help hotspot
     */
    Help4.control2.hotspot.Hotspot = class extends Help4.control2.Control {
        /**
         * @override
         * @param {Help4.control2.hotspot.Hotspot.Params} [params]
         * @param {Help4.jscore.ControlBase.Params} [derived]
         */
        constructor(params, derived) {
            const {ControlBase} = Help4.jscore;
            const T = ControlBase.TYPES;
            const TT = ControlBase.TEXT_TYPES;

            super(params, {
                params: {
                    role:             {init: 'button'},  // needs to be in sync with Connected.js
                    tabIndex:         {init: 1},         // needs to be in sync with Connected.js

                    borderSize:       {type: T.number, readonly: true},
                    spotlight:        {type: T.boolean},
                    spotlightOffset:  {type: T.number},
                    spotlightBlur:    {type: T.number},
                    spotlightOpacity: {type: T.number},
                    callout:          {type: T.boolean},
                    instantHelp:      {type: T.boolean},

                    autoEvent:        {init: true}
                },
                statics: {
                    _content: {destroy: false}
                },
                config: {
                    css: 'control-hotspot'
                },
                texts: {
                    text: TT.innerText
                },
                derived
            });
        }

        /**
         * @param {string} text
         * @returns {Help4.control2.hotspot.Hotspot}
         */
        setText(text) {
            const {_content} = this;
            if (_content) {
                _content.innerHTML = '';
                if (text) {
                    const span = this._createElement('span', {css: 'text', dom: _content, text: text});
                    this._setTextAttribute(span, 'text');
                }
            }
            return this;
        }

        /**
         * @override
         * @returns {Help4.control2.hotspot.Hotspot}
         */
        focus() {
            !this.mobile && Help4.Element.execAfterTransition(this.getDom(), () => !this.isDestroyed() && super.focus());
            return this;
        }

        /**
         * gets the position (excluding offsets, including positions like top-left, bottom-right)
         * @returns {Help4.control2.PositionXY}
         */
        getDragStartPosition() {
            // see XRAY-4790; drag enabled hotspots have their own implementation
            // this one is a placeholder which returns actual DOM position
            return this.getPosition();
        }

        /**
         * @override
         */
        getConnectionPoints(params) {
            const p = super.getConnectionPoints(params);

            // due to CSS layout the connection point t & b appear to be to much left; correct
            const b = this.borderSize >> 1;
            p.t.x += b;
            p.b.x += b;

            return p;
        }

        /**
         * returns the meeting point from a line starting in "point" to the hotspot
         * @param {Help4.control2.PositionXY} point
         * @returns {Help4.control2.PositionXY}
         * @abstract
         */
        calcMeetingPoint(point) {
            return point;
        }

        /**
         * @override
         * @param {HTMLElement} dom - control DOM
         */
        _onDomCreated(dom) {
            dom.setAttribute('aria-haspopup', 'dialog');

            // on mobile, we need to add an onclick handler to a div to be able to click it
            this.mobile && dom.addEventListener('click', Help4.noop, false);
        }

        /**
         * @override
         * @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
         */
        _applyPropertyToDom({name, value, oldValue}) {
            switch (name) {
                case 'active':
                    this.getDom().setAttribute('aria-pressed', value);
                    break;

                case 'spotlight':
                    value ? this.addCss(name) : this.removeCss(name);
                    _setCSSVars.call(this);
                    break;

                case 'spotlightOffset':
                case 'spotlightBlur':
                case 'spotlightOpacity':
                    this.spotlight && _setCSSVars.call(this, name);
                    break;

                case 'callout':
                    value && !this.instantHelp ? this.addCss(name) : this.removeCss(name);
                    break;
            }
            super._applyPropertyToDom({name, value, oldValue});
        }
    }

    /**
     * @memberof Help4.control2.hotspot.Hotspot#
     * @param {string} [name]
     * @private
     */
    function _setCSSVars(name) {
        const {spotlight} = this;

        const apply = (name, {unit = ''}) => {
            spotlight
                ? Help4.setCssVariable(`hotspot-${name}`, this[name] + unit)
                : Help4.removeCssVariable(`hotspot-${name}`);
        }

        if (typeof name === 'string') {
            apply(name, CSS_VARS[name]);
        } else {
            for (const [name, value] of Object.entries(CSS_VARS)) {
                apply(name, value);
            }
        }
    }
})();