I am trying to apply style to the text selected by the user(mouse drag) for which I need to get the start and end index of the selected text.
I have tried using "indexOf(...)" method. but it returns the first occurrence of the selected substring. I want the actual position of the substring with respect to the original string. For example.., if I select the letter 'O' at position 3 [YOLO Cobe], I expect the index as 3 but the indexOf() method returns 1 which is the first occurrence of 'O' in [YOLO Cobe].
Is there any other method of getting the actual start and end index of selected text and not the first occurrence ?
function getSelectionText()
{
var text = "";
if (window.getSelection) {
text = window.getSelection().toString();
}
return text;
}
document.getElementById('ip').addEventListener('mouseup',function(e)
{
var txt=this.innerText;
console.log(txt);
var selectedText = getSelectionText();
console.log(selectedText);
var start = txt.indexOf(selectedText);
var end = start + selectedText.length;
if (start >= 0 && end >= 0){
console.log("start: " + start);
console.log("end: " + end);
}
});
<div id="ip">YOLO Cobe</div>
What you are looking for is available inside object returned by window.getSelection()
document.getElementById('ip').addEventListener('mouseup',function(e)
{
var txt = this.innerText;
var selection = window.getSelection();
var start = selection.anchorOffset;
var end = selection.focusOffset;
if (start >= 0 && end >= 0){
console.log("start: " + start);
console.log("end: " + end);
}
});
<div id="ip">YOLO Cobe</div>
And here is example for more complex selections on page based on #Kaiido comment:
document.addEventListener('mouseup',function(e)
{
var txt = this.innerText;
var selection = window.getSelection();
var start = selection.anchorOffset;
var end = selection.focusOffset;
console.log('start at postion', start, 'in node', selection.anchorNode.wholeText)
console.log('stop at position', end, 'in node', selection.focusNode.wholeText)
});
<div><span>Fragment1</span> fragment2 <span>fragment3</span></div>
window.getSelection().anchorOffset will give you the index that you are looking for.
MDN link: https://developer.mozilla.org/en-US/docs/Web/API/Selection/anchorOffset
I tried using the anchorOffset and focusOffset from the getSelection() method and it wasn't giving me the desired index.
So I came up with this solution myself (Note: it worked in chrome, don't know about other browsers)
HTML
<input type="text" class="input" hidden />
<textarea> remind me to be here to morrow </textarea>
JS
let text = document.querySelector("textarea");
let input = document.querySelector(".input");
In this instance, "here to morrow" is the highlighted portion.
For the selected text I did
input.value = getSelection();
let selectedText = input.value;
For the starting index of the selected text, I did
let start = body.value.indexOf(getSelection());
let end = start + selectedText.lenght;
Hope this proves useful
Related
I am trying to use window.getSelection().getRangeAt(0) to get the index of the selected word in a sentence. It works fine in a text without any <mark> and <abbr>. But when there are such tags in a sentence, it seems this function will cut the sentence into several pieces.
For example, one sentence in HTML looks like My car <abbr title="car_state"><mark>broke down</mark></abbr>. What do I do?
When I selected the text before broke down, it works fine. But when I selected the text after, for example,e What, it will give the startOffset at 2 instead of 22.
Is it possible to get index regarding the whole sentence?
Inspired by Kaiido's answer, the following method will work.
Although the highlighted texts will not match, I will not need the highlighted text anyway
Please feel free to add comments about the solution.
The running example
$('#selected_text').click(function(){
var text = "My car is broke down. What do I do?";
var range = window.getSelection().getRangeAt(0);
var start = range.startOffset;
var end = range.endOffset;
var extra = 0;
var selected_string = range.toString();
var t = $('span').contents();
for(var i = 0; i < t.length; i++){
console.log(extra);
if(t[i].wholeText === undefined){
extra += t[i].textContent.length;
}else if(t[i].wholeText.includes(selected_string)){
break;
}else{
extra += t[i].length;
}
}
start += extra;
end += extra;
console.log("start index: " + start);
console.log("end index: " + end);
console.log(text.slice(start, end));
console.log(selected_string);
console.log("match: ", (selected_string === text.slice(start, end)));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<body>
<span>My car is <abbr title="car_state"><mark>broke down</mark></abbr>. What do <mark>I</mark> d<mark>o</mark>?</span>
<button id='selected_text'>show selected text</button>
</body>
</html>
Quoting MDN
The Range.startOffset read-only property returns a number representing where in the startContainer the Range starts.
And same goes with Range.endOffset, which returns the position in the endContainer.
When you select the word What in the page, the startContainer is the TextNode that starts after your </abbr>. So the indice you get are relative to this TextNode.
If you want to get the selected text, then simply call the Selection.toString() method.
$('#selected_text').click(function() {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var start = range.startOffset;
var end = range.endOffset;
console.log("start index: " + start);
console.log("end index: " + end);
console.log('startContainer', range.startContainer.nodeName, range.startContainer.textContent);
console.log('endContainer', range.endContainer.nodeName, range.endContainer.textContent);
console.log('toString:', sel.toString());
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<span>My car is <abbr title="car_state"><mark>broke down</mark></abbr>. What do I do?</span>
<button id='selected_text'>show selected text</button>
And if you know the common container and want to know where you are relatively to this ancestor's text content, then you'd have to walk through its childNodes until you find both the startContainer and endContainer.
var container = $('#container')[0];
$('#selected_text').click(function() {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var sel_start = range.startOffset;
var sel_end = range.endOffset;
var charsBeforeStart = getCharactersCountUntilNode(range.startContainer, container);
var charsBeforeEnd = getCharactersCountUntilNode(range.endContainer, container);
if(charsBeforeStart < 0 || charsBeforeEnd < 0) {
console.warn('out of range');
return;
}
var start_index = charsBeforeStart + sel_start;
var end_index = charsBeforeEnd + sel_end;
console.log('start index', start_index);
console.log('end index', end_index);
console.log(container.textContent.slice(start_index, end_index));
});
function getCharactersCountUntilNode(node, parent) {
var walker = document.createTreeWalker(
parent || document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
var found = false;
var chars = 0;
while (walker.nextNode()) {
if(walker.currentNode === node) {
found = true;
break;
}
chars += walker.currentNode.textContent.length;
}
if(found) {
return chars;
}
else return -1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<span id="container">My car is <abbr title="car_state"><mark>broke down</mark></abbr>. What do I do?</span>
<button id='selected_text'>show selected text</button>
I have textbox/textarea input fields by selecting some text inside e.g.
Test Message with sub text and last message
on selecting "Mes" and press ctrl+B it should append <b> (or any character or multiple chars of my choice) to selected text at both ends in JavaScript as shown below
Test <b>Mes<b>sage with sub text and last message
I am able to simulate Ctrl+B key event successfully but not able to replace selected text with new text.
You can get selection start and end from corresponding fields in textarea.
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<textarea>Test Message with sub text and last message</textarea>
<script>
jQuery('textarea').on('keydown', function(e) {
if (e.keyCode == 66 && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
var text = jQuery(this).val();
var start = this.selectionStart;
var end = this.selectionEnd;
var selection = '<b>' + text.substring(start, end) + '</b>';
text = text.substring(0, start) + selection + text.substring(end);
jQuery(this).val(text);
this.selectionStart = start;
this.selectionEnd = start + selection.length;
}
});
</script>
Following function wraps the selected text in a text area with given string .
I have written an example case
function wrapSelection(textArea, wrappingString) {
var s = textArea.selectionStart;
var e = textArea.selectionEnd;
var oldValue = textArea.value;
var newValue = oldValue.slice(0, s) + wrappingString + oldValue.slice(s, e) + wrappingString + oldValue.slice(e, oldValue.length);
textArea.value = newValue;
}
var element = document.getElementById('your-input-element');
element.onselect = function () {
wrapSelection(element, '<b>');
}
I simply try to replace the selected text from an input field e.g from a textarea, with another text.
E.g. if i select a part of the text inside the input field and click the Button CENTER, then the selected text should be wrapped in <center></center>.
I found this "solution" from 2009 which seems to be outdated, I tried it with Chrome and Firefox and I get the info that my browser is not supported.
Is there another way to achieve this? It should work at least with Firefox and Chrome.
Try this:
function getSel() // javascript
{
console.log("test");
// obtain the object reference for the textarea>
var txtarea = document.getElementById("mytextarea");
// obtain the index of the first selected character
var start = txtarea.selectionStart;
// obtain the index of the last selected character
var finish = txtarea.selectionEnd;
//obtain all Text
var allText = txtarea.value;
// obtain the selected text
var sel = allText.substring(start, finish);
//append te text;
var newText=allText.substring(0, start)+"<center>"+sel+"</center>"+allText.substring(finish, allText.length);
console.log(newText);
txtarea.value=newText;
}
https://jsfiddle.net/qpw1eemr/1/
Here is one simple example
$(function() {
$('textarea').select(function(event) {
var elem = $(this);
var start = elem.prop("selectionStart");
var end = elem.prop("selectionEnd");
var prefixStr = elem.text().substring(0, start);
var sufixStr = elem.text().substring(end, elem.text().length);
var selectedStr = elem.text().substring(start, end);
function transform(str) {
return '<center>' + str + '</center>'
}
elem.text(prefixStr + transform(selectedStr) + sufixStr);
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea>iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii</textarea>
// hope this would be usefull
// i used these codes for auto completing the texts in textarea.
// other methods change all matching items before the selected text
// but this affects only the text where caret on.
// at first, i divided textarea.value into 3 pieces. these are ;
// p1; until the 'searched' item, p2 after 'searched' item
// and pa = new item that will replaced with 'searched' item
// then, i combined them together again.
var tea = document.getElementById(targetTextArea);
caretPosition = tea.selectionStart - ara.length; //ara=searched item
p1 = tea.value.substring(0,caretPosition);
console.log('p1 text : ' + p1);
p2 = tea.value.substring(caretPosition+ara.length,tea.value.length);
console.log('p2 text : ' + p2);
pa = yeni; //new item
console.log('pa text : ' + pa);
tea.value = p1 + pa + p2;
tea.selectionStart = caretPosition + yeni.length;
tea.selectionEnd = caretPosition + yeni.length;
tea.focus();
I have a contenteditable div
<div id="divTest" contenteditable="true">
I need to get the last word from caret position and on certain condition I have to test and remove this specific word only. Below is how am I doing
$('#divTest').on('keyup focus', function (e) {
if (e.keyCode == 32) {
var lastWord = getWordPrecedingCaret(this), spanLastWord = $('#lastWord');
}
});
function getWordPrecedingCaret(containerEl) {
var preceding = "",
sel,
range,
precedingRange;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(containerEl, 0);
preceding = range.toString();
}
} else if ((sel = document.selection) && sel.type != "Control") {
range = sel.createRange();
precedingRange = range.duplicate();
precedingRange.moveToElementText(containerEl);
precedingRange.setEndPoint("EndToStart", range);
preceding = precedingRange.text;
}
var words = range.toString().trim().split(' '),
lastWord = words[words.length - 1];
if (lastWord) {
var resultValue = 'some'; // this value is coming from some other function
if (resultValue == lastWord) {
alert('do nothing');
// do nothing
}
else
{
alert('replace word');
// delete That specific word and replace if with resultValue
}
return lastWord;
}
}
Demo: http://codepen.io/anon/pen/ogzpXV
I have tried range.deleteContents(); but that will delete all the content in the div.
How can I replace specific word only?
To work with Ranges we need to keep in mind that we are working with Nodes, not only the text that is rendered. The structure you want to manipulate is:
<div id="divTest" contenteditable="true"> <-- Element Node
"some text" <-- TextNode
</div>
But it also could be:
<div id="divTest" contenteditable="true"> <-- Element Node
"some text" <-- TextNode
"more text" <-- TextNode
"" <-- TextNode
</div>
To solve your problem is simplier to handle only one TextNode, I propose to use the normalize() function to join all of them into a single one.
Then you only need to set the Range to the word's bounds before deleteContents(). Once deleted, you can insert a new TextNode with the substitution using insertNode().
var wordStart = range.toString().lastIndexOf(lastWord);
var wordEnd = wordStart + lastWord.length;
/* containerEl.firstChild refers to the div's TextNode */
range.setStart(containerEl.firstChild, wordStart);
range.setEnd(containerEl.firstChild, wordEnd);
range.deleteContents();
range.insertNode(document.createTextNode(resultValue));
For this to work, you need that the text is in a single TextNode. But after ìnsertNode the div will contain multiple text nodes. To fix this simply call normalize() to join all TextNode elements.
containerEl.normalize();
Edit:
As Basj points out, the original solution fails for multiline. That's because when hitting ENTER the structure changes from:
<div id="divTest" contenteditable="true"> <-- Element Node
"some text" <-- TextNode
</div>
to something like:
<div id="divTest" contenteditable="true"> <-- Element Node
<div>"some text"</div>
<div>"more text"</div>
</div>
I've updated this answer, but it's also worth to read Basj's answer at this question: Replace word before cursor, when multiple lines in contenteditable
JSFiddle demo or runnable code snippet:
document.getElementById('divTest').onkeyup = function (e) {
if (e.keyCode == 32) {
getWordPrecedingCaret(this);
}
};
function getWordPrecedingCaret(containerEl) {
var preceding = "",
sel,
range,
precedingRange;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(containerEl, 0);
preceding = range.toString();
}
} else if ((sel = document.selection) && sel.type != "Control") {
range = sel.createRange();
precedingRange = range.duplicate();
precedingRange.moveToElementText(containerEl);
precedingRange.setEndPoint("EndToStart", range);
preceding = precedingRange.text;
}
var words = range.toString().trim().split(' '),
lastWord = words[words.length - 1];
if (lastWord) {
var resultValue = 'some'; // this value is coming from some other function
if (resultValue == lastWord) {
console.log('do nothing: ' + lastWord);
// do nothing
} else {
console.log('replace word ' + lastWord);
/* Find word start and end */
var wordStart = range.endContainer.data.lastIndexOf(lastWord);
var wordEnd = wordStart + lastWord.length;
console.log("pos: (" + wordStart + ", " + wordEnd + ")");
range.setStart(range.endContainer, wordStart);
range.setEnd(range.endContainer, wordEnd);
range.deleteContents();
range.insertNode(document.createTextNode(resultValue));
// delete That specific word and replace if with resultValue
/* Merge multiple text nodes */
containerEl.normalize();
}
return lastWord;
}
}
<div id="divTest" contenteditable="true">Write words here and hit SPACE BAR</div>
words = ['oele', 'geel', 'politie', 'foo bar'];
function markWords() {
var html = div.html().replace(/<\/?strong>/gi, ''),
text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '),
exp;
$.each(words, function(i, word) {
exp = new RegExp('\\b(' + word + ')\\b', 'gi');
html = html.replace(exp, function(m) {
console.log('WORD MATCH:', m);
return '<strong>' + m + '</strong>';
});
});
//html = html.replace(' ', ' ').replace(/\s+/g, ' ');
console.log('HTML:', html);
console.log('----');
div.html(html);
}
Call this function on setinterval
Fiddle
Tobías' solution works well for single-line contenteditable div. But if you add multiple lines, it doesn't work anymore.
Here is a general solution that works for both single-line or multiline contenteditable div.
I'm using a Kendo UI Editor. I want to highlight the excess characters that are typed/pasted in the editor. This is what I've done:
$(function () {
var $editor = $('#txt-editor');
$editor.kendoEditor({
keydown: ValidateRichTextEditor
});
});
function ValidateRichTextEditor(e) {
var editor = $(e.sender.textarea),
kendoEditor = editor.data('kendoEditor'),
characters = kendoEditor.body.innerText.length,
limit = editor.data('valLengthMax');
if (characters > limit) {
var textNodes = getTextNodes(kendoEditor.body),
charCount = 0,
startNode, startOffset;
for (var i = 0, textNode; textNode = textNodes[i++];) {
var chars = charCount + textNode.length;
if (limit < chars) {
//set the text node as the starting node
//if the characters hit the limit set
startNode = textNode;
startOffset = chars - charCount;
break;
}
//add the length of the text node to the current character count
charCount += textNode.length;
}
var range = kendoEditor.createRange();
range.setStart(startNode, startOffset);
kendoEditor.selectRange(range);
kendoEditor.document.execCommand('backColor', false, '#fcc');
}
}
function getTextNodes(node) {
var textNodes = [];
//node type 3 is a text node
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, getTextNodes(children[i]));
}
}
return textNodes;
}
jsfiddle
So far, the highlighting works but the cursor position is always at the position where the highlighting starts. How can I position the cursor so that it would remember the last place it was? Say for example I just keep on typing, the cursor should be at the end of the editor content. Or when I click somewhere in the middle of the content, the cursor should start where I clicked on the content.
Help on this would be greatly appreciated. Thanks!
If I am correctly interpreting your requirement, there is a much simpler solution than what you are attempting.
(function () {
var $editor = $('#txt-editor'),
limit = $editor.data('valLengthMax')
limitExceeded = false;
$editor.kendoEditor({
keyup: ValidateRichTextEditor
});
function ValidateRichTextEditor(e) {
var characters = this.body.innerText.length;
console.log('\'' + this.body.innerText + '\' : ' + this.body.innerText.length);
if (characters >= limit && !limitExceeded) {
limitExceeded = true;
this.exec('backColor', { value: '#fcc' });
}
}
})();
Update 1: This solution is a bit buggy. The backspace key causes some hiccups.
Update 2: After a lot of fiddling, you cannot trust body.innerText.length. It never returns the correct value once the background color style is executed. My reasoning is that the <span> elements that are added to the body are counted as characters and the backspace key does not remove them as would be expected.
Here is a JSBin example where you can read the console output as you type. Illogical to say the least.