2

Given an element and any selector, I need to find the closest element which matches it, not matter if it's inside the element or outside of it.

Currently jQuery doesn't provide such traversing functionality, but there is a need. Here is the scenario:

A list of many items where the <button> element reside inside <a>

<ul>
  <li>
    <a>
      <button>click me</button>
      <img src="..." />
    </a>
  </li>
  <li>
    <a>
      <button>click me</button>
      <img src="..." />
    </a>
  </li>
  ...
</ul>

Or the <button> element might reside outside of the <a> element

<ul>
  <li>
    <a>
      <img src="..." />
    </a>
    <button>click me</button>
  </li>
  <li>
    <a>
      <img src="..." />
    </a>
    <button>click me</button>
  </li>
  ...
</ul>

The very very basic code would look like this:

$('a').closest1('button'); // where `closest1` is a new custom function
// or
$('a').select('> button') // where `select` can parse any selector relative to the object, so it would also know this:
$('a').select('~ button') // where the button is a sibling to the element

the known element is <a> and anything else can change. I want to locate the nearest <button> element for a given <a> element, no matter if that button is inside or outside of <a>'s DOM tree.

It would be very logical that native jQuery function "closest" would do as the name suggests and find the closest, but it only searches upwards as you all know. (it should have been named differently IMO).


Does anyone know any custom traversing function which does the above? Thanks. (i'm asking you people because someone must have written this for sure but I was unlucky to find a lead on the internet)

empiric
  • 7,825
  • 7
  • 37
  • 48
vsync
  • 118,978
  • 58
  • 307
  • 400
  • None of the built-in selectors allow searching up *and* down the tree. I did create a custom `findThis` extension that allows you to do things like `$elementClicked.('li:has(this) button') ` which would allow you to do something similar. Is that of interest to you? – iCollect.it Ltd Apr 23 '15 at 11:26
  • yes i know jquery has nothing built-in, this is why i'm asking of course. does your function has the ability to work like the demo pseudo code i've envisioned this to work like? thanks – vsync Apr 23 '15 at 11:28
  • This _feels_ like an XY problem... you'd have to potentially search the entire DOM? – James Thorpe Apr 23 '15 at 11:28
  • Yes, I think it will handle you test cases. Give me a minute to create a JSfiddle to demo it. – iCollect.it Ltd Apr 23 '15 at 11:29
  • You didn't specify the restriction on how far at most the searched element (button) must be located from the anchor element (a) – dekkard Apr 23 '15 at 11:30
  • I think you're looking at this problem as if the DOM is a flat list. Because it's a tree, that relationship has meaning. Finding the *closest* in your terms seems like the structure is wrong, rather than something which needs solved. – CodingIntrigue Apr 23 '15 at 11:31
  • @JamesThorpe - many selectors has the potential of search much of a DOM (mostly not all of it, but a big part). I would say, the selector would have to part the selection string and internally use the right jquery selector for the job. – vsync Apr 23 '15 at 11:31
  • Okay. Working solution added that works for both cases. – iCollect.it Ltd Apr 23 '15 at 11:33
  • @RGraham - i'm not looking at this wrong at all. I am looking to parsing something which I we can call "mega selector", and internally that selector would be parsed and the right jquery selector would be utilized to find what the user was after, like the code I envisioned in my question. – vsync Apr 23 '15 at 11:33
  • @RGraham: I hit a similar problem months ago, where in my case I needed single selectors to provide an up-down search capability. The `findThis` extension below allows that. Taken from my answer here: http://stackoverflow.com/questions/25721390/is-it-possible-to-create-custom-jquery-selectors-that-navigate-ancestors-e-g-a/25733588#25733588 – iCollect.it Ltd Apr 23 '15 at 11:36
  • It's usually a fairly defined subset though, whereas this would need to scan through every sibling, child, parent (and the children and parents of those) etc etc to check if it had one "closer" than one it's already found. It's also not like it's just keeping track of the found elements, it's also then having to determine which one is "closest". – James Thorpe Apr 23 '15 at 11:36
  • 1
    I fully understand the required solution. I just don't understand what problem it solves. I can't understand why an element, in a potentially completely unrelated part of the DOM tree, would be semantically linked to the original element without some sort of identifying attributes. – CodingIntrigue Apr 23 '15 at 11:37
  • @RGraham - fine question. I have built an image gallery, which is a quite complex one (https://github.com/yairEO/photobox) and a use has opened an issue where he wants to enlarge an image not by clicking on the links themselves which is the default behavior, but by placing a button somewhere in the DOM, for each image item, and that element will trigger the enlargement of an image. now, that button could be anywhere..but each `` element will have a button, hence, my question was born – vsync Apr 23 '15 at 11:41
  • 3
    What if it's something like `
    – James Thorpe Apr 23 '15 at 11:43
  • 2
    `data-` attributes, linking the elements, would be a more practical solution if your DOM has no structure at all. Otherwise you might as well code up a custom search. – iCollect.it Ltd Apr 23 '15 at 11:45
  • @JamesThorpe - well, for this use case the DOM cannot look like that, and nobody would use this custom traversing for such a DOM, since it is not meant for that. it is only meant for cases as I have described above. Like any other selector in jQuery, it would have it's own scenario of uses. – vsync Apr 23 '15 at 11:47
  • @TrueBlueAussie - yes i am talking about custom search here. I want to make this transparent and handle it internally rather than asking people who use my code to change their existing DOM to add `data` attributes. – vsync Apr 23 '15 at 11:48
  • ok - so is it limited to just your two examples then (ish - will the button always be inside the `li` say?). If so, just find the closest li, then the button within it? `$(this).closest('li').find('button')`? (assuming you can't use attributes to semantically link them, which seems like the appropriate solution here) – James Thorpe Apr 23 '15 at 11:50
  • @James Thorpe: Funny enough that is exactly what `findThis` emulates below, but in a single selector (which I presumed, *apparently incorrectly*, was the overall aim). – iCollect.it Ltd Apr 23 '15 at 11:54
  • @vsync I think either you need to use attributes to link them, or expose an API point to allow your consumers to specify either a selector or a callback function that given an element, returns the button since they'll know how to get from one to the other. – James Thorpe Apr 23 '15 at 11:56
  • I think I will go with asking the user to just point to some parent which he would know all buttons would exist inside it. something like `li` because the user knows that would be a repeating pattern, every link and button will be inside an `li` element, so then i could just internally do `$('a').closest([user selector]).find('button')`. it's not what I wanted but it would work. – vsync Apr 23 '15 at 12:00
  • Added a second (really simple) answer based on my suggested alternative. Hope that matches you needs better :) – iCollect.it Ltd Apr 23 '15 at 12:01
  • @TrueBlueAussie - solved this - http://stackoverflow.com/a/29823718/104380 – vsync Apr 23 '15 at 12:38

5 Answers5

1

Here is another attempt using the idea I mentioned in comment:

$(this).parents(':has(button):first').find('button').css({
        "border": '3px solid red'
});

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

It basically looks for the first ancestor that contains both the elements (clicked and target), then finds the target.

Performance:

With regard to speed, this is used at human interaction speeds, i.e. a few times per second maximum, so being a "slow selector" is irrelevant if it solves the problem, in a reasonably obvious way, with minimal code. You would have to click 100s of times per second to notice any different compared to a fast selector :)

iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • 1
    I think this is pretty close - so long as the buttons live somewhere relatively near each image. Certainly solves the examples given – James Thorpe Apr 23 '15 at 12:01
  • that is a pretty slow selector :) – vsync Apr 23 '15 at 12:02
  • 1
    @vsync: What on Earth were you expecting, speed-wise, from your requirements? My other answer is quite efficient, but requires a starting point (some common ancestor element). At least you now have two choices more than you had before :) – iCollect.it Ltd Apr 23 '15 at 12:04
  • please see my answer and tell me your thoughts. – vsync Apr 23 '15 at 12:39
