Sean Fujiwara
Sean Fujiwara

Reputation: 4546

Is there a jQuery selector function that can reference relative elements?

Here are some examples, which I would expect to work with $:

$(':target', element); // Should return element
$(':parent', element); // Should return parent of element
$(':parent textarea', element); // Should return textareas within parent of element

The reason I don't want to use the parent method is because the selector will be dynamic, and I don't want to have to parse it.

Edit:

Here's a concrete example.

HTML:

<div>
    <span data-hide=":parent span:eq(1)">Span 1</span>
    <span>Span 2</span>
</div>

JavaScript:

$('[data-hide]').on('click', function()
{
    var selector = $(this).attr('data-hide');
    $(selector, $(this)).hide();
});

If the 2nd parameter of $ (called context) and :parent pseudo selector worked the way I expected, clicking "Span 1" would hide "Span 2".

But it doesn't. So how can I write this function so that any reasonable absolute or relative selector can be used?

Upvotes: 0

Views: 796

Answers (1)

iCollect.it Ltd
iCollect.it Ltd

Reputation: 93611

I had a similar requirement (basically search up then down the DOM using a single string selector), and after digging into how selectors work I found this was not possible with normal selectors as the evaluation of the selector elements is right-to-left, whereas you need left-to-right to make your suggestion work. I was able to make a custom selector that could have a :this reference, which can let you do similar searches including :has(:this). I have repeated my answer here from Is it possible to create custom jQuery selectors that navigate ancestors? e.g. a :closest or :parents selector

Based on numerous comments, and one detailed explanation on why this is impossible, it occurred to me that the aim I wanted could be met with a $(document).find(), but with some concept of targeted elements. That is, some way to target the original query elements, within the selector.

To that end I came up with the following, a :this selector, which works like this (no pun intended):

// Find all labels under .level3 classes that have the .starthere class beneath them
$('.starthere').findThis('.level3:has(:this) .label')

This allows us to now, effectively, search up the DOM then down into adjacent branches in a single selector string! i.e. it does the same job this does (but in a single selector):

$('.starthere').parents('.level3').find('.label')

Steps:

1 - Add a new jQuery.findThis method

2 - If the selector has :this, substitute an id search and search from document instead

3 - If the selector does not contain a :this process normally using the original find

4 - Test with selector like $('.target').find('.ancestor:has(:this) .label') to select a label within the ancestor(s) of the targetted element(s)

This is the revised version, based on comments, that does not replace the existing find and uses a generated unique id.

JSFiddle: http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/36/

// Add findThis method to jQuery (with a custom :this check)
jQuery.fn.findThis = function (selector) {
    // If we have a :this selector
    if (selector.indexOf(':this') > 0) {
        var ret = $();
        for (var i = 0; i < this.length; i++) {
            var el = this[i];
            var id = el.id;
            // If not id already, put in a temp (unique) id
            el.id = 'id'+ new Date().getTime();
            var selector2 = selector.replace(':this', '#' + el.id);
            ret = ret.add(jQuery(selector2, document));
            // restore any original id
            el.id = id;
        }
        ret.selector = selector;
        return ret;
    }
    // do a normal find instead
    return this.find(selector);
}

// Test case
$(function () {
    $('.starthere').findThis('.level3:has(:this) .label').css({
        color: 'red'
    });
});

Known issues:

  • This leaves a blank id attribute on targetted elements that did not have an id attribute to begin with (this causes no problem, but is not as neat as I would like)

  • Because of the way it has to search from document, it can only emulate parents() and not closest(), but I have a feeling I can use a similar approach and add a :closest() pseudo selector to this code.

This was based on 6 hours slaving over jQuery/Sizzle source code, so be gentle. Always happy to hear of ways to improve this replacement find as I am new to the internals of jQuery :)

You can then solve your original problem with something like this:

<div>
    <span data-hide="div:has(:this) span:eq(1)">Span 1</span>
    <span>Span 2</span>
</div>

JavaScript:

$('[data-hide]').on('click', function()
{
    var selector = $(this).attr('data-hide');
    $(this).findThis(selector).hide();
});

Upvotes: 1

Related Questions