Search code examples
jquerysiblingsarrow-keys

jQuery next sibling that has a particular kind of child


My HTML has a container element with many sibling div elements, each of which contains a contenteditable p. These sibling div are "interrupted", however, by other div which do not contain an editable element.

What is challenging me at the moment is how to "hop over" these interrupting div when using the left and right arrow keys to move from C to D or from D back to C (see snippet). Navigation stops when it encounters these div lacking an editable element. How can I correct this?

$('#foo p[contenteditable = "true"]').bind("keydown", function(e) {
  switch (e.key) {
    case "ArrowLeft":
      var $P = $(this).parent().prev().children('p[contenteditable = "true"]');
      setTimeout(() => $P.focus(), 0);
      e.stopImmediatePropagation();
      e.preventDefault();
      break;

    case "ArrowRight":
      var $P = $(this).parent().next().children('p[contenteditable = "true"]');
      setTimeout(() => $P.focus(), 0);
      e.stopImmediatePropagation();
      e.preventDefault();
      break;
  }
});
#foo p[contenteditable="true"] {
  /* font-size: 22px;*/
  height: 22px;
  width: 22px;
  color: cyan;
  font-weight: bold;
  background-color: cyan;
}

#foo p.const {
  background-color: inherit;
  border: none;
  height: 22px;
  width: 22px;
}

#foo div {
  text-align: center;
  display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="foo">
  <div>
    <p contenteditable="true" tabindex="0"></p>
    <p>A</p>
  </div>
  <div>
    <p contenteditable="true" tabindex="1"></p>
    <p>B</p>
  </div>
  <div>
    <p contenteditable="true" tabindex="2"></p>
    <p>C</p>
  </div>
  <div>
    <p class="const">&nbsp;</p>
    <p>&nbsp;</p>
  </div>
  <div>
    <p class="const">"</p>
    <p>"</p>
  </div>
  <div>
    <p contenteditable="true" tabindex="3"></p>
    <p>D</p>
  </div>
  <div>
    <p contenteditable="true" tabindex="4"></p>
    <p>E</p>
  </div>
</div>


Solution

  • Instead of prev() or next(), use .prevAll(":has(p[contenteditable])").first() and .nextAll(":has(p[contenteditable])").first():

    $('#foo p[contenteditable = "true"]').bind("keydown", function(e) {
    
      switch (e.key) {
        case "ArrowLeft":
          var $P = $(this).parent().prevAll(":has(p[contenteditable])").first().children('p[contenteditable = "true"]');
          setTimeout(() => $P.focus(), 0);
          e.stopImmediatePropagation();
          e.preventDefault();
          break;
    
        case "ArrowRight":
          var $P = $(this).parent().nextAll(":has(p[contenteditable])").first().children('p[contenteditable = "true"]');
          setTimeout(() => $P.focus(), 0);
          e.stopImmediatePropagation();
          e.preventDefault();
          break;
    
      }
    
    });
    #foo p[contenteditable="true"] {
      /* font-size: 22px;*/
      height: 22px;
      width: 22px;
      color: cyan;
      font-weight: bold;
      background-color: cyan;
    }
    
    #foo p.const {
      background-color: inherit;
      border: none;
      height: 22px;
      width: 22px;
    }
    
    #foo div {
      text-align: center;
      display: inline-block;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div id="foo">
    
      <div>
        <p contenteditable="true" tabindex="0"></p>
        <p>A</p>
      </div>
      <div>
        <p contenteditable="true" tabindex="1"></p>
        <p>B</p>
      </div>
      <div>
        <p contenteditable="true" tabindex="2"></p>
        <p>C</p>
      </div>
      <div>
        <p class="const">&nbsp;</p>
        <p>&nbsp;
          <p>
      </div>
      <div>
        <p class="const">"</p>
        <p>"
          <p>
      </div>
      <div>
        <p contenteditable="true" tabindex="3"></p>
        <p>D</p>
      </div>
      <div>
        <p contenteditable="true" tabindex="4"></p>
        <p>E</p>
      </div>
    
    
    
    </div>

    prevAll/nextAll match all previous/following siblings that match the selector, returning a jQuery object in order (so the nearest sibling is first, then the second nearest, etc.); using .first() then reduces the set to just the nearest sibling.

    I've used the :has pseudo-selector in that, but you might be better off giving the relevant elements a class instead, since :has is a jQuery-specific addition to CSS. Here I've used the class stop so it's .xxxxAll(".stop").first():

    $('#foo p[contenteditable = "true"]').bind("keydown", function(e) {
    
      switch (e.key) {
        case "ArrowLeft":
          var $P = $(this).parent().prevAll(".stop").first().children('p[contenteditable = "true"]');
          setTimeout(() => $P.focus(), 0);
          e.stopImmediatePropagation();
          e.preventDefault();
          break;
    
        case "ArrowRight":
          var $P = $(this).parent().nextAll(".stop").first().children('p[contenteditable = "true"]');
          setTimeout(() => $P.focus(), 0);
          e.stopImmediatePropagation();
          e.preventDefault();
          break;
    
      }
    
    });
    #foo p[contenteditable="true"] {
      /* font-size: 22px;*/
      height: 22px;
      width: 22px;
      color: cyan;
      font-weight: bold;
      background-color: cyan;
    }
    
    #foo p.const {
      background-color: inherit;
      border: none;
      height: 22px;
      width: 22px;
    }
    
    #foo div {
      text-align: center;
      display: inline-block;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <div id="foo">
    
      <div class="stop">
        <p contenteditable="true" tabindex="0"></p>
        <p>A</p>
      </div>
      <div class="stop">
        <p contenteditable="true" tabindex="1"></p>
        <p>B</p>
      </div>
      <div class="stop">
        <p contenteditable="true" tabindex="2"></p>
        <p>C</p>
      </div>
      <div>
        <p class="const">&nbsp;</p>
        <p>&nbsp;
          <p>
      </div>
      <div>
        <p class="const">"</p>
        <p>"
          <p>
      </div>
      <div class="stop">
        <p contenteditable="true" tabindex="3"></p>
        <p>D</p>
      </div>
      <div class="stop">
        <p contenteditable="true" tabindex="4"></p>
        <p>E</p>
      </div>
    
    
    
    </div>