Implement the :has() pseudo class

NEW
Unassigned

Status

()

--
enhancement
11 years ago
a month ago

People

(Reporter: josh, Unassigned)

Tracking

(Blocks: 1 bug, {dev-doc-needed})

Firefox Tracking Flags

(Not tracked)

Details

(URL)

(Reporter)

Description

11 years ago
For numerous reasons, I would really like to have has-child and has-descendant selectors in CSS.  These selectors would select an element which has an immediate child matching a selector or a descendant matching a selector, respectively.

To avoid conflicting with any official CSS feature, this should most likely use CSS extension syntax, which would make the selectors -moz-has-child and -moz-has-descendant.

These selectors would allow rules such as:

/* Put a red border around forms that have a field with class="error". */
fieldset:-moz-has-child(.error) { border: 1px solid red; }

/* Show a message asking the user to mark at least one checkbox from
   the form. */
form.choices:-moz-has-descendant(*:checked) div.warning { display: none; }

/* Highlight the section containing the current anchor. */
div:-moz-has-descendant(*:target) { background-color: yellow; }

/* Put a page break before tables which have at least 20 rows, putting
   them at the top of their own page. */
table:-moz-has-child(tr:nth-of-type(20)) { page-break-before: always; }

/* Put a border between sections, treating divs as sections if they
   contain an h2 as their first child. */
div:-moz-has-child(h2:first-child) { border-bottom: 1px solid black; }

/* Picture the following as an Adblock Plus rule; it could easily
   replace many of the heuristics currently used to block the
   containing elements of ads, such as matching divs with a given
   width or style. */
div:-moz-has-child(img[href^="http://adserver.example.org"]) { display: none; }


I think these examples show some of the promise of these selectors.
These proposals look more like the :matches() and :has() proposals than the :subject proposal that was in early drafts of css3-selectors.  That said, they're all extremely inefficient to implement.  The danger of implementing them is that authors might use them, and then either authors or users will complain about how (unavoidably) slow they are.
(Reporter)

Comment 3

11 years ago
Looking at that proposal, I like :subject much better than the approach I've suggested.  :subject avoids introducing new syntax which lacks symmetry with the existing selectors.  All but one of the examples I gave seems to work with the :subject selector:

/* Put a red border around forms that have a field with class="error". */
fieldset:subject > .error { border: 1px solid red; }

/* Highlight the section containing the current anchor. */
div:subject *:target { background-color: yellow; }

/* Put a page break before tables which have at least 20 rows, putting
   them at the top of their own page. */
table:subject tr:nth-of-type(20) { page-break-before: always; }

/* Put a border between sections, treating divs as sections if they
   contain an h2 as their first child. */
div:subject h2:first-child { border-bottom: 1px solid black; }

/* Picture the following as an Adblock Plus rule; it could easily
   replace many of the heuristics currently used to block the
   containing elements of ads, such as matching divs with a given
   width or style. */
div:subject img[href^="http://adserver.example.org"] { display: none; }


The one example that doesn't work:

/* Show a message asking the user to mark at least one checkbox from
   the form. */
form.choices:-moz-has-descendant(*:checked) div.warning { display: none; }

That one requires applying subject and then applying further CSS combinators starting from the element chosen by :subject:

(form.choices:subject *:checked) div.warning

While useful, I can definitely live without that, and it might prove inefficient as well as complex to implement.

However, the :subject selector itself does not seem fundamentally less efficient than other CSS selectors or combinators.  It does seem harder to implement, but not computationally harder.  The main difficulty seems more likely to come from the need to process child elements before rendering the parent.  However, in terms of computation, what if Gecko simply matched the CSS rule precisely as it would without :subject, and then *once it found a matching element* it went up to the :subject element and applied the style?  That seems no less efficient than existing CSS rules.
It is computationally harder.  Just matching as though :subject weren't there is far from trivial (if possible at all), for a number of reasons:
 * "div:subject p" can match many divs for a single p
 * "div:subject :link:hover" mean we need to recompute style for the entire document when somebody changes whether they're hovering over a link.

