Source: control2/hotspot/Triangle.js

(function() {
    /**
     * @typedef {Help4.control2.hotspot.Hotspot.Params} Help4.control2.hotspot.Triangle.Params
     * @property {Help4.control2.AreaXYWH} [rect = {x: 0, y: 0, w: 0, h: 0}] - the assigned elements rect
     * @property {Help4.control2.PositionLeftTop} [delta = {left: 0, top: 0}] - in case hotspot is moved
     * @property {string} [size = ''] - triangle size
     * @property {Help4.control2.hotspot.Triangle.POSITIONS} pos - triangle position relative to element
     */

    /**
     * Creates a triangle hotspot.
     * @augments Help4.control2.hotspot.Hotspot
     * @property {Help4.control2.AreaXYWH} rect - the assigned elements rect
     * @property {Help4.control2.PositionLeftTop} delta - in case hotspot is moved
     * @property {string} size - triangle size
     * @property {Help4.control2.hotspot.Triangle.POSITIONS} pos - triangle position relative to element
     */
    Help4.control2.hotspot.Triangle = class extends Help4.control2.hotspot.Hotspot {
        /**
         * @override
         * @param {Help4.control2.hotspot.Triangle.Params} [params]
         */
        constructor(params) {
            const T = Help4.jscore.ControlBase.TYPES;
            super(params, {
                params: {
                    rect:  {type: T.xywh},
                    delta: {type: T.leftTop},
                    size:  {type: T.string},
                    pos:   {type: T.string, mandatory: true}
                },
                config: {
                    css: 'triangle'
                }
            });

            this._dragProps = {min: null, max: null};
        }

        /**
         * @enum {string}
         * @memberof Help4.control2.hotspot.Triangle
         * @property {'topstart'} topstart
         * @property {'topend'} topend
         * @property {'bottomstart'} bottomstart
         * @property {'bottomend'} bottomend
         */
        static POSITIONS = {
            topstart: 'topstart',
            topend: 'topend',
            bottomstart: 'bottomstart',
            bottomend: 'bottomend'
        }

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

        /**
         * @override
         * @param {HTMLElement} dom
         */
        _onDomCreated(dom) {
            super._onDomCreated(dom);

            const c = this._content = this._createElement('div', {css: 'inner'});
            this._createElement('div', {dom: c, id: '-arrow', css: 'arrow'});
        }

        /**
         * @override
         * @param {Help4.control2.PositionXY} point
         */
        calcMeetingPoint({x, y}) {
            const {x: rx, y: ry, w: rw, h: rh} = this.rect;
            const {left: dx, top: dy} = this.delta;
            const {pos} = this;
            const {icon} = _getSizeConfig(this.size);

            const halfWH = icon / 4;  // half of width and height of triangle div element: icon / (2 * 2)
            const isStart = pos[pos.length - 1] === (this.rtl ? 'd' : 't');  // "start" vs "end"
            const xValue = isStart ? rx : rx + rw - halfWH;
            const yValue = pos[0] === 't' ? ry : ry + rh - halfWH;  // "top" vs "bottom"

            const rect = {
                x: xValue + dx,
                y: yValue + dy,
                w: halfWH,
                h: halfWH
            };
            const line = {
                x1: rect.x + rect.w / 2,
                y1: rect.y + rect.h / 2,
                x2: x,
                y2: y
            };

            return Help4.getRectIntersect(rect, line, this.borderSize) || {x, y};
        }

        /**
         * @override
         * @param {Help4.jscore.ControlBase.PropertyChangeEvent} event - the change event
         */
        _applyPropertyToDom({name, value, oldValue}) {
            switch (name) {
                case 'rect':
                case 'delta':
                    _apply.call(this);
                    break;

                case 'pos':
                    this.removeCss(oldValue)
                    this.addCss(value);
                    setTimeout(() => _autoAdjustOffset.call(this), 100);  // async, otherwise delta will be overwritten with old value
                    break;

                case 'size':
                    let config = _getSizeConfig(oldValue);
                    this.removeCss('size-' + config.localization);

                    config = _getSizeConfig(value);
                    this.addCss('size-' + config.localization);
                    break;

                default:
                    super._applyPropertyToDom({name, value, oldValue});
                    break;
            }

            if (Help4.includes(['rect', 'pos', 'size', 'active'], name)) _calculateDragProps.call(this);
        }

        /**
         * @override
         * @returns {Help4.control2.DragDropParams} - allows to define specific DOM (object) and dragable area (area)
         */
        _onGetDragDropParams() {
            return {object: this._dom, ...this._dragProps};
        }

        /**
         * @override
         */
        getDragStartPosition() {
            const {x, y} = this.rect;
            return {x, y};
        }
    }

    /**
     * @memberof Help4.control2.hotspot.Triangle#
     * @private
     */
    function _apply() {
        const dom = this.getDom();
        if (!dom) return;

        const {x: rx, y: ry, w: rw, h: rh} = this.rect;
        const {left: dx, top: dy} = this.delta;

        Help4.extendObject(dom.style, {
            left: rx + dx + 'px',
            top: ry + dy + 'px',
            width: rw + 'px',
            height: rh + 'px'
        });
    }

    /**
     * @memberof Help4.control2.hotspot.Triangle#
     * @param {string} size
     * @returns {Help4.typedef.HotspotSize}
     * @private
     */
    function _getSizeConfig(size) {
        const {HOTSPOT_SIZES} = Help4;
        return HOTSPOT_SIZES.find(hs => hs.size === size) || HOTSPOT_SIZES[0];
    }

    /**
     * @memberOf Help4.control2.hotspot.Triangle#
     * @private
     */
    function _calculateDragProps() {
        const {w: hsWidth, h: hsHeight} = this.getArea();  // hotspot
        const {width: triWidth, height: triHeight} = this.getDom('-arrow').getBoundingClientRect();  // triangle

        const {visualViewport} = window;
        const min = {x: 0, y: 0};
        const max = {x: visualViewport.width, y: visualViewport.height};

        const {POSITIONS} = Help4.control2.hotspot.Triangle;
        switch (this.pos) {
            case POSITIONS.topstart:
                max.x = max.x - triWidth;
                max.y = max.y - triHeight;
                break;
            case POSITIONS.topend:
                min.x = -(hsWidth - triWidth);

                max.x = max.x - hsWidth;
                max.y = max.y - triHeight;
                break;
            case POSITIONS.bottomstart:
                min.y = -(hsHeight - triHeight);

                max.x = max.x - triWidth;
                max.y = max.y - hsHeight;
                break;
            case POSITIONS.bottomend:
                min.x = -(hsWidth - triWidth);
                min.y = -(hsHeight - triHeight);

                max.x = max.x - hsWidth;
                max.y = max.y - hsHeight;
                break;
        }

        this._dragProps = {min, max};

        if (this.enableDragDrop) {
            // hack to apply the updated dragProps
            this.enableDragDrop = false;
            this.enableDragDrop = true;
        }
    }

    /**
     * @memberOf Help4.control2.hotspot.Triangle#
     * @private
     */
    function _autoAdjustOffset() {
        if (this.isDestroyed() || !this.enableDragDrop) return;

        // when offsets applied and the position change makes the hotspot disappear from view, bring it back
        const triangle = this.getDom('-arrow').getBoundingClientRect();
        const {POSITIONS} = Help4.control2.hotspot.Triangle;
        const delta = this.delta;
        switch (this.pos) {
            case POSITIONS.topstart:
                if (triangle.x < 0) delta.left = delta.left - triangle.x;
                if (triangle.y < 0) delta.top = delta.top - triangle.y;
                break;
            case POSITIONS.topend:
                if ((triangle.x + triangle.width) > window.innerWidth) delta.left = window.innerWidth - (this.rect.x + this.rect.w) - triangle.width;
                if (triangle.y < 0) delta.top = delta.top - triangle.y;
                break;
            case POSITIONS.bottomstart:
                if (triangle.x < 0) delta.left = delta.left - triangle.x;
                if ((triangle.y + triangle.width) > window.innerHeight) delta.left = window.innerHeight - (this.rect.y + this.rect.h) - triangle.height;
                break;
            case POSITIONS.bottomend:
                if ((triangle.x + triangle.width) > window.innerWidth) delta.left = window.innerWidth - (this.rect.x + this.rect.w) - triangle.width;
                if ((triangle.y + triangle.width) > window.innerHeight) delta.left = window.innerHeight - (this.rect.y + this.rect.h) - triangle.height;
                break;
        }
        this.delta = delta;  // move triangle to new offset position
        this._fireEvent({type: 'dragdrop', position: delta, action: 'auto'});  // update the newly adjusted offset in edit dialog bubble
    }
})();