Rebuild DOM after a window.selection has been removed - javascript

I am running into an issue with highlighting selected text in a paragraph. Specifically, when the user removes the selected highlight the paragraph sometimes re-renders and sometimes it does not causing issues if/when the user tries to create a new highlight.
Here is the scenario:
The paragraph
<p>Over the last few weeks you have learned how a philosophical commitment to naturalism has led the scientific community to disallow the possibility of a supernatural explanation of the origin of our universe and the origin of life. You have also seen that this commitment is not based on scientifically observed data, and in fact, conveniently throws aside the very methodology that modern science claims to be built upon. The battle between modern science and the Bible is a battle of worldviews. It is not an argument about the facts that can be observed, but the explanations offered to account for them.</p>
The DOM from inspecting the element
The User highlights a selection:
...learned how a philosophical commitment to...
The DOM after highlight
The User removes the highlight:
...learned how a philosophical commitment to...
The DOM after the highlight is removed
Now if the User tries to highlight/unhighlight some or parts of those words again it removes the text from the DOM.
I am trying to force the DOM to refresh after the selection has been removed.
Here is my Selection method:
surroundSelection(action) {
var span = document.createElement("span")
if(action == 'highlight')
span.className = 'highlighted'
var sel = window.getSelection()
if(action == 'remove') {
let ancestor = sel.anchorNode.parentNode.parentNode
let parent = sel.anchorNode.parentElement
ancestor.insertBefore(sel.anchorNode, parent)
sel.anchorNode.remove()
}
if (window.getSelection) {
if (sel.rangeCount) {
var range = this.selectedRange.cloneRange()
if(action == 'highlight')
range.surroundContents(span)
sel.removeAllRanges()
sel.addRange(range)
}
}
this.saveHighlight(sel)
}

I found a solution. at the the saveHighlight method I call a new function that takes the ID and innerHtml of the selection and then I replace the innerHtml

Related

jQuery select matching text within element

