w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.43k stars 656 forks source link

[cssom] Implement CSSStyleRule.p.matches #3670

Open jridgewell opened 5 years ago

jridgewell commented 5 years ago

With style rules nested inside @media and @support conditions, it's a bit difficult to determine if a style rule applies to an element. Is it possible to expose a matches method that would do this for us?

const div = document.querySelector('div');
const span = document.querySelector('span');

{
  // Eg, `@media screen { div { color: red; } }`
  const rule = document.styleSheets[0].cssRules[0].cssRules[0];

  rule.matches(div);
  // => true
  rule.matches(span);
  // => false
}

{
  // Eg, `@media not screen { div { color: red; } }`
  const rule = document.styleSheets[1].cssRules[0].cssRules[0];

  rule.matches(div);
  // => false
  rule.matches(span);
  // => false
}
emilio commented 5 years ago

I think that makes sense, but some questions:

So if I understand correctly the way to do it right now would be something like:

function singleRuleMatches(rule: CSSRule, element: Element) {
  if (rule instanceof CSSStyleRule)
    return element.matches(rule.selectorText);
  if (!rule instanceof CSSConditionRule)
    return true;
  if (rule instanceof CSSMediaRule)
    return window.matchMedia(rule.conditionText).matches;
  if (rule instanceof CSSSupportsRule)
    return CSS.supports(rule.conditionText);
  throw "Unknown condition rule?";
}

function singleSheetMatches(sheet: StyleSheet, element: Element) {
  if (sheet.disabled)
    return false;
  if (!window.matchMedia(Array.from(sheet.media).join(",")).matches)
    return false;
  return true;
}

function ruleMatchesElement(rule: CSSRule, element: Element) {
  for (let r = rule; r; r = r.parentRule)
    if (!singleRuleMatches(r, element))
      return false;
  for (let s = rule.parentSyleSheet; s; s = s.parentStyleSheet)
    if (!singleSheetMatches(s, element))
      return false;
  return true;
}

Is that right? A bit of a simpler approach would be to add something like CSSConditionRule.prototype.matches(document) or something, that'd avoid the questions at least. Though you'd need to still check for stylesheets and such manually...

jridgewell commented 5 years ago

Should media queries be evaluated in the element's document? Or in the stylesheet's document? Or the current global's document? All three may be different and have different viewports, etc.

I think maybe the stylesheet's doc would be simplest to understand? Maybe a TypeError if the element belongs to a different document than the stylesheet.

Should it account for the StyleSheet's media attribute? disabled too, maybe?

I hadn't thought of that. I would think yes, we should include them in the check.

What about if it's an alternate sheet?

If the alternate were active, it should match normally. If not, everything should return false.

Or an @import-ed sheet?

I'm not 100% clear how imported sheets are represented in the CSSOM. From your code sample, I'm assuming they just have parentSyleSheets? If so, I think checking each parentStyleSheet to see if they also apply is the right choice.

emilio commented 5 years ago

Note that my code is completely untested, I just wrote it on the GitHub comment box. Should be p. close though, hopefully :)

Another question, what should happen if the rule is unparented from a stylesheet? That is, the rule hierarchy has been removed, but somebody still holds a reference to that? I guess it should return true? After all the rule itself matches, though it feels slightly inconsistent with looking to whether the associated stylesheet matches...

tabatkins commented 5 years ago

My guess is that the intent of this isn't to find out if an element could theoretically match some rule, but rather to see if it actually does, at this moment, match the rule.

So all the weird conditions that would cause the element to not be styled by the stylesheet, or the rule in particular, would all cause the function to return false.

tabatkins commented 5 years ago

@jridgewell Tho, of course, your initial post doesn't mention an actual use-case, so I'm guessing here. What sort of things were you intending to do with this information?

jridgewell commented 5 years ago

Another question, what should happen if the rule is unparented from a stylesheet? That is, the rule hierarchy has been removed, but somebody still holds a reference to that? I guess it should return true?

I think throwing a TypeError would be best here. Maybe always false.

But maybe just checking if its selector matches would work. But I can't think of a use case that would hold on to a detached rule.

My guess is that the intent of this isn't to find out if an element could theoretically match some rule, but rather to see if it actually does, at this moment, match the rule.

Exactly. My intention is to figure out if this rule currently affects my elements style.

AMP uses a really complicated system to fix styling bugs with fixed-position elements in Safari. It's easy to see if an element is currently fixed (just a call to getComputedStyle). But it's not easy to determine if the element has a defined (not auto) top and-or bottom offset. We can only apply our fixes to elements that have a top/bottom.

Right now, we do a complicated series of measures and mutates to determine if there's a defined top (measure the top, change bottom to -999999, measure top again, reset bottom, check to see if top changed meaning it was never defined). My hope was to store references to every rule on the page that has a top/bottom, and then iterate until I find that one of them applies to the element.

There are edge cases where some later rule could have more specificity and resets the top/bottom, but I'm willing to live with that.

emilio commented 5 years ago

Looks like the most reliable thing to do would be to return the actual computed (not resolved) top / bottom / left / right values instead? Maybe that's just easier.

emilio commented 5 years ago

I mean, add another API to the computed style declaration that returns the computed, not resolved value.

jridgewell commented 5 years ago

Looks like the most reliable thing to do would be to return the actual computed (not resolved) top / bottom / left / right values instead? Maybe that's just easier.

It would be, but I don't think it would be easy to polyfill in the meantime. I've got to support IE11, Edge, recent Chrome, recent Firefox, and Safari 9. The matches approach would be easy enough to polyfill, and it won't hurt performance at all.

jridgewell commented 5 years ago

There's also computedStyleMap, which solves my case entirely. But it not easily polyfillable. If only we had this a few years ago.