How to Maintain Independent Scrolling and Clickable Links in Preloaded Iframe/WebView Squares on 4 Pages?

I’m building a web application with an interactive UI where users can drag, resize, rotate, and zoom content inside four draggable squares (#square1, #square2, etc.). Each square contains an iframe with scrollable content, and I’m using Interact.js to handle drag/resize operations and jQuery for event handling.

I’m facing a strange issue when multiple users interact with different squares at the same time:

  1. Simultaneous Scrolling Problem:

If User1 starts scrolling the content in square1 and User2 starts scrolling square2, I want both to be able to scroll simultaneously without interrupting each other.
However, as soon as User2 begins interacting with square2, User1’s scroll in square1 stops. I want both scrolls to continue independently.
Touch Gesture Interference:

  1. The bigger issue is that links inside the squares stop working when users are interacting with touch gestures (e.g., dragging, pinching, rotating). Links work fine when no touch gestures are active, but once the user starts interacting with a square, the hyperlinks become unresponsive.
    Ideally, I want users to continue scrolling content while still being able to click on the hyperlinks inside each square.

I’ve set up separate scroll event listeners for each square, with each scroll position stored in localStorage to persist across sessions. I’m also using CSS touch-action settings (e.g., touch-action: none) for gesture-based interactions, and I’ve ensured each square is draggable using Interact.js.

Despite these efforts, the scroll behavior and hyperlink functionality are still problematic when touch gestures are active. Specifically, the link inside the square becomes unclickable as long as gestures (dragging, zooming, or rotation) are taking place.
Independent Scrolling: Users should be able to scroll content in any square without interrupting other squares, even if they’re interacting with different squares at the same time.
Clickable Links: Links inside each square should remain clickable during touch interactions (drag, pinch, rotate, etc.), and scrolling should not interfere with hyperlink functionality.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Multi-touch Squares</title>
    <script src="js/jquery.min.js"></script>
    <script src="js/interact.min.js"></script>
    <link rel="stylesheet" href="css/style.css" />
  </head>
  <body>
    
    <!-- Buttons to show hidden squares -->
    <div class="show-buttons">
      <div class="show-btn type1" id="circle1"></div>
      <div class="show-btn type2" id="circle2"></div>
      <div class="show-btn type3" id="circle3"></div>
      <div class="show-btn type4" id="circle4"></div>
    </div>

    <!-- Boundary container for draggable squares -->
    <div id="boundary">
      <!-- Square 1 -->
      <div id="square1" class="square hidden">
        <div class="square-header">
          Name of Item 1
          <div class="close-btn">&times;</div>
        </div>
        <div class="square-content">
          <iframe src="a.html" frameborder="0" style="pointer-events: auto;"></iframe>
        </div>
      </div>
      <!-- Square 2 -->
      <div id="square2" class="square hidden">
        <div class="square-header">
          Name of Item 2
          <div class="close-btn">&times;</div>
        </div>
        <div class="square-content">
          <iframe src="b.html" frameborder="0" style="pointer-events: auto;"></iframe>
        </div>
      </div>
      <!-- Square 3 -->
      <div id="square3" class="square hidden">
        <div class="square-header">
          Name of Item 3
          <div class="close-btn">&times;</div>
        </div>
        <div class="square-content">
          <iframe src="c.html" frameborder="0" style="pointer-events: auto;"></iframe>
        </div>
      </div>
      <!-- Square 4 -->
      <div id="square4" class="square hidden">
        <div class="square-header">
          Name of Item 4
          <div class="close-btn">&times;</div>
        </div>
        <div class="square-content">
          <iframe src="d.html" frameborder="0" style="pointer-events: auto;"></iframe>
        </div>
      </div>
    </div>


    
    <script src="js/script.js"></script>
  </body>
</html>
/* Style for draggable squares */
.square {
    width: 480px;
    height: 1080px;
    position: absolute;
    user-select: none;
    transform-origin: center;
    background-color: white;
    overflow: hidden;
    z-index: 500;
}

/* Content inside each square (scrollable if necessary) */
.square-content {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
}

/* Make iframes fit square dimensions and remove borders */
.square iframe {
    width: 100%;
    height: 100%;
    border: none;
}

/* Individual styling for each square with unique colors and positions */
#square1 {
    border: 2px solid red;
    left: 50px;
    top: 50px;
    border-top: 30px solid #FF7F7F;
}
#square2 {
    border: 2px solid red;
    left: 300px;
    top: 50px;
    border-top: 30px solid #ADD8E6;
}
#square3 {
    border: 2px solid red;
    left: 50px;
    top: 300px;
    border-top: 30px solid #90EE90;
}
#square4 {
    border: 2px solid blue;
    left: 50px;
    top: 50px;
    border-top: 30px solid #ADD8E6;
}