I need a way to pass an element id and a string to a function which selects any occurrences of that string in the element text, which is not already wrapped in a sub element.
I have an existing event handler which performs some actions, including wrapping the text in an element, when a user highlights a section of text. However some strings can be matched automatically without needing to be selected by a user, which is what I am trying to accomplish here. A different event will sometimes produce an element id and a string, and all instances of the string in the element text which are not all ready tagged, should be passed to the event handler as if a user had selected them.
Example:
var selectionString = 'word';
<p id="1">select this word, but not this <span>word</span>, but yes to this word.</p>
// My approach so far which may not be on the right track.
function selectStringInElement(selectionString, elementId){
var el = document.getElementById(elementId);
var selections = el.innerHTML.split(/<.+>/);
for(var i=0; i < selections.length; i++){
// if the selection contains the selectionString, find its range, select it, and pass to existing handler.
}
}
Any help would be much appreciated.
HTML nodes have children accessible by node.childNodes, and you can tell if a child node is text or an element by node.nodeType.
This example takes the search string and the element id to search, filters it so only the text nodes are left, and then does a replace to generate a new html string. You can't just jam a HTML string into a text node, so I used a temporary div to parse the html string into nodes, which are then injected into the element's parent before removing the source text node itself.
function selectStringInElement(selectionString, elementId, className) {
$("#" + elementId).contents()
.filter(function(i, el){
// only process text nodes
return el.nodeType == 3
}).each(function(i, el){
// create a div to process our html string with new tags
var fake = document.createElement('div');
fake.innerHTML = el.textContent.replace(
new RegExp(selectionString,'g'),
"<span class='" + className + "'>" + selectionString + "</span>"
);
// take all the nodes in our div and append them to the actual element's parent
$(fake.childNodes).each(function(i, child) {
el.parentNode.insertBefore(child, el);
});
// we've now duplicated our actual element with a number of new elements, we don't need to keep the original
el.parentNode.removeChild(el);
});
}
selectStringInElement('chowder','test', 'red');
selectStringInElement('ukulele','test', 'blue');
.red {
color: #f00;
}
.blue {
color: #00f;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='test'>
The bottle of shampoo just returned from a safari, that he left the tea and the church bazaar. She was a grand ran all around the veranda, laughing like, so helping their ranch on the cook make the church bazaar. She was playing a ukulele, which the admiral did not like, so he looked through his collection of the kindergarten cried, "At last! Hurrah!" -- and by accident spilled the ketchup all over the taffy apples! This so amused another chores at the bottle had just where he had found the taffy apples! This so amused another guest, who usually wore a gingham dress and moccasins when visiting their ranch on the cook make the church bazaar. She was no helping the chowder and kayak he had seen.
Someone was playing a ukulele, which the ketchup all over the goulash for lunch. the label.
The admiral heard them talking in the label.
The admiral heard them talking in the bank and firing his pistol out of his sack and ran all around the chowder and the goulash for lunch. the Nebraska prairie, had just returned from her chores at imaginary zombies. It was a grand party.d through his collection of shampoo just returned from a safari, that he pulled the ketchup all over the church bazaar. She was no helping the church bazaar. She was no helping the admiral hated to a pretty mazurka by Chopin. Then he looked through his sack and ran all around the kitchen.
Someone was a grand party.ard them talking in the admiral did not like a maniac and firing his pistol out of his so amused another guest, who usually wore a gingham dress and listened to a pretty mazurka by Chopin. The bottle had seen.
Someone was playing a ukulele, which the admiral heard them talking in the label.
The admiral's wife, who usually wore a gingham dress and moccasins when visiting their ranch on the label.
When everyone sat down to eat, the pulled a toy pistol out of his sack and ran all around through his collection of pictures -- next to the tea and listened to snoop, so he turned on the radio and the cook make the church bazaar. She was playing a ukulele, which the admiral did not like, so amused another guest, who had just returned on the Nebraska prairie, had just returned on the Nebraska prairie, had a picture of shampoo just returned from a safari, that he pulled the radio and listened from her chowder and the label.
</div>
Here is JQuery implementation:
JSFiddle
selectStringInElement('word','1');
function selectStringInElement(selectionString, elementId)
{
var $el = $("#"+elementId);
$el.contents()
.filter(function(){
return this.nodeType === 3 // Only text nodes
})
.each(function(){
this.textContent = this.textContent.replace(new RegExp(selectionString,'g'),"<span class='highlight'>"+selectionString+"</span>");
});
// Decoding lt/gt
var html = $("<textarea/>").html($("#1").html()).val()
// Replacing original content
$el.html(html);
// Attaching onMouseOver events
console.log($el.find("span.highlight"));
$el.find("span.highlight").on("mouseover",function(){
alert($(this).text());
});
}
Result: "word" is surrounded with span tag, and onMouseOver event attached to that tags.
HTML resulting code:
<p id="1">select this <span class="highlight">word</span>, but not this <span>word</span>, but yes to this <span class="highlight">word</span>.</p>

JS - surroundContents only retains highlight on text about 20% of the highlight attempts

I am using a mouseup event to trigger a function which highlights text and surrounds the highlighted text with a span (function from stack overflow):
function highlightText(e) {
var t = window.getSelection().toString();
if (t) {
$("#mySpan").remove();
var range = window.getSelection().getRangeAt(0);
newNode = document.createElement("span");
newNode.id = 'mySpan';
range.surroundContents(newNode);
}
}
The main problem I am encountering is that as long as surroundContents is included, the text remains highlighted only about 20% of the highlight attempts (otherwise highlighting disappears immediately). I tried adding a setTimeout, not calling surroundContent for 1s. I also tried removing the remove() statement, but still no good.
Any ideas on why this is happening?
I was facing the same problem with Chromium on Android. In some specific cases, the call of range.surroundContents(newNode) would cause a very weird behaviour of page reload and so on. After checking the documentation of the function:
This method is nearly equivalent to
newNode.appendChild(range.extractContents());
range.insertNode(newNode). After surrounding, the boundary points of
the range include newNode.
So the obvious thing was to apply another way highlight the text. I found mark.js library which did exactly what I wanted without that annoying side effect. (Here's a JSFiddle sample that shows how it's used to highlight just selection). The difference is that library was not using range.surroundContents(newNode) nor newNode.appendChild but rather node.replaceChild.
Based on that, here's the solution to the problem I was having and I think it applies to your case as well.
function surroundRangeWithSpan(range) {
var span = document.createElement('span');
// The text is within the same node (no other html elements inside of it)
if (range.startContainer.isEqualNode(range.endContainer) && range.startContainer.childNodes.length == 0) {
// Here you customise your <span> element
customSurroundContents(range, span);
} else {
// Here you have to break the selection down
}
return span;
}
function customSurroundContents(range, span) {
var node = range.commonAncestorContainer;
var startNode = node.splitText(range.startOffset);
var ret = startNode.splitText(range.toString().length);
span.textContent = startNode.textContent;
startNode.parentNode.replaceChild(span, startNode);
}
And you pass window.getSelection().getRangeAt(0) to the function.
The likely cause of the failure is the selected text encompasses only the beginning or the ending of a non-text node, and not both of them.
So if were to run that code only selecting "This is Bo" in the following it will fail (and throw an exception) because it doesn't also capture the closing tag in the selection:
This is <em>bold</em>
So ending up with:
This is <em>bo
Reference: https://developer.mozilla.org/en-US/docs/Web/API/Range/surroundContents

InsertOrderedList doesn't make new list with marked text

I created a contenteditable div and added features for formatting.
One of the feature I added is creating a list from paragraph, I'll reproduce the steps below.
====
1) This is the paragraph I wanted part of it to be a list.
2) So I marked the text to make it a list
, I use plugin from https://github.com/kenshin54/popline/ (modified it myself).
And it uses
document.execCommand("InsertOrderedList", false);
for creating list
3) And then after I clicking the button, it made the whole paragraph to list. I just want the marked text to be (just for "Sed imperdiet....orci,")
The reason that I posting on Stackoverflow instead of Github repository issues (https://github.com/kenshin54/popline/) because I'm not sure that is an issue or not, I just want it to work like that.
Anyone has some suggestions?
Bun, your answer doesn't work for me, but this does:
var sel = rangy.getSelection();
var range = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
var el = document.createElement("li");
if (range.canSurroundContents(el)) {
range.surroundContents(el);
var ul = document.createElement("ul");
range.surroundContents(ul);
} else {
alert("Unable to surround range because range partially selects a non-text node..");
}
}
(taken mostly from the rangy core demo)
After long time of finding workarounds, kenshin (https://github.com/kenshin54) just saved my life.
He gave me this one http://jsfiddle.net/j83ueh6g
You can use document.execCommand('insertOrderedList', false); as the same way it works.

Javascript: execCommand("removeformat") doesn't strip h2 tag

I'm tweaking a wysiwyg editor, and I'm trying to create an icon which will strip selected text of h2.
In a previous version, the following command worked perfectly:
oRTE.document.execCommand("removeformat", false, "");
But in the current version, although that command successfully removes from selected text such tags as bold, underline, italics, it leaves the h2 tag intact.
(Interestingly enough, execCommand("formatblock"...) successfully creates the h2 tag.)
I'm thinking that I'm going to have to abandon execCommand and find another way, but I'm also thinking that it will be a lot more than just 1 line of code! Would be grateful for suggestions.
You can change your format to div, it's not the best solution but it works and it's short:
document.execCommand('formatBlock', false, 'div')
There is also this other solution to get the closest parent from selected text then you can unwrap it, note that this can be some tag like <b>:
var container = null;
if (document.selection) //for IE
container = document.selection.createRange().parentElement();
else {
var select = window.getSelection();
if (select.rangeCount > 0)
container = select.getRangeAt(0).startContainer.parentNode;
}
$(container).contents().unwrap(); //for jQuery1.4+
This is in accordance with the proposed W3C Editing APIs. It has a list of formatting elements, and the H# elements are not listed. These are considered structural, not simply formatting. It doesn't make any more sense to remove these tags than it would to remove UL or P.
I think you can use the Range object.
you can find it from Professional JavaScript for Web Developers 3rd Edition.
chapter 12(12.4) and chapter 14(14.5) ...
an example from that book:
var selection = frames["richedit"].getSelection();
var selectedText = selection.toString();
var range = selection.getRangeAt(0);
var span = frames["richedit"].document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);

