Source: modules/responsiveOverlayTool.js

/**
 * @module modules/responsiveOverlayTool
 * @description This module adds an overlay to work with responsive interfaces within the Chromium viewer. This can be activated by adding the query string or command line param of responsivetool=true.
 */
define([
], 

function ()  {

    var ResonsiveTool = function (){ 
        
        window.tEvent.eventStr.EVENT_DO_RESPONSIVE = "EVENT_DO_RESPONSIVE"; 

        this.sourceRect = -1;

        this.ui = {
            "avatarPlaceholder": ".js-avatar-placeholder"
        };

        this.jConfig = -1;

        this.init();
    };


    ResonsiveTool.prototype = {

        // ______________________________________________________________
        //                                                           init
        /**
            * init
            * @description Initializes class. Sets data-lang attribute of <body>
            * @fires onModelReady()
            * @memberOf module:modules/responsiveOverlayTool
            * @instance
        */
        init: function() { 
            var self = this;

            // is model ready?
            if (typeof window.oModel === "undefined" || window.oModel.jConfig === -1){
                window.tEvent.addListener("EVENT_MODEL_READY", function(evt, data) {
                    self.onModelReady();                    
                });                
            } else {
                self.onModelReady();    
            }                       
        },


        // ______________________________________________________________
        //                                                   onModelReady
        /**
            * onModelReady
            * @description After the model is ready, initialize responsive tool.
            * @fires activeTool()
            * @fires onResponsive()
            * @memberOf module:modules/responsiveOverlayTool
            * @instance
        */
        onModelReady: function() { 

            this.jConfig = window.oModel.jConfig;

            let self = this;
            let qs = window.helpers.parseQuerystring();
            let activeTool = false;

            if (typeof qs.responsivetool !== "undefined" && qs.responsivetool === "true"){
                activeTool = true;
            }

            // check commandline
            if (typeof window.oPrsonas !== "undefined" &&
                typeof window.oPrsonas.app !== "undefined" &&
                window.oPrsonas.app.getCommandLineParameters() !== null){

                if (typeof window.oPrsonas.app.getCommandLineParameters().responsivetool !== "undefined"){

                    if (window.oPrsonas.app.getCommandLineParameters().responsivetool === "true") {
                        activeTool = true;
                    }

                }                
            }


            if (activeTool) {

                $(self.ui.avatarPlaceholder).css("border", "1px solid red");

                if (window.oPrsonas.avatar.isConnected()) {
                    window.oPrsonas.app.getWindowRect(function(res) { 
                        self.sourceRect = res;
                        self.activeTool();
                    } );                    
                } else {
                    self.sourceRect = {
                        left: "0",
                        top: "0"
                    };
                    self.activeTool();
                    self.onResponsive();
                }


            }

            // set up responsive avater handling.
            if (this.jConfig.avatar.isResponsive) {
                window.addEventListener('resize', () => {   
                    self.onResponsive();
                });



                window.tEvent.addListener(window.tEvent.eventStr.EVENT_TEMPLATE_LOADED_DETAILS, function(evt, eventId){ 
                    self.onResponsive();              
                });
                
            }
        },

        // ______________________________________________________________
        //                                                     activeTool

        /**
            * activeTool
            * @description Applies responsive tool.
            * @memberOf module:modules/responsiveOverlayTool
            * @instance
        */
        activeTool: function() { 

            // append tool
            $("body").append(window.oDxpTemplates["partial_responsive_tool"]());

            console.log(" * <ResonsiveTool> Activated");

            const viewport      = document.getElementById('viewport');
            const overlay       = document.getElementById('resize-overlay');
            const miniGhost     = document.getElementById('mini-ghost');
            const widthSlider   = document.getElementById('width-slider');
            const heightSlider  = document.getElementById('height-slider');
            const widthInput    = document.getElementById('width-input');
            const heightInput   = document.getElementById('height-input');
            const widthValue    = document.getElementById('width-value');
            const heightValue   = document.getElementById('height-value');
            const submitBtn     = document.getElementById('submit-btn');
            const panel         = document.getElementById('control-panel');
            const toggleBtn     = document.getElementById('toggle-minimize');
            const showBtn       = document.getElementById('show-controls-btn');

            const hHandle = document.getElementById('resize-horizontal');
            const vHandle = document.getElementById('resize-vertical');
            const cHandle = document.getElementById('resize-corner');

            let isResizing      = false;
            let isSliderActive  = false;
            let resizeType      = '';
            let startX = 0, startY = 0;
            let startW = 0, startH = 0;
            let ghost = null;
            let pendingW = 0;
            let pendingH = 0;

            const SPEED_MULT  = 5;
            const MINI_BASE_W = 180;

            // ──────────────────────────────
            // SINGLE PLACE for commit / resize trigger
            // ──────────────────────────────
            function triggerCommit(w, h) {
                const finalW = Math.round(w);
                const finalH = Math.round(h);

                console.log(` * <ResonsiveTool> COMMIT SIZE → ${finalW} × ${finalH} px`);

                window.oPrsonas.app.resize(Number(oResponsiveTool.sourceRect.top), Number(oResponsiveTool.sourceRect.left), finalW, finalH);
            }

            // Silent update – only changes display, no commit
            function silentUpdateDisplay(w, h) {
                w = Math.round(w);
                h = Math.round(h);

                overlay.textContent = `Width: ${w} px  |  Height: ${h} px`;
                widthValue.textContent  = w;
                heightValue.textContent = h;
                widthSlider.value  = w;
                heightSlider.value = h;
                widthInput.value   = w;
                heightInput.value  = h;

                const scale = MINI_BASE_W / Math.max(w, 1);
                miniGhost.style.width  = `${MINI_BASE_W}px`;
                miniGhost.style.height = `${h * scale}px`;
                miniGhost.style.display = 'block';
            }

            // Update show button text with current real dimensions
            function updateShowButtonText() {
                const w = viewport.clientWidth;
                const h = viewport.clientHeight;
                showBtn.textContent = `Show Controls ${w} × ${h}`;
            }

            // Live preview during interaction
            function setLiveSize(w, h) {
                pendingW = w;
                pendingH = h;
                silentUpdateDisplay(w, h);
            }

            // Commit → log → reset to real size
            function commitAndReset() {
                triggerCommit(pendingW, pendingH);

                // Reset controllers to real viewport size (silent)
                const realW = viewport.clientWidth;
                const realH = viewport.clientHeight;
                silentUpdateDisplay(realW, realH);
                updateShowButtonText(); // keep button up-to-date
            }

            // ──────────────────────────────
            // Slider + text input sync + ghost preview
            // ──────────────────────────────
            let sliderGhost = null;

            function createSliderGhost() {
                if (sliderGhost) return;
                
                const r = viewport.getBoundingClientRect();
                sliderGhost = document.createElement('div');
                sliderGhost.id = 'slider-ghost';
                sliderGhost.style.left   = `${r.left}px`;
                sliderGhost.style.top    = `${r.top}px`;
                sliderGhost.style.width  = `${viewport.clientWidth}px`;
                sliderGhost.style.height = `${viewport.clientHeight}px`;
                document.body.appendChild(sliderGhost);
            }

            function updateSliderGhost(w, h) {
                if (!sliderGhost) return;
                sliderGhost.style.width  = `${Math.round(w)}px`;
                sliderGhost.style.height = `${Math.round(h)}px`;
                sliderGhost.style.display = 'block';
            }

            function removeSliderGhost() {
                if (sliderGhost) {
                    document.body.removeChild(sliderGhost);
                    sliderGhost = null;
                }
            }

            // Live sync for sliders
            function syncInputsFromSlider() {
                if (!isSliderActive) return;
                
                const w = parseInt(widthSlider.value, 10) || viewport.clientWidth;
                const h = parseInt(heightSlider.value, 10) || viewport.clientHeight;
                
                setLiveSize(w, h);
                updateSliderGhost(w, h);
                
                widthInput.value = w;
                heightInput.value = h;
            }

            // Live sync for number inputs
            function syncSlidersFromInput() {
                const w = parseInt(widthInput.value, 10) || viewport.clientWidth;
                const h = parseInt(heightInput.value, 10) || viewport.clientHeight;
                
                widthSlider.value = w;
                heightSlider.value = h;
                setLiveSize(w, h);
                updateSliderGhost(w, h);
            }

            // Attach listeners
            widthSlider.addEventListener('input', syncInputsFromSlider);
            heightSlider.addEventListener('input', syncInputsFromSlider);

            widthInput.addEventListener('input', syncSlidersFromInput);
            heightInput.addEventListener('input', syncSlidersFromInput);

            // Slider drag lifecycle: create ghost on mousedown, update on input, remove + commit on mouseup
            function startSlider() {
                isSliderActive = true;
                createSliderGhost();
                // Listen for mouseup anywhere to detect release
                document.addEventListener('mouseup', onSliderMouseUp, { once: true });
            }

            function onSliderMouseUp() {
                isSliderActive = false;
                removeSliderGhost();
                commitAndReset();
            }

            widthSlider.addEventListener('mousedown', startSlider);
            heightSlider.addEventListener('mousedown', startSlider);

            // Submit button – manual commit (no ghost needed here)
            submitBtn.addEventListener('click', () => {
                const w = parseInt(widthInput.value, 10) || viewport.clientWidth;
                const h = parseInt(heightInput.value, 10) || viewport.clientHeight;
                setLiveSize(w, h);
                commitAndReset();
            });

            // ──────────────────────────────
            // Drag handles
            // ──────────────────────────────
            function createGhost() {
                const r = viewport.getBoundingClientRect();
                ghost = document.createElement('div');
                ghost.style.cssText = `
                    position: fixed;
                    border: 2px dashed #e74c3c;
                    pointer-events: none;
                    z-index: 100000;
                    left: ${r.left}px;
                    top: ${r.top}px;
                    width: ${viewport.clientWidth}px;
                    height: ${viewport.clientHeight}px;
                `;
                document.body.appendChild(ghost);
            }

            function startResize(e, type) {
                e.preventDefault();
                isResizing = true;
                resizeType = type;
                startX = e.clientX;
                startY = e.clientY;
                startW = viewport.clientWidth;
                startH = viewport.clientHeight;
                createGhost();
                document.addEventListener('mousemove', onDrag);
                document.addEventListener('mouseup', stopResize);
            }

            function onDrag(e) {
                if (!isResizing) return;

                let dx = e.clientX - startX;
                let dy = e.clientY - startY;

                if (e.shiftKey) {
                    dx *= SPEED_MULT;
                    dy *= SPEED_MULT;
                }

                let newW = startW;
                let newH = startH;

                if (resizeType === 'vertical'   || resizeType === 'both') newW = Math.max(320, startW + dx);
                if (resizeType === 'horizontal' || resizeType === 'both') newH = Math.max(240, startH + dy);

                if (ghost) {
                    ghost.style.width  = `${newW}px`;
                    ghost.style.height = `${newH}px`;
                }

                setLiveSize(newW, newH);
            }

            function stopResize() {
                if (!isResizing) return;
                isResizing = false;
                if (ghost) {
                    document.body.removeChild(ghost);
                    ghost = null;
                }
                commitAndReset();
                document.removeEventListener('mousemove', onDrag);
                document.removeEventListener('mouseup', stopResize);
            }

            hHandle.addEventListener('mousedown', e => startResize(e, 'horizontal'));
            vHandle.addEventListener('mousedown', e => startResize(e, 'vertical'));
            cHandle.addEventListener('mousedown', e => startResize(e, 'both'));

            // ──────────────────────────────
            // Minimize / Show Controls + Viewport toggle
            // ──────────────────────────────
            toggleBtn.addEventListener('click', () => {
                panel.classList.add('hidden');
                viewport.classList.add('hidden');
                $(".resp-tool").addClass("minimized");
            });

            showBtn.addEventListener('click', () => {
                panel.classList.remove('hidden');
                viewport.classList.remove('hidden');
                $(".resp-tool").removeClass("minimized");
            });

            // Initialize + update show button text
            function init() {
                const w = Math.max(320, viewport.clientWidth);
                const h = Math.max(240, viewport.clientHeight);
                silentUpdateDisplay(w, h);
                updateShowButtonText();
            }

            function updateShowButtonText() {
                const w = viewport.clientWidth;
                const h = viewport.clientHeight;
                showBtn.textContent = `Show Controls ${w} × ${h}`;
            }

            init();
            window.addEventListener('resize', () => {
                init();
                updateShowButtonText();
            });
        },


        // ______________________________________________________________
        //                                                   onResponsive
        /**
            * onResponsive
            * @description Fires command to avatar to resize the avatar within the bounds of the avatar placeholder element.
            * @memberOf module:modules/responsiveOverlayTool
            * @instance
        */
        onResponsive: function() { 
            let self = this;
            let rect = $(this.ui.avatarPlaceholder)[0].getBoundingClientRect();
            let vw = window.innerWidth;
            let vh = window.innerHeight;

            console.table({
                "pixel (left)":   rect.left,
                "pixel (top)":    rect.top,
                "pixel (w)":      rect.width,
                "pixel (h)":      rect.height,
                "viewport w":     vw,
                "viewport h":     vh,
                "norm top":       rect.top / vh,
                "norm left":      rect.left / vw,                
                "norm width":     rect.width / vw,
                "norm height":    rect.height / vh
            });

            window.oPrsonas.avatar.setCameraAvatarBox(
                rect.top / vh, 
                rect.left / vw,
                rect.width / vw,
                rect.height / vh
            );

            window.tEvent.fire(window.tEvent.eventStr.EVENT_DO_RESPONSIVE);
            
        }


    };

    
    window.oResponsiveTool = new ResonsiveTool();        
    return (window.oResponsiveTool);    

});