Editor
A rich text editor on a textarea. It is TinyMCE, loaded by the page, with a quiet toolbar and the page's theme.
The editor is TinyMCE
TinyMCE 8.10.0 is vendored in this repository as one file,
dist/tinymce.min.js (core, the silver theme, the DOM
model, the icons, the plugins autoresize, code, fullscreen, image,
link, lists and media, and the oxide skins, light and dark). It is
under the GPL (license_key: "gpl"), see
vendor/tinymce/LICENSE.md. It is not part of
tsunami.min.js: a page with an editor loads it with
its own script first. Pages without an editor do not
load it. Nothing else is fetched at run time.
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tinymce.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tsunami.min.js"></script>
If a textarea.editor is on a page and TinyMCE is not
loaded, the script throws an error that says so.
Source code in a code editor
The code button opens TinyMCE's own dialog, a plain textarea. If the page also loads Monaco, the button opens a dialog with a code editor instead: coloured HTML, line numbers and the page's theme, and Save sets the HTML back. The editors on this page do that. Monaco costs about 40 KB when the page loads and about 6 MB the first time a code editor opens, so a page that does not need it leaves it out and keeps the plain dialog. Load Monaco's loader after TinyMCE and before the Tsunami script.
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tinymce.min.js"></script>
<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>
A textarea with the class editor
The textarea's text is the starting HTML. Without the script, or before TinyMCE has started, the textarea is all there is, and it already has the height of the editor.
<div class="field">
<label for="ed-main">Description</label>
<textarea class="editor" id="ed-main" name="description">
<h2>Magic Sheets</h2>
<p>Woven from <strong>long-staple cotton</strong> for a cool, crisp feel. See the <a href="#care">care guide</a> before the first wash.</p>
<ul>
<li>300 thread count</li>
<li>Fits mattresses up to 40cm deep</li>
</ul>
</textarea>
</div>
The placeholder shows in an empty editor
<div class="field">
<label for="ed-placeholder">Excerpt</label>
<textarea class="editor" id="ed-placeholder" name="excerpt" placeholder="A sentence or two for the product listing"></textarea>
</div>
A form sends the HTML
The textarea always holds the editor's HTML, so a form sent by the
browser, new FormData(form) and
textarea.value all carry it. No call to save is
needed.
<form class="stack" id="ed-form" method="dialog">
<div class="field">
<label for="ed-form-note">Note</label>
<textarea class="editor" id="ed-form-note" name="note"><p>Ships in <strong>two parcels</strong>.</p></textarea>
</div>
<button type="submit" class="btn" data-variant="primary">Save</button>
<pre id="ed-form-result">Nothing sent yet.</pre>
</form>
<script>
{
const form = document.getElementById("ed-form");
form.addEventListener("submit", () => {
document.getElementById("ed-form-result").textContent = new FormData(form).get("note");
});
}
</script>
The save bar sees edits, Reset puts the text back
Typing, a toolbar command, undo and redo make the textarea send a
bubbling input event, which is all the save bar
needs. Discard (a reset button) restores the text the page started
with.
<form id="ed-bar" class="stack" method="dialog">
<div class="field">
<label for="ed-bar-text">Care instructions</label>
<textarea class="editor" id="ed-bar-text" name="care"><p>Wash at <em>30 degrees</em>.</p></textarea>
</div>
<div class="save-bar" data-form="ed-bar" role="region" aria-label="Unsaved changes" hidden>
<p>Unsaved changes</p>
<div>
<button type="reset" form="ed-bar" class="btn" data-variant="ghost">Discard</button>
<button type="submit" form="ed-bar" class="btn" data-variant="primary">Save</button>
</div>
</div>
</form>
A script reads and sets the HTML through the element
The element's editor property is the TinyMCE
instance, null until it has started.
Nothing read yet.
<div class="stack">
<textarea class="editor" id="ed-read" aria-label="Care instructions"><p>Wash at <em>30 degrees</em>.</p></textarea>
<div class="stack" data-direction="row" data-gap="sm">
<button type="button" class="btn" id="ed-read-get" data-size="sm">Read the HTML</button>
<button type="button" class="btn" id="ed-set" data-size="sm">Set the HTML</button>
</div>
<pre id="ed-read-result">Nothing read yet.</pre>
</div>
<script>
{
const area = document.getElementById("ed-read");
document.getElementById("ed-read-get").addEventListener("click", () => {
document.getElementById("ed-read-result").textContent = area.editor.getContent();
});
document.getElementById("ed-set").addEventListener("click", () => {
area.editor.setContent("<p>Do not tumble dry.</p>");
});
}
</script>
Data attributes set the toolbar and the height
data-toolbar="none" hides the toolbar.
data-menubar and data-statusbar turn
those bars on.
<div class="field">
<label for="ed-data">Short note, with a small toolbar</label>
<textarea class="editor" id="ed-data" name="short_note" data-toolbar="bold italic underline | link" data-min-height="120" data-max-height="240"><p>Gift wrapping on request.</p></textarea>
</div>
Other TinyMCE options go through mount()
mount(el, options) takes any TinyMCE option. They win
over the defaults and over the data attributes. The safety
settings below are defaults too, so an option can loosen them:
think before passing valid_elements.
<div class="field">
<label for="ed-mount">A textarea without the class, started by a script</label>
<textarea id="ed-mount" name="mounted"><p>Started with <code>tsunami.editor.mount()</code>.</p></textarea>
</div>
<script>
window.tsunami.editor.mount(document.getElementById("ed-mount"), {
toolbar: "bold italic | bullist numlist",
min_height: 120,
max_height: 240,
});
</script>
Read only, disabled and invalid
readonly, disabled (also from a disabled
fieldset) and required are read from the
textarea when the editor starts.
aria-invalid="true" gives the frame the
danger colour. A required, empty editor stops the form and the
browser shows its message at the editor.
A summary is required.
<div class="stack">
<div class="field">
<label for="ed-readonly">Terms, read only</label>
<textarea class="editor" id="ed-readonly" name="terms" readonly><p>Returns are accepted within <strong>30 days</strong>.</p></textarea>
</div>
<div class="field">
<label for="ed-disabled">Notes, disabled</label>
<textarea class="editor" id="ed-disabled" name="notes" disabled><p>Not editable and not sent.</p></textarea>
</div>
<div class="field">
<label for="ed-invalid">Summary, invalid</label>
<textarea class="editor" id="ed-invalid" name="summary" aria-invalid="true" required data-min-height="120"></textarea>
<p>A summary is required.</p>
</div>
</div>
What the editor allows
The HTML may be shown by other apps, so the editor does not produce just anything. Pasted text, the textarea's text and the source code dialog all pass the same filter.
-
Elements:
p br hr h1-h6 pre blockquote strong b em i u s strike code sub sup ul ol li span a img video audio. Anything else (script,style,iframe,object,embed,form,svg, ...) is removed. -
Attributes:
href title target relon links,src alt title width heighton images,src controls width heighton video andsrc controlson audio. Noclass, noid, noon*handler. -
Styles:
text-alignandpadding-left/padding-right(the alignment and indent buttons), andwidth/heighton images and video. -
Addresses:
http,httpsandmailto, plus relative and#addresses. Ajavascript:,data:or other address is emptied. A link that opens a new tab getsrel="noopener". Pasted images are not kept. -
Media: the dialog takes the address of a video
(
mp4 webm ogv mov) or audio file and writes avideooraudioelement. Embed codes and pages such as YouTube are refused, since they need aniframe.
The editor does not upload images. The image dialog takes an
address; an app that wants uploads passes TinyMCE's
images_upload_handler through mount().
Look
The text being edited loads the Tsunami stylesheet and takes the
typography class, in the page's theme. The skin
follows the theme too: oxide in light and
oxide-dark in dark, and a
tsunami:themechange starts the editor again with the
other skin (the text stays; undo history does not). TinyMCE's CSS
is not in a layer and would beat Tsunami's, so the few rules that
recolour its frame, toolbar, menus and dialogs are
!important in the components layer and use only the
CSS variables.
API
The functions live on window.tsunami.editor.
| Function | Description |
|---|---|
tsunami.editor.mount(el, options?) |
Starts an editor on a textarea, with or without the class,
and starts it again if it already has one.
options are TinyMCE options.
|
tsunami.editor.get(el) |
The TinyMCE instance, or null before it has
started.
|
tsunami.editor.destroy(el) |
Removes the editor and shows the textarea again with the text as it stands. Removing the textarea from the page does this itself. |
On the element
| Member | Description |
|---|---|
el.editor |
The TinyMCE instance, null until it has
started. It has TinyMCE's own methods:
getContent(), setContent(html),
save(), focus().
|
data-editor-ready |
Set on the textarea once the editor has started. The textarea is then kept out of sight, but it stays in the form. |
el.value |
The HTML, kept up to date as the text is edited. Setting
it does not change the editor: call
setContent().
|
Events
| Event | Detail | Description |
|---|---|---|
tsunami:editorready |
{ editor } |
From the textarea, bubbling, when the editor has started (again after a theme change). |
input |
From the textarea, bubbling, after typing, a command, undo or redo. The save bar listens to it. | |
tsunami:themechange |
{ theme } |
From the theme module; the editor starts again with the matching skin. |
A form's reset puts the text back to the textarea's
default. On start the editor takes its own tidy version of the
starting HTML as that default, so Reset and the save bar compare
the same text.
Passing options
The defaults are below. The data attributes come next and the
options given to mount() last.
| Setting | Value |
|---|---|
| Toolbar | Block style, bold, italic, underline, bullet list, numbered list, link, image, media, four alignments, outdent, indent, clear formatting, source code, fullscreen. It wraps on a narrow screen. |
| Plugins |
image link fullscreen media autoresize lists code. Only these are in tinymce.min.js; a button
from another plugin needs that plugin added to the file.
|
| Height | From 200px to 800px, growing with the text. |
| Menu bar, status bar, branding, promotion | Off. |
Attributes
| Attribute / element | Values | Description |
|---|---|---|
textarea.editor |
Starts an editor on the textarea, when the page loads or when it is added later. | |
name |
text | The HTML is sent with the form under this name. |
placeholder |
text | Shown in an empty editor. |
required |
present | An empty editor stops the form. |
readonly |
present | The text cannot be changed. |
disabled |
present |
The editor is dimmed and not sent. A disabled
fieldset counts.
|
aria-invalid |
true |
Draws the frame in the danger colour. |
aria-label |
text |
Names the editing area when there is no
label.
|
data-toolbar |
toolbar string, or none |
The buttons, as TinyMCE's toolbar option.
|
data-min-height |
pixels | The least height, default 200. |
data-max-height |
pixels | The greatest height, default 800. |
data-menubar |
present or false |
Shows TinyMCE's menu bar. |
data-statusbar |
present or false |
Shows TinyMCE's status bar. |
data-editor-ready |
set by the script | On the textarea once the editor has started. |