Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cssom] Implement CSSStyleRule.p.matches #3670

Open
jridgewell opened this issue Feb 22, 2019 · 10 comments
Open

[cssom] Implement CSSStyleRule.p.matches #3670

jridgewell opened this issue Feb 22, 2019 · 10 comments
Labels

Comments

@jridgewell
Copy link

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
Copy link
Collaborator

emilio commented Feb 24, 2019

I think that makes sense, but some questions:

  • What should happen when the element given lives in another document? 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.
  • Should it account for the StyleSheet's media attribute? disabled too, maybe? What about if it's an alternate sheet? Or an @import-ed sheet?

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
Copy link
Author

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
Copy link
Collaborator

emilio commented Feb 25, 2019

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
Copy link
Member

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
Copy link
Member

@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
Copy link
Author

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
Copy link
Collaborator

emilio commented Feb 25, 2019

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
Copy link
Collaborator

emilio commented Feb 25, 2019

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

@jridgewell
Copy link
Author

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
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants