Unable to un-highlight selection in 2 different paragraphs using Rangy library - javascript

JSfiddle for reference: https://jsfiddle.net/9jp346r4/20/
I am trying to create functionality that allows user to highlight the selected text upon pressing a button, and unhighlight the highlighted text upon right-clicking.
I've gotten it mostly working using the rangy library except there's one scenario that doesn't work and I'm not sure how to solve it.
When I highlight text that is in 2 different paragraphs, it highlights it successfully.
The issue arises when I would like to come back later and un-highlight both the paragraphs.
The expected behaviour is: I right-click any highlighted text regardless of if it is selected or not and it will un-highlight all nearby highlighted text even if it's separated by a paragraph tag or strong tag.
The current behaviour is: It only unhighlights the text in the paragraph I clicked.
To re-produce:
1) Select text that overlaps both the first and second paragraph and press the "Press" button.
2) Un-select the selected text by clicking somewhere else on the screen.
3) Right-click any of the highlighted text. Notice only one of the paragraphs gets un-highlighted.
If something is unclear, feel free to ask questions. Would appreciate the help.
Here is my HTML:
<div id="content">
<p>
Paragraph 1
</p>
<p>
Paragraph 2
</p>
</div>
<div id="divId">
<input id="myBtn" type="button" value="Press" onclick = "javascript:toggleItalicYellowBg()"/>
</div>
Here is my javascript:
function coverAll() {
var ranges = [];
for(var i=0; i<window.getSelection().rangeCount; i++) {
var range = window.getSelection().getRangeAt(i);
while(range.startContainer.nodeType == 3
|| range.startContainer.childNodes.length == 1)
range.setStartBefore(range.startContainer);
while(range.endContainer.nodeType == 3
|| range.endContainer.childNodes.length == 1)
range.setEndAfter(range.endContainer);
ranges.push(range);
}
window.getSelection().removeAllRanges();
for(var i=0; i<ranges.length; i++) {
window.getSelection().addRange(ranges[i]);
}
return true;
}
function getSelectedText() {
if (window.getSelection) {
return window.getSelection().toString();
} else if (document.selection) {
return document.selection.createRange().text;
}
return '';
}
var italicYellowBgApplier;
function toggleItalicYellowBg() {
italicYellowBgApplier.toggleSelection();
}
window.onload = function() {
$(document).on("contextmenu", ".italicYellowBg", function(e){
if(coverAll()) {
italicYellowBgApplier.undoToSelection();
return false;
}
});
rangy.init();
// Enable buttons
var classApplierModule = rangy.modules.ClassApplier;
// Next line is pure paranoia: it will only return false if the browser has no support for ranges,
// selections or TextRanges. Even IE 5 would pass this test.
if (rangy.supported && classApplierModule && classApplierModule.supported) {
italicYellowBgApplier = rangy.createClassApplier("italicYellowBg", {
tagNames: ["span", "a", "b", "img"]
});
}
};

I guess to easy solve this problem, in memory keep an array of user highlights, one item of that array is not a single highlight item, but a further "selection of selected items during the highlight", when somebody would right click on a single segment of highlight, in-memory find from array all associated highlights and unhighlight the related segments by yourself.

Related

How to add all containers in a selection to an array

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.

Modify existing match code to exclude matches

I have some existing jQuery code I need to modify but it is beyond my JS capability (I was never a JS coder).
Currently I have...
jQuery(document).ready(function($) {
function doFilters() {
// Show all rows
$("tr:hidden").show();
// Chemistry
if ($("#filter_chemistry").val() != "") {
var chemFilter = $("#filter_chemistry").val().toLowerCase().split(',');
$("td.polymerTypel").each(function(){
var texts = $(this).text().split(',');
var match = chemFilter.every(function(v) { return texts.indexOf(v) != -1; });
$(this).parent().toggle(!!match);
});
}
};
});
This is not all of the filters but it is the relevant one. Basically it shows all rows in a table and then hides rows that don't match certain criteria.
The table has a hidden column with the class "polymerTypel" which could contain "st,ac" for example and then is someone puts "st" into the filter_chemistry field it will hide any row not containing st.
I need to modify this so that if someone enters "ac,-st" it will exclude those with st.
I have played with it but with literally 0 success.
A working copy is available here...
http://products.dunwood.co.uk/?range=all

Expect selected text

I can simulate text selection with protractor by a lot of different ways. But I can't find solution how I can expect that text was really selected. How I can do this without using marking?
I can simulate select text by protractor with:
DragAndDrop function (mouse)
Combination: browser.actions().sendKeys(protractor.Key.CONTROL, 'a').perform();
Combination: shift + left arrow
Elaborating Michael's comment into a working example.
In the following demo example, we are navigating to the AngularJS website, sending "testing" text to the search input on the top right, pressing left arrow 3 times to move the cursor 3 characters to the left, then pressing SHIFT + ARROW_RIGHT keyboard combination 2 times to select the next 2 characters - which is in:
Finally, we are applying the solution to get the selection text provided here and asserting:
describe("Get selection text", function () {
beforeEach(function () {
browser.get("https://angularjs.org/");
});
it("should input 'testing', select 'in' and assert it is selected", function () {
var q = element(by.name("as_q"));
// enter 'testing' and go 3 chars left
q.sendKeys("testing")
.sendKeys(protractor.Key.ARROW_LEFT)
.sendKeys(protractor.Key.ARROW_LEFT)
.sendKeys(protractor.Key.ARROW_LEFT);
// select 2 chars to the right
browser.actions()
.keyDown(protractor.Key.SHIFT)
.sendKeys(protractor.Key.ARROW_RIGHT)
.sendKeys(protractor.Key.ARROW_RIGHT)
.keyUp(protractor.Key.SHIFT)
.perform();
// get highlighted text
var highligtedText = browser.executeScript(function getSelectionText() {
var text = "";
if (window.getSelection) {
text = window.getSelection().toString();
} else if (document.selection && document.selection.type != "Control") {
text = document.selection.createRange().text;
}
return text;
});
expect(highligtedText).toEqual("in");
});
});
Hope this would help you and others coming here.
Here you might also need to verify that the text inside a particular text box is selected. You can do that by using these 2 lines below:
*document.evaluate( 'XpathOfTheElement//input', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue.selectionStart
document.evaluate( 'XpathOfTheElement//input', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue.selectionEnd*
if 'in' is selected in the string 'testing' then first line will return 4 and second line will return 6.

Append div only if it does not contain text JQuery

I am creating a social media website and am trying to make a comment textarea appear when users click on a status.
Here is what I have so far:
$(".status").click(function (){
//Check if status div they clicked on contains a textarea already
if($(this).closest('div').innerHTML.indexOf("<textarea name='status_body'>") == -1) {
//Append with textarea
var textarea = "<textarea name='status_body'></textarea>";
$(this).closest('div').append(textarea);
}
});
This was working before I put the if statement in. Of course without the if statement a textarea will be added everytime the user clicks. I would only like for a textarea to appear if there is not one aready.
Another method I tried and failed with is:
if(!$(this).closest('div:contains("<textarea")').length > 0)
You're close, just check for the existence with .length
if ($(this).closest("div").find("textarea").length > 0) {
//contains a text area!
} else {
//Doesnt!
}
Do not search for it as text. You should threat the textarea for what it is, a DOM element.
if($(this).closest('div').find("textarea[name='status_body']").length == 0) {
/*...*/
}

Get all IDs of selected/highlighted DIVs

I may be going in the completely wrong direction with what I'm trying to do, so I wanted to ask for help.
Background / Overview
I need to display a paragraph of text and allow a user to select one or more words from the paragraph and save their highlighted text to a database, for just their profile. Actually, hat selection of text will eventually be (1) stored with the highlight AND (2) linked up to another set of highlighted text from another paragraph (basically, I'm tying a phrase from one source to a reference source)
What I've tried...
I have tried to put each word of the paragraph into a DIV (and a unique ID) with each DIV set to float left, so that the display looks okay.
<style>
div { float: left}
</style>
and...using an example:
<div id="GEN_1_1">
<div id="GEN_1_1_1">In</div>
<div id="GEN_1_1_2">the</div>
<div id="GEN_1_1_3">beginning</div>
<div id="GEN_1_1_4">God</div>
<div id="GEN_1_1_5">created</div>
<div id="GEN_1_1_6">the</div>
<div id="GEN_1_1_7">heaven</div>
<div id="GEN_1_1_8">and</div>
<div id="GEN_1_1_9">the</div>
<div id="GEN_1_1_10">earth</div>.
</div>
Which looks like: In the beginning God created the heaven and the earth. (minus the bold)
So far, I have used the
window.getSelection()
function to determine/grab the words that have been highlighted.
I then tried using this:
if (window.getSelection)
{
selected_len = window.getSelection().toString().length;
if (window.getSelection().toString().length>0)
{
div_id = window.getSelection().getRangeAt(0).commonAncestorContainer.parentNode.id;
}
}
to get the ID's for each DIV selected, BUT I only get a single DIV ID returned right now.
Help Request:
Is there are slick way to get the ID for each DIV selected and put it into an Array, so that I can construct a SQL query to put it into the database (the query is easy)? The selected words could total up to several hundred, if not a thousand words, so I need to make sure the solution will work with a ton of words selected.
UPDATE: JSFIDDLE DEMO
I modified the code again. See if it works for you now.
$(document).ready(function () {
$(document).on('mouseup keyup', '.checked', function () {
console.log(window.getSelection());
if (window.getSelection().toString().length>0) {
var count = window.getSelection();
var arr = [];
$('.checked span').each(function(){
var span = $(this)[0];
var isT = window.getSelection().containsNode(span, true);
if(isT){
arr.push($(this).attr('id'));
}
});
console.log(arr,count.toString());
alert(arr);
alert(count.toString());
}
});
});
I created a fiddle for solution. Check it out here: http://jsfiddle.net/lotusgodkk/GCu2D/18/
Also, I used span instead of div for the text selection. I hope that won't be an issue for you. So the code works as you want. It will return the id of the parent span in which text is selected. You can modify it to save the ID into array or as per your needs.
$(document).ready(function () {
$(document).on('mouseup', '.checked', function () {
if (window.getSelection) {
var i = getSelectionParentElement();
console.log(i);
alert('parent selected: ' + i.id);
}
});
});
function getSelectionParentElement() {
var parent = null, selection;
if (window.getSelection) {
selection = window.getSelection();
if (selection.rangeCount) {
parent = selection.getRangeAt(0).commonAncestorContainer;
if (parent.nodeType != 1) {
parent = parent.parentNode;
}
}
} else if ((selection = document.selection) && selection.type != "Control") {
parent = selection.createRange().parentElement();
}
return parent;
}

Categories