I’m using InteractJS’s draggable and dropzone functions to implement a drag-and-drop UI, but my cards are not landing in the correct position within the drop zones. The endPosition is misaligned, sometimes defaulting to 0 or resolving incorrectly (e.g., 5 instead of 2). I’ve confirmed that contentZones contains the correct drop areas, but dropzone isn’t always matching them properly. How can I ensure that dropped elements correctly align with their expected positions?
Here is the use of InteractJS’s dropzone and draggable:
interact('.content').dropzone({
accept: '.card',
overlap: 0.5,
ondrop: function(event){
const dragged = event.relatedTarget;
const dropzone = event.target;
const contentZones = Array.from(document.querySelectorAll('.content'));
const startPosition = dragged.originalContentIndex ?? contentZones.indexOf(dragged.closest('.draggable-content'));
const endPosition = contentZones.indexOf(dropzone);
// Validate positions
if (endPosition === -1 || startPosition === endPosition) {
console.log('Invalid move detected:', { startPosition, endPosition });
return;
}
// Handle existing card first
const existing = dropzone.querySelector('.card');
if (existing && existing !== dragged) {
// Remove existing card before any other DOM changes
dropzone.removeChild(existing);
// Calculate next position
let nextIndex = endPosition + 1;
if (nextIndex >= contentZones.length) {
nextIndex = 0;
}
//const targetZone = contentZones[nextIndex];
const targetZone = contentZones[endPosition];
if (!targetZone) {
console.error(` drop zone not found at index ${endPosition}`);
return;
}
console.log(`Target drop zone confirmed:`, targetZone.id || 'No ID');
// Move existing card
/*targetZone.appendChild(existing);
resetElementStyle(existing);*/
// Update backend for existing card
const existingGroupID = existing.querySelector('.dropdown-item[data-group-id]')?.getAttribute('data-group-id');
const existingItemID = existing.querySelector('.dropdown-item[data-group-id]')?.getAttribute('data-item-id');
const existingAppTreeID = existing.querySelector('.dropdown-item[data-applicationtreeid]')?.getAttribute('data-applicationtreeid');
if (existingGroupID && existingItemID && existingAppTreeID) {
/*moveItemMultipleLevels(
existingAppTreeID,
existingGroupID,
existingItemID,
'down',
1,
endPosition,
nextIndex,
dropzone
);*/
}
}
// Handle dragged card
if (dragged.parentNode) {
//dragged.parentNode.removeChild(dragged);
}
/*dropzone.appendChild(dragged);
resetElementStyle(dragged);*/
// Update backend for dragged card
const direction = endPosition > startPosition ? 'down' : 'up';
const levels = Math.abs(endPosition - startPosition);
const dropdownItem = dragged.querySelector('.dropdown-item[data-group-id]');
if (dropdownItem) {
const groupID = dropdownItem.getAttribute('data-group-id');
const itemID = dropdownItem.getAttribute('data-item-id');
const applicationTreeID = dropdownItem.getAttribute('data-applicationtreeid');
console.log('Available move actions:', Array.from(dragged.querySelectorAll('.dropdown-item')).map(action => ({
href: action.getAttribute('href'),
groupID: action.getAttribute('data-group-id'),
itemID: action.getAttribute('data-item-id'),
applicationTreeID: action.getAttribute('data-applicationtreeid')
})));
if (groupID && itemID && applicationTreeID && levels > 0) {
//alert(startPosition);
//alert(endPosition);
moveItemMultipleLevels(
applicationTreeID,
groupID,
itemID,
direction,
levels,
startPosition,
endPosition,
dropzone
);
}
}
// Update tracking
//dragged.originalContentIndex = endPosition;
}
});
interact('.card').draggable({
inertia: false,
autoScroll: true,
listeners: {
start(event) {
//alert('start');
const target = event.target;
const rect = target.getBoundingClientRect();
dragStartPosition.x = event.clientX;
dragStartPosition.y = event.clientY;
console.log('drag start event.clientY: ' + event.clientY);
dragOffset.x = event.clientX - rect.left;
dragOffset.y = event.clientY - rect.top;
const computedStyle = window.getComputedStyle(target);
target.originalStyles = {
width: computedStyle.width,
height: computedStyle.height,
minWidth: computedStyle.minWidth,
minHeight: computedStyle.minHeight,
maxWidth: computedStyle.maxWidth,
maxHeight: computedStyle.maxHeight,
position: computedStyle.position,
flex: computedStyle.flex,
flexBasis: computedStyle.flexBasis,
flexGrow: computedStyle.flexGrow,
flexShrink: computedStyle.flexShrink
};
const deltaX = event.clientX - dragStartPosition.x;
const deltaY = event.clientY - dragStartPosition.y;
direction = Math.abs(deltaX) > Math.abs(deltaY)
? (deltaX > 0 ? 'down' : 'up')
: (deltaY > 0 ? 'down' : 'up');
const contentZones = Array.from(document.querySelectorAll('.content'));
target.originalContentIndex = contentZones.indexOf(target.closest('.content'));
startPosition = contentZones.indexOf(target.closest('.content'));
target.style.width = computedStyle.width;
target.style.height = computedStyle.height;
target.style.minWidth = computedStyle.width;
target.style.minHeight = computedStyle.height;
target.style.maxWidth = computedStyle.width;
target.style.maxHeight = computedStyle.height;
target.style.flex = 'none';
target.style.position = 'fixed';
target.style.zIndex = 1000;
target.style.left = (event.clientX - dragOffset.x) + 'px';
target.style.top = (event.clientY - dragOffset.y) + 'px';
target.classList.add('dragging');
},
move(event) {
const target = event.target;
target.style.left = (event.clientX - dragOffset.x) + 'px';
target.style.top = (event.clientY - dragOffset.y) + 'px';
dragLastPosition.x = event.clientX;
dragLastPosition.y = event.clientY;
// Track zones moved over for cards
const elementsUnder = document.elementsFromPoint(event.clientX, event.clientY);
//console.log('Elements detected at drop point:', elementsUnder.map(el => el.className));
const containerUnder = elementsUnder.find(el => el.classList.contains('content'));
/*if (containerUnder) {
zonesMovedOver.add(containerUnder);
}*/
const correctedLevelsMoved = Math.min(zonesMovedOver.size - 1, 2); // Prevent excessive zone shifts
const endPosition = startPosition + (direction === 'up' ? -1 : 1) * correctedLevelsMoved;
if (containerUnder && !zonesMovedOver.has(containerUnder)) {
//alert('added!');
zonesMovedOver.add(containerUnder);
console.log('Added container:', containerUnder.id);
}
},
end(event) {
const target = event.target;
// Reset styles
target.classList.remove('dragging');
Object.entries(target.originalStyles).forEach(([key, value]) => {
target.style[key] = value;
});
target.style.position = 'relative';
target.style.left = '0';
target.style.top = '0';
const contentZones = Array.from(document.querySelectorAll('.content'));
const startPosition = target.originalContentIndex ?? contentZones.indexOf(target.closest('.content'));
const endPosition = target._dropIndex;
// Only proceed if we have a valid drop
if (endPosition === undefined || startPosition === endPosition) return;
const direction = endPosition > startPosition ? 'down' : 'up';
const levels = Math.abs(endPosition - startPosition);
alert('this moveItemMultipleLevels, direction: ' + direction);
const dropzone = contentZones[endPosition];
const existing = dropzone.querySelector('.card');
// Move existing card first if needed
if (existing && existing !== target) {
let nextIndex = endPosition + 1;
if (nextIndex >= contentZones.length) nextIndex = 0;
const targetZone = contentZones[1];
//const targetZone = layoutGroup3.querySelector(`.content:nth-child(${expectedIndex + 1})`);
if (!existing.contains(targetZone)) {
targetZone.appendChild(existing);
resetElementStyle(existing);
const existingGroupID = existing.querySelector('.dropdown-item[data-group-id]')?.getAttribute('data-group-id');
const existingItemID = existing.querySelector('.dropdown-item[data-group-id]')?.getAttribute('data-item-id');
const existingAppTreeID = existing.querySelector('.dropdown-item[data-applicationtreeid]')?.getAttribute('data-applicationtreeid');
if (existingGroupID && existingItemID && existingAppTreeID) {
moveItemMultipleLevels(
existingAppTreeID,
existingGroupID,
existingItemID,
'down',
1,
endPosition,
nextIndex
);
}
} else {
console.warn('Blocked circular move: existing card contains targetZone.');
return;
}
}
// Now drop the dragged card
dropzone.appendChild(target);
resetElementStyle(target);
// Backend update for dragged card
const draggedContainerMenu = target.closest('.card');
if (draggedContainerMenu) {
const dropdownItem = draggedContainerMenu.querySelector('.dropdown-item[data-group-id]');
if (dropdownItem) {
const groupID = dropdownItem.getAttribute('data-group-id');
const itemID = dropdownItem.getAttribute('data-item-id');
const applicationTreeID = dropdownItem.getAttribute('data-applicationtreeid');
if (groupID && itemID && applicationTreeID && levels > 0) {
moveItemMultipleLevels(
applicationTreeID,
groupID,
itemID,
direction,
levels,
startPosition,
endPosition
);
}
}
}
// Update index for future moves
target.originalContentIndex = endPosition;
delete target._dropIndex; // Clean up
}
}
});
Here is moveItemMultipleLevels:
const moveItemMultipleLevels = async (applicationTreeID, groupID, itemID, direction, levels, startPosition, endPosition, dropzone) => {
console.log('moveItemMultipleLevels called with:', {
applicationTreeID,
groupID,
itemID,
direction,
levels,
startPosition,
endPosition,
dropzone,
moving: `from position ${startPosition} to position ${endPosition}`
});
const moves = [];
try {
const fetchUrl = `index.cfm?fa=MoveGroupItem&ApplicationTreeID=${applicationTreeID}&GroupID=${groupID}&ItemID=${itemID}&direction=${direction}&levels=${levels}`;
const response = await fetch(fetchUrl);
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
moves.push(data);
await new Promise(resolve => setTimeout(resolve, 100));
// Ensure dragged card is properly detected
const draggedCard = window.draggedCard;
if(!draggedCard) {
console.error(`No dragged card detected—dragging not properly initiated.`);
return;
}
console.log('Dragged card detected:', draggedCard.querySelector('.dropdown-item')?.dataset.itemId);
const layoutGroup3 = document.querySelector('#Content_2BEC2B57-5E08-4F32-9DDC6006A8B13775');
if(!layoutGroup3) {
console.error('Layout Group 3 not found');
return;
}
// Get all content zones inside Layout Group 3
const contentZones = Array.from(layoutGroup3.querySelectorAll('.content'));
console.log('Debugging Layout Group 3 structure before drop:');
console.log('Detected zones:', contentZones.map((zone, i) => `Index ${i}: ${zone.id || 'No ID'}`));
console.log('Detected cards:', contentZones.map((zone, i) => `Index ${i}: ${zone.querySelector('.card')?.id || 'Empty'}`));
console.log('Expected drop at index:', endPosition);
document.querySelectorAll('.card').forEach((card, index) => {
card.setAttribute('id', `card-${index}`);
window.draggedCard = card;
console.log(`Assigned ID to card: card-${index}`);
});
console.log('Detected draggable cards:', [...document.querySelectorAll('.card')].map(card => card.id));
contentZones.forEach(zone => {
zone.addEventListener('drop', (event) => {
event.preventDefault();
console.log(`Attempting drop...`);
console.log(`window.draggedCard at drop:`, window.draggedCard);
if(!window.draggedCard) {
console.error('No dragged card detected.');
return;
}
zone.appendChild(window.draggedCard);
console.log(`Dropped card successfully.`);
});
});
// Ensure the drop is happening within valid content zones
if(endPosition < 0 || endPosition >= contentZones.length) {
console.error(`Invalid drop index: ${endPosition}.`);
return;
}
//const targetZone = contentZones[endPosition];
const expectedIndex = contentZones.findIndex(zone => zone === dropzone);
if(expectedIndex === -1) {
console.error(`Dropzone not found in contentZones.`);
return;
}
endPosition = expectedIndex;
console.log('Calculated drop position:', endPosition);
const targetZone = layoutGroup3.querySelector(`.content:nth-child(${expectedIndex + 1})`);
if(!targetZone) {
console.error(`Target drop zone not found at index ${endPosition}`);
return;
}
console.log(`Target drop zone confirmed:`, targetZone.id || 'No ID');
// Move the dragged card to the new position
console.log('targetZone');
console.log(targetZone);
console.log('draggedCard');
console.log(draggedCard);
console.log('endPosition');
console.log(endPosition);
if(draggedCard.parentNode) {
draggedCard.parentNode.removeChild(draggedCard);
}
targetZone.appendChild(draggedCard);
console.log(`Dragged card moved to position ${endPosition}`);
// Cleanup hover effect
contentZones.forEach(zone => zone.classList.remove('hovered'));
return moves;
} catch (error) {
console.error('Error in moveItemMultipleLevels:', error);
throw error;
}
};
I tried debugging by logging detected zones and verifying dropzone against contentZones, but dropzone isn’t always found in the expected location, and the cards occasionally land in unintended areas. I’m looking for guidance on ensuring accurate positioning within drop zones.
