Search code examples
javascriptevent-handlingdom-eventscontenteditablekeydown

How to a catch a 'keydown' event by an element which inherited attribute contenteditable?


For introduction I would like to explain what I try to achieve:

Inside the *div#content* I would like to detect keydown events AND get the element for which the keydown element is detected. For example TAB inside the paragraph "Coding is fun." should return the paragraph element and TAB key pressed.

//javascript
const buttonEditable = document.getElementById("buttonEditable");
const contentDiv = document.getElementById("content");
const paragraph = document.getElementById('paragraph');
paragraph.addEventListener('keydown',keydown);

function keydown(){
    console.log(event.key); // should return TAB
    console.log(event.target); //should return paragraph element
}

buttonEditable.addEventListener('click',toggleEditable);

function toggleEditable(){
    if(contentDiv.contentEditable === 'false'){
        contentDiv.contentEditable = true;
    } else {
        contentDiv.contentEditable = false;
    };
}
<!-- HTML -->

<div id="content"> <!-- div container keeps HTML element which are toggled from editable to non-editable-->
    <p id="paragraph">Coding is fun.</p>
    <!-- ...bunch of further html element-->
</div>

<button id="buttonEditable">toggleEditable</button>


I made the following investigation: 'keydown' events are only detectable for elements which have the attribute contenteditable = true. By the way 'click' events are detectable for no-matter the contentEditable attribute is set to.

When add contenteditable = true to the html code. It works... TAB keydown inside paragraph is detected:

 <div id="content">
    <p id="paragraph" contenteditable="true">Coding is fun.</p> <!-- <<<<<<<<<<-------contenteditable added -->
</div>

<button id="buttonEditable">toggleEditable</button>

Does somebody know a way how the TAB keydown AND element can be detected by the paragraph which inherits contenteditable = "true"? Of course with a queryselector I could assign assign contenteditable attribute to any element inside div#content but this seems not very elegant to me.


Solution

  • It seems, that once a parent element is set as content-editable its child-nodes are not anymore detectable/targetable. The event-target will always be the top-most content-editable element-node (close to like everything else is merely seen now as a single overall text-content).

    ... test case ...

    function handleKeydown(evt) {
      console.log(evt.key);
      console.log(evt.target);
    }
    function toggleTopMostContenteditable () {
      const elm = document.querySelector('#content');
    
      elm.contentEditable = (elm.contentEditable !== 'true');
    }
    document
      .querySelectorAll('#content, #content > p')
      .forEach(elm =>
        elm.addEventListener('keydown', handleKeydown)
      );
    document
      .querySelector('button')
      .addEventListener('click', toggleTopMostContenteditable);
    body { margin: 0;}
    p { margin: 5px; }
    
    #content {
      width: 30.7%;
      margin: 0 0 10px 0;
      padding: 5px;
    
      border: 1px dashed red;
      
      &[contenteditable="true"],
      [contenteditable="true"] {
        
        border: 1px solid green;
      }
    }
    .as-console-wrapper {
      left: auto!important;
      bottom: 0;
      width: 67%;
      min-height: 100%;
    }
    <div id="content" contenteditable="true">
      <p contenteditable="true">Coding is fun.</p>
      <p contenteditable="true">The quick brown fox ...</p>
      <p contenteditable="true">... jumps over the lazy dog.</p>
    </div>
    
    <button>Toggle top-most contenteditable</button>

    In order to achieve what the OP wants, one has to fake the parent element's contentEditable state together with an implementation which handles the child-element specific contentEditable states.

    Something similar too ...

    function handleTargetSpecificKeydown(evt) {
      console.log(evt.key);    // - should return TAB
      console.log(evt.target); // - should return paragraph element
    }
    function handleTargetSpecificEditableState({ target }) {
      if (!target.matches('#content')) {
    
        const recentlyEditable = target.contentEditable === 'true';
        const currentlyEditable =
          (target.closest('#content').dataset.contenteditable === 'true');
    
        if (!recentlyEditable && currentlyEditable) {
    
          target.addEventListener('keydown', handleTargetSpecificKeydown);
          target.contentEditable = true;
          target.focus();
    
        } else if (recentlyEditable && !currentlyEditable) {
    
          target.contentEditable = false;
          target.removeEventListener('keydown', handleTargetSpecificKeydown);
        }
      }
      console.log({
        target,
        'target.contentEditable': target.contentEditable,
      });
    }
    
    function toggleOverallEditableState () {
      const { dataset } = contentDiv;
    
      dataset.contenteditable = (dataset.contenteditable !== 'true');
    
      console.log({ contentDiv });
    }
    const contentDiv = document.querySelector('#content');
    
    contentDiv
      .addEventListener('click', handleTargetSpecificEditableState);
    
    document
      .querySelector('button')
      .addEventListener('click', toggleOverallEditableState);
    body { margin: 0;}
    p { margin: 5px; }
    
    #content {
      width: 30.7%;
      margin: 0 0 10px 0;
      padding: 5px;
    
      p {
        border: 1px dashed red;
      }
      border: 1px dashed red;
      
      &[data-contenteditable="true"],
      [contenteditable="true"] {
        
        border: 1px solid green;
      }
    }
    .as-console-wrapper {
      left: auto!important;
      bottom: 0;
      width: 67%;
      min-height: 100%;
    }
    <div id="content">
      <p>Coding is fun.</p>
      <p>The quick brown fox ...</p>
      <p>... jumps over the lazy dog.</p>
    </div>
    
    <button>Toggle overeall editable state</button>