Javascript - Cannot Get Parent Of Selected Text After Replacing The Selection - javascript

I am trying to get the closest parent element of the selected text, it works if the selected text hasn't been replaced, but it fails after the text replacement.
Here is the code (I modified from other source):
jQuery(document).ready(function($){
$("#getparent").on("click touch", function(){
var selection, elements = [], ranges = [], rangeCount = 0, parent_id, parent_class;
if (window.getSelection){
selection = window.getSelection();
if (selection.rangeCount) {
var i = selection.rangeCount;
while (i--) {
selectionrange = selection.getRangeAt(i);
ranges[i] = selectionrange.cloneRange();
// SELECTION START'S CONTAINER
var parentDataStart = selectionrange.startContainer.childNodes[ranges[i].startOffset];
parentDataStart = parentDataStart || selectionrange.startContainer.parentNode;
var parentTagStart = parentDataStart.tagName.toLowerCase(); // parent tag name of selection
console.log(parentTagStart);
alert(parentTagStart);
// SELECTED TEXT NODE
selectedtext = selection.toString();
elements[i] = document.createTextNode(selectedtext);
ranges[i].deleteContents();
ranges[i].insertNode(elements[i]);
ranges[i].selectNode(elements[i]);
}
// Restore text selection
selection.removeAllRanges();
i = ranges.length;
while (i--) {
selection.addRange(ranges[i]);
}
}
}
});
});
And Here is A Fiddle
DEMO

the node selection boundary should be moved to the node's content.
So use selectNodeContents instead.
in the case of that question, it should be:
ranges[i].selectNodeContents(elements[i]); instead of ranges[i].selectNode(elements[i]);
Update:
Fiddle Demo

Related

How to clear background color of previously seelected text?

I have div element with multiple lines of text. User can select texts using the mouse. I have javascript that sets the background color of the selected text when user click on a btnChangeColor button.
$(function(){
var result = [];
$("#btnChangeColor").click(function () {
let currentColor = "red";
if (window.getSelection()) {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var selectedText = range.toString();
var startOffset = range.startOffset;
var endOffset = startOffset + selectedText.length;
var obj = {
Id: "SomeUniqueIdPerSelection",
start: startOffset,
end: endOffset,
text: selectedText
}
result.push(obj);
document.designMode = "on";
//if (range) {
// sel.removeAllRanges();
// sel.addRange(range);
//}
// Colorize text
document.execCommand("BackColor", false, currentColor);
// Set design mode to off
document.designMode = "off";
}
})
})
The scripts stores the selection with unique id in an array.
There is another button on the same page. When user click, I want to clear the background color of the particular selection. Assume that SomeUniqueIdPerSelection is supplied as parameter
$("btnClearBtn").click(function(){
let id = GetSelectionId();
let selected = result.find(x=>x.Id === id);
//How do i clear the background color of this selection
})

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

how to get the html source of the selected text in ckeditor

Suppose I have written 5 lines in ckeditor and I have selected the 2nd and 3rd line. How to get the html source of the selected text.Assuming the code will be selected in continuation always.
function getSelectionHtml()
{
editor=CKEDITOR.instances.editor1;
var sel = editor.getSelection();
var ranges = sel.getRanges();
var el = new CKEDITOR.dom.element("div");
for (var i = 0, len = ranges.length; i < len; ++i) {
el.append(ranges[i].cloneContents());
}
console.log("OrgHtml:\n"+el.getHtml());
return el.getHtml();
}
Use this function to get the selected text's InnerHtml value.I'm using this function also.
You can get your active selection with
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
text = container.innerHTML;
}
the HTML you selected will be stored in the variable text.
All selected HTML nodes - CKEditor
Note: It all depends on the current structure of data in your ckeditor. If you have selected a text which is inside a tag and text is partially selected then complete tag will get returned.
Note eg:
<p>my ckeditor text</p> enclosed in a p-tag
if you have selected "editor te" then complete p-tag will get return as you said you need HTML not selected text.
Code:
var range = editor.getSelection().getRanges()[0] //editor is instance of your ck-editor
var selectedHTML = [];
console.log(range.startPath().elements[0].$, 'first')
selectedHTML.push(range.startPath().elements[0].$)
var selectedSibling = "";
if(range.startPath().elements[0].$ != range.endPath().elements[0].$) {
selectedSibling = range.startContainer.$.parentNode.nextElementSibling
while(selectedSibling && (selectedSibling != range.endPath().elements[0].$)) {
console.log(selectedSibling, 'next')
selectedHTML.push(selectedSibling)
selectedSibling = selectedSibling.nextElementSibling
}
console.log(range.endPath().elements[0].$, 'last')
selectedHTML.push(range.endPath().elements[0].$)
}
console.log(selectedHTML, 'selected HTML Tags')

If text is selected, and dropdown option is clicked, change color of selected text

