Skip to content

Commit 46dda23

Browse files
chore: cleanup code (sveltejs#189)
* create state writable, split out adapter stuff into output component * move filetree into slug route * move solution/constraints into state * move completion into derived store * fix select/completion * wording * move scope into state, cleanup * move icons * code style * possible tweaks (sveltejs#192) * fixes Co-authored-by: Rich Harris <[email protected]>
1 parent 6333fd7 commit 46dda23

15 files changed

+532
-499
lines changed
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/routes/tutorial/[slug]/+page.svelte

Lines changed: 17 additions & 403 deletions
Large diffs are not rendered by default.

src/routes/tutorial/[slug]/Chrome.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script>
22
import { createEventDispatcher } from 'svelte';
3-
import chevron from './chevron.svg';
4-
import refresh from './refresh.svg';
3+
import chevron from '$lib/icons/chevron.svg';
4+
import refresh from '$lib/icons/refresh.svg';
55
66
/** @type {string} */
77
export let path;

src/routes/tutorial/[slug]/Editor.svelte

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
import { dev } from '$app/environment';
3-
import { createEventDispatcher, onMount } from 'svelte';
3+
import { onMount } from 'svelte';
4+
import { stubs, selected, state } from './state.js';
45
56
/**
67
* file extension -> monaco language
@@ -18,14 +19,8 @@
1819
* */
1920
const models = new Map();
2021
21-
/** @type {import('$lib/types').Stub[]} */
22-
export let stubs;
23-
/** @type {import('$lib/types').Stub | null} */
24-
export let selected = null;
2522
export let read_only = false;
2623
27-
const dispatch = createEventDispatcher();
28-
2924
/** @type {HTMLDivElement} */
3025
let container;
3126
@@ -35,7 +30,7 @@
3530
let w = 0;
3631
let h = 0;
3732
38-
/**
33+
/**
3934
* The iframe sometimes takes focus control in ways we can't prevent
4035
* while the editor is focussed. Refocus the editor in these cases.
4136
* This boolean tracks whether or not the editor should be refocused.
@@ -209,7 +204,7 @@
209204
210205
if (notify) {
211206
stub.contents = contents;
212-
dispatch('change', stub);
207+
state.update_file(stub);
213208
}
214209
});
215210
@@ -225,15 +220,15 @@
225220
}
226221
227222
$: if (instance) {
228-
instance.update_files(stubs);
223+
instance.update_files($stubs);
229224
}
230225
231226
$: if (instance) {
232227
instance.editor.updateOptions({ readOnly: read_only });
233228
}
234229
235-
$: if (instance && stubs /* to retrigger on stubs change */) {
236-
const model = selected && models.get(selected.name);
230+
$: if (instance && $stubs /* to retrigger on stubs change */) {
231+
const model = $selected && models.get($selected.name);
237232
instance.editor.setModel(model ?? null);
238233
}
239234
@@ -263,7 +258,7 @@
263258
// because else navigations inside the iframe refocus the editor.
264259
remove_focus_timeout = setTimeout(() => {
265260
preserve_focus = false;
266-
}, 500)
261+
}, 500);
267262
}}
268263
/>
269264
</div>

