18

I have an HTML element with only visible text inside. This example is a <div> element, but it could be a <span>, <p>, or other DOM element.

<div>This is a simple example.</div>

When clicked, I can get the position of the cursor on the surface of the div, but I need to determine the position of the nearest character and/or its index into the div.innerHTML string at the time of the click.

I found a similar implementation in the "getCharNumAtPosition" method in SVG text entities here.

Is it possible to implement such a function in JavaScript that works with HTML?

(Solutions would be most useful if they are portable across most modern browsers, work with most written languages, and are based on relatively stable standards so that they will not become buggy later.)

James Wilkins
  • 6,836
  • 3
  • 48
  • 73
micnic
  • 10,915
  • 5
  • 44
  • 55

4 Answers4

18
$('div').click( function () {
  getSelectionPosition (); 
});


function getSelectionPosition () {
  var selection = window.getSelection();
  console.log(selection.focusNode.data[selection.focusOffset]);
  alert(selection.focusOffset);
}

This works with "click", as well as with a "range" for most browsers. (selection.type = "caret" / selection.type = "range").

selection.focusOffset() gives you the position in the inner node. If elements are nested, within <b> or <span> tags for example, it will give you the position inside the inner element, not the full text, more or less. I'm unable to "select" the first letter of a sub tag with focusOffset and "caret" type (click, not range select). When you click on the first letter, it gives the position of the last element before the start of tag plus 1. When you click on the second letter, it correctly gives you "1". But I didn't find a way to access the first element (offset 0) of the sub element. This "selection/range" stuff seems buggy (or very non-intuitive to me). ^^

But it's quite simple to use without nested elements! (Works fine with your <div>)

Here is a fiddle

Important edit 2015-01-18:

This answer worked back when it was accepted, but not anymore, for reasons given below. Other answers are now most useful.

  • Matthew's general answer
  • The working example provided by Douglas Daseeco.

Both Firefox and Chrome debugged window.getSelection() behavior. Sadly, it is now useless for this use case. (Reading documentation, IE 9 and beyond shall behave the same).

Now, the middle of a character is used to decide the offset. That means that clicking on a character can give back 2 results. 0 or 1 for the first character, 1 or 2 for second, etc.

I updated the JSFiddle example.

Please note that if you resize the window (Ctrl + mouse), the behavior is quite buggy on Chrome for some clicks.

Douglas Daseeco
  • 3,475
  • 21
  • 27
roselan
  • 3,755
  • 1
  • 20
  • 20
  • Thanks a lot, this is best answer I could get ) – micnic Nov 12 '11 at 22:02
  • Offtopic, but why a named function wrapped inside an anonymous function, instead of just the named function in the `$('element').event(function())` part, which would be `$('div').click( getSelectionPosition() );` in the example? I know mine is a general JS syntax question, but this specific code example raised the question to me. Explanation appreciated! – porg Nov 23 '16 at 08:21
  • your are right `$('div').click(getSelectionPosition);` would work prefectly in this case (but not `$('div').click(getSelectionPosition());` !). I used the indirect form by force of habit. I can only invite you to google javascript callbacks. – roselan Nov 23 '16 at 14:16
  • 1
    It is not likely that dealing with characters at a level below the DOM objects is ever going to be very portable. – Douglas Daseeco Jan 21 '17 at 02:01
  • @micnic, now that this answer is not portable across commonly used browsers (which its author identified back in 2015 above), perhaps you could select one of the working answers so that people don't use buggy code thinking it to be proven? – Douglas Daseeco Feb 04 '17 at 00:15
3

You could, using JavaScript, break each character into it's own SPAN tag and add an onclick event for each. Then, read the x, y position from the event data when the SPAN is clicked.

As well, you will have access to the character clicked with event.target.innerHTML

Matthew
  • 8,183
  • 10
  • 37
  • 65
  • 2
    I was thinking about this idea, but I want to implement this functionality in a simple notepad web application that uses HTML elements, and if I use spans for every character it would use a lot of memory, I want to find a low level solution, if it exists :) – micnic Nov 12 '11 at 17:51
  • I would say you can count the character's position based on the font size but unless you are using a fixed width font, this wouldn't work. – Matthew Nov 12 '11 at 17:56
  • font size won't do, use can ctrl+mousewheel. I guess the op best hope lies with window.getSelection() and the range object (and createRange) – roselan Nov 12 '11 at 18:33
  • You wouldn't be able to predict the wrapping in every case even in a fixed width font. Using getSelection could be very buggy in some cases, depending on how the character position would be processed, and there are portability issues. What used to be too much memory utilization is probably not an issue anymore. The entire issue is surrounding the idea that each character is NOT a DOM object. The pure OO solution is to make them so, and this is not particularly novel. It is done all the time. I wonder what the byte overhead is for a div or span DOM node in a real browser? – Douglas Daseeco Jan 21 '17 at 01:27
  • @Douglas Daseeco I absolutely addressed *how* to do it - the implementation is up to you. It pretty simple... – Matthew Jan 31 '17 at 23:24
1

This example is quick loading for moderately sized blocks of text, portable, and robust. Although its elegance is not immediately apparent, reliability lies in the creation of a simple one to one correspondence between international characters and DOM event listeners 1.

It is best to relying only on widely adopted character encoding, DOM, and ECMA standards as in this example. Other approaches often rely on low level event target attributes or functions like getSelection(), which are neither stable over time nor portable across browsers and languages 2.

The example code could be modified in a number of ways for specific applications. For instance other mechanisms could be employed to select which DOM objects are initialized for charClick callbacks. Surrogate pairs could be supported in the i loop too.

<!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) {
                        to = to + pre + i + inf + from.charAt(i) + 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">Test &#xE6;&#x20AC; &ndash; &#xFD7;&#xD8; &mdash; string.</p>
        <p id="p1">Next  E para.</p>
        <p id="p2">&copy; 2017</p>
        <hr />
        <p id="result">&nbsp;</p>
    </body>
</html>

Full testing is recommended before using this in internationalized production scenarios. If bugs are found, please suggest edits or add comments so that refinements can be added.


Notes

[1] These event listeners could have been added directly but would have required branching for browser portability or the extra weight of libraries that add no particular speed or convenience value for this use case.

[2] See notes about select events in W3C's UI events page.

Robouste
  • 3,020
  • 4
  • 33
  • 55
Douglas Daseeco
  • 3,475
  • 21
  • 27
  • 1
    This code is working better across browsers and mobile devices than the JQuery component we had been using in our back office content editor. – Douglas Daseeco Feb 04 '17 at 00:10
1

In many cases, a possible solution can be to add zero-width space character (U+200B) between each char of the content of the tag. Then using javascript window.getSelection().focusOffset + some simple computations does the job.

Sample html code (including zero-width space characters):

<div onclick='magic(this)'>T​h​i​s​ ​i​s​ ​a​ ​s​i​m​p​l​e​ ​e​x​a​m​p​l​e​.</div>

Sample javascript code:

<script>
    function magic(e)
    {
        var s, p, c;
        s = e.innerHTML;
        p = window.getSelection().focusOffset;
        c = s.charAt((p >> 1) << 1);
        alert("index:" + (p >> 1) + " char: " + c);
    }
</script>

Seems to work widely, even on internet explorer!

jsFidle: http://jsfiddle.net/by1a9pfm/

Simon Hi
  • 2,838
  • 1
  • 17
  • 17