Get the cursor position in a text that has emojis and insert tags

What's up guys, how's it going? I'm having trouble saving the cursor position and inserting dynamic tags.I'm using the Emojiarea plugin to create a div where I can write texts, insert emojis, templates and tags. https://github.com/mervick/emojionearea

I use the following function below to create a div on my textarea:

$("#email_campaign_description").emojioneArea({
    search: false,
    recentEmojis: false,
    pickerPosition: "right",
    events: {
      blur: function (editor, event) {
        $scope.lastPosition = getCaretCharacterOffsetWithin(editor[0])
      },
    }
  });

The next function returns the last position of my cursor when I click somewhere in the text:

function getCaretCharacterOffsetWithin(element) {
    var caretOffset = 0;
    var doc = element.ownerDocument;
    var win = doc.defaultView;
    var sel;
    if (typeof win.getSelection != "undefined") {
      sel = win.getSelection();
      if (sel.rangeCount > 0) {
        var range = win.getSelection().getRangeAt(0);
        var preCaretRange = range.cloneRange();
        preCaretRange.selectNodeContents(element);
        preCaretRange.setEnd(range.endContainer, range.endOffset);
        caretOffset = preCaretRange.toString().length;
      }
    } else if ( (sel = doc.selection) && sel.type != "Control") {
      var textRange = sel.createRange();
      var preCaretTextRange = doc.body.createTextRange();
      preCaretTextRange.moveToElementText(element);
      preCaretTextRange.setEndPoint("EndToEnd", textRange);
      caretOffset = preCaretTextRange.text.length;
    }
    debugger
    return caretOffset;
  }

And the last function adds dynamic tags to the text:

$scope.chooseTag = function (label) {
    var domElement = $('#email_campaign_description');
    var emojiElement = domElement[0].emojioneArea;

    if (document.selection) {
      domElement.focus();
      var sel = document.selection.createRange();
      sel.text = $scope.tags.model;
      domElement.focus();
    } else if ($scope.lastPosition) {
      var startPos = $scope.lastPosition;
      var endPos = startPos + $scope.tags.model[0].length;
      emojiElement.setText(emojiElement.getText().substring(0, startPos) + ' ' + $scope.tags.model + ' ' + emojiElement.getText().substring(endPos, emojiElement.getText().length));
      domElement.focus();
    } else {
      emojiElement.setText($scope.tags.model);
      domElement.focus();
    }

    if ($scope.tags.model === '[vendor_name]') {
      emojiElement.setText(domElement.val().replace('[vendor_name]', $scope.vendor.name));
    }

    $scope.campaign.description = $('#email_campaign_description').val();
  };

The problem happens that the function that stores the click position in the text does not read emojis. So, if I write a text like: "Hello [emoji], welcome!" and at the end of that text I try to add a tag, my function will not read the emoji, and will insert the tag over the last character of my sentence, in this case "!". Likewise if I add two emojis, my function will not read and insert the tag over the last two characters in this case "o!". The correct thing would be my function to read these two emojis, and add my tag exactly in the desired location, that is: "Hello [emoji], welcome! [Tag]"

What can I do for my function getCaretCharacterOffsetWithin(element) to read emojis as a character, or a space occupied?

Upvotes: 2

Views: 1091

Answers (2)

Vadim Berman
Vadim Berman

Reputation: 2032

A library-free (Typescript) solution:

    correctUnicodeOffset(offset: number, str: string): number {
        if (offset < 1) return offset;
        return Array.from(str.substr(0, offset)).length;
    }

Use it like this:

   myOffset = this.correctUnicodeOffset(myOffset, myStr);

Upvotes: 0

Matt Davies
Matt Davies

Reputation: 141

The problem is that javascript isn't great at handling Unicode strings.

For example:

"hello".length === 5

"👩🏻‍🦰".length === 7

There are several libraries that can help accurately measure the length of unicode strings. Graphemer is one of them (full disclosure: I published this library).

To fix your getCaretCharacterOffsetWithin(element) function do the following:

  1. Import and instantiate the Graphemer library.
import Graphemer from 'graphemer';

const splitter = new Graphemer();

function getCaretCharacterOffsetWithin(element) {...}
  1. Update the first instance where you count the string length.
caretOffset = preCaretRange.toString().length; // original

caretOffset = splitter.countGraphemes(preCaretRange.toString()); // updated
  1. Update the second instance where you count the string length.
caretOffset = preCaretTextRange.text.length; // original

caretOffset = splitter.countGraphemes(preCaretTextRange.text); // updated

Upvotes: 2

Related Questions