From 6e87d568449307865ddcb8364170002c5734c2f6 Mon Sep 17 00:00:00 2001 From: Matt Godbolt Date: Tue, 7 Oct 2025 11:34:26 -0500 Subject: [PATCH] Fix embedded iframe links not updating with code state (#8166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes the issue where the "Edit on Compiler Explorer" link in embedded iframes doesn't update with the current code state, staying as "/" instead of including the serialized state. ## Changes - Refactored `Sharing` class into a base class `SharingBase` that handles state tracking and embedded link updates - `Sharing` class now extends `SharingBase` and adds the full sharing UI features - Created `initialiseSharing()` function that instantiates the appropriate class based on embedded mode - Updated `main.ts` to use the new initialization function - Updated `history.ts` to use `SharingBase.filterComponentState()` instead of `Sharing.filterComponentState()` ## Testing - TypeScript compilation passes (`npm run ts-check`) - Linting passes (`npm run lint`) - Tests pass (`npm run test-min`) - Manual browser testing confirms embedded links now update correctly Closes #8097 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude --- static/history.ts | 4 +- static/main.ts | 4 +- static/sharing.ts | 119 +++++++++++++++++++++++++++------------------- 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/static/history.ts b/static/history.ts index e3c0df11f..451b196bd 100644 --- a/static/history.ts +++ b/static/history.ts @@ -24,7 +24,7 @@ import _ from 'underscore'; import {localStorage} from './local.js'; -import {Sharing} from './sharing.js'; +import {SharingBase} from './sharing.js'; const maxHistoryEntries = 30; export type HistorySource = {dt: number; source: string}; @@ -100,7 +100,7 @@ export function trackHistory(layout: any) { let lastState: string | null = null; const debouncedPush = _.debounce(push, 500); layout.on('stateChanged', () => { - const stringifiedConfig = JSON.stringify(Sharing.filterComponentState(layout.toConfig())); + const stringifiedConfig = JSON.stringify(SharingBase.filterComponentState(layout.toConfig())); if (stringifiedConfig !== lastState) { lastState = stringifiedConfig; debouncedPush(stringifiedConfig); diff --git a/static/main.ts b/static/main.ts index 6ddd0a57f..7f2686a0c 100644 --- a/static/main.ts +++ b/static/main.ts @@ -64,7 +64,7 @@ import {Presentation} from './presentation.js'; import {Printerinator} from './print-view.js'; import {setupRealDark, takeUsersOutOfRealDark} from './real-dark.js'; import {Settings, SiteSettings} from './settings.js'; -import {Sharing} from './sharing.js'; +import {initialiseSharing} from './sharing.js'; import {Themer} from './themes.js'; import * as url from './url.js'; import {formatISODate, updateAndCalcTopBarHeight} from './utils.js'; @@ -779,7 +779,7 @@ function start() { History.trackHistory(layout); setupSiteTemplateWidgetButton(layout); - if (!options.embedded) new Sharing(layout); + initialiseSharing(layout, !!options.embedded); new Printerinator(hub, themer); hub.layout.eventHub.emit('settingsChange', settings); // Ensure everyone knows the settings diff --git a/static/sharing.ts b/static/sharing.ts index bdb1d1268..0bd150a95 100644 --- a/static/sharing.ts +++ b/static/sharing.ts @@ -85,10 +85,61 @@ const shareServices = { }, }; -export class Sharing { - private layout: GoldenLayout; - private lastState: any; +// Base class that handles state tracking and embedded link updates +export class SharingBase { + protected layout: GoldenLayout; + protected lastState: string | null = null; + constructor(layout: GoldenLayout) { + this.layout = layout; + this.initCallbacks(); + } + + protected initCallbacks(): void { + this.layout.on('stateChanged', this.onStateChanged.bind(this)); + } + + protected onStateChanged(): void { + const config = SharingBase.filterComponentState(this.layout.toConfig()); + this.ensureUrlIsNotOutdated(config); + + // Update embedded links if present (works in both modes) + if (options.embedded) { + const strippedToLast = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1); + $('a.link').prop('href', strippedToLast + '#' + url.serialiseState(config)); + } + } + + protected ensureUrlIsNotOutdated(config: any): void { + const stringifiedConfig = JSON.stringify(config); + if (stringifiedConfig !== this.lastState) { + if (this.lastState != null && window.location.pathname !== window.httpRoot) { + window.history.replaceState(null, '', window.httpRoot); + } + this.lastState = stringifiedConfig; + } + } + + public static filterComponentState(config: any): any { + function filterComponentStateImpl(component: any) { + if (component.content) { + for (let i = 0; i < component.content.length; i++) { + filterComponentStateImpl(component.content[i]); + } + } + + if (component.componentState) { + delete component.componentState.selection; + } + } + + config = cloneDeep(config); + filterComponentStateImpl(config); + return config; + } +} + +export class Sharing extends SharingBase { private readonly share: JQuery; private readonly shareTooltipTarget: JQuery; private readonly shareShort: JQuery; @@ -100,9 +151,8 @@ export class Sharing { private clippyButton: ClipboardJS | null; private readonly shareLinkDialog: HTMLElement; - constructor(layout: any) { - this.layout = layout; - this.lastState = null; + constructor(layout: GoldenLayout) { + super(layout); this.shareLinkDialog = unwrap(document.getElementById('sharelinkdialog'), 'Share modal element not found'); this.share = $('#share'); @@ -119,17 +169,16 @@ export class Sharing { this.clippyButton = null; this.initButtons(); - this.initCallbacks(); + this.initFullCallbacks(); } - private initCallbacks(): void { + private initFullCallbacks(): void { this.layout.eventHub.on('displaySharingPopover', () => { this.openShareModalForType(LinkType.Short); }); this.layout.eventHub.on('copyShortLinkToClip', () => { this.copyLinkTypeToClipboard(LinkType.Short); }); - this.layout.on('stateChanged', this.onStateChanged.bind(this)); this.shareLinkDialog.addEventListener('show.bs.modal', this.onOpenModalPane.bind(this)); this.shareLinkDialog.addEventListener('hidden.bs.modal', this.onCloseModalPane.bind(this)); @@ -152,25 +201,6 @@ export class Sharing { }); } - private onStateChanged(): void { - const config = Sharing.filterComponentState(this.layout.toConfig()); - this.ensureUrlIsNotOutdated(config); - if (options.embedded) { - const strippedToLast = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1); - $('a.link').prop('href', strippedToLast + '#' + url.serialiseState(config)); - } - } - - private ensureUrlIsNotOutdated(config: any): void { - const stringifiedConfig = JSON.stringify(config); - if (stringifiedConfig !== this.lastState) { - if (this.lastState != null && window.location.pathname !== window.httpRoot) { - window.history.replaceState(null, '', window.httpRoot); - } - this.lastState = stringifiedConfig; - } - } - private static bindToLinkType(bind: string): LinkType { switch (bind) { case 'Full': @@ -466,28 +496,6 @@ export class Sharing { return (navigator.clipboard as Clipboard | undefined) !== undefined; } - public static filterComponentState(config: any, keysToRemove: [string] = ['selection']): any { - function filterComponentStateImpl(component: any) { - if (component.content) { - for (let i = 0; i < component.content.length; i++) { - filterComponentStateImpl(component.content[i]); - } - } - - if (component.componentState) { - Object.keys(component.componentState) - .filter(e => keysToRemove.includes(e)) - .forEach(key => { - delete component.componentState[key]; - }); - } - } - - config = cloneDeep(config); - filterComponentStateImpl(config); - return config; - } - private static updateShares(container: JQuery, url: string): void { const baseTemplate = $('#share-item'); _.each(shareServices, (service, serviceName) => { @@ -508,3 +516,14 @@ export class Sharing { }); } } + +// Initialize sharing functionality based on whether we're in embedded mode or not +export function initialiseSharing(layout: GoldenLayout, isEmbedded: boolean): void { + if (isEmbedded) { + // Just create the base class for state tracking and link updates + new SharingBase(layout); + } else { + // Create full Sharing with all features + new Sharing(layout); + } +}