I have a scenario/dilemma, I am a little stuck trying to figure out. Below will be my best of a visual representation.
1. I select
This is dummy text to select
2. I bold and it gets wrapped with a span
This is dummy text to select
<h2 This is dummy <span style="font-weight: 700;">text to select</span></h2>
3. Now lets say I want to select some text but we know it's already wrapped in a span
This is dummy [BOLD]text[BOLD] to select
4. Its a crossover with selected text, so now my code. In my important comment, I can identify that the element is found in the selection. But how can I find it and then remove the span.
const el = document.createElement('span');
function handleNodeWrap(){
if(selection?.containsNode(el,true)){
//! IMPORTANT find and remove element of selected text
}
surroundSelection(el);
}
Here's one approach to remove the span from the selected text:
const el = document.createElement('span');
function handleNodeWrap() {
if (selection?.containsNode(el, true)) {
// find parent node of the selected text
let parentNode = selection.anchorNode.parentNode;
// create a new range that selects the entire content of the parent node
let range = document.createRange();
range.selectNodeContents(parentNode);
// replace the content of the parent node with the plain text version
let text = range.extractContents().textContent;
parentNode.textContent = text;
} else {
surroundSelection(el);
}
}
Related
I am trying to get all the containers in a selection and add them into an array. So far, I have been able to get only the first container using the following code:
function getSelectedNode()
{
var containers = [];//need to add containers here so we can later loop on it and do the transformations
if (document.selection)
return document.selection.createRange().parentElement();
else
{
var selection = window.getSelection();
if (selection.rangeCount > 0)
return selection.getRangeAt(0).startContainer.parentNode;
}
}
So if I had:
<p>
<b>Here's some more content</b>.
<span style="background-color: #ffcccc">Highlight some</span>
and press the button. Press the other button to remove all highlights
</p>
and I selected this part of the text:
"Here's some more content Highlight"
Once I use the container returned by getSelectedNode() and do some transformation on it only "Here's some more content" gets affected correctly and not "Highlight". So is there a way to make it get all containers and not just the first one?
Note: I was also previously looking at this link:
How can I get the DOM element which contains the current selection?
and someone even commented:
"This solution doesn't work for all cases. If you try to select more than one tag, all the subsequent tags except the first one will be ignored."
Use Range.commonAncestorContainer and Selection.containsNode:
function getSelectedNode()
{
var containers = [];//need to add containers here so we can later loop on it and do the transformations
if (document.selection)
return document.selection.createRange().parentElement();
else
{
var selection = window.getSelection();
if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
if (range.startContainer === range.endContainer) {
containers.push(range.startContainer);
} else {
var children = range.commonAncestorContainer.children;
containers = Array.from(children || []).filter(node => selection.containsNode(node, true));
}
}
}
return containers;
}
In your case, all possible "containers" are siblings that have no children, and we are selecting using a mouse or keyboard. In this case, we only have to consider two possibilities: you've selected a single node, or you've selected sibling nodes.
However, if your HTML were more complicated and you considered the possibility of scripts creating multiple selections, we'd have need a different solution. You would have to go through each node in the DOM, looking for ones that were part of something selection.
Maybee i am blondie and old school but if i have to fill a array i use a for next loop and something called push to fill the array. That might not be cool but usually works. I can not see any loop or pushing. So there will be only one element.`
other code
...
if (selection.rangeCount > 0)
for (var i;i<selection.rangeCount;i++){
var x= selection.getRangeAt(i).startContainer.parentNode ; //make a var
containers.push(x);//push var to array
}
return containers ;
}`
It seems that you wan't to unhighlight the selected text, it seems easer to go through the highlighted portions and see if they are part of the selection, here is an example:
document.addEventListener('mouseup', event => {
const sel = document.getSelection();
if (!sel.isCollapsed) {
const elms = [...document.querySelectorAll('.highlighted')];
const selectedElms = elms.filter(e => sel.containsNode(e, true));
if (selectedElms.length) {
selectedElms.forEach(e => {
let prev = e.nextSibling;
[...e.childNodes].forEach(child => e.parentElement.insertBefore(child, e));
e.remove();
});
sel.empty();
}
}
});
.highlighted {
background-color: #ffcccc
}
<p>
<b>Here's <span class="highlighted">Highlight <b>some</b></span> some more content</b>.
<span class="highlighted">Highlight some</span>
and press the button. Press the <span class="highlighted">Highlight some</span> other button to remove all highlights
</p>
Because I've used true as the second parameter to containsNode(...), this example will unhighlight the elements that are only partially selected.
I have a use case where a user can select some text in a p element and I need to toggle 'bold' it - ie, if it's NOT bold, make the text bold and vice-versa. Is there a way to determine the node type/name from the selected text?
For example: in the below example, when the user selects 'Foo' I want to know that a span has been selected. If the user selects 'Bar'; I want to know that a p was selected. If the user selects 'Foo B'; I want to know that a p was selected.
$('#toggle-bold').click(function() {
// The following will always add a new span wrapping the selected text then make it bold
var selection= window.getSelection().getRangeAt(0);
// Anyway to determine the node type/name of the selected text?
var selectedText = selection.extractContents();
var span= document.createElement("span");
span.appendChild(selectedText);
selection.insertNode(span);
$(span).css('font-weight', 'bold');
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p contenteditable="true">
<span>Foo</span> Bar
</p>
<button id="toggle-bold">Toggle Bold</button>
The Range object has two properties for this: startContainer (the node where the range starts) and endContainer (the node where it ends). Then you can get the node name via the nodeName property. (You may or may not want to use parentNode if the node in question is a Text node.) So for instance, since your selection variable actually references a Range (not a Selection):
console.log("Start node's nodeName: " + selection.startContainer.nodeName);
console.log("End node's nodeName: " + selection.endContainer.nodeName);
Often, again, those will show "#text" because the range may be in a Text node.
$('#toggle-bold').click(function() {
// The following will always add a new span wrapping the selected text then make it bold
var selection = window.getSelection().getRangeAt(0);
// Anyway to determine the node type/name of the selected text?
console.log("Start node's nodeName: " + selection.startContainer.nodeName);
console.log("End node's nodeName: " + selection.endContainer.nodeName);
var selectedText = selection.extractContents();
var span = document.createElement("span");
span.appendChild(selectedText);
selection.insertNode(span);
$(span).css('font-weight', 'bold');
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p contenteditable="true">
<span>Foo</span> Bar
</p>
<button id="toggle-bold">Toggle Bold</button>
Range also tells you where in those container elements the range starts and ends (via startOffset and endOffset).
I'm building a keyboard access library for my web app. To hint at what keys should be pressed i want to underline the key in links. E.g.
Orders --- Invoices --- Open Incidents
For pure text snippets this is easy (jQuery used here):
part.html(part.html().replace(new RegExp(key, 'i'), '<u>$&</u>'));
But this breaks horribly if there is any html markup inside part. Is there an elegant way to just update text nodes and never markup?
Clarification: My use case is hundreds of nested templates server side generating the HTML. Currently accesskey attributes are added by hand. Resulting in something like <i class="fa fa-fw fa-podcast"></i>Ange<b>bote</b>. A Javascript Front-End Script then among other things adds keybindings and trying to underline the bound keys.
To get to the text nodes only, and make the replacement there, you could use a tree walker. I would also create a function that isolates the found letter in a span element and returns that. Then you can use jQuery (or whatever methods) to decorate that element as you wish:
function getLetter(elem, letter){
if (elem.get) elem = elem.get(0); // remove jQuery wrapper
var nodes = document.createTreeWalker(elem, NodeFilter.SHOW_TEXT, null, null),
$node, text, pos, $span;
letter = letter.toLowerCase();
while ($node = $(nodes.nextNode())) {
text = $node.text();
pos = text.toLowerCase().indexOf(letter);
if (pos > -1) {
$span = $('<span>').text(text[pos]);
$node.replaceWith( // separate the letter from the other text
text.substr(0, pos),
$span,
text.substr(pos+1)
);
return $span; // return the element that just has that one letter
}
}
}
getLetter($('#orders'), 'o').addClass('underline');
getLetter($('#invoices'), 'i').addClass('underline');
getLetter($('#incidents'), 'n').addClass('underline');
.underline { text-decoration: underline }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3>Some mix of HTML:</h3>
<div id="orders">
<span style="color: purple">Order<i>(s)</i></span>
</div>
<div id="invoices">
<ul><li><b>!!</b>Invoices <i>(urgent)</i></li></ul>
</div>
<div id="incidents">
<table style="border: 1px solid"><tr><td>Open</td><td>incidents</td></tr></table>
</div>
Explanation of the Function
The function takes two arguments:
elem: the container element which has the text that needs to be searched. This element may or may not have nested HTML mark up.
letter: the letter to identify within that element: only the first occurrence of that letter will play a role.
The function returns:
undefined when there is no match, i.e. the provided letter does not occur in any text contained by the provided element.
A new span element that the function has created: it will contain the letter that was found. The remaining text is put in separate text nodes before and after that new element.
For single letters it should be trivial with the range API, since they won't be split over multiple nested nodes.
let range = document.createRange();
let div = document.createElement("div")
div.textContent = "Orders --- Invoices --- Open Incidents"
let textNode = div.firstChild
let underline = document.createElement("u")
range.setStart(textNode, 0);
range.setEnd(textNode, 1);
range.surroundContents(underline);
console.log(div.outerHTML)
// <div><u>O</u>rders --- Invoices --- Open Incidents</div>
[].forEach.call(document.getElementsByTagName('a'),function(el){
el.innerHTML=el.innerHTML.replace(el.getAttribute('acceskey'),
'<u>'+el.getAttribute('acceskey')+'</u>')
})
Try it here: JS FIddle
I am trying to wrap the intro/help text in html document using jQuery.It is not inside any tag but between two closed html tags.
Please see attached code snippet for example. the 2nd end tag can also be other than <p>.
var txtHelp = jQuery('b.page-title').nextUntil('p').text();
console.log(txtHelp);
//jQuery('b.page-title').nextUntil('p').text().wrap("<p />");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<b class="page-title"><h4>System Log</h4><hr class="text-primary"></b>
How to select this text and wrap it in new P-Tag
<p align="center">This can by any html tag</p>
The nextUntil() method not selects textnodes.
You can get the text node by nextSibling property of node and get text content by textContent property of text node.
var txtHelp = jQuery('b.page-title')[0] // get the dom object
.nextSibling // get the text node next to it
.textContent; // get text content
console.log(txtHelp);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<b class="page-title"><h4>System Log</h4><hr class="text-primary"></b>
How to select this text and wrap it in new P-Tag
<p align="center">This can by any html tag</p>
UPDATE 1 : If you want to wrap the element by a p tag then do it like.
$( // wrap by jquery to convert to jQuery object
$('b.page-title')[0] // get the dom element also you can use `get(0)`
.nextSibling // get the textnode which is next to it
).wrap('<p/>'); // wrap the element by p tag
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<b class="page-title"><h4>System Log</h4><hr class="text-primary"></b>
How to select this text and wrap it in new P-Tag
<p align="center">This can by any html tag</p>
UPDATE 2 : If it contains br tag and you want to include it as a text then do something tricky using contents() method.
var get = false;
$($('b.page-title')
.parent() // get it's parent
.contents() // get all children node including text node
.filter(function() {
if ($(this).is('b.page-title')) {
get = true; // if element is 'b.page-title' then set flag true , we need to get element from here
return false // return false that we don't need the 'b.page-title'
}
if ($(this).is('p')) // check element is `p`, that we need to get element uptop tag
get = false; // update flag
return get; // return the flag for filtering
})).wrapAll('<p/>'); // use wrapAll to wrap all elements withing single tag
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<b class="page-title"><h4>System Log</h4><hr class="text-primary"></b>
How to select this text
<br/>and wrap it in new P-Tag
<p align="center">This can by any html tag</p>
For a pure jQuery approach, you can try this:
var contents = $('b.page-title').contents(),
textNodes = contents.filter(function() { return this.nodeType === 3; });
console.log(textNodes[0].textContent);
See contents()
How do I write a JS function to remove the span on click and just retain the inner text ?
<span class="test" onclick="removespan(this);">Data in red</span>
removespan = function(e){
alert(e.innerText);
}
CSS : span.test{color:red;}
onclick I would like to remove the span and just retain the inner text .
Infact I would like to have a onmodify event ...that removes the span .
The purpose is to remove a spellchecker span class in WYSIWYG editor .
If the span is the only child element inside its parent node
<div>
<span class="test" onclick="removespan(this);">Data in red</span>
</div>
removespan = function(span) {
span.parentNode.innerHTML = span.innerHTML;
}
Otherwise use this function
removespan = function(span) {
span.parentNode.replaceChild(document.createTextNode(span.innerHTML), span);
}
In case anyone is interested in an "expanded" version of Diode's second option:
function removespan(span) {
// Get the text contents of the clicked span
var span_contents = span.innerHTML;
// Get the parent node of the clicked span
var span_parent = span.parentNode;
// Create a new text node in the DOM to hold the text contents
var text_node = document.createTextNode(span.innerHTML);
// Replace the clicked span node with your newly created text node
span_parent.replaceChild(text_node, span);
// Alternatively, do the above in one line...
// span.parentNode.replaceChild(document.createTextNode(span.innerHTML), span);
}
<span id="test"></span>
y=doc.getElementsById("test");
y.getParent().removeChild(y);