w3c / csswg-drafts

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

[selectors] Postcede selector #1705

Open Nokel81 opened 7 years ago

Nokel81 commented 7 years ago

Similar to the element1~element2 and element1+element2 selectors. However, this will select in the other direction.

Suggestion:

element1:precedes(element2) Example: label:precedes(input) will select all labels that immediately precede any input tags.

I was also thinking about suggestion the opposite to the ~ selector but that seems like it would have much more speed difficulties compared to just this and to me at least it seems like it would be less used.

upsuper commented 7 years ago

FWIW, some similar discussion appeared on www-style several days ago: https://lists.w3.org/Archives/Public/www-style/2017Jul/0025.html

SelenIT commented 7 years ago

Selectors Level 4 has the theoretical possibility to do this as element1:has(~element2), but the :has() pseudo class is still not implemented anywhere, and I guess that limiting it to the "static" profile makes implementers less interested in it. But recently @tabatkins mentioned the proposal of :has-following-siblings() selector that that could be implemented for "dynamic" profile, too.

tomhodgins commented 7 years ago

This feels like a feature that falls outside of the limitations of CSS so I'm not optimistic that a 'previous element selector' will ever happen. However, it is something that is easily attainable with JS via previousElementSibling as well as XPath in the browser, but neither of these can perform this with the speed and performance CSS requires.

Almost half of the styling plugins I've built include some kind of previous selector functionality (7/18 plugins at the time of posting this) including a simple JS mixin explaining my take on it: a selector that grabs the immediately preceding element sibling of any element(s) matching a given selector.

Here are some demos:

After implementing this functionality a few different times in a few different ways I still haven't found too many compelling use cases for its inclusion. Most of the time I hear people wishing for a previous element selector is when styling forms, where there's a desire to style different elements based on the state of an element that appears after another one in HTML. In these cases the solution often requires using JavaScript to babysit the elements in the form and apply a class to a parent or the previous element at the Right Time™.

It's definitely a feature that would be 'nice to have' for prototyping, but from what I can see having this in CSS would only abstract away a few lines of JS in a layout and would be needed pretty infrequently. Also, the difference between 'immediately preceding sibling' and 'matching older sibling(s)' is kind of like the difference between 'parent' and 'matching ancestor(s)', and the latter would perform even worse.

Can anybody else think of any compelling use-cases for a previous element selector in CSS? I've got plugins ready to go we can use to test out the ideas and see if they're worthwhile, what can we test?


Here's the example you gave of selecting all <label> elements that appear immediately before <input> elements executed in EQCSS, CSSplus/Selectory, and reproCSS with the previous selector mixin:

<div id=eqcss>
  <label>label</label>
  <input>
</div>

<style>
  @element #eqcss input {
    $prev {
      color: lime;
    }
  }
</style>

<script src=http://elementqueries.com/EQCSS.js></script>
<div id=selectory>
  <label>label</label>
  <input>
</div>

<style>
  #selectory label[test="this.nextElementSibling.tagName == 'INPUT'"] {
    color: blue;
  }
</style>

<script src=http://tomhodgins.github.io/cssplus/selectory.js></script>
<div id=reprocss>
  <label>label</label>
  <input>
</div>

<style process=auto>
  ${prev('#reprocss input', `
    color: orange;
  `)}
</style>

<script src=https://rawgit.com/tomhodgins/reprocss/master/mixins/prev-selector.js></script>
<script src=https://rawgit.com/tomhodgins/reprocss/master/reprocss.js></script>
Nokel81 commented 7 years ago

So you mentioned forms and how JS would have to be used to "babysit" the elements. With the introduction of css-grid and the general favour towards being able to use repeat() in the gaps so that you can have different gaps between different columns and rows.

With all this you don't need JS to have elements next to each other. Also this came from this talk https://www.youtube.com/watch?v=7kVeCqQCxlk. Here the speaker advocates for removing as much as possible of what he describes as layout tags. He says that it is desirable for the HTML to be as much as possible content orientated.

Lastly, not using JS also allows to have separation between View design and controller which seems to be good design and how the web is set up.

tomhodgins commented 7 years ago

Hmm, I'm not quite sure what you're meaning here about CSS grid - selecting a previous element is something that could be done regardless of the layout you're using.

One way I do witness people working around the lack of previous element selector is by using (abusing?) the flexbox order property though, so they can visually rearrange things but use the + sibling selector to 'fake' what a previous selector might do.

Here's how people use/abuse flexbox order to fake a previous selector:

<div id=flex>
  <input type=checkbox>
  <label>label</label>
</div>

<style>
  #flex {
    display: flex;
  }
  label {
    order: 1;
  }
  input {
    order: 2;
  }
  input:checked + label {
    color: red;
  }
</style>

This way the <label> can appear before the <input> in the HTML, but we can kind of fake the previous selector by using a normally-ordered sibling selector. When I was saying that a previous selector might be able to abstract away some lines of JS that are sitting and watching form elements and tasked with styling previous element(s) I was thinking more along the lines of:

<section id=prev>
  <label>Label 1</label>
  <input>
  <label>Label 2</label>
  <input>
  <label>Label 3</label>
  <input>
</section>

<style>
  .example { color: red }
</style>

