Skip to content

Commit e6fb9b9

Browse files
Merge pull request #7290 from hotosm/fix/id-editor-sandbox-editor-global-bleed
Fix cross-session state bleed between OSM iD editor and Sandbox editor
2 parents 54f1583 + 4c41e46 commit e6fb9b9

3 files changed

Lines changed: 81 additions & 15 deletions

File tree

frontend/src/components/editor.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useLayoutEffect, useState } from 'react';
22
import { useSelector, useDispatch } from 'react-redux';
33
import { useIntl } from 'react-intl';
44
import { gpx } from '@tmcw/togeojson';
@@ -7,6 +7,19 @@ import '@openstreetmap/id/dist/iD.css';
77

88
import { OSM_CLIENT_ID, OSM_REDIRECT_URI, OSM_SERVER_URL } from '../config';
99
import messages from './messages';
10+
import {
11+
registerIdEditorStylesheet,
12+
activateIdEditorStylesheet,
13+
} from '../utils/idEditorStylesheets';
14+
15+
// @openstreetmap/id has no real ESM/CJS exports — it only assigns itself to
16+
// window.iD as a side effect on import. Capture it here, right after the
17+
// import above forces that assignment, so this module keeps its own private
18+
// reference instead of the shared global, which @osm-sandbox/sandbox-id
19+
// (imported by sandboxEditor.js) later overwrites.
20+
const officialID = window.iD;
21+
22+
registerIdEditorStylesheet('official');
1023

1124
export default function Editor({ setDisable, comment, presets, imagery, gpxUrl, extraIdParams }) {
1225
const dispatch = useDispatch();
@@ -19,13 +32,21 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl,
1932
const customSource =
2033
iDContext && iDContext.background() && iDContext.background().findSource('custom');
2134

35+
// Only one of the OSM iD editor / Sandbox editor is ever mounted at a
36+
// time, but both of their stylesheets stay loaded for the whole page
37+
// session once visited. Disable the other one's so its rules can't leak
38+
// into this editor via their shared ".ideditor" root class.
39+
useLayoutEffect(() => {
40+
activateIdEditorStylesheet('official');
41+
}, []);
42+
2243
useEffect(() => {
2344
if (!customImageryIsSet && imagery && customSource) {
2445
if (imagery.startsWith('http')) {
2546
iDContext.background().baseLayerSource(customSource.template(imagery));
2647
setCustomImageryIsSet(true);
2748
// this line is needed to update the value on the custom background dialog
28-
window.iD.prefs('background-custom-template', imagery);
49+
officialID.prefs('background-custom-template', imagery);
2950
} else {
3051
const imagerySource = iDContext.background().findSource(imagery);
3152
if (imagerySource) {
@@ -52,7 +73,7 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl,
5273
const offsetStr = params.get('offset'); // "10,-10"
5374
if (!offsetStr) return;
5475
const offsetInMeters = offsetStr.split(',').map(Number); // [10, -10]
55-
const offset = window.iD.geoMetersToOffset(offsetInMeters);
76+
const offset = officialID.geoMetersToOffset(offsetInMeters);
5677
iDContext.background().offset(offset);
5778
} else {
5879
// reset offset if params not present
@@ -66,13 +87,13 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl,
6687
if (iDContext === null) {
6788
// we need to keep iD context on redux store because iD works better if
6889
// the context is not restarted while running in the same browser session
69-
dispatch({ type: 'SET_EDITOR', context: window.iD.coreContext() });
90+
dispatch({ type: 'SET_EDITOR', context: officialID.coreContext() });
7091
}
7192
}
7293
}, [windowInit, iDContext, dispatch]);
7394

7495
// Reset context on unmount so the sandbox editor always gets a fresh context
75-
// from its own window.iD (sandbox-id), preventing cross-editor context bleed.
96+
// from its own iD module (sandbox-id), preventing cross-editor context bleed.
7697
useEffect(() => {
7798
return () => {
7899
dispatch({ type: 'SET_EDITOR', context: null });
@@ -90,12 +111,12 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl,
90111
// if presets is not a populated list we need to set it as null
91112
try {
92113
if (presets.length) {
93-
window.iD.presetManager.addablePresetIDs(presets);
114+
officialID.presetManager.addablePresetIDs(presets);
94115
} else {
95-
window.iD.presetManager.addablePresetIDs(null);
116+
officialID.presetManager.addablePresetIDs(null);
96117
}
97118
} catch (e) {
98-
window.iD.presetManager.addablePresetIDs(null);
119+
officialID.presetManager.addablePresetIDs(null);
99120
}
100121
// setup the context
101122
iDContext

frontend/src/components/sandboxEditor.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useLayoutEffect, useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { useSelector, useDispatch } from 'react-redux';
44
import { useIntl } from 'react-intl';
@@ -15,6 +15,19 @@ import {
1515
import { useSandboxOAuthCallback } from '../hooks/UseSandboxOAuthCallback';
1616
import { getValidTokenOrInitiateAuth, fetchSandboxLicense } from '../utils/sandboxUtils';
1717
import { useOsmFeaturesQuery } from '../api/projects';
18+
import {
19+
registerIdEditorStylesheet,
20+
activateIdEditorStylesheet,
21+
} from '../utils/idEditorStylesheets';
22+
23+
// @osm-sandbox/sandbox-id has no real ESM/CJS exports — it only assigns
24+
// itself to window.iD as a side effect on import. Capture it here, right
25+
// after the imports above force that assignment, so this module keeps its
26+
// own private reference instead of the shared global, which
27+
// @openstreetmap/id (imported by editor.js) later overwrites.
28+
const sandboxID = window.iD;
29+
30+
registerIdEditorStylesheet('sandbox');
1831

1932
export default function SandboxEditor({
2033
setDisable,
@@ -49,6 +62,14 @@ export default function SandboxEditor({
4962

5063
useSandboxOAuthCallback(sandboxId);
5164

65+
// Only one of the OSM iD editor / Sandbox editor is ever mounted at a
66+
// time, but both of their stylesheets stay loaded for the whole page
67+
// session once visited. Disable the other one's so its rules can't leak
68+
// into this editor via their shared ".ideditor" root class.
69+
useLayoutEffect(() => {
70+
activateIdEditorStylesheet('sandbox');
71+
}, []);
72+
5273
const customSource =
5374
iDContext && iDContext.background() && iDContext.background().findSource('custom');
5475

@@ -58,7 +79,7 @@ export default function SandboxEditor({
5879
iDContext.background().baseLayerSource(customSource.template(imagery));
5980
setCustomImageryIsSet(true);
6081
// this line is needed to update the value on the custom background dialog
61-
window.iD.prefs('background-custom-template', imagery);
82+
sandboxID.prefs('background-custom-template', imagery);
6283
} else {
6384
const imagerySource = iDContext.background().findSource(imagery);
6485
if (imagerySource) {
@@ -72,7 +93,7 @@ export default function SandboxEditor({
7293
if (iDContext === null) {
7394
// we need to keep iD context on redux store because iD works better if
7495
// the context is not restarted while running in the same browser session
75-
dispatch({ type: 'SET_EDITOR', context: window.iD.coreContext() });
96+
dispatch({ type: 'SET_EDITOR', context: sandboxID.coreContext() });
7697
}
7798
}, [iDContext, dispatch]);
7899

@@ -113,12 +134,12 @@ export default function SandboxEditor({
113134
// set up presets
114135
try {
115136
if (presets && presets.length) {
116-
window.iD.presetManager.addablePresetIDs(presets);
137+
sandboxID.presetManager.addablePresetIDs(presets);
117138
} else {
118-
window.iD.presetManager.addablePresetIDs(null);
139+
sandboxID.presetManager.addablePresetIDs(null);
119140
}
120141
} catch (e) {
121-
window.iD.presetManager.addablePresetIDs(null);
142+
sandboxID.presetManager.addablePresetIDs(null);
122143
}
123144

124145
// set up the context
@@ -268,7 +289,7 @@ export default function SandboxEditor({
268289
// Reset auth status for this sandbox on unmount
269290
dispatch(setSandboxAuthStatus(sandboxId, 'idle'));
270291
// Reset context on unmount so the OSM iD editor always gets a fresh context
271-
// from its own window.iD (@openstreetmap/id), preventing cross-editor context bleed.
292+
// from its own iD module (@openstreetmap/id), preventing cross-editor context bleed.
272293
dispatch({ type: 'SET_EDITOR', context: null });
273294
};
274295
}, [dispatch, sandboxId]);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// @openstreetmap/id and @osm-sandbox/sandbox-id each ship a stylesheet that,
2+
// once loaded as part of the Editor/SandboxEditor lazy chunk, is never
3+
// unloaded for the rest of the page session. Both stylesheets style the same
4+
// shared ".ideditor" root class, so once a user has opened both editor types
5+
// in one session, whichever stylesheet loaded most recently can leak
6+
// conflicting rules into the other editor. Only one of the two editors is
7+
// ever mounted at a time, so tag each package's <link> as it loads and keep
8+
// only the active one's stylesheet enabled.
9+
const ATTR = 'data-id-editor-pkg';
10+
11+
// Call once, at module scope, immediately after importing a package's own
12+
// CSS file. Webpack defers running a lazy chunk's JS until its <link> has
13+
// loaded, so at this exact point the tag we're looking for is reliably the
14+
// most recently appended stylesheet link.
15+
export function registerIdEditorStylesheet(key) {
16+
const link = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).pop();
17+
if (link) link.setAttribute(ATTR, key);
18+
}
19+
20+
export function activateIdEditorStylesheet(key) {
21+
document.querySelectorAll(`link[${ATTR}]`).forEach((link) => {
22+
link.disabled = link.getAttribute(ATTR) !== key;
23+
});
24+
}

0 commit comments

Comments
 (0)