Live editor
shared/editor/editor.js
import {EditorView, basicSetup} from 'codemirror';
import {EditorState} from '@codemirror/state';
import {javascript} from '@codemirror/lang-javascript';
import {python} from '@codemirror/lang-python';
import {html} from '@codemirror/lang-html';
import {css} from '@codemirror/lang-css';
import {vue} from '@codemirror/lang-vue';
const source = document.getElementById('source-code');
const initial = source.textContent;
const editable = source.dataset.editable === 'true';
const languages = {python, javascript, jsx: () => javascript({jsx: true}), html, css, vue};
const holder = document.createElement('div');
source.before(holder);
source.hidden = true;
const status = document.createElement('p');
status.setAttribute('role', 'status');
status.textContent = editable ? 'Change the code and press Run to see your version.' : 'Read-only source';
holder.after(status);
const preview = () => parent.document.querySelector('.example-panel iframe');
const editor = new EditorView({
doc: initial,
parent: holder,
extensions: [basicSetup, languages[source.dataset.language]?.() ?? [],
EditorState.readOnly.of(!editable), EditorView.editable.of(editable),
EditorView.contentAttributes.of({'aria-label': editable ? 'Recipe editor' : 'Read-only source'}),
EditorView.lineWrapping,
EditorView.theme({
'&': {fontSize: '13px', backgroundColor: '#fcfdff'},
'.cm-content': {fontFamily: 'ui-monospace, SFMono-Regular, Consolas, monospace'},
'.cm-gutters': {backgroundColor: '#f5f7fb', border: 'none'},
}),
EditorView.updateListener.of(update => {
if (editable && update.docChanged) status.textContent = 'Changes ready to run.';
}),
],
});
function applyRecipe() {
if (!preview()) { status.textContent = 'Open the demo to see the preview.'; return; }
status.textContent = 'Running…';
preview().contentWindow.postMessage({type: 'recipe', code: editor.state.doc.toString()}, location.origin);
}
if (editable) {
const controls = document.createElement('div');
controls.className = 'lab-controls';
for (const [text, action] of [
['Run', applyRecipe],
['Reset', () => {
editor.dispatch({changes: {from: 0, to: editor.state.doc.length, insert: initial}});
applyRecipe();
}],
]) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = text;
button.addEventListener('click', action);
controls.append(button);
}
holder.after(controls);
window.addEventListener('message', event => {
if (event.origin !== location.origin || event.source !== preview()?.contentWindow || event.data?.type !== 'recipe-result') return;
status.textContent = event.data.error ? `Error: ${event.data.error}` : 'Preview updated. Changes are not saved.';
});
}
window.addEventListener('pagehide', () => editor.destroy());