Drag Spotfire controls and other html elements by adding the draggable class to a wrapper. For wrappers containing Spotfire filters, use draggablex for the filters to work properly.
html
<div id="filter" class="draggablex">
place a spotfire filter control here
<SpotfireControl id="d064ba85fb9a4399b5c4c604b2151a4a" />
</div>
js
setTimeout(()=>{
// Add styles only if they don't already exist
document.getElementById('draggable-styles')?.remove();
const style = document.createElement('style');
style.id = 'draggable-styles';
style.textContent = `
.drag-wrapper {
position: absolute;
border: 2px dashed transparent;
padding: 10px;
margin: 5px;
border-radius: 4px;
cursor: move;
XXXtransition: border-color 0.2s ease;
}
/* Prevent text selection during dragging but allow Spotfire controls to work */
.drag-wrapper.dragging {
opacity: 0.8;
z-index: 1000;
user-select: none;
}
.drag-wrapper:hover {
border-color: #007acc;
background-color: rgba(0, 122, 204, 0.1);
}
/* Hide dragging visual indicators when locked */
.drag-wrapper.locked {
border: 2px dashed transparent !important;
cursor: default !important;
}
.drag-wrapper.locked:hover {
border-color: transparent !important;
background-color: transparent !important;
}
/* Hide hover effects when showHoveringLayer is false */
.drag-wrapper.hide-hover:hover {
border-color: transparent !important;
background-color: transparent !important;
}
.config-titlebar{
background: #007acc;
color: white;
}
.config-titlebar,.config-titlebar>div {
padding: 1px 2px;
font-family: monospace;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
}
.config-arrow {
background: rgba(255,255,255,0.2);
padding: 2px 6px;
border-radius: 3px;
cursor: pointer !important;
transition: background-color 0.2s ease;
}
.config-arrow:hover {
background: rgba(255,255,255,0.4);
}
#draggable-config {
border: 2px solid #007acc;
border-radius: 5px;
overflow: hidden;
padding: 0;
}
`;
document.head.appendChild(style);
// the target container for the draggable elements
const targetContainer = document.querySelector(".sf-element.sf-element-visualization-area")?.firstElementChild;
if (!targetContainer) {
console.log('Target container not found');
return;
}
// Read configuration from the pre element
let config = { debug: { showConsoleLog: true, showHoveringLayer: true, showConfig: true, lockDragging: false }, elements: [] };
const configElement = document.getElementById('draggable-config');
let configTextarea = null;
// Helper function for conditional console logging
const debugLog = (...args) => {
if (config.debug?.showConsoleLog) {
console.log(...args);
}
};
// Convert pre to textarea at runtime for better editing
if (configElement) {
const preContent = configElement.textContent.trim();
// Create titlebar
const titlebar = document.createElement('div');
titlebar.className = 'config-titlebar';
titlebar.innerHTML = `
<div>draggable-config</div>
<div class="config-arrow">🗕</div>
`;
// Create textarea to replace pre content
configTextarea = document.createElement('textarea');
configTextarea.style.width = '90%';
configTextarea.style.height = '200px';
configTextarea.style.fontFamily = 'monospace';
configTextarea.style.fontSize = '12px';
configTextarea.style.border = '1px solid #ccc';
configTextarea.style.padding = '5px';
configTextarea.style.background = 'unset';
configTextarea.value = preContent;
// Replace pre content with titlebar and textarea
configElement.innerHTML = '';
configElement.appendChild(titlebar);
configElement.appendChild(configTextarea);
// Add collapse/expand functionality
const arrowElement = titlebar.querySelector('.config-arrow');
let isCollapsed = false;
arrowElement.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent dragging when clicking arrow
if (isCollapsed) {
// Expand
configTextarea.style.display = 'block';
arrowElement.textContent = '🗕';
isCollapsed = false;
} else {
// Collapse
configTextarea.style.display = 'none';
arrowElement.textContent = '🗖';
isCollapsed = true;
}
});
try {
config = JSON.parse(preContent);
// Ensure debug properties exist with defaults
config.debug = config.debug || {};
config.debug.showConsoleLog = config.debug.showConsoleLog !== undefined ? config.debug.showConsoleLog : true;
config.debug.showHoveringLayer = config.debug.showHoveringLayer !== undefined ? config.debug.showHoveringLayer : true;
config.debug.showConfig = config.debug.showConfig !== undefined ? config.debug.showConfig : true;
config.debug.lockDragging = config.debug.lockDragging !== undefined ? config.debug.lockDragging : false;
config.elements = config.elements || [];
} catch (e) {
console.warn('Invalid JSON in draggable-config, using defaults');
config = { debug: { showConsoleLog: true, showHoveringLayer: true, showConfig: true, lockDragging: false }, elements: [] };
}
// Hide config if showConfig is false
if (!config.debug.showConfig) {
configElement.style.display = 'none';
}
} else {
config = { debug: { showConsoleLog: true, showHoveringLayer: true, showConfig: true, lockDragging: false }, elements: [] };
}
// Function to save current positions back to config
function savePositions() {
const elements = [];
document.querySelectorAll('.drag-wrapper').forEach(wrapper => {
const element = wrapper.querySelector('.draggable, .draggablex');
if (element && element.id) {
elements.push({
id: element.id,
x: parseInt(wrapper.style.left) || 0,
y: parseInt(wrapper.style.top) || 0
});
}
});
config.elements = elements;
// Update the textarea with new configuration
if (configTextarea) {
configTextarea.value = JSON.stringify(config, null, 2);
}
}
// CLEANUP: Remove any existing drag wrappers before creating new ones
const existingWrappers = targetContainer.querySelectorAll('.drag-wrapper');
existingWrappers.forEach(wrapper => wrapper.remove());
// Also restore any draggable/draggablex elements that might be inside wrappers
document.querySelectorAll('.draggable, .draggablex').forEach(element => {
// If element is inside a wrapper, move it back to document body temporarily
if (element.closest('.drag-wrapper')) {
document.body.append(element);
}
});
// Function to update config titlebar with dragged element info
function updateConfigTitlebar(elementId, x, y) {
const titlebar = document.querySelector('.config-titlebar div:first-child');
if (titlebar) {
titlebar.textContent = `${elementId} (${x},${y})`;
}
}
// Function to reset config titlebar to default
function resetConfigTitlebar() {
const titlebar = document.querySelector('.config-titlebar div:first-child');
if (titlebar) {
titlebar.textContent = 'draggable-config';
}
}
// Function to make element draggable
function makeDraggable(wrapper) {
// If dragging is locked, don't add dragging functionality
if (config.debug.lockDragging) {
wrapper.classList.add('locked');
return;
}
let isDragging = false;
let startX, startY, startLeft, startTop;
wrapper.addEventListener('mousedown', (e) => {
// Only allow dragging if clicking directly on the wrapper itself, not its children
if (e.target !== wrapper) {
return;
}
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(wrapper.style.left) || 0;
startTop = parseInt(wrapper.style.top) || 0;
wrapper.classList.add('dragging');
// Update titlebar with element being dragged
const draggableElement = wrapper.querySelector('.draggable, .draggablex');
if (draggableElement && draggableElement.id) {
updateConfigTitlebar(draggableElement.id, startLeft, startTop);
}
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const newLeft = startLeft + deltaX;
const newTop = startTop + deltaY;
wrapper.style.left = newLeft + 'px';
wrapper.style.top = newTop + 'px';
// Update titlebar with current position during dragging
const draggableElement = wrapper.querySelector('.draggable, .draggablex');
if (draggableElement && draggableElement.id) {
updateConfigTitlebar(draggableElement.id, newLeft, newTop);
}
// Update positions live during dragging
savePositions();
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
wrapper.classList.remove('dragging');
// Reset titlebar to default
resetConfigTitlebar();
// Save positions after dragging ends
savePositions();
}
});
}
// Process each draggable and draggablex element
document.querySelectorAll('.draggable, .draggablex').forEach((element, index) => {
debugLog(`Processing element ${index}:`, element.id, element);
// Check if this is a simple draggable (no cloning) or draggablex (with cloning)
const isSimpleDraggable = element.classList.contains('draggable');
const isCloneDraggable = element.classList.contains('draggablex');
// Exception for config element - it works fine as is without cloning
if (element.id === 'draggable-config') {
// Skip if config is hidden
if (!config.debug.showConfig) {
return;
}
// Create wrapper div for the config element (no cloning needed)
const wrapper = document.createElement('div');
wrapper.className = 'drag-wrapper';
// Apply hover visibility setting
if (!config.debug.showHoveringLayer) {
wrapper.classList.add('hide-hover');
}
// Apply lock styling if dragging is disabled
if (config.debug.lockDragging) {
wrapper.classList.add('locked');
}
// Position the wrapper
const savedElement = config.elements?.find(el => el.id === element.id);
if (savedElement) {
wrapper.style.left = savedElement.x + 'px';
wrapper.style.top = savedElement.y + 'px';
} else {
wrapper.style.left = (20 + index * 20) + 'px';
wrapper.style.top = (20 + index * 20) + 'px';
}
// Move the config element directly into the wrapper
wrapper.appendChild(element);
targetContainer.appendChild(wrapper);
makeDraggable(wrapper);
return; // Skip the rest of the processing for config
}
// Handle simple draggable elements (no cloning)
if (isSimpleDraggable && !isCloneDraggable) {
debugLog(`Processing simple draggable element: ${element.id}`);
// Create wrapper div for simple dragging
const wrapper = document.createElement('div');
wrapper.className = 'drag-wrapper';
// Apply hover visibility setting
if (!config.debug.showHoveringLayer) {
wrapper.classList.add('hide-hover');
}
// Apply lock styling if dragging is disabled
if (config.debug.lockDragging) {
wrapper.classList.add('locked');
}
// Position the wrapper
const savedElement = config.elements?.find(el => el.id === element.id);
if (savedElement) {
wrapper.style.left = savedElement.x + 'px';
wrapper.style.top = savedElement.y + 'px';
} else {
wrapper.style.left = (20 + index * 20) + 'px';
wrapper.style.top = (20 + index * 20) + 'px';
}
// Move the element directly into the wrapper (no cloning)
wrapper.appendChild(element);
targetContainer.appendChild(wrapper);
makeDraggable(wrapper);
return; // Skip clone processing for simple draggable
}
// Handle draggablex elements (with clone/original swapping)
if (isCloneDraggable) {
debugLog(`Processing clone draggable element: ${element.id}`);
} else {
return; // Skip if neither draggable nor draggablex
}
// Set initial state for original elements - fixed position but hidden off-screen
element.style.position = 'fixed';
element.style.left = '-9999px';
element.style.top = '-9999px';
element.style.zIndex = '-1';
// Step 1: Create a deep clone - now that controls are fully rendered, this should work perfectly
const clonedElement = element.cloneNode(true);
// Clear any positioning styles from the clone so it displays normally
clonedElement.style.position = '';
clonedElement.style.left = '';
clonedElement.style.top = '';
clonedElement.style.zIndex = '';
debugLog(`Successfully cloned ${element.id}:`, clonedElement);
// Additional check for SpotfireControl elements
if (element.id === 'myControl') {
debugLog('Original myControl children:', element.children.length);
debugLog('Cloned myControl children:', clonedElement.children.length);
debugLog('Original first child:', element.children[0]);
debugLog('Cloned first child:', clonedElement.children[0]);
}
// Step 2: Create wrapper div for the clone
const wrapper = document.createElement('div');
wrapper.className = 'drag-wrapper';
// Apply hover visibility setting
if (!config.debug.showHoveringLayer) {
wrapper.classList.add('hide-hover');
}
// Apply lock styling if dragging is disabled
if (config.debug.lockDragging) {
wrapper.classList.add('locked');
}
// Step 3: Position the wrapper
// Check if we have saved position for this element
const savedElement = config.elements?.find(el => el.id === element.id);
if (savedElement) {
// Use saved position
wrapper.style.left = savedElement.x + 'px';
wrapper.style.top = savedElement.y + 'px';
} else {
// Use default position
wrapper.style.left = (20 + index * 20) + 'px';
wrapper.style.top = (20 + index * 20) + 'px';
}
// Step 4: Add the cloned element to the wrapper
wrapper.appendChild(clonedElement);
// Step 5: Add hover functionality to show original at clone position and delete clone
clonedElement.addEventListener('mouseenter', function() {
// Get the clone's position in the viewport
const cloneRect = clonedElement.getBoundingClientRect();
// Position the original element at the clone's location
element.style.position = 'fixed';
element.style.left = cloneRect.left + 'px';
element.style.top = cloneRect.top + 'px';
element.style.zIndex = '9999';
debugLog(`Showing original ${element.id} at position:`, cloneRect.left, cloneRect.top);
// Completely remove the clone and all its children
if (clonedElement.parentNode) {
clonedElement.parentNode.removeChild(clonedElement);
}
// Force cleanup of any remaining references
clonedElement.innerHTML = '';
});
// Step 6: Add hover out functionality to hide the original and create new clone
element.addEventListener('mouseleave', function() {
// Keep the original element fixed but hide it behind everything
element.style.zIndex = '-1';
// Create a fresh clone from the current original state
const newClone = element.cloneNode(true);
// Clear any positioning styles from the new clone so it displays normally
newClone.style.position = '';
newClone.style.left = '';
newClone.style.top = '';
newClone.style.zIndex = '';
// Add the fresh clone to the wrapper
wrapper.appendChild(newClone);
// Re-attach the hover event to the new clone
newClone.addEventListener('mouseenter', function() {
// Get the new clone's position in the viewport
const cloneRect = newClone.getBoundingClientRect();
// Position the original element at the clone's location
element.style.position = 'fixed';
element.style.left = cloneRect.left + 'px';
element.style.top = cloneRect.top + 'px';
element.style.zIndex = '9999';
debugLog(`Showing original ${element.id} at position:`, cloneRect.left, cloneRect.top);
// Completely remove the clone and all its children
if (newClone.parentNode) {
newClone.parentNode.removeChild(newClone);
}
// Force cleanup of any remaining references
newClone.innerHTML = '';
});
debugLog(`Hidden original ${element.id} and created new clone`);
});
// Step 7: Add wrapper to target container
targetContainer.appendChild(wrapper);
// Step 8: Make wrapper draggable
makeDraggable(wrapper);
});
},1000);
No comments:
Post a Comment