Select odd even child excluding the hidden child

2014-09-26 css css-selectors

Line 3 is a hidden <div> . I don't want that one to be taken from the odd/even css rule.

enter image description here

What is the best approach to get this to work?

.hidden {display:none;}
.box:not(.hidden):nth-child(odd)  { background: orange; }
.box:not(.hidden):nth-child(even) { background: green;  }
<div class="wrap">
    <div class="box">1</div>
    <div class="box">2</div>
    <div class="box hidden">3</div>
    <div class="box">4</div>
    <div class="box">5</div>
    <div class="box">6</div>
    <div class="box">7</div>
</div>

http://jsfiddle.net/k0wzoweh/

Note: There can be multiple hidden elements.

Answers

Pseudo-selectors don't stack, so your :not doesn't affect the :nth-child (nor would it affect :nth-of-type etc.

If you can resort to jQuery, you can use the :visible pseudo-selector there, although that's not a part of the CSS spec.

If you're generating the HTML and can change that, you can apply odd/even with logic at run-time, eg in PHP:

foreach ($divs AS $i => $div) {
    echo '<div class="box ' . ($i % 2 ? 'even' : 'odd') . '">x</div>';
}

Even trying to do something tricky like

.box[class='box']:nth-of-type(even)

doesn't work, because the psuedo-selector doesn't even stack onto the attribute selector.

I'm not sure there's any way to do this purely with CSS - I can't think of any right now.

Thanks Joe (+1) for pointing out the css pseudo-selector rules and the php code which I could pretty much use 1:1 in jQuery looking like this:

var classToAdd = visibleBoxes%2 ? 'even' : 'odd' ;
$(this).addClass(classToAdd)

:nth-child() pseudo-class looks through the children tree of the parent to match the valid child (odd, even, etc), therefore when you combine it with :not(.hidden) it won't filter the elements properly.

Alternatively, we could fake the effect by CSS gradient as follows:

.hidden {display:none;}

.wrap {
  line-height: 1.2em;
  
  background-color: orange; 
  background-image: linear-gradient(transparent 50%, green 50%);
  background-size: 100% 2.4em;
}
<div class="wrap">
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box hidden">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
</div>

Since my rows are being hidden with js, I found that the easiest approach for me was to just add an additional hidden row after each real row that I hide, and remove the hidden rows when I show the real rows again.

Here's a CSS-only solution:

.box {
  background: orange;
}

.box:nth-child(even) {
  background: green;
}

.box.hidden {
  display: none;
}

.box.hidden ~ .box:nth-child(odd) {
  background: green;
}

.box.hidden ~ .box:nth-child(even) {
  background: orange;
}
<div class="wrap">
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box hidden">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
  <div class="box">xx</div>
</div>

Hide the rows you want to hide calling .hide() for each table row, then call

$("tr:visible:even").css( "background-color", "" ); // clear attribute for all rows

$("tr:visible:even").css( "background-color", "#ddddff" ); // set attribute for even rows

Add your table name to the selector to be more specific. Using :even makes it skip the Header row.

As @Fateh Khalsa pointed out, I had a similar problem and since I was manipulating my table with JavaScript (jQuery to be precise), I was able to do the following:

(Note: This assumes use of JavaScript/jQuery which the OP did not state whether or not would be available to them. This answer assumes yes, it would be, and that we may want to toggle visibility of hidden rows at some point.)

  • Inactive records (identified with the CSS class "hideme") are currently visible.
  • Visitor clicks link to hide inactive records from the list.
  • jQuery adds "hidden" CSS class to "hideme" records.
  • jQuery adds additional empty row to the table immediately following the row we just hid, adding CSS classes "hidden" (so it doesn't show) and "skiprowcolor" so we can easily identify these extra rows.

This process is then reversed when the link is clicked again.

  • Inactive records (identified with the CSS class "hideme") are currently hidden.
  • Visitor clicks link to show inactive records from the list.
  • jQuery removes "hidden" CSS class to "hideme" records.
  • jQuery removes additional empty row to the table immediately following the row we just showed, identified by CSS class "skiprowcolor".

Here's the JavaScript (jQuery) to do this:

// Inactive Row Toggle
$('.toginactive').click(function(e) {
    e.preventDefault();
    if ($(this).hasClass('on')) {
        $(this).removeClass('on');                  // Track that we're no longer hiding rows
        $('.wrap tr.hideme').removeClass('hidden'); // Remove hidden class from inactive rows
        $('.wrap tr.skiprowcolor').remove();        // Remove extra rows added to fix coloring
    } else {
        $(this).addClass('on');                     // Track that we're hiding rows
        $('.wrap tr.hideme').addClass('hidden');    // Add hidden class from inactive rows
        $('.wrap tr.hideme').after('<tr class="hidden skiprowcolor"></tr>');
                                                    // Add extra row after each hidden row to fix coloring
    }
});

The HTML link is simple

<a href="#" class="toginactive">Hide/Show Hidden Rows</a>

scss for @tim answer's above, to keep class name changes to a minimum

$selector: "box";
$hidden-selector: "hidden";

.#{$selector} {
  background: orange;

  :nth-child(even) {
    background: green;
  }

  &.#{$hidden-selector} {
    display: none;
  }

  &.#{$hidden-selector} ~ {
    .#{$selector} {
      &:nth-of-type(odd) {
        background: green;
      }

      &:nth-of-type(even) {
        background: orange;
      }
    }
  }
}

