Javascript selected text highlighting prob - javascript

I have a html page with text content. On selecting any text and pressing the highlight button, I can change the style of the selected text to highlight the same. To implement this feature, i have written the following method.
sel = window.getSelection();
var range = sel.getRangeAt(0);
var span = document.createElement('span');
span.className = "highlight" + color;
range.surroundContents(span);
This is working fine if you choose a text with no html tag, but when the text has any html tag in between, it is giving error
Failed to execute 'surroundContents' on 'Range': The Range has partially selected a non-Text node.
How to solve this problem. Is it possible to highlight the same separately for each part(divided by html tags)?

See Range.extractContents:
document.getElementById('execute').addEventListener('click', function() {
var range = window.getSelection().getRangeAt(0),
span = document.createElement('span');
span.className = 'highlight';
span.appendChild(range.extractContents());
range.insertNode(span);
});
.highlight { background-color: yellow; }
<div id="test">
Select any part of <b>this text and</b> then click 'Run'.
</div>
<button id="execute">Run</button>

Rather than reinvent the wheel, I'd use Rangy's highlighting capabilities.
I've forked the fiddle that RGraham created and created a new fiddle that shows how it works. This is how it is done:
var applier = rangy.createClassApplier("highlight");
var highlighter = rangy.createHighlighter();
highlighter.addClassApplier(applier);
document.getElementById('execute').addEventListener('click', function() {
highlighter.removeAllHighlights();
highlighter.highlightSelection("highlight");
});
What this does is create a highlighter that will set the highlight class on elements that are wholly inside the selection, and create spans with the highlight class as needed for elements that straddle the selection. When the button with the id execute is clicked, the old highlights are removed and the new highlights applied.
The highlighter functionality is part of release of Rangy that are considered to be "alpha". However, I've been consistently using alpha releases of Rangy for a few years now but it has been extremely rare that I found a problem with my application that I could trace back to Rangy. And the few times I found a problem with Rangy, Tim Down (its author) was quite responsive.

My solution highlighting all selected nodes.
function highlight() {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
const {
commonAncestorContainer,
startContainer,
endContainer,
startOffset,
endOffset,
} = range;
const nodes = [];
console.group("range");
console.log("range", range);
console.log("commonAncestorContainer", commonAncestorContainer);
console.log("startContainer", startContainer);
console.log("endContainer", endContainer);
console.log("startOffset", startOffset);
console.log("endOffset", endOffset);
console.log("startContainer.parentNode", startContainer.parentNode);
console.groupEnd();
if (startContainer === endContainer) {
const span = document.createElement("span");
span.className = "highlight";
range.surroundContents(span);
return;
}
// get all posibles selected nodes
function getNodes(childList) {
console.group("***** getNode: ", childList);
childList.forEach((node) => {
console.log("node:", node, "nodoType", node.nodeType);
const nodeSel = sel.containsNode(node, true);
console.log("nodeSel", nodeSel);
// if is not selected
if (!nodeSel) return;
const tempStr = node.nodeValue;
console.log("nodeValue:", tempStr);
if (node.nodeType === 3 && tempStr.replace(/^\s+|\s+$/gm, "") !== "") {
console.log("nodo agregado");
nodes.push(node);
}
if (node.nodeType === 1) {
if (node.childNodes) getNodes(node.childNodes);
}
});
console.groupEnd();
}
getNodes(commonAncestorContainer.childNodes);
console.log(nodes);
nodes.forEach((node, index, listObj) => {
const { nodeValue } = node;
let text, prevText, nextText;
if (index === 0) {
prevText = nodeValue.substring(0, startOffset);
text = nodeValue.substring(startOffset);
} else if (index === listObj.length - 1) {
text = nodeValue.substring(0, endOffset);
nextText = nodeValue.substring(endOffset);
} else {
text = nodeValue;
}
const span = document.createElement("span");
span.className = "highlight";
span.append(document.createTextNode(text));
const { parentNode } = node;
parentNode.replaceChild(span, node);
if (prevText) {
const prevDOM = document.createTextNode(prevText);
parentNode.insertBefore(prevDOM, span);
}
if (nextText) {
const nextDOM = document.createTextNode(nextText);
parentNode.insertBefore(nextDOM, span.nextSibling);
}
});
sel.removeRange(range);
}
Example https://codesandbox.io/s/api-selection-multiple-with-nodes-gx2is?file=/index.html

