contenteditable selection of text not working

2019-06-21 23:23发布

问题:

I am faced with following: when I try to select text in a contenteditable element and the end of the selection is the start of the element content, then no select event is fired and there are no Selection and Range objects.

Could somebody please give me any advice on why this might occur or how I can prevent this?

Code responsible for getting selection range:

$('div[contenteditable="true"]').bind("mouseup keyup touchend", function() {
  lastCaretIndex = getSelectionRange();
});

function getSelectionRange() {
  var sel;
  if (window.getSelection) {
    sel = window.getSelection();

    console.log(sel); // this doesn't print anything event empty string

    if (sel.rangeCount) {
      return sel.getRangeAt(0);
    }
  } else if (document.selection) {
    return document.createRange();
  }

  return null;
}
<div id="main-input" contenteditable="true">Hello world!</div>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script>

JSFiddle (open your browser console to make sure that selection doesn't get logged).

回答1:

The issue is that you only log selection changes when specific events occur on the contenteditable element. More specifically, you have

$('div[contenteditable="true"]').bind("mouseup keyup touchend", // ...

In particular the mouseup event will normally be triggered when the selection changes. Except when it doesn't. When you release the mouse outside of the editable div (which you do in your example!), then the div will never receive a mouseup event and thus never log the selection.

There are two ways around this:

  1. Listen for events on the entire body. Downsides are that you receive more events that do not influence the selection and that it is still possible to get mouseup events outside of the page.
  2. Listen for the selectionchange event.

document.addEventListener('selectionchange', function(event) {
  console.log(event.type);
});
<div contenteditable="true">Hello world!</div>

You can of course still access the selection as you currently do inside this event handler. This event is triggered every time the selection changes, so you may want to throttle it.

Full implementation of that can be found below.

function handler() {
  // do whatever you want here
  // this shows the selection and all ranges it consists of
  var sel = window.getSelection(),
      ranges = Array(sel.rangeCount).fill(0).map((_, i) => sel.getRangeAt(i));
  ranges = ranges.map((r) => `${r.startOffset}-${r.endOffset}`).join(';');
  console.log(`Selection [${ranges}:"${sel.toString()}"]`);
}

function throttle(func) {
  var timeoutId = false,
      called = false,
      wrap = function() {
        if (!called) {
          clearInterval(timeoutId);
          timeoutId = false;
        } else {
          func();
        }
        called = false;
      };
  return function() {
    if (timeoutId === false) {
      func();
      timeoutId = setInterval(wrap, 500);
    } else {
      called = true;
    }
  };
}

document.addEventListener('selectionchange', throttle(handler));
<div contenteditable="true">Hello world!</div>



回答2:

Your actual code works perfectly and logs a Selection object in the console, even if the end of the selection is the start of the element content.

Indeed you need to log this Selection text instead of logging the whole object which reflects the whole Selection object changes for each event.

I updated your snippet to log the text of the selection using Selection.toString(), you can see it working here:

$('div[contenteditable="true"]').bind("mouseup keyup touchend", function() {
  lastCaretIndex = getSelectionRange();
});

function getSelectionRange() {
  var sel;
  if (window.getSelection) {
    sel = window.getSelection();

    console.log(sel.toString()); // this doesn't print anything event empty string

    if (sel.rangeCount) {
      return sel.getRangeAt(0);
    }
  } else if (document.selection) {
    return document.createRange();
  }

  return null;
}
<div id="main-input" contenteditable="true">Hello world!</div>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script>

You can check this answer, it shows and explains the perfect way to get text selection.