Michael
Michael

Reputation: 9402

How to determine character in textnode that was clicked on?

I can set up an event listener to tell me when a mouse click occurred at some place in an HTML document. But if the click occurred on some text, I need to know which character in the text the click occurred over. Is there a way to do this?

I can think of some really obnoxious solutions. For instance, for every single character in the document I could wrap it in a separate element with its own event. Or, since I can tell which textnode the click occurred in, I could perform some kind of calculation (basically almost like simulating rendering of the text) perhaps using clientWidth, to determine which character the click occurred in.

Surely there must be something easier?

Upvotes: 3

Views: 3531

Answers (4)

Oleksa
Oleksa

Reputation: 665

This is my effort to implement what Michael wrote in his answer:

function hitCharBinSearch(mClientX, inmostHitEl) {

    const originalInmost = inmostHitEl
    const bareText = inmostHitEl.firstChild.textContent
    var textNode = inmostHitEl.firstChild
    var textLenghtBeforeHit = 0
    do {
        let textNodeR = textNode.splitText(textNode.length / 2)
        let textNodeL = textNode

        let spanL = document.createElement('span')
        spanL.appendChild(textNodeL)
        let spanR = document.createElement('span')
        spanR.appendChild(textNodeR)

        inmostHitEl.appendChild(spanL)
        inmostHitEl.appendChild(spanR)

        if (mClientX >= spanR.getBoundingClientRect().left) {
            textNode = textNodeR
            inmostHitEl = spanR
            textLenghtBeforeHit += textNodeL.length
        }
        else {
            textNode = textNodeL
            inmostHitEl = spanL
        }
    } while (textNode.length > 1)

    /* This is for proper caret simulation. Can be omitted */
    var rect = inmostHitEl.getBoundingClientRect()
    if (mClientX >= (rect.left + rect.width / 2)) 
        textLenghtBeforeHit++
    /*******************************************************/

    originalInmost.innerHTML = bareText
    return textLenghtBeforeHit
}

Upvotes: 3

Douglas Daseeco
Douglas Daseeco

Reputation: 3671

Placing each character in a document model object is not as obnoxious as it sounds. HTML parsing, DOM representation, and event handling is quite efficient in terms of memory and processing in modern browsers. A similar mechanism is used at a low level to render the characters too. To simulate what the browser does at that level takes much work.

  • Most documents are constructed with variable width characters
  • Wrapping can be justified or aligned in a number of ways
  • There is not a one to one mapping between characters and bytes
  • To be a truly internationalized and robust solution, surrogate pairs must be supported too 1

This example is lightweight, loads quickly, and is portable across common browsers. Its elegance is not immediately apparent, much reliability is gained by establishing a one to one correspondence between international characters and event listeners.

  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="utf-8">
      <title>Character Click Demo</title>
      <script type='text/javascript'>
          var pre = "<div onclick='charClick(this, ";
          var inf = ")'>";
          var suf = "</div>"; 
          function charClick(el, i) {
              var p = el.parentNode.id;
              var s = "para '" + p + "' idx " + i + " click";
              ele = document.getElementById('result');
              ele.innerHTML = s; }
          function initCharClick(ids) {
              var el; var from; var length; var to; var cc;
              var idArray = ids.split(" ");
              var idQty = idArray.length;
              for (var j = 0; j < idQty; ++ j) {
                  el = document.getElementById(idArray[j]);
                  from = unescape(el.innerHTML);
                  length = from.length;
                  to = "";
                  for (var i = 0; i < length; ++ i) {
                      cc = from.charAt(i);
                      to = to + pre + i + inf + cc + suf; }
                  el.innerHTML = to; } }
      </script>
      <style>
          .characters div {
              padding: 0;
              margin: 0;
              display: inline }
      </style>
  </head>
  <body class='characters' onload='initCharClick("h1 p0 p2")'>
      <h1 id='h1'>Character Click Demo</h1>
      <p id='p0'>&#xE6;&#x20AC; &ndash; &#xFD7;&#xD8; &mdash;</p>
      <p id='p1'>Next 𐐷 😀E para.</p>
      <p id='p2'>&copy; 2017</p>
      <hr>
      <p id='result'>&nbsp;</p>
  </body>
  </html>

[1] This simple example does not have handling for surrogate pairs, but such could be added in the body of the i loop.

Upvotes: 0

Michael
Michael

Reputation: 9402

Once the mouse event is captured, split the text in the element into two separate spans. Look at the offset of each span to determine which the event occurred in. Now split that span in two and compare again. Repeat until you have a span that has a single character whose coordinates contain the coordinates of the mouse click. Since this is essentially a binary search this whole process will be fairly quick, and the total number of span low compared to the alternative. Once the character has been found, the spans can be dissolved and all the text returned to the original element.

Upvotes: 11

icktoofay
icktoofay

Reputation: 129001

You do, unfortunately, have to wrap every character in an element, but you do not have to attach an event listener to each one. When the click event is fired on the element, it is bubbled up to its parents. You can then retrieve the element that was actually clicked by using the target property of the event.

Say we've got some text in an element named textElement. It contains a span for each character. If we wanted to be able to click on characters to delete them, we could use this code:

textElement.addEventListener('click', function(e) {
    textElement.removeChild(e.target);
}, false);

Try it out.

Upvotes: 3

Related Questions