From 68b1ba44526bae1f2f0f75d86fda6450b3b21c44 Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 09:40:58 +0200 Subject: [PATCH 1/8] preload html2canvas on share click --- javascript/commons/ExportImage.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index b096a7c37e7..1dce753e017 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -761,8 +761,11 @@ class DropdownWidget { }, buttonContent ); button.addEventListener( 'click', () => { - if ( menuElement.style.display === 'none' && onOpen ) { - onOpen(); + if ( menuElement.style.display === 'none' ) { + this.exportService.ensureHtml2CanvasLoaded(); + if ( onOpen ) { + onOpen(); + } } this.toggleMenu( menuElement, button ); } ); From 6399bee74fd926f9a214d15a956cf6737e84e0de Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 10:37:30 +0200 Subject: [PATCH 2/8] include alternative grouptable selector --- javascript/commons/ExportImage.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index 1dce753e017..c4e6f5dff27 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -58,7 +58,7 @@ const EXPORT_IMAGE_CONFIG = { SELECTORS: [ { selector: '.brkts-bracket-wrapper', targetSelector: '.brkts-bracket', typeName: 'Bracket' }, { - selector: '.group-table', + selector: '.group-table, .grouptable', targetSelector: null, typeName: 'Group Table', titleSelector: '.group-table-title' @@ -578,6 +578,7 @@ class DOMUtils { const configs = EXPORT_IMAGE_CONFIG.SELECTORS; const headingsToElements = new Map(); + const processedElements = new Set(); for ( const config of configs ) { const elements = document.querySelectorAll( config.selector ); @@ -586,10 +587,12 @@ class DOMUtils { element.querySelector( config.targetSelector ) : element; - if ( !targetElement ) { + if ( !targetElement || processedElements.has( targetElement ) ) { continue; } + processedElements.add( targetElement ); + const headingInfo = this.findPreviousHeading( element ); if ( !headingInfo ) { continue; From ee1c5bf5d87dcdbccdf494da99b5aa6821ca4372 Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 11:32:08 +0200 Subject: [PATCH 3/8] fix zooming issue by resetting scale --- javascript/commons/ExportImage.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index c4e6f5dff27..3931120924e 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -407,7 +407,15 @@ class ExportService { const backgroundColor = this.getBackgroundColor(); element.style.background = backgroundColor; - const capturedCanvas = await html2canvas( element ); + const capturedCanvas = await html2canvas( element, { + scale: 1, + windowWidth: document.documentElement.scrollWidth, + windowHeight: document.documentElement.scrollHeight, + scrollX: 0, + scrollY: 0, + backgroundColor: backgroundColor + } ); + element.style.background = originalBackground; if ( capturedCanvas.width === 0 || capturedCanvas.height === 0 ) { From e64e91c74704f33d32bebd7f53e903692c248d8c Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 11:56:42 +0200 Subject: [PATCH 4/8] fix failing copy due to window losing focus --- javascript/commons/ExportImage.js | 113 ++++++++++++++++++------------ 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index 3931120924e..780b5d415ed 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -398,13 +398,30 @@ class ExportService { } this.activeExports.add( exportId ); - const originalBackground = element.style.background; try { await this.ensureHtml2CanvasLoaded(); - const isDarkTheme = document.documentElement.classList.contains( 'theme--dark' ); - const backgroundColor = this.getBackgroundColor(); + if ( mode === 'copy' ) { + await this.copyToClipboard( element, title ); + } else if ( mode === 'download' ) { + const blob = await this.generateImageBlob( element, title ); + await this.downloadBlob( blob, this.generateFilename( title ) ); + } else { + throw new Error( `Unknown export mode: ${ mode }` ); + } + + } finally { + this.activeExports.delete( exportId ); + } + } + + async generateImageBlob( element, title ) { + const originalBackground = element.style.background; + const isDarkTheme = document.documentElement.classList.contains( 'theme--dark' ); + const backgroundColor = this.getBackgroundColor(); + + try { element.style.background = backgroundColor; const capturedCanvas = await html2canvas( element, { @@ -423,14 +440,61 @@ class ExportService { } const composedCanvas = await this.canvasComposer.compose( capturedCanvas, title, isDarkTheme ); - await this.outputResult( composedCanvas, mode, this.generateFilename( title ) ); - } finally { + return new Promise( ( resolve, reject ) => { + composedCanvas.toBlob( ( blob ) => { + if ( blob ) { + resolve( blob ); + } else { + reject( new Error( 'Failed to create image blob' ) ); + } + }, 'image/png' ); + } ); + + } catch ( error ) { element.style.background = originalBackground; - this.activeExports.delete( exportId ); + throw error; } } + async copyToClipboard( element, title ) { + + if ( !window.ClipboardItem || !navigator.clipboard || !navigator.clipboard.write ) { + mw.notify( 'This browser does not support copying images to the clipboard.', { type: 'error' } ); + return; + } + + try { + const blobPromise = this.generateImageBlob( element, title ); + + // eslint-disable-next-line compat/compat + const clipboardItem = new ClipboardItem( { + 'image/png': blobPromise + } ); + + // eslint-disable-next-line compat/compat + await navigator.clipboard.write( [ clipboardItem ] ); + mw.notify( 'Image copied to clipboard!' ); + + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Clipboard write failed:', error ); + mw.notify( 'Failed to copy image to clipboard. Please try the Download option.', { type: 'error' } ); + } + } + + async downloadBlob( blob, filename ) { + const url = URL.createObjectURL( blob ); + const link = document.createElement( 'a' ); + link.download = `${ filename }.png`; + link.href = url; + link.click(); + + setTimeout( () => { + URL.revokeObjectURL( url ); + }, 100 ); + } + async ensureHtml2CanvasLoaded() { if ( this.html2canvasLoaded ) { return; @@ -472,43 +536,6 @@ class ExportService { return `${ year }${ month }${ day }_${ hour }${ min }${ sec }`; } - async outputResult( canvas, mode, filename ) { - if ( mode === 'download' ) { - await this.downloadImage( canvas, filename ); - } else if ( mode === 'copy' ) { - await this.copyToClipboard( canvas ); - } else { - throw new Error( `Unknown export mode: ${ mode }` ); - } - } - - async downloadImage( canvas, filename ) { - const link = document.createElement( 'a' ); - link.download = `${ filename }.png`; - link.href = canvas.toDataURL( 'image/png' ); - link.click(); - } - - async copyToClipboard( canvas ) { - if ( !window.ClipboardItem ) { - throw new Error( 'Clipboard API not supported in this browser' ); - } - - const blob = await new Promise( ( resolve, reject ) => { - canvas.toBlob( ( result ) => { - if ( result ) { - resolve( result ); - } else { - reject( new Error( 'Failed to create image blob' ) ); - } - }, 'image/png' ); - } ); - - // eslint-disable-next-line compat/compat - await navigator.clipboard.write( [ new ClipboardItem( { 'image/png': blob } ) ] ); - mw.notify( 'Image copied to clipboard!' ); - } - isExporting() { return this.activeExports.size > 0; } From 6054037fce10746eae798ff7b128f8f7a92033a0 Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 12:37:50 +0200 Subject: [PATCH 5/8] info icon fix --- javascript/commons/ExportImage.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index 780b5d415ed..9b1a4232e20 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -390,6 +390,24 @@ class ExportService { this.activeExports = new Set(); } + // Fixes info icon used in match lists and brackets rendering + // by replacing the icon with a SVG instead of backgroundImage for the exported image + applyCloneFixes( clonedDoc ) { + const infoSvg = '' + + '' + + '' + + ''; + const infoIcons = clonedDoc.querySelectorAll( '.brkts-match-info-icon' ); + infoIcons.forEach( ( icon ) => { + icon.style.backgroundImage = 'none'; + icon.innerHTML = infoSvg; + icon.style.display = 'inline-flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + icon.style.verticalAlign = 'middle'; + } ); + } + async export( element, title, mode ) { const exportId = Symbol( 'export' ); @@ -430,7 +448,8 @@ class ExportService { windowHeight: document.documentElement.scrollHeight, scrollX: 0, scrollY: 0, - backgroundColor: backgroundColor + backgroundColor: backgroundColor, + onclone: ( clonedDoc ) => this.applyCloneFixes( clonedDoc ) } ); element.style.background = originalBackground; @@ -1050,6 +1069,5 @@ class ExportImageModule { } } -// Export for liquipedia integration liquipedia.exportImage = new ExportImageModule(); liquipedia.core.modules.push( 'exportImage' ); From aa10614741a11e12ad0fda5ec102e00ba4faac88 Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 12:54:05 +0200 Subject: [PATCH 6/8] extract icon fix for cleaner resposibilities --- javascript/commons/ExportImage.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index 9b1a4232e20..81f4f4ab225 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -55,6 +55,12 @@ const EXPORT_IMAGE_CONFIG = { TEXT: '#181818' } }, + SVG_ICONS: { + INFO: '' + + '' + + '' + + '' + }, SELECTORS: [ { selector: '.brkts-bracket-wrapper', targetSelector: '.brkts-bracket', typeName: 'Bracket' }, { @@ -390,17 +396,18 @@ class ExportService { this.activeExports = new Set(); } + // Applies fixes to cloned document for proper rendering in exported images + applyCloneFixes( clonedDoc ) { + this.fixInfoIcons( clonedDoc ); + } + // Fixes info icon used in match lists and brackets rendering // by replacing the icon with a SVG instead of backgroundImage for the exported image - applyCloneFixes( clonedDoc ) { - const infoSvg = '' + - '' + - '' + - ''; + fixInfoIcons( clonedDoc ) { const infoIcons = clonedDoc.querySelectorAll( '.brkts-match-info-icon' ); infoIcons.forEach( ( icon ) => { icon.style.backgroundImage = 'none'; - icon.innerHTML = infoSvg; + icon.innerHTML = EXPORT_IMAGE_CONFIG.SVG_ICONS.INFO; icon.style.display = 'inline-flex'; icon.style.alignItems = 'center'; icon.style.justifyContent = 'center'; From 8cb7557115afe4560912191cd12fdd80838ce485 Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 12:55:41 +0200 Subject: [PATCH 7/8] add clarity to timeout --- javascript/commons/ExportImage.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index 81f4f4ab225..9cd156d669a 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -516,9 +516,12 @@ class ExportService { link.href = url; link.click(); + // Delay revoking the object URL to ensure the download has time to start reliably in all browsers. + const URL_REVOKE_OBJECT_URL_DELAY_MS = 100; + setTimeout( () => { URL.revokeObjectURL( url ); - }, 100 ); + }, URL_REVOKE_OBJECT_URL_DELAY_MS ); } async ensureHtml2CanvasLoaded() { From 2c02f0716dd6bbd7c6048e814c724fa15909f4e0 Mon Sep 17 00:00:00 2001 From: Eetu Rantanen Date: Tue, 27 Jan 2026 14:26:22 +0200 Subject: [PATCH 8/8] hide info icons completely --- javascript/commons/ExportImage.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/javascript/commons/ExportImage.js b/javascript/commons/ExportImage.js index 9cd156d669a..406fe2ae427 100644 --- a/javascript/commons/ExportImage.js +++ b/javascript/commons/ExportImage.js @@ -55,12 +55,6 @@ const EXPORT_IMAGE_CONFIG = { TEXT: '#181818' } }, - SVG_ICONS: { - INFO: '' + - '' + - '' + - '' - }, SELECTORS: [ { selector: '.brkts-bracket-wrapper', targetSelector: '.brkts-bracket', typeName: 'Bracket' }, { @@ -398,20 +392,13 @@ class ExportService { // Applies fixes to cloned document for proper rendering in exported images applyCloneFixes( clonedDoc ) { - this.fixInfoIcons( clonedDoc ); + this.hideInfoIcons( clonedDoc ); } - // Fixes info icon used in match lists and brackets rendering - // by replacing the icon with a SVG instead of backgroundImage for the exported image - fixInfoIcons( clonedDoc ) { + hideInfoIcons( clonedDoc ) { const infoIcons = clonedDoc.querySelectorAll( '.brkts-match-info-icon' ); infoIcons.forEach( ( icon ) => { - icon.style.backgroundImage = 'none'; - icon.innerHTML = EXPORT_IMAGE_CONFIG.SVG_ICONS.INFO; - icon.style.display = 'inline-flex'; - icon.style.alignItems = 'center'; - icon.style.justifyContent = 'center'; - icon.style.verticalAlign = 'middle'; + icon.style.display = 'none'; } ); }