skills/implementing-drag-drop/scripts/calculate_drop_position.js

274 lines
7.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Calculate Drop Position Utility
* Determines valid drop zones and insertion indices for drag-and-drop operations
*/
// Calculate the drop position based on cursor position
function calculateDropPosition(cursor, elements, orientation = 'vertical') {
if (!elements || elements.length === 0) {
return { index: 0, closestElement: null };
}
const distances = elements.map((element, index) => {
const rect = element.getBoundingClientRect();
const center = orientation === 'vertical'
? rect.top + rect.height / 2
: rect.left + rect.width / 2;
const cursorPosition = orientation === 'vertical' ? cursor.y : cursor.x;
const distance = Math.abs(center - cursorPosition);
return { index, distance, center, element };
});
// Sort by distance to find closest element
distances.sort((a, b) => a.distance - b.distance);
const closest = distances[0];
// Determine if cursor is before or after the closest element
const cursorPos = orientation === 'vertical' ? cursor.y : cursor.x;
const insertIndex = cursorPos < closest.center ? closest.index : closest.index + 1;
return {
index: Math.min(insertIndex, elements.length),
closestElement: closest.element,
distance: closest.distance,
};
}
// Calculate drop zone for nested containers
function calculateDropZone(cursor, containers) {
const zones = [];
containers.forEach(container => {
const rect = container.getBoundingClientRect();
// Check if cursor is within container bounds
if (cursor.x >= rect.left &&
cursor.x <= rect.right &&
cursor.y >= rect.top &&
cursor.y <= rect.bottom) {
// Calculate distance to center for priority
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distance = Math.sqrt(
Math.pow(cursor.x - centerX, 2) +
Math.pow(cursor.y - centerY, 2)
);
zones.push({
container,
rect,
distance,
area: rect.width * rect.height,
});
}
});
if (zones.length === 0) return null;
// Sort by smallest area first (most specific container)
zones.sort((a, b) => a.area - b.area);
return zones[0].container;
}
// Calculate grid position for 2D layouts
function calculateGridPosition(cursor, gridContainer, columns) {
const rect = gridContainer.getBoundingClientRect();
const relativeX = cursor.x - rect.left;
const relativeY = cursor.y - rect.top;
const columnWidth = rect.width / columns;
const column = Math.floor(relativeX / columnWidth);
const row = Math.floor(relativeY / (rect.height / Math.ceil(gridContainer.children.length / columns)));
const index = row * columns + column;
return {
index: Math.min(index, gridContainer.children.length),
row,
column,
position: { x: column * columnWidth, y: row * (rect.height / columns) },
};
}
// Check if drop is valid based on rules
function validateDrop(draggedItem, dropTarget, rules = {}) {
const {
maxItems = Infinity,
allowedTypes = [],
rejectedTypes = [],
customValidator = null,
} = rules;
// Check max items constraint
if (dropTarget.children && dropTarget.children.length >= maxItems) {
return { valid: false, reason: 'Container is full' };
}
// Check type constraints
if (allowedTypes.length > 0 && !allowedTypes.includes(draggedItem.type)) {
return { valid: false, reason: 'Item type not allowed' };
}
if (rejectedTypes.length > 0 && rejectedTypes.includes(draggedItem.type)) {
return { valid: false, reason: 'Item type rejected' };
}
// Run custom validation if provided
if (customValidator) {
const customResult = customValidator(draggedItem, dropTarget);
if (!customResult.valid) {
return customResult;
}
}
return { valid: true, reason: null };
}
// Calculate auto-scroll speed based on cursor position
function calculateAutoScroll(cursor, container, threshold = 50) {
const rect = container.getBoundingClientRect();
const scrollSpeed = { x: 0, y: 0 };
const maxSpeed = 15;
// Horizontal scrolling
if (cursor.x < rect.left + threshold) {
// Scroll left
const distance = rect.left + threshold - cursor.x;
scrollSpeed.x = -Math.min((distance / threshold) * maxSpeed, maxSpeed);
} else if (cursor.x > rect.right - threshold) {
// Scroll right
const distance = cursor.x - (rect.right - threshold);
scrollSpeed.x = Math.min((distance / threshold) * maxSpeed, maxSpeed);
}
// Vertical scrolling
if (cursor.y < rect.top + threshold) {
// Scroll up
const distance = rect.top + threshold - cursor.y;
scrollSpeed.y = -Math.min((distance / threshold) * maxSpeed, maxSpeed);
} else if (cursor.y > rect.bottom - threshold) {
// Scroll down
const distance = cursor.y - (rect.bottom - threshold);
scrollSpeed.y = Math.min((distance / threshold) * maxSpeed, maxSpeed);
}
return scrollSpeed;
}
// Find the nearest valid drop target
function findNearestDropTarget(cursor, dropTargets, maxDistance = 100) {
let nearest = null;
let minDistance = maxDistance;
dropTargets.forEach(target => {
const rect = target.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distance = Math.sqrt(
Math.pow(cursor.x - centerX, 2) +
Math.pow(cursor.y - centerY, 2)
);
if (distance < minDistance) {
minDistance = distance;
nearest = target;
}
});
return { target: nearest, distance: minDistance };
}
// Handle edge cases for drop position
function handleDropEdgeCases(dropPosition, container, item) {
const adjustedPosition = { ...dropPosition };
// Don't allow dropping item on itself
if (item && container.contains(item)) {
const itemIndex = Array.from(container.children).indexOf(item);
if (adjustedPosition.index > itemIndex) {
adjustedPosition.index--;
}
}
// Ensure index is within bounds
adjustedPosition.index = Math.max(0, Math.min(adjustedPosition.index, container.children.length));
return adjustedPosition;
}
// Main function for CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === '--help') {
console.log(`
Calculate Drop Position Utility
Usage:
node calculate_drop_position.js [command] [options]
Commands:
position <x> <y> <orientation> Calculate drop position for cursor at (x,y)
validate <item-type> <max-items> Validate if drop is allowed
autoscroll <x> <y> <threshold> Calculate auto-scroll speed
Examples:
node calculate_drop_position.js position 100 200 vertical
node calculate_drop_position.js validate card 10
node calculate_drop_position.js autoscroll 50 400 30
Options:
--help Show this help message
`);
process.exit(0);
}
// Example output for demonstration
const command = args[0];
switch (command) {
case 'position':
console.log(JSON.stringify({
index: 3,
closestElement: 'element-3',
distance: 15.5,
}, null, 2));
break;
case 'validate':
console.log(JSON.stringify({
valid: true,
reason: null,
}, null, 2));
break;
case 'autoscroll':
console.log(JSON.stringify({
x: 0,
y: 10,
}, null, 2));
break;
default:
console.error(`Unknown command: ${command}`);
process.exit(1);
}
}
// Export functions for use in other scripts
module.exports = {
calculateDropPosition,
calculateDropZone,
calculateGridPosition,
validateDrop,
calculateAutoScroll,
findNearestDropTarget,
handleDropEdgeCases,
};