Code editor
A code editor on a textarea. It is Monaco, loaded by the page, with the page's colours and fonts.
The editor is Monaco
Monaco Editor 0.57.0 (MIT) is vendored in this repository in
vendor/monaco and copied to
dist/monaco/vs: the loader, the editor, its workers
and the languages CSS, SCSS, HTML, JavaScript and JSON (7.4 MB
in the folder, of which a page loads about 40 KB at first and
about 6 MB when the first editor opens). Other languages and the
TypeScript and JavaScript language service are not shipped, so
JavaScript is coloured but has no completions or checks. Monaco is
not part of tsunami.min.js: a page with an editor
loads loader.js with its own script
before the Tsunami script. The loader finds the rest next to
itself, so it works from jsDelivr as well as from your own
server. From another origin Monaco starts its web workers from
blob URLs, which needs the CORS header that jsDelivr sends.
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/monaco/vs/loader.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tsunami.min.js"></script>
If a .code-editor is on a page and the loader is not,
the script throws an error that says so. The loader defines the
globals require and define, as an AMD
loader does. A library that ships as UMD (Chart.js, Vanilla
Calendar, ...) registers itself as an AMD module when it finds
define.amd and does not set its global, so load such
libraries before loader.js, or not on the
same page. Another AMD loader such as RequireJS cannot share the
page with Monaco's.
Basic
A .code-editor box wraps a textarea that
holds the code. data-language picks the language;
without it the code is plain text. A label for the
textarea names the editor and focuses it.
<div class="field">
<label for="ce-basic">Template</label>
<div class="code-editor" data-language="html" style="height: 12rem">
<textarea id="ce-basic" name="template"><h1>{{ product.title }}</h1>
<p class="lead">{{ product.summary }}</p>
<ul>
{% for item in product.features %}
<li>{{ item }}</li>
{% endfor %}
</ul></textarea>
</div>
</div>
Languages
HTML, CSS, SCSS, JavaScript and JSON are coloured. The grid and
the heights are the page's: the box takes its height from
style.
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4)">
<div class="field">
<label for="ce-css">CSS</label>
<div class="code-editor" data-language="css" style="height: 10rem">
<textarea id="ce-css" name="css">.card {
padding: 1rem;
border: 1px solid #e3e8ea;
border-radius: 6px;
}</textarea>
</div>
</div>
<div class="field">
<label for="ce-json">JSON</label>
<div class="code-editor" data-language="json" style="height: 10rem">
<textarea id="ce-json" name="json">{
"name": "tsunami",
"private": true,
"tags": ["css", "ui"],
"size": 57
}</textarea>
</div>
</div>
<div class="field">
<label for="ce-scss">SCSS</label>
<div class="code-editor" data-language="scss" style="height: 12rem">
<textarea id="ce-scss" name="scss">$gap: 0.5rem;
.stack {
display: grid;
gap: $gap;
> * {
margin: 0;
}
}</textarea>
</div>
</div>
<div class="field">
<label for="ce-javascript">JavaScript</label>
<div class="code-editor" data-language="javascript" style="height: 12rem">
<textarea id="ce-javascript" name="javascript">const total = (items) =>
items.reduce((sum, item) => sum + item.price, 0);
console.log(total([{ price: 12 }, { price: 30 }]));</textarea>
</div>
</div>
</div>
Read only
readonly on the textarea (or disabled)
shows the code but blocks typing. It can still be selected and
copied.
<div class="field">
<label for="ce-ro">Base layout</label>
<div class="code-editor" data-language="html" style="height: 14rem">
<textarea id="ce-ro" name="layout" readonly><!doctype html>
<html lang="en">
<head>
<title>{{ page.title }}</title>
</head>
<body>
{{ content }}
</body>
</html></textarea>
</div>
</div>
In a form
The textarea keeps the code, so the form sends it and
new FormData(form) reads it. After each edit the
textarea sends a bubbling input event; here the
form's oninput copies the code into the result block.
<form class="stack" method="dialog" oninput="document.getElementById('ce-form-result').textContent = new FormData(this).get('announcement')">
<div class="field">
<label for="ce-form">Announcement</label>
<div class="code-editor" data-language="html" style="height: 10rem">
<textarea id="ce-form" name="announcement"><h2>Summer sale</h2>
<p>Free delivery on every order this week.</p></textarea>
</div>
</div>
<button type="submit" class="btn" data-variant="primary">Save</button>
<pre id="ce-form-result">Type in the editor.</pre>
</form>
A script reads and sets the code
The element's editor property is the Monaco instance
(null until it has loaded), so
getValue() and setValue() are Monaco's
own. Setting the code also updates the textarea and sends
input.
Nothing read yet.
<div class="stack">
<div class="code-editor" id="ce-script" data-language="css" aria-label="Stylesheet" style="height: 10rem">
<textarea name="stylesheet">.banner {
color: #2b7f93;
}</textarea>
</div>
<div>
<button type="button" class="btn" id="ce-read">Read the code</button>
<button type="button" class="btn" id="ce-set">Set the code</button>
</div>
<pre id="ce-script-result">Nothing read yet.</pre>
</div>
<script>
{
const box = document.getElementById("ce-script");
const result = document.getElementById("ce-script-result");
document.getElementById("ce-read").addEventListener("click", () => {
result.textContent = box.editor.getValue();
});
document.getElementById("ce-set").addEventListener("click", () => {
box.editor.setValue(".banner {\n color: #b42323;\n}");
});
}
</script>
Several files
One box can hold several textareas, one per file. The textarea
that is not hidden is the one shown; here the
tabs component shows and hides them (each
textarea is a tab panel). Each file keeps its cursor, scroll and
undo history, and the form sends every file. Closing a tab removes
its textarea and its file.
<form class="stack" method="dialog" id="ce-files-form">
<div class="tabs" data-tabs>
<div role="tablist" aria-label="Files">
<button type="button" role="tab" id="ce-t0" aria-selected="true" aria-controls="ce-f0">index.html<i class="icon icon-x" data-close aria-hidden="true"></i></button>
<button type="button" role="tab" id="ce-t1" aria-selected="false" aria-controls="ce-f1" tabindex="-1">style.css<i class="icon icon-x" data-close aria-hidden="true"></i></button>
<button type="button" role="tab" id="ce-t2" aria-selected="false" aria-controls="ce-f2" tabindex="-1">data.json<i class="icon icon-x" data-close aria-hidden="true"></i></button>
</div>
</div>
<div class="code-editor" style="height: 12rem">
<textarea role="tabpanel" id="ce-f0" name="index.html" aria-labelledby="ce-t0" data-language="html"><h1>Hello</h1>
<p>A page of three files.</p></textarea>
<textarea role="tabpanel" id="ce-f1" name="style.css" aria-labelledby="ce-t1" data-language="css" hidden>h1 {
font-size: 2rem;
}</textarea>
<textarea role="tabpanel" id="ce-f2" name="data.json" aria-labelledby="ce-t2" data-language="json" hidden>{
"title": "Hello"
}</textarea>
</div>
<button type="submit" class="btn" data-variant="primary">Save</button>
<pre id="ce-files-result">Nothing sent yet.</pre>
</form>
<script>
{
const form = document.getElementById("ce-files-form");
form.addEventListener("submit", () => {
document.getElementById("ce-files-result").textContent = [...new FormData(form).keys()].join(", ");
});
}
</script>
Options through mount()
tsunami.codeEditor.mount(el, options) starts a box
and takes any
Monaco editor option
over the defaults. It also starts a box that does not have the
class.
<div class="code-editor" id="ce-options" aria-label="Notes" style="height: 10rem">
<textarea name="notes">A long line that wraps at the edge of the editor instead of scrolling sideways, with no line numbers in the margin.
A second line.</textarea>
</div>
<script>
tsunami.codeEditor.mount(document.getElementById("ce-options"), {
wordWrap: "on",
lineNumbers: "off",
});
</script>
Features
-
Until Monaco has loaded the textarea is the editor, in a
monospace field of the same height. Then the box gets
data-loaded, Monaco takes its place and the textarea stays in the form, out of sight. -
Defaults:
automaticLayouton,fontSize13, no minimap,scrollBeyondLastLineoff, the framework's monospace font. -
Monaco has one theme per page. Two themes are built from the CSS
variables (surface, text, line, accent, and the ok, warning,
danger and info colours for the code), and the editor switches
on
tsunami:themechange. -
A box takes its height from the page (default
10rem). It has a 1px border, no shadow, and the accent border while it has focus. - Reset on the form puts the textareas' default text back into the editor.
Attributes
| Attribute | Where | Description |
|---|---|---|
data-language |
box or textarea |
html, css, scss,
javascript or json. Plain text
otherwise. A textarea's own value wins over the box's.
|
readonly, disabled |
textarea | The editor is read only. |
aria-label |
textarea or box |
Names the editor. A label for the textarea or
an aria-labelledby works too.
|
hidden |
textarea | In a box with several textareas, the file that is not shown. Toggling it switches the file. |
name |
textarea | The form field that carries the code. |
data-loaded |
box, set by the script | Monaco has started. |
API
The functions live on window.tsunami.codeEditor.
| Function | Description |
|---|---|
tsunami.codeEditor.mount(el, options?) |
Starts an editor on a box, with or without the class, and
starts it again if it already has one.
options are Monaco editor options.
|
tsunami.codeEditor.get(el) |
The Monaco instance, or null before it has
started.
|
tsunami.codeEditor.destroy(el) |
Removes the editor and shows the textareas again with the code as it stands. Removing the box from the page does this itself. |
On the element
| Member | Description |
|---|---|
el.editor |
On the .code-editor box (not the textarea):
the Monaco instance, null until it has
started. It has Monaco's methods: getValue(),
setValue(text), focus(),
updateOptions(). They work on the file being
shown.
|
textarea.value |
The code, kept up to date as it is edited. Setting it does
not change the editor: call setValue().
|
Events
| Event | Detail | Description |
|---|---|---|
tsunami:codeeditorready |
{ editor } |
From the box, bubbling, when Monaco has started. |
input |
From the textarea of the file being edited, bubbling, after each edit. The save bar listens to it. | |
tsunami:themechange |
{ theme } |
From the theme module; the editor switches theme. |
Keyboard
These are Monaco's own.
| Keys | Action |
|---|---|
| Ctrl F | Find in the code. |
| F1 | Monaco's command list. |
| Ctrl M | Tab moves focus out of the editor instead of indenting (and back). |
Accessibility
-
Name every editor:
aria-labelon the textarea, or alabel forit. A label click focuses the editor. - A required or invalid textarea that gets the browser's message passes the focus to the editor.
-
With several files, give each tab
aria-controlspointing at its textarea, as in the example. - Monaco's screen reader support turns on when a screen reader is detected. Press Ctrl M so that Tab leaves the editor.