/* Close button styling */
.close-btn {
    position: absolute;
    top: 5px;
    right: 5px;
    background: white;
    border: 1px solid #333;
    border-radius: 50%;
    width: 20px;
    height: 20px;
    line-height: 18px;
    text-align: center;
    cursor: pointer;
    font-family: Arial, sans-serif;
    font-size: 12px;
    z-index: 500;
    touch-action: none;
}

/* Styling for the show buttons container */
.show-buttons {
    position: absolute;
    top: 0%;             /* Position from the top */
    left: 50%;             /* Center horizontally */
    transform: translateX(-50%); /* Adjust to truly center */
    z-index: 1;
    display: flex;         /* Use flexbox for alignment */
}

/* Styling for each show button */
.show-btn {
    width: 50px;           /* Set a fixed width */
    height: 50px;          /* Set a fixed height */
    margin: 5px;           /* Space between circles */
    border-radius: 50%;    /* Make it circular */
    cursor: pointer;        /* Pointer cursor on hover */
    touch-action: none;     /* Disable touch actions */
}

/* Specific styles for each show button */
#circle1 { 
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: red; 
}
#circle2 { 
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: blue; 
}
#circle3 {
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: green; 
}
#circle4 { 
    width: 480px;          /* Specific width */
    height: 1080px;        /* Specific height */
    border-radius: 0;      /* Ensure square shape */
    background-color: yellow; 
}

/* Hide buttons by default */
.hidden {
    display: none;
}

/* Styles for squares in scroll mode */
.square.scroll-mode {
    touch-action: pan-y pinch-zoom;
}

/* Styles for squares not in scroll mode */
.square:not(.scroll-mode) {
    touch-action: none;
}

/* Ensure iframe is interactive */
.square iframe {
    pointer-events: auto;
}

/* Disable interactions when not scrolling */
.square:not(.scroll-mode) iframe {
    pointer-events: none;
}