That said, by caching a bunch of things in various places and being very careful about what caches to invalidate when, it might be possible.
Er, what I meant is that any reasonably simple way of implementing it is computationally harder.  However, there might be some tricks we could use to avoid that, but they're far from trivial, if possible at all.
Actually, if we cache on each node (or maybe even on the style context -- that might be a little trickier) the list of nodes matching rules that would normally have it as a subject but instead had a :subject, it probably wouldn't be that bad, although processing the reresolution process would be tricky.  We'd need to build lists of what needed rules changed outside the tree we're reresolving style on -- and then we'd need something like ReParentStyleContext to do a bunch of rebuilding.  Except to make this efficient we'd probably want to do something like cache an mNextRuleNode on style contexts -- along with caching both old and new lists of the external rules.  Or something roughly like that.
Summary: CSS parent (has-child) and ancestor (has-descendant) selectors → CSS parent (has-child) and ancestor (has-descendant) selectors (:subject)
Caching on the style context would be tricky because of the FindChildWithRules optimization.
The other things that would be bad are:
 * potentially messy re-resolution as we matched rules on descendants (for style contexts that we'd already resolved)

 * having to do style resolution inside display:none subtrees just to match things whose :subject is outside the subtree

Updated

4 years ago
Blocks: 693083
Keywords: dev-doc-needed

Comment 10

4 years ago
The current editors draft proposes :has().
Summary: CSS parent (has-child) and ancestor (has-descendant) selectors (:subject) → Implement the :has() pseudo class
Blocks: 1169065

Comment 11

8 months ago
How much priority should this have?
It's been years and still no priority.... To be honest I really wanted to start using this on my websites....
Flags: needinfo?(mreavy)

Comment 12

8 months ago
Just so you know: If you use jQuery then you can already use it.

Comment 13

8 months ago
I think you got it wrong. This is for CSS. jQuery is a javascript library, m8...
If it was about finding such stuff with javascript, I would have already solved it quite long ago. Currently, I have a javascript hack to do the job which is not fast enough for our objectives.
(In reply to brunoais from comment #13)
> I think you got it wrong. This is for CSS. jQuery is a javascript library,
> m8...
> If it was about finding such stuff with javascript, I would have already
> solved it quite long ago. Currently, I have a javascript hack to do the job
> which is not fast enough for our objectives.

:has would only be available on the static profile (that is, querySelector), so it wouldn't help you anyway? It'd be way to slow to allow its use in stylesheets.

Comment 15

8 months ago
If I understood right, the ":has()" has the same meaning as the proposed "$", "^" or "!" selector.
Why do you believe it is a slow selector? The node being selected will be in the nodes found stack when running the processing.

If I have:
<div class="a">
	<div class="b1">
		<div class="c1">
			<div class="d1">Must not find b container of me</div>
		</div>
	</div>
	<div class="b2">
		<div class="c2">
			<div class="d2 findMe">Find b countainer of me</div>
		</div>
	</div>
</div>

Example 1:

a *:has(.findMe)
->
Finds all that match ".findMe" (indexed class search)
Go up the tree.... Pass through (in order):
- .c2 (push .c2 node as possible apply style)
- .b2 (push .b2 node as possible apply style)
- a (Found a)

Add to the cascade table: apply style to c2 and b2

Example 2:

:has(.findMe)
->
Finds all that match ".findMe" (indexed class search)
Go up the tree.... Pass through (in order):
- .c2 (push .c2 node as possible apply style)

Add to the cascade table: apply style to c2

Example 2:

a:has(.d1)
->
Finds all that match ".d1" (indexed class search)
Go up the tree.... Pass through (in order):
- .c1
abort


I know these are cases where only one node is found by rightmost selector but the basic idea is the exact same
Regardless, :emilio do explain why is :has() is so slow so not to implement on a stylesheet? The above comments approach towards a possible solution before stylo. Now, with stylo, the performance results can be quite different
Flags: needinfo?(emilio)
Cherry-picking most trivial cases to prove the point? Nice try. 

First of all you have no way to decide if you should go up or down the tree to be "faster" and either of will be slow in most cases really. Let's take a:has(.d1). Imagine there are 10000 .d1 nodes and one a. And now imagine there are 1 .d1 and 10000 a. And additionally there might be really deep tree. And then you can have div:has(a) and now what you do?

As for :has selector, currently you can use XPath (better and faster than implementing :has by hand in JS) where you can make query explicitly specifying if you want to go up or down the tree, and have other constrains that reduce search space.

Bottom line is if you really need the :has selector you might want to change your design.
You're making assumptions about how a browser does style computation that are not true in any browser.

The way browser compute the style for an element is checking whether the same element matches all the selectors in the site, not the other way around (as you're suggesting, given a selector, finding all the elements that match, and stashing the result in some kind of global table).

That is, selector-matching works looking up and to the left of the tree following combinators. To implement :has you need to potentially look at all the descendants of an element, which is something that no other selector requires you to do.

Additionally, supporting :has in stylesheets would have another deep implication, which is that a change in an element could now affect the style of its ancestors, which is just something that doesn't happen with other selectors, and that engines rely on to handle dynamic changes efficiently.
Flags: needinfo?(emilio)
(Reporter)

Comment 18

8 months ago
> If I understood right, the ":has()" has the same meaning as the proposed "$", "^" or "!" selector.

I haven't seen those, and I don't know the right search terms to find them. Would you mind pointing to a reference for them?

Comment 19

8 months ago
:kasper93 :
Oh, you mean:
Situation 1:
A web page with 10000 .d1 nodes and a single a node. In that case, the difficulty of:
"a:has(.d1)" is the same as "a .d1". "a .d1" already exists as valid so I don't see anything wrong there.

Situation 2:
A web page with a single .d1 node and 1000 a nodes. In that case, the difficulty of:
"a:has(.d1)" is, indeed, more complex than "a .d1". That is because the algorithm always has to run until the ":root" tag to find all matches.
If the node with class "d1" is not inside any of the <a> tags, then the computation required is the same as there was no ":has()". Otherwise, it relies on how deep the single tag of class "d1" is. In a realistic situation, though, a tag is not placed 1000 tags deep. In the most complex websites, the closest ":root" or ":scope" tag is no more than 50 tags deep. In such realistic situation, only, at most, 47 tags would be the a tags referred there. The rest to the 1000 a tags would be ignored while processing that selector because they would not be in the upwards search graph.

As for: "div:has(a)"... What's the issue with that one? 
It is true it is more efficient to have "div:has(> a)" but it is still the same kind of thing. I don't get the problem with that

:emilio
> The way browser compute the style for an element is checking whether the same element matches all the selectors in the site, not the other way around (as you're suggesting, given a selector, finding all the elements that match, and stashing the result in some kind of global table).
OK

> That is, selector-matching works looking up and to the left of the tree following combinators. To implement :has you need to potentially look at all the descendants of an element, which is something that no other selector requires you to do.
True from a conceptual standpoint but it doesn't have to be implemented that way.

> Additionally, supporting :has in stylesheets would have another deep implication, which is that a change in an element could now affect the style of its ancestors, which is just something that doesn't happen with other selectors, and that engines rely on to handle dynamic changes efficiently.
OK. That is a good point.

> I haven't seen those, and I don't know the right search terms to find them. Would you mind pointing to a reference for them?
It's not easy to find... Still... I found a commit of when the idea of w3c at the time was to use "!"
https://github.com/w3c/csswg-drafts/blob/3e071f642fa737adce2a7c8529606049f29fdaff/selectors/Overview.html#L1245

The "$" was very very short lived... after 20 minutes looking for it in the drafts, I couldn't find it.
The "^" I don't remember but I did find it being mentioned in an article from that time.
See: https://hugogiraudel.com/2014/02/13/parent-selector/

There are mentions about "!" in one of the comments of: https://css-tricks.com/parent-selectors-in-css/ back when it was very young.

I think that's about it of what I can find. There were not many news at the time too.
Flags: needinfo?(mreavy)

Comment 20

7 months ago
(In reply to David Baron :dbaron: 🏴󠁵󠁳󠁣󠁡󠁿 ⌚UTC-7 from comment #1)
> ...
> ...
> ... extremely inefficient to implement.  ...
> ... will complain about how (unavoidably) slow they are.

Can't the algorthim just break up at a certain point, when the nesting/recursion (or whatever makes it inefficient) gets too deep/high and leave a warning in the console?

Comment 21

7 months ago
You forgot to "?needinfo" him
Flags: needinfo?(dbaron)

Comment 22

7 months ago
I actually thought this would be a rule that can be almost directly solved...
For "a:has(b.show)", for example, it would just be the same work as matching "a b.show" except it is "a" tag that gets the rules applied instead of "b" tag.....
(In reply to altugtekin85 from comment #20)
> Can't the algorthim just break up at a certain point, when the
> nesting/recursion (or whatever makes it inefficient) gets too deep/high and
> leave a warning in the console?

A warning in the console and a broken web page?  I don't think such an approach could work unless the limits were specified, but it's not clear what it is you're proposing to limit, or how that would interfere with the use cases for the feature.  (I could try to work that out... but it seems like a decent amount of work that isn't an appropriate use of "needinfo?".)
Flags: needinfo?(dbaron)

Comment 24

7 months ago
In your first comment you state that:
(In reply to David Baron :dbaron: 🏴󠁵󠁳󠁣󠁡󠁿 ⌚UTC-7 from comment #1)
> ... they're all extremely inefficient to implement. The danger of implementing
> them is that authors might use them, and then either authors or users will
> complain about how (unavoidably) slow they are.

Given that statement I concluded, that when the browser is evaluating the CSS containing the "has()" it somehow enters an inefficient loop, which has an exponential (or n^99) runtime. And my thought was that, if I knew the source code better and had the time to edit it, I would simply count the number of iterations the browser spends in the loop and eventually at a fixed value break out of the loop, so that nobody complains about bad performance, but on the other hand leaves a warning, thsat the developer must change his/her CSS in order to make it work again.

(In reply to David Baron :dbaron: 🏴󠁵󠁳󠁣󠁡󠁿 ⌚UTC-7 from comment #23)
> (In reply to altugtekin85 from comment #20)
> > Can't the algorthim just break up at a certain point, when the
> > nesting/recursion (or whatever makes it inefficient) gets too deep/high and
> > leave a warning in the console?
> 
> ... but it's not clear what it is you're proposing to limit ...

Was the above explanation elaborate enough?

Comment 25

7 months ago
A reference: This feature is implemented first(?) in Prince 12 https://www.princexml.com/. See the documentation here: https://www.princexml.com/doc-refs/ [search ":has("]. I use it to generate PDF from XML or HTML.

I would like to use this feature in Firefox to preview a _static_ (CSS formatted) XML document. Is this case part of "snapshot profile" described in CSS 4 Selector Specification here: https://drafts.csswg.org/selectors/#snapshot-profile?
You need to log in before you can comment on or make changes to this bug.