I have the following project which is based on highlighting the hashtags that are written within the contenteditable box.
The problem is that the code does not allow me to create line breaks.
I need to be able to create new lines and that the cursor position works well.
$(document).ready(function() {
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();
var currentNode = null;
var previousNode = null;
for (var i = 0; i < element.childNodes.length; i++) {
previousNode = currentNode;
currentNode = element.childNodes[i];
while (currentNode.childNodes.length > 0) {
currentNode = currentNode.childNodes[0];
}
if (previousNode != null) {
offset -= previousNode.length;
}
if (offset <= currentNode.length) {
break;
}
}
if (currentNode != null) {
range.setStart(currentNode, offset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
var textbox = $(".textbox");
$(document).on("input", textbox, function(event) {
var position = getCaretCharacterOffsetWithin(textbox.get(0));
var text = textbox.text();
text = text.replace(/(^|\s)(#[^\s]+)/ig, "$1<span style=\"color: blue;\">$2</span>");
textbox.html($.parseHTML(text));
setCaretPosition(textbox.get(0), position);
});
});
.textbox {
padding: 6px;
border: solid 1px #aaa;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div class="textbox" contenteditable="true"></div>
Related
I have a html tag which is
<span>This is first text<span class="ignore">Second</span> This is third text<span>
I am trying to get the start and end index from the selected text. When I select third I get start and end index as 34 39
But I expect 27 32
I tried the below approach
export const findTextRange = (element) => {
if (!element) return;
let start = 0, end = 0;
let sel, range, priorRange, text;
if (typeof window.getSelection != "undefined") {
sel = window.getSelection();
text = sel + '';
if (window.getSelection().rangeCount <= 0) {
return;
}
range = window.getSelection().getRangeAt(0);
priorRange = range.cloneRange();
priorRange.selectNodeContents(element);
priorRange.setEnd(range.startContainer, range.startOffset);
start = priorRange.toString().length;
end = start + (sel + '').length;
} else if (typeof document.selection !== "undefined" &&
(sel = document.selection).type !== "Control") {
text = sel + '';
range = sel.createRange();
priorRange = document.body.createTextRange();
priorRange.moveToElementText(element);
priorRange.setEndPoint("EndToStart", range);
start = priorRange.text.length;
end = start + (sel + '').length;
}
return { start, end, text };
}
Is there any way where I can ignore the span element with ignore class.
Store the initial HTML, then remove all elements having the .ignore class:
const html = element.innerHTML;
element.querySelectorAll('.ignore').forEach((e) => e.remove());
After getting the range, restore the original HTML:
element.innerHTML = html;
Snippet
const findTextRange = (element) => {
if (!element) return;
const html = element.innerHTML; // store original HTML
element.querySelectorAll('.ignore').forEach((e) => e.remove()); // remove ignore elements
let start = 0, end = 0;
let sel, range, priorRange, text;
if (typeof window.getSelection != "undefined") {
sel = window.getSelection();
text = sel + '';
if (window.getSelection().rangeCount <= 0) {
return;
}
range = window.getSelection().getRangeAt(0);
priorRange = range.cloneRange();
priorRange.selectNodeContents(element);
priorRange.setEnd(range.startContainer, range.startOffset);
start = priorRange.toString().length;
end = start + (sel + '').length;
} else if (typeof document.selection !== "undefined" &&
(sel = document.selection).type !== "Control") {
text = sel + '';
range = sel.createRange();
priorRange = document.body.createTextRange();
priorRange.moveToElementText(element);
priorRange.setEndPoint("EndToStart", range);
start = priorRange.text.length;
end = start + (sel + '').length;
}
element.innerHTML = html; // restore HTML
console.log(start, end, text);
return { start, end, text };
}
document.querySelector('#P').addEventListener('click', function() {findTextRange(this)});
<span id="P">This is first text<span class="ignore">Second</span> This is third text<span>
I'm currently making a WYSIWYG editor but having some problems. I'm trying to add links in but when I go to add a link it takes the focus away from the div as the user must type the link in a text box.
I've got a function that gets the last position of the cursor:
<div id="editor" contenteditable="true"></div>
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;
}
var update = function() {
getCaretCharacterOffsetWithin(this);
};
$('#editor').on("mousedown mouseup keydown keyup", update);
Is there a way to ExecCommand at the caret point?
EDIT: Added a JSFiddle to see how things work - https://jsfiddle.net/hju3bLyx/2/
you need to save the selection when editor lost focus
var savedSel;
function createLink() {
$('#editor').focus();
var url = document.getElementById("url").value;
restoreSelection(savedSel);
document.execCommand("CreateLink", false, url);
}
// it saved here
$('#editor').focusout(function(){
savedSel = saveSelection();
})
I'm trying to get the element that the caret is in at any given time within a contenteditable DIV.
So far, I am able to get the caret position within the text.
But I want to go a step further to actually get the element selected. For instance, if the caret is after the first letter in the word "wombat" in my example, the solution would also return the id of the selected element "wombatid" and not just the caret position. Any tips or solutions?
function getSelectionCharacterOffsetWithin(element) {
var start = 0;
var end = 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.startContainer, range.startOffset);
start = preCaretRange.toString().length;
preCaretRange.setEnd(range.endContainer, range.endOffset);
end = 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("EndToStart", textRange);
start = preCaretTextRange.text.length;
preCaretTextRange.setEndPoint("EndToEnd", textRange);
end = preCaretTextRange.text.length;
}
return { start: start, end: end };
}
function reportSelection() {
var selOffsets = getSelectionCharacterOffsetWithin( document.getElementById("editor") );
document.getElementById("selectionLog").innerHTML = "Selection offsets: " + selOffsets.start + ", " + selOffsets.end+" <br> Selected Element's id: 'element here'";
}
window.onload = function() {
document.addEventListener("selectionchange", reportSelection, false);
document.addEventListener("mouseup", reportSelection, false);
document.addEventListener("mousedown", reportSelection, false);
document.addEventListener("keyup", reportSelection, false);
};
#editor {
padding: 5px;
border: solid green 1px;
}
Select something in the content below:
<div id="editor" contenteditable="true">A <i id="wombatid">wombat</i> is a marsupial native to <b id="australiaid">Australia</b></div>
<div id="selectionLog"></div>
To get ID you can try something like:
document.querySelectorAll("#editor *").forEach(el => el.onclick = e => alert(e.target.id));
here e.target will be your HTML element, it should work for any HTML element
function getSelectionCharacterOffsetWithin(element) {
var start = 0;
var end = 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.startContainer, range.startOffset);
start = preCaretRange.toString().length;
preCaretRange.setEnd(range.endContainer, range.endOffset);
end = 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("EndToStart", textRange);
start = preCaretTextRange.text.length;
preCaretTextRange.setEndPoint("EndToEnd", textRange);
end = preCaretTextRange.text.length;
}
return { start: start, end: end };
}
function reportSelection() {
var selOffsets = getSelectionCharacterOffsetWithin( document.getElementById("editor") );
document.getElementById("selectionLog").innerHTML = "Selection offsets: " + selOffsets.start + ", " + selOffsets.end+" <br> Selected Element's id: 'element here'";
}
window.onload = function() {
document.addEventListener("selectionchange", reportSelection, false);
document.addEventListener("mouseup", reportSelection, false);
document.addEventListener("mousedown", reportSelection, false);
document.addEventListener("keyup", reportSelection, false);
document.querySelectorAll("#editor *").forEach(el => el.onclick = e => alert(e.target.id));
};
#editor {
padding: 5px;
border: solid green 1px;
}
Select something in the content below:
<div id="editor" contenteditable="true">A <i id="wombatid">wombat</i> is a marsupial native to <b id="australiaid">Australia</b></div>
<div id="selectionLog"></div>
When a user enters the URL of any image into the contenteditable div, the code below will automatically convert it into an image (through the img tag on the contenteditable div).
The problem with this is that it converts any URL that the user enters into an image tag.
http://example.com would be converted to `http://example.com
Is there a way to convert ordinary links into links (through the <a> tag) and image URL's inside image tags?
<script>
var saveSelection, restoreSelection;
if (window.getSelection && document.createRange) {
saveSelection = function(containerEl) {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(containerEl);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
};
restoreSelection = function(containerEl, savedSel) {
var charIndex = 0, range = document.createRange();
range.setStart(containerEl, 0);
range.collapse(true);
var nodeStack = [containerEl], node, foundStart = false, stop = false;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType == 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
} else if (document.selection) {
saveSelection = function(containerEl) {
var selectedTextRange = document.selection.createRange();
var preSelectionTextRange = document.body.createTextRange();
preSelectionTextRange.moveToElementText(containerEl);
preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
var start = preSelectionTextRange.text.length;
return {
start: start,
end: start + selectedTextRange.text.length
}
};
restoreSelection = function(containerEl, savedSel) {
var textRange = document.body.createTextRange();
textRange.moveToElementText(containerEl);
textRange.collapse(true);
textRange.moveEnd("character", savedSel.end);
textRange.moveStart("character", savedSel.start);
textRange.select();
};
}
function createLink(matchedTextNode) {
var el = document.createElement("img");
el.src = matchedTextNode.data;
el.appendChild(matchedTextNode);
return el;
}
function shouldLinkifyContents(el) {
return el.tagName != "A";
}
function surroundInElement(el, regex, surrounderCreateFunc, shouldSurroundFunc) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1 && shouldSurroundFunc(el)) {
surroundInElement(child, regex, createLink, shouldSurroundFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
var textbox = document.getElementById("text");
var urlRegex = /http(s?):\/\/($|[^ ]+)/;
function updateLinks() {
surroundInElement(textbox, urlRegex, createLink, shouldLinkifyContents);
}
var $textbox = $(textbox);
$(document).ready(function () {
$textbox.focus();
var keyTimer = null, keyDelay = 1000;
$textbox.keyup(function() {
if (keyTimer) {
window.clearTimeout(keyTimer);
}
keyTimer = window.setTimeout(function() {
updateLinks();
keyTimer = null;
}, keyDelay);
});
});
</script>
I would like to create a function that select a given text inside a HTML element.
For example calling selectText('world') would select world in a markup like <span>Hello </span><strong>world</strong>!
Lots of answers on similar questions suggests to use range and selection but none of them work in my case (some would select all the text, some won't work with such markup, ...).
For now this is what I have (it doesn't work):
function selectText ( element, textToSelect ) {
var text = element.textContent,
start = text.indexOf( textToSelect ),
end = start + textToSelect.length - 1,
selection, range;
element.focus();
if( window.getSelection && document.createRange ) {
range = document.createRange();
range.setStart( element.firstChild, start );
range.setEnd( element.lastChild, end );
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange( range );
} else if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText( element );
range.moveStart( 'character', start );
range.collapse( true );
range.moveEnd( 'character', end );
range.select();
}
}
Here is a jsfiddle so you see what is actually happening: http://jsfiddle.net/H2H2p/
Outputed error :
Uncaught IndexSizeError: Failed to execute 'setStart' on 'Range': The offset 11 is larger than or equal to the node's length (5).
P.S.: no jQuery please :)
You could use a combination of your approach of finding the text within the element's textContent and this function.
Demo: http://jsfiddle.net/H2H2p/3/
Code:
function selectText(element, textToSelect) {
var elementText;
if (typeof element.textContent == "string" && document.createRange && window.getSelection) {
elementText = element.textContent;
} else if (document.selection && document.body.createTextRange) {
var textRange = document.body.createTextRange();
textRange.moveToElement(element);
elementText = textRange.text;
}
var startIndex = elementText.indexOf(textToSelect);
setSelectionRange(element, startIndex, startIndex + textToSelect.length);
}
function getTextNodesIn(node) {
var textNodes = [];
if (node.nodeType == 3) {
textNodes.push(node);
} else {
var children = node.childNodes;
for (var i = 0, len = children.length; i < len; ++i) {
textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
}
}
return textNodes;
}
function setSelectionRange(el, start, end) {
if (document.createRange && window.getSelection) {
var range = document.createRange();
range.selectNodeContents(el);
var textNodes = getTextNodesIn(el);
var foundStart = false;
var charCount = 0, endCharCount;
for (var i = 0, textNode; textNode = textNodes[i++]; ) {
endCharCount = charCount + textNode.length;
if (!foundStart && start >= charCount
&& (start < endCharCount ||
(start == endCharCount && i < textNodes.length))) {
range.setStart(textNode, start - charCount);
foundStart = true;
}
if (foundStart && end <= endCharCount) {
range.setEnd(textNode, end - charCount);
break;
}
charCount = endCharCount;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (document.selection && document.body.createTextRange) {
var textRange = document.body.createTextRange();
textRange.moveToElementText(el);
textRange.collapse(true);
textRange.moveEnd("character", end);
textRange.moveStart("character", start);
textRange.select();
}
}