I’m building a multi-touch application using HTML, CSS, jQuery, and Interact.js. The application has draggable squares that display content (HTML files loaded via iframes). Each square has its own content area, and I want users to scroll the content of different squares independently.
The issue arises when one user scrolls the content of one square while another user tries to scroll another square. In this case, the scrolling in the first square stops, and only the second square scrolls. My goal is to allow simultaneous independent scrolling in all squares without interruption.
Implemented Interact.js for drag, gesture, and touch-based interactions.
Used touchmove and touchend events for scrolling inside the iframe content areas.
Disabled interactions temporarily during scrolling using interact(element).unset() and re-enabled them after the scroll ends.
Expected Behavior:
Users should be able to scroll the content of multiple squares independently and simultaneously.
Actual Behavior:
When one user starts scrolling a square, any new scrolling interaction in another square interrupts or stops the first scroll.
<!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">×</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">×</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">×</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">×</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>
/* Remove body margins and center content vertically and horizontally */
body, html {
background-color: #3a3a3a;
margin: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* Define the boundary container for draggable squares */
#boundary {
width: 1920px;
height: 1080px;
border: 2px solid #333;
position: relative;
}
/* 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);
});
});