Editor
<astl-editor> is a ready exercise editor: the authoring kit (insert menu, inspector,
issues list, preview) around an editing surface. The surface is TipTap by default. Any other editor plugs in
through an EditorAdapter and a slot="surface" child, and then TipTap is never loaded.
import { defineEditor } from 'astrolabe/editor';
defineEditor(); // every built-in component, editor descriptor, preview element and look<astl-editor locale="pl" components="astl-gap astl-choice"></astl-editor> <!-- TipTap, restricted --><astl-editor><textarea slot="surface"></textarea></astl-editor> <!-- another editor -->astrolabe/editor:defineEditor(options?)with every built-in as default,AstlEditor,createTextareaAdapter,tokensCss, the built-in lists (builtInDefinitions,builtInEditors,builtInElements,builtInStyleTemplates) and@astrolabe/core.astrolabe-editor.js: the script-tag bundle ofastrolabe/editor(indist/cdn/); it callsdefineEditor()on load and exports the same.@astrolabe/editor-tiptap:defineEditor(options)without defaults (components,editorsandelementsare required) andAstlEditor, for lean installs.@astrolabe/editor-tiptap/tiptap:astrolabeExtensionsandcreateTiptapAdapter, for your own TipTap editor; the only entry that imports TipTap.
None of the first three imports TipTap: <astl-editor> imports its surface, with TipTap, as a chunk of its own when it
creates it.
defineEditor
Section titled “defineEditor”defineEditor(options) registers what every <astl-editor> offers and defines <astl-editor> with the kit
elements. Nothing is defined on import. A second call adds to the first; a tag, a look or a slot kind already
registered by another object throws, as two copies would otherwise mix silently.
| Option | Type and meaning, and the default in astrolabe/editor |
|---|---|
components |
ComponentDefinition[]: the types documents may contain, as for createEngine. Default: builtInDefinitions |
editors |
EditorDescriptor[]: insert menu and inspector entries, one per type authors can add. Default: the builtInEditors of the components types |
elements |
ElementEntry[]: player elements for the preview, eager or lazy, as for definePlayer. Default: the builtInElements (lazy) of the components types |
styleTemplates |
StyleTemplate[]: looks the inspector offers and the preview renders; []: only the default look. Default: builtInStyleTemplates (playful, notebook) |
catalogs |
MessageCatalog[]: more or overriding messages, whose keys win; English and Polish are always there. Default: none |
slotKinds, profile, urlPolicy |
as for createEngine. Defaults: none, DEFAULT_PROFILE, isSafeUrl |
uploadAsset |
(file, { signal }) => Promise<{ src, width?, height? }>: your upload, for pictures (below). Its address must pass urlPolicy. Default: none, so pictures are added by address only |
registry |
CustomElementRegistry: where the elements are defined. Default: the global customElements |
An option replaces its default and is never merged with it. Extend a list with the built-ins spread first, for example a component of your own:
import { builtInDefinitions, builtInEditors, builtInElements, defineEditor } from 'astrolabe/editor';
defineEditor({ components: [...builtInDefinitions, yesNoDefinition], editors: [...builtInEditors, yesNoEditor], elements: [...builtInElements, yesNoElement],});To let authors use fewer types, set the components attribute: the other types stay registered, so pasted content
of those types is kept and reported. To register fewer, pass fewer components: the default editors and elements
follow them. A list you pass still replaces its default, and an editor descriptor without its component throws.
const tags = ['astl-gap', 'astl-choice'];defineEditor({ components: builtInDefinitions.filter((d) => tags.includes(d.manifest.tag)) });There is no secret: the preview compiles in the browser with a random secret and salt, so preview ids never match
published ones.
Attributes and properties
Section titled “Attributes and properties”| Name | Values | Default | Meaning |
|---|---|---|---|
locale (attribute, property) |
en, pl |
the editor's lang, else the browser languages, else en |
UI locale of the kit and the surface; also the lang of a new document |
components (attribute, property) |
space-separated tags, or string[] | null |
null: every registered type |
the types of the menu, the inspector, validation and the preview; other types stay in the document and show ELEMENT_UNKNOWN |
theme (attribute, property) |
light, dark, hc, auto |
auto |
written to data-theme and passed to the kit elements and the preview |
value (property) |
source HTML (<astl-doc>) |
an empty <astl-doc> in the UI locale's lang |
reads adapter.getSource(), writes adapter.setSource(); kept until there is an adapter |
adapter (property) |
EditorAdapter | null |
TipTap, on a surface the editor creates | setting another adapter swaps the editor; null brings the TipTap surface back, with its document, unless a slot="surface" child is there |
authoring (read-only property) |
Authoring |
the kit's logic for these registrations, locale and components: validate, describe, preview and more |
|
surface (slot) |
your editor's elements | the TipTap surface | with an adapter set (also before defineEditor runs) or a slot="surface" child when the editor first renders, TipTap is never loaded |
astl-editor-change (event) |
no detail | after every change of the document, the author's or value; bubbles and is composed |
The TipTap schema keeps every registered type, whatever components says, so pasted content is never dropped. To
check a source as the editor does: editor.authoring.validate(editor.value).
Parts: surface, insert-menu, inspector, issues and preview. Tokens set on <astl-editor> reach the kit
elements, the preview player and the surface; see Looks.
The kit never publishes. When the author saves, take editor.value, validate it (editor.authoring.validate) and
store it; a source with zero errors compiles, warnings are advice.
A textarea instead of TipTap
Section titled “A textarea instead of TipTap”Put the textarea (and its label) in slot="surface" and set adapter to createTextareaAdapter, which edits the
source as text and keeps your formatting:
<astl-editor> <label slot="surface" for="source">Source (HTML)</label> <textarea slot="surface" id="source"></textarea></astl-editor>import { createTextareaAdapter, defineEditor } from 'astrolabe/editor';
defineEditor();const editor = document.querySelector('astl-editor');editor.adapter = createTextareaAdapter(editor.querySelector('textarea'));Until the adapter is set, the kit waits: its buttons are disabled and the issues list is empty. It runs live on the
authoring kit example page (examples/editor-textarea).
Your own editor
Section titled “Your own editor”Any editor works the same way: its element in slot="surface", and an EditorAdapter over it in adapter (source
in and out, one element by id, the selection, change events). How to write one for CKEditor, Lexical or ProseMirror:
Any WYSIWYG editor.
The TipTap surface
Section titled “The TipTap surface”The surface is a TipTap editor (StarterKit restricted to the content format) in the editor's light DOM, slotted into
surface: ProseMirror selection is unreliable inside shadow roots. It is imported on demand, as its own chunk; while
it loads, the surface says "Loading the editor…" in a status message, and if the chunk cannot be loaded, "The editor
could not be loaded. Reload the page to try again." while the kit stays disabled. Its nodes come from the manifests:
inline types are atoms shown as chips ([grey]), blocks with rich children are framed with their children editable,
other blocks are atoms. TipTap adds no <style> element: the surface stylesheet is adopted, so the editor works under
the strict policy of the security checklist.
- Enter in a child adds a sibling: another option after an option, the first option after the prompt. Nothing of
the answer is copied (an option's
correct), and each child keeps a local id of its own: a new or pasted copy gets<tag>-<n>, for exampleoption-1. - A pasted or dropped copy of a component or a page whose id another one has gets a fresh id, as the insert menu
makes them (
gap-2), and keeps everything else, its answer too. The ids inside it (a choice's option ids) are local to it and stay. Cut and paste is a move: the id stays. <astl-doc>keeps its attributes as written; rich text keepslanganddir, never other attributes.
Pictures
Section titled “Pictures”"Picture" in the insert menu's "Content" group asks for a picture in a dialog: a file, which uploadAsset stores, or
its address (the only way without uploadAsset, as the dialog says); then the alt text, required unless the picture
is decorative (alt=""), and an optional caption, which makes it a <figure>. The address must pass urlPolicy, an
uploaded one too, and the surface shows only pictures whose address passes.
defineEditor({ // `signal` aborts when the author cancels or picks another file. Check type and size on your server. uploadAsset: async (file, { signal }) => { const response = await fetch('/assets', { method: 'POST', body: file, signal }); const { url, width, height } = await response.json(); return { src: url, width, height }; },});- A picture file pasted or dropped into the surface goes through the same upload and dialog, where it lands. Without
uploadAssetnothing is inserted, and a status message says why until the next edit. - To change a picture's address or alt text later, select it and press Enter (in a figure: Enter in its caption, which stays one line), or press its "Edit" button. The caption is edited in place.
- Store the file where students can load it: the player shows only pictures its own URL policy allows, and your
Content Security Policy needs the asset origin in
img-src(URLs).
Your own TipTap editor
Section titled “Your own TipTap editor”Install @astrolabe/editor-tiptap: its /tiptap entry has the same building blocks for a TipTap editor you create
yourself (astrolabe/editor does not export them, so that importing it never loads TipTap):
astrolabeExtensions(authoring, manifests?, { pictures? }): the extensions of the content format: StarterKit restricted to it,sub,sup,mark,span,langanddir, pictures and figures, the document, pages and a node per type (default: the authoring's), labelled in the authoring locale.pictures(PictureHooks) is how the surface asks about pictures:ask(request)opens a picture dialog (<astl-picture-dialog>'saskdoes) andrefuse()says that a pasted or dropped file was not inserted (no upload). Without it, picture files are left to the browser and Enter on a picture does what it does on any selection.createTiptapAdapter(editor, manifests): theEditorAdapterover that editor, for the kit elements or for<astl-editor>'sadapter.
import { Editor } from '@tiptap/core';import { astrolabeExtensions, createTiptapAdapter } from '@astrolabe/editor-tiptap/tiptap';
const editor = new Editor({ element, injectCSS: false, extensions: astrolabeExtensions(authoring) });const adapter = createTiptapAdapter(editor, authoring.engine.manifests);Messages
Section titled “Messages”The surface strings (surface.label, surface.pageTitle, surface.loading, surface.failed, surface.picture.*)
and the picture dialog's (picture.*) are in the kit's namespace astl.editor, in English and Polish. catalogs rewords any of them (Internationalisation).