I want to make a text editor, and when you select some text in the text area, then click an option from the drop down, the selected text from the text area changes color. Unfortunately I don't know how to do this because when I try to access the drop down, the selection from the text area disappears.
jsFiddle :: http://jsfiddle.net/MatthewKosloski/a77sM/
function GetSelectedText () {
if (window.getSelection) { // all browsers, except IE before version 9
var range = window.getSelection ();
var selection = range.toString();
alert(selection);
}
}
I've been playing around with trying to change color of highlighted content within the textarea, but it seems that you cannot change the color of text within a textarea. An alternative, as has been suggested before, is to create an editable div that acts as a rich textbox. You can set the contentEditable property on the div to true in order for this to work. Here is my jsfiddle if you want to play around with it.
And here is my JS code. I changed a couple things on the HTML too so check out the jsfiddle for the full code.
function GetSelectedText (origtext, seltext, tcolor) {
//alert(origtext + ", " + seltext + ", " + tcolor);
var divcontent = document.getElementById('sec');
var spanTag = document.createElement("span");
var selIndex = origtext.indexOf(seltext);
var selLength = seltext.length;
//split the text to insert a span with a new color
var fpart = origtext.substr(0, selIndex);
var spart = origtext.substr(selIndex + selLength);
//alert(fpart + ", " + spart);
// add the text that was highlighted and set the color
spanTag.appendChild(document.createTextNode(seltext));
spanTag.style.color = tcolor;
//remove all the children of the div
while(divcontent.hasChildNodes()){
divcontent.removeChild(divcontent.lastChild);
}
//append the original text with the highlighted part
divcontent.appendChild(document.createTextNode(fpart));
divcontent.appendChild(spanTag);
divcontent.appendChild(document.createTextNode(spart));
}
// this function was found at http://stackoverflow.com/questions/275761/how-to-get-selected-text-from-textbox-control-with-javascript
function getTextFieldSelection() {
var textComponent = document.getElementById('content');
var selectElem = document.getElementById("myselect");
var selectedText;
// IE version
if (document.selection != undefined)
{
textComponent.focus();
var sel = document.selection.createRange();
selectedText = sel.text;
}
// Mozilla version
else if (textComponent.selectionStart != undefined)
{
var startPos = textComponent.selectionStart;
var endPos = textComponent.selectionEnd;
selectedText = textComponent.value.substring(startPos, endPos)
}
//alert("You selected: " + selectedText);
selectElem.onchange = GetSelectedText(textComponent.value, selectedText,selectElem.options[selectElem.selectedIndex].text.toLowerCase());
}
var content = document.getElementById("content");
var selectElem = document.getElementById("myselect");
selectElem.onfocus = function (e) { getTextFieldSelection(); };
The problem is when you click the select element it steals focus from textarea.
You have to give back the focus to textarea element, here is a working example (at least in chrome):
var color = document.getElementById("color"), // this is the select element
content = document.getElementById("content");
color.onfocus = function (){ content.focus(); };

Set caret position right after the inserted element in a contentEditable div

I'm inserting an element into a contentEditable div but the browser sets the position of the cursor before the inserted element. Is it possible to set the cursor right after the inserted element so that the user keeps typing without having to re-adjust the cursor position?
The following function will do it. DOM Level 2 Range objects make this easy in most browsers. In IE, you need to insert a marker element after the node you're inserting, move the selection to it and then remove it.
Live example: http://jsfiddle.net/timdown/4N4ZD/
Code:
function insertNodeAtCaret(node) {
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0);
range.collapse(false);
range.insertNode(node);
range = range.cloneRange();
range.selectNodeContents(node);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
} else if (typeof document.selection != "undefined" && document.selection.type != "Control") {
var html = (node.nodeType == 1) ? node.outerHTML : node.data;
var id = "marker_" + ("" + Math.random()).slice(2);
html += '<span id="' + id + '"></span>';
var textRange = document.selection.createRange();
textRange.collapse(false);
textRange.pasteHTML(html);
var markerSpan = document.getElementById(id);
textRange.moveToElementText(markerSpan);
textRange.select();
markerSpan.parentNode.removeChild(markerSpan);
}
}
Alternatively, you could use my Rangy library. The equivalent code there would be
function insertNodeAtCaret(node) {
var sel = rangy.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0);
range.collapse(false);
range.insertNode(node);
range.collapseAfter(node);
sel.setSingleRange(range);
}
}
If you're inserting an empty div, p or span, I believe there needs to be "something" inside the newly created element for the range to grab onto -- and in order to put the caret inside there.
Here's my hack that seems to work OK in Chrome. The idea is simply to put a temporary string inside the element, then remove it once the caret is in there.
// Get the selection and range
var idoc = document; // (In my case it's an iframe document)
var sel = idoc.getSelection();
var range = sel.getRangeAt(0);
// Create a node to insert
var p = idoc.createElement("p"); // Could be a div, span or whatever
// Add "something" to the node.
var temp = idoc.createTextNode("anything");
p.appendChild(temp);
// -- or --
//p.innerHTML = "anything";
// Do the magic (what rangy showed above)
range.collapse(false);
range.insertNode( p );
range = range.cloneRange();
range.selectNodeContents(p);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
// Clear the non
p.removeChild(p.firstChild);
// -- or --
//p.innerHTML = "";
Here's what worked for me, using Rangy, in a VueJS context.
// When the user clicks the button to open the popup to enter
// the URL, run this function to save the location of the user's
// selection and the selected text.
newSaveSel: function() {
if (this.savedSel) {
rangy.removeMarkers(this.savedSel);
}
// Save the location of the selected text
this.savedSel = rangy.saveSelection();
// Save the selected text
this.savedSelText = rangy.getSelection().toString();
this.showLinkPopup = true;
console.log('newSavedSel', this.savedSel);
},
surroundRange: function() {
// Restore the user's selected text. This is necessary since
// the selection is lost when the user stars entering text.
if (this.savedSel) {
rangy.restoreSelection(this.savedSel, true);
this.savedSel = null;
}
// Surround the selected text with the anchor element
var sel = rangy.getSelection();
var range = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
// Create the new anchor element
var el = document.createElement("a");
el.style.backgroundColor = "pink";
el.href = this.anchorHref;
el.innerHTML = this.savedSelText;
if (this.checked) {
el.target = "_blank";
}
// Delete the originally selected text
range.deleteContents();
// Insert the anchor tag
range.insertNode(el);
// Ensure that the caret appears at the end
sel.removeAllRanges();
range = range.cloneRange();
range.selectNode(el);
range.collapse(false);
sel.addRange(range);
this.showLinkPopup = false;
}
},

Categories