Miro
Miro

Reputation: 8650

Find closest similar siblings

I want to be able to click on the <i>, the <a>, or the <li> and get all the <li>s.

The point is to detect the closest repetitive element no matter how deep in it's children you click.

This is what I have so far: It shows the count of the siblings but what I'd like to isolate is same-ness.

For example: <body> shows 2 siblings but they are not both <body>. <header> shows 4 but they not all <header>. How do I only count same siblings. In this case <li> but can be anything.

//Run in full-page↗️

$('body').on('click', (e) => {
  $el = $(e.target);

  $parentsAndSelf = $el.parents().addBack();
  
  $parentsAndSelf.each( (i,el) => {
    if( $(el).siblings().addBack().length > 1){
      console.log( el,  $(el).siblings().addBack().length );
      //return false;
    }
    
  });
});
* {
  padding: 5px;
  margin:5px;
  outline: 1px solid pink;
  list-style-position: inside;
}

a {
  display: block
}

.highlight{
  background:yellow;
}

*:hover {
  outline: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<header>
  HEADER:
  <nav>
    NAV:
    <ul>
      UL:
      <li>li <a href="#">item <i>1</i></a> </li>
      <li>li <a href="#">item <i>2</i></a> </li>
      <li>li <a href="#">item <i>3</i></a> </li>
      <li>li <a href="#">item <i>4</i></a> </li>
    </ul>
  </nav>
</header>

Upvotes: 4

Views: 282

Answers (3)

ikiK
ikiK

Reputation: 6532

This really got me occupied hehe.

So if:

The point is to detect the closest repetitive element no matter how deep in it's children you click.

It means:

  • you have to get parent of clicked element
  • search for its children, if any, get its tag-names (types), react if two are the same
  • if not, move to next parent, do the same

I want to be able to click on the <i>, the <a>, or the <li> and get all the <li>s.

Example:

let num
document.addEventListener("click", function(event) {
  num = 1;
  let el = event.target
  let s
  check(s, el)
})

function check(s, el) {
  console.clear()

  // jumping parents part and getting only first level children
  let ss = ".parentNode"
  s = "el" + ss.repeat(num) + ".children"

  // map tagnames
  let c = [...eval(s)].map(type => type.tagName)

  // isolate duplicates part
  const yourArray = c
  const yourArrayWithoutDuplicates = [...new Set(yourArray)]
  let duplicates = [...yourArray]
  yourArrayWithoutDuplicates.forEach((item) => {
    const i = duplicates.indexOf(item)
    duplicates = duplicates
      .slice(0, i)
      .concat(duplicates.slice(i + 1, duplicates.length))
  })

  // if no duplicates call this same function just add num++ to add .parentNode
  // it will loop unitl it hits 2 same tagnames on same level, sibilings
  duplicates.length === 0 ? (num++, check(eval(s), el)) : (console.log("Repeting element :" + duplicates[0]), console.log("Repets: " + (parseInt(duplicates.length) + 1)), console.log("Position from clicked element: " + s))

}
* {
  padding: 5px;
  margin: 5px;
  outline: 1px solid pink;
  list-style-position: inside;
}

a {
  display: block
}

.highlight {
  background: yellow;
}

*:hover {
  outline: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<header>
  <div>1</div>
  <div>2</div>
  HEADER:
  <nav>
    NAV:
    <ul>
      UL:
      <li>li <a href="#">item <i>1</i></a> </li>
      <li>li <a href="#">item <i>2</i></a> </li>
      <li>li <a href="#">item <i>3</i></a> </li>
      <li>li <a href="#">item <i>4</i></a> </li>
    </ul>
  </nav>
</header>

EDIT:

And here is a bit more simplified version still following same logic:

let num
document.addEventListener("click", function(event) {
  num = 1;
  let s
  check(s, event.target)
})

function check(s, el) {
  console.clear()
  const ss = ".parentNode"
  s = "el" + ss.repeat(num) + ".children";
  let count = {};
  [...eval(s)].map(type => type.tagName).forEach( i => {count[i] = (count[i] || 0) + 1})
  const max = Math.max.apply(null, Object.values(count));

  max <= 1 ? (num++, check(eval(s), el)) : (console.log("Repeting element :" + Object.keys(count)[0]), console.log("Repets: " + max), console.log("Position from clicked element: " + s))
}
* {
  padding: 5px;
  margin: 5px;
  outline: 1px solid pink;
  list-style-position: inside;
}

a {
  display: block
}

.highlight {
  background: yellow;
}

*:hover {
  outline: 1px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<header>
  <div>1</div>
  <div>2</div>
  HEADER:
  <nav>
    NAV:
    <ul>
      UL:
      <li>li <a href="#">item <i>1</i></a> </li>
      <li>li <a href="#">item <i>2</i></a> </li>
      <li>li <a href="#">item <i>3</i></a> </li>
      <li>li <a href="#">item <i>4</i></a> </li>
    </ul>
  </nav>
</header>

Upvotes: 2

Roko C. Buljan
Roko C. Buljan

Reputation: 206151

Let me start with a pretty simple JavaScript idea before presenting the jQuery one:

JavaScript - Get first closest multiple siblings

This could be easily made by using Element.querySelectorAll() with a ":scope >" query for immediate child elements of the same Element.tagName, and a recursive function:

const closestMultiple = el => {
  const par = el.parentElement;
  const sib = par && par.querySelectorAll(`:scope > ${el.tagName}`);
  return (sib && sib.length > 1 ? sib : par && closestMultiple(par))
}

document.body.addEventListener("click", (ev) => {
  const cm = closestMultiple(ev.target)
  if(cm) cm.forEach(EL => EL.classList.toggle("highlight"));
});
* {
  padding: 5px;
  margin: 5px;
  outline: 1px solid pink;
  list-style-position: inside;
  background: #fff;
}
*:hover {
  outline: 1px solid red;
}

a {
  display: block
}

.highlight {
  background: yellow;
}
<header>
  <div>
    HEADER:
  </div>
  <div>
    <nav>
      NAV:
      <ul>
        UL:
        <li>li <a href="#">item <i>1</i></a> </li>
        <li>li <a href="#">item <i>2</i></a> </li>
        <li>li <a href="#">item <i>3</i></a> </li>
        <li>li <a href="#">item <i>4</i></a> </li>
      </ul>
    </nav>
  </div>
</header>

jQuery - Get first closest multiple siblings

  • Invert the array using .reverse() to speed up the process starting from the Target Element (instead of document).
  • Stop the loop as soon as Array.prototype.some() matches the length > 1 criteria:
function closestMultiple(el, $mu) {
  return $(el).parents().addBack().get().reverse().some(el => {
    const $gr = $(el).siblings(el.tagName).addBack();
    return ($gr.length > 1 ? $mu = $gr : 0);
  }) && $mu || $();
}

function closestMultiple(el, $mu) {
  return $(el).parents().addBack().get().reverse().some(el => {
    const $gr = $(el).siblings(el.tagName).addBack();
    return ($gr.length > 1 ? $mu = $gr : 0);
  }) && $mu || $();
}

$(document).on('click', (ev) => {
  const $clRep = closestMultiple(ev.target).toggleClass("highlight");
  console.clear(); console.log($clRep.length +" "+ $clRep.prop("tagName"));
});
* {
  padding: 5px;
  margin: 5px;
  outline: 1px solid pink;
  list-style-position: inside;
  background: #fff;
}
*:hover {
  outline: 1px solid red;
}

a {
  display: block
}

.highlight {
  background: yellow;
}
<header>
  <div>
    HEADER:
  </div>
  <div>
    <nav>
      NAV:
      <ul>
        UL:
        <li>li <a href="#">item <i>1</i></a> </li>
        <li>li <a href="#">item <i>2</i></a> </li>
        <li>li <a href="#">item <i>3</i></a> </li>
        <li>li <a href="#">item <i>4</i></a> </li>
      </ul>
    </nav>
  </div>
</header>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

jQuery micro-plugin

For a nicer experience (since you're already using jQuery) you could extend the jQuery constructor with your own .closestMultiple() method.
Here's a suggestion for a Micro-plugin:

$.fn.closestMultiple = function($mu) {
  return this.parents().addBack().get().reverse().some(el => {
    const $gr = $(el).siblings(el.tagName).addBack();
    return ($gr.length > 1 ? $mu = $gr : 0);
  }) && $mu || $();
}

$(document).on('click', (ev) => {
  $(ev.target).closestMultiple().toggleClass("highlight");
});
* {
  padding: 5px;
  margin: 5px;
  outline: 1px solid pink;
  list-style-position: inside;
  background: #fff;
}

a {
  display: block
}

.highlight {
  background: yellow;
}

*:hover {
  outline: 1px solid red;
}
<header>
  <div>
    HEADER:
  </div>
  <div>
    <nav>
      NAV:
      <ul>
        UL:
        <li>li <a href="#">item <i>1</i></a> </li>
        <li>li <a href="#">item <i>2</i></a> </li>
        <li>li <a href="#">item <i>3</i></a> </li>
        <li>li <a href="#">item <i>4</i></a> </li>
      </ul>
    </nav>
  </div>
</header>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

Upvotes: 2

Erik P_yan
Erik P_yan

Reputation: 798

I think this simple way may help you:

  $("i, a, li").on('click', function(event) {
      var closest_li = $(this).closest('ul').find('li');

      console.log(closest_li);
  });

So you will get array of all <li> elements which exist in parent <ul>.

Upvotes: -2

Related Questions