Components
Rich text editor
A toolbar over an editable area, whose value is HTML. Its engine is Tiptap on ProseMirror, as Flux's is, and the browser fetches it the first time an editor mounts.
Drawn and behaving as Flux UI's rich text editor does, measured against its live docs.
Rich text editor
Bound, and written as it changes. The value is the reader's HTML, shown here as text.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorExample : Component
{
public EditorExample() =>
Notes = "<h3>What's changed</h3><ul><li><p>Markdown notation support</p></li><li><p>Accessible toolbar</p></li>"
+ "<li><p>Shortcut keys</p></li></ul>";
private string? Notes { get; set; }
// The value is HTML the reader typed: it is shown here as text, and never written into the page as markup.
protected override Component? Render() =>
Div.Class("space-y-4")[
Ui.Editor.Bind(() => Notes).Live().Label("Release notes").Description("Explain what's new in this release."),
Ui.Text.Data("testid", "ui-editor-value").Class("font-mono text-xs break-all whitespace-pre-wrap")[
string.IsNullOrEmpty(Notes) ? "(empty)" : Notes
]
];
}What's changed
Markdown notation support
Accessible toolbar
Shortcut keys
<h3>What's changed</h3><ul><li><p>Markdown notation support</p></li><li><p>Accessible toolbar</p></li><li><p>Shortcut keys</p></li></ul>
Configuring items
Which items, in what order: | is a separator and ~ a spacer.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorToolbarExample : Component
{
protected override Component? Render() =>
Ui.Editor.Toolbar("heading | bold italic underline | align ~ undo redo").Placeholder("Write something...");
}Custom items
Where Flux resolves an item's name to a Blade file, a button of your own is a child of the toolbar, and runs C#.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorCustomItemExample : Component
{
private bool _copied;
// Set in code, so a trimmed build keeps the setter the binding writes through.
public EditorCustomItemExample() => Content = "";
private string? Content { get; set; }
protected override Component? Render() =>
Div.Class("space-y-3")[
Ui.Editor.Bind(() => Content).Live().Placeholder("Write something...")[
Ui.EditorToolbar[
Ui.EditorHeading, Ui.EditorSeparator,
Ui.EditorBold, Ui.EditorItalic, Ui.EditorStrike, Ui.EditorSeparator,
Ui.EditorBullet, Ui.EditorOrdered, Ui.EditorBlockquote, Ui.EditorSeparator,
Ui.EditorLink, Ui.EditorSeparator,
Ui.EditorAlign,
Ui.EditorSpacer,
Ui.EditorButton.Icon(_copied ? Ui.IconName.ClipboardDocumentCheck : Ui.IconName.Clipboard)
.IconVariant(Ui.IconVariant.Outline).Tooltip("Copy to clipboard").OnClick(Copy)
],
Ui.EditorContent
],
Ui.Text.Data("testid", "ui-editor-copied")[_copied ? $"Copied {(Content ?? "").Length} characters of HTML." : "Nothing copied yet."]
];
// A toolbar item of the app's own is a button that runs C#: this one hands the editor's HTML to the clipboard.
private async Task Copy()
{
await Navigator.Clipboard.WriteText(Content ?? "");
_copied = true;
}
}Nothing copied yet.
Customization
The editor composed from its parts, with a dropdown menu in its toolbar.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorCustomizationExample : Component
{
private string _last = "nothing yet";
protected override Component? Render() =>
Div.Class("space-y-3")[
Ui.Editor[
Ui.EditorToolbar[
Ui.EditorHeading, Ui.EditorSeparator,
Ui.EditorBold, Ui.EditorItalic, Ui.EditorStrike, Ui.EditorSeparator,
Ui.EditorBullet, Ui.EditorOrdered, Ui.EditorBlockquote, Ui.EditorSeparator,
Ui.EditorLink, Ui.EditorSeparator,
Ui.EditorAlign,
Ui.EditorSpacer,
Ui.Dropdown.Position(Ui.DropdownPosition.Bottom).Align(Ui.DropdownAlign.End).Offset(-15)[
Ui.EditorButton.Icon(Ui.IconName.EllipsisHorizontal).Tooltip("More"),
Ui.Menu[
Ui.MenuItem.Icon(Ui.IconName.ArrowTopRightOnSquare).OnClick(() => { _last = "preview"; })["Preview"],
Ui.MenuItem.Icon(Ui.IconName.ArrowDownTray).OnClick(() => { _last = "export"; })["Export"],
Ui.MenuItem.Icon(Ui.IconName.Share).OnClick(() => { _last = "share"; })["Share"]
]
]
],
Ui.EditorContent
],
Ui.Text.Data("testid", "ui-editor-menu")[$"Last action: {_last}."]
];
}Last action: nothing yet.
Height
The area is 200 to 500 px tall unless a class on its content slot says otherwise.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorHeightExample : Component
{
protected override Component? Render() =>
Ui.Editor.Class("**:data-[slot=content]:min-h-[100px]!");
}Every item
Each of the fifteen items Flux's toolbar names.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorEveryItemExample : Component
{
protected override Component? Render() =>
Ui.Editor
.Toolbar("heading | bold italic strike underline | subscript superscript highlight code | bullet ordered blockquote | link | align ~ undo redo")
.Placeholder("Write something...");
}Disabled
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorDisabledExample : Component
{
protected override Component? Render() =>
Ui.Editor.Value("<p>This one is read-only.</p>").Disabled();
}This one is read-only.
Invalid
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class EditorInvalidExample : Component
{
protected override Component? Render() =>
Ui.Editor.Placeholder("Write something...").Invalid();
}Behaviour
The value is the reader's HTML
The value is the document as HTML, <p>Hello <strong>world</strong></p>, and an empty document is the empty string. Bind writes it with the next action (.Live(): on every change) and validates the field then; Value with OnChange leaves it to you. A value the app changes afterwards is shown in the editor.
The editor itself only produces the tags of its schema, but what you bind may have come from anywhere, and the editor writes its value into the page as it is, exactly as Raw does. Sanitize HTML on the server before you store it or bind it, and never render a stored value as markup without doing so. Shown as text, as the first example shows it, it is always safe.
Nothing to configure
The engine is 117 KB gzipped, so it is in neither Rask's runtime nor the kit's assembly: it is a static file, wwwroot/js/rask-ui-editor.js, which the build of every app that references the kit writes, and which the browser fetches the first time an editor mounts. A page without an editor never requests it. An app that will never draw one keeps it out of its publish with <RaskUiEditorEngine>false</RaskUiEditorEngine>.
Until the script has loaded, and with scripting off, the editor shows its value as plain markup. A strict Content-Security-Policy needs nothing added: the file is same-origin script. Its notices (Tiptap and ProseMirror, MIT; eleven Lucide icons, ISC) are rask-ui-editor.LICENSES.txt, written beside it.
The toolbar and its keys
The default is heading | bold italic strike | bullet ordered blockquote | link | align. The toolbar is one tab stop and the arrow keys walk its controls. Cmd on a Mac. Markdown works while typing: #, ##, ###, **bold**, *italic*, ~~strike~~, -, 1., >, a backtick for code, three for a code block and --- for a rule.
Item | What happens |
|---|---|
heading | Text, or a heading of level 1 to 3, from a list. Ctrl+Alt+0 to 3. |
bold italic strike underline | The mark. Ctrl+B, I, Shift+S, U. |
subscript superscript highlight code | The mark. Ctrl+,, ., Shift+H, E. |
bullet ordered blockquote | The block. Ctrl+Shift+8, 7, B. |
link | A panel with the address, a button to set it and one to remove it. Ctrl+K. |
align | Left, center or right, from a list. Ctrl+Shift+L, E, R. |
undo redo | History. Ctrl+Z, Ctrl+Shift+Z. |
Extensions
Highlight, Link, Placeholder, StarterKit, Subscript, Superscript, TextAlign and Underline are on; Table, TableRow, TableCell and TableHeader are bundled and off. Before an editor is created it raises ui:editor on itself, Flux's flux:editor event under the kit's name, and a script of the page can call enableExtension, disableExtension, registerExtensions and init on its detail, as on Flux.
The editor's root has what Flux's has: element.value, to get and set, and the editor instance, and it raises input and change for every change of the document.
Reference
Ui.Editor
A toolbar over an editable area, whose value is HTML. Its children, when it has any, compose it from its parts.
Step | Takes | What it does |
|---|---|---|
.Bind(…) | () => a string | The member the HTML is bound to: () => m.Body. Or Value with OnChange. |
.Value(…) | string | The HTML it starts with, for a page that holds it itself. |
.OnChange(…) | a handler given the new value | Runs with the HTML each time it changes; an empty document is "". |
.Live() | bool | Writes the model at every change, not with the next action. |
.Toolbar(…) | string | The items, in order: "heading | bold italic ~ undo redo". | is a separator, ~ a spacer. |
.Placeholder(…) | string | Shown while the document is empty. |
.Label(…) | string | Draws a field around it, with this label. |
.Description(…) | string | Help text between the label and the editor. |
.DescriptionTrailing(…) | string | Help text under the editor. |
.Badge(…) | string | A word beside the label: "Optional". |
.Disabled() | bool | Read-only, with the toolbar off. |
.Invalid() | bool | Error styling. |
Ui.EditorToolbar
A composed editor's toolbar: the items named, or the ones inside it.
Step | Takes | What it does |
|---|---|---|
.Items(…) | string | The items, as the editor's Toolbar names them. With none, and nothing inside, the default toolbar. |
Ui.EditorContent
A composed editor's editable area. What is inside it is the HTML it starts with.
No steps of its own: what goes inside it is its content.
Ui.EditorButton
A toolbar button of your own: an icon, words, or both.
Step | Takes | What it does |
|---|---|---|
.Icon(…) | Ui.IconName — one of 317 names | The icon it shows. |
.IconVariant(…) | Ui.IconVariantOutline Solid Mini Micro Each is a step of its own too, unless an HTML tag has its name. | Which drawing of the icon. Mini alone, micro beside words, when unset. |
.Tooltip(…) | string | A hint on hover, which also names a button with no words. |
.OnClick(…) | a handler, with no argument | Runs when it is pressed. |
.Disabled() | bool | A button that cannot be pressed. |
Ui.EditorHeading
Text, or a heading of level one to three, from a list.
No steps of its own: what goes inside it is its content.
Ui.EditorBold
Bold.
No steps of its own: what goes inside it is its content.
Ui.EditorItalic
Italic.
No steps of its own: what goes inside it is its content.
Ui.EditorStrike
Strikethrough.
No steps of its own: what goes inside it is its content.
Ui.EditorUnderline
Underline.
No steps of its own: what goes inside it is its content.
Ui.EditorSubscript
Subscript.
No steps of its own: what goes inside it is its content.
Ui.EditorSuperscript
Superscript.
No steps of its own: what goes inside it is its content.
Ui.EditorHighlight
Highlighted text.
No steps of its own: what goes inside it is its content.
Ui.EditorCode
Inline code.
No steps of its own: what goes inside it is its content.
Ui.EditorBullet
A bulleted list.
No steps of its own: what goes inside it is its content.
Ui.EditorOrdered
A numbered list.
No steps of its own: what goes inside it is its content.
Ui.EditorBlockquote
A block quote.
No steps of its own: what goes inside it is its content.
Ui.EditorLink
A panel with the address, a button to set it and one to remove the link.
No steps of its own: what goes inside it is its content.
Ui.EditorAlign
Left, center or right, from a list.
No steps of its own: what goes inside it is its content.
Ui.EditorUndo
Undoes the last change.
No steps of its own: what goes inside it is its content.
Ui.EditorRedo
Redoes the last change undone.
No steps of its own: what goes inside it is its content.
Ui.EditorSeparator
A hairline between two groups of items: | in a list.
No steps of its own: what goes inside it is its content.
Ui.EditorSpacer
Pushes the items after it to the far edge: ~ in a list.
No steps of its own: what goes inside it is its content.