This commit is contained in:
Josh at WLTechBlog
2025-09-30 15:10:13 -05:00
parent 396718be59
commit cb4ec135ec
5 changed files with 520 additions and 95 deletions

View File

@@ -6677,9 +6677,11 @@ func (d *Daemon) performHTML5DragAndDrop(page *rod.Page, sourceSelector, targetS
// injectEnhancedDragDropHelpers injects the complete JavaScript drag and drop helper functions
func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
// Read the complete JavaScript helper file content
// Read the perfect JavaScript helper file content
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() {
'use strict';
@@ -6687,7 +6689,10 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
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) {
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);
}
// Make source draggable if not already
// Ensure source is draggable
if (!sourceElement.draggable) {
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', {
bubbles: 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);
if (!dragStartResult) {
console.log('Dragstart was cancelled');
return false;
}
// Step 2: Small delay for realism
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', {
bubbles: 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', {
bubbles: true,
cancelable: true,
dataTransfer: dragStartEvent.dataTransfer
dataTransfer: dataTransfer
});
const dropResult = targetElement.dispatchEvent(dropEvent);
// Create and dispatch dragend event on source
// Step 6: Dispatch dragend event on source
const dragEndEvent = new DragEvent('dragend', {
bubbles: true,
cancelable: true,
dataTransfer: dragStartEvent.dataTransfer
dataTransfer: dataTransfer
});
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) {
// 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;
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();
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) {
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) {
if (this.hasDropEventListener(element)) {
console.log('Found drop target:', element.tagName, element.id, element.className);
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
*/
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) {
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
* 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);
@@ -6829,24 +6850,35 @@ func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error {
throw new Error('Source element not found: ' + sourceSelector);
}
const targetElement = document.elementFromPoint(x, y);
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: new DataTransfer()
dataTransfer: dataTransfer
});
dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector);
dragStartEvent.dataTransfer.effectAllowed = 'all';
const dragStartResult = sourceElement.dispatchEvent(dragStartEvent);
if (!dragStartResult) {
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));
// 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: dragStartEvent.dataTransfer
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: dragStartEvent.dataTransfer
dataTransfer: dataTransfer
});
const dropResult = targetElement.dispatchEvent(dropEvent);
// Step 5: Dragend on source
const dragEndEvent = new DragEvent('dragend', {
bubbles: true,
cancelable: true,
dataTransfer: dragStartEvent.dataTransfer
dataTransfer: dataTransfer
});
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');
})();
`

View 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');
})();