Title: CSS Custom Highlight API Module Level 1
Level: 1
Shortname: css-highlight-api
Status: ED
Work Status: exploring
Group: csswg
ED: https://drafts.csswg.org/css-highlight-api-1/
Editor: Florian Rivoal, On behalf of Bloomberg, https://florian.rivoal.net/, w3cid 43241
Editor: Sanket Joshi, Microsoft Corporation https://www.microsoft.com, https://github.com/sanketj
Editor: Theresa O'Connor, w3cid 40614, Apple Inc. https://apple.com/, hober@apple.com
Abstract:
This CSS module describes a mechanism
for styling arbitrary ranges of a document identified by script.
Complain About: accidental-2119 yes
spec:css-color-4; type:property; text:color
spec:css-pseudo-4; type:dfn; text:highlight overlay
Introduction
This section is non-normative.
The Custom Highlight API extends the concept of [=highlight pseudo-elements=] (see [[css-pseudo-4#highlight-pseudos]])
by providing a way for web developers to style the text
of arbitrary Range objects,
rather than being limited to the user agent defined
''::selection'',
''::inactive-selection'',
''::spelling-error'',
and '''::grammar-error'''.
This is useful in a variety of scenarios,
including editing frameworks that wish to implement their own selection,
find-on-page over virtualized documents,
multiple selection to represent online collaboration,
or spellchecking frameworks.
The Custom Highlight API provides a programmatic way of adding and removing highlights
that do not affect the underlying DOM structure,
but instead applies styles to text based on [=range=] objects,
accessed via the ''::highlight()'' pseudo element.
The following code uses the ''::highlight()'' pseudo-element
to apply a yellow background and blue foreground color to the text
One two
.
It does so by adding a {{Highlight}} to the {{HighlightsRegister}}
(both of these are new concepts introduced by this specification).
The {{Highlight}} will contain a {{Range}} whose boundary points surround the text
One two
.
One two three…
The result would look like:
One Two three…
Module Interactions
This module depends on the Infra Standard [[!INFRA]]
and on WebIDL [[!WebIDL]].
It assumes general familiarity with CSS
and with the DOM Standard [[DOM]],
and specifically extends the mechanisms defined in CSS Pseudo-Elements Module Level 4 [[!css-pseudo-4]]
to handle [=highlight pseudo-elements=].
The Selectors Level 4 [[!selectors-4]] specification defines how [=pseudo-elements=] work in general.
See [[#references]] for a full list of dependencies.
Note: This draft is an early version.
As it matures, the CSS-WG could decide to keep it as an independent module,
or might prefer to fold it into [[css-pseudo-4]],
or a later version of that module.
Setting up Custom Highlights
Creating Custom Highlights
A custom highlight is a named collection of [=ranges=]
representing portions of a document.
They do not necessarily fit into the element tree,
and can arbitrarily cross element boundaries without honoring its nesting structure.
They can be used to affect the appearance of these portions of the document
(see [[#styling-highlights]]),
or to handle to events associated with them
(see [[#events]]).
[=Custom highlights=] are represented by
Highlight objects,
[=setlike=] objects whose [=set entries=] are {{AbstractRange}} objects.
[=Ranges=] can be added to a [=custom highlight=]
either by passing them to its constructor,
or by using the usual API of [=setlike=] objects
to manipulate its [=set entries=].
Note: As the [=ranges=] in a [=custom highlight=] are {{AbstractRange}} objects,
authors can chose between using {{Range}} objects and {{StaticRange}} objects.
See [[#range-invalidation]] for more details about this choice and its implications.
The name of a [=custom highlight=]
is represented by its {{Highlight/name}} attribute,
which must must be a valid <>.
[Exposed=Window]
interface Highlight {
constructor(CSSOMString name, AbstractRange... initialRanges);
setlike;
attribute double priority;
readonly attribute CSSOMString name;
};
See [[#priorities]] for more information on the {{Highlight/priority}} attribute.
When the
Highlight(CSSOMString name, AbstractRange... initialRanges) constructor is invoked,
run the following steps:
-
Let |highlight| be the new {{Highlight}} object.
-
If {{name!!argument}} does not [=CSS/parse=] as an <>, then [=throw=] a {{"SyntaxError"}}.
-
Let |nameArg| be the result of [=converted to an ECMAScript value|converting=] {{name!!argument}} to an ECMAScript value.
-
Set |highlight|'s {{Highlight/name}} to |nameArg|
-
Set |highlight|'s {{Highlight/priority}} to
0
.
-
For each |range| of {{initialRanges}},
let |rangeArg| be the result of [=converted to an ECMAScript value|converting=] |range| to an ECMAScript value,
then run [[webidl#es-add-delete|the steps for a built-in setlike add function]],
with |highlight| as the
this
value,
and |rangeArg| as the argument.
-
Return |highlight|.
Registering Custom Highlights
In order to have any effect,
[=custom highlights=] then needs to be
[=registered=] it into the [=highlights register=].
The highlights register is accessed via the {{CSS/highlights}} attribute of the {{CSS}} namespace,
and represents all the [=custom highlights=] [=registered=] for the [=current global object=]’s [=associated Document=].
It is a [=setlike=], and can be updated using the usual methods.
It's [=set entries=] is initially empty.
A [=custom highlight=] is said to be registered
if it is in the [=highlights register=].
It stops being [=registered=] if it is later removed.
partial namespace CSS {
readonly attribute HighlightsRegister highlights;
};
[Exposed=Window]
interface HighlightsRegister {
setlike;
HighlightsRegister add(Highlight value);
};
To [=register=] a [=custom highlight=],
invoke the {{HighlightsRegister/add()}} of the [=highlights register=]
with the [=custom highlight=] as the argument.
When invoked,
the add(Highlight value) method must run these steps:
1. If there is already a [=set entry=] with the same {{Highlight/name}} as the {{Highlight/name}} of {{value}},
then [=throw=] an {{"OperationError"}}.
3. Let |valueArg| be the result of [=converted to an ECMAScript value|converting=] {{value}} to an ECMAScript value.
4. Let |result| be the result of running [[webidl#es-add-delete|the steps for a built-in setlike add function]],
with the [=context object=] as the this
value
and with |valueArg| as the argument.
5. Return |result|.
Styling Custom Highlights
The Custom Highlight Pseudo-element: ''::highlight()''
The ::highlight(<>) pseudo-element
(also known as the custom highlight pseudo-element)
represents the portion of a document that
is being [=contained=] or [=partially contained=]
in all the [=ranges=] of the [=registered=] [=custom highlight=]
with the [=custom highlight/name=] <>,
if any.
<> must be a valid CSS <>.
Processing Model
Applicable Properties
[=Custom highlight pseudo-elements=],
like the built-in [=highlight pseudo-elements=],
can only be styled with a limited set of properties.
See [[css-pseudo-4#highlight-styling]] for the full list.
Cascading and Inheritance
The [=cascading=] and [=inheritance=] of [=custom highlight pseudo-elements=] is handled
identically to that of the built-in [=highlight pseudo-elements=],
as defined in [[css-pseudo-4#highlight-cascade]].
Painting
The painting of [=custom highlights=] is also handled
identically to that of the built-in [=highlight pseudo-elements=],
as specified in
[[css-pseudo-4#highlight-bounds]] and [[css-pseudo-4#highlight-painting]],
with the following clarifications:
-
[=Collapsed=] [=ranges=] are not rendered.
-
Overlapping [=ranges=] within a single [=custom highlight=] are rendered
as if a single range representing the union of the overlapping ones
had been specified.
The following example renders in a single highlight with semi-transparent blue background,
not two overlapping ones which can be seen through each other.
Lorem Ipsum.
In other words, this rendering would be correct:
Lorem Ipsum.
However, this one would be incorrect:
Lorem Ipsum.
-
The [=highlight overlays=] of the [=custom highlights=]
are above those of the built-in [=highlight pseudo-elements=]
in the stacking order described in [[css-pseudo-4#highlight-painting]].
-
The relative stacking order of the [=highlight overlays=]
of multiple [=custom highlights=]
is defined by their [=priority=]
(see [[#priorities]]).
Priority of Overlapping Highlights
A [=custom highlight=]'s {{Highlight/priority}} attribute
defines its priority.
This is used to determine the stacking order of the corresponding [=highlight overlay=]
during painting operations (see [[#painting]]).
A higher [=priority=] results in being above in the stacking order.
When two ore more [=custom highlights=] have the same numerical priority,
the one most recently [=registered=] has the higher effective [=priority=].
Issue(4593): should negative numbers mean stacking
below the built-in [=highlight pseudo-elements=]?
Issue(4592): Should priority be an (unsigned) integer instead?
That would make comparisons more reliable,
but would likely lead to numbering reminiscent of BASIC line numbers.
Issue(4591): Should we drop priority by numbers entirely,
and replace it with some other ordering mechanism?
Experience with BASIC line number or z-index
does not give much confidence that ordering by number is a good idea.
Is placing in an ordered data-structure better?
Should authors be able to express a desired to be placed above/below
other named highlights,
and let the UA figure it out?
Issue(4594): Should the built-in [=highlight pseudo-elements=]
be exposed as well,
so that they too can be reordered,
and so that they can be interleaved with custom ones freely?
Some text
As there are no priorities set
(i.e. there is a tie between
rg1
and
rg2
),
the custom highlights' styles are stacked
in order of insertion into the [=highlights register=].
The rendered results will have "Som" with blue text on yellow background,
"e t" with blue text on orange background,
and "ext" with the default color on orange background.
Some text
Setting
rg1.priority = 1;
would cause
rg1
to stack higher than
rg2
,
which would result in "Some t" being blue on yellow,
and "ext" being default color on orange.
Some text
Responding to Changes
Repaints
The addition or removal
of a [=custom highlight=] in the [=highlights register=],
or of a [=range=] in a [registered=] [=custom highlight=],
must cause the User Agent to reevaluate the rendering,
and to repaint if appropriate.
The User Agent must also repaint highlights as needed
in response to changes by the author
to the {{Highlight/priority}},
or to the [=boundary points=] of {{Range}}s
of a [=registered=] [=custom highlight=].
Issue(4596): How should we specify the timing (and synchronicity) of this reevaluation?
Range Updating and Invalidation
Authors can build [=custom highlights=] using either {{Range}}s or {{StaticRange}}s.
The resulting [=custom highlight=] represents the same parts of the document,
and can be styled identically.
However, the behavior is different
in case the underlying document is modified.
{{Range}}s are [=live ranges=].
The User Agent will adjust the [=boundary points=] of {{Range}}s
in response to DOM changes overlapping the range or at its boundary,
and [[#repaint|repaint]] accordingly.
[=Boundary points=] of [=live ranges=] can also be changed
by the author.
On the other hand,
the User Agent must not adjust the [=boundary points=] of {{StaticRange}}s
in response to DOM changes,
nor can they be modified by the author after creation.
Updating all {{Range}} objects as the DOM is modified
has a significant performance cost.
Authors who intend to observe DOM changes and react to them
by adjusting or recreating the ranges in their [=custom highlights=]
are strongly encouraged to user {{StaticRange}}s
in order to avoid this costly but unnecessary step.
Conversedly, authors who use {{StaticRange}}s
should observe and react to DOM changes,
by discarding stale [=ranges=] or [=custom highlights=]
and recreating new ones.
When computing how to render the document,
if [=start node=] or [=end node=] of any [=range=]
in the [=highlights register=]
refer to a {{Node}} which is no longer [=in a document tree=],
the User Agent must ignored that [=range=].
If the [=start offset=] or [=end offset=] of any [=range=]
are greater than the corresponding node’s length,
The User Agent must behave as if it was equal to that length.
Issue(4597): As far as I am aware,
prior uses of {{StaticRange}}s were for [=ranges=] created by the User Agent
and passed to the author.
Here, it's the other way around,
which raises (for the first time?)
the question of invalidation of static ranges.
Can the above work?
Is it Fast enough that it's worth distinguishing static and live ranges?
Would some alternative handling be better?
Issue(4598): The interaction of {{StaticRange}}s in a [=custom highlight=]
and [[css-contain-2]]
seems problematic:
on a fully contained element,
you should expect that DOM changes to descendants of that element
will not cause invalidation and restyling/repainting
of elements outside the contained one.
However, if a static range has a boundary point inside the contained subtree
and another boundary point outside of it,
and the DOM in the contained subtree is changed
so that the boundary point inside no longer points to a valid node,
the whole range should be ignored,
which would affect painting outside the contained subtree.
Is this a weakness of [=style containment=],
or of the invalidation logic above,
or something else?
Event Handling
Issue: Section on Events TBD, based on https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/master/highlight/events-explainer.md
Issue: should custom highlights have a dedicated event handling mechanism,
or should that be added to pseudo-elements in general?
Appendix A. Privacy and Security Considerations
This section is non-normative.
Issue: Provide summary of Privacy and Security Considerations for this specification
Note: The
TAG has developed a
self-review questionnaire
to help editors and Working Groups evaluate the risks introduced by their specifications.
This document was used in preparing this section.
Appendix B. Acknowledgements
This section is non-normative.
Issue: Acknowledge people (other than editors) who deserve credit for this.