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.
Related
I have many of the below 'k-top' div elements, with the same inner div structure, except different unique text in two places, in 'k-in' and in my checkbox id.
<div class="k-top">
<span class="k-icon k-i-expand"></span><-------------- trigger click on this if below text is found
<span class="k-checkbox-wrapper" role="presentation">
<input type="checkbox" tabindex="-1" id="unique TEXT99" class="k-checkbox">
<span class="k-checkbox-label checkbox-span"></span>
</span>
<span class="k-in">unique TEXT99</span></div><- if this text is found in k-in trigger click on elem above
I want to iterate through all my span.k-ins until I find the innerText to match contains of 'unique' for instance, then once unique is found, I want to .click(); on it's sibling element '.k-i-expand' as seen in the mark-up above. I do not want to trigger a .click(); on all .k-i-expand just the specific one that has same parent as where my 'unique text' is found.
Thus far I have tried .closest, I have also tried sibling.parent.. both return null or undefined.. Note, I am not using jQuery.
The below works successfully to click all .k-i-expand - but I need to .click() only the one where k-in innerText contains 'unique'. Ideally I'd use starts with, or contains, but I'd specify the whole word if needed i.e. unique TEXT99
let exp = document.querySelectorAll('.k-i-expand');
let i;
for (i = 0; i < exp.length; ++i) {
exp[i].click();
};
More previous attempts can be seen here: how to run a .click on elems parent sibling selector?
I created a recursive function which checks all it's Siblings until it finds one with the specified innerHTML. If it does not find one, it does nothing:
function checkSibling(node) {
if (node.innerHTML == "unique TEXT99") {
return true;
} else if (node.nextSibling) {
return checkSibling(node.nextSibling);
} else {
return false;
}
}
async function clickOnNode() {
let exp = document.querySelectorAll(".k-i-expand");
for await (const node of exp) {
const hasText = await checkSibling(node);
if (hasText) {
console.log("Result: ", hasText);
node.click();
}
}
}
clickOnNode();
I also created a codepen with the code for you to play around. I guess the innerHTML check could be improved via a Regex.
Have you tried iterating over the .k-top elements and looking into each one to find your .k-in?
const expandItemsContaining = (text) => {
// Let's get all the .k-top divs
const kTops = document.querySelectorAll('.k-top');
// And peek into each and every one of them
kTops.forEach(kTop => {
// First we check whether there is a .k-in containing your text
const kIn = kTop.querySelector('.k-in');
const shouldClick = kIn && kIn.innerText && kIn.innerText.indexOf(text) !== -1;
// And if there is one we find the .k-i-expand and click it
if (shouldClick) {
const kExpand = kTop.querySelector('.k-i-expand');
if (kExpand) {
kExpand.click();
}
}
})
}
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.
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;
}
The function below allows users to filter products by data-attributes, and accommodates filtering by multiple values simultaneously. It does this by creating an array of the values selected, and when any of the values are clicked (in this case checked/unchecked) it hides all the items and then re-shows those that match the values in the updated array.
It works correctly when filtering for one data-attribute, but when combined to filter by more than one attribute it no longer shows all results matching any of the values and instead only shows results matching all the specified values.
I've posted a fiddle which demonstrates the problem here: http://jsfiddle.net/chayacooper/WZpMh/94/ All but one of the items have the values of both data-style="V-Neck" and data-color="Black" and they should therefore remain visible if either of the filters are selected, but if another value from a different data-attribute some of the items are hidden.
$(document).ready(function () {
var selected = [];
$('#attributes-Colors *').click(function () {
var attrColor = $(this).data('color');
var $this = $(this);
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected.splice(selected.indexOf(attrColor),1);
}
else {
$this.parent().addClass("active");
selected.push(attrColor);
}
$("#content").find("*").hide();
$.each(selected, function(index,item) {
$('#content').find('[data-color *="' + item + '"]').show();
});
return false;
});
$('#attributes-Silhouettes *').click(function () {
var attrStyle = $(this).data('style');
var $this = $(this);
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected.splice(selected.indexOf(attrStyle),1);
}
else {
$this.parent().addClass("active");
selected.push(attrStyle);
}
$("#content").find("*").hide();
$.each(selected, function(index,item) {
$('#content').find('[data-style *="' + item + '"]').show();
});
return false;
});
});
Both of your handlers are updating the selected array, but only one handler executes on a click. The first one if a color was (de)selected, the second if a style. Let's say you've clicked on "Black" and "Crew Neck". At that time your selected array would look like this: [ "Black", "Crew_Neck" ]. The next time you make a selection, let's say you click "Short Sleeves", the second (style) handler executes. Here's what is happening:
Short_Sleeves gets added to the selected array.
All of the items are hidden using $("#content").find("*").hide();
The selected array is iterated and items are shown again based on a dynamic selector.
Number 3 is the problem. In the above example, a style was clicked so the style handler is executing. Any items in the selected array that are colors will fail because, for example, no elements will be found with a selector such as $('#content').find('[data-style *="Black"]').show();.
I would suggest 2 things.
Keep 2 arrays of selections, one for color, one for style.
Combine your code to use only a single handler for both groups.
Here's a (mostly) working example.
Note that I added a data-type="color|style" to your .filterOptions containers to allow for combining to use a single handler and still know which group was changed.
Here's the full script:
$(document).ready(function () {
// use 2 arrays so the combined handler uses correct group
var selected = { color: [], style: [] };
// code was similar enough to combine to 1 handler for both groups
$('.filterOptions').on("click", "a", function (e) {
// figure out which group...
var type = $(e.delegateTarget).data("type");
var $this = $(this);
// ...and the value of the checkbox checked
var attrValue = $this.data(type);
// same as before but using 'type' to access the correct array
if ($this.parent().hasClass("active")) {
$this.parent().removeClass("active");
selected[type].splice(selected[type].indexOf(attrValue),1);
}
else {
$this.parent().addClass("active");
selected[type].push(attrValue);
}
// also showing all again if no more boxes are checked
if (attrValue == 'All' || $(".active", ".filterOptions").length == 0) {
$('#content').find('*').show();
}
else {
// hide 'em all
$("#content").find("*").hide();
// go through both style and color arrays
for (var key in selected) {
// and show any that have been checked
$.each(selected[key], function(index,item) {
$('#content').find('[data-' + key + ' *="' + item + '"]').show();
});
}
}
});
});
UPDATE: incorporating suggestions from comments
To make the handler work with checkboxes instead of links was a small change to the event binding code. It now uses the change method instead of click and listens for :checkbox elements instead of a:
$('.filterOptions').on("change", ":checkbox", function (e) {
// handler code
});
The "All" options "hiccup" was a little harder to fix than I thought it would be. Here's what I ended up with:
// get a jQuery object with all the options the user selected
var checked = $(":checked", ".filterOptions");
// show all of the available options if...
if (checked.length == 0 // ...no boxes are checked
|| // ...or...
checked.filter(".all").length > 0) // ...at least one "All" box is checked...
{
// remainder of code, including else block, unchanged
}
I also added an all class to the appropriate checkbox elements to simplify the above conditional.
Updated Fiddle
I'm looking for a way to gather all of the text in a jQuery wrapped set, but I need to create spaces between sibling nodes that have no text nodes between them.
For example, consider this HTML:
<div>
<ul>
<li>List item #1.</li><li>List item #2.</li><li>List item #3.</li>
</ul>
</div>
If I simply use jQuery's text() method to gather the text content of the <div>, like such:
var $div = $('div'), text = $div.text().trim();
alert(text);
that produces the following text:
List item #1.List item #2.List item #3.
because there is no whitespace between each <li> element. What I'm actually looking for is this (note the single space between each sentence):
List item #1. List item #3. List item #3.
This suggest to me that I need to traverse the DOM nodes in the wrapped set, appending the text for each to a string, followed by a space. I tried the following code:
var $div = $('div'), text = '';
$div.find('*').each(function() {
text += $(this).text().trim() + ' ';
});
alert(text);
but this produced the following text:
This is list item #1.This is list item #2.This is list item #3. This is list item #1. This is list item #2. This is list item #3.
I assume this is because I'm iterating through every descendant of <div> and appending the text, so I'm getting the text nodes within both <ul> and each of its <li> children, leading to duplicated text.
I think I could probably find/write a plain JavaScript function to recursively walk the DOM of the wrapped set, gathering and appending text nodes - but is there a simpler way to do this using jQuery? Cross-browser consistency is very important.
Thanks for any help!
jQuery deals mostly with elements, its text-node powers are relatively weak. You can get a list of all children with contents(), but you'd still have to walk it checking types, so that's really no different from just using plain DOM childNodes. There is no method to recursively get text nodes so you would have to write something yourself, eg. something like:
function collectTextNodes(element, texts) {
for (var child= element.firstChild; child!==null; child= child.nextSibling) {
if (child.nodeType===3)
texts.push(child);
else if (child.nodeType===1)
collectTextNodes(child, texts);
}
}
function getTextWithSpaces(element) {
var texts= [];
collectTextNodes(element, texts);
for (var i= texts.length; i-->0;)
texts[i]= texts[i].data;
return texts.join(' ');
}
This is the simplest solution I could think of:
$("body").find("*").contents().filter(function(){return this.nodeType!==1;});
You can use the jQuery contents() method to get all nodes (including text nodes), then filter down your set to only the text nodes.
$("body").find("*").contents().filter(function(){return this.nodeType!==1;});
From there you can create whatever structure you need.
I built on #bobince's terrific answer to make search tool that would search all columns of a table and filter the rows to show only those that matched (case-insensitively) all of a user's search terms (provided in any order).
Here is a screenshot example:
And here is my javascript/jQuery code:
$(function orderFilter() {
// recursively collect all text from child elements (returns void)
function collectTextNodes(element, texts) {
for (
let child = element.firstChild;
child !== null;
child = child.nextSibling
) {
if (child.nodeType === Node.TEXT_NODE) {
texts.push(child);
} else if (child.nodeType === Node.ELEMENT_NODE) {
collectTextNodes(child, texts);
}
}
}
// separate all text from all children with single space
function getAllText(element) {
const texts = [];
collectTextNodes(element, texts);
for (let i = texts.length; i-- > 0; ) texts[i] = texts[i].data;
return texts.join(' ').replace(/\s\s+/g, ' ');
}
// check to see if the search value appears anywhere in child text nodes
function textMatchesFilter(tbody, searchVal) {
const tbodyText = getAllText(tbody).toLowerCase();
const terms = searchVal.toLowerCase().replace(/\s\s+/g, ' ').split(' ');
return terms.every(searchTerm => tbodyText.includes(searchTerm));
}
// filter orders to only show those matching certain fields
$(document).on('keyup search', 'input.js-filter-orders', evt => {
const searchVal = $(evt.target).val();
const $ordersTable = $('table.js-filterable-table');
$ordersTable.find('tbody[hidden]').removeAttr('hidden');
if (searchVal.length <= 1) return;
// Auto-click the "Show more orders" button and reveal any collapsed rows
$ordersTable
.find('tfoot a.show-hide-link.collapsed, tbody.rotate-chevron.collapsed')
.each((_idx, clickToShowMore) => {
clickToShowMore.click();
});
// Set all tbodies to be hidden, then unhide those that match
$ordersTable
.find('tbody')
.attr('hidden', '')
.filter((_idx, tbody) => textMatchesFilter(tbody, searchVal))
.removeAttr('hidden');
});
});
For our purposes, it works perfectly! Hope this helps others!