src/routes/tutorial/[slug]/Menu.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script>
22
import { page } from '$app/stores';
33
import { slide } from 'svelte/transition';
4-
import arrow from './arrow.svg';
4+
import arrow from '$lib/icons/arrow.svg';
55
66
import Icon from '@sveltejs/site-kit/components/Icon.svelte';
77
import { browser } from '$app/environment';
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
<script>
2+
import { afterNavigate } from '$app/navigation';
3+
import { onMount } from 'svelte';
4+
import { browser, dev } from '$app/environment';
5+
import Chrome from './Chrome.svelte';
6+
import Loading from './Loading.svelte';
7+
import { create_adapter } from './adapter';
8+
import { state } from './state';
9+
10+
/** @type {string} */
11+
export let path;
12+
13+
/** @type {HTMLIFrameElement} */
14+
let iframe;
15+
let loading = true;
16+
let initial = true;
17+
18+
/** @type {Error | null} */
19+
let error = null;
20+
21+
let progress = 0;
22+
let status = 'initialising';
23+
24+
/** @type {import('$lib/types').Adapter} Will be defined after first afterNavigate */
25+
let adapter;
26+
/** @type {string[]} */
27+
let history_bwd = [];
28+
/** @type {string[]} */
29+
let history_fwd = [];
30+
let ignore_path_change = false;
31+
32+
function reset_history() {
33+
history_bwd = [];
34+
history_fwd = [];
35+
}
36+
37+
onMount(() => {
38+
const unsub = state.subscribe(async (state) => {
39+
if (state.status === 'set' || state.status === 'switch') {
40+
loading = true;
41+
42+
try {
43+
clearTimeout(timeout);
44+
await reset_adapter(state);
45+
initial = false;
46+
} catch (e) {
47+
error = /** @type {Error} */ (e);
48+
console.error(e);
49+
}
50+
51+
loading = false;
52+
} else if (state.status === 'update' && state.last_updated) {
53+
const reload = await adapter.update([state.last_updated]);
54+
if (reload === true) {
55+
schedule_iframe_reload();
56+
}
57+
}
58+
});
59+
60+
function on_iframe_load() {
61+
iframe.classList.add('loaded');
62+
}
63+
function destroy() {
64+
iframe.removeEventListener('load', on_iframe_load);
65+
unsub();
66+
if (adapter) {
67+
adapter.destroy();
68+
}
69+
}
70+
71+
document.addEventListener('pagehide', destroy);
72+
iframe.addEventListener('load', on_iframe_load);
73+
return destroy;
74+
});
75+
76+
afterNavigate(() => {
77+
clearTimeout(timeout);
78+
reset_history();
79+
});
80+
81+
/**
82+
* Loads the adapter initially or resets it. This method can throw.
83+
* @param {import('./state').State} state
84+
*/
85+
async function reset_adapter(state) {
86+
let reload_iframe = true;
87+
if (adapter) {
88+
const result = await adapter.reset(state.stubs);
89+
if (result === 'cancelled') {
90+
return;
91+
} else {
92+
reload_iframe = result || state.status === 'switch';
93+
}
94+
} else {
95+
const _adapter = create_adapter(state.stubs, (p, s) => {
96+
progress = p;
97+
status = s;
98+
});
99+
adapter = _adapter;
100+
await _adapter.init;
101+
102+
set_iframe_src(adapter.base + path);
103+
}
104+
105+
await new Promise((fulfil, reject) => {
106+
let called = false;
107+
108+
window.addEventListener('message', function handler(e) {
109+
if (e.origin !== adapter.base) return;
110+
if (e.data.type === 'ping') {
111+
window.removeEventListener('message', handler);
112+
called = true;
113+
fulfil(undefined);
114+
}
115+
});
116+
117+
setTimeout(() => {
118+
if (!called) {
119+
// Updating the iframe too soon sometimes results in a blank screen,
120+
// so we try again after a short delay if we haven't heard back
121+
set_iframe_src(adapter.base + path);
122+
}
123+
}, 5000);
124+
125+
setTimeout(() => {
126+
if (!called) {
127+
reject(new Error('Timed out (re)setting adapter'));
128+
}
129+
}, 10000);
130+
});
131+
132+
if (reload_iframe) {
133+
await new Promise((fulfil) => setTimeout(fulfil, 200));
134+
set_iframe_src(adapter.base + path);
135+
}
136+
137+
return adapter;
138+
}
139+
140+
/** @type {any} */
141+
let reload_timeout;
142+
function schedule_iframe_reload() {
143+
clearTimeout(reload_timeout);
144+
reload_timeout = setTimeout(() => {
145+
set_iframe_src(adapter.base + path);
146+
}, 1000);
147+
}
148+
149+
/** @type {any} */
150+
let timeout;
151+
152+
/** @param {MessageEvent} e */
153+
async function handle_message(e) {
154+
if (!adapter) return;
155+
if (e.origin !== adapter.base) return;
156+
157+
if (e.data.type === 'ping') {
158+
const new_path = e.data.data.path ?? path;
159+
if (path !== new_path) {
160+
// skip `nav_to` step if triggered by bwd/fwd action
161+
if (ignore_path_change) {
162+
ignore_path_change = false;
163+
} else {
164+
nav_to();
165+
}
166+
path = new_path;
167+
}
168+
169+
clearTimeout(timeout);
170+
timeout = setTimeout(() => {
171+
if (dev && !iframe) return;
172+
173+
// we lost contact, refresh the page
174+
loading = true;
175+
set_iframe_src(adapter.base + path);
176+
loading = false;
177+
}, 1000);
178+
} else if (e.data.type === 'ping-pause') {
179+
clearTimeout(timeout);
180+
}
181+
}
182+
183+
/** @param {string} src */
184+
function set_iframe_src(src) {
185+
// removing the iframe from the document allows us to
186+
// change the src without adding a history entry, which
187+
// would make back/forward traversal very annoying
188+
const parentNode = /** @type {HTMLElement} */ (iframe.parentNode);
189+
iframe.classList.remove('loaded');
190+
parentNode?.removeChild(iframe);
191+
iframe.src = src;
192+
parentNode?.appendChild(iframe);
193+
}
194+
195+
/** @param {string} path */
196+
function route_to(path) {
197+
const url = new URL(path, adapter.base);
198+
path = url.pathname + url.search + url.hash;
199+
set_iframe_src(adapter.base + path);
200+
}
201+
202+
/** @param {string | null} new_path */
203+
function nav_to(new_path = null) {
204+
if (path !== history_bwd[history_bwd.length - 1]) {
205+
history_bwd = [...history_bwd, path];
206+
}
207+
history_fwd = [];
208+
if (new_path) route_to(new_path);
209+
}
210+
211+
function go_bwd() {
212+
const new_path = history_bwd[history_bwd.length - 1];
213+
if (new_path) {
214+
ignore_path_change = true;
215+
[history_bwd, history_fwd] = [history_bwd.slice(0, -1), [path, ...history_fwd]];
216+
route_to(new_path);
217+
}
218+
}
219+
220+
function go_fwd() {
221+
const new_path = history_fwd[0];
222+
if (new_path) {
223+
ignore_path_change = true;
224+
[history_bwd, history_fwd] = [[...history_bwd, path], history_fwd.slice(1)];
225+
route_to(new_path);
226+
}
227+
}
228+
</script>
229+
230+
<svelte:window on:message={handle_message} />
231+
<Chrome
232+
{history_bwd}
233+
{history_fwd}
234+
{path}
235+
{loading}
236+
on:refresh={() => {
237+
set_iframe_src(adapter.base + path);
238+
}}
239+
on:change={(e) => nav_to(e.detail.value)}
240+
on:back={go_bwd}
241+
on:forward={go_fwd}
242+
/>
243+
244+
<div class="content">
245+
{#if browser}
246+
<iframe bind:this={iframe} title="Output" />
247+
{/if}
248+
249+
{#if loading || error}
250+
<Loading {initial} {error} {progress} {status} />
251+
{/if}
252+
</div>
253+
254+
<style>
255+
.content {
256+
display: flex;
257+
flex-direction: column;
258+
position: relative;
259+
min-height: 0;
260+
height: 100%;
261+
max-height: 100%;
262+
background: var(--sk-back-2);
263+
--menu-width: 5.4rem;
264+
}
265+
266+
iframe {
267+
width: 100%;
268+
height: 100%;
269+
flex: 1;
270+
resize: none;
271+
box-sizing: border-box;
272+
border: none;
273+
background: var(--sk-back-2);
274+
}
275+
276+
iframe:not(.loaded) {
277+
display: none;
278+
}
279+
</style>

0 commit comments

Comments
 (0)