try this:
newNode.appendChild(range.extractContents())
according to MDN:
Partially selected nodes are cloned to include the parent tags
necessary to make the document fragment valid.
Whereas Range.surroundContents:
An exception will be thrown, however, if the Range splits a non-Text
node with only one of its boundary points. That is, unlike the
alternative above, if there are partially selected nodes, they will
not be cloned and instead the operation will fail.
Didn't test, but...

This solution is bit tricky, but I find it would be sufficient
When you will see closely in selection object that we get through calling
window.getSelection().getRangeAt(0)
You will se that there are 4 properties: startContainer, startOffset, endContainer, endOffset.
So now you need to start with startContainer with startOffset and start putting your necessary span nodes from there.
If now it endContainer is different node then you need to start traversing nodes from startContainer to endContainer
For traversing you need to check for child nodes and sibling nodes which you can get from DOM objects. So first go through startContainer, go through all its child and check if child node is inline element then apply span tag around it, and then you need to write few coding for various corner cases.

The solution is really tricky.
I somehow find a workaround. See my fiddle
function highlight() {
var range = window.getSelection().getRangeAt(0),
parent = range.commonAncestorContainer,
start = range.startContainer,
end = range.endContainer;
var startDOM = (start.parentElement == parent) ? start.nextSibling : start.parentElement;
var currentDOM = startDOM.nextElementSibling;
var endDOM = (end.parentElement == parent) ? end : end.parentElement;
//Process Start Element
highlightText(startDOM, 'START', range.startOffset);
while (currentDOM != endDOM && currentDOM != null) {
highlightText(currentDOM);
currentDOM = currentDOM.nextElementSibling;
}
//Process End Element
highlightText(endDOM, 'END', range.endOffset);
}
function highlightText(elem, offsetType, idx) {
if (elem.nodeType == 3) {
var span = document.createElement('span');
span.setAttribute('class', 'highlight');
var origText = elem.textContent, text, prevText, nextText;
if (offsetType == 'START') {
text = origText.substring(idx);
prevText = origText.substring(0, idx);
} else if (offsetType == 'END') {
text = origText.substring(0, idx);
nextText = origText.substring(idx);
} else {
text = origText;
}
span.textContent = text;
var parent = elem.parentElement;
parent.replaceChild(span, elem);
if (prevText) {
var prevDOM = document.createTextNode(prevText);
parent.insertBefore(prevDOM, span);
}
if (nextText) {
var nextDOM = document.createTextNode(nextText);
parent.appendChild(nextDOM);
}
return;
}
var childCount = elem.childNodes.length;
for (var i = 0; i < childCount; i++) {
if (offsetType == 'START' && i == 0)
highlightText(elem.childNodes[i], 'START', idx);
else if (offsetType == 'END' && i == childCount - 1)
highlightText(elem.childNodes[i], 'END', idx);
else
highlightText(elem.childNodes[i]);
}
}

if (window.getSelection) {
var sel = window.getSelection();
if (!sel) {
return;
}
var range = sel.getRangeAt(0);
var start = range.startContainer;
var end = range.endContainer;
var commonAncestor = range.commonAncestorContainer;
var nodes = [];
var node;
for (node = start.parentNode; node; node = node.parentNode){
var tempStr=node.nodeValue;
if(node.nodeValue!=null && tempStr.replace(/^\s+|\s+$/gm,'')!='')
nodes.push(node);
if (node == commonAncestor)
break;
}
nodes.reverse();
for (node = start; node; node = getNextNode(node)){
var tempStr=node.nodeValue;
if(node.nodeValue!=null && tempStr.replace(/^\s+|\s+$/gm,'')!='')
nodes.push(node);
if (node == end)
break;
}
for(var i=0 ; i<nodes.length ; i++){
var sp1 = document.createElement("span");
sp1.setAttribute("class", "highlight"+color );
var sp1_content = document.createTextNode(nodes[i].nodeValue);
sp1.appendChild(sp1_content);
var parentNode = nodes[i].parentNode;
parentNode.replaceChild(sp1, nodes[i]);
}
}

Related

javascript doing multiple createrange() while encountering unexpected anchorOffset

