I want to have a button that adds a highlight to the currently selected text. I plan on doing this with the caret position of the selection. I have the following code:
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 showCaretPos() {
var el = document.getElementById("test");
var caretPosEl = document.getElementById("caretPos");
caretPosEl.innerHTML = "Caret position: " + getCaretCharacterOffsetWithin(el);
}
document.body.onkeyup = showCaretPos;
document.body.onmouseup = showCaretPos;
Non-editable text. Editable is below:
<div id="test" contenteditable="true">Hello, some <b>bold</b> and <i>italic and <b>bold</b></i> text</div>
<div id="caretPos"></div>
The above code finds the caret position. How can I now use the caret position to highlight only that position of text.
Possible Idea
Get caret position from above and now substr() to add highlight. str.substr(above caret position).
What is the best way to accomplish this?
EDIT
I don't want to change the color selection. I only want to use the caret position to then add a div that acts as a highlighter. For example:
Hi, my name is Bob.
Now, I use the caret position to track the position for only my text selection. Now, I replace the text with the original text but also with a div for the text selection.
Let's say I want highlight the phrase name is. I'm using this method of using caret position to not highlight every instance of this phrase. Only the exact position of where the phrase is. For example, let's my text has the phrase name is twice, it will only highlight the phrase that matches the caret position. Then, replace the text with something like: <div id='highlight'> name is </div>
In the following Snippet are 2 functions that do the same thing except that the first one uses a <span> and the second one uses a <div>. What the functions do is essentially...
...removes the selected content (text)...
...creates a <span> or <div>...
...sets the backgroundColor to yellow...
...the content is then appended to the <span/div>...
...then finally the <span/div> is inserted into the range.
The <span> as you can see is probably a better choice. Using a <div> is just plain messy.
SNIPPET
var spanEdit = document.getElementById('spanEdit');
var divEdit = document.getElementById('divEdit');
function highlightSpan() {
var range = document.getSelection().getRangeAt(0);
var contents = range.extractContents();
var node = document.createElement('span');
node.style.backgroundColor = "yellow";
node.appendChild(contents);
range.insertNode(node);
}
function highlightDiv() {
var range = document.getSelection().getRangeAt(0);
var contents = range.extractContents();
var node = document.createElement('div');
node.style.backgroundColor = "yellow";
node.appendChild(contents);
range.insertNode(node);
}
spanEdit.onkeyup = highlightSpan;
spanEdit.onmouseup = highlightSpan;
divEdit.onkeyup = highlightDiv;
divEdit.onmouseup = highlightDiv;
<div id='spanEdit' contenteditable="true">Highlight inserted span. This uses extractContents() method.</div>
<div> </div>
<div id='divEdit' contenteditable="true">Highlight inserted div. This uses extractContents() method.</div>
The functions are an adaptation from this post
Related
I am in react, and have content editable div. Now there is a button, on click button "hey" content will be pasted/render in the cursor position.
Now I am having the issue with caret position as if I want to render the content between two text then it will shift to the last.
If type content and then click button then it works perfect as it is append on last. Problem arises between the content.
UPDATED QUESTION WITH SOME SUCCESS
class Work extends React.Component{
constructor(props, context) {
super(props, context);
this.state = {
cursorEnd: 0
}
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;
}
showCaretPos = () => {
var el = document.getElementById("work");
console.log('Position: ', this.getCaretCharacterOffsetWithin(el))
this.setState({ cursorEnd: this.getCaretCharacterOffsetWithin(el)})
}
positionCursor = () => {
var tag = document.getElementById("work");
// Creates range object
var setpos = document.createRange();
// Creates object for selection
var set = window.getSelection();
setpos.setStart(tag.childNodes[0], this.state.cursorEnd);
setpos.collapse(true);
// Remove all ranges set
set.removeAllRanges();
// Add range with respect to range object.
set.addRange(setpos);
// Set cursor on focus
tag.focus();
}
renderValue = () => {
let content = "Hey";
this.positionCursor()
let selection = window.getSelection();
let range = selection.getRangeAt(0);
range.deleteContents();
let node = document.createTextNode(content);
range.insertNode(node);
for (let position = 0; position != content.length; position++) {
selection.modify("move", "right", "character");
};
}
render(){
<button onClick={() => this.renderValue()}>Paste</button>
<div id="work" onClick={(e)=>e.stopPropagation()}
contentEditable
onInput={this.emitChange}
onKeyUp={this.showCaretPos}
onMouseUp={this.showCaretPos}
dangerouslySetInnerHTML={{__html: this.props.html}}
>
</div>
}
}
Can anyone help me on this. Any help or suggestion is really appreciated.
After this I will be able to render the content from the desired caret position. Only One issue is that it will work from right to left.
Suppose you write content "Hello world"
Now if you first click on w_orld (click after w) and click the button then string will render
Hello wHeyorld
On inspect element I checked it now converted from one continuous string to 2 parts. I am attaching the image so that picture will be clear. Hope might be able to understand the issue because after this string break my value render on caret position stop.
Any help or suggestion is highly appreciated.
Issue is after adding the emoji if write some more content and then add emoji then return error
How can I keep the position of caret in contenteditable element, when I try to update the innerHTML of the editable element the caret position jumps to the first of the line, I need a function that can keep the position of the caret when editable element innerHTML is updated or changed thanks.
My code:
// content editable element
let editor = document.getElementById("editor");
editor.onkeypress = (e) => {
if (e.keyCode === 32) {
let lines = editor.children;
for (let line of lines) {
line.innerHTML += "Hello World !!!";
}
}
}
My answer :
// content editable element
let editor = document.getElementById("editor");
// on keypress
editor.onkeypress = () => {
// get the position of caret
let pos = getCaretPos(editor.children[0]);
// update the innerHTML
editor.children[0].innerHTML += "Hello World !!!";
// set the caret position
setCaretPos(editor.children[0], pos);
}
function getCaretPos(element) {
var ie = (typeof document.selection != "undefined" && document.selection.type != "Control") && true;
var w3 = (typeof window.getSelection != "undefined") && true;
var caretOffset = 0;
if (w3) {
var range = window.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
} else if (ie) {
var textRange = document.selection.createRange();
var preCaretTextRange = document.body.createTextRange();
preCaretTextRange.moveToElementText(element);
preCaretTextRange.setEndPoint("EndToEnd", textRange);
caretOffset = preCaretTextRange.text.length;
}
return caretOffset;
}
function setCaretPos(element, position) {
var node = element;
node.focus();
var textNode = node.firstChild;
var caret = position; // insert caret after the 10th character say
var range = document.createRange();
range.setStart(textNode, caret);
range.setEnd(textNode, caret);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
The simplest answer would be to not use innerHTML. If you're using contenteditable, the whole point of it is that you have a consistent active DOM document to work on. If you insert a string, the browser has no way of knowing where the cursor should be afterward because you are destroying all of the information it has. It is pretty much like you did line.innerHTML = ""; line.innerHTML = "Hello World !!!";, so the cursor gets pushed all the way to the beginning/end.
Given that, you can do
if (e.keyCode === 32) {
// 1. Get the children as a normal JS array, so that adding
// children in the next bit of code doesn't break for for-of.
let lines = Array.from(editor.children);
// Old Browsers:
// let lines = Array.prototype.slice.call(editor.children);
for (let line of lines) {
const newText = "Hello World !!!";
// 2. Insert the text immediately after the 'line' DOM node.
line.after(newText);
// Older Browsers:
// line.parentNode.insertBefore(
// document.createTextNode(newText), line.nextSibling);
}
// 3. Normalize the document, so that if the code above inserted multiple text nodes
// in a row (potentially meaning that `editor.children` is no longer just lines),
// the nodes will be merged back together.
editor.normalize();
}
Without knowing what editor.children actually specifically contains, I can't be 100% certain that this will work, but something like it certainly should.
I have a div with the contenteditable attribute. The user needs to be able to type and insert several select menus where the cursor is. I've managed to get the cursor position and to insert the first select menu, but it only works on the first text node.
That's how I get the cursor position:
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;
}
Then I update it every time the user types or clicks.
function updatePos() {
var el = document.getElementById("msg");
pos = getCaretCharacterOffsetWithin(el);
}
document.body.onkeyup = updatePos;
document.body.onmouseup = updatePos;
Then here's how I'm handling the button that adds the select. I'm not sure how to insert an element after a text node, so I insert a br tag and remove it later. There has to be a cleaner way, right?
$('#btn').click(function(){
var selectList = document.createElement('select');
var msg = $('#msg');
$(msg).html(function(){
var first = $(msg).html().substring(0, pos);
var last = $(msg).html().substring(pos);
return first + '<br>' + last;
});
$(msg).contents().filter('br').after(selectList);
$(msg).contents().filter('br').remove();
$(msg).focus();
})
I guess the problem is that I'm using substring to split the text and be able to insert the select there, and as soon as there is another select tag, the substring is not able to go past the first text node. So maybe I'm supposed to redo the whole thing with a different approach, but I'm completely stuck.
Here's the jsfiddle: https://jsfiddle.net/8a63sosr/
Thanks!
The problem is that for HTML elements it takes, well, in terms of jQuery, text(), not html().
I guess there's a better solution with some range parameters, but here's something:
//fix caret pos
var temlOffset = pos;
$('#msg').html().split(/(<[^>]+>)/g).forEach(function(el){
if(temlOffset > 0){
if(el.length && el[0] === '<'){
pos += el.length;
} else {
temlOffset -= el.length;
}
}
});
https://jsfiddle.net/Lemz17L8/
So, what it does is adding the html tag length to the pos value.
Best regards, Alexander
My requirement is to get the start and end character index of the text selected by the user with respect to the main div, lets say "content" div. See sample code below.
But when I select the "Rich" word in the web view. Its start and end character index comes respective to the where the "span" tag i.e. 1 and 5, respectively. Whereas it should be 12 and 16 w.r.t "content" div.
<html>
<body>
<div id="content" contenteditable="true" style="font-family: Times New Roman; color: #fff; font-size: 18;">
This is <span style="background-color: yellow;">out</span> Rich Text Editing View
</div>
</body>
JavaScript function m using at the moment is
function getHighlightedString()
{
var text = document.getSelection();
startIndex = text.anchorOffset;
endIndex = text.focusOffset;
selectedText = text.anchorNode.textContent.substr(startIndex, endIndex - text.anchorOffset);
}
Please help me out.
Here's an answer adapted from this one. The same caveats from that answer apply.
Demo: http://jsfiddle.net/62Bcf/1/
Code:
function getSelectionCharacterOffsetsWithin(element) {
var startOffset = 0, endOffset = 0;
if (typeof window.getSelection != "undefined") {
var range = window.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.startContainer, range.startOffset);
startOffset = preCaretRange.toString().length;
endOffset = startOffset + range.toString().length;
} else if (typeof document.selection != "undefined" &&
document.selection.type != "Control") {
var textRange = document.selection.createRange();
var preCaretTextRange = document.body.createTextRange();
preCaretTextRange.moveToElementText(element);
preCaretTextRange.setEndPoint("EndToStart", textRange);
startOffset = preCaretTextRange.text.length;
endOffset = startOffset + textRange.text.length;
}
return { start: startOffset, end: endOffset };
}
How to retrieve the position of selected text by calculating it's offset immediate after body tag?
For Example consider the following html,
<body> <div>hi</div> <div>dude!</div> </body>
on selecting from "i" (in hi) to "du" (in dude), I need to get 2 as start position & 4 as end position.
Note: Ignore the tag elements.
Javascript is preferable!
Here's some simple, naive code to do this that may well do the job for your use case. It does not take into account any text that may be made invisible (either by CSS or by being inside a or element, for example) and may have browser discrepancies (IE versus everything else) with line breaks, and takes no account of collapsed whitespace (such as 2 or more consecutive space characters collapsing to one visible space on the page). However, it does work for your example in all major browsers.
Live demo: http://jsfiddle.net/UuDpL/2/
Code:
function getSelectionCharOffsetsWithin(element) {
var start = 0, end = 0;
var sel, range, priorRange;
if (typeof window.getSelection != "undefined") {
range = window.getSelection().getRangeAt(0);
priorRange = range.cloneRange();
priorRange.selectNodeContents(element);
priorRange.setEnd(range.startContainer, range.startOffset);
start = priorRange.toString().length;
end = start + range.toString().length;
} else if (typeof document.selection != "undefined" &&
(sel = document.selection).type != "Control") {
range = sel.createRange();
priorRange = document.body.createTextRange();
priorRange.moveToElementText(element);
priorRange.setEndPoint("EndToStart", range);
start = priorRange.text.length;
end = start + range.text.length;
}
return {
start: start,
end: end
};
}
alert( getSelectionCharOffsetsWithin(document.body).start );
Use following java script function for Highlighting the html page..
function stylizeHighlightedString()
{
var range = window.getSelection().getRangeAt(0);
var selectionContents = range.extractContents();
var span = document.createElement("span");
span.appendChild(selectionContents);
span.setAttribute("class","uiWebviewHighlight");
span.style.backgroundColor = "red";
span.style.color = "white";
range.insertNode(span);
}