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>
Related
I'm making a text-searching mechanism (like ⌘ + F) for an iOS app and It's working but I have two issues.
Whenever someone searches something in Arabic, the word becomes disconnected.
Users can't search if there are diacritics in the text but their search does not (so basically I'm trying to make it diacritic-insensitive)
Here's the code for my highlighting (which I found from this):
var uiWebview_SearchResultCount = 0;
/*!
#method uiWebview_HighlightAllOccurencesOfStringForElement
#abstract // helper function, recursively searches in elements and their child nodes
#discussion // helper function, recursively searches in elements and their child nodes
element - HTML elements
keyword - string to search
*/
function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
if (element) {
if (element.nodeType == 3) { // Text node
var count = 0;
var elementTmp = element;
while (true) {
var value = elementTmp.nodeValue; // Search for keyword in text node
var idx = value.toLowerCase().indexOf(keyword);
if (idx < 0) break;
count++;
elementTmp = document.createTextNode(value.substr(idx+keyword.length));
}
uiWebview_SearchResultCount += count;
var index = uiWebview_SearchResultCount;
while (true) {
var value = element.nodeValue; // Search for keyword in text node
var idx = value.toLowerCase().indexOf(keyword);
if (idx < 0) break; // not found, abort
//we create a SPAN element for every parts of matched keywords
var span = document.createElement("span");
var text = document.createTextNode(value.substr(idx,keyword.length));
var spacetxt = document.createTextNode("\u200D");//\u200D
span.appendChild(text);
span.appendChild(spacetxt);
span.setAttribute("class","uiWebviewHighlight");
span.style.backgroundColor="#007DC8a3";
span.style.borderRadius="3px";
index--;
span.setAttribute("id", "SEARCH WORD"+(index));
//span.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);
//element.parentNode.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);
//uiWebview_SearchResultCount++; // update the counter
text = document.createTextNode(value.substr(idx+keyword.length));
element.deleteData(idx, value.length - idx);
var next = element.nextSibling;
//alert(element.parentNode);
element.parentNode.insertBefore(span, next);
element.parentNode.insertBefore(text, next);
element = text;
}
} else if (element.nodeType == 1) { // Element node
if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') {
for (var i=element.childNodes.length-1; i>=0; i--) {
uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword);
}
}
}
}
}
// the main entry point to start the search
function uiWebview_HighlightAllOccurencesOfString(keyword) {
uiWebview_RemoveAllHighlights();
uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
}
// helper function, recursively removes the highlights in elements and their childs
function uiWebview_RemoveAllHighlightsForElement(element) {
if (element) {
if (element.nodeType == 1) {
if (element.getAttribute("class") == "uiWebviewHighlight") {
var text = element.removeChild(element.firstChild);
element.parentNode.insertBefore(text,element);
element.parentNode.removeChild(element);
return true;
} else {
var normalize = false;
for (var i=element.childNodes.length-1; i>=0; i--) {
if (uiWebview_RemoveAllHighlightsForElement(element.childNodes[i])) {
normalize = true;
}
}
if (normalize) {
element.normalize();
}
}
}
}
return false;
}
// the main entry point to remove the highlights
function uiWebview_RemoveAllHighlights() {
uiWebview_SearchResultCount = 0;
uiWebview_RemoveAllHighlightsForElement(document.body);
}
function uiWebview_ScrollTo(idx) {
var idkNum = uiWebview_SearchResultCount - idx
var scrollTo = document.getElementById("SEARCH WORD" + idkNum);
if (scrollTo) scrollTo.scrollIntoView();
}
and I also found this that actually does exactly what I want (does not disconnect words and is diacritic-insensitive) but it's in JQuery and I couldn't figure out how to implement it in my code.
Instead of using indexOf, you can convert the string to an NSString and then use range(of:options:):
var range = value.range(of: keyword, options: [.caseInsensitive, .diacriticInsensitive])
On my page I use searching for a text and highlighting it like this:
document.querySelector('#search').addEventListener('keyup', function() {
var inputValue = this.value;
var tableTDs = document.querySelectorAll('table td');
for(var i = 0; i < tableTDs.length; i++) {
tableTDs[i].scrollIntoView();
if(document.createRange) {
var range = document.createRange();
var childs = tableTDs[i].childNodes;
for(var n = 0; n < childs.length; n++) {
var childNode = childs[n];
if(childNode.nodeValue) { // if child is a text node
var childValue = childNode.nodeValue;
} else { // if child is a nested element e.g. a link
var childValue = childNode.firstChild.nodeValue;
var childNode = childNode.firstChild;
}
if(childValue && childValue.indexOf(inputValue) != -1) {
range.setStart(childNode, childValue.indexOf(inputValue));
range.setEnd(childNode, childValue.indexOf(inputValue) + inputValue.length);
var span = document.createElement('span');
span.style.backgroundColor = 'yellow';
range.surroundContents(span);
return;
}
}
}
}
});
<input type="text" id="search">
<table>
<tr><td>First cell</td><td>Second cell and link anchor</td></tr>
</table>
If I want to find for example the word "anchor", I type "a", next "n" and next "c" and I see two highlightings, but not one highlighting of "anc".
So as I can understand, I need to remove all the wrappers before new range.surroundContents()
How can I resolve the issue?
UPDATED
Ok, the first possible solution is adding before tableTDs[i].scrollIntoView() the following code
tableTDs[i].innerHTML = tableTDs[i].innerHTML.replace(/<span style=\"background-color: yellow;\">/g,'');
tableTDs[i].innerHTML = tableTDs[i].innerHTML.replace(/<\/span>/g,'');
But is there something better?
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]);
}
}
Ok I am making a text editor and have to try and make the last word typed change font color based on if it's a keyword or not... I have tried multiple solutions to this but nothing has prevailed... Here is what I have tried so far
function getLastWord() {
var input = document.getElementById("my_text").value;
//var input = document.getElementById(textArea.value);
var inputValue = input.value;
var lastWordTyped
var changeColorOfWord;
var ele = document.querySelector("#my_text");
//ele.style.color = "blue"
if (input == null) {
input == " ";
}
lastWordTyped = input.substr(input.trim().lastIndexOf(" ") + 1);
//lastWordTyped = inputValue.substr(inputValue.trim().lastIndexOf(" ") + 1);
if (input != null) {
for (var i = 0; i < reservedKeyWords.length; i++) {
if ( lastWordTyped == reservedKeyWords[i] ) {
//changeColor(lastWordTyped);
//my_text.replace(inputValue, lastWordTyped);
//ele.fieldNameElement.innerHTML = lastWordTyped;
//ele.innerHTML = lastWordTyped;
ele.innerHTML.fontcolor = 'Blue';
return;
} else if (lastWordTyped !== reservedKeyWords[i]) {
//ele.innerHTML = ele.innerHTML.replace(lastWordTyped, '<span style="color:black"></span>');
//resetFontColor();
}
}
}
}
I have tried this function (found from SO)
function changeColor(word) {
var ele = document.querySelector("my_text");
ele.onkeypress = function () {
setTimeout(function () {
//the setTimeout is so the content is inserted before execution
document.getElementById('view_text').value = ele.textContent;
if (ele.innerHTML.indexOf(word) !== -1) {
ele.innerHTML = ele.innerHTML.replace(word, '<span style="color:blue">' + word + '</span>');
}
}, 50);
}
}
Also I have tried this one:
function colorMyKeyword(keywordColor, text) {
return '<span style="color:' + keywordColor + '>' + text + '</span>';
}
None of these functions have gotten the job done though. I have it now so that it will change the text color to blue but then the problem is that it changes ALL of the text to blue after that word...
I would prefer this to be in javascript as I do not know how to use JQuery, or really CSS for that matter or even know how to write it..
Thank you for any responses.
Updated code based on comment(s) below (changed to div from input)
Not the best code in the world but it should work. The CSS should probably be done by adding a class instead of changing the style attribute.
<div id="my_text">This is some text</div>
var isKeyword = false;
var el = document.getElementById('my_text');
var arr = el.innerHTML.split(' ');
var lastWordTyped = arr.pop();
/* replace with yours*/
var reservedKeyWords = ['text','another','word', 'here'];
for (var i = 0, len = reservedKeyWords.length ; i < len ; i++) {
if ( lastWordTyped == reservedKeyWords[i] ) {
lastWordTyped = '<span style="color:blue">'+lastWordTyped +'</span>'; //update color
arr.push(lastWordTyped);
isKeyword = true;
}
}
if (!isKeyword) { arr.push(lastWordTyped); } //put original back
el.innerHTML = arr.join(' ');
UPDATED: Do the whole thing on keyup
here's a simple example that you can use: DEMO
$('#text').keyup(function(){
$('#result').html($('#text').val());
var splittedText=$('#result').html().split(/\s/);
var lastWord=splittedText[splittedText.length-1];
$('#result').html($('#result').html().replace(lastWord,'<span>'+lastWord+'</span>'));
$('#result').children('span').css('color',$('#color').val());
});
you need to write a sentence in the first input and a hexa-deciaml color in the second one.(including the # at the beginning)
Here is an attempt to answer your questions :
I am using #GaryStorey's answer as it was a better starting point than what I add (I do like pop&push).
The problem with his answer were that it only showed how to change the color but it wasn't relevant on how to do it in an input nor in a contenteditable element.
So here are my adjustements, with a setCaret function to deal with the fact that caret always returns to start if we do change the innerHTML of edited element.
Be carefull, it is still very buggy and you should not use it in any production,
however it can give you a good starting point.
var reservedKeyWords = ['text', 'another', 'word', 'here'];
var el = document.getElementById('my_text');
el.addEventListener('keyup', function (evt) {
if (evt.keyCode == 32 || evt.keyCode == 13) {
var isKeyword = false;
var arr = el.innerHTML.split(/\s/);
var lastWordTyped = arr.pop();
lastWordTyped = lastWordTyped.replace(' ', '');
for (var i = 0, len = reservedKeyWords.length; i < len; i++) {
if (lastWordTyped == reservedKeyWords[i]) {
lastWordTyped = '<span style="color:blue">' + lastWordTyped + '</span>'; //update color
arr.push(lastWordTyped);
isKeyword = true;
}
}
if (!isKeyword) {
arr.push(lastWordTyped);
} //put original back
el.innerHTML = arr.join(' ') + ' ';
setCaret(el);
}
});
function setCaret(el) {
var range = document.createRange();
var endNode = el.lastChild;
range.selectNodeContents(endNode);
range.setStart(endNode, range.endOffset);
range.setEnd(endNode, range.endOffset);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
#my_text {
min-width: 100%;
min-height: 2em;
padding: 0.5em;
display: block;
border : dashed 0.5px grey;
}
<p> list of KeyWords : 'text', 'another', 'word', 'here';</p>
<span id="my_text" contenteditable="true">Edit me using "reservedKeyWords" defined in JS</span>
Oh and note that I am using a span instead of a div, because div tag adds some <br> from nowhere after the textNode.
The following codes doesn't work and the result is broken because there are white spaces in a HTML tag.
HTML:
<div>Lorem ipsum <a id="demo" href="demo" rel="demo">dolor sit amet</a>, consectetur adipiscing elit.</div>
Javascript:
var div = document.getElementsByTagName('div')[0];
div.innerHTML = div.innerHTML.replace(/\s/g, '<span class="space"> </span>');
How to replace replace white spaces which are not in HTML tags?
It would be a better idea to actually use the DOM functions rather than some unreliable string manipulation using a regexp. splitText is a function of text nodes that allows you to split text nodes. It comes in handy here as it allows you to split at spaces and insert a <span> element between them. Here is a demo: http://jsfiddle.net/m5Qe8/2/.
var div = document.querySelector("div");
// generates a space span element
function space() {
var elem = document.createElement("span");
elem.className = "space";
elem.textContent = " ";
return elem;
}
// this function iterates over all nodes, replacing spaces
// with space span elements
function replace(elem) {
for(var i = 0; i < elem.childNodes.length; i++) {
var node = elem.childNodes[i];
if(node.nodeType === 1) {
// it's an element node, so call recursively
// (e.g. the <a> element)
replace(node);
} else {
var current = node;
var pos;
while(~(pos = current.nodeValue.indexOf(" "))) {
var next = current.splitText(pos + 1);
current.nodeValue = current.nodeValue.slice(0, -1);
current.parentNode.insertBefore(space(), next);
current = next;
i += 2; // childNodes is a live array-like object
// so it's necessary to advance the loop
// cursor as well
}
}
}
}
You can deal with the text content of the container, and ignore the markup.
var div = document.getElementsByTagName('div')[0];
if(div.textContent){
div.textContent=div.textContent.replace(/(\s+)/g,'<span class="space"> </span>';
}
else if(div.innerText){
div.innerText=div.innerText.replace(/(\s+)/g,'<span class="space"> </span>';
}
First split the string at every occurrence of > or <. Then fit together all parts to a string again by replacing spaces only at the even parts:
var div = document.getElementsByTagName('div')[0];
var parts = div.innerHTML.split(/[<>]/g);
var newHtml = '';
for (var i = 0; i < parts.length; i++) {
newHtml += (i % 2 == 0 ? parts[i].replace(/\s/g, '<span class="space"> </span>') : '<' + parts[i] + '>');
}
div.innerHTML = newHtml;
Also see this example.
=== UPDATE ===
Ok, the result of th IE split can be different then the result of split of all other browsers. With following workaround it should work:
var div = document.getElementsByTagName('div')[0];
var sHtml = ' ' + div.innerHTML;
var sHtml = sHtml.replace(/\>\</g, '> <');
var parts = sHtml.split(/[<>]/g);
var newHtml = '';
for (var i = 0; i < parts.length; i++) {
if (i == 0) {
parts[i] = parts[i].substr(1);
}
newHtml += (
i % 2 == 0 ?
parts[i].replace(/\s/g, '<span class="space"> </span>') :
'<' + parts[i] + '>'
);
}
div.innerHTML = newHtml;
Also see this updated example.
=== UPDATE ===
Ok, I have completly changed my script. It's tested with IE8 and current firefox.
function parseNodes(oElement) {
for (var i = oElement.childNodes.length - 1; i >= 0; i--) {
var oCurrent = oElement.childNodes[i];
if (oCurrent.nodeType != 3) {
parseNodes(oElement.childNodes[i]);
} else {
var sText = (typeof oCurrent.nodeValue != 'undefined' ? oCurrent.nodeValue : oCurrent.textContent);
var aParts = sText.split(/\s+/g);
for (var j = 0; j < aParts.length; j++) {
var oNew = document.createTextNode(aParts[j]);
oElement.insertBefore(oNew, oCurrent);
if (j < aParts.length - 1) {
var oSpan = document.createElement('span');
oSpan.className = 'space';
oElement.insertBefore(oSpan, oCurrent);
var oNew = document.createTextNode(' ');
oSpan.appendChild(oNew);
}
}
oElement.removeChild(oCurrent);
}
}
}
var div = document.getElementsByTagName('div')[0];
parseNodes(div);
Also see the new example.