How can I dynamically highlight strings on a web page? - javascript

I want to create pages with urls such as:
http://xyzcorp/schedules/2015Aug24_Aug28/Jim_Hawkins
http://xyzcorp/schedules/2015Aug24_Aug28/Billy_Bones
http://xyzcorp/schedules/2015Aug24_Aug28/John_Silver
These particular URLs would all contain the exact same content (the "2015Aug24_Aug28" page), but would highlight all instances of the name tagged on to the end. For example, "http://xyzcorp/schedules/2015Aug24_Aug28/Billy_Bones" would show every instance of the name "Billy Bones" highlighted, as if a "Find" for that name was executed on the page via the browser.
I imagine something like this is required, client-side:
var employee = getLastURLPortion(); // return "Billy_Bones" (or whatever)
employee = humanifyTheName(employee); // replaces underscores with spaces, so that it's "Billy Bones" (etc.)
Highlight(employee); // this I have no clue how to do
Can this be done in HTML/CSS, or is JavaScript or jQuery also required for this?

If you call the function
highlight(employee);
this is what that function would look like in ECMAScript 2018+:
function highlight(employee){
Array.from(document.querySelectorAll("body, body *:not(script):not(style):not(noscript)"))
.flatMap(({childNodes}) => [...childNodes])
.filter(({nodeType, textContent}) => nodeType === document.TEXT_NODE && textContent.includes(employee))
.forEach((textNode) => textNode.replaceWith(...textNode.textContent.split(employee).flatMap((part) => [
document.createTextNode(part),
Object.assign(document.createElement("mark"), {
textContent: employee
})
])
.slice(0, -1))); // The above flatMap creates a [text, employeeName, text, employeeName, text, employeeName]-pattern. We need to remove the last superfluous employeeName.
}
And this is an ECMAScript 5.1 version:
function highlight(employee){
Array.prototype.slice.call(document.querySelectorAll("body, body *:not(script):not(style):not(noscript)")) // First, get all regular elements under the `<body>` element
.map(function(elem){
return Array.prototype.slice.call(elem.childNodes); // Then extract their child nodes and convert them to an array.
})
.reduce(function(nodesA, nodesB){
return nodesA.concat(nodesB); // Flatten each array into a single array
})
.filter(function(node){
return node.nodeType === document.TEXT_NODE && node.textContent.indexOf(employee) > -1; // Filter only text nodes that contain the employee’s name.
})
.forEach(function(node){
var nextNode = node.nextSibling, // Remember the next node if it exists
parent = node.parentNode, // Remember the parent node
content = node.textContent, // Remember the content
newNodes = []; // Create empty array for new highlighted content
node.parentNode.removeChild(node); // Remove it for now.
content.split(employee).forEach(function(part, i, arr){ // Find each occurrence of the employee’s name
newNodes.push(document.createTextNode(part)); // Create text nodes for everything around it
if(i < arr.length - 1){
newNodes.push(document.createElement("mark")); // Create mark element nodes for each occurrence of the employee’s name
newNodes[newNodes.length - 1].innerHTML = employee;
// newNodes[newNodes.length - 1].setAttribute("class", "highlighted");
}
});
newNodes.forEach(function(n){ // Append or insert everything back into place
if(nextNode){
parent.insertBefore(n, nextNode);
}
else{
parent.appendChild(n);
}
});
});
}
The major benefit of replacing individual text nodes is that event listeners don’t get lost. The site remains intact, only the text changes.
Instead of the mark element you can also use a span and uncomment the line with the class attribute and specify that in CSS.
This is an example where I used this function and a subsequent highlight("Text"); on the MDN page for Text nodes:
(The one occurrence that isn’t highlighted is an SVG node beyond an <iframe>).

