Authoring kit
@astrolabe/editor adds the Astrolabe parts of an editor to the WYSIWYG editor you already have: an insert menu, an
inspector for answers and settings, a live issues list and a preview that grades. Your editor connects through one
interface, EditorAdapter. How to use it: Any WYSIWYG editor. The kit assembled around a
surface, ready to use, is <astl-editor>; a running example on a textarea:
Authoring kit.
createAuthoring
Section titled “createAuthoring”createAuthoring(options) returns the editor-independent logic (no DOM, so it also runs in workers, servers and
agent tools).
| Option | Type | Meaning |
|---|---|---|
engine |
Engine |
validates and compiles; give it a throw-away secret such as 'editor-preview-only-key' |
descriptors |
EditorDescriptor[] |
one per component authors can insert and inspect; each tag must be registered in the engine |
elements |
ElementEntry[] |
player elements for <astl-preview>, eager or lazy |
locale |
string |
the author's UI locale: labels, issues, template texts |
catalogs |
MessageCatalog[] |
more or overriding messages (any namespace), for example another locale |
styleTemplates |
StyleTemplate[] |
looks the inspector offers per component and for the document, rendered by <astl-preview> (Looks); the inspector also offers each type's variants |
urlPolicy |
UrlPolicy |
the engine's URL policy: editors show only pictures it allows, and <astl-preview> sets it as the page's player policy (the default never replaces a policy the page set). Default isSafeUrl (URLs) |
uploadAsset |
UploadAsset |
the host's upload, (file, { signal }) to a Promise of { src, width?, height? }: stores a picture file (or a component's media file, through a url field with accept) and gives its address (and size in pixels). The picture dialog, pasted and dropped pictures, picture fields and url fields with accept use it; the address must pass urlPolicy, or nothing is written. signal aborts when the author cancels or picks another file. Without it, pictures are added by address only, and the UI says so; files too |
| Member | Returns | Meaning |
|---|---|---|
instantiate(tag, source) |
string |
the descriptor's template with a fresh id: <idPrefix>-<n>, the smallest n source does not use; {{t:key}} becomes an escaped message; other components written without an id get fresh ones too; an element of a type with a companion and that attribute empty names the first such element of the template, else of source (or the one withCompanion adds). A third argument, the id of a named starter, instantiates that starter instead. Throws for an unknown tag or starter |
withCompanion(tag, source) |
string | null |
for a descriptor with a companion ({ tag, attribute }, a gap and its word bank): source with that tag's template at the end of the document when it has no such element yet; else null. <astl-insert-menu> applies it after inserting a default template and keeps the new element selected |
templates(tag) |
EditorTemplate[] |
the named starters of a type (its descriptor's templates), none without |
inspect(elementHtml, source?) |
InspectorModel | null |
the inspector model of one component element: { tag, id, label, icon, fields }; with the whole document source, fields offer what other elements hold (a drop lists the words of its bank) |
inspectDocument(source) |
InspectorModel | null |
the look of the whole exercise: a style-template field for <astl-doc>; null when no template allows the document scope and none is written (Looks) |
update(elementHtml, change) |
string |
the element with one InspectorChange applied; everything else stays byte-identical |
validate(source) |
ValidationReport |
{ ok, issues } from the engine |
describe(issue) |
string |
an issue as a sentence in the authoring locale |
assignIds(source) |
string |
source with fresh ids on pages and components that lack one |
outline(source) |
Outline |
pages, their components and error and warning counts |
preview(source) |
Promise<PreviewResult> |
compiles with a random per-session salt and grades locally ({ ok, view, checker, issues }) |
descriptor(tag) |
EditorDescriptor | null |
the descriptor of a tag |
t(namespace, key, params?) |
string |
a message in the authoring locale |
engine, locale, descriptors, elements |
the options | |
styleTemplates, urlPolicy |
the options, with their defaults | |
uploadAsset |
UploadAsset | null |
the host's upload, or null |
pictureHtml({ src, alt, width?, height?, caption? }) writes the source of a picture as the picture dialog gives it:
an <img> (it sits in text), or with a caption a <figure> with its <figcaption>. Every value is escaped and only the
attributes of the content format are written; alt: '' is a decorative picture.
Named starters
Section titled “Named starters”A type can bring whole exercises with sample content next to its default template, which stays what its own entry
inserts: templates: [{ id, label, description?, icon?, template }] on its EditorDescriptor. Ids are kebab-case and
unique in the type (createAuthoring throws otherwise); each template has the placeholders of template (exactly one
{{id}}, texts as {{t:key}} from the type's catalogs, so a starter comes in every UI language).
A starter can hold more components than its own, such as the word bank's "Drag words into gaps": {{id}} goes on the
type's own element (the bank) and the others (its gaps) are written without an id. Each insert gives them fresh ones,
and every element of a type with a companion (a gap) that leaves the attribute out names the starter's own bank.
Inspector changes
Section titled “Inspector changes”type InspectorChange = | { kind: 'attribute'; name: string; value: string | boolean | null } // true = bare attribute, false/null = remove | { kind: 'text-list'; tag: string; values: readonly string[] } // e.g. the <astl-alt> alternatives | { kind: 'child-flag'; tag: string; attribute: string; index: number; value: boolean } // e.g. `correct` on option 2 | { kind: 'child-choice'; tag: string; attribute: string; index: number; value: string | null } // e.g. `answer` on statement 1 | { kind: 'child-attributes'; tag: string; index: number; attributes: Record<string, string | null> } // a zone's shape and coordinates | { kind: 'child-add'; tag: string; attributes: Record<string, string> } // a new empty child after the last of its tag | { kind: 'child-remove'; tag: string; index: number }; // one child, its content includedInspector fields come from the EditorDescriptor (or from the manifest when it has no fields); controls follow
the attribute types: text, number with min, max and step, checkbox, select with translated options from
editor.value.<attribute>.<value>, text-list, child-flag, child-text (a text input per child for a free-text
attribute, e.g. the word each clue of a word search hides; an empty input removes it, through a child-attributes
change), child-choice (radios of the enum values, or with options: '<tag>' a select of the ids of those
children) and child-table ({ kind: 'child-table', tag, columns: [{ attribute, label }], label, help? }: a row per
child, named by its text, with a text, number or select control per column, Remove per row and Add, through
child-attributes, child-remove and child-add changes; a crossword's words). An attribute field with
options: { from, tag } is a select of the ids of the tag children of the element whose id is in the attribute
from, by their text (a drop's answer among the words of its bank bank); without the document or that element it
keeps its own control. Each component's reference page lists its fields.
Two field options are for media:
{ kind: 'attribute', name, label, help?, accept }on aurlattribute: next to its address, an upload of the filesacceptnames (a file input'saccept, e.g.video/mp4,video/webm) throughuploadAsset. The address it gives goes to the element the upload was for, whenurlPolicyallows it; the field says what happened.{ kind: 'text-list', tag, label, help?, multiline: true }: each child in a text area that keeps its line breaks (a transcript), not on one line.
Two field kinds are for components with a picture (<astl-picture-field> draws them):
{ kind: 'picture-regions', tag, src, label, help? }: zones on the picture of the element'surlattributesrc, one child oftageach. A zone hasid,shape(rect,circleorpolygon),xandy(a rectangle's top-left corner, a circle's centre),widthandheight(rectangle),r(circle, a share of the width),points(polygon:x,y x,y ...) and an optionallabel. Every coordinate is a percentage of the picture: x of its width from the left edge, y of its height from the top.{ kind: 'picture-points', tag, src, label, help? }: pins on the picture, one child oftageach, withid,x,y(percent) andlabel.
Both show the picture, take its address or an upload, and draw each zone or pin in the palette colour of its number
(--astl-palette-1 to -8) with the number on it, so no zone is told apart by colour alone. Authors drag on the
picture to draw a zone or click it to place a pin, and edit every value in number fields (no pointer needed):
add, remove, change the shape around the same centre, add or remove polygon points, label.
Elements
Section titled “Elements”Call defineAuthoringElements() once (nothing is defined on import), then give each element its authoring and
adapter properties. Use as many as you like.
| Element | Members | What it does |
|---|---|---|
<astl-insert-menu> |
insert(tag, template?): string | null, insertPicture(): Promise<string | null> |
buttons in named groups: "Components" (each type with its default template), "Exercises" (the named starters, when any) and "Content" ("Picture"); each button's name is its label and description. A press inserts at the cursor |
<astl-inspector> |
model: InspectorModel | null |
the form for the component at the cursor: answers, alternatives, correct options, scoring, points |
<astl-issues> |
issues: Issue[] | null, validate() |
live validation in the UI locale, 300 ms after the last change, errors first; choosing an issue calls adapter.select(id) |
<astl-preview> |
state, player, refresh(): Promise<void> |
compiles on request and plays the result in <astl-player> with local grading; an invalid source shows its errors instead. state: idle, loading, ready, invalid or failed |
<astl-picture-dialog> |
ask(request?): Promise<PictureFields | null> |
a modal dialog for one picture: a file (uploaded with uploadAsset) or an address, the alt text (required, or "decorative" for alt="") and, for a new picture, an optional caption. The address must pass urlPolicy. request: { current?, file?, caption?, decorative? }; null when the author cancels. The insert menu has its own |
<astl-picture-field> |
field, authoring |
the inspector's picture fields (above); it fires astl-field-change with an InspectorChange |
All of them use native controls, the --astl-* tokens (override them on the elements), data-theme for the theme, and
the parts root and title (plus button, icon and player where they have them). See
Looks.
Adapters
Section titled “Adapters”EditorAdapter (@astrolabe/core) is the only thing an editor integration implements:
| Member | Meaning |
|---|---|
getSource() / setSource(source) |
the whole <astl-doc> as source HTML |
insert(html) |
insert an instantiated template at the cursor |
getElement(id) / replaceElement(id, html) |
one component element as source HTML, read and replaced in place |
getSelection() |
{ id, tag } of the component at the cursor, or null |
select(id) |
move the cursor to a component or a page and focus the editor |
onChange(listener) / onSelectionChange(listener) |
subscriptions; each returns an unsubscribe function |
createTextareaAdapter(textarea) is the reference implementation: it edits the source as text, keeps your
formatting and finds elements by id. destroy() removes its listeners. insert(html) replaces the selection unless
that would break the source: inside a tag it inserts after the tag, inside or on a component after that
component, outside <astl-doc> at the end of the last page. Change events fire on input and after the adapter's
own edits; selection events on select, keyup, click and focus, only when the component at the caret
changes.
scanElements(source) lists pages and components with their { tag, id, start, end } offsets in the text, for
text-based adapters. It tolerates comments, quoting, case and unclosed tags.
Messages
Section titled “Messages”The kit's own strings are in the namespace astl.editor (editorMessages, English and Polish). Labels, help texts
and templates of each component come from its own catalogs (editor.*, template.* keys).