<script>
  function prevver() {

    var tag = document.querySelectorAll('#prev input')

    for (var i=0; i<tag.length; i++) {

      var prev = tag[i].previousElementSibling

      if (tag[i].value == '') {

        prev.classList.add('example')

      } else {

        prev.classList.remove('example')

      }

    }

  }

  var tag = document.querySelectorAll('#prev input')

  for (var i=0; i<tag.length; i++) {

    tag[i].addEventListener('input', prevver)
    tag[i].addEventListener('blur', prevver)

  }

  window.addEventListener('load', prevver)
</script>
Nokel81 commented 7 years ago

A problem with using the ordering property is that is breaks the other selectors since you cannot correctly select the button after a label in the above case. When I have seen people wanting to select predecessor they also want to select successor as well

Nokel81 commented 6 years ago

I also believe that this won't break the single pass objective since it does not allow for arbitrary depth searching, just at the same level, not even a parent selector.

philsward commented 5 years ago

I'd like to +1 this feature request. On more than one occasion, I'm left to deal with poor HTML coding by someone else's hand and re-coding it is either not an option or too much of a pain. I recently was met with an obstacle where an input was put inside of a label and the text for the label wasn't wrapped in anything, it was part of the label itself. In other words, the input was completely cut off from any CSS styling because someone wasn't thinking... In this use case I was trying to style :checked vs :not(:checked) but found it was impossible because the checkbox input was the last sibling in the chain.

Styling a previous selector is obviously not ideal and the HTML should be coded properly to account for these types of situations, however many of us are left to the coding of other people who either don't know any better or weren't paying attention. "I need a fix now, not 6 months from now when xyz maintainer feels like getting around to fixing his code". Since I don't code by nature, coming up with javascript workarounds or re-theming workarounds to someone else's code isn't possible.

Being able to style a previous selector based on a sibling condition, would be fantastic. On a side note, do a google search for "CSS Previous sibling" and you'll find a lot of other folks who want to use it for their own various reason, but are left with answers of "You can't". If the people using CSS want it, why can't we have it?

Am I wrong in saying the DOM can handle it? https://www.w3schools.com/jsref/prop_node_previoussibling.asp

Code Example:

<div class="form-type-checkbox checkbox">
    <label class="control-label" for="edit-attributes-1">
        <input type="checkbox" id="edit-attribute-1" name="attributes[1]" class="form-checkbox ajax-processed">
    Free product with purchase!
    </label>
</div>
marrus-sh commented 5 years ago

Can anybody else think of any compelling use-cases for a previous element selector in CSS?

Sure. Say I'm writing a basic webpage (just HTML and CSS). It has a number of sections, either <nav> or <section>. They look like this:

nav,
section {
    height: 100vh; /* Fills the screen */
    overflow: hidden;
}

For clarity, I want to stick an arrow at the bottom of these sections so that readers know that they can keep scrolling. This arrow isn't really a part of the document's contents (and shouldn't be rendered in alternate environments like Reader View), so it makes sense to add using CSS:

nav::after,
section::after {
    display: block;
    position: absolute;
    bottom: 0;
    margin: auto;
    width: 1em;
    content: "↓";
}

Naturally I don't want an arrow pointing towards nothing, so I can hide it on the last child.

nav:last-child::after,
section:last-child::after { content: none }

However, my document might have a page footer (a <footer>). I don't want an arrow pointing towards the footer either, if it exists. So, I need to find the section which precedes the footer and hide the arrow there.

  1. I can't use :last-of-type because the final section could be EITHER a <section> or a <nav>.

  2. I can't use :nth-last-child() because the footer might not be shown on every page.

  3. Changing the order of the elements in the document is a no-go, since it reduces accessibility and messes up important features like Reader View.

  4. It is silly to require JavaScript for a simple static, textual webpage, and requiring authors to manually type class="LASTONE" makes webpages more difficult to maintain (what if I forget to delete it when I add a new section?).

nav:immediately-precedes(footer),
section:immediately-precedes(footer) { content: none }

I kept the above example as simple as possible but things get even more complicated if we consider a grid layout; suppose I want to display a page footer at the right-hand side of the grid rather than at the bottom. This could potentially affect the positioning of all preceding elements. (It could happen! In such cases, one might even want both :precedes and :immediately-precedes.)

TL;DR: forms and web apps are definitely not the place where this selector seems most useful; vanilla text-based webpages with innovative typographic layouts are. Not having this feature leads to inaccessible webpages, by encouraging people to code documents whose tree doesn't reflect what is rendered on the page. This breaks important features like Reader View.

(As a sidenote, since :last-of-type (and :not(:last-of-type)) already allows the addition/removal of siblings late in the tree to affect early ones, I'm not entirely clear as to how :precedes() is any different? Which is to say, it might be slowish, but hopefully not unforgivably slowish? (Maybe it just needs to be limited to simple selectors? :last-of-type is just :not(:precedes(elt)) after all.))

SebastianZ commented 2 years ago

My syntax suggestion would be -+ as previous sibling combinator and -~ as precedent sibling combinator. So, input -+ label selects all <label> elements that are followed by an <input> element. .class1 -~ .class2 selects all elements that have a class class2 that have a subsequent sibling with a class class1.

Sebastian