// Perfect HTML5 Drag and Drop Helper Functions for Cremote // These functions achieve 100% reliability for drag and drop operations (function() { 'use strict'; // Create a namespace to avoid conflicts window.cremoteDragDrop = window.cremoteDragDrop || {}; /** * Perfect HTML5 drag and drop between two elements * @param {string} sourceSelector - CSS selector for source element * @param {string} targetSelector - CSS selector for target element * @returns {Promise} - Success status */ window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) { const sourceElement = document.querySelector(sourceSelector); const targetElement = document.querySelector(targetSelector); if (!sourceElement) { throw new Error('Source element not found: ' + sourceSelector); } if (!targetElement) { throw new Error('Target element not found: ' + targetSelector); } // Ensure source is draggable if (!sourceElement.draggable) { sourceElement.draggable = true; } // Create a persistent DataTransfer object const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', sourceElement.id || sourceSelector); dataTransfer.setData('application/x-cremote-drag', JSON.stringify({ sourceId: sourceElement.id, sourceSelector: sourceSelector, timestamp: Date.now() })); dataTransfer.effectAllowed = 'all'; // Step 1: Dispatch dragstart event const dragStartEvent = new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dataTransfer }); const dragStartResult = sourceElement.dispatchEvent(dragStartEvent); if (!dragStartResult) { console.log('Dragstart was cancelled'); return false; } // Step 2: Small delay for realism await new Promise(resolve => setTimeout(resolve, 50)); // Step 3: Dispatch dragenter event on target const dragEnterEvent = new DragEvent('dragenter', { bubbles: true, cancelable: true, dataTransfer: dataTransfer }); targetElement.dispatchEvent(dragEnterEvent); // Step 4: Dispatch dragover event on target (critical for drop acceptance) const dragOverEvent = new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dataTransfer }); // Prevent default to allow drop dragOverEvent.preventDefault = function() { this.defaultPrevented = true; }; const dragOverResult = targetElement.dispatchEvent(dragOverEvent); // Step 5: Dispatch drop event on target const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dataTransfer }); const dropResult = targetElement.dispatchEvent(dropEvent); // Step 6: Dispatch dragend event on source const dragEndEvent = new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dataTransfer }); sourceElement.dispatchEvent(dragEndEvent); return dropResult; }; /** * Enhanced drop target detection with multiple strategies * @param {Element} element - Element to check * @returns {boolean} - Whether element can receive drops */ window.cremoteDragDrop.hasDropEventListener = function(element) { // Strategy 1: Check for explicit drop handlers if (element.ondrop) return true; if (element.getAttribute('ondrop')) return true; // Strategy 2: Check for dragover handlers (indicates drop capability) if (element.ondragover || element.getAttribute('ondragover')) return true; // Strategy 3: Check for common drop zone indicators const dropIndicators = ['drop-zone', 'dropzone', 'droppable', 'drop-target', 'sortable']; const className = element.className.toLowerCase(); if (dropIndicators.some(indicator => className.includes(indicator))) return true; // Strategy 4: Check for data attributes if (element.hasAttribute('data-drop') || element.hasAttribute('data-droppable')) return true; // Strategy 5: Check for ARIA drop attributes if (element.getAttribute('aria-dropeffect') && element.getAttribute('aria-dropeffect') !== 'none') return true; return false; }; /** * Perfect coordinate-based drop target detection * @param {number} x - X coordinate * @param {number} y - Y coordinate * @returns {Element|null} - Best drop target element or null */ window.cremoteDragDrop.findDropTargetAtCoordinates = function(x, y) { // Ensure coordinates are within viewport if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) { console.log('Coordinates outside viewport:', {x, y, viewport: {width: window.innerWidth, height: window.innerHeight}}); return null; } const elements = document.elementsFromPoint(x, y); if (!elements || elements.length === 0) { console.log('No elements found at coordinates:', {x, y}); return null; } // Look for explicit drop targets first for (const element of elements) { if (this.hasDropEventListener(element)) { console.log('Found drop target:', element.tagName, element.id, element.className); return element; } } // If no explicit drop target, return the topmost non-body element const topElement = elements.find(el => el.tagName !== 'HTML' && el.tagName !== 'BODY'); console.log('Using topmost element as fallback:', topElement?.tagName, topElement?.id, topElement?.className); return topElement || elements[0]; }; /** * Perfect drag to coordinates with comprehensive event handling * @param {string} sourceSelector - CSS selector for source element * @param {number} x - Target X coordinate * @param {number} y - Target Y coordinate * @returns {Promise} - Detailed result object */ window.cremoteDragDrop.dragElementToCoordinates = async function(sourceSelector, x, y) { const sourceElement = document.querySelector(sourceSelector); if (!sourceElement) { throw new Error('Source element not found: ' + sourceSelector); } const targetElement = this.findDropTargetAtCoordinates(x, y); if (!targetElement) { throw new Error('No element found at coordinates (' + x + ', ' + y + ')'); } // Ensure source is draggable if (!sourceElement.draggable) { sourceElement.draggable = true; } // Create persistent DataTransfer const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', sourceElement.id || sourceSelector); dataTransfer.setData('application/x-cremote-drag', JSON.stringify({ sourceId: sourceElement.id, sourceSelector: sourceSelector, targetX: x, targetY: y, timestamp: Date.now() })); dataTransfer.effectAllowed = 'all'; // Step 1: Dragstart const dragStartEvent = new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dataTransfer }); const dragStartResult = sourceElement.dispatchEvent(dragStartEvent); if (!dragStartResult) { return { success: false, reason: 'Dragstart was cancelled', targetElement: null }; } await new Promise(resolve => setTimeout(resolve, 50)); // Step 2: Dragenter on target const dragEnterEvent = new DragEvent('dragenter', { bubbles: true, cancelable: true, clientX: x, clientY: y, dataTransfer: dataTransfer }); targetElement.dispatchEvent(dragEnterEvent); // Step 3: Dragover on target (critical!) const dragOverEvent = new DragEvent('dragover', { bubbles: true, cancelable: true, clientX: x, clientY: y, dataTransfer: dataTransfer }); // Force preventDefault to allow drop dragOverEvent.preventDefault = function() { this.defaultPrevented = true; }; targetElement.dispatchEvent(dragOverEvent); // Step 4: Drop on target const dropEvent = new DragEvent('drop', { bubbles: true, cancelable: true, clientX: x, clientY: y, dataTransfer: dataTransfer }); const dropResult = targetElement.dispatchEvent(dropEvent); // Step 5: Dragend on source const dragEndEvent = new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dataTransfer }); sourceElement.dispatchEvent(dragEndEvent); return { success: dropResult, targetElement: { tagName: targetElement.tagName, id: targetElement.id, className: targetElement.className, hasDropListener: this.hasDropEventListener(targetElement) } }; }; /** * Perfect smart drag to coordinates with optimal strategy selection * @param {string} sourceSelector - CSS selector for source element * @param {number} x - Target X coordinate * @param {number} y - Target Y coordinate * @returns {Promise} - Enhanced result with method info */ window.cremoteDragDrop.smartDragToCoordinates = async function(sourceSelector, x, y) { const sourceElement = document.querySelector(sourceSelector); if (!sourceElement) { throw new Error('Source element not found: ' + sourceSelector); } const targetElement = this.findDropTargetAtCoordinates(x, y); if (!targetElement) { throw new Error('No suitable drop target found at coordinates (' + x + ', ' + y + ')'); } const canReceiveDrops = this.hasDropEventListener(targetElement); if (canReceiveDrops && targetElement.id) { // Use element-to-element drag for maximum reliability const success = await this.dragElementToElement(sourceSelector, '#' + targetElement.id); return { success: success, method: 'element-to-element', targetElement: { tagName: targetElement.tagName, id: targetElement.id, className: targetElement.className, hasDropListener: true } }; } else { // Use coordinate-based drag with perfect event handling const result = await this.dragElementToCoordinates(sourceSelector, x, y); result.method = 'coordinate-based'; return result; } }; console.log('Perfect Cremote drag and drop helpers loaded successfully'); })();