1

I am trying to build a tab system. If I have the following HTML:

<div class="wizard-tabs">
  <ul>
    <li class="active"><a href="#" data-formstep="step1">Step 1</a></li>
    <li><a href="#" data-formstep="step2">Step 2</a></li>
    <li><a href="#" data-formstep="step3">Step 3</a></li>
  </ul>
</div>

How do I get the list item that has an anchor with a data-formstep attribute equal to a certain value? I am currently using this:

document.querySelector('.wizard-tabs > ul > li:has(a[data-formstep="'+newStep+'"])').classList.add("active")

But I am getting an error saying:

Failed to execute 'querySelector' on 'Document': '.wizard-tabs > ul > li:has(a[data-formstep="step1"])' is not a valid selector.

What am I doing wrong?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
ShoeLace1291
  • 4,551
  • 12
  • 45
  • 81
  • make the selector target the `a`, then `li` you want is the parent -e.g. `document.querySelector('.wizard-tabs > ul > li >a[data-formstep="'+newStep+'"]').parentElement` – Jaromanda X Sep 15 '17 at 03:54
  • That just seems like an unnecessary extra step. There's gotta be a way to select the LI based on an attribute of an anchor instead of it. – ShoeLace1291 Sep 15 '17 at 03:56
  • but `:has` isn't supported in any browser, so it's actually a necessary extra step - there's no point arguing about what browsers support or don't – Jaromanda X Sep 15 '17 at 03:57
  • Oh, Ok. I didn't realize that. I saw :has() in jquery(which I am trying to use vanilla) and thought that was originally a JS selector. – ShoeLace1291 Sep 15 '17 at 03:59
  • 1
    [MDN documentation for :has](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) - I find MDN documentation very good, because even if firefox doesn't support the feature (yet) MDN will document it as it is an emerging standard – Jaromanda X Sep 15 '17 at 04:01
  • @JaromandaX https://stackoverflow.com/questions/46231430/vanilla-js-selector-for-an-element-that-has-a-child-element-with-a-certain-attri#comment-79427112 – Kaiido Sep 15 '17 at 04:47
  • @Jaromanda X: I would not trust MDN for this. No one actually knows the status of :has() in the spec - not even browser vendors themselves, much less the community. – BoltClock Sep 15 '17 at 04:52
  • who would you trust? MDN links to [a draft CSS4 document](https://drafts.csswg.org/selectors-4/#relational) - would you trust that? – Jaromanda X Sep 15 '17 at 04:54
  • @Jaromanda X: Yes, because at least the spec is maintained by the working group and not the community. – BoltClock Sep 15 '17 at 04:56

3 Answers3

0

:has is an experimental technology, it currently has no support in any browser.

See https://developer.mozilla.org/en-US/docs/Web/CSS/:has (thanks to @jaromanda-x)

The fix: As Jaromanda said, just reference the <a> and then get it's .parentElement.

var li = document.querySelector('.wizard-tabs > ul > li >a[data-formstep="'+newStep+'"]').parentElement;
Kyle Lin
  • 827
  • 8
  • 16
  • `("parent:has(tag)")` is not the same as `('parent>tag').parentElement` `has()` does target all descendants, if it were `has(>tag)` then yes it would be the same. – Kaiido Sep 15 '17 at 04:45
  • 1
    however, @Kaiido `("parent:has(tag)")` is the same as `('parent tag')`, and then traversing up the parentElement chain to `parent` ... yes, it's more complex, but in the example given, your point is moot – Jaromanda X Sep 15 '17 at 04:50
0

:has() as of 2017 is a non-standard jQuery selector only. Its status in the spec is unknown, no implementations exist, and you should not assume that it will ever be implemented until it is ready.

There is no equivalent to :has() in current selector syntax (which is the entire reason why it was added to the spec); your best bet is selecting the child element itself and using .parentElement to get its parent.

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
  • it is currently `an experimental technology` - so, "non-standard" is overstating things – Jaromanda X Sep 15 '17 at 04:52
  • @Jaromanda X: It's been "experimental" with **no** native implementations for nearly **six years** since the FPWD. These six years have been no different from the past decade that jQuery has been around. – BoltClock Sep 15 '17 at 04:55
  • the linked draft css document is dated `Selectors Level 4 Editor’s Draft, 23 August 2017` - not sure where you get 6 years from - the MDN page has existed only since `Jan 10, 2016, 3:50:50 PM` so ... again .. what are you going on about it being experimental for 6 years? Are you from the future? – Jaromanda X Sep 15 '17 at 04:56
  • @Jaromanda X: Sorry, I misremembered, [:has() was only added to the spec in 2014/2015, replacing the subject indicator from the FPWD](https://stackoverflow.com/questions/27982922/latest-on-css-parent-selector/27983098#27983098) (which, by the way, means First Public Working Draft). So about three years, not six. My point remains that I would not hold my breath for implementations since no progress has been made whatsoever in those three years (unless you count renaming selector profiles as progress). – BoltClock Sep 15 '17 at 05:00
0

Here is a very rough polyfill for has() selector which would probably need more testings, but that can still be helpful:

(function() {

  // Can probably be improved
  // Will search for sequence ":has(xxx)" even if xxx contains other sets of func(yyy)
  function searchForHas(selector) {
    if (!selector) {return null;}
    const closed = [], valids = [], open = [], lastFour = ['', '', '', ''];

    selector.split('').forEach(char => {
      if (char == ')') {
        closed.push(open.pop()); // close the last open one
      }
      open.forEach(o => o.s += char); // add this char to all open ones
      if (char == '(') {
        open.push({
          s: ''
        }); // open a new one as an object so we can cross reference
        if (lastFour.join('') === ':has') {
          valids.push(open[open.length - 1]); // this one is interesting
        }
      }
      // update our ':has' sequence identifier
      lastFour.shift();
      lastFour.push(char);
    });
    if (!valids.length) {
      return null;
    } else {
      let str = selector.split(':has(' + valids[0].s + ')');
      return [str.shift(), valids[0].s, str.join('')]; // [pre, current_has, post]
    }
  }

  function has(that, sel_parts) {
    const matches = [...that.querySelectorAll(sel_parts[0])] // pre selector
      .filter(el => el.querySelector(sel_parts[1])); // which has one current_has
    return sel_parts[2] ? Array.prototype.concat.apply([], // if there is a post
      matches.map(el => [...el.querySelectorAll(':scope' + sel_parts[2])] // flatten all the matches
        .filter(el => !!el)
      )
    ) : matches;
  }
  // Overwrite the protos
  const dQS = Document.prototype.querySelector;
  Document.prototype.querySelector = function querySelector(selector) {
    const has_parts = searchForHas(selector);
    if (has_parts) {
      return has(this, has_parts)[0] || null;
    } else {
      return dQS.apply(this, [selector]);
    }
  };
  const dQSA = Document.prototype.querySelectorAll;
  Document.prototype.querySelectorAll = function(selector) {
    const has_parts = searchForHas(selector);
    if (has_parts) {
      let arr = has(this, has_parts);
      return arr && arr.length ? arr : null;
    } else {
      return dQSA.apply(this, [selector]);
    }
  };
  const eQS = Element.prototype.querySelector;
  Element.prototype.querySelector = function(selector) {
    const has_parts = searchForHas(selector);
    if (has_parts) {
      return has(this, has_parts)[0] || null;
    } else {
      return eQS.apply(this, [selector]);
    }
  };
  var eQSA = Element.prototype.querySelectorAll;
  Element.prototype.querySelectorAll = function(selector) {
    const has_parts = searchForHas(selector);
    if (has_parts) {
      let arr = has(this, has_parts);
      return arr && arr.length ? arr : null;
    } else {
      return eQSA.apply(this, [selector]);
    }
  };
})();

console.log(document.querySelector('.wizard-tabs > ul > li:has(a[data-formstep="step1"])'));
// and even
console.log(document.querySelector('li:has(a[data-formstep="step2"])>i'));
<div class="wizard-tabs">
  <ul>
    <li class="active"><a href="#" data-formstep="step1">Step 1</a><i data-s="1"></i></li>
    <li><a href="#" data-formstep="step2">Step 2</a><i data-s="2"></i></li>
    <li><a href="#" data-formstep="step3">Step 3</a><i data-s="3"></i></li>
  </ul>
</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285