javascript get id of "commonAncestorContainer"

So I feel like I'm a bit out of my league here. But here's what I want to do.
Basically, I want to have a user select part of text within in a paragraph (which may contain many elemnts (i.e. <span> and <a>) to return the value of the id attribute of that paragraph. Here's what I thinking.
function getParaID() //function will be called using a mouseUp event
{
var selObj = window.getSelection();
var selRange = selObj.getRangeAt(0); //btw can anyone explain what this zero means
var paraElement = selRange.commonAncestorContainer;
var paraID = paraElement.getAttribute;
return paraID;
}
What do you think? Am I close?
The selection range's commonAncestorContainer property may be a reference to a text node, or a <span> or <a> element or <body> element, or whatever else you may have in your page. That being the case, you need to work up the DOM tree to find the containing <p> element, if one exists. You also need to be aware that IE < 9 does not support window.getSelection() or DOM Range, although it is possible to do what you want quite easily in IE < 9. Here's some code that will work in all major browsers, including IE 6:
jsFiddle: http://jsfiddle.net/44Juf/
Code:
function getContainingP(node) {
while (node) {
if (node.nodeType == 1 && node.tagName.toLowerCase() == "p") {
return node;
}
node = node.parentNode;
}
}
function getParaID() {
var p;
if (window.getSelection) {
var selObj = window.getSelection();
if (selObj.rangeCount > 0) {
var selRange = selObj.getRangeAt(0);
p = getContainingP(selRange.commonAncestorContainer);
}
} else if (document.selection && document.selection.type != "Control") {
p = getContainingP(document.selection.createRange().parentElement());
}
return p ? p.id : null;
}
Regarding the 0 passed to getRangeAt(), that is indicating which selected range you want. Firefox supports multiple selected ranges: if you make a selection and then hold down Ctrl and make another selection, you will see you now have two discontinous ranges selected, which can be accessed via getRangeAt(0) and getRangeAt(1). Also in Firefox, selecting a column of cells in a table creates a separate range for each selected cell. The number of selected ranges can be obtained using the rangeCount property of the selection. No other major browser supports multiple selected ranges.
You're quite close. If all you want is the id of the parent element, then you should replace your paraElement.getAttribute with paraElement.id, like:
var paraID = paraElement.id;
Regarding the parameter to getRangeAt(), it is specifying the index of the selection range to return, and it's only really relevant to controls that allow discontinuous selections. For instance, a select box in which the user can use ctrl + click to select several arbitrary groups of rows simultaneously. In such a case you could use the parameter to step from one selected region to the next. But for highlighting text within a paragraph you should never have a discontinuous selection and thus can always pass 0. In essence it means that you're asking for "the first selected region".
Also note that if your interface allows the user's selection to span multiple paragraphs then your commonAncestorContainer may not be a paragraph, it might also be whatever element it is that contains all of your paragraph tags. So you should be prepared to handle that case.
Edit:
After playing with this a bit, here is my suggestion: http://jsfiddle.net/vCsZH/
Basically, instead of relying on commonAncestorContainer this code applies a mouseDown and a mouseUp listener to each paragraph (in addition to the one already applied to the top-level container). The listeners will, in essence, record the paragraphs that the selection range starts and ends at, making it much simpler to reliably work out which paragraph(s) are selected.
If ever there was a case in favor of using dynamic event binding through a framework like jQuery, this is it.

Categories

Resources