Replace specific word in contenteditable - javascript

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.

Related

How to find index of selected text in getSelection() using javascript?

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

window.getSelection().getRange(0) does not work when text is wrapped by <mark>

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>

get last character before caret position in javascript

I am about to implement Facebook like in integration in my contenteditable div where if i give '$' and some character like 'a' i need a auto-suggestion which should pop up near my caret position.
I need to know how to find out the last character before caret position either in JavaScript for IE and Other browsers. I have access to the Jquery library.
(function($) {
$.fn.getCursorPosition = function() {
var input = this.get(0);
if (!input) return; // No (input) element found
if ('selectionStart' in input) {
// Standard-compliant browsers
return input.selectionStart;
} else if (document.selection) {
// IE
input.focus();
var sel = document.selection.createRange();
var selLen = document.selection.createRange().text.length;
sel.moveStart('character', -input.value.length);
return sel.text.length - selLen;
}
}
})(jQuery);
eg.
var caretPosition = $("#contenteditablediv").getCursorPosition();
var lastchar = getchar(caretposition -1);???
Here's an example of how to do this. It creates a range that starts at the start of the editable element and ends immediately before the caret, gets the range's text and returns the last character of that range.
Demo: http://jsfiddle.net/MH5xX/
function getCharacterPrecedingCaret(containerEl) {
var precedingChar = "", 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);
precedingChar = range.toString().slice(-1);
}
} else if ( (sel = document.selection) && sel.type != "Control") {
range = sel.createRange();
precedingRange = range.duplicate();
precedingRange.moveToElementText(containerEl);
precedingRange.setEndPoint("EndToStart", range);
precedingChar = precedingRange.text.slice(-1);
}
return precedingChar;
}
var editableEl = document.getElementById("editable");
var precedingCharEl = document.getElementById("precedingChar");
function reportCharacterPrecedingCaret() {
precedingCharEl.innerHTML = "Character preceding caret: " + getCharacterPrecedingCaret(editableEl);
}
editableEl.onmouseup = editableEl.onkeyup = reportCharacterPrecedingCaret;
<div contenteditable="true" id="editable">Here is some text. Please put the caret somewhere in here.</div>
<div id="precedingChar" style="font-weight: bold"></div>

Suggestion to improve the script

In the below script I am trying to highlight all the words in a sentence
function SearchQueue(text)
{
if(text !== null)
{
text = text.replace(/“/g, "\"");
text = text.replace(/”/g, "\"");
text = text.replace(/’/g, "\'");
text = text.replace(/‘/g, "\'");
text = text.replace(/–/g, "\-");
text = text.replace(/ +(?= )/g,'');
$.trim(text);
text = text.replace(/\d\.\s+|[a-z]\)\s+|•\s+|[A-Z]\.\s+|[IVX]+\.\s+/g, "");
text = text.replace(/([0-9A-Z]+[.)]|•)\s+/gi, "");
text = text.replace(/(\r\n|\n|\r)/gm," ");
}
var words = text.split(' ');
for(var i=0;i<words.length;i++)
$('*').highlight(''+words[i]+''); // Will highlight the script with background color
}
But this is making my page "unresponsive". Please suggest me to improve the script...
You are selecting the entire dom tree in each iteration which may explain the unresponsiveness.
Try the following:
var body = $('body'); // since thats where all the text should live
for(var i=0;i<words.length;i++){
body.highlight(''+words[i]+''); // Will highlight the script with background color
}
Here's my first set of adjustments:
var $all = $('*');
function SearchQueue(text) {
if(text) {
text = text.replace(/[“”]/g, '"');
text = text.replace(/[’‘]/g, "'");
text = text.replace(/–/g, '-');
text = text.replace(/\s+/g, ' ');
$.trim(text);
text = text.replace(/\d\.\s+|[a-z]\)\s+|•\s+|[A-Z]\.\s+|[IVX]+\.\s+/g, '');
text = text.replace(/([0-9A-Za-z]+[.)]|•)\s+/g, '');
var words = text.split(' ');
for(var i = 0; i < words.length; i++) {
$all.highlight(words[i]); // Will highlight the script with background color
}
}
}
You can combine a few of your replaces using a match evaluator (don't know what javascript calls them).
example : http://jsfiddle.net/zyqVE/
function match_eval(m){
switch (m){
case "“":case "”":
return "\"";
case "‘":case "’":
return "'";
// etc...
}
return m;
}
alert("this “i“ a test".replace(/[““’‘–]/g, match_eval));
in context :
function match_eval(m){
switch (m){
case "“":case "”":
return "\"";
case "‘":case "’":
return "'";
case "–"
return "-";
}
return m;
}
function SearchQueue(text)
{
if(text !== null)
{
text = text.replace(/[“”’‘–]/g, match_eval);
text = text.replace(/ +(?= )/g,'');
$.trim(text);
text = text.replace(/\d\.\s+|[a-z]\)\s+|•\s+|[A-Z]\.\s+|[IVX]+\.\s+/g, "");
text = text.replace(/([0-9A-Z]+[.)]|•)\s+/gi, "");
text = text.replace(/(\r\n|\n|\r)/gm," ");
}
var words = text.split(' ');
for(var i=0;i<words.length;i++)
$('*').highlight(''+words[i]+''); // Will highlight the script with background color
}

