Customer Banners (Ads) - SpiceUp. AX and SpotfireX Disclaimer



If you find this site useful and you want to support, buy me a coffee   to keep this site alive and without ads.

Drag Spotfire and Controls and other elements

 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: