Skip to content

Commit b1a4ee0

Browse files
lbogdanCompuIves
authored andcommitted
Sandbox preview: replace window.history with our own emulated history (codesandbox#198)
* [WIP] Sandbox preview: replace window.history with our own emulated history, when inside the editor. * Changed the way to monkey patch window.history, __proto__ had issues in Firefox. * Refactored window.history monkey patching. * Sandbox preview: made history start from 0 instead of 1. * Sandbox preview: clear history when navigating to a new URL or refreshing. * Sandbox preview: reset history when forking a sandbox. * Removed debug logging.
1 parent 0be37da commit b1a4ee0

File tree

2 files changed

+138
-40
lines changed

2 files changed

+138
-40
lines changed

src/app/components/sandbox/Preview/index.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default class Preview extends React.PureComponent<Props, State> {
6565
this.state = {
6666
frameInitialized: false,
6767
history: [],
68-
historyPosition: 0,
68+
historyPosition: -1,
6969
urlInAddressBar: frameUrl(props.sandboxId, props.initialPath || ''),
7070
url: null,
7171
};
@@ -86,16 +86,24 @@ export default class Preview extends React.PureComponent<Props, State> {
8686
noDelay: false,
8787
};
8888

89+
componentWillReceiveProps(nextProps: Props) {
90+
if (nextProps.sandboxId !== this.props.sandboxId) {
91+
const url = frameUrl(nextProps.sandboxId, this.initialPath);
92+
this.setState({
93+
history: [url],
94+
historyPosition: 0,
95+
urlInAddressBar: url,
96+
});
97+
}
98+
}
99+
89100
componentDidUpdate(prevProps: Props) {
90101
if (prevProps.isInProjectView !== this.props.isInProjectView) {
91102
this.executeCodeImmediately();
92103
return;
93104
}
94105

95-
if (prevProps.sandboxId !== this.props.sandboxId) {
96-
this.executeCodeImmediately();
97-
return;
98-
} else if (prevProps.forcedRenders !== this.props.forcedRenders) {
106+
if (prevProps.forcedRenders !== this.props.forcedRenders) {
99107
this.executeCodeImmediately();
100108
return;
101109
}
@@ -269,16 +277,23 @@ export default class Preview extends React.PureComponent<Props, State> {
269277

270278
document.getElementById('sandbox').src = urlInAddressBar;
271279

272-
this.commitUrl(urlInAddressBar);
280+
this.setState({
281+
history: [urlInAddressBar],
282+
historyPosition: 0,
283+
urlInAddressBar,
284+
});
273285
};
274286

275287
handleRefresh = () => {
276288
const { history, historyPosition } = this.state;
289+
const url = history[historyPosition];
277290

278-
document.getElementById('sandbox').src = history[historyPosition];
291+
document.getElementById('sandbox').src = url;
279292

280293
this.setState({
281-
urlInAddressBar: history[historyPosition],
294+
history: [url],
295+
historyPosition: 0,
296+
urlInAddressBar: url,
282297
});
283298
};
284299

@@ -347,7 +362,7 @@ export default class Preview extends React.PureComponent<Props, State> {
347362
url={decodeURIComponent(url)}
348363
onChange={this.updateUrl}
349364
onConfirm={this.sendUrl}
350-
onBack={historyPosition > 1 ? this.handleBack : null}
365+
onBack={historyPosition > 0 ? this.handleBack : null}
351366
onForward={
352367
historyPosition < history.length - 1 ? this.handleForward : null
353368
}

src/sandbox/url-listeners.js

Lines changed: 114 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { dispatch } from 'codesandbox-api';
1+
import { dispatch, isStandalone } from 'codesandbox-api';
22

33
function sendUrlChange(url: string) {
44
dispatch({
@@ -7,40 +7,123 @@ function sendUrlChange(url: string) {
77
});
88
}
99

10+
/* eslint-disable no-console */
11+
12+
const origHistoryProto = window.history.__proto__; // eslint-disable-line no-proto
13+
const historyList = [];
14+
let historyPosition = -1;
15+
let disableNextHashChange = false;
16+
17+
function pushHistory(url, state) {
18+
if (historyPosition === -1 || historyList[historyPosition].url !== url) {
19+
historyPosition += 1;
20+
historyList.length = historyPosition + 1;
21+
historyList[historyPosition] = { url, state };
22+
}
23+
}
24+
25+
function pathWithHash(location) {
26+
return `${location.pathname}${location.hash}`;
27+
}
28+
1029
export default function setupHistoryListeners() {
11-
const pushState = window.history.pushState;
12-
window.history.pushState = function(state) {
13-
if (typeof history.onpushstate === 'function') {
14-
window.history.onpushstate({ state });
15-
}
16-
// ... whatever else you want to do
17-
// maybe call onhashchange e.handler
18-
return pushState.apply(window.history, arguments);
19-
};
20-
21-
const replaceState = window.history.replaceState;
22-
window.history.replaceState = function(state) {
23-
if (typeof history.onpushstate === 'function') {
24-
window.history.onpushstate({ state });
25-
}
26-
// ... whatever else you want to do
27-
// maybe call onhashchange e.handler
28-
return replaceState.apply(window.history, arguments);
29-
};
30-
31-
history.onpushstate = e => {
32-
setTimeout(() => {
33-
sendUrlChange(document.location.href);
30+
if (!isStandalone) {
31+
Object.assign(window.history, {
32+
go(delta) {
33+
console.log(`go(${delta})`);
34+
const newPos = historyPosition + delta;
35+
if (newPos >= 0 && newPos <= historyList.length - 1) {
36+
historyPosition = newPos;
37+
const { url, state } = historyList[historyPosition];
38+
const oldURL = document.location.href;
39+
origHistoryProto.replaceState.call(window.history, state, '', url);
40+
const newURL = document.location.href;
41+
if (newURL.indexOf('#') === -1) {
42+
window.dispatchEvent(new PopStateEvent('popstate', { state }));
43+
} else {
44+
disableNextHashChange = true;
45+
window.dispatchEvent(
46+
new HashChangeEvent('hashchange', { oldURL, newURL })
47+
);
48+
}
49+
}
50+
},
51+
52+
back() {
53+
console.log('back()');
54+
window.history.go(-1);
55+
},
56+
57+
forward() {
58+
console.log('forward()');
59+
window.history.go(1);
60+
},
61+
62+
pushState(state, title, url) {
63+
origHistoryProto.replaceState.call(window.history, state, title, url);
64+
pushHistory(url, state);
65+
sendUrlChange(document.location.href);
66+
},
67+
68+
replaceState(state, title, url) {
69+
origHistoryProto.replaceState.call(window.history, state, title, url);
70+
historyList[historyPosition] = { state, url };
71+
sendUrlChange(document.location.href);
72+
},
73+
});
74+
75+
Object.defineProperties(window.history, {
76+
length: {
77+
get() {
78+
return historyList.length;
79+
},
80+
},
81+
82+
state: {
83+
get() {
84+
return historyList[historyPosition].state;
85+
},
86+
},
87+
});
88+
89+
window.addEventListener('hashchange', () => {
90+
if (!disableNextHashChange) {
91+
const url = pathWithHash(document.location);
92+
pushHistory(url, null);
93+
sendUrlChange(document.location.href);
94+
} else {
95+
disableNextHashChange = false;
96+
}
3497
});
35-
};
3698

37-
history.onreplacestate = e => {
99+
document.addEventListener(
100+
'click',
101+
ev => {
102+
const el = ev.target;
103+
if (el.nodeName === 'A' && el.href.indexOf('#') !== -1) {
104+
const url = el.href;
105+
const oldURL = document.location.href;
106+
origHistoryProto.replaceState.call(window.history, null, '', url);
107+
const newURL = document.location.href;
108+
if (oldURL !== newURL) {
109+
disableNextHashChange = true;
110+
window.dispatchEvent(
111+
new HashChangeEvent('hashchange', { oldURL, newURL })
112+
);
113+
pushHistory(pathWithHash(document.location), null);
114+
sendUrlChange(document.location.href);
115+
}
116+
ev.preventDefault();
117+
ev.stopPropagation();
118+
}
119+
},
120+
true
121+
);
122+
123+
pushHistory(pathWithHash(document.location), null);
124+
38125
setTimeout(() => {
39126
sendUrlChange(document.location.href);
40127
});
41-
};
42-
43-
setTimeout(() => {
44-
sendUrlChange(document.location.href);
45-
});
128+
}
46129
}

0 commit comments

Comments
 (0)