2

I'm trying to remove leading or trailing (sometimes both) whitespace from the user selected text. Implemented according to this answer. This works for simple cases, however, when the selected text contains tags or  , it fails.

Example: in the fiddle try to highlight -- hi this is bob. from right to left, including the space at the end, then press Trim.

This results in:

Uncaught IndexSizeError: Failed to execute 'setEnd' on 'Range': The offset 24 is larger than or equal to the node's length (5).

I guess this can be caught with

 if (method == range.setEnd  && range.startOffset + ind >= range.endContainer.length)

but I'm not sure how to handle it. I've also tried to replace the hard spaces using

e2 = document.getElementById('e2');
e2.innerHTML = e2.innerHTML.replace(/ /gi, ' ');

However, this makes the selection empty. Code:

function removeWsFromSelection(fromStart) {
  selection = window.getSelection();
  range = selection.getRangeAt(0);
  if (fromStart) {
    regex = /[^\s]/;
    container = range.startContainer;
    method = range.setStart;
  } else {
    regex = /\s+$/;
    container = range.endContainer;
    method = range.setEnd;
  }

  match = regex.exec(selection.toString());
  if (match) {
    ind = match.index;
    if (ind > 0) {
      // ind is the first non-ws char from the start or first ws char from the end,
      // hence (startOffset + ind)
      method.call(range, container, range.startOffset + ind);
      rng = range.cloneRange();
      selection.removeAllRanges();
      selection.addRange(rng);
    }
  }
}

BTW, unfortunately, Selection.modify didn't work for me, and besides it's considered non-standard.

Community
  • 1
  • 1
dimid
  • 7,285
  • 1
  • 46
  • 85

3 Answers3

3

If you are fine with range modifying, you're able to check the length of trimmed symbols from the start and the end, and then modify startOffset and endOffset of the Range. But sure this is not a silver bullet for a cases where startContainer and endContainer are not the same node. At least it works for a certain of cases.

const sel = window.getSelection();
const text = sel.toString();
const range = sel.getRangeAt(0);

const startOffset = text.length - text.trimStart().length;
const endOffset = text.length - text.trimEnd().length;

if (startOffset) {
  range.setStart(range.startContainer, range.startOffset + startOffset);
}

if (endOffset) {
  range.setEnd(range.endContainer, range.endOffset - endOffset);
}
0

This is very ugly and doesn't handle the general case, but seems to work:

function removeWsFromSelection(fromStart) {
      selection = window.getSelection();
      range = selection.getRangeAt(0);
      if (fromStart) {
        regex = /[^\s]/;
        container = range.startContainer;
        method = range.setStart;
      }
      else {
        regex = /\s+$/;
        container = range.endContainer;
        method = range.setEnd;
      }

      match = regex.exec(selection.toString());
      if (match) {
        ind = match.index;
        if (ind > 0) {
            // ind is the first non-ws char from the start or first ws char from the end,
            // hence (startOffset + ind)
            if (method == range.setEnd  && range.startOffset + ind >= range.endContainer.length) {
              match = regex.exec(range.endContainer.textContent);
              if (match) {
                range.setEnd(range.endContainer, match.index); 
              }
            }
            else {
              method.call(range, container, range.startOffset + ind);
            }
            rng = range.cloneRange();
            selection.removeAllRanges();
            selection.addRange(rng);
        }
      }
    }
dimid
  • 7,285
  • 1
  • 46
  • 85
0

I've modified Micheal's answer to work with multiple ranges and ranges that cross node boundaries. This seems to trim selections consistently in all my tests, but there's always room for edge cases I'm sure.

TypeScript, but easy enough to adapt to vanilla JS.

export function trimRanges(selection: Selection) {
    for (let i = 0, range = selection.getRangeAt(0); i < selection.rangeCount; range = selection.getRangeAt(i++)) {
        const text = selection.toString();
        const startOffset = text.length - text.trimStart().length;
        const endOffset = text.length - text.trimEnd().length;

        if (startOffset) {
            const offset = range.startOffset + startOffset;
            if (offset < 0) {
                // If the range will underflow the current element, then it belongs in the previous element
                const start = range.startContainer.parentElement.previousSibling;
                range.setStart(start, start.textContent.length + offset);
            } else if (offset > range.startContainer.textContent.length) {
                // If the range will overflow the current element, then it belongs in the next element
                const start = range.startContainer.parentElement.nextSibling;
                range.setStart(start, offset - range.startContainer.textContent.length);
            } else {
                range.setStart(range.startContainer, offset);
            }
        }
        if (endOffset) {
            const offset = range.endOffset - endOffset;
            if (offset < 0) {
                // If the range will underflow the current element, then it belongs in the previous element
                const end = range.endContainer.parentElement.previousSibling;
                range.setEnd(end, end.textContent.length + offset);
            } else if (offset > range.endContainer.textContent.length) {
                // If the range will overflow the current element, then it belongs in the next element
                const end = range.endContainer.parentElement.nextSibling;
                range.setEnd(end, offset - range.endContainer.textContent.length);
            } else {
                range.setEnd(range.endContainer, offset);
            }
        }
    }
}

Note that this does modify the ranges attached to the selection. If you need atomicity, you could hypothetically clone each range before editing it, remove all ranges, then add the cloned ranges back to the selection, but I haven't found any issues with the approach as it stands.

ndm13
  • 1,189
  • 13
  • 19