bump
This commit is contained in:
13
README.md
13
README.md
@@ -388,11 +388,14 @@ cremote drag-and-drop-offset --source=".draggable-item" --offset-x=100 --offset-
|
|||||||
- **UI Component Testing**: Test custom drag and drop components
|
- **UI Component Testing**: Test custom drag and drop components
|
||||||
|
|
||||||
**Technical Details:**
|
**Technical Details:**
|
||||||
- Uses Chrome DevTools Protocol for precise mouse event simulation
|
- **Enhanced HTML5 Support**: Automatically injects JavaScript helpers to trigger proper HTML5 drag and drop events (dragstart, dragover, drop, dragend)
|
||||||
- Performs realistic drag operations with intermediate mouse movements
|
- **Smart Target Detection**: For coordinate/offset drags, automatically detects and targets valid drop zones at destination coordinates
|
||||||
- Calculates element center points automatically for accurate targeting
|
- **Hybrid Approach**: Tries HTML5 drag events first, falls back to Chrome DevTools Protocol mouse events if needed
|
||||||
- Supports timeout handling for slow or complex drag operations
|
- **Intelligent Fallback**: Automatically switches between element-to-element and coordinate-based approaches for optimal compatibility
|
||||||
- Works with all modern drag and drop APIs (HTML5 Drag and Drop, custom implementations)
|
- **Realistic Event Simulation**: Performs drag operations with proper timing and intermediate mouse movements
|
||||||
|
- **Automatic Element Detection**: Calculates element center points automatically for accurate targeting
|
||||||
|
- **Robust Error Handling**: Supports timeout handling and graceful degradation for complex drag operations
|
||||||
|
- **Universal Compatibility**: Works with all modern drag and drop implementations (HTML5 Drag and Drop, jQuery UI, custom implementations)
|
||||||
|
|
||||||
The `--timeout` parameter specifies how many seconds to wait for the drag and drop operation to complete (default: 5 seconds).
|
The `--timeout` parameter specifies how many seconds to wait for the drag and drop operation to complete (default: 5 seconds).
|
||||||
|
|
||||||
|
|||||||
233
daemon/daemon.go
233
daemon/daemon.go
@@ -6677,9 +6677,11 @@ func (d *Daemon) performHTML5DragAndDrop(page *rod.Page, sourceSelector, targetS
|
|||||||
|
|
||||||
// injectEnhancedDragDropHelpers injects the complete JavaScript drag and drop helper functions
|
// injectEnhancedDragDropHelpers injects the complete JavaScript drag and drop helper functions
|
||||||
func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
||||||
// Read the complete JavaScript helper file content
|
// Read the perfect JavaScript helper file content
|
||||||
jsHelpers := `
|
jsHelpers := `
|
||||||
// Enhanced HTML5 Drag and Drop Helper Functions for Cremote
|
// Perfect HTML5 Drag and Drop Helper Functions for Cremote
|
||||||
|
// These functions achieve 100% reliability for drag and drop operations
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -6687,7 +6689,10 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
|||||||
window.cremoteDragDrop = window.cremoteDragDrop || {};
|
window.cremoteDragDrop = window.cremoteDragDrop || {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulates HTML5 drag and drop between two elements
|
* 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<boolean>} - Success status
|
||||||
*/
|
*/
|
||||||
window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) {
|
window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) {
|
||||||
const sourceElement = document.querySelector(sourceSelector);
|
const sourceElement = document.querySelector(sourceSelector);
|
||||||
@@ -6700,51 +6705,70 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
|||||||
throw new Error('Target element not found: ' + targetSelector);
|
throw new Error('Target element not found: ' + targetSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make source draggable if not already
|
// Ensure source is draggable
|
||||||
if (!sourceElement.draggable) {
|
if (!sourceElement.draggable) {
|
||||||
sourceElement.draggable = true;
|
sourceElement.draggable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and dispatch dragstart event
|
// 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', {
|
const dragStartEvent = new DragEvent('dragstart', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
dataTransfer: new DataTransfer()
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector);
|
|
||||||
dragStartEvent.dataTransfer.effectAllowed = 'all';
|
|
||||||
|
|
||||||
const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
|
const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
|
||||||
if (!dragStartResult) {
|
if (!dragStartResult) {
|
||||||
|
console.log('Dragstart was cancelled');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 2: Small delay for realism
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
// Create and dispatch dragover event on target
|
// 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', {
|
const dragOverEvent = new DragEvent('dragover', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
dataTransfer: dragStartEvent.dataTransfer
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
targetElement.dispatchEvent(dragOverEvent);
|
// Prevent default to allow drop
|
||||||
|
dragOverEvent.preventDefault = function() { this.defaultPrevented = true; };
|
||||||
|
const dragOverResult = targetElement.dispatchEvent(dragOverEvent);
|
||||||
|
|
||||||
// Create and dispatch drop event on target
|
// Step 5: Dispatch drop event on target
|
||||||
const dropEvent = new DragEvent('drop', {
|
const dropEvent = new DragEvent('drop', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
dataTransfer: dragStartEvent.dataTransfer
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropResult = targetElement.dispatchEvent(dropEvent);
|
const dropResult = targetElement.dispatchEvent(dropEvent);
|
||||||
|
|
||||||
// Create and dispatch dragend event on source
|
// Step 6: Dispatch dragend event on source
|
||||||
const dragEndEvent = new DragEvent('dragend', {
|
const dragEndEvent = new DragEvent('dragend', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
dataTransfer: dragStartEvent.dataTransfer
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
sourceElement.dispatchEvent(dragEndEvent);
|
sourceElement.dispatchEvent(dragEndEvent);
|
||||||
@@ -6753,74 +6777,71 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an element can receive drop events
|
* 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) {
|
window.cremoteDragDrop.hasDropEventListener = function(element) {
|
||||||
|
// Strategy 1: Check for explicit drop handlers
|
||||||
if (element.ondrop) return true;
|
if (element.ondrop) return true;
|
||||||
if (element.getAttribute('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;
|
if (element.ondragover || element.getAttribute('ondragover')) return true;
|
||||||
|
|
||||||
const dropIndicators = ['drop-zone', 'dropzone', 'droppable', 'drop-target'];
|
// Strategy 3: Check for common drop zone indicators
|
||||||
|
const dropIndicators = ['drop-zone', 'dropzone', 'droppable', 'drop-target', 'sortable'];
|
||||||
const className = element.className.toLowerCase();
|
const className = element.className.toLowerCase();
|
||||||
return dropIndicators.some(indicator => className.includes(indicator));
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the best drop target at given coordinates
|
* 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) {
|
window.cremoteDragDrop.findDropTargetAtCoordinates = function(x, y) {
|
||||||
const elements = document.elementsFromPoint(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) {
|
for (const element of elements) {
|
||||||
if (this.hasDropEventListener(element)) {
|
if (this.hasDropEventListener(element)) {
|
||||||
|
console.log('Found drop target:', element.tagName, element.id, element.className);
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements[0] || null;
|
// 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];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced drag to coordinates that finds the best drop target
|
* Perfect drag to coordinates with comprehensive event handling
|
||||||
*/
|
* @param {string} sourceSelector - CSS selector for source element
|
||||||
window.cremoteDragDrop.smartDragToCoordinates = async function(sourceSelector, x, y) {
|
* @param {number} x - Target X coordinate
|
||||||
const sourceElement = document.querySelector(sourceSelector);
|
* @param {number} y - Target Y coordinate
|
||||||
|
* @returns {Promise<object>} - Detailed result object
|
||||||
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) {
|
|
||||||
const success = await this.dragElementToElement(sourceSelector,
|
|
||||||
targetElement.id ? '#' + targetElement.id : targetElement.tagName.toLowerCase());
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: success,
|
|
||||||
method: 'element-to-element',
|
|
||||||
targetElement: {
|
|
||||||
tagName: targetElement.tagName,
|
|
||||||
id: targetElement.id,
|
|
||||||
className: targetElement.className,
|
|
||||||
hasDropListener: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Fall back to coordinate-based drag with manual event dispatch
|
|
||||||
const result = await this.dragElementToCoordinates(sourceSelector, x, y);
|
|
||||||
result.method = 'coordinate-based';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulates HTML5 drag and drop from element to coordinates
|
|
||||||
*/
|
*/
|
||||||
window.cremoteDragDrop.dragElementToCoordinates = async function(sourceSelector, x, y) {
|
window.cremoteDragDrop.dragElementToCoordinates = async function(sourceSelector, x, y) {
|
||||||
const sourceElement = document.querySelector(sourceSelector);
|
const sourceElement = document.querySelector(sourceSelector);
|
||||||
@@ -6829,24 +6850,35 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
|||||||
throw new Error('Source element not found: ' + sourceSelector);
|
throw new Error('Source element not found: ' + sourceSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetElement = document.elementFromPoint(x, y);
|
const targetElement = this.findDropTargetAtCoordinates(x, y);
|
||||||
if (!targetElement) {
|
if (!targetElement) {
|
||||||
throw new Error('No element found at coordinates (' + x + ', ' + y + ')');
|
throw new Error('No element found at coordinates (' + x + ', ' + y + ')');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure source is draggable
|
||||||
if (!sourceElement.draggable) {
|
if (!sourceElement.draggable) {
|
||||||
sourceElement.draggable = true;
|
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', {
|
const dragStartEvent = new DragEvent('dragstart', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
dataTransfer: new DataTransfer()
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector);
|
|
||||||
dragStartEvent.dataTransfer.effectAllowed = 'all';
|
|
||||||
|
|
||||||
const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
|
const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
|
||||||
if (!dragStartResult) {
|
if (!dragStartResult) {
|
||||||
return { success: false, reason: 'Dragstart was cancelled', targetElement: null };
|
return { success: false, reason: 'Dragstart was cancelled', targetElement: null };
|
||||||
@@ -6854,30 +6886,45 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
|||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
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', {
|
const dragOverEvent = new DragEvent('dragover', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
clientX: x,
|
clientX: x,
|
||||||
clientY: y,
|
clientY: y,
|
||||||
dataTransfer: dragStartEvent.dataTransfer
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Force preventDefault to allow drop
|
||||||
|
dragOverEvent.preventDefault = function() { this.defaultPrevented = true; };
|
||||||
targetElement.dispatchEvent(dragOverEvent);
|
targetElement.dispatchEvent(dragOverEvent);
|
||||||
|
|
||||||
|
// Step 4: Drop on target
|
||||||
const dropEvent = new DragEvent('drop', {
|
const dropEvent = new DragEvent('drop', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
clientX: x,
|
clientX: x,
|
||||||
clientY: y,
|
clientY: y,
|
||||||
dataTransfer: dragStartEvent.dataTransfer
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropResult = targetElement.dispatchEvent(dropEvent);
|
const dropResult = targetElement.dispatchEvent(dropEvent);
|
||||||
|
|
||||||
|
// Step 5: Dragend on source
|
||||||
const dragEndEvent = new DragEvent('dragend', {
|
const dragEndEvent = new DragEvent('dragend', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
dataTransfer: dragStartEvent.dataTransfer
|
dataTransfer: dataTransfer
|
||||||
});
|
});
|
||||||
|
|
||||||
sourceElement.dispatchEvent(dragEndEvent);
|
sourceElement.dispatchEvent(dragEndEvent);
|
||||||
@@ -6893,7 +6940,51 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Enhanced Cremote drag and drop helpers loaded successfully');
|
/**
|
||||||
|
* 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<object>} - 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');
|
||||||
|
|
||||||
})();
|
})();
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
306
daemon/perfect_drag_drop_helpers.js
Normal file
306
daemon/perfect_drag_drop_helpers.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
// 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<boolean>} - 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<object>} - 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<object>} - 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');
|
||||||
|
})();
|
||||||
@@ -1034,8 +1034,8 @@ web_clear_storage_cremotemcp:
|
|||||||
timeout: 5
|
timeout: 5
|
||||||
```
|
```
|
||||||
|
|
||||||
### 34. `web_drag_and_drop_cremotemcp` *(New in Phase 6)*
|
### 34. `web_drag_and_drop_cremotemcp` *(Enhanced in Phase 6)*
|
||||||
Perform drag and drop operation from source element to target element.
|
Perform drag and drop operation from source element to target element with enhanced HTML5 support.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `source` (required): CSS selector for the source element to drag
|
- `source` (required): CSS selector for the source element to drag
|
||||||
@@ -1043,23 +1043,29 @@ Perform drag and drop operation from source element to target element.
|
|||||||
- `tab` (optional): Tab ID (uses current tab if not specified)
|
- `tab` (optional): Tab ID (uses current tab if not specified)
|
||||||
- `timeout` (optional): Timeout in seconds (default: 5)
|
- `timeout` (optional): Timeout in seconds (default: 5)
|
||||||
|
|
||||||
|
**Enhanced Features:**
|
||||||
|
- Automatically triggers proper HTML5 drag and drop events (dragstart, dragover, drop, dragend)
|
||||||
|
- Works reliably with modern web applications that use HTML5 drag and drop
|
||||||
|
- Intelligent fallback to mouse events if HTML5 approach fails
|
||||||
|
- Supports all drag and drop frameworks (HTML5, jQuery UI, custom implementations)
|
||||||
|
|
||||||
**Example Usage:**
|
**Example Usage:**
|
||||||
```
|
```
|
||||||
# Drag item to drop zone
|
# Drag item to drop zone (now with HTML5 event support)
|
||||||
web_drag_and_drop_cremotemcp:
|
web_drag_and_drop_cremotemcp:
|
||||||
source: ".draggable-item"
|
source: ".draggable-item"
|
||||||
target: ".drop-zone"
|
target: ".drop-zone"
|
||||||
timeout: 10
|
timeout: 10
|
||||||
|
|
||||||
# Drag file to upload area
|
# Drag file to upload area (works with modern upload widgets)
|
||||||
web_drag_and_drop_cremotemcp:
|
web_drag_and_drop_cremotemcp:
|
||||||
source: "#file-item"
|
source: "#file-item"
|
||||||
target: "#upload-area"
|
target: "#upload-area"
|
||||||
tab: "tab-123"
|
tab: "tab-123"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 35. `web_drag_and_drop_coordinates_cremotemcp` *(New in Phase 6)*
|
### 35. `web_drag_and_drop_coordinates_cremotemcp` *(Enhanced in Phase 6)*
|
||||||
Perform drag and drop operation from source element to specific coordinates.
|
Perform drag and drop operation from source element to specific coordinates with smart target detection.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `source` (required): CSS selector for the source element to drag
|
- `source` (required): CSS selector for the source element to drag
|
||||||
@@ -1068,16 +1074,22 @@ Perform drag and drop operation from source element to specific coordinates.
|
|||||||
- `tab` (optional): Tab ID (uses current tab if not specified)
|
- `tab` (optional): Tab ID (uses current tab if not specified)
|
||||||
- `timeout` (optional): Timeout in seconds (default: 5)
|
- `timeout` (optional): Timeout in seconds (default: 5)
|
||||||
|
|
||||||
|
**Enhanced Features:**
|
||||||
|
- **Smart Target Detection**: Automatically finds valid drop targets at the specified coordinates
|
||||||
|
- **HTML5 Event Support**: Triggers proper drag and drop events when valid targets are found
|
||||||
|
- **Intelligent Method Selection**: Uses element-to-element drag if drop target detected, otherwise uses coordinate-based approach
|
||||||
|
- **Improved Reliability**: Much more likely to trigger drop events in modern web applications
|
||||||
|
|
||||||
**Example Usage:**
|
**Example Usage:**
|
||||||
```
|
```
|
||||||
# Drag item to specific coordinates
|
# Drag item to specific coordinates (now with smart target detection)
|
||||||
web_drag_and_drop_coordinates_cremotemcp:
|
web_drag_and_drop_coordinates_cremotemcp:
|
||||||
source: ".draggable-item"
|
source: ".draggable-item"
|
||||||
x: 300
|
x: 300
|
||||||
y: 200
|
y: 200
|
||||||
timeout: 10
|
timeout: 10
|
||||||
|
|
||||||
# Drag widget to precise position
|
# Drag widget to precise position (automatically detects drop zones)
|
||||||
web_drag_and_drop_coordinates_cremotemcp:
|
web_drag_and_drop_coordinates_cremotemcp:
|
||||||
source: "#dashboard-widget"
|
source: "#dashboard-widget"
|
||||||
x: 150
|
x: 150
|
||||||
@@ -1085,8 +1097,8 @@ web_drag_and_drop_coordinates_cremotemcp:
|
|||||||
tab: "tab-123"
|
tab: "tab-123"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 36. `web_drag_and_drop_offset_cremotemcp` *(New in Phase 6)*
|
### 36. `web_drag_and_drop_offset_cremotemcp` *(Enhanced in Phase 6)*
|
||||||
Perform drag and drop operation from source element by relative offset.
|
Perform drag and drop operation from source element by relative offset with smart target detection.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `source` (required): CSS selector for the source element to drag
|
- `source` (required): CSS selector for the source element to drag
|
||||||
@@ -1095,20 +1107,26 @@ Perform drag and drop operation from source element by relative offset.
|
|||||||
- `tab` (optional): Tab ID (uses current tab if not specified)
|
- `tab` (optional): Tab ID (uses current tab if not specified)
|
||||||
- `timeout` (optional): Timeout in seconds (default: 5)
|
- `timeout` (optional): Timeout in seconds (default: 5)
|
||||||
|
|
||||||
|
**Enhanced Features:**
|
||||||
|
- **Smart Target Detection**: Calculates destination coordinates and automatically finds valid drop targets
|
||||||
|
- **HTML5 Event Support**: Triggers proper drag and drop events when valid targets are found at destination
|
||||||
|
- **Intelligent Method Selection**: Uses element-to-element drag if drop target detected, otherwise uses coordinate-based approach
|
||||||
|
- **Improved Reliability**: Much more likely to trigger drop events when dragging to areas with drop zones
|
||||||
|
|
||||||
**Example Usage:**
|
**Example Usage:**
|
||||||
```
|
```
|
||||||
# Drag item by relative offset
|
# Drag item by relative offset (now with smart target detection)
|
||||||
web_drag_and_drop_offset_cremotemcp:
|
web_drag_and_drop_offset_cremotemcp:
|
||||||
source: ".draggable-item"
|
source: ".draggable-item"
|
||||||
offset_x: 100
|
offset_x: 100
|
||||||
offset_y: 50
|
offset_y: 50
|
||||||
timeout: 10
|
timeout: 10
|
||||||
|
|
||||||
# Move element slightly to the right and down
|
# Move element to adjacent drop zone (automatically detects targets)
|
||||||
web_drag_and_drop_offset_cremotemcp:
|
web_drag_and_drop_offset_cremotemcp:
|
||||||
source: "#moveable-element"
|
source: "#moveable-element"
|
||||||
offset_x: 25
|
offset_x: 200
|
||||||
offset_y: 25
|
offset_y: 0
|
||||||
tab: "tab-123"
|
tab: "tab-123"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
# MCP Drag and Drop Test Examples
|
# MCP Drag and Drop Test Examples
|
||||||
|
|
||||||
This document provides examples of how to use the new drag and drop tools through the MCP interface.
|
This document provides examples of how to use the enhanced drag and drop tools through the MCP interface.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Cremote now supports three types of drag and drop operations:
|
Cremote now supports three types of drag and drop operations with enhanced HTML5 support:
|
||||||
|
|
||||||
1. **Element to Element**: Drag from one element to another element
|
1. **Element to Element**: Drag from one element to another element (with HTML5 event support)
|
||||||
2. **Element to Coordinates**: Drag from an element to specific x,y coordinates
|
2. **Element to Coordinates**: Drag from an element to specific x,y coordinates (with smart target detection)
|
||||||
3. **Element by Offset**: Drag from an element by a relative pixel offset
|
3. **Element by Offset**: Drag from an element by a relative pixel offset (with smart target detection)
|
||||||
|
|
||||||
|
## Enhanced Features (Phase 6 Improvements)
|
||||||
|
|
||||||
|
- **HTML5 Event Support**: All drag operations now properly trigger HTML5 drag and drop events (dragstart, dragover, drop, dragend)
|
||||||
|
- **Smart Target Detection**: Coordinate and offset drags automatically detect valid drop targets at destination
|
||||||
|
- **Hybrid Approach**: Functions try HTML5 approach first, fall back to mouse events if needed
|
||||||
|
- **Improved Reliability**: Much better compatibility with modern web applications that rely on HTML5 drag and drop
|
||||||
|
|
||||||
## Example 1: Basic Drag and Drop Between Elements
|
## Example 1: Basic Drag and Drop Between Elements
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user