msyavuz commented on code in PR #34233:
URL: https://github.com/apache/superset/pull/34233#discussion_r2228134783


##########
superset-frontend/src/utils/downloadAsImage.ts:
##########
@@ -32,57 +32,260 @@ import { addWarningToast } from 
'src/components/MessageToasts/actions';
 const generateFileStem = (description: string, date = new Date()) =>
   `${kebabCase(description)}-${date.toISOString().replace(/[: ]/g, '-')}`;
 
-/**
- * Create an event handler for turning an element into an image
- *
- * @param selector css selector of the parent element which should be turned 
into image
- * @param description name or a short description of what is being printed.
- *   Value will be normalized, and a date as well as a file extension will be 
added.
- * @param isExactSelector if false, searches for the closest ancestor that 
matches selector.
- * @returns event handler
- */
-export default function downloadAsImage(
+const CRITICAL_STYLE_PROPERTIES = new Set([
+  'display',
+  'position',
+  'width',
+  'height',
+  'max-width',
+  'max-height',
+  'margin',
+  'padding',
+  'top',
+  'right',
+  'bottom',
+  'left',
+  'font',
+  'font-family',
+  'font-size',
+  'font-weight',
+  'font-style',
+  'line-height',
+  'letter-spacing',
+  'word-spacing',
+  'text-align',
+  'text-decoration',
+  'color',
+  'background-color',
+  'border',
+  'border-width',
+  'border-style',
+  'border-color',
+  'opacity',
+  'visibility',
+  'overflow',
+  'z-index',
+  'transform',
+  'flex',
+  'flex-direction',
+  'justify-content',
+  'align-items',
+  'grid',
+  'grid-template',
+  'grid-area',
+  'table-layout',
+  'vertical-align',
+  'text-align',
+]);
+
+const styleCache = new WeakMap<Element, CSSStyleDeclaration>();
+
+const copyAllComputedStyles = (original: Element, clone: Element) => {
+  const queue: Array<[Element, Element]> = [[original, clone]];
+  const processed = new WeakSet<Element>();
+
+  while (queue.length) {
+    const [origNode, cloneNode] = queue.shift()!;
+    if (processed.has(origNode)) continue;
+    processed.add(origNode);
+
+    let computed = styleCache.get(origNode);
+    if (!computed) {
+      computed = window.getComputedStyle(origNode);
+      styleCache.set(origNode, computed);
+    }
+
+    for (const property of CRITICAL_STYLE_PROPERTIES) {
+      const value = computed.getPropertyValue(property);
+      if (value && value !== 'initial' && value !== 'inherit') {
+        (cloneNode as HTMLElement).style.setProperty(
+          property,
+          value,
+          computed.getPropertyPriority(property),
+        );
+      }
+    }
+
+    if (origNode.textContent?.trim()) {
+      const { color } = computed;
+      if (!color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)') {
+        (cloneNode as HTMLElement).style.color = '#000';
+      }
+      (cloneNode as HTMLElement).style.visibility = 'visible';
+      if (computed.display === 'none') {
+        (cloneNode as HTMLElement).style.display = 'block';
+      }
+    }
+
+    for (let i = 0; i < origNode.children.length; i += 1) {
+      queue.push([origNode.children[i], cloneNode.children[i]]);
+    }
+  }
+};
+
+const processCloneForVisibility = (clone: HTMLElement) => {
+  const cloneStyle = clone.style;
+  cloneStyle.height = 'auto';
+  cloneStyle.maxHeight = 'none';
+
+  const scrollableSelectors = [
+    '[style*="overflow"]',
+    '.scrollable',
+    '.table-responsive',
+    '.ant-table-body',
+    '.table-container',
+    '.ant-table-container',
+    '.table-wrapper',
+    '.ant-table-tbody',
+    'tbody',
+    '.table-body',
+    '.virtual-table',
+    '.react-window',
+    '.react-virtualized',
+  ];
+
+  scrollableSelectors.forEach(selector => {
+    clone.querySelectorAll(selector).forEach(el => {
+      const element = el as HTMLElement;
+      element.style.overflow = 'visible';
+      element.style.height = 'auto';
+      element.style.maxHeight = 'none';
+    });
+  });
+
+  clone
+    .querySelectorAll('table, .ant-table, .table-container, .data-table')
+    .forEach(table => {
+      const el = table as HTMLElement;
+      el.style.margin = '0 auto';
+      el.style.display = 'table';
+      el.style.width = '100%';
+      el.style.tableLayout = 'auto';
+    });
+
+  clone
+    .querySelectorAll('tr, .ant-table-row, .table-row, .data-row')
+    .forEach(row => {
+      const el = row as HTMLElement;
+      el.style.display = 'table-row';
+      el.style.visibility = 'visible';
+      el.style.height = 'auto';
+    });
+
+  clone
+    .querySelectorAll('td, th, .ant-table-cell, .table-cell')
+    .forEach(cell => {
+      const el = cell as HTMLElement;
+      el.style.display = 'table-cell';
+      el.style.visibility = 'visible';
+    });
+
+  clone.querySelectorAll('*').forEach(el => {
+    const element = el as HTMLElement;
+    if (element.textContent?.trim()) {
+      const computed = window.getComputedStyle(element);
+      if (computed.color === 'transparent') {
+        element.style.color = '#000';
+      }
+      element.style.visibility = 'visible';
+      if (computed.display === 'none') {
+        element.style.display = 'block';
+      }
+    }
+  });
+
+  clone
+    .querySelectorAll('[data-virtualized], .virtualized, .lazy-load')
+    .forEach(el => {
+      const element = el as HTMLElement;
+      element.style.height = 'auto';
+      element.style.maxHeight = 'none';
+    });
+};
+
+const createEnhancedClone = (
+  originalElement: Element,
+): { clone: HTMLElement; cleanup: () => void } => {
+  const clone = originalElement.cloneNode(true) as HTMLElement;
+  copyAllComputedStyles(originalElement, clone);
+
+  const tempContainer = document.createElement('div');
+  tempContainer.style.cssText = `
+    position: absolute;
+    left: -20000px;
+    top: -20000px;
+    visibility: hidden;
+    pointer-events: none;
+    z-index: -1000;
+  `;
+  tempContainer.appendChild(clone);
+  document.body.appendChild(tempContainer);
+
+  processCloneForVisibility(clone);
+
+  const cleanup = () => {
+    styleCache.delete?.(originalElement);
+    if (tempContainer.parentElement) {
+      tempContainer.parentElement.removeChild(tempContainer);
+    }
+  };
+
+  return { clone, cleanup };
+};
+
+export default function downloadAsImageOptimized(
   selector: string,
   description: string,
   isExactSelector = false,
   theme?: SupersetTheme,
 ) {
-  return (event: SyntheticEvent) => {
+  return async (event: SyntheticEvent) => {
     const elementToPrint = isExactSelector
       ? document.querySelector(selector)
       : event.currentTarget.closest(selector);
 
     if (!elementToPrint) {
-      return addWarningToast(
+      addWarningToast(
         t('Image download failed, please refresh and try again.'),
       );
+      return;
     }
 
-    // Mapbox controls are loaded from different origin, causing CORS error
-    // See 
https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL#exceptions
-    const filter = (node: Element) => {
-      if (typeof node.className === 'string') {
-        return (
-          node.className !== 'mapboxgl-control-container' &&
-          !node.className.includes('header-controls')
-        );
-      }
-      return true;
-    };
+    let cleanup: (() => void) | null = null;
+
+    try {
+      const { clone, cleanup: cleanupFn } = 
createEnhancedClone(elementToPrint);
+      cleanup = cleanupFn;
+
+      const filter = (node: Element) =>
+        typeof node.className === 'string'
+          ? !node.className.includes('mapboxgl-control-container') &&
+            !node.className.includes('header-controls')
+          : true;
 
-    return domToImage
-      .toJpeg(elementToPrint, {
+      const dataUrl = await domToImage.toJpeg(clone, {
         bgcolor: theme?.colors.grayscale.light4,
         filter,
-      })
-      .then((dataUrl: string) => {
-        const link = document.createElement('a');
-        link.download = `${generateFileStem(description)}.jpg`;
-        link.href = dataUrl;
-        link.click();
-      })
-      .catch((e: Error) => {
-        console.error('Creating image failed', e);
+        quality: 0.95,

Review Comment:
   Can we have a variable for this instead of a magic number?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org
For additional commands, e-mail: notifications-h...@superset.apache.org

Reply via email to