0

None of the built-in selectors allow searching up and down the tree. I did create a custom findThis extension that allows you to do things like $elementClicked.('li:has(this) button') which would allow you to do something similar.

// 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 () {
    $('a').click(function () {
        $(this).findThis('li:has(:this) button').css({
            "border": '3px solid red'
        });
    });
});

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

note: Click the images/links to test.

iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • i don't want to involve the `li` in the selector, but it automatically relative to the element ( in this case). decoupling markup from selectors as much as possible – vsync Apr 23 '15 at 11:38
  • @vsync. There has to be some for of limiting, or you will have to trawl all parents until you get a match, but who is to say a single match is enough. You are likely to have some form of container in your code, it can be broad, but otherwise you are basically searching all `parents` in turn until you `find` a single match. That is relative slow. – iCollect.it Ltd Apr 23 '15 at 11:42
  • that is how `closest` work already. all I want it to do is to expand it's functionality, so it would first look inside, and if the element wasn't found, then look up the DOM. is all. I assume here that the inside DOM is not big, and would be "cheaper" to search in first and only if not found, traverse up. would you agree? – vsync Apr 23 '15 at 11:45
  • Not possible with a single jQuery Sizzle selector, as it works right to left and will not allow upward referencing. I know as I spent a lot of time inside the Sizzle source code trying to solve this problem, before coming up with the above answer. You would need to replace Sizzle. – iCollect.it Ltd Apr 23 '15 at 11:46
  • It's not just down and up the dom though. You need to search down, then up, then across at every level on the way up, and down within each of those etc etc. – James Thorpe Apr 23 '15 at 11:46
  • 1
    @James Thorpe: `down` from ancestors is enough to find a common parent (across not needed). Worst case you hit `body` for every search (then he is in trouble) :) – iCollect.it Ltd Apr 23 '15 at 11:47
  • @TrueBlueAussie that's true. You'd still need to count each of the levels passed through to find the "closest" one though. – James Thorpe Apr 23 '15 at 11:48
  • @James Thorpe: Nah, Just sequentially check down from each parent as you move upwards through ancestors. Stop on first match. Horribly slow, but I think this is all beside this point. Totally unstructured content is not a good idea for efficiency :) – iCollect.it Ltd Apr 23 '15 at 11:50
  • mind you that inside the `
  • ` could be a much more complex DOM than what I've written, so just going to the parent of the element and searching from there wouldn't be suffice.
  • – vsync Apr 23 '15 at 11:50
  • @vsync: Who said anything about parent? I literally mean `parents()` (all the ancestors). – iCollect.it Ltd Apr 23 '15 at 11:51
  • yeah well basically I could ask the user to provide a starting reference point of where to start the search for that element. that would be an ugly solution to the problem. the real problem here is lack of technological means to just search anything relative to something, no matter inside or outside. – vsync Apr 23 '15 at 11:57
  • Anyway, I have written a new search function and published it as an answer so check it out, tell me any thoughts you might have ans thanks for taking the time to participate in this discussion which sadly got downvoted – vsync Apr 23 '15 at 12:43