snapdom test

ʕ•ᴥ•ʔ Capture page DOM snapshot using snapdom

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         snapdom test
// @namespace    http://tampermonkey.net/
// @version      2025-12-31
// @description  ʕ•ᴥ•ʔ Capture page DOM snapshot using snapdom
// @author       [email protected]
// @match        https://*.antgroup.com/*
// @match        https://*.pro.ant.design/*
// @match        https://*.shadcn.com/*
// @match        https://localhost:*/*
// @icon         https://snapdom.dev/assets/favicon/favicon.ico
// @grant        GM_registerMenuCommand
// @require      https://unpkg.com/@zumer/snapdom/dist/snapdom.js
// @license      AGPL-3.0
// ==/UserScript==

(function () {
  'use strict';

  const prefix = `snapdom-${Date.now().toString(36)}`;
  const toastId = `${prefix}-toast`;
  const toastStylesId = `${toastId}-styles`;
  const highlightBoxId = `${prefix}-highlight-box`;

  // Wait for snapdom library to load
  function waitForSnapdom() {
    return new Promise((resolve) => {
      if (window.snapdom) {
        resolve(window.snapdom);
        return;
      }
      const checkInterval = setInterval(() => {
        if (window.snapdom) {
          clearInterval(checkInterval);
          resolve(window.snapdom);
        }
      }, 100);
    });
  }

  // Create Toast notification (Sonner style)
  function showToast(message, type = 'info', duration = 3000) {
    // Remove existing toast
    const existingToast = document.getElementById(toastId);
    if (existingToast) {
      existingToast.remove();
    }

    // Create toast container
    const toast = document.createElement('div');
    toast.id = toastId;
    toast.dataset.capture = "exclude";

    // Set icon and color based on type (Sonner style)
    const config = {
      success: {
        icon: '✓',
        accentColor: '#10b981',
        iconBg: 'rgba(16, 185, 129, 0.1)',
        iconColor: '#10b981'
      },
      error: {
        icon: '✕',
        accentColor: '#ef4444',
        iconBg: 'rgba(239, 68, 68, 0.1)',
        iconColor: '#ef4444'
      },
      loading: {
        icon: '⟳',
        accentColor: '#3b82f6',
        iconBg: 'rgba(59, 130, 246, 0.1)',
        iconColor: '#3b82f6'
      },
      info: {
        icon: 'ℹ',
        accentColor: '#6366f1',
        iconBg: 'rgba(99, 102, 241, 0.1)',
        iconColor: '#6366f1'
      }
    };

    const style = config[type] || config.info;

    // Add animation styles (if not already added)
    if (!document.getElementById(toastStylesId)) {
      const styleSheet = document.createElement('style');
      styleSheet.dataset.capture = "exclude";
      styleSheet.id = toastStylesId;
      styleSheet.textContent = `
        @keyframes toast-slide-in {
          from {
            transform: translateX(-50%) translateY(-20px);
            opacity: 0;
          }
          to {
            transform: translateX(-50%) translateY(0);
            opacity: 1;
          }
        }
        @keyframes toast-slide-out {
          from {
            transform: translateX(-50%) translateY(0);
            opacity: 1;
          }
          to {
            transform: translateX(-50%) translateY(-20px);
            opacity: 0;
          }
        }
        @keyframes toast-spin {
          from {
            transform: rotate(0deg);
          }
          to {
            transform: rotate(360deg);
          }
        }
        #${toastId} {
          animation: toast-slide-in 0.35s cubic-bezier(0.21, 1.02, 0.73, 1) forwards;
        }
        #${toastId}.toast-exit {
          animation: toast-slide-out 0.2s cubic-bezier(0.06, 0.71, 0.55, 1) forwards;
        }
        #${toastId} .toast-icon.loading {
          animation: toast-spin 1s linear infinite;
        }
      `;
      document.head.appendChild(styleSheet);
    }

    // Set styles (Sonner style)
    toast.style.cssText = `
      position: fixed;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);
      z-index: 1000000;
      min-width: 356px;
      max-width: 420px;
      background: #ffffff;
      border: 1px solid rgba(0, 0, 0, 0.1);
      border-radius: 8px;
      box-shadow: 0 10px 38px -10px rgba(22, 23, 24, 0.35), 0 10px 20px -15px rgba(22, 23, 24, 0.2);
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
      pointer-events: auto;
      overflow: hidden;
    `;

    // Create content structure
    const content = document.createElement('div');
    content.dataset.capture = "exclude";
    content.style.cssText = `
      display: flex;
      align-items: flex-start;
      gap: 12px;
      padding: 16px;
    `;

    // Icon container
    const iconContainer = document.createElement('div');
    iconContainer.className = 'toast-icon-container';
    iconContainer.style.cssText = `
      flex-shrink: 0;
      width: 20px;
      height: 20px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 4px;
      background: ${style.iconBg};
      color: ${style.iconColor};
      font-size: 14px;
      font-weight: 600;
      line-height: 1;
    `;

    const icon = document.createElement('span');
    icon.className = type === 'loading' ? 'toast-icon loading' : 'toast-icon';
    icon.textContent = style.icon;
    icon.style.cssText = `
      display: inline-block;
      ${type === 'loading' ? 'font-size: 16px;' : ''}
    `;
    iconContainer.appendChild(icon);

    // Text content
    const messageEl = document.createElement('div');
    messageEl.style.cssText = `
      flex: 1;
      font-size: 14px;
      line-height: 1.5;
      color: #09090b;
      font-weight: 400;
      word-break: break-word;
    `;
    messageEl.textContent = message;

    // Left accent bar
    const accentBar = document.createElement('div');
    accentBar.style.cssText = `
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      width: 3px;
      background: ${style.accentColor};
    `;

    // Assemble structure
    content.appendChild(iconContainer);
    content.appendChild(messageEl);
    toast.appendChild(accentBar);
    toast.appendChild(content);

    // Add to page
    document.body.appendChild(toast);

    // Auto remove (loading type doesn't auto-remove)
    if (type !== 'loading' && duration > 0) {
      setTimeout(() => {
        toast.classList.add('toast-exit');
        setTimeout(() => {
          if (toast.parentNode) {
            toast.remove();
          }
        }, 200);
      }, duration);
    }

    return toast;
  }

  /**
   * Execute screenshot function
   * @param {'svg' | 'png'} format
   * @param {HTMLElement} targetElement - Optional, specify element to capture, defaults to entire page
   */
  async function takeScreenshot(format = 'svg', targetElement = null) {
    // Show loading toast
    const loadingToast = showToast('Capturing screenshot...', 'loading', 0);

    try {
      const snapdom = await waitForSnapdom();
      if (!snapdom) {
        throw new Error('snapdom library not loaded');
      }

      // Determine element to capture
      const elementToCapture = targetElement || document.documentElement;

      // Execute screenshot
      const result = await snapdom(elementToCapture);

      // Generate filename
      let filename = `${location.host.split('.')[0]}_${location.pathname}_${new Date().toLocaleTimeString().split(' ')[0].replace(/:/g, '')}`;
      if (targetElement) {
        // If targeting specific element, add element identifier
        const tagName = targetElement.tagName.toLowerCase();
        const className = targetElement.className ? targetElement.className.split(' ')[0] : '';
        filename += `_${tagName}${className ? '_' + className : ''}`;
      }
      // Keep only letters, numbers, and underscores
      filename = filename.replace(/[^a-zA-Z0-9_]/g, '');

      await result.download({ format, filename });

      // Remove loading toast
      if (loadingToast && loadingToast.parentNode) {
        loadingToast.remove();
      }

      // Show success toast
      showToast('Screenshot saved! File downloaded', 'success');
    } catch (error) {
      console.error('Screenshot failed:', error);

      // Remove loading toast
      if (loadingToast && loadingToast.parentNode) {
        loadingToast.remove();
      }

      // Show error toast
      showToast(`Screenshot failed: ${error.message}`, 'error');
    }
  }

  // Element selection mode related variables
  let isElementSelectMode = false;
  let highlightBox = null;
  let currentHoveredElement = null;

  /**
   * Create highlight box
   */
  function createHighlightBox() {
    if (highlightBox) return highlightBox;

    highlightBox = document.createElement('div');
    highlightBox.dataset.capture = "exclude";
    highlightBox.id = highlightBoxId;
    highlightBox.style.cssText = `
      position: fixed;
      pointer-events: none;
      z-index: 999998;
      border: 2px solid #3b82f6;
      background: rgba(59, 130, 246, 0.1);
      box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.15);
      transition: all 0.1s ease-out;
      box-sizing: border-box;
      display: none;
    `;
    document.body.appendChild(highlightBox);
    return highlightBox;
  }

  /**
   * Update highlight box position
   * @param {HTMLElement} element
   */
  function updateHighlightBox(element) {
    if (!highlightBox || !element) return;

    const rect = element.getBoundingClientRect();

    // Use fixed positioning, directly use getBoundingClientRect values
    highlightBox.style.left = `${rect.left}px`;
    highlightBox.style.top = `${rect.top}px`;
    highlightBox.style.width = `${rect.width}px`;
    highlightBox.style.height = `${rect.height}px`;
    highlightBox.style.display = 'block';
  }

  /**
   * Hide highlight box
   */
  function hideHighlightBox() {
    if (highlightBox) {
      highlightBox.style.display = 'none';
    }
  }

  /**
   * Remove highlight box
   */
  function removeHighlightBox() {
    if (highlightBox && highlightBox.parentNode) {
      highlightBox.remove();
      highlightBox = null;
    }
  }

  /**
   * Get element under mouse (exclude highlight box and toast)
   * @param {MouseEvent} e
   * @returns {HTMLElement | null}
   */
  function getElementUnderMouse(e) {
    // Temporarily hide highlight box to avoid affecting element detection
    if (highlightBox) {
      highlightBox.style.pointerEvents = 'none';
    }

    const element = document.elementFromPoint(e.clientX, e.clientY);

    // If clicking on highlight box or toast, return null
    if (!element ||
      element.id === highlightBoxId ||
      element.id === toastId ||
      element.closest(`#${highlightBoxId}`) ||
      element.closest(`#${toastId}`)) {
      return null;
    }

    return element;
  }

  /**
   * Handle mouse move
   */
  function handleMouseMove(e) {
    if (!isElementSelectMode) return;

    const element = getElementUnderMouse(e);

    if (element && element !== currentHoveredElement) {
      currentHoveredElement = element;
      updateHighlightBox(element);
    } else if (!element && currentHoveredElement) {
      // Hide highlight box when mouse leaves element
      currentHoveredElement = null;
      hideHighlightBox();
    }
  }

  /**
   * Handle mouse click
   */
  async function handleMouseClick(e) {
    if (!isElementSelectMode) return;

    e.preventDefault();
    e.stopPropagation();

    const element = getElementUnderMouse(e);

    if (element) {
      // Exit selection mode
      exitElementSelectMode();

      // Capture selected element
      await takeScreenshot('svg', element);
    }
  }

  /**
   * Handle keyboard events (ESC to exit selection mode)
   */
  function handleKeyDown(e) {
    if (!isElementSelectMode) return;

    if (e.key === 'Escape') {
      exitElementSelectMode();
      showToast('Element selection cancelled', 'info');
    }
  }

  /**
   * Handle scroll (update highlight box position)
   */
  function handleScroll() {
    if (!isElementSelectMode || !currentHoveredElement) return;
    updateHighlightBox(currentHoveredElement);
  }

  /**
   * Enter element selection mode
   */
  function enterElementSelectMode() {
    if (isElementSelectMode) return;

    isElementSelectMode = true;
    createHighlightBox();

    // Add event listeners
    document.addEventListener('mousemove', handleMouseMove, true);
    document.addEventListener('click', handleMouseClick, true);
    document.addEventListener('keydown', handleKeyDown, true);
    window.addEventListener('scroll', handleScroll, true);

    // Change cursor style
    document.body.style.cursor = 'crosshair';
    document.body.style.userSelect = 'none';

    showToast('Select an element to capture, press ESC to cancel', 'info', 5000);
  }

  /**
   * Exit element selection mode
   */
  function exitElementSelectMode() {
    if (!isElementSelectMode) return;

    isElementSelectMode = false;
    currentHoveredElement = null;

    // Remove event listeners
    document.removeEventListener('mousemove', handleMouseMove, true);
    document.removeEventListener('click', handleMouseClick, true);
    document.removeEventListener('keydown', handleKeyDown, true);
    window.removeEventListener('scroll', handleScroll, true);

    // Restore cursor style
    document.body.style.cursor = '';
    document.body.style.userSelect = '';

    // Hide highlight box
    hideHighlightBox();
  }

  // Register Tampermonkey menu commands
  if (typeof GM_registerMenuCommand !== 'undefined') {
    // Register screenshot menu items
    GM_registerMenuCommand('📸 Screenshot (SVG)', () => takeScreenshot('svg'), 's');
    GM_registerMenuCommand('📸 Screenshot (PNG)', () => takeScreenshot('png'), 'p');
    // Register element selection screenshot menu item
    GM_registerMenuCommand('🎯 Select Element to Capture', () => enterElementSelectMode(), 'e');
  }
})();