Rewrite a IE Code to a FF Code

This is the code (now is full):
HTML:
<div id="content" contentEditable="true" onkeyup="highlight(this)">This is some area to type.</div>
Javascript:
function highlight(elem){
// store cursor position
var cursorPos=document.selection.createRange().duplicate();
var clickx = cursorPos.getBoundingClientRect().left;
var clicky = cursorPos.getBoundingClientRect().top;
// copy contents of div
var content = elem.innerHTML;
var replaceStart = '';
var replaceEnd = '';
// only replace/move cursor if any matches
// note the spacebands - this prevents duplicates
if(content.match(/ test /)) {
elem.innerHTML = content.replace(/ test /g,' '+replaceStart+'test'+replaceEnd+' ');
// reset cursor and focus
cursorPos = document.body.createTextRange();
cursorPos.moveToPoint(clickx, clicky);
cursorPos.select();
}
}
Just woks on IE, unhapply.
Anyone can 'adjust' this code, to work on FF too!...
Thanks
Edit[1]:
Div Editable and More... More
This code replaces a especific word by the same word formatted...
And the caret (cursor) stay always after the word replaced! <<< "This is the big"
But just works on IE, and I like so much to rewrite this code to work on FF... but I can't do it... Its so hard...
Anyone can help?
Edit[2]:
My problem is just with this part:
// reset cursor and focus
cursorPos = document.body.createTextRange();
cursorPos.moveToPoint(clickx, clicky);
cursorPos.select();
Because, moveToPotion and select functions just works on IE... Until then it is easy...
On FF there is another set of functions that make it possible... But i don't know how to write another code that do the same things. Do you got it?
You can preserve the caret position by inserting a marker element at its current location before doing your replacement on the element's innerHTML. (Using DOM methods to traverse the text nodes and searching each for the text you want would be preferable to using innerHTML, by the way).
The following works, so long as the caret is not positioned within or adjacent to the word "text". I also added a timer to prevent calling this function every time a key is pressed and to wait for the user to stop typing for half a second.
function insertCaretMarker() {
var range;
var markerId = "sel_" + new Date() + "_" + ("" + Math.random()).substr(2);
if (window.getSelection) {
var sel = window.getSelection();
range = sel.getRangeAt(0);
range.collapse(true);
var markerEl = document.createElement("span");
markerEl.appendChild(document.createTextNode("\u00a0"));
markerEl.id = markerId;
range.insertNode(markerEl);
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
range.collapse(true);
if (range.pasteHTML) {
range.pasteHTML("<span id=\"" + markerId + "\"> </span>");
}
}
return markerId;
}
function restoreCaret(markerId) {
var el = document.getElementById(markerId);
var range;
if (el) {
if (window.getSelection && document.createRange) {
var sel = window.getSelection();
range = document.createRange();
range.setStartBefore(el);
sel.removeAllRanges();
sel.addRange(range);
} else if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(el);
range.collapse(true);
range.select();
}
el.parentNode.removeChild(el);
}
}
function preserveCaretPosition(func) {
var id = insertCaretMarker();
func();
restoreCaret(id);
}
var highlightTimer;
function highlight(elem) {
if (highlightTimer) {
window.clearTimeout(highlightTimer);
}
highlightTimer = window.setTimeout(function() {
highlightTimer = null;
var replaceStart = '<b>';
var replaceEnd = '</b>';
// only replace/move cursor if any matches
// note the spacebands - this prevents duplicates
if (elem.innerHTML.match(/ test /)) {
preserveCaretPosition(function() {
elem.innerHTML = elem.innerHTML.replace(/ test /g, ' ' + replaceStart + 'test' + replaceEnd + ' ');
});
}
}, 500);
}

Categories

Resources