/* Allow interaction when in scroll mode */
.square.scroll-mode iframe {
    pointer-events: auto;
}
$(document).ready(function () {
    // Object to store transformation states (position, scale, rotation) for each square
    const transformStates = {};

    // Boundary element where squares are confined
    const boundary = document.getElementById('boundary');

    // Variables for touch-based scrolling in square content
    let touchStartY = 0;
    let scrolling = false;

    // Variables for tracking click vs drag
    let isDragging = false;
    let dragStartTime = 0;
    const DRAG_THRESHOLD = 10; // Pixel movement threshold
    const DRAG_TIME_THRESHOLD = 200; // Milliseconds

    // Track the highest z-index dynamically
    let highestZIndex = 1000;

    // Prevent default behavior for touch events on buttons to avoid accidental gestures
    $('.show-btn, .close-btn').on('touchstart touchmove touchend', function (e) {
        e.preventDefault(); // Prevent default touch behavior
    });

    // Disable dragging for the show-buttons (circles)
    interact('.show-btn').unset();

    // Explicitly ensure show-buttons cannot be moved
    $('.show-btn').on('mousedown touchstart', function (e) {
        e.stopPropagation(); // Stop event propagation to prevent dragging
    });

    // Function to set up interactions for draggable and gesturable elements
    function setupInteractions(element) {
        interact(element)
            .draggable({
                inertia: true, // Enable inertia for smooth dragging
                modifiers: [
                    interact.modifiers.restrict({
                        restriction: boundary, // Restrict dragging within boundary
                        elementRect: { top: 0, left: 0, bottom: 1, right: 1 },
                    }),
                ],
                listeners: {
                    start(event) {
                        bringToFrontIfNeeded(event.target); // Bring square to the front if needed
                    },
                    move: dragMoveListener, // Handle drag movements
                    end(event) {
                        // Retain z-index on release
                        const state = transformStates[event.target.id];
                        state.scale = state.currentScale;
                    },
                },
            })
            .gesturable({
                listeners: {
                    start(event) {
                        // Initialize the starting angle and scale for gestures
                        const state = transformStates[event.target.id];
                        state.startAngle = state.angle - event.angle;
                        state.startScale = state.currentScale;
                    },
                    move(event) {
                        // Update the scale and angle based on gesture movements
                        const state = transformStates[event.target.id];
                        const newScale = state.startScale * event.scale;
                        state.currentScale = Math.max(0.5, Math.min(1.5, newScale));

                        const interpolationFactor = 0.1;
                        state.scale = state.scale + (state.currentScale - state.scale) * interpolationFactor;
                        state.angle = state.startAngle + event.angle;

                        updateElementTransform(event.target); // Apply transformations
                    },
                    end(event) {
                        // Finalize the scale after gesture ends
                        const state = transformStates[event.target.id];
                        state.scale = state.currentScale;
                    },
                },
            });

        // Add touch/click listener to bring tapped squares to the front
        $(element).on('mousedown touchstart', function (e) {
            if (!isDragging) {
                bringToFrontIfNeeded(element); // Bring to front if not dragging
            }
        });

        const $content = $(element).find('.square-content');

        // Handle touch-based scrolling within square content
        $content.on('touchmove', function (e) {
            const touchY = e.originalEvent.touches[0].clientY;
            const deltaY = touchStartY - touchY;
            if (!scrolling && Math.abs(deltaY) > 10) {
                scrolling = true;
                $(element).addClass('scroll-mode'); // Enable scroll mode
                interact(element).unset(); // Disable interactions during scroll
            }

            if (scrolling) {
                e.stopPropagation(); // Stop event propagation
                this.scrollTop += deltaY; // Scroll content
                touchStartY = touchY; // Update touch start position
            }
        });

        // Reset scrolling state on touch end
        $content.on('touchend', function () {
            scrolling = false;
            $(element).removeClass('scroll-mode'); // Disable scroll mode
            setTimeout(() => setupInteractions(element), 100); // Re-enable interactions
        });
    }

    // Function to set up handlers for each circle button
    function setupCircleHandlers(circleId, squareIds) {
        $(`#${circleId}`).on("mousedown touchstart", function (e) {
            isDragging = false;
            dragStartTime = Date.now(); // Record start time

            const startX = e.type === "mousedown" ? e.pageX : e.originalEvent.touches[0].pageX;
            const startY = e.type === "mousedown" ? e.pageY : e.originalEvent.touches[0].pageY;

            $(document).on("mousemove touchmove", function (moveEvent) {
                const currentX = moveEvent.type === "mousemove" ? moveEvent.pageX : moveEvent.originalEvent.touches[0].pageX;
                const currentY = moveEvent.type === "mousemove" ? moveEvent.pageY : moveEvent.originalEvent.touches[0].pageY;

                if (Math.abs(currentX - startX) > DRAG_THRESHOLD || Math.abs(currentY - startY) > DRAG_THRESHOLD) {
                    isDragging = true; // Mark as dragging if movement exceeds threshold
                }
            });

            $(document).on("mouseup touchend", function () {
                $(document).off("mousemove touchmove");
                $(document).off("mouseup touchend");

                if (!isDragging && Date.now() - dragStartTime < DRAG_TIME_THRESHOLD) {
                    positionSquare(circleId, squareIds[0]); // Position square if not dragging
                }
            });
        });
    }

    // Function to position a square based on the corresponding circle button
    function positionSquare(circleId, squareId) {
        const circlePosition = $(`#${circleId}`).offset();
        const top = circlePosition.top;
        const left = circlePosition.left;

        const $square = $(`#${squareId}`);
        $square.css({
            top: top + 'px',
            left: left + 'px',
            transform: 'translate(0px, 0px) scale(1) rotate(0deg)'
        }).removeClass('hidden').show(); // Show and position square

        // Reset transform state when repositioned
        transformStates[squareId] = {
            x: 0,
            y: 0,
            scale: 1,
            angle: 0,
            currentScale: 1,
        };

        // Reinitialize interactions to ensure proper dragging
        setupInteractions($square[0]);

        // Reset iframe content to default
        const iframe = $square.find('iframe');
        if (iframe.length > 0) {
            const defaultSrc = iframe.attr('data-default-src');
            if (defaultSrc) {
                iframe.attr('src', defaultSrc);
            }
        }

        // Bring the square to the front
        bringToFrontIfNeeded($square[0]);
    }

    // Function to bring an element to the front by updating its z-index
    function bringToFrontIfNeeded(element) {
        const currentZIndex = parseInt($(element).css('z-index'), 10);
        if (currentZIndex < highestZIndex) {
            highestZIndex += 1; // Increment the highest z-index
            $(element).css('z-index', highestZIndex); // Assign the new z-index
        }
    }

    // Listener for drag movements
    function dragMoveListener(event) {
        const target = event.target;
        const state = transformStates[target.id];

        state.x += event.dx; // Update x position
        state.y += event.dy; // Update y position

        updateElementTransform(target); // Apply transformations
    }

    // Function to update the transform property of an element
    function updateElementTransform(element) {
        const state = transformStates[element.id];
        element.style.transform = `translate(${state.x}px, ${state.y}px) scale(${state.scale}) rotate(${state.angle}deg)`; // Apply CSS transform
    }

    // Set up handlers for each circle button
    setupCircleHandlers("circle1", ["square1"]);
    setupCircleHandlers("circle2", ["square2"]);
    setupCircleHandlers("circle3", ["square3"]);
    setupCircleHandlers("circle4", ["square4"]);

    // Initialize interactions for each square
    $(".square").each(function () {
        const element = this;
        const $square = $(element);

        // Initialize transformation state for this square
        transformStates[element.id] = {
            x: 0,
            y: 0,
            scale: 1,
            angle: 0,
            currentScale: 1,
        };

        // Store default iframe source
        const iframe = $square.find('iframe');
        if (iframe.length > 0) {
            iframe.attr('data-default-src', iframe.attr('src'));
        }

        setupInteractions(element); // Set up interactions for the square
    });

    // Close button handler to hide the square
    $(".close-btn").on("click touchend", function (e) {
        e.stopPropagation();
        e.preventDefault();
        const square = $(this).closest(".square");
        const squareId = square[0].id;
        
        // Force reset any ongoing interaction
        interact(square[0]).unset();
        
        // Reset transform state
        transformStates[squareId] = {
            x: 0,
            y: 0,
            scale: 1,
            angle: 0,
            currentScale: 1,
        };
        
        // Reset the square's transform
        square.css('transform', 'translate(0px, 0px) scale(1) rotate(0deg)');
        
        // Hide the square
        square.addClass("hidden").hide();
        
        // Simulate touchend/mouseup to reset any ongoing drag
        $(document).trigger('touchend');
        $(document).trigger('mouseup');
        
        // Re-initialize interactions after a brief delay
        setTimeout(() => {
            setupInteractions(square[0]);
        }, 100);
    });
});