MarkHighlighter
Description
Marks every occurrence of a keyword inside rendered content, the DOM equivalent of find-in-page
MarkHighlighter is a static helper rather than an IComponent. It walks every text node under a root element, descending into same-origin iframes, and marks the ones that match a keyword. Adapted from mark.js.
Where the browser supports the CSS Custom Highlight API — MarkHighlighter.IsHighlightApiSupported answers that — matches in the page itself are painted through CSS.highlights as live ranges, with no DOM mutation at all, so components holding references to their own text nodes are untouched. The registry names tss-marked and tss-marked-focused are styled by tss.markhighlighter.css.
Three cases get the classic treatment instead, each match wrapped in a <mark data-marked="true"> element and unwrapped again on unmark: text inside iframes, whose documents do not carry the page stylesheet; browsers without the API; and callers that opt out by setting MarkOptions.UseHighlightApi = false or a custom Element, MarkData or ClassName.
Matching is case-insensitive by default, folds diacritics (searching cafe finds café), and merges whitespace runs, so a keyword typed with one space matches text with many. Text inside script, style, title, head and already-marked elements is skipped.
Passes on the same root are serialized: starting a new mark or unmark cancels the in-flight one and waits for it, so calling on every keystroke is safe without guarding it yourself. Large documents are walked cooperatively — the pass yields to the event loop periodically — so the page stays responsive and cancellation engages mid-pass.
Usage
| Method | What it does |
|---|---|
MarkAsync(ctx, keyword, options, eachCb, cancellationToken) |
Marks every occurrence of keyword under ctx. eachCb receives one MarkedMatch per match in document order; collect them for navigation. |
MarkAsync(ctx, keyword, eachCb, cancellationToken) |
The element-only legacy shape: forces element wrapping and reports each wrapper element individually. |
UnmarkAsync(ctx, options, cancellationToken) |
Removes every mark under ctx: registry ranges are deregistered, wrapper elements unwrapped and their parents re-normalized. Call it before re-marking with a new keyword. |
FocusResult(ctx, match, scrollIntoViewIfNeeded) |
Moves the focused-match highlight to one match and clears it from the others. |
IsHighlightApiSupported |
Feature check for the CSS Custom Highlight API. |
A MarkedMatch carries either Range (highlight registry) or Elements (the wrapper elements, one per crossed text node), plus Text. FocusResult handles both: a registry match joins the tss-marked-focused highlight, which has priority above the base one, and is scrolled into view by nudging its scrollable ancestors; a wrapped match takes the class tss-highlight-focused plus an inline theme danger background, so it also shows inside iframes, and the standard scrollIntoView. An HTMLElement overload remains for element-only callers.
MarkOptions are all optional, and anything unset falls back to the static defaults:
| Option | Effect |
|---|---|
Separator |
split the keyword into several terms |
SeparateWordSearch |
match each word of the keyword on its own |
WholeWord |
require word boundaries around the match |
Wildcards |
* matches any run of non-space characters, ? one optional non-space character |
IgnoreJoiners |
match across soft hyphens and zero-width joiners |
MinLength |
skip keywords shorter than this |
AcrossElements |
match phrases split by inline tags (<b>web</b> applications). Through the registry a spanning match is one range; wrapped, it becomes one mark element per crossed text node, all in that match's Elements |
UseHighlightApi |
null (default) uses the highlight API when supported and no custom Element / MarkData / ClassName is set; true uses it whenever supported; false always wraps in elements. Iframe text is always wrapped |
Because per-call options exist, the static fields (MarkHighlighter.Element, .MarkData, .ClassName, RegExpCreator.CaseSensitive) are only defaults. Pass MarkOptions so independent surfaces never fight over global state.
API reference
public static class MarkHighlighterMarks every occurrence of a keyword inside a DOM subtree (same-origin iframes included) and unmarks it again. Where the browser supports the CSS Custom Highlight API, matches in the page itself are painted through the highlight registry (see tss.markhighlighter.css) with no DOM mutation at all; text inside iframes, browsers without the API, and callers that opt out get the classic wrap-in-mark-element treatment. Passes on the same root are serialized: starting a new mark or unmark cancels and awaits the previous one, so callers can fire on every keystroke without racing themselves. Adapted from mark.js (https://github.com/julkue/mark.js, MIT).
- Namespace
- Tesserae
Properties
| Name | Description |
|---|---|
| IsHighlightApiSupported | Whether this browser has the CSS Custom Highlight API (CSS.highlights and the Highlight constructor). |
Methods
| Name | Description |
|---|---|
| ScrollRangeIntoView | Brings a highlight-registry match into view: unlike an element, a range has no scrollIntoView, so each scrollable ancestor is nudged until the range's rect is visible |
| RunExclusiveAsync | Runs one pass at a time per root: a new pass cancels the in-flight one and waits for it to finish, so a mark can never interleave with the unmark it replaces |
private static void ScrollRangeIntoView(Range range)Brings a highlight-registry match into view: unlike an element, a range has no scrollIntoView, so each scrollable ancestor is nudged until the range's rect is visible
private static async Task RunExclusiveAsync(HTMLElement ctx, Func<CancellationToken, Task> pass, CancellationToken cancellationToken)Runs one pass at a time per root: a new pass cancels the in-flight one and waits for it to finish, so a mark can never interleave with the unmark it replaces
Fields
| Name | Description |
|---|---|
| Element | Default wrapper tag, used when Element is unset. |
| MarkData | Default data-* attribute name, used when MarkData is unset. |
| ClassName | Default extra class on each wrapper, used when ClassName is unset. |
public static string Element = "mark"Default wrapper tag, used when Element is unset.
public static string MarkData = "marked"Default data-* attribute name, used when MarkData is unset.
public sealed class MarkOptionsPer-call configuration for MarkHighlighter and RegExpCreator. A field left unset falls back to the matching static default, so two surfaces highlighting at the same time never fight over global state.
- Namespace
- Tesserae
Properties
| Name | Description |
|---|---|
| Element | Tag name of the wrapper element. Default: Element ("mark"). |
| MarkData | Name of the data-* attribute stamped on wrapped elements. Default: MarkData ("marked"). |
| ClassName | Extra class set on each wrapper element. Default: ClassName. |
| CaseSensitive | Case-sensitive matching. Default: CaseSensitive. |
| Diacritics | Fold diacritics, so 'cafe' also matches 'café'. Default: true. |
| WholeWord | Only match the keyword where it is not part of a longer word. |
| SeparateWordSearch | Split a multi-word keyword on whitespace and mark each word on its own. |
| Wildcards | Enable '*' (any run of non-space characters) and '?' (one optional non-space character) in the keyword. |
| IgnoreJoiners | Match across soft hyphens and zero-width joiners, which hyphenated or justified documents carry mid-word. |
| MinLength | Keywords shorter than this are not marked. Default: 0 (everything is marked). |
| AcrossElements | Match across element boundaries, so a phrase split by inline tags (boldweb applications) is still found. |
| UseHighlightApi | Whether matches are painted through the CSS Custom Highlight API (CSS.highlights + the ::highlight() rules in tss.markhighlighter.css) instead of wrapping them in mark elements - no DOM mutation at all, so components that hold references to their own text nodes are untouched. Null (default): use it when the browser supports it and no custom Element/MarkData/ClassName is asked for. True: use it whenever supported. False: always wrap in elements. Text inside iframes is always wrapped in elements - a highlight registration is per document and an iframe's document doesn't carry the ::highlight() rules. |
public string Element { get; set; }Tag name of the wrapper element. Default: Element ("mark").
public string MarkData { get; set; }Name of the data-* attribute stamped on wrapped elements. Default: MarkData ("marked").
public string ClassName { get; set; }Extra class set on each wrapper element. Default: ClassName.
public bool? CaseSensitive { get; set; }Case-sensitive matching. Default: CaseSensitive.
public bool Diacritics { get; set; }Fold diacritics, so 'cafe' also matches 'café'. Default: true.
public bool WholeWord { get; set; }Only match the keyword where it is not part of a longer word.
public bool SeparateWordSearch { get; set; }Split a multi-word keyword on whitespace and mark each word on its own.
public bool Wildcards { get; set; }Enable '*' (any run of non-space characters) and '?' (one optional non-space character) in the keyword.
public bool IgnoreJoiners { get; set; }Match across soft hyphens and zero-width joiners, which hyphenated or justified documents carry mid-word.
public int MinLength { get; set; }Keywords shorter than this are not marked. Default: 0 (everything is marked).
public bool AcrossElements { get; set; }Match across element boundaries, so a phrase split by inline tags (boldweb applications) is still found.
public bool? UseHighlightApi { get; set; }Whether matches are painted through the CSS Custom Highlight API (CSS.highlights + the ::highlight() rules in tss.markhighlighter.css) instead of wrapping them in mark elements - no DOM mutation at all, so components that hold references to their own text nodes are untouched. Null (default): use it when the browser supports it and no custom Element/MarkData/ClassName is asked for. True: use it whenever supported. False: always wrap in elements. Text inside iframes is always wrapped in elements - a highlight registration is per document and an iframe's document doesn't carry the ::highlight() rules.
public sealed class MarkedMatchOne keyword match reported by MarkAsync. Depending on the backend that painted it, the match is either wrapped in mark elements or registered as a live range in the CSS custom highlight registry - exactly one of Elements and Range is set. Hand the match back to FocusResult to focus it.
- Namespace
- Tesserae
Properties
| Name | Description |
|---|---|
| Elements | The wrapper elements of this match, in document order - one per text node the match crosses. Null when the match is painted through the CSS highlight registry. |
| Range | The live range painted through the CSS highlight registry. Null when the match is wrapped in elements (iframes, browsers without the API, or opted out). |
| Text | The matched text. |
public HTMLElement[] Elements { get; internal set; }The wrapper elements of this match, in document order - one per text node the match crosses. Null when the match is painted through the CSS highlight registry.
public Range Range { get; internal set; }The live range painted through the CSS highlight registry. Null when the match is wrapped in elements (iframes, browsers without the API, or opted out).
See also
- Search Box — the input that usually drives it
- Omni Result — a search-result row that highlights its own hits
- Component catalog