I used the following regex to replace all the matching url to create anchors with highlighted text:
(http://xyzcorp/schedules/(.*?)/)(.*?)( |<|\n|\r|$)
Debuggex Demo
The following code will replace all plain urls. If you don't need them to be replaced to links, just highlight them, remove the tags:
var str = "http://xyzcorp/schedules/2015Aug24_Aug28/Jim_Hawkins http://xyzcorp/schedules/2015Aug24_Aug28/Billy_Bones http://xyzcorp/schedules/2015Aug24_Aug28/John_Silver ";
var highlighted = str.replace( new RegExp("(http://xyzcorp/schedules/(.*?)/)(.*?)( |<|\n|\r|$)","g"), "<a href='$1$3'>$1<span style='background-color: #d0d0d0'>$3</span></a>" );
The content of the highlighted string will be:
<a href='http://xyzcorp/schedules/2015Aug24_Aug28/Jim_Hawkins'>http://xyzcorp/schedules/2015Aug24_Aug28/<span style='background-color: #d0d0d0'>Jim_Hawkins</span></a>
<a href='http://xyzcorp/schedules/2015Aug24_Aug28/Billy_Bones'>http://xyzcorp/schedules/2015Aug24_Aug28/<span style='background-color: #d0d0d0'>Billy_Bones</span></a>
<a href='http://xyzcorp/schedules/2015Aug24_Aug28/John_Silver'>http://xyzcorp/schedules/2015Aug24_Aug28/<span style='background-color: #d0d0d0'>John_Silver</span></a>
UPDATE:
This function will replace the matching names from the input text:
function highlight_names( html_in )
{
var name = location.href.split("/").pop().replace("_"," ");
return html_in.replace( new RegExp( "("+name+")", "g"), "<span style='background-color: #d0d0d0'>$1</span>" );
}

One solution would be, after window is loaded, to traverse all nodes recursively and wrap search terms in text nodes with a highlight class. This way, original structure and event subscriptions are not preserved.
(Here, using jquery, but could be done without):
Javascript:
$(function() {
// get term from url
var term = window.location.href.match(/\/(\w+)\/?$/)[1].replace('_', ' ');
// search regexp
var re = new RegExp('(' + term + ')', 'gi');
// recursive function
function highlightTerm(elem) {
var contents = $(elem).contents();
if(contents.length > 0) {
contents.each(function() {
highlightTerm(this);
});
} else {
// text nodes
if(elem.nodeType === 3) {
var $elem = $(elem);
var text = $elem.text();
if(re.test(text)) {
$elem.wrap("<span/>").parent().html(text.replace(re, '<span class="highlight">$1</span>'));
}
}
}
}
highlightTerm(document.body);
});
CSS:
.highlight {
background-color: yellow;
}
$(function() {
// get term from url
//var term = window.location.href.match(/\/(\w+)\/?$/)[1].replace('_', ' ');
var term = 'http://xyzcorp/schedules/2015Aug24_Aug28/Billy_Bones/'.match(/\/(\w+)\/?$/)[1].replace('_', ' ');
// search regexp
var re = new RegExp('(' + term + ')', 'gi');
// recursive function
function highlightTerm(elem) {
var contents = $(elem).contents();
if(contents.length > 0) {
contents.each(function() {
highlightTerm(this);
});
} else {
// text nodes
if(elem.nodeType === 3) {
var $elem = $(elem);
var text = $elem.text();
if(re.test(text)) {
$elem.wrap("<span/>").parent().html(text.replace(re, '<span class="highlight">$1</span>'));
}
}
}
}
highlightTerm(document.body);
});
.highlight {
background-color: yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div>
<div class="post-text" itemprop="text">
<p>I want to create pages with urls such as:</p>
<pre style="" class="default prettyprint prettyprinted">
<code>
<span class="pln">http</span>
<span class="pun">:</span>
<span class="com">//xyzcorp/schedules/2015Aug24_Aug28/Jim_Hawkins</span>
<span class="pln">
http</span>
<span class="pun">:</span>
<span class="com">//xyzcorp/schedules/2015Aug24_Aug28/Billy_Bones</span>
<span class="pln">
http</span>
<span class="pun">:</span>
<span class="com">//xyzcorp/schedules/2015Aug24_Aug28/John_Silver</span>
</code>
</pre>
<p>These particular URLs would all contain the exact same content (the "2015Aug24_Aug28" page), but would highlight all instances of the name tagged on to the end. For example, " <code>http://xyzcorp/schedules/2015Aug24_Aug28/Billy_Bones</code>
" would show every instance of the name "Billy Bones" highlighted, as if a "Find" for that name was executed on the page via the browser.</p>
<p>I imagine something like this is required, client-side:</p>
<pre style="" class="default prettyprint prettyprinted">
<code>
<span class="kwd">var</span>
<span class="pln"> employee </span>
<span class="pun">=</span>
<span class="pln"> getLastURLPortion</span>
<span class="pun">();</span>
<span class="pln"></span>
<span class="com">// return "Billy_Bones" (or whatever)</span>
<span class="pln">
employee </span>
<span class="pun">=</span>
<span class="pln"> humanifyTheName</span>
<span class="pun">(</span>
<span class="pln">employee</span>
<span class="pun">);</span>
<span class="pln"></span>
<span class="com">// replaces underscores with spaces, so that it's "Billy Bones" (etc.)</span>
<span class="pln"></span>
<span class="typ">Highlight</span>
<span class="pun">(</span>
<span class="pln">employee</span>
<span class="pun">);</span>
<span class="pln"></span>
<span class="com">// this I have no clue how to do</span>
</code>
</pre>
<p>Can this be done in HTML/CSS, or is JavaScript or jQuery also required for this?</p>
</div>
Demo: http://plnkr.co/edit/rhfqzWThLTu9ccBb1Amy?p=preview

Related

How to pick out span tag that doesn't have a class

Using document.getElementsByClassName("span3 pickItem").outerHTML) I set a variable htmlData to contain:
<div itemscope="" class="span3 pickItem">
<p itemprop="name" class="name">
<a href="/user/view?id=4943">
<span>John Doe</span>
<br />
<span>'Arizona'</span>
<br />
<span>'Student'</span>
</a>
</p>
</div>
How can I pick each value from the span tag and console.log them as such:
console.log(...span[0]) output: John Doe
console.log(...span[1]) output: Arizona
console.log(...span[2]) output: Student
Could do something like this
let namesArr = [];
let name = document.querySelectorAll("span");
name.forEach(function(names) {
namesArr.push(names.innerHTML);//Stores all names in array so you can access later
});
console.log(namesArr[0]);
console.log(namesArr[1]);
console.log(namesArr[2]);
Something like this should work:
// console log all names
const items = document.querySelectorAll('div.pickItem span')
items.forEach(item => {
console.log(item.innerText)
})
// console each from array index
console.log(items[0].innerText)
console.log(items[1].innerText)
console.log(items[2].innerText)
let spans = document.getElementsByTagName('span');
console.log(spans[0].innerHTML); //'Joe Doe'
You don't even need the htmlData variable because the DOM elements already exist. If you want to learn about parsing a string of HTML (this is what your htmlData variable has in it) into DOM elements, you can reivew DOMParser.parseFromString() - Web APIs | MDN.
Select the anchor
Select its child spans and map their textContent properties
function getTextFromSpan (span) {
// Just return the text as-is:
// return span.textContent?.trim() ?? '';
// Or, you can also remove the single quotes from the text value if they exist:
const text = span.textContent?.trim() ?? '';
const singleQuote = `'`;
const hasQuotes = text.startsWith(singleQuote) && text.endsWith(singleQuote);
return hasQuotes ? text.slice(1, -1) : text;
}
const anchor = document.querySelector('div.span3.pickItem > p.name > a');
const spanTexts = [...anchor.querySelectorAll(':scope > span')].map(getTextFromSpan);
for (const text of spanTexts) {
console.log(text);
}
<div itemscope="" class="span3 pickItem">
<p itemprop="name" class="name">
<a href="/user/view?id=4943">
<span>John Doe</span>
<br />
<span>'Arizona'</span>
<br />
<span>'Student'</span>
</a>
</p>
</div>

cheerio: Get normal + text nodes

I am using cheerio to parse HTML code in different nodes. I can easily do $("*"), but this gets me only normal HTML nodes and not the separate text nodes. Lets consider 3 user inputs:
One:
text only
I need: single text node.
Two:
<div>
text 1
<div>
inner text
</div>
text 2
</div>
I need: text node + div node + text node in same sequence.
Three:
<div>
<div>
inner text 1
<div>
inner text 2
</div>
</div>
<div>
inner text 3
</div>
</div>
I need: 2 div nodes
Possible?
In hope to help someone, filter function seems to return text nodes also.
I got help from this answer: https://stackoverflow.com/a/6520267/3800042
var $ = cheerio.load(tree);
var iterate = function(node, level) {
if (typeof level === "undefined") level = "--";
var list = $(node).contents().filter(function() { return true; });
for (var i=0; i<=list.length-1; i++) {
var item = list[i];
console.log(level, "(" + i + ")", item.type, $(item).text());
iterate(item, level + "--");
}
}
iterate($.root());
HTML input
<div>
text 1
<div>
inner text
</div>
text 2
</div>
Result
-- (0) tag
text 1
inner text
text 2
---- (0) text
text 1
---- (1) tag
inner text
------ (0) text
inner text
---- (2) text
text 2
I hope the following codes can help you.
const cheerio = require("cheerio");
const htmlText = `<ul id="fruits">
<!--This is a comment.-->
<li class="apple">Apple</li>
Peach
<li class="orange">Orange</li>
<li class="pear">Pear</li>
</ul>`;
const $ = cheerio.load(htmlText);
const contents = $('ul#fruits').contents();
console.log(contents.length);// 9, since nodes like '\n' are included
console.log(new RegExp('^\\s*$').test('\n '));
function isWhitespaceTextNode(node){
if(node.type !== 'text'){
return false;
}
if(new RegExp('^\\s*$').test(node.data)){
return true;
}
return false;
}
//Note here: filter is a function provided by cheerio, not Array.filter
const nonWhitespaceTextContents = contents.filter(nodeIndex=>{
const node = contents[nodeIndex];
if(isWhitespaceTextNode(node)){
return false;
}else{
return true;
}
});
console.log(nonWhitespaceTextContents.length);// 5, since nodes like '\n ' are excluded
nonWhitespaceTextContents.each((_, node)=>console.log(node));
//[comment node]
//[li node] apple
//[text node] peach
//[li node] orange
//[li node] pear
If you want all of the immediate children of a node, both text nodes and tag nodes, use .contents() and filter out whitespace-only text nodes.
Here's the code running on your examples:
const cheerio = require("cheerio"); // 1.0.0-rc.12
const tests = [
// added a div container to make the parent selector consistent
`<div>text only</div>`,
`<div>
text 1
<div>
inner text
</div>
text 2
</div>`,
`<div>
<div>
inner text 1
<div>
inner text 2
</div>
</div>
<div>
inner text 3
</div>
</div>`
];
tests.forEach(html => {
const $ = cheerio.load(html);
const result = [...$("div").first().contents()]
.filter(e => e.type !== "text" || $(e).text().trim())
// the following is purely for display purposes
.map(e => e.type === "text" ? $(e).text().trim() : e.tagName);
console.log(result);
});
Output:
[ 'text only' ]
[ 'text 1', 'div', 'text 2' ]
[ 'div', 'div' ]
If you only want the text nodes and not the tags, see How to get a text that's separated by different HTML tags in Cheerio.

Targeting a Label using jQuery when an extra Span is present

I have a label in a form defined as follows and the jQuery code below locates the 'Description' label and changes it for me to 'Program Description':
$(".ms-accentText").filter(function () {
return $(this).text() == "Description";
}).text('Program Description');
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3 class="ms-accentText">Description</h3>
But I have another label of 'Name' that has html markup that is used to tag the label with an asterisk, to define the associated field as being required, as listed below:
The similar jQuery code which is listed below does not locate and change the label:
$(".ms-accentText").filter(function () {
return $(this).text() == "Name";
}).text('Program Name');
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3 class="ms-accentText">Name<span class="ms-formvalidation"> * </span></h3>
How can I target the 'Name' label, when the extra asterisk related html mark-up code included?
I'm new to using jQuery.
Thanks,
Wayne
By using combination of :contains jquery selector + append you can achieve more efficient flow:
$(() => {
const query = 'Name'
const $name = $(`.ms-accentText:contains(${query})`)
.filter(function () {
const textChildTexts = [...this.childNodes]
// filtering to select only direct text nodes
// to skip the asterisk part
.filter(node => node && node.nodeType === Node.TEXT_NODE)
// getting the text out of node
.map(node => node && node.textContent)
// joining all the texts into a string to compare
.join('');
return textChildTexts === query;
});
const $asterisk = $name.find('.ms-formvalidation');
$name.text('Program Name').append($asterisk);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3 class="ms-accentText">Name<span class="ms-formvalidation"> * </span></h3>
<h3 class="ms-accentText">Project Full Name<span class="ms-formvalidation"> * </span></h3>
You could create a custom filtering function that removes any descendant elements and trims the containing text, and use that like this
function f(what) {
return function(index, elem) {
var clone = $(elem).clone();
clone.find('*').remove();
return clone.text().trim() === what;
}
}
$(".ms-accentText").filter(f('Description')).css('color', 'blue');
$(".ms-accentText").filter(f('Name')).css('color', 'red');
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3 class="ms-accentText">Description</h3>
<h3 class="ms-accentText">Name<span class="ms-formvalidation"> * </span></h3>

how to get exact text value inside a tag in jquery when entities are used

I have a list inside a anchor tag like this
<a href="#" class = "set_priority" data-value = "{{$candidate->id}}" style="text-decoration: none;">
#if($candidate->priority == "")
<li> Prioritize</li></a><br>
#elseif($candidate->priority == "yes")
<li> Deprioritize</li></a><br>
#endif
I have used entity &nbsp for styling purpose. The above code generate html tag according to the response from the server. So it might look either one of these
<li> Prioritize</li>
<li> Deprioritize</li>
I want to alert something when the list item is clicked. I don't know how to compare when &nbsp is used
$('.set_priority').on('click',function(){
var priority_value = $(this).first().text();
if (priority_value = "Prioritize") {
alert('he Prioritised');
}
else if (priority_value = "Deprioritize") {
alert('Prioritised');
}
});
It always alerts alert('he Prioritised'); whatever may be the condition.
You should use comparison operator instead assignment operator and use trim method to remove spaces.
$('.set_priority').on('click',function(){
var priority_value = $(this).first().text().trim();
if (priority_value == "Prioritize") {
alert('he Prioritised');
}
else if (priority_value == "Deprioritize") {
alert('Prioritised');
}
});
replace &nbsp with empty in temporary variable, then compare it. like:-
var priority_value = $(this).first().text();
var temp = priority_value.replace(/ /gi,'');
if (temp == "DePrioritize") {
alert('he Prioritised');
}
else if (temp == "Deprioritize") {
alert('Prioritised');
}
I think you should set the click handler like this
$('set_property li').on('click', function(){
var priority = $(this).first().text(); // This will give you the actual clicked element
// .... rest is same
});

Assert text value of list of webelements using nightwatch.js

I am new to using nightwatch.js.
I want to get a list of elements and verify text value of each and every element with a given string.
I have tried :
function iter(elems) {
elems.value.forEach(function(element) {
client.elementIdValue(element.ELEMENT)
})
};
client.elements('css selector', 'button.my-button.to-iterate', iter);
For another stackoverflow question
But what I am using right now is
waitForElementPresent('elementcss', 5000).assert.containsText('elementcss','Hello')
and it is returning me the output
Warn: WaitForElement found 5 elements for selector "elementcss". Only the first one will be checked.
So I want that it should verify text value of each and every element of list.
All the things can not be done by nightwatch js simple commands , so they have provided the custom command means selenium protocol. Here you can have all the selenium protocol. I have used following code to assert text value of each and every element with a given string "text". Hope it will help you
module.exports = {
'1. test if multiple elements have the same text' : function (browser) {
function iter(elems) {
elems.value.forEach(function(element) {
browser.elementIdText(element.ELEMENT, function(result){
browser.assert.equal(result.value,'text')
})
})
};
browser
.url('file:///home/user/test.html')
.elements('tag name', 'a', iter);
}
};
My HTML snippet
<div id="test">
<a href="google.com" class='red'> text </a>
<a href="#" class='red'> text </a>
<a href="#" class='red'> text 1</a>
</div>
I was able to do it as :
.elements('css selector', 'cssValue', function (elements) {
for(var i=0;i<elements.value.length;i++){
var elementCss = 'div.search-results-item:nth-child(' + (i+1) + ') span';
client.assert.containsText(elementCss,'textValue');
}
})
Put your function iter in a for loop and before that use
client.elements('css selector', '#CollectionClass', function (result) {
if(result.value.length > 1) {
var count;
for(count=1; count<result.value.length; count++) {
result.value.forEach(function(element) {
client.elementIdValue(element.ELEMENT);
client.elementIdText(selectedHighlight.ELEMENT, function(resuddlt) {
this.assert.equal(typeof resuddlt, "object");
this.assert.equal(resuddlt.status, 0);
this.assert.equal(resuddlt.value, "your value");
});
}
}
}
};
You have stated what you have tried (which is good) but you haven't presented us with sanitized HTML that demonstrates the problem (which reduces precision in possible answers).
There are many ways in HTML to contain information, and the built-in Nightwatch containsText will serialize any text it finds within a structure that contains substructures.
So for example, if you have as Juhi suggested,
<div id="test">
<a href="google.com" class='red'> text </a>
<a href="#" class='red'> text </a>
<a href="#" class='red'> text 1</a>
</div>
Then the assertions
.verify.containsText('#test', ' text ') // first one
.verify.containsText('#test', ' text ') // second one
.verify.containsText('#test', ' text 1') // third one
will pass, because they each verify the specific information without the need for writing a loop. Nightwatch will look at the test element and serialize the elements into the string text text text 1
Now if you need a loop for other reasons this is all academic, but your original question seemed to be targeted at how to get the text information out, not necessarily how to execute one possible solution to the problem (which is writing a loop).
I created custom assertions - custom-assertions/hasItems.js with content:
exports.assertion = function hasItems(selector, items) {
this.message = `Testing if element <${selector}> has items: ${items.join(", ")}`;
this.expected = items;
this.pass = selectedItems => {
if (selectedItems.length !== items.length) {
return false;
}
for (let i = 0; i < selectedItems.length; i++) {
if (selectedItems[i].trim() !== items[i].trim()) {
return false;
}
}
return true;
};
this.value = res => res.value;
function evaluator(_selector) {
return [...document.querySelectorAll(_selector)].map(
item => item.innerText
);
}
this.command = cb => this.api.execute(evaluator, [selector], cb);
};

Categories