Rask
Rask
v0.23.1-alpha.0.494

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.

EditorExample.cs
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
            ]
        ];
}
Live result
Explain what's new in this release.

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.

EditorToolbarExample.cs
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...");
}
Live result


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#.

EditorCustomItemExample.cs
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;
    }
}
Live result


Nothing copied yet.

Customization

The editor composed from its parts, with a dropdown menu in its toolbar.

EditorCustomizationExample.cs
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}."]
        ];
}
Live result


Last action: nothing yet.

Height

The area is 200 to 500 px tall unless a class on its content slot says otherwise.

EditorHeightExample.cs
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]!");
}
Live result


Every item

Each of the fifteen items Flux's toolbar names.

EditorEveryItemExample.cs
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...");
}
Live result


Disabled

EditorDisabledExample.cs
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();
}
Live result

This one is read-only.

Invalid

EditorInvalidExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class EditorInvalidExample : Component
{
    protected override Component? Render() =>
        Ui.Editor.Placeholder("Write something...").Invalid();
}
Live result


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
headingText, or a heading of level 1 to 3, from a list. Ctrl+Alt+0 to 3.
bold italic strike underlineThe mark. Ctrl+B, I, Shift+S, U.
subscript superscript highlight codeThe mark. Ctrl+,, ., Shift+H, E.
bullet ordered blockquoteThe block. Ctrl+Shift+8, 7, B.
linkA panel with the address, a button to set it and one to remove it. Ctrl+K.
alignLeft, center or right, from a list. Ctrl+Shift+L, E, R.
undo redoHistory. 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

flux: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 stringThe member the HTML is bound to: () => m.Body. Or Value with OnChange.
.Value(…)stringThe HTML it starts with, for a page that holds it itself.
.OnChange(…)a handler given the new valueRuns with the HTML each time it changes; an empty document is "".
.Live()boolWrites the model at every change, not with the next action.
.Toolbar(…)stringThe items, in order: "heading | bold italic ~ undo redo". | is a separator, ~ a spacer.
.Placeholder(…)stringShown while the document is empty.
.Label(…)stringDraws a field around it, with this label.
.Description(…)stringHelp text between the label and the editor.
.DescriptionTrailing(…)stringHelp text under the editor.
.Badge(…)stringA word beside the label: "Optional".
.Disabled()boolRead-only, with the toolbar off.
.Invalid()boolError styling.

Ui.EditorToolbar

flux:editor.toolbar

A composed editor's toolbar: the items named, or the ones inside it.

Step
Takes
What it does
.Items(…)stringThe items, as the editor's Toolbar names them. With none, and nothing inside, the default toolbar.

Ui.EditorContent

flux:editor.content

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

flux:editor.button

A toolbar button of your own: an icon, words, or both.

Step
Takes
What it does
.Icon(…)Ui.IconName — one of 317 namesThe icon it shows.
.IconVariant(…)
Ui.IconVariant
Outline
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(…)stringA hint on hover, which also names a button with no words.
.OnClick(…)a handler, with no argumentRuns when it is pressed.
.Disabled()boolA button that cannot be pressed.

Ui.EditorHeading

flux:editor.heading

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

flux:editor.bold

Bold.

No steps of its own: what goes inside it is its content.

Ui.EditorItalic

flux:editor.italic

Italic.

No steps of its own: what goes inside it is its content.

Ui.EditorStrike

flux:editor.strike

Strikethrough.

No steps of its own: what goes inside it is its content.

Ui.EditorUnderline

flux:editor.underline

Underline.

No steps of its own: what goes inside it is its content.

Ui.EditorSubscript

flux:editor.subscript

Subscript.

No steps of its own: what goes inside it is its content.

Ui.EditorSuperscript

flux:editor.superscript

Superscript.

No steps of its own: what goes inside it is its content.

Ui.EditorHighlight

flux:editor.highlight

Highlighted text.

No steps of its own: what goes inside it is its content.

Ui.EditorCode

flux:editor.code

Inline code.

No steps of its own: what goes inside it is its content.

Ui.EditorBullet

flux:editor.bullet

A bulleted list.

No steps of its own: what goes inside it is its content.

Ui.EditorOrdered

flux:editor.ordered

A numbered list.

No steps of its own: what goes inside it is its content.

Ui.EditorBlockquote

flux:editor.blockquote

A block quote.

No steps of its own: what goes inside it is its content.

Ui.EditorLink

flux:editor.link

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

flux:editor.align

Left, center or right, from a list.

No steps of its own: what goes inside it is its content.

Ui.EditorUndo

flux:editor.undo

Undoes the last change.

No steps of its own: what goes inside it is its content.

Ui.EditorRedo

flux:editor.redo

Redoes the last change undone.

No steps of its own: what goes inside it is its content.

Ui.EditorSeparator

flux:editor.separator

A hairline between two groups of items: | in a list.

No steps of its own: what goes inside it is its content.

Ui.EditorSpacer

flux:editor.spacer

Pushes the items after it to the far edge: ~ in a list.

No steps of its own: what goes inside it is its content.

Confirm