mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
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:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user