Another way, albeit on the fringe side, is to have an extra <tbody> and either move or copy rows there. Or, an extra div wrapper if using OPs example. Copying easiest of course in regards to restoring etc.

This approach can be useful in some cases.

Below is a simple example where rows are moved when filtered. And yes, it is ranking of stripper names, found it fitting as we are talking stripes ... hah

const Filter = {
  table: null,
  last: {
    tt: null,
    value: ''
  },
  name: function (txt) {
    let tb_d = Filter.table.querySelector('.data'),
        tb_f = Filter.table.querySelector('.filtered'),
        tr = tb_d.querySelectorAll('TR'),
        f = 0
    ;
    tb_f.innerHTML = '';
    if (txt.trim() == '') {
      tb_d.classList.remove('hide');
    } else {
      txt = txt.toLowerCase();
      for (let i = 0; i < tr.length; ++i) {
        let td = tr[i].querySelectorAll('TD')[1];
        if (td.textContent.toLowerCase().includes(txt)) {
          tb_f.appendChild(tr[i].cloneNode(true));
          f = 1;
        }
      }
      if (f)
        tb_d.classList[f ? 'add' : 'remove']('hide');
    }
  },
  key: function (e) {
    const v = e.target.value;
    if (v == Filter.last.value)
      return;
    Filter.last.value = v;
    clearTimeout(Filter.last.tt);
    Filter.last.tt = setTimeout(function () { Filter.name(v); }, 200);
  }
};

Filter.table = document.getElementById('table');
Filter.table.addEventListener('keyup', Filter.key);
table {
  width: 200px;
  border: 3px solid #aaa;
}
tbody tr { background: #e33; }
tbody tr:nth-child(even) { background: #e3e;  }

.hide { display: none; }
<table id="table">
  <thead>
    <tr><th></th><th><input type="text" id="filter" data-keyup="filter" /></th></tr>
    <tr><th>#</th><th>Name</th></tr>
  </thead>
  <tbody class="filtered">
  </tbody>
  <tbody class="data">
    <tr><td>1</td><td>Crystal</td></tr>
    <tr><td>2</td><td>Tiffany</td></tr>
    <tr><td>3</td><td>Amber</td></tr>
    <tr><td>4</td><td>Brandi</td></tr>
    <tr><td>5</td><td>Lola</td></tr>
    <tr><td>6</td><td>Angel</td></tr>
    <tr><td>7</td><td>Ginger</td></tr>
    <tr><td>8</td><td>Candy</td></tr>
  </tbody>
</table>

Related