mirror of https://github.com/sveltejs/svelte
parent
3200352bc4
commit
e57637b3ca
@ -1,5 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
yarn-error.log
|
|
||||||
/cypress/screenshots/
|
|
||||||
/__sapper__/
|
|
@ -0,0 +1,286 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import ExampleSelector from './ExampleSelector.html';
|
||||||
|
import UserMenu from './UserMenu.html';
|
||||||
|
import Icon from '../../../components/icon.html';
|
||||||
|
import * as doNotZip from 'do-not-zip';
|
||||||
|
import downloadBlob from '../_utils/downloadBlob.js';
|
||||||
|
import { user } from '../../../user.js';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let examples;
|
||||||
|
export let gist;
|
||||||
|
export let name;
|
||||||
|
export let components;
|
||||||
|
export let json5;
|
||||||
|
export let zen_mode;
|
||||||
|
export let bundle;
|
||||||
|
|
||||||
|
let saving = false;
|
||||||
|
let downloading = false;
|
||||||
|
let justSaved = false;
|
||||||
|
let justForked = false;
|
||||||
|
|
||||||
|
const isMac = typeof navigator !== 'undefined' && navigator.platform === 'MacIntel';
|
||||||
|
|
||||||
|
function wait(ms) {
|
||||||
|
return new Promise(f => setTimeout(f, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
let canSave;
|
||||||
|
$: canSave = !!$user && !!gist && $user.id == gist.owner.id; // comparing number and string
|
||||||
|
|
||||||
|
function handleKeydown(event) {
|
||||||
|
if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const loginWindow = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
|
||||||
|
|
||||||
|
const handleLogin = event => {
|
||||||
|
loginWindow.close();
|
||||||
|
user.set(event.data.user);
|
||||||
|
window.removeEventListener('message', handleLogin);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fork(intentWasSave) {
|
||||||
|
saving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`gist/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
components,
|
||||||
|
json5
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (r.status < 200 || r.status >= 300) {
|
||||||
|
const { error } = await r.json();
|
||||||
|
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gist = await r.json();
|
||||||
|
dispatch('forked', { gist });
|
||||||
|
|
||||||
|
if (intentWasSave) {
|
||||||
|
justSaved = true;
|
||||||
|
await wait(600);
|
||||||
|
justSaved = false;
|
||||||
|
} else {
|
||||||
|
justForked = true;
|
||||||
|
await wait(600);
|
||||||
|
justForked = false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (navigator.onLine) {
|
||||||
|
alert(err.message);
|
||||||
|
} else {
|
||||||
|
alert(`It looks like you're offline! Find the internet and try again`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (saving) return;
|
||||||
|
|
||||||
|
if (!canSave) {
|
||||||
|
fork(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = {};
|
||||||
|
|
||||||
|
// null out any deleted files
|
||||||
|
const set = new Set(components.map(m => `${m.name}.${m.type}`));
|
||||||
|
Object.keys(gist.files).forEach(file => {
|
||||||
|
if (/\.(html|js)$/.test(file)) {
|
||||||
|
if (!set.has(file)) files[file] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
components.forEach(module => {
|
||||||
|
const file = `${module.name}.${module.type}`;
|
||||||
|
if (!module.source.trim()) {
|
||||||
|
throw new Error(`GitHub does not allow saving gists with empty files - ${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gist.files[files] || module.source !== gist.files[file].content) {
|
||||||
|
files[file] = { content: module.source };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!gist.files['data.json5'] || json5 !== gist.files['data.json5'].content) {
|
||||||
|
files['data.json5'] = { content: json5 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// data.json has been deprecated in favour of data.json5
|
||||||
|
if (gist.files['data.json']) gist.files['data.json'] = null;
|
||||||
|
|
||||||
|
const r = await fetch(`gist/${gist.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
description: name,
|
||||||
|
files
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (r.status < 200 || r.status >= 300) {
|
||||||
|
const { error } = await r.json();
|
||||||
|
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await r.json();
|
||||||
|
|
||||||
|
justSaved = true;
|
||||||
|
await wait(600);
|
||||||
|
justSaved = false;
|
||||||
|
} catch (err) {
|
||||||
|
if (navigator.onLine) {
|
||||||
|
alert(err.message);
|
||||||
|
} else {
|
||||||
|
alert(`It looks like you're offline! Find the internet and try again`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
downloading = true;
|
||||||
|
|
||||||
|
const files = await (await fetch('/svelte-app.json')).json();
|
||||||
|
|
||||||
|
if (bundle.imports.length > 0) {
|
||||||
|
const idx = files.findIndex(({ path }) => path === 'package.json');
|
||||||
|
const pkg = JSON.parse(files[idx].data);
|
||||||
|
const deps = {};
|
||||||
|
bundle.imports.forEach(mod => {
|
||||||
|
const match = /^(@[^\/]+\/)?[^@\/]+/.exec(mod);
|
||||||
|
deps[match[0]] = 'latest';
|
||||||
|
});
|
||||||
|
pkg.dependencies = deps;
|
||||||
|
files[idx].data = JSON.stringify(pkg, null, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(...components.map(component => ({ path: `src/${component.name}.${component.type}`, data: component.source })));
|
||||||
|
files.push({
|
||||||
|
path: `src/main.js`, data: `import App from './App.html';
|
||||||
|
|
||||||
|
var app = new App({
|
||||||
|
target: document.body,
|
||||||
|
props: ${JSON.stringify(data, null, '\t').replace(/\n/g, '\n\t')}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;` });
|
||||||
|
|
||||||
|
downloadBlob(doNotZip.toBlob(files), 'svelte-app.zip');
|
||||||
|
|
||||||
|
downloading = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
|
<div class="app-controls">
|
||||||
|
<div class="icon" style="position: relative; width: 3.2rem; margin-top:-.5rem">
|
||||||
|
<Icon name="menu" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ExampleSelector {examples} bind:name on:select />
|
||||||
|
|
||||||
|
<div style="flex: 1 0 auto" />
|
||||||
|
<div style="text-align: right; margin-right:.8rem">
|
||||||
|
{#if $user}
|
||||||
|
<UserMenu />
|
||||||
|
{:else}
|
||||||
|
<a class="icon" on:click={login} href="auth/login" title="Login to save">
|
||||||
|
<Icon name="log-in" />
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button class="icon" on:click="{() => zen_mode = !zen_mode}" title="fullscreen editor">
|
||||||
|
{#if zen_mode}
|
||||||
|
<Icon name="close" />
|
||||||
|
{:else}
|
||||||
|
<Icon name="maximize" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="icon" disabled={downloading} on:click={download} title="download zip file">
|
||||||
|
<Icon name="download" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if $user}
|
||||||
|
<button class="icon" disabled="{saving || !$user}" on:click={fork} title="fork">
|
||||||
|
{#if justForked}
|
||||||
|
<Icon name="check" />
|
||||||
|
{:else}
|
||||||
|
<Icon name="git-branch" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="icon" disabled="{saving || !$user}" on:click={save} title="save">
|
||||||
|
{#if justSaved}
|
||||||
|
<Icon name="check" />
|
||||||
|
{:else}
|
||||||
|
<Icon name="save" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if gist}
|
||||||
|
<a class="icon" href={gist.html_url} title="link to gist">
|
||||||
|
<Icon name="save" />
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--app-controls-h);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: .6rem var(--side-nav);
|
||||||
|
background-color: var(--second);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding: 0 .8rem;
|
||||||
|
opacity: .5;
|
||||||
|
transition: opacity .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:hover { opacity: 1 }
|
||||||
|
.icon:disabled { opacity: .3 }
|
||||||
|
|
||||||
|
.icon[title^='fullscreen'] { display: none }
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.icon[title^='fullscreen'] { display: inline }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,224 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount, beforeUpdate, createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let mode;
|
||||||
|
export let code;
|
||||||
|
export let readonly;
|
||||||
|
export let error;
|
||||||
|
export let errorLoc;
|
||||||
|
export let warningCount = 0;
|
||||||
|
|
||||||
|
let w;
|
||||||
|
let h;
|
||||||
|
|
||||||
|
export function resize() {
|
||||||
|
editor.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const modes = {
|
||||||
|
json: {
|
||||||
|
name: 'javascript',
|
||||||
|
json: true
|
||||||
|
},
|
||||||
|
handlebars: {
|
||||||
|
name: 'handlebars',
|
||||||
|
base: 'text/html'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refs = {};
|
||||||
|
let CodeMirror;
|
||||||
|
let editor;
|
||||||
|
let updating = false;
|
||||||
|
let marker;
|
||||||
|
let error_line;
|
||||||
|
let destroyed = false;
|
||||||
|
|
||||||
|
$: if (CodeMirror) {
|
||||||
|
createEditor(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (editor && !updating && code != null) {
|
||||||
|
updating = true;
|
||||||
|
editor.setValue(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (editor && w && h) {
|
||||||
|
editor.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (marker) marker.clear();
|
||||||
|
|
||||||
|
if (errorLoc) {
|
||||||
|
const line = errorLoc.line - 1;
|
||||||
|
const ch = errorLoc.column;
|
||||||
|
|
||||||
|
marker = editor.markText({ line, ch }, { line, ch: ch + 1 }, {
|
||||||
|
className: 'error-loc'
|
||||||
|
});
|
||||||
|
|
||||||
|
error_line = line;
|
||||||
|
} else {
|
||||||
|
error_line = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let previous_error_line;
|
||||||
|
$: if (editor) {
|
||||||
|
if (previous_error_line != null) {
|
||||||
|
editor.removeLineClass(previousLine, 'wrap', 'error-line')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error_line && (error_line !== previous_error_line)) {
|
||||||
|
editor.addLineClass(error_line, 'wrap', 'error-line');
|
||||||
|
previous_error_line = error_line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
import(/* webpackChunkName: "codemirror" */ './_codemirror.js').then(mod => {
|
||||||
|
CodeMirror = mod.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
destroyed = true;
|
||||||
|
if (editor) editor.toTextArea();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeUpdate(() => {
|
||||||
|
updating = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function createEditor(mode) {
|
||||||
|
if (destroyed) return;
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.toTextArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
editor = CodeMirror.fromTextArea(refs.editor, {
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
indentWithTabs: true,
|
||||||
|
indentUnit: 2,
|
||||||
|
tabSize: 2,
|
||||||
|
value: code,
|
||||||
|
mode: modes[mode] || {
|
||||||
|
name: mode
|
||||||
|
},
|
||||||
|
readOnly: readonly
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on('change', instance => {
|
||||||
|
if (!updating) {
|
||||||
|
updating = true;
|
||||||
|
code = instance.getValue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.refresh();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.codemirror-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-container :global(.CodeMirror) {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
background: var(--background);
|
||||||
|
font: 300 var(--code-fs)/1.7 var(--font-mono);
|
||||||
|
color: var(--base);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.codemirror-container {
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-container :global(.CodeMirror) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-container :global(.CodeMirror-gutters) {
|
||||||
|
padding: 0 1.6rem 0 .8rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-container .message {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2.4rem;
|
||||||
|
left: 2.4rem;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-container :global(.error-loc) {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 2px solid #da106e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-container :global(.error-line) {
|
||||||
|
background-color: rgba(200, 0, 0, .05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 2.4rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea { width: 100%; border: none }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
-----------------------------------------------
|
||||||
|
syntax-highlighting [prism]
|
||||||
|
NOTE
|
||||||
|
- just started to transfer colors from prism to codemirror
|
||||||
|
-----------------------------------------------
|
||||||
|
-->
|
||||||
|
<div class='codemirror-container' bind:offsetWidth={w} bind:offsetHeight={h}>
|
||||||
|
<textarea tabindex='2' bind:this={refs.editor}></textarea>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p class='error message'>
|
||||||
|
{#if error.loc}
|
||||||
|
<strong>
|
||||||
|
{#if error.filename}
|
||||||
|
<span
|
||||||
|
class='filename'
|
||||||
|
on:click="{() => dispatch('navigate', { filename: error.filename })}"
|
||||||
|
>{error.filename}</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
({error.loc.line}:{error.loc.column})
|
||||||
|
</strong>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{error.message}
|
||||||
|
</p>
|
||||||
|
{:elseif warningCount > 0}
|
||||||
|
<p class='warning message'>
|
||||||
|
Compiled, but with {warningCount} {warningCount === 1 ? 'warning' : 'warnings'} — check the console for details
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !CodeMirror}
|
||||||
|
<p class='loading'>loading editor...</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- TODO -->
|
||||||
|
<!-- {:catch err}
|
||||||
|
<p class='error'>error loading CodeMirror</p>
|
||||||
|
{/await} -->
|
@ -0,0 +1,77 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { enter } from './events.js';
|
||||||
|
|
||||||
|
export let examples;
|
||||||
|
export let name;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='select-wrapper'>
|
||||||
|
<select on:change="{e => dispatch('select', { slug: e.target.value })}">
|
||||||
|
<option value={null} disabled>Select an example</option>
|
||||||
|
|
||||||
|
{#each examples as group}
|
||||||
|
<optgroup label={group.name}>
|
||||||
|
{#each group.examples as example}
|
||||||
|
<option value={example.slug}>{example.title}</option>
|
||||||
|
{/each}
|
||||||
|
</optgroup>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class='visible'>
|
||||||
|
<span class='widther'>{name}</span>
|
||||||
|
<input bind:value={name} on:focus="{e => e.target.select()}" use:enter="{e => e.target.blur()}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin: 0 0 0 -4rem;
|
||||||
|
padding: 0 0 0 4.8rem;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
/* debug only */
|
||||||
|
/* background-color: rgba(221, 255, 205, .377); */
|
||||||
|
/* opacity: .5; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
position: relative;
|
||||||
|
padding: .5rem 1.6rem .5rem .4em;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
paddding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: .4em;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,206 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import Icon from '../../../../components/icon.html';
|
||||||
|
import { enter } from '../events.js';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let components;
|
||||||
|
export let selectedComponent;
|
||||||
|
|
||||||
|
// let previous_components;
|
||||||
|
|
||||||
|
// $: {
|
||||||
|
// // bit of a hack...
|
||||||
|
// if (components !== previous_components) {
|
||||||
|
// selectedComponent = components[0];
|
||||||
|
// previous_components = components;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
function selectComponent(component, selectedComponent) {
|
||||||
|
if (selectedComponent != component) {
|
||||||
|
selectedComponent.edit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('select', { component });
|
||||||
|
}
|
||||||
|
|
||||||
|
function editTab(component, selectedComponent) {
|
||||||
|
if (selectedComponent === component) {
|
||||||
|
selectedComponent.edit = true; // TODO can we make this local state?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEdit(selectedComponent) {
|
||||||
|
const match = /(.+)\.(html|js)$/.exec(selectedComponent.name);
|
||||||
|
selectedComponent.name = match ? match[1] : selectedComponent.name;
|
||||||
|
if (match && match[2]) selectedComponent.type = match[2];
|
||||||
|
selectedComponent.edit = false;
|
||||||
|
|
||||||
|
components = components; // TODO necessary?
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(component) {
|
||||||
|
let result = confirm(`Are you sure you want to delete ${component.name}.${component.type}?`);
|
||||||
|
if (result) dispatch('remove');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectInput(event) {
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.select();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.component-selector {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tabs {
|
||||||
|
border: none;
|
||||||
|
padding: 0 0 0 5rem;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tabs button {
|
||||||
|
position: relative;
|
||||||
|
font: 300 1.2rem/1.5 var(--font-ui);
|
||||||
|
color: var(--second);
|
||||||
|
border-bottom: var(--border-w) solid transparent;
|
||||||
|
padding: 1.2rem 1.2rem 0.8rem 0.5rem;
|
||||||
|
margin: 0 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tabs button.active {
|
||||||
|
border-bottom: var(--border-w) solid var(--prime);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable, .uneditable, .input-sizer, input {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-sizer {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
left: 0.5rem;
|
||||||
|
top: 1.2rem;
|
||||||
|
/* padding: 0 0.4rem; */
|
||||||
|
/* font-size: 1rem; */
|
||||||
|
font: 300 1.2rem/1.5 var(--font-ui);
|
||||||
|
border: none;
|
||||||
|
color: var(--flash);
|
||||||
|
outline: none;
|
||||||
|
line-height: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable {
|
||||||
|
/* margin-right: 2.4rem; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.uneditable {
|
||||||
|
/* padding-left: 1.2rem; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
right: .1rem;
|
||||||
|
top: .4rem;
|
||||||
|
width: 1.6rem;
|
||||||
|
text-align: right;
|
||||||
|
padding: 1.2em 0 1.2em .5em;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove:hover {
|
||||||
|
color: var(--flash);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tabs button.active .editable {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tabs button.active .remove {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-new {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 5rem;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-new:hover {
|
||||||
|
color: var(--flash);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- WUT -->
|
||||||
|
|
||||||
|
<div class="component-selector">
|
||||||
|
<div class="file-tabs" on:dblclick="{() => dispatch('create')}">
|
||||||
|
{#each components as component}
|
||||||
|
<button
|
||||||
|
id={component.name}
|
||||||
|
class:active="{component === selectedComponent}"
|
||||||
|
data-name={component.name}
|
||||||
|
on:click="{() => selectComponent(component, selectedComponent)}"
|
||||||
|
on:dblclick="{e => e.stopPropagation()}"
|
||||||
|
>
|
||||||
|
{#if component.name == 'App'}
|
||||||
|
<div class="uneditable">
|
||||||
|
App.html
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#if component.edit}
|
||||||
|
<span class="input-sizer">{component.name + (/\./.test(component.name) ? '' : '.html')}</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
autofocus
|
||||||
|
bind:value={component.name}
|
||||||
|
on:focus={selectInput}
|
||||||
|
on:blur="{() => closeEdit(selectedComponent)}"
|
||||||
|
use:enter="{e => e.target.blur()}"
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="editable"
|
||||||
|
title="edit component name"
|
||||||
|
on:click="{() => editTab(component, selectedComponent)}"
|
||||||
|
>
|
||||||
|
{component.name}.{component.type}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="remove" on:click="{() => remove(component)}">
|
||||||
|
<Icon name="close"/>
|
||||||
|
<!-- × -->
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="add-new" on:click="{() => dispatch('create')}" title="add new component">
|
||||||
|
<Icon name="plus" />
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
import CodeMirror from '../CodeMirror.html';
|
||||||
|
|
||||||
|
export let selectedComponent;
|
||||||
|
export let error;
|
||||||
|
export let errorLoc;
|
||||||
|
export let warningCount = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editor-wrapper {
|
||||||
|
z-index: 5;
|
||||||
|
background: var(--back-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.editor-wrapper {
|
||||||
|
/* make it easier to interact with scrollbar */
|
||||||
|
padding-right: 8px;
|
||||||
|
height: auto;
|
||||||
|
/* height: 100%; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
{#if selectedComponent}
|
||||||
|
<CodeMirror
|
||||||
|
mode="{selectedComponent.type === 'js' ? 'javascript' : 'handlebars'}"
|
||||||
|
bind:code={selectedComponent.source}
|
||||||
|
{error}
|
||||||
|
{errorLoc}
|
||||||
|
{warningCount}
|
||||||
|
on:navigate
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import ComponentSelector from './ComponentSelector.html';
|
||||||
|
import ModuleEditor from './ModuleEditor.html';
|
||||||
|
|
||||||
|
export let components;
|
||||||
|
export let selectedComponent;
|
||||||
|
export let sourceError;
|
||||||
|
export let sourceErrorLoc;
|
||||||
|
export let runtimeErrorLoc;
|
||||||
|
export let warningCount;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ComponentSelector
|
||||||
|
{components}
|
||||||
|
bind:selectedComponent
|
||||||
|
on:create
|
||||||
|
on:remove
|
||||||
|
on:select
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModuleEditor
|
||||||
|
bind:selectedComponent
|
||||||
|
error={sourceError}
|
||||||
|
errorLoc="{sourceErrorLoc || runtimeErrorLoc}"
|
||||||
|
{warningCount}
|
||||||
|
on:navigate
|
||||||
|
/>
|
@ -0,0 +1,386 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from 'svelte';
|
||||||
|
import getLocationFromStack from '../../_utils/getLocationFromStack.js';
|
||||||
|
import { decode } from 'sourcemap-codec';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let bundle;
|
||||||
|
export let dom;
|
||||||
|
export let ssr;
|
||||||
|
export let data;
|
||||||
|
export let sourceError;
|
||||||
|
export let error;
|
||||||
|
|
||||||
|
const refs = {};
|
||||||
|
const importCache = {};
|
||||||
|
let pendingImports = 0;
|
||||||
|
let pending = false;
|
||||||
|
|
||||||
|
function fetchImport(id, curl) {
|
||||||
|
return new Promise((fulfil, reject) => {
|
||||||
|
curl([`https://bundle.run/${id}`]).then(module => {
|
||||||
|
importCache[id] = module;
|
||||||
|
fulfil(module);
|
||||||
|
}, err => {
|
||||||
|
console.error(err.stack);
|
||||||
|
reject(new Error(`Error loading ${id} from bundle.run`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespaceSpecifier = /\*\s+as\s+(\w+)/;
|
||||||
|
const namedSpecifiers = /\{(.+)\}/;
|
||||||
|
|
||||||
|
function parseSpecifiers(specifiers) {
|
||||||
|
specifiers = specifiers.trim();
|
||||||
|
|
||||||
|
let match = namespaceSpecifier.exec(specifiers);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
namespace: true,
|
||||||
|
name: match[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let names = [];
|
||||||
|
|
||||||
|
specifiers = specifiers.replace(namedSpecifiers, (match, str) => {
|
||||||
|
names = str.split(',').map(name => {
|
||||||
|
const split = name.split('as');
|
||||||
|
const exported = split[0].trim();
|
||||||
|
const local = (split[1] || exported).trim();
|
||||||
|
|
||||||
|
return { local, exported };
|
||||||
|
});
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
match = /\w+/.exec(specifiers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
namespace: false,
|
||||||
|
names,
|
||||||
|
default: match ? match[0] : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let createComponent;
|
||||||
|
let init;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
refs.child.addEventListener('load', () => {
|
||||||
|
const iframe = refs.child;
|
||||||
|
const body = iframe.contentDocument.body;
|
||||||
|
const evalInIframe = iframe.contentWindow.eval;
|
||||||
|
|
||||||
|
// intercept links, so that we can use #hashes inside the iframe
|
||||||
|
body.addEventListener('click', event => {
|
||||||
|
if (event.which !== 1) return;
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
|
||||||
|
// ensure target is a link
|
||||||
|
let el = event.target;
|
||||||
|
while (el && el.nodeName !== 'A') el = el.parentNode;
|
||||||
|
if (!el || el.nodeName !== 'A') return;
|
||||||
|
|
||||||
|
if (el.hasAttribute('download') || el.getAttribute('rel') === 'external' || el.target) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (el.href.startsWith(top.location.href)) {
|
||||||
|
const hash = el.href.replace(top.location.href, '');
|
||||||
|
if (hash[0] === '#') {
|
||||||
|
iframe.contentWindow.location.hash = hash;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(el.href, '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
let promise = null;
|
||||||
|
let updating = false;
|
||||||
|
|
||||||
|
let toDestroy = null;
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
if (sourceError) return;
|
||||||
|
|
||||||
|
const imports = [];
|
||||||
|
|
||||||
|
const missingImports = bundle.imports.filter(x => !importCache[x]);
|
||||||
|
|
||||||
|
const removeStyles = () => {
|
||||||
|
const styles = iframe.contentDocument.querySelectorAll('style');
|
||||||
|
let i = styles.length;
|
||||||
|
while (i--) styles[i].parentNode.removeChild(styles[i]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ready = () => {
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
if (toDestroy) {
|
||||||
|
removeStyles();
|
||||||
|
|
||||||
|
toDestroy.$destroy();
|
||||||
|
toDestroy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle.imports.forEach(x => {
|
||||||
|
const module = importCache[x];
|
||||||
|
const name = bundle.importMap.get(x);
|
||||||
|
|
||||||
|
iframe.contentWindow[name] = module;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ssr) { // this only gets generated if component uses lifecycle hooks
|
||||||
|
pending = true;
|
||||||
|
createHtml();
|
||||||
|
} else {
|
||||||
|
pending = false;
|
||||||
|
createComponent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHtml = () => {
|
||||||
|
try {
|
||||||
|
evalInIframe(`${ssr.code}
|
||||||
|
var rendered = SvelteComponent.render(${JSON.stringify(data)});
|
||||||
|
|
||||||
|
if (rendered.css.code) {
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.textContent = rendered.css.code;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.innerHTML = rendered.html;
|
||||||
|
`)
|
||||||
|
} catch (e) {
|
||||||
|
const loc = getLocationFromStack(e.stack, ssr.map);
|
||||||
|
if (loc) {
|
||||||
|
e.filename = loc.source;
|
||||||
|
e.loc = { line: loc.line, column: loc.column };
|
||||||
|
}
|
||||||
|
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = () => {
|
||||||
|
// remove leftover styles from SSR renderer
|
||||||
|
if (ssr) removeStyles();
|
||||||
|
|
||||||
|
try {
|
||||||
|
evalInIframe(`${dom.code}
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
window.location.hash = '';
|
||||||
|
window._svelteTransitionManager = null;
|
||||||
|
|
||||||
|
var component = new SvelteComponent({
|
||||||
|
target: document.body,
|
||||||
|
data: ${JSON.stringify(data)}
|
||||||
|
});`);
|
||||||
|
|
||||||
|
component = window.app = window.component = iframe.contentWindow.component;
|
||||||
|
|
||||||
|
// component.on('state', ({ current }) => {
|
||||||
|
// if (updating) return;
|
||||||
|
// updating = true;
|
||||||
|
// this.fire('data', { current });
|
||||||
|
// updating = false;
|
||||||
|
// });
|
||||||
|
} catch (e) {
|
||||||
|
// TODO show in UI
|
||||||
|
component = null;
|
||||||
|
|
||||||
|
const loc = getLocationFromStack(e.stack, dom.map);
|
||||||
|
if (loc) {
|
||||||
|
e.filename = loc.source;
|
||||||
|
e.loc = { line: loc.line, column: loc.column };
|
||||||
|
}
|
||||||
|
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pendingImports = missingImports.length;
|
||||||
|
|
||||||
|
if (missingImports.length) {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
promise = Promise.all(
|
||||||
|
missingImports.map(id => fetchImport(id, iframe.contentWindow.curl).then(module => {
|
||||||
|
pendingImports -= 1;
|
||||||
|
return module;
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
promise.cancel = () => cancelled = true;
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
ready();
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
if (cancelled) return;
|
||||||
|
error = e;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
run = () => {
|
||||||
|
pending = false;
|
||||||
|
|
||||||
|
// TODO do we need to clear out SSR HTML?
|
||||||
|
createComponent();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_handler = bundle => {
|
||||||
|
if (!bundle) return; // TODO can this ever happen?
|
||||||
|
if (promise) promise.cancel();
|
||||||
|
|
||||||
|
toDestroy = component;
|
||||||
|
component = null;
|
||||||
|
|
||||||
|
if (data !== undefined) init();
|
||||||
|
};
|
||||||
|
|
||||||
|
data_handler = data => {
|
||||||
|
if (updating) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (component) {
|
||||||
|
error = null;
|
||||||
|
updating = true;
|
||||||
|
component.$set(data);
|
||||||
|
updating = false;
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const loc = getLocationFromStack(e.stack, bundle.map);
|
||||||
|
if (loc) {
|
||||||
|
e.filename = loc.source;
|
||||||
|
e.loc = { line: loc.line, column: loc.column };
|
||||||
|
}
|
||||||
|
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function noop(){}
|
||||||
|
let run = noop;
|
||||||
|
let bundle_handler = noop;
|
||||||
|
let data_handler = noop;
|
||||||
|
|
||||||
|
$: bundle_handler(bundle);
|
||||||
|
$: data_handler(data);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.iframe-container {
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh -3em);
|
||||||
|
/* height: calc(100vh - var(--nav-h)); */
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.iframe-container {
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.greyed-out {
|
||||||
|
filter: grayscale(50%) blur(1px);
|
||||||
|
opacity: .25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay p {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending button {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 6rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="iframe-container">
|
||||||
|
<iframe title="Result" bind:this={refs.child} class="{error || pending || pendingImports ? 'greyed-out' : ''}" srcdoc='
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/repl-viewer.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="/curl.js"></script>
|
||||||
|
<script>curl.config({ dontAddFileExt: /./ });</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overlay">
|
||||||
|
{#if error}
|
||||||
|
<p class="error message">
|
||||||
|
{#if error.loc}
|
||||||
|
<strong>
|
||||||
|
{#if error.filename}
|
||||||
|
<span class="filename" on:click="{() => dispatch('navigate', { filename: error.filename })}">{error.filename}</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
({error.loc.line}:{error.loc.column})
|
||||||
|
</strong>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{error.message}
|
||||||
|
</p>
|
||||||
|
{:elseif pending}
|
||||||
|
<div class="pending" on:click={run}>
|
||||||
|
<button class="bg-second white">Click to run</button>
|
||||||
|
</div>
|
||||||
|
{:elseif pendingImports}
|
||||||
|
<p class="info message">loading {pendingImports} {pendingImports === 1 ? 'dependency' : 'dependencies'} from
|
||||||
|
https://bundle.run</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -0,0 +1,101 @@
|
|||||||
|
<script>
|
||||||
|
import SplitPane from '../SplitPane.html';
|
||||||
|
import Viewer from './Viewer.html';
|
||||||
|
import CodeMirror from '../CodeMirror.html';
|
||||||
|
|
||||||
|
export let bundle
|
||||||
|
export let compiled;
|
||||||
|
export let dom;
|
||||||
|
export let ssr;
|
||||||
|
export let data;
|
||||||
|
export let json5;
|
||||||
|
export let sourceError;
|
||||||
|
export let runtimeError;
|
||||||
|
export let dataError;
|
||||||
|
export let dataErrorLoc;
|
||||||
|
|
||||||
|
let view = 'result';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.view-toggle {
|
||||||
|
height: var(--pane-controls-h);
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
/* width: 50%;
|
||||||
|
height: 100%; */
|
||||||
|
text-align: left;
|
||||||
|
font: 300 1.2rem/1.5 var(--font-ui);
|
||||||
|
border-bottom: var(--border-w) solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active {
|
||||||
|
border-bottom: var(--border-w) solid var(--prime);
|
||||||
|
color: var(--second);
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="view-toggle">
|
||||||
|
<button
|
||||||
|
class:active="{view === 'result'}"
|
||||||
|
on:click="{() => view = 'result'}"
|
||||||
|
>Result</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class:active="{view === 'code'}"
|
||||||
|
on:click="{() => view = 'code'}"
|
||||||
|
>Compiled code</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{#if view === 'result'}
|
||||||
|
<SplitPane type="vertical">
|
||||||
|
<div slot="a">
|
||||||
|
{#if bundle}
|
||||||
|
<Viewer
|
||||||
|
{bundle}
|
||||||
|
{dom}
|
||||||
|
{ssr}
|
||||||
|
{data}
|
||||||
|
{sourceError}
|
||||||
|
bind:error={runtimeError}
|
||||||
|
on:data="{e => updateData(e.detail.current)}"
|
||||||
|
on:navigate={navigate}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<p class="loading">loading Svelte compiler...</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section slot="b">
|
||||||
|
<CodeMirror
|
||||||
|
mode="json"
|
||||||
|
bind:code={json5}
|
||||||
|
error={dataError}
|
||||||
|
errorLoc={dataErrorLoc}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</SplitPane>
|
||||||
|
{:else}
|
||||||
|
<SplitPane type="vertical">
|
||||||
|
<div slot="a">
|
||||||
|
<CodeMirror
|
||||||
|
mode="javascript"
|
||||||
|
code={compiled}
|
||||||
|
error={sourceError}
|
||||||
|
errorLoc={sourceErrorLoc}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section slot="b">
|
||||||
|
compiler options go here
|
||||||
|
</section>
|
||||||
|
</SplitPane>
|
||||||
|
{/if}
|
@ -0,0 +1,272 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import * as fleece from 'golden-fleece';
|
||||||
|
import SplitPane from './SplitPane.html';
|
||||||
|
import CodeMirror from './CodeMirror.html';
|
||||||
|
import Input from './Input/index.html';
|
||||||
|
import Output from './Output/index.html';
|
||||||
|
|
||||||
|
export let version;
|
||||||
|
export let components;
|
||||||
|
export let selectedComponent;
|
||||||
|
export let data;
|
||||||
|
export let json5;
|
||||||
|
|
||||||
|
let bundle = null;
|
||||||
|
let dom;
|
||||||
|
let ssr;
|
||||||
|
let sourceError = null;
|
||||||
|
let runtimeError = null;
|
||||||
|
let dataError = null;
|
||||||
|
let dataErrorLoc = null;
|
||||||
|
let warningCount = 0;
|
||||||
|
let compiled = '';
|
||||||
|
let uid = 0;
|
||||||
|
|
||||||
|
let sourceErrorLoc, runtimeErrorLoc;
|
||||||
|
|
||||||
|
let worker;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
worker = new Worker('/repl-worker.js');
|
||||||
|
|
||||||
|
function listener(event) {
|
||||||
|
switch (event.data.type) {
|
||||||
|
case 'version':
|
||||||
|
version = event.data.version;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bundled':
|
||||||
|
({ bundle, dom, ssr, warningCount, error: sourceError } = event.data.result);
|
||||||
|
runtimeError = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'compiled':
|
||||||
|
compiled = event.data.result;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.addEventListener('message', listener);
|
||||||
|
|
||||||
|
worker.postMessage({ type: 'init', version });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
worker.removeEventListener('message', listener);
|
||||||
|
worker.terminate();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function createComponent() {
|
||||||
|
const newComponent = {
|
||||||
|
name: uid++ ? `Component${uid}` : 'Component1',
|
||||||
|
type: 'html',
|
||||||
|
source: '',
|
||||||
|
edit: true
|
||||||
|
};
|
||||||
|
|
||||||
|
components = components.concat(newComponent);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById(newComponent.name).scrollIntoView(false);
|
||||||
|
selectedComponent = newComponent;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeComponent() {
|
||||||
|
const component = selectedComponent;
|
||||||
|
|
||||||
|
if (component.name === 'App') {
|
||||||
|
// App.html can't be removed
|
||||||
|
component.source = '';
|
||||||
|
selectedComponent = component;
|
||||||
|
} else {
|
||||||
|
const index = components.indexOf(component);
|
||||||
|
if (~index) {
|
||||||
|
components = components.slice(0, index).concat(components.slice(index + 1));
|
||||||
|
} else {
|
||||||
|
console.error(`Could not find component! That's... odd`);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedComponent = components[index] || components[components.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateData(current) {
|
||||||
|
const data = fleece.evaluate(json5);
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
data[key] = current[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
json5 = fleece.patch(json5, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(filename) {
|
||||||
|
const name = filename.replace(/\.html$/, '');
|
||||||
|
|
||||||
|
if (selectedComponent.name === name) return;
|
||||||
|
selectedComponent = components.find(c => c.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (sourceError && selectedComponent) {
|
||||||
|
sourceErrorLoc = sourceError.filename === `${selectedComponent.name}.${selectedComponent.type}`
|
||||||
|
? sourceError.start
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (runtimeError && selectedComponent) {
|
||||||
|
runtimeErrorLoc = runtimeError.filename === `${selectedComponent.name}.${selectedComponent.type}`
|
||||||
|
? runtimeError.start
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: try {
|
||||||
|
data= fleece.evaluate(json5);
|
||||||
|
dataError = null;
|
||||||
|
dataErrorLoc = null;
|
||||||
|
} catch (err) {
|
||||||
|
dataError = err;
|
||||||
|
dataErrorLoc = err && err.loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (worker && components.length > 0) {
|
||||||
|
worker.postMessage({ type: 'bundle', components });
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_selected_component;
|
||||||
|
$: {
|
||||||
|
// slightly counterintuitively, we only want to rebundle if
|
||||||
|
// this is the *same* component — not if we've just selected
|
||||||
|
// a different one
|
||||||
|
// if (selectedComponent === last_selected_component) {
|
||||||
|
// if (worker && components.length > 0) {
|
||||||
|
// console.log(`2`, components);
|
||||||
|
// worker.postMessage({ type: 'bundle', components });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// recompile the currently selected component
|
||||||
|
if (selectedComponent) {
|
||||||
|
if (selectedComponent.type === 'html') {
|
||||||
|
worker.postMessage({ type: 'compile', component: selectedComponent });
|
||||||
|
} else {
|
||||||
|
compiled = selectedComponent.source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
last_selected_component = selectedComponent;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.repl-inner { height: 100% }
|
||||||
|
|
||||||
|
.repl-inner :global(section) {
|
||||||
|
position: relative;
|
||||||
|
padding: 4.2rem 0 0 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(section) > :global(*):first-child {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 4.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(section) > :global(*):last-child {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(.message) {
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--border-r);
|
||||||
|
margin: 0;
|
||||||
|
padding: 1.2rem 1.6rem 1.2rem 4.4rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
font: 300 1.2rem/1.7 var(--font-ui);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(.message::before) {
|
||||||
|
content: '!';
|
||||||
|
position: absolute;
|
||||||
|
left: 1.2rem;
|
||||||
|
top: 1.1rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
padding: .4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
border: .2rem solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(.error.message) {
|
||||||
|
background-color: #da106e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(.warning.message) {
|
||||||
|
background-color: #e47e0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(.info.message) {
|
||||||
|
background-color: var(--second);
|
||||||
|
animation: fade-in .4s .2s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-inner :global(.error) :global(.filename) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.show-if-mobile { display: none }
|
||||||
|
|
||||||
|
.repl-outer.zen-mode {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
section { height: 100% }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="repl-inner">
|
||||||
|
<SplitPane type="horizontal">
|
||||||
|
<section slot=a>
|
||||||
|
<Input
|
||||||
|
{components}
|
||||||
|
bind:selectedComponent
|
||||||
|
error={sourceError}
|
||||||
|
errorLoc="{sourceErrorLoc || runtimeErrorLoc}"
|
||||||
|
{warningCount}
|
||||||
|
on:create={createComponent}
|
||||||
|
on:remove={removeComponent}
|
||||||
|
on:select="{e => selectedComponent = e.detail.component}"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section slot=b style='height: 100%;'>
|
||||||
|
<Output
|
||||||
|
{compiled}
|
||||||
|
{bundle}
|
||||||
|
{ssr}
|
||||||
|
{dom}
|
||||||
|
{data}
|
||||||
|
{json5}
|
||||||
|
{sourceError}
|
||||||
|
{runtimeError}
|
||||||
|
{dataError}
|
||||||
|
{dataErrorLoc}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</SplitPane>
|
||||||
|
</div>
|
@ -0,0 +1,160 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let type;
|
||||||
|
|
||||||
|
const refs = {};
|
||||||
|
const side = type === 'horizontal' ? 'left' : 'top';
|
||||||
|
const dimension = type === 'horizontal' ? 'width' : 'height';
|
||||||
|
|
||||||
|
let pos = 50;
|
||||||
|
let dragging = false;
|
||||||
|
|
||||||
|
function setPos(event) {
|
||||||
|
const { top, bottom, left, right } = refs.container.getBoundingClientRect();
|
||||||
|
|
||||||
|
pos = 100 * (type === 'vertical'
|
||||||
|
? (event.clientY - top) / (bottom - top)
|
||||||
|
: (event.clientX - left) / (right - left));
|
||||||
|
|
||||||
|
dispatch('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(node, callback) {
|
||||||
|
const mousedown = event => {
|
||||||
|
if (event.which !== 1) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
dragging = true;
|
||||||
|
|
||||||
|
const onmouseup = () => {
|
||||||
|
dragging = false;
|
||||||
|
|
||||||
|
window.removeEventListener('mousemove', callback, false);
|
||||||
|
window.removeEventListener('mouseup', onmouseup, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', callback, false);
|
||||||
|
window.addEventListener('mouseup', onmouseup, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener('mousedown', mousedown, false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
node.removeEventListener('mousedown', onmousedown, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mousecatcher {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255,255,255,.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
/* background-color: #eee; */
|
||||||
|
background-color: var(--second);
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
|
padding: 0 8px;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal::after {
|
||||||
|
left: 8px;
|
||||||
|
top: 0;
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical {
|
||||||
|
padding: 8px 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical::after {
|
||||||
|
top: 8px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.pane {
|
||||||
|
/* override divider-set dimensions */
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.left, .right, .divider {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.left, .right {
|
||||||
|
height: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top, .bottom {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top { top: 0; }
|
||||||
|
.bottom { bottom: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container" bind:this={refs.container}>
|
||||||
|
<div class="pane" style="{dimension}: {pos}%;">
|
||||||
|
<slot name="a"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pane" style="{dimension}: {100 - pos}%;">
|
||||||
|
<slot name="b"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="{type} divider" style="{side}: calc({pos}% - 8px)" use:drag={setPos}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if dragging}
|
||||||
|
<div class="mousecatcher"></div>
|
||||||
|
{/if}
|
@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import { user, logout } from '../../../user.js';
|
||||||
|
|
||||||
|
let showMenu = false;
|
||||||
|
let name;
|
||||||
|
|
||||||
|
$: name = $user.displayName || $user.username;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}">
|
||||||
|
<span>{name}</span>
|
||||||
|
<img alt="{name} avatar" src="{$user.photo}">
|
||||||
|
|
||||||
|
{#if showMenu}
|
||||||
|
<div class="menu">
|
||||||
|
<button on:click={logout}>log out</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.user {
|
||||||
|
position: relative;
|
||||||
|
float: right;
|
||||||
|
display: block;
|
||||||
|
padding: .5em 2.2em 0 .8em;
|
||||||
|
line-height: 1;
|
||||||
|
z-index: 99;
|
||||||
|
height: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
/* position: relative; padding: 0 2em 0 0; */
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
padding: .05em 0;
|
||||||
|
font-family: Rajdhani;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
top: .2em;
|
||||||
|
right: 0;
|
||||||
|
width: 1.6em;
|
||||||
|
height: 1.6em;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
min-width: calc(100% + .1em);
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: .5em;
|
||||||
|
z-index: 99;
|
||||||
|
top: calc(2.5em - 1px);
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
border-left: 1px solid #eee;
|
||||||
|
right: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu button {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,10 @@
|
|||||||
|
const CodeMirror = require('codemirror');
|
||||||
|
require('./codemirror.css');
|
||||||
|
require('codemirror/mode/javascript/javascript.js');
|
||||||
|
require('codemirror/mode/shell/shell.js');
|
||||||
|
require('codemirror/mode/handlebars/handlebars.js');
|
||||||
|
require('codemirror/mode/htmlmixed/htmlmixed.js');
|
||||||
|
require('codemirror/mode/xml/xml.js');
|
||||||
|
require('codemirror/mode/css/css.js');
|
||||||
|
|
||||||
|
module.exports = CodeMirror;
|
@ -0,0 +1,350 @@
|
|||||||
|
/* BASICS */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
/* copied colors over from prism */
|
||||||
|
--background: var(--back-light);
|
||||||
|
--base: #9b978b;
|
||||||
|
--comment: #afbfcf;
|
||||||
|
--keyword: #5ba3d3;
|
||||||
|
--function: #db794b;
|
||||||
|
--string: #b69b61;
|
||||||
|
--number: #86af75;
|
||||||
|
--tags: var(--function);
|
||||||
|
--important: var(--string);
|
||||||
|
|
||||||
|
/* Set height, width, borders, and global font properties here */
|
||||||
|
/* see prism.css */
|
||||||
|
height: 300px;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PADDING */
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
padding: 4px 0; /* Vertical padding around content */
|
||||||
|
}
|
||||||
|
.CodeMirror pre {
|
||||||
|
padding: 0 4px; /* Horizontal padding of content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||||
|
background-color: white; /* The little square between H and V scrollbars */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GUTTER */
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
padding: 0 3px 0 5px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--comment);
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-guttermarker { color: black; }
|
||||||
|
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||||
|
|
||||||
|
/* CURSOR */
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: none;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
/* Shown when moving in bi-directional text */
|
||||||
|
.CodeMirror div.CodeMirror-secondarycursor {
|
||||||
|
border-left: 1px solid silver;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor .CodeMirror-cursor {
|
||||||
|
width: auto;
|
||||||
|
border: 0 !important;
|
||||||
|
background: #7e7;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor div.CodeMirror-cursors {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor-mark {
|
||||||
|
background-color: rgba(20, 255, 20, .5);
|
||||||
|
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||||
|
-moz-animation: blink 1.06s steps(1) infinite;
|
||||||
|
animation: blink 1.06s steps(1) infinite;
|
||||||
|
}
|
||||||
|
.cm-animate-fat-cursor {
|
||||||
|
width: auto;
|
||||||
|
border: 0;
|
||||||
|
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||||
|
-moz-animation: blink 1.06s steps(1) infinite;
|
||||||
|
animation: blink 1.06s steps(1) infinite;
|
||||||
|
background-color: #7e7;
|
||||||
|
}
|
||||||
|
@-moz-keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
@keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||||
|
|
||||||
|
.CodeMirror-rulers {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: -50px; bottom: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.CodeMirror-ruler {
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEFAULT THEME */
|
||||||
|
.cm-s-default .cm-header {color: blue}
|
||||||
|
.cm-s-default .cm-quote {color: #090}
|
||||||
|
.cm-negative {color: #d44}
|
||||||
|
.cm-positive {color: #292}
|
||||||
|
.cm-header, .cm-strong {font-weight: bold}
|
||||||
|
.cm-em {font-style: italic}
|
||||||
|
.cm-link {text-decoration: underline}
|
||||||
|
.cm-strikethrough {text-decoration: line-through}
|
||||||
|
|
||||||
|
.cm-s-default .cm-atom,
|
||||||
|
.cm-s-default .cm-def,
|
||||||
|
.cm-s-default .cm-property,
|
||||||
|
.cm-s-default .cm-variable-2,
|
||||||
|
.cm-s-default .cm-variable-3,
|
||||||
|
.cm-s-default .cm-punctuation {color: var(--base)}
|
||||||
|
.cm-s-default .cm-hr,
|
||||||
|
.cm-s-default .cm-comment {color: var(--comment)}
|
||||||
|
.cm-s-default .cm-attribute,
|
||||||
|
.cm-s-default .cm-keyword {color: var(--keyword)}
|
||||||
|
.cm-s-default .cm-variable,
|
||||||
|
.cm-s-default .cm-bracket,
|
||||||
|
.cm-s-default .cm-tag {color: var(--tags)}
|
||||||
|
.cm-s-default .cm-number {color: var(--number)}
|
||||||
|
.cm-s-default .cm-string {color: var(--string)}
|
||||||
|
|
||||||
|
.cm-s-default .cm-string-2 {color: #f50}
|
||||||
|
.cm-s-default .cm-type {color: #085}
|
||||||
|
.cm-s-default .cm-meta {color: #555}
|
||||||
|
.cm-s-default .cm-qualifier {color: #555}
|
||||||
|
.cm-s-default .cm-builtin {color: #30a}
|
||||||
|
.cm-s-default .cm-link {color: var(--flash)}
|
||||||
|
.cm-s-default .cm-error {color: #ff008c}
|
||||||
|
.cm-invalidchar {color: #ff008c}
|
||||||
|
|
||||||
|
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||||
|
|
||||||
|
/* Default styles for common addons */
|
||||||
|
|
||||||
|
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
|
||||||
|
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||||
|
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||||
|
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||||
|
|
||||||
|
/* STOP */
|
||||||
|
|
||||||
|
/* The rest of this file contains styles related to the mechanics of
|
||||||
|
the editor. You probably shouldn't touch them. */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scroll {
|
||||||
|
overflow: scroll !important; /* Things will break if this is overridden */
|
||||||
|
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||||
|
/* See overflow: hidden in .CodeMirror */
|
||||||
|
margin-bottom: -30px; margin-right: -30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
height: 100%;
|
||||||
|
outline: none; /* Prevent dragging from highlighting the element */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.CodeMirror-sizer {
|
||||||
|
position: relative;
|
||||||
|
border-right: 30px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||||
|
before actual scrolling happens, thus preventing shaking and
|
||||||
|
flickering artifacts. */
|
||||||
|
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-vscrollbar {
|
||||||
|
right: 0; top: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-hscrollbar {
|
||||||
|
bottom: 0; left: 0;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-scrollbar-filler {
|
||||||
|
right: 0; bottom: 0;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-filler {
|
||||||
|
left: 0; bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
position: absolute; left: 0; top: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
white-space: normal;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-bottom: -30px;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-elt {
|
||||||
|
position: absolute;
|
||||||
|
cursor: default;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
|
||||||
|
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
cursor: text;
|
||||||
|
min-height: 1px; /* prevents collapsing before first draw */
|
||||||
|
}
|
||||||
|
.CodeMirror pre {
|
||||||
|
/* Reset some styles that the rest of the page might have set */
|
||||||
|
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||||
|
border-width: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-font-variant-ligatures: contextual;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
|
}
|
||||||
|
.CodeMirror-wrap pre {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linebackground {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: .1px; /* Force widget margins to stay inside of the container */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-rtl pre { direction: rtl; }
|
||||||
|
|
||||||
|
.CodeMirror-code {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force content-box sizing for the elements where we expect it */
|
||||||
|
.CodeMirror-scroll,
|
||||||
|
.CodeMirror-sizer,
|
||||||
|
.CodeMirror-gutter,
|
||||||
|
.CodeMirror-gutters,
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-measure {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-measure pre { position: static; }
|
||||||
|
|
||||||
|
div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
div.CodeMirror-dragcursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-focused div.CodeMirror-cursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-selected { background: #d9d9d9; }
|
||||||
|
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||||
|
.CodeMirror-crosshair { cursor: crosshair; }
|
||||||
|
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
|
||||||
|
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||||
|
|
||||||
|
.cm-searching {
|
||||||
|
background-color: #ffa;
|
||||||
|
background-color: rgba(255, 255, 0, .4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Used to force a border model for a node */
|
||||||
|
.cm-force-border { padding-right: .1px; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Hide the cursor when printing */
|
||||||
|
.CodeMirror div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* See issue #2901 */
|
||||||
|
.cm-tab-wrap-hack:after { content: ''; }
|
||||||
|
|
||||||
|
/* Help users use markselection to safely style text background */
|
||||||
|
span.CodeMirror-selectedtext { background: none; }
|
@ -0,0 +1,62 @@
|
|||||||
|
.module-name {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
/* padding: 0 40px .5em 0; */
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
padding: 0 2em 0 0;
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown::after {
|
||||||
|
content: '▼';
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: .55rem;
|
||||||
|
font-size: .8em;
|
||||||
|
color: #999;
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
line-height: 1;
|
||||||
|
/* margin: 0 .3em 0 0; */
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tabs li.active {
|
||||||
|
/* background-color: var(--back-light); */
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widther {
|
||||||
|
display: block;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
padding: calc(.5em - 1px) .25em;
|
||||||
|
line-height: 1;
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-extension {
|
||||||
|
display: inline-block;
|
||||||
|
padding: calc(.5em - 1px) 0;
|
||||||
|
color: var(--prime);
|
||||||
|
left: -.2em;
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: #ff00d4;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
export function keyEvent(code) {
|
||||||
|
return function (node, callback) {
|
||||||
|
node.addEventListener('keydown', handleKeydown);
|
||||||
|
|
||||||
|
function handleKeydown(event) {
|
||||||
|
if (event.keyCode === code) {
|
||||||
|
callback.call(this, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
node.removeEventListener('keydown', handleKeydown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enter = keyEvent(13);
|
@ -0,0 +1,11 @@
|
|||||||
|
export default (blob, filename) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.style.display = 'none';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
link.remove();
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
import { decode } from 'sourcemap-codec';
|
||||||
|
|
||||||
|
export default function getLocationFromStack(stack, map) {
|
||||||
|
if (!stack) return;
|
||||||
|
const last = stack.split('\n')[1];
|
||||||
|
const match = /<anonymous>:(\d+):(\d+)\)$/.exec(last);
|
||||||
|
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const line = +match[1];
|
||||||
|
const column = +match[2];
|
||||||
|
|
||||||
|
return trace({ line, column }, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trace(loc, map) {
|
||||||
|
const mappings = decode(map.mappings);
|
||||||
|
const segments = mappings[loc.line - 1];
|
||||||
|
|
||||||
|
for (let i = 0; i < segments.length; i += 1) {
|
||||||
|
const segment = segments[i];
|
||||||
|
if (segment[0] === loc.column) {
|
||||||
|
const [, sourceIndex, line, column] = segment;
|
||||||
|
const source = map.sources[sourceIndex].slice(2);
|
||||||
|
|
||||||
|
return { source, line: line + 1, column };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
Loading…
Reference in new issue