Source: control2/hotspot/Connected.js

(function() {
    /**
     * @typedef {Help4.control2.Control.Params} Help4.control2.hotspot.Connected.Params
     * @property {string} hotspotType - hotspot type, such as "circle" or "rectangle"
     */

    /**
     * Combines a hotspot with one or more connector lines ("laser beams").
     * @augments Help4.control2.Control
     * @property {string} hotspotType - hotspot type, such as "circle" or "rectangle"
     */
    Help4.control2.hotspot.Connected = class extends Help4.control2.Control {
        /**
         * @override
         * @param {Help4.control2.hotspot.Connected.Params} [params]
         * @param {Help4.jscore.ControlBase.Params} [derived]
         */
        constructor(params, derived) {
            const dfSet = (json, params, dfSet) => {
                if (json === this) return this;  // is not a JSON but myself

                // is not a JSON but another instance of my class
                if (json instanceof this.constructor) {
                    json = json.dataFunctions.toObject();
                }

                const jsonKeys = Object.keys(json);
                const myKeys = this.dataFunctions.getKeys();
                const myProperties = {};

                const hotspot = this._hotspot;
                const hotspotKeys = hotspot ? hotspot.dataFunctions.getKeys() : [];
                const hotspotProperties = {};

                for (const key of jsonKeys) {
                    if (myKeys.indexOf(key) >= 0) {
                        myProperties[key] = json[key];
                    } else if (hotspotKeys.indexOf(key) >= 0) {
                        hotspotProperties[key] = json[key];
                    }
                }

                dfSet(myProperties, params);
                hotspot.dataFunctions.set(hotspotProperties, params);

                return this;
            }

            const T = Help4.jscore.ControlBase.TYPES;
            super(params, {
                params: {
                    role:        {init: 'button'},  // needs to be in sync with Hotspot.js
                    tabIndex:    {init: 1},         // needs to be in sync with Hotspot.js

                    hotspotType: {type: T.string, readonly: true, mandatory: true}
                },
                statics: {
                    _hotspot:       {},
                    _connections:   {},
                    _hotspotParams: {destroy: false}
                },
                dataFunctions: {
                    set: dfSet
                },
                config: {
                    css: 'control-connectedhotspot'
                },
                derived
            });
        }

        /** @returns {Help4.control2.hotspot.Connected} */
        cleanConnections() {
            this._destroyControl('_connections');
            this._connections = null;
            return this;
        }

        /**
         * @param {Object} data - parameters for line
         * @returns {Help4.control2.Line}
         */
        addConnection(data) {
            this._connections ||= this._createControl(Help4.control2.container.Container, {
                id: this.id + '-c',
                type: 'Line',
                dom: this.getDom()
            });

            const {_connections, _hotspot} = this;
            const {id, point: point1, ending: ending1, thickness, endingSize, visible = false} = data;
            const point2 = _hotspot.calcMeetingPoint(data.point);
            return _connections.add({id, point1, point2, ending1, thickness, endingSize, visible});
        }

        /**
         * @param {string} connId
         * @returns {Help4.control2.hotspot.Connected}
         */
        removeConnection(connId) {
            const {_connections} = this;
            if (_connections) {  // connections will be null if no connection exists
                _connections.remove(connId);
                if (!_connections.count()) this.cleanConnections();
            }
            return this;
        }

        /**
         * return position of the connected hotspot control
         * @override
         */
        getPosition() {
            return this._hotspot?.getPosition();
        }

        /**
         * return position of the connected hotspot control
         * @override
         */
        getDragStartPosition() {
            return this._hotspot?.getDragStartPosition();
        }

        /**
         * @override
         * @returns {Help4.control2.hotspot.Connected}
         */
        focus() {
            !this.mobile && this._hotspot?.focus();
            return this;
        }

        /** @override */
        getConnectionPoints() {
            const {_hotspot} = this;
            return _hotspot?.getConnectionPoints.apply(_hotspot, arguments);
        }

        /**
         * @override
         * @param {Help4.control2.hotspot.Connected.Params} params - same params as provided to the constructor
         */
        _onAfterInit(params) {
            super._onAfterInit(params);
            this._hotspotParams = params;
            this._hotspot = null;
            this._connections = null;
        }

        /** @override */
        _onBeforeDestroy() {
            this.cleanConnections();
            super._onBeforeDestroy();
        }

        /**
         * @override
         * @param {HTMLElement} dom - control DOM
         */
        _onDomAttached(dom) {
            // create hotspot control params
            Help4.extendObject(this._hotspotParams, {
                _metadata: {connectedHotspotId: this.id},
                hotspotType: null,
                container: null,
                css: null,
                id: this.id + '-h',
                active: this.active,
                rtl: this.rtl,
                language: this.language,
                mobile: this.mobile,
                dom: dom,

                // small hack: min-width is misused to signal the width that is consumed by all hotspot borders from CSS
                // see this._hotspot.calcMeetingPoint
                borderSize: parseInt(getComputedStyle(dom).minWidth) || 0
            });

            const t = Help4.toFirstUpperCase(this.hotspotType);
            const h = this._hotspot = this._createControl(Help4.control2.hotspot[t], this._hotspotParams);
            h.addListener('*', (event) => {
                this._fireEvent(event);
            });
            this._hotspotParams = null;  // no longer needed

            // add missing properties; just as proxy to direct to hotspot
            const hotspotKeys = h.dataFunctions.getKeys();
            for (const key of hotspotKeys) {
                if (!this.hasOwnProperty(key)) _defineProperty(this, h, key);
            }

            // override my own properties if needed
            _defineProperty(this, h, 'active');

            this.addCss(this.hotspotType);

            super._onDomAttached(dom);
        }

        /**
         * @override
         * @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
         */
        _applyPropertyToDom({name, value, oldValue}) {
            const hs = this._hotspot;
            if (hs && hs.hasOwnProperty(name)) {
                const attConfig = hs.dataFunctions.getConfig(name);
                if (attConfig.readonly) return;

                hs[name] = value;

                name === 'visible' && super._applyPropertyToDom({name, value, oldValue});  // XRAY-3603
            } else {
                super._applyPropertyToDom({name, value, oldValue});
            }
        }
    }

    // XRAY-3664: new function is needed to solve issue with IE11
    /**
     * @memberof Help4.control2.hotspot.Connected#
     * @param {Help4.control2.hotspot.Connected} scope
     * @param {Help4.control2.hotspot.Hotspot} hotspot
     * @param {string} property
     * @private
     */
    function _defineProperty(scope, hotspot, property) {
        // XXX: dataFunctions.onChange tracking most likely not working for this construct
        // needs to attach a listener to the hotspot and forward its events through me
        Object.defineProperty(scope, property, {
            get: () => hotspot[property],
            set: value => hotspot[property] = value
        });
    }
})();