loading
Note:
Why there is no Range object here A Range holds live DOM node references, so it cannot cross the interop boundary. This works the way the browser's own editing commands do instead: every call acts on the current selection, or on a range expressed as character offsets inside one element you name. Those offsets count text nodes only, so they survive markup changing around the text - which is what makes them safe to save across a re-render.

Read the selection

IsSupported / Get / GetText / GetHtml

Get returns the selection's text, whether it is collapsed to a caret, and its offsets. GetHtml returns the selected markup instead of its text - what a 'quote this passage' feature needs. The markup is page content, so treat it as untrusted before storing it.

C#
@inject Bit.Butil.Selection selection

var current = await selection.Get();       // WindowSelection?
var text = await selection.GetText();
var html = await selection.GetHtml();
Live sample

Select any part of this paragraph - including some of its markup - and then press one of the buttons below. The selection is a document-wide thing, so every section below carries a paragraph of its own to work on.

read output
Results will appear here when you interact with the samples.

Where it is on screen

GetRects / GetBoundingRect / ContainsElement

GetRects gives one rectangle per line box, because a selection spanning wrapped text is not a rectangle - that is what a highlight overlay draws. GetBoundingRect gives the single enclosing rectangle, which is where a floating toolbar goes.

C#
var rects = await selection.GetRects();            // one per line box
var box = await selection.GetBoundingRect();       // where to anchor a toolbar
var inside = await selection.ContainsElement(element, partly: true);
Live sample

Select part of this paragraph - across a line break, to see one rectangle per line box - and then measure it.

measure output
Results will appear here when you interact with the samples.

Change the selection

SelectElement / SelectElementContents / SelectRange / RemoveAll / Collapse

Selecting an element's contents is the 'select all in this box' primitive. SelectRange takes character offsets within one element, which is how a caret is restored after re-rendering.

C#
await selection.SelectElement(element);           // the element itself, tags and all
await selection.SelectElementContents(element);   // only what is inside it
await selection.SelectRange(element, 0, 12);
await selection.Collapse(toStart: true);
await selection.RemoveAll();
Live sample

The buttons below select this paragraph, its contents, or its first 24 characters.

change output
Results will appear here when you interact with the samples.

Save and restore a caret

GetRangeIn / SelectRange

Read the offsets before a re-render, hand them back afterwards. Try it on the editable box: select something, save, click away, then restore.

C#
var saved = await selection.GetRangeIn(editor);
// ... re-render ...
if (saved is not null) await selection.SelectRange(editor, saved.Start, saved.End);
Live sample
This box is editable. Select part of it, save the offsets, click somewhere else, then restore.
restore output
Results will appear here when you interact with the samples.

Edit through the selection

Surround / ReplaceWithText / DeleteContents

Surround wraps the selection in a new element - the highlighting primitive. It returns false when the selection's ends are in different elements, which is a normal thing for a user to have selected rather than an error: the browser refuses because no single element could contain it.

C#
var wrapped = await selection.Surround("mark");
await selection.ReplaceWithText("[redacted]");
await selection.DeleteContents();
Live sample

Select part of this paragraph and then wrap, replace or delete it. The edits land here, where you can see them.

edit output
Results will appear here when you interact with the samples.

What is under the pointer

IsCaretFromPointSupported / CaretFromPoint

Resolves a viewport point to a text position - which character of which node the pointer is over. Click anywhere in the sample paragraph above and this reports what was under the click. The basis of drag-to-insert, hover dictionaries and click-to-annotate.

C#
private async Task OnClick(MouseEventArgs e)
{
    var caret = await selection.CaretFromPoint(e.ClientX, e.ClientY);
    // caret.Offset within caret.Text, inside a <caret.ElementTag>
}
Live sample

Click anywhere in this paragraph and the text position under the pointer is reported below - which character, of which node.

caret output
Click inside the paragraph above.

Follow the selection

OnChange

selectionchange fires on every caret move, so it is frequent: read the selection inside the handler rather than doing work per event, and debounce anything expensive. The subscription below is live while this page is open - select something and watch it count.

Razor
@implements IAsyncDisposable
@inject Bit.Butil.Selection selection

@code {
    private ButilSubscription? _subscription;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        _subscription = await selection.OnChange(async () =>
        {
            var text = await selection.GetText();
            // ... update a floating toolbar ...
        });
    }

    public async ValueTask DisposeAsync()
    {
        if (_subscription is not null) await _subscription.DisposeAsync();   // stops listening
    }
}
Live sample

Start watching, then select part of this paragraph and drag the caret through it - the count below climbs with every move.

watch output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes window.getSelection. Returns default (false) during prerender/SSR instead of throwing.
Get
ValueTask<WindowSelection?> Get()
The current selection: text, collapsed state, range count, type and offsets.
GetText
ValueTask<string> GetText()
The selected text, or an empty string.
GetHtml
ValueTask<string> GetHtml()
The selected markup rather than its text. Page content - treat it as untrusted.
GetRects
ValueTask<Rect[]> GetRects()
One rectangle per line box the selection covers, in viewport coordinates.
GetBoundingRect
ValueTask<Rect?> GetBoundingRect()
The single rectangle enclosing the whole selection.
ContainsElement
ValueTask<bool> ContainsElement(ElementReference element, bool partly = true)
Whether an element is inside the selection.
SelectElement
ValueTask<bool> SelectElement(ElementReference element)
Selects an element and its contents.
SelectElementContents
ValueTask<bool> SelectElementContents(ElementReference element)
Selects everything inside an element but not the element itself.
SelectRange
ValueTask<bool> SelectRange(ElementReference element, int start, int end)
Selects a character range inside one element. False when the offsets fall outside its text.
GetRangeIn
ValueTask<SelectionOffsets?> GetRangeIn(ElementReference element)
Where the selection sits inside one element, in characters. Null when there is no selection, or when it isn't inside this element.
RemoveAll
ValueTask RemoveAll()
Clears the selection.
Collapse
ValueTask<bool> Collapse(bool toStart = false)
Collapses the selection to a caret at one of its ends.
Surround
ValueTask<bool> Surround(string tagName, string? className = null, string? style = null)
Wraps the selection in a new element. False when its ends are in different elements.
ReplaceWithText
ValueTask<bool> ReplaceWithText(string text)
Replaces the selection with plain text.
DeleteContents
ValueTask<bool> DeleteContents()
Deletes the selected content, leaving a caret in its place.
IsCaretFromPointSupported
ValueTask<bool> IsCaretFromPointSupported()
True when the runtime can resolve a point to a text position (either spelling of the API).
CaretFromPoint
ValueTask<CaretPosition?> CaretFromPoint(double x, double y)
The text position under a viewport point. Null when there is no text there.
OnChange
ValueTask<ButilSubscription> OnChange(Action handler)
Calls the handler on every selectionchange. Dispose the subscription to stop.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, detaches any selectionchange listener whose subscription was never disposed, and releases the JS callback reference.
An unhandled error has occurred. Reload 🗙