Fix embedded iframe links not updating with code state (#8166)

## 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 <noreply@anthropic.com>
This commit is contained in:
Matt Godbolt
2025-10-07 11:34:26 -05:00
committed by GitHub
parent 4fb82e8ce2
commit 6e87d56844
3 changed files with 73 additions and 54 deletions

View File

@@ -24,7 +24,7 @@
import _ from 'underscore'; import _ from 'underscore';
import {localStorage} from './local.js'; import {localStorage} from './local.js';
import {Sharing} from './sharing.js'; import {SharingBase} from './sharing.js';
const maxHistoryEntries = 30; const maxHistoryEntries = 30;
export type HistorySource = {dt: number; source: string}; export type HistorySource = {dt: number; source: string};
@@ -100,7 +100,7 @@ export function trackHistory(layout: any) {
let lastState: string | null = null; let lastState: string | null = null;
const debouncedPush = _.debounce(push, 500); const debouncedPush = _.debounce(push, 500);
layout.on('stateChanged', () => { layout.on('stateChanged', () => {
const stringifiedConfig = JSON.stringify(Sharing.filterComponentState(layout.toConfig())); const stringifiedConfig = JSON.stringify(SharingBase.filterComponentState(layout.toConfig()));
if (stringifiedConfig !== lastState) { if (stringifiedConfig !== lastState) {
lastState = stringifiedConfig; lastState = stringifiedConfig;
debouncedPush(stringifiedConfig); debouncedPush(stringifiedConfig);

View File

@@ -64,7 +64,7 @@ import {Presentation} from './presentation.js';
import {Printerinator} from './print-view.js'; import {Printerinator} from './print-view.js';
import {setupRealDark, takeUsersOutOfRealDark} from './real-dark.js'; import {setupRealDark, takeUsersOutOfRealDark} from './real-dark.js';
import {Settings, SiteSettings} from './settings.js'; import {Settings, SiteSettings} from './settings.js';
import {Sharing} from './sharing.js'; import {initialiseSharing} from './sharing.js';
import {Themer} from './themes.js'; import {Themer} from './themes.js';
import * as url from './url.js'; import * as url from './url.js';
import {formatISODate, updateAndCalcTopBarHeight} from './utils.js'; import {formatISODate, updateAndCalcTopBarHeight} from './utils.js';
@@ -779,7 +779,7 @@ function start() {
History.trackHistory(layout); History.trackHistory(layout);
setupSiteTemplateWidgetButton(layout); setupSiteTemplateWidgetButton(layout);
if (!options.embedded) new Sharing(layout); initialiseSharing(layout, !!options.embedded);
new Printerinator(hub, themer); new Printerinator(hub, themer);
hub.layout.eventHub.emit('settingsChange', settings); // Ensure everyone knows the settings hub.layout.eventHub.emit('settingsChange', settings); // Ensure everyone knows the settings

View File

@@ -85,10 +85,61 @@ const shareServices = {
}, },
}; };
export class Sharing { // Base class that handles state tracking and embedded link updates
private layout: GoldenLayout; export class SharingBase {
private lastState: any; 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 share: JQuery;
private readonly shareTooltipTarget: JQuery; private readonly shareTooltipTarget: JQuery;
private readonly shareShort: JQuery; private readonly shareShort: JQuery;
@@ -100,9 +151,8 @@ export class Sharing {
private clippyButton: ClipboardJS | null; private clippyButton: ClipboardJS | null;
private readonly shareLinkDialog: HTMLElement; private readonly shareLinkDialog: HTMLElement;
constructor(layout: any) { constructor(layout: GoldenLayout) {
this.layout = layout; super(layout);
this.lastState = null;
this.shareLinkDialog = unwrap(document.getElementById('sharelinkdialog'), 'Share modal element not found'); this.shareLinkDialog = unwrap(document.getElementById('sharelinkdialog'), 'Share modal element not found');
this.share = $('#share'); this.share = $('#share');
@@ -119,17 +169,16 @@ export class Sharing {
this.clippyButton = null; this.clippyButton = null;
this.initButtons(); this.initButtons();
this.initCallbacks(); this.initFullCallbacks();
} }
private initCallbacks(): void { private initFullCallbacks(): void {
this.layout.eventHub.on('displaySharingPopover', () => { this.layout.eventHub.on('displaySharingPopover', () => {
this.openShareModalForType(LinkType.Short); this.openShareModalForType(LinkType.Short);
}); });
this.layout.eventHub.on('copyShortLinkToClip', () => { this.layout.eventHub.on('copyShortLinkToClip', () => {
this.copyLinkTypeToClipboard(LinkType.Short); 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('show.bs.modal', this.onOpenModalPane.bind(this));
this.shareLinkDialog.addEventListener('hidden.bs.modal', this.onCloseModalPane.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 { private static bindToLinkType(bind: string): LinkType {
switch (bind) { switch (bind) {
case 'Full': case 'Full':
@@ -466,28 +496,6 @@ export class Sharing {
return (navigator.clipboard as Clipboard | undefined) !== undefined; 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 { private static updateShares(container: JQuery, url: string): void {
const baseTemplate = $('#share-item'); const baseTemplate = $('#share-item');
_.each(shareServices, (service, serviceName) => { _.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);
}
}