Selected content to upperCase? - javascript

I am just wondering if there is a simple solution already to the problem of turning selected content in tinymce to upperCase letters.
Anyone got a solution?
PS: The upperCase-function is known, but won't solve the tinymce setting of selected content alone.

This is what i came up with after some fiddling
// check if a node intersects the given range
rangeIntersectsNode: function (range, node) {
var nodeRange;
if (range.intersectsNode) {
return range.intersectsNode(node);
}
else {
nodeRange = node.ownerDocument.createRange();
try {
nodeRange.selectNode(node);
} catch (e) {
nodeRange.selectNodeContents(node);
}
return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
}
},
// Tinymce-Shortcut: (cmd/ctrl + shift +a)
if ( ( (mac && evt.metaKey)|| (!mac && evt.ctrlKey)) && evt.shiftKey && evt.keyCode == 65 ){
if (!ed.selection.isCollapsed()) {
var selection = ed.getWin().getSelection(); // user selection
var range = selection.getRangeAt(0); // erste range
var start = range.startContainer;
var start_offset = range.startOffset;
var end = range.endContainer;
var end_offset = range.endOffset;
// Get all textnodes of the common ancestor
// Check foreach of those textnodes if they are inside the selection
// StartContainer and EndContainer may be partially inside the selection (if textnodes)
// concatenate those textnode parts and make toUppercase the selected part only
// all textnodes inbetween need to become upperCased (the nodeContents)
// Selection needs to be reset afterwards.
var textnodes = t.getTextNodes(range.commonAncestorContainer);
for (var i=0; i<textnodes.length; i++) {
if (t.rangeIntersectsNode(range, textnodes[i])){
if (textnodes[i] == start && textnodes[i] == end) {
var text_content = start.textContent;
text_content = start.textContent.substring(0, start_offset) + text_content.substring(start_offset, end_offset).toUpperCase() + end.textContent.substring(end_offset);
textnodes[i].nodeValue = text_content;
}
else if (textnodes[i] == start){
var text_content = start.textContent.substring(0, start_offset) + start.textContent.substring(start_offset).toUpperCase();
textnodes[i].nodeValue = text_content;
}
else if (textnodes[i] == end){
var text_content = end.textContent.substring(0, end_offset).toUpperCase() + end.textContent.substring(end_offset);
textnodes[i].nodeValue = text_content;
}
else {
// Textnodes between Start- and Endcontainer
textnodes[i].nodeValue = textnodes[i].nodeValue.toUpperCase();
}
}
}
// reset selection
var r = ed.selection.dom.createRng();
r.setStart(start, start_offset);
r.setEnd(end, end_offset);
ed.selection.setRng(r);
evt.preventDefault();
return false;
}
}

Related

Javascript search text highlight with Swift

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])

how to get the caret position of a contenteditable div which contains images

I have this contentedittable div
<div contenteditable="true" id="text">minubyv<img src="images/smiley/Emoji Smiley-01.png" class="emojiText" />iubyvt</div>
Here is an image description of the code output
so I want to get the caret position of the div and lets assume that the cursor is after the last character. And this is my code for getting the caret position
function getCaretPosition(editableDiv) {
var caretPos = 0,
sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode == editableDiv) {
caretPos = range.endOffset;
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range.parentElement() == editableDiv) {
var tempEl = document.createElement("span");
editableDiv.insertBefore(tempEl, editableDiv.firstChild);
var tempRange = range.duplicate();
tempRange.moveToElementText(tempEl);
tempRange.setEndPoint("EndToEnd", range);
caretPos = tempRange.text.length;
}
}
return caretPos;
}
var update = function() {
console.log(getCaretPosition(this));
};
$('#text').on("mousedown mouseup keydown keyup", update);
But the problem is that it returns 6 instead of 14. The caret position goes back to 0 after the image. Please is there a way I can get the caret position to be 14 in this case.
EDIT
I want to also insert some element starting from the caret position. so this is my function to do that
selectStart = 0;
var update = function() {
selectStart = getCaretPosition(this);
};
function insertEmoji(svg){
input = $('div#text').html();
beforeCursor = input.substring(0, selectStart);
afterCursor = input.substring(selectStart, input.length);
emoji = '<img src="images/smiley/'+svg+'.png" class="emojiText" />';
$('div#text').html(beforeCursor+emoji+afterCursor);
}
See Tim Down's answer on Get a range's start and end offset's relative to its parent container.
Try to use the function he has to get the selection index with nested elements like this:
function getCaretCharacterOffsetWithin(element) {
var caretOffset = 0;
var doc = element.ownerDocument || element.document;
var win = doc.defaultView || doc.parentWindow;
var sel;
if (typeof win.getSelection != "undefined") {
sel = win.getSelection();
if (sel.rangeCount > 0) {
var range = win.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
}
} else if ( (sel = doc.selection) && sel.type != "Control") {
var textRange = sel.createRange();
var preCaretTextRange = doc.body.createTextRange();
preCaretTextRange.moveToElementText(element);
preCaretTextRange.setEndPoint("EndToEnd", textRange);
caretOffset = preCaretTextRange.text.length;
}
return caretOffset;
}
var update = function() {
console.log(getCaretCharacterOffsetWithin(this));
};
$('#text').on("mousedown mouseup keydown keyup", update);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable="true" id="text">minubyv<img src="https://themeforest.net/images/smileys/happy.png" class="emojiText" />iubyvt</div>
I wrote my own function, based on Tim Down's, that works like you want it. I changed the treeWalker to filter NodeFilter.ELEMENT_NODE insted of NodeFilter.SHOW_TEXT, and now <img/> elements also get processed inside our loop. I start by storing the range.startOffset and then recurse through all the selection tree nodes. If it finds an img node, then it adds just 1 to the position; if the current node element is different than our range.startContainer, then it adds that node's length. The position is altered by a different variable lastNodeLength that is adds to the charCount at each loop. Finally, it adds whatever is left in the lastNodeLength to the charCount when it exists the loop and we have the correct final caret position, including image elements.
Final working code (it returns 14 at the end, exactly as it should and you want)
function getCharacterOffsetWithin_final(range, node) {
var treeWalker = document.createTreeWalker(
node,
NodeFilter.ELEMENT_NODE,
function(node) {
var nodeRange = document.createRange();
nodeRange.selectNodeContents(node);
return nodeRange.compareBoundaryPoints(Range.END_TO_END, range) < 1 ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
},
false
);
var charCount = 0, lastNodeLength = 0;
if (range.startContainer.nodeType == 3) {
charCount += range.startOffset;
}
while (treeWalker.nextNode()) {
charCount += lastNodeLength;
lastNodeLength = 0;
if(range.startContainer != treeWalker.currentNode) {
if(treeWalker.currentNode instanceof Text) {
lastNodeLength += treeWalker.currentNode.length;
} else if(treeWalker.currentNode instanceof HTMLBRElement ||
treeWalker.currentNode instanceof HTMLImageElement /* ||
treeWalker.currentNode instanceof HTMLDivElement*/)
{
lastNodeLength++;
}
}
}
return charCount + lastNodeLength;
}
var update = function() {
var el = document.getElementById("text");
var range = window.getSelection().getRangeAt(0);
console.log("Caret pos: " + getCharacterOffsetWithin_final(range, el))
};
$('#text').on("mouseup keyup", update);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable="true" id="text">minubyv<img contenteditable="true" src="https://themeforest.net/images/smileys/happy.png" class="emojiText" />iubyvt</div>

Convert html element to string Javascript (obtained using search, not inner/outerHTML)

I have a JS function which searches for a string in the HTML source, and outputs the parent node:
function searchHTML(searchTerm) {
queue = [document.body],
curr
;
while (curr = queue.pop()) {
if (!curr.textContent.match(searchTerm)) continue;
for (var i = 0; i < curr.childNodes.length; ++i) {
switch (curr.childNodes[i].nodeType) {
case Node.TEXT_NODE : // 3
if (curr.childNodes[i].textContent.match(searchTerm)) {
console.log(curr);
// End of search
}
break;
case Node.ELEMENT_NODE : // 1
queue.push(curr.childNodes[i]);
break;
}
}
}
}
Currently, its output (in Javascript console) is not a string.
I need to perform regex on the output (curr), so I need it to be a string.
What I have tried:
curr = curr.toString()
curr = curr.replace(/[0-9]/g, "")
You can use .text() jQuery function to get the string from an HTML.
Here is an example of how you get string :
text= curr.text();
curr = text.replace(/[0-9]/g, "");
It seems to me you need to find the commonAncestorContainer for the term searched. That means if the term starts in a node and ends in another, you don't really have a clear definition of the common parent, until you get the a range.
I put together the function below where you can call search('My Term') and it should get a commonAncestorContainer. Some tweek should still be needed to search the same term more than once and to make sure that words ending inside an element still consider that element as the parent instead of the next one after the parent.
var search = function (searchTerm) {
// Stop if there is nothing to look for
if (!searchTerm || typeof searchTerm !== 'string')
return null;
searchTerm = searchTerm.toLowerCase();
var bodyText = document.body.textContent.toLowerCase(),
range = document.createRange(),
startOffset = bodyText.indexOf(searchTerm),
endOffset = startOffset + searchTerm.length,
iterationObject = {
index: 0,
length: bodyText.length,
startOffset: startOffset,
endOffset: endOffset,
startInNodeOffset: -1,
endInNodeOffset: -1,
startNode: null,
endNode: null
};
var textContent = function (textNode) {
return textNode.nodeValue || textNode.textContent || textNode.wholeText;
};
(function iterate (node, iterationObject) {
if (node.nodeType === 1) {
var childNodes = node.childNodes;
// Keep iterating but we should try to stop it when nodes are found
for (var i = 0, iLen = childNodes.length; i < iLen; i++)
iterate(childNodes[i], iterationObject);
} else if (node.nodeType === 3) {
var text = textContent(node),
startInNodeOffset,
endInNodeOffset;
// Change index and move on
if (iterationObject.index + text.length < iterationObject.startOffset)
iterationObject.index += text.length;
else if (iterationObject.startNode === null) {
startInNodeOffset = iterationObject.startOffset - iterationObject.index;
// Start range in the current node
// This condition should really only be needed to decide if the selection should start
// before or after this node. But that is another story.
if (startInNodeOffset <= text.length) {
iterationObject.startNode = node;
iterationObject.startInNodeOffset = startInNodeOffset;
}
iterationObject.index += text.length;
} else {
// Now try to find the endNode
if (iterationObject.index + text.length < iterationObject.endOffset)
iterationObject.index += text.length;
else if (iterationObject.endNode === null) {
endInNodeOffset = iterationObject.endOffset - iterationObject.index;
if (endInNodeOffset <= text.length) {
iterationObject.endNode = node;
iterationObject.endInNodeOffset = endInNodeOffset;
}
}
}
}
if (iterationObject.startNode !== null && iterationObject.endNode !== null)
return;
})(document.body, iterationObject);
if (iterationObject.startInNodeOffset > -1 && iterationObject.endInNodeOffset > -1) {
range.setStart(iterationObject.startNode, iterationObject.startInNodeOffset);
range.setEnd(iterationObject.endNode, iterationObject.endInNodeOffset);
return range.commonAncestorContainer;
}
return null;
};
If you are using jQuery, you can try outerHTML to get the string from the commonAncestorContainer.
var parentElement = search('Whatever'),
result = '';
if (parentElement !== null)
result = $(parentElement).outerHTML();
You can create a temporary DOM node, and then append curr to it. Then get the innerHTML and the result will be a string:
var tempNode = document.createElement("div");
tempNode.appendChild(curr);
console.log(temp.innerHTML);
Working example here:
http://codepen.io/anon/pen/QypxwM

Determing caret position in tokenized html input?

(no jquery please)
Given an html input whose value is split by /\s+/g, how would you find the current token that the caret is positioned at?
For instance, if your input value is
abc ab monkey
And you split it, it will become
["abc, "ab", "monkey"]
But if your caret position in the input is here...
abc ab monk^(caret here)ey
How would you determine which token the caret is currently in?
The api I'm looking for would be something like
var currentToken = getPosition(inputEl.value); // { index: 2, token: "monkey" }
I have most of it down, but when I start backtracking inside the input with the left arrow it gets messed up.
http://jsfiddle.net/dlizik/zmbpq5hz/
html
<input id="input" />
<pre id="test"></pre>
cursor at current token: <span id="res"></span>
js
(function($doc) {
"use strict";
var single = /\s/g;
var spacer = /\s+/g;
var disp = $doc.getElementById("test");
var input = $doc.getElementById("input");
var res = $doc.getElementById("res");
function keyListen(e) {
var tokens = this.value.split(spacer);
var tokenLengths = tokens.map(function(i) { return i.length; });
var cumulative = tokenLengths.map(function(i, n) {
return tokenLengths.slice(0, n + 1).reduce(function(a, b) {
return a + b;
});
});
var cursor = caretPos(this);
var currToken = tokens[getPos(cursor, cumulative)];
res.textContent = currToken;
disp.textContent = JSON.stringify(this.value.split(spacer), null, 2);
}
function getPos(curr, arr) {
for (var i = 0; i < arr.length; i++) {
if (curr <= arr[i]) return i;
}
}
function caretPos(el) {
var val = el.value;
var extra = val.match(single);
var whitespace = extra == null ? 0 : extra.length;
var pos = 0;
if ($doc.selection) {
el.focus();
var sel = $doc.selection.createRange();
sel.moveStart('character', -val.length);
pos = sel.text.length;
}
else if (el.selectionStart || el.selectionStart == '0') pos = el.selectionStart;
return (pos - whitespace);
}
input.addEventListener("keyup", keyListen.bind(input), false);
})(document);
Ok so you can just use regular expressions...wasn't that hard now that I think about it. You just have to find the number of words left of the caret position. That will give you the current token index as well as the string itself. However, you just have to account for a caret whose adjacent strings are both empty spaces.
http://jsfiddle.net/dlizik/zmbpq5hz/1/
This is the function you want to run
(function($doc) {
"use strict";
var single = /\s/g;
var tokenize = /[^\s+]/g;
var ledge = /[\s]*[^\s]+[\s]*/g;
var disp = $doc.getElementById("test");
var input = $doc.getElementById("input");
var res = $doc.getElementById("res");
function keyListen(e) {
var tokens = this.value.match(tokenize);
var pos = caretPos(this);
var adj = this.value.substring(pos - 1, pos + 1);
var left = this.value.slice(0, pos + 1).match(ledge).length - 1;
var curr = adj === " " ? null : tokens[left];
res.textContent = curr + "";
disp.textContent = JSON.stringify(this.value.split(spacer), null, 2);
}
function caretPos(el) {
var pos = 0;
if ($doc.selection) {
el.focus();
var sel = $doc.selection.createRange();
sel.moveStart('character', -el.value.length);
pos = sel.text.length;
}
else if (el.selectionStart || el.selectionStart == '0') pos = el.selectionStart;
return (pos);
}
input.addEventListener("keyup", keyListen.bind(input), false);
})(document);

Javascript selected text highlighting prob

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]);
}
}

Categories