My goal:
Let users highlight different substring in a single long string.
However, once I've highlighted one substring with range.surroundContents(newNode) (newNode is a span with yellow background), the innerHTML of the whole long string changed-- it started to contain the span element; consequently, if the user wants to highlight a substring after the previous highlighted substring in the same long string, the anchorOffset will return the index starting after the previous span.
For example, in this long string:
"Mr. and Mrs. Dursley, of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much."
this long sentence is wrapped by a p whose class name is noting. If the range.surroundContents() method the substring "Privet Drive", then, when I want to get the window.getSelection().anchorOffset of the substring "thank", the answer wrongly is 53 while the correct answer should be 102.
How should I do? Thank you!!
P.S. I don't want to use substring method to find the position, thank you!
$(".noting").mouseup(function(e){
$("#noteContent").val("");/*flushing*/
curSentNum = $(this).attr("id").split("-")[1];
$('#curSentNum').val(curSentNum);
highlightLangName = $(this).attr("id").split("-")[2];
$('#highlightLangName').val(highlightLangName);
//console.log(".noting $(this).html()"+$(this).html()+" "+$(this).attr("id"));//id, for example: p-2-French
if (window.getSelection) {
highlightedText = window.getSelection().toString();
curAnchorOffset = window.getSelection().anchorOffset;
$('#anchorAt').val(curAnchorOffset);
$('#highlightLen').val(highlightedText.length);
}
else if (document.selection && document.selection.type != "Control") {
highlightedText = document.selection.createRange().text;
}
});
And then I'll save the anchorAt information to db; after the db operation, I'll immediately call this function using the previous variables remained:
function highlightNoteJustSaved(){
var curI = noteCounter;
var anchorAt = parseInt($("#anchorAt").val());
var highlightLen = parseInt($("#highlightLen").val());
/*p to find, for example: p-2-French*/
var curP = document.getElementById('p-'+curSentNum.toString()+"-"+$("#highlightLangName").val());
var range = document.createRange();
root_node = curP;
range.setStart(root_node.childNodes[0], anchorAt);
range.setEnd(root_node.childNodes[0], anchorAt+highlightLen);
var newNode = document.createElement("span");
newNode.style.cssText="background-color:#ceff99";//yellow
newNode.className = alreadyNoteStr;
newNode.setAttribute('id','already-note-'+curI.toString());
range.surroundContents(newNode);
}
for HTML tree node structure, please take a look at the comment below( I didn't figure out how to copy-paste the code at this asking area).
I replaced your method to highlight text with 2 methods. highlightTextNodes finds the word in the content of the node. Searching each child. Also I implemented a highlight remover to show how it works. I replaced the span with a mark tag.
let alreadyNoteStr = 'already';
let noteCounter = 0;
let elementId;
$('p.noting').mouseup(function(e) {
elementId = $(this).attr('id');
$('#noteContent').val(''); /*flushing*/
curSentNum = elementId.split('-')[1];
$('#curSentNum').val(curSentNum);
highlightLangName = elementId.split('-')[2];
$('#highlightLangName').val(highlightLangName);
//console.log(".noting $(this).html()"+$(this).html()+" "+$(this).attr("id"));//id, for example: p-2-French
if (window.getSelection) {
highlightedText = window.getSelection().toString();
curAnchorOffset = window.getSelection().anchorOffset;
$("#noteContent").val(highlightedText);
$('#anchorAt').val(curAnchorOffset);
$('#highlightLen').val(highlightedText.length);
highlight(elementId, highlightedText);
} else if (document.selection && document.selection.type != "Control") {
highlightedText = document.selection.createRange().text;
}
});
function highlightNoteJustSaved() {
let curI = noteCounter;
let anchorAt = parseInt($("#anchorAt").val());
let highlightLen = parseInt($("#highlightLen").val());
/*p to find, for example: p-2-French*/
let curP = document.getElementById('p-' + curSentNum.toString() + "-" + $("#highlightLangName").val());
let range = document.createRange();
rootNode = curP;
let childNode = rootNode.childNodes[0];
range.setStart(rootNode.childNodes[0], anchorAt);
range.setEnd(rootNode.childNodes[0], anchorAt + highlightLen);
var newNode = document.createElement("span");
newNode.style.cssText = "background-color:#ceff99"; //yellow
newNode.className = alreadyNoteStr;
newNode.setAttribute('id', 'already-note-' + curI.toString());
range.surroundContents(newNode);
}
/*
* Takes in an array of consecutive TextNodes and returns a document fragment with `word` highlighted
*/
function highlightTextNodes(nodes, word) {
if (!nodes.length) {
return;
}
let text = '';
// Concatenate the consecutive nodes to get the actual text
for (var i = 0; i < nodes.length; i++) {
text += nodes[i].textContent;
}
let fragment = document.createDocumentFragment();
while (true) {
// Tweak this if you want to change the highlighting behavior
var index = text.toLowerCase().indexOf(word.toLowerCase());
if (index === -1) {
break;
}
// Split the text into [before, match, after]
var before = text.slice(0, index);
var match = text.slice(index, index + word.length);
text = text.slice(index + word.length);
// Create the <mark>
let mark = document.createElement('mark');
mark.className = 'found';
mark.appendChild(document.createTextNode(match));
// Append it to the fragment
fragment.appendChild(document.createTextNode(before));
fragment.appendChild(mark);
}
// If we have leftover text, just append it to the end
if (text.length) {
fragment.appendChild(document.createTextNode(text));
}
// Replace the nodes with the fragment
nodes[0].parentNode.insertBefore(fragment, nodes[0]);
for (var i = 0; i < nodes.length; i++) {
let node = nodes[nodes.length - i - 1];
node.parentNode.removeChild(node);
}
}
/*
* Highlights all instances of `word` in `$node` and its children
*/
function highlight(id, word) {
let node = document.getElementById(id);
let children = node.childNodes;
let currentRun = [];
for (var i = 0; i < children.length; i++) {
let child = children[i];
if (child.nodeType === Node.TEXT_NODE) {
// Keep track of consecutive text nodes
currentRun.push(child);
} else {
// If we hit a regular element, highlight what we have and start over
highlightTextNodes(currentRun, word);
currentRun = [];
// Ignore text inside of our <mark>s
if (child.nodeType === Node.ELEMENT_NODE && child.className !== 'found') {
highlight(child, word);
}
}
}
// Just in case we have only text nodes as children
if (currentRun.length) {
highlightTextNodes(currentRun, word);
}
}
/*
* Removes all highlighted <mark>s from the given node
*/
function unhighlight(id) {
let node = document.getElementById(id);
let marks = [].slice.call(node.querySelectorAll('mark.found'));
for (var i = 0; i < marks.length; i++) {
let mark = marks[i];
// Replace each <mark> with just a text node of its contents
mark.parentNode.replaceChild(document.createTextNode(mark.childNodes[0].textContent), mark);
}
}
label {
display: block;
position: relative;
padding-left: 100px;
}
button {
margin-top: 20px;
margin-bottom: 20px;
padding: 10px;
}
label>span {
position: absolute;
left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button type="button" onclick="unhighlight(elementId);">Unhighlight</button>
<div id="div-0" class="only-left-border">
<p class="lan-English noting" id="p-1-English">Mr. and Mrs. Dursley, of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much.</p>
</div>
<label><span>Content:</span><input type="text" id="noteContent"></input></label>
<label><span>Numer:</span><input type="text" id="curSentNum"></input></label>
<label><span>Language:</span><input type="text" id="highlightLangName"></input></label>
<label><span>Anchor:</span><input type="text" id="anchorAt"></input></label>
<label><span>Length:</span><input type="text" id="highlightLen"></input></label>

Prevent editable div's behavior when typing certain characters

I have the following code taken from Pranav C Balan's answer to my previous question:
var div = document.getElementById('div');
div.addEventListener('input', function() {
var pos = getCaretCharacterOffsetWithin(this);
// get all red subtring and wrap it with span
this.innerHTML = this.innerText.replace(/red/g, '<span style="color:red">$&</span>')
setCaretPosition(this, pos);
})
// following code is copied from following question
// https://stackoverflow.com/questions/26139475/restore-cursor-position-after-changing-contenteditable
function getCaretCharacterOffsetWithin(element) {
var caretOffset = 0;
var doc = element.ownerDocument || element.document;
var win = doc.defaultView || doc.parentWindow;
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;
}
return caretOffset;
}
function setCaretPosition(element, offset) {
var range = document.createRange();
var sel = window.getSelection();
//select appropriate node
var currentNode = null;
var previousNode = null;
for (var i = 0; i < element.childNodes.length; i++) {
//save previous node
previousNode = currentNode;
//get current node
currentNode = element.childNodes[i];
//if we get span or something else then we should get child node
while (currentNode.childNodes.length > 0) {
currentNode = currentNode.childNodes[0];
}
//calc offset in current node
if (previousNode != null) {
offset -= previousNode.length;
}
//check whether current node has enough length
if (offset <= currentNode.length) {
break;
}
}
//move caret to specified offset
if (currentNode != null) {
range.setStart(currentNode, offset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
<span contenteditable="true" id="div" style="width:100%;display:block">sss</span>
It has a editable <div> where the user can input and it automatically colors the word red as red just like some code editors color key words like HTML tags, strings, functions, etc.Type "red" and you will understand what I mean.
The issue I'm having is, when I type "<", it deletes all the characters in front of it unless it finds a ">" where it will stop. Another error happens if you type "&#1" (or any other number instead of 1 really).
Any ideia on how to prevent this behavior?
You're running into this problem because you're expecting the user to be able to input HTML-like entities such as <xyz... or { but don't want to parse that input as HTML, but at the same time, you're yourself putting html elements in the same div and you want that to be parsed as HTML. So there are two ways you can go about this:
Keep the input and presentation separate. So user can input anything, which you'll sanitize and display in an output box.
Or... change the addEventListener function:
div.addEventListener('input', function() {
var pos = getCaretCharacterOffsetWithin(this);
var userString = sanitizeHTML(this.innerText);
// get all red subtring and wrap it with span
this.innerHTML = userString.replace(/red/g, '<span style="color:red">$&</span>')
setCaretPosition(this, pos);
})
This would work in most scenarios, but it'd break (badly) if you're expecting user to input HTML too, for example <span class="red" style="color: red">red</span> would become horribly mutilated. Other than that, you're good to go. Get sanitizeHTML from here: https://github.com/punkave/sanitize-html

Selected content to upperCase?

I am just wondering if there is a simple solution already to the problem of turning selected content in tinymce to upperCase letters.
Anyone got a solution?
PS: The upperCase-function is known, but won't solve the tinymce setting of selected content alone.
This is what i came up with after some fiddling
// check if a node intersects the given range
rangeIntersectsNode: function (range, node) {
var nodeRange;
if (range.intersectsNode) {
return range.intersectsNode(node);
}
else {
nodeRange = node.ownerDocument.createRange();
try {
nodeRange.selectNode(node);
} catch (e) {
nodeRange.selectNodeContents(node);
}
return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
}
},
// Tinymce-Shortcut: (cmd/ctrl + shift +a)
if ( ( (mac && evt.metaKey)|| (!mac && evt.ctrlKey)) && evt.shiftKey && evt.keyCode == 65 ){
if (!ed.selection.isCollapsed()) {
var selection = ed.getWin().getSelection(); // user selection
var range = selection.getRangeAt(0); // erste range
var start = range.startContainer;
var start_offset = range.startOffset;
var end = range.endContainer;
var end_offset = range.endOffset;
// Get all textnodes of the common ancestor
// Check foreach of those textnodes if they are inside the selection
// StartContainer and EndContainer may be partially inside the selection (if textnodes)
// concatenate those textnode parts and make toUppercase the selected part only
// all textnodes inbetween need to become upperCased (the nodeContents)
// Selection needs to be reset afterwards.
var textnodes = t.getTextNodes(range.commonAncestorContainer);
for (var i=0; i<textnodes.length; i++) {
if (t.rangeIntersectsNode(range, textnodes[i])){
if (textnodes[i] == start && textnodes[i] == end) {
var text_content = start.textContent;
text_content = start.textContent.substring(0, start_offset) + text_content.substring(start_offset, end_offset).toUpperCase() + end.textContent.substring(end_offset);
textnodes[i].nodeValue = text_content;
}
else if (textnodes[i] == start){
var text_content = start.textContent.substring(0, start_offset) + start.textContent.substring(start_offset).toUpperCase();
textnodes[i].nodeValue = text_content;
}
else if (textnodes[i] == end){
var text_content = end.textContent.substring(0, end_offset).toUpperCase() + end.textContent.substring(end_offset);
textnodes[i].nodeValue = text_content;
}
else {
// Textnodes between Start- and Endcontainer
textnodes[i].nodeValue = textnodes[i].nodeValue.toUpperCase();
}
}
}
// reset selection
var r = ed.selection.dom.createRng();
r.setStart(start, start_offset);
r.setEnd(end, end_offset);
ed.selection.setRng(r);
evt.preventDefault();
return false;
}
}

contentEditable div no focus after text replace

I have a contentEditable div which in some cases replaces text with images. It works but when it replaces specific text with an image the div lose focus, and I can't write anything in, how to make it to work?
obj.onfocus = function() {Replace(this)};
obj.onblur = function() {Replace(this)};
obj.onkeyup = function() {Replace(this)};
obj.onkeydown = function() {Replace(this)};
obj.onclick = function() {Replace(this)};
function Replace(element)
{
element.innerHTML = element.innerHTML.replace(/some_text/gi, '<img src="image.." />')
}
contentEditable areas seem to be buggy when replacing the innerHTML with actual HTML elements directly. Instead, you'll have to do low-level node replacement with a created element:
function Replace(element) {
var someText = 'some_text',
index, child, img;
// traverse nodes
for (var i = 0, il = element.childNodes.length; i < il; i++) {
child = element.childNodes[i];
// if an element node
if (child.nodeType == 1) {
// recurse
Replace(child);
// if a text node
} else if (child.nodeType == 3) {
index = child.data.indexOf(someText);
if (index >= 0) {
// create img element
var img = document.createElement('img'),
sel, rng;
img.src = "src.gif";
// split target text into its own node and replace with img
child.splitText(index);
child.nextSibling.splitText(someText.length);
child.parentNode.replaceChild(img, child.nextSibling);
// BONUS CODE!
// move cursor to the end of the editable area
if (document.createRange) {
rng = document.createRange();
rng.selectNodeContents(element);
rng.collapse(false);
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(rng);
} else if (document.selection) {
rng = document.body.createTextRange();
rng.moveToElementText(element);
rng.collapse(false);
rng.select();
}
}
}
}
}
See demo

How to alternate moveStart in Firefox?

Does anybody know how to use range.setStart in the same way as range.moveStart works in IE? I'd like to implement backspace/delete in JS, something like this:
range.moveStart('character',-1);
range.deleteContents();
but in Firefox
Firefox, along with all modern browsers except IE <= 8 uses DOM Ranges. There's no direct analogue to the moveStart method of IE's TextRange and it's tricky to do in the general case. If the range is within a text node and not at the start, it's easy; otherwise you'll need to walk backwards in the document to find the preceding text node and move the range into it. The following only works within a single text node:
function backspace() {
var sel = window.getSelection();
// If there is a selection rather than a caret, just delete the selection
if (!sel.isCollapsed) {
sel.deleteFromDocument();
} else if (sel.rangeCount) {
var range = sel.getRangeAt(0);
if (range.startContainer.nodeType == 3 && range.startOffset > 0) {
range.setStart(range.startContainer, range.startOffset - 1);
sel.removeAllRanges();
sel.addRange(range);
sel.deleteFromDocument();
}
}
}
WebKit and Firefox 4 have the modify method of Selection objects which solves the problem completely:
function backspace2() {
var sel = window.getSelection();
// If there is a selection rather than a caret, just delete the selection
if (!sel.isCollapsed) {
sel.deleteFromDocument();
} else if (sel.rangeCount && sel.modify) {
sel.modify("extend", "backward", "character");
sel.deleteFromDocument();
}
}
Here’s a function to expand selection to cover full words:
document.body.addEventListener('keydown', ({key}) => {
if (key === 'Enter') {
getWordRange();
}
});
function getWordRange() {
const range = document.getSelection().getRangeAt(0);
const {startContainer, startOffset, endContainer, endOffset} = range;
const treeWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
);
treeWalker.currentNode = startContainer;
do {
const container = treeWalker.currentNode;
const content = container === startContainer
? container.textContent.substr(0, startOffset)
: container.textContent;
const offset = content.lastIndexOf(' ') + 1;
range.setStart(container, 0);
if (offset) {
range.setStart(container, offset);
break;
}
} while (treeWalker.previousNode());
treeWalker.currentNode = endContainer;
do {
const container = treeWalker.currentNode;
const content = container === endContainer
? container.textContent.substr(endOffset)
: container.textContent;
const offset = content.indexOf(' ');
const actualOffset = offset + container.textContent.length - content.length;
range.setEnd(container, content.length);
if (offset !== -1) {
range.setEnd(container, actualOffset);
break;
}
} while (treeWalker.nextNode());
}
<p>
Select text then hit Enter to expand selection to word edges.<br>
Works with <b>nested <i>tags</i></b> as well.
</p>

Categories