<mark> text across <li> elements with hover behavior in React - javascript

Suppose I have HTML / JSX like this:
<ul>
<li>First bullet point</li>
<li>Second bullet point</li>
</ul>
and I want to highlight a range that "spans" multiple list elements "contiguously":
<ul>
<li>First bullet <mark>point</mark></li>
<li><mark>Second bullet</mark> point</li>
</ul>
Lastly, I'd like to add behaviour such that hovering over the first <mark> tags will trigger something for the second <mark> tags and vice versa. From the user's perspective, it should look and feel like a single <mark> tag.
Can this be done exclusively with CSS / Tailwind? Based on what I've seen so far, the sibling ~ combinator can only be used on earlier elements to affect later elements. The same appears to be true for the peer class in Tailwind.
If Javascript is required, can someone point me in the right direction? FWIW, this is for a React project so there may be some caveats to manipulating the DOM directly.

If hovering over any mark will also "hover" every other mark, then it is a simple has() call.
ul:has(mark:hover) mark {
background-color: red;
}
<ul>
<li><span>First bullet </span><mark>point</mark></li>
<li><mark>Second bullet</mark><span> point</span></li>
<li><span>Third bullet </span><mark>point</mark></li>
<li><mark>Fourth bullet</mark><span> point</span></li>
<li><span>The </span><mark>Fifth bullet</mark><span> point</span></li>
</ul>
If hovering over a mark only "hovers" marks that are contiguous, then it is a bit more complicated.
li mark:hover,
li:has(mark:last-child:hover) + li > mark:first-child,
li:has(mark:last-child):has(+ li > mark:first-child:hover) mark {
background-color: red;
}
<ul>
<li><span>First bullet </span><mark>point</mark></li>
<li><mark>Second bullet</mark><span> point</span></li>
<li><span>Third bullet </span><mark>point</mark></li>
<li><mark>Fourth bullet</mark><span> point</span></li>
<li><span>The </span><mark>Fifth bullet</mark><span> point</span></li>
</ul>
Since :has() is not enabled by default on Firefox (as of v109), here's a workaround using JavaScript.
const ul = document.querySelector("ul");
const removeActive = function (evt) {
const active = evt.currentTarget.querySelector("mark.active");
if (active) active.classList.remove("active");
}
ul.addEventListener("mouseover", function (evt) {
removeActive(evt);
let elem = null;
const mark = evt.target.closest("mark");
if (!mark) return;
const li = mark.closest("li");
if (!mark.previousSibling) {
const prevLi = li.previousElementSibling;
elem = prevLi && prevLi.lastChild;
} else if (!mark.nextSibling) {
const nextLi = li.nextElementSibling;
elem = nextLi && nextLi.firstChild;
}
if (elem && elem.nodeName == "MARK") {
elem.classList.add("active");
}
});
ul.addEventListener("mouseleave", removeActive);
li mark:hover,
li mark.active {
background-color: red;
}
<ul>
<li>First bullet <mark>point</mark></li>
<li><mark>Second bullet</mark> point</li>
<li>Third bullet <mark>point</mark></li>
<li><mark>Fourth bullet</mark> point</li>
<li>The </span><mark>Fifth bullet</mark><span> point</li>
</ul>

Related

How do I add active/inactive class when click on list item?

<ul id="myList">
<li class="top" data-pos="A">Text1</li>
<li class="top" data-pos="B">Text2</li>
</ul>
Now I want to create a JavaScript function that will add active/inactive class, when click on it.
click (data-pos="A") add class active
click (data-pos="B") add class inactive
Expected Result After Click
<ul id="myList">
<li class="top active" data-pos="A">Text1</li>
<li class="top inactive" data-pos="B">Text2</li>
</ul>
How can I do that? with JavaScript
How about this?
var myList = document.getElementById('myList')
/* We will add the click listener to the parent <ul> element! */
myList.addEventListener('click', e => {
/* Create a loop and iterate over all of the <li> elements inside #myList */
for (var i = 0; i < myList.children.length; i++) {
var li = myList.children[i]
if (li === e.target) {
/*
If the <li> inside the current loop matches our clicked element (e.target),
append active class to it
*/
li.classList.add('active')
} else {
/*. */
li.classList.remove('active')
}
}
})
.active {
color: yellow;
background: red;
}
<ul id="myList">
<li>Item #1</li>
<li>Item #2</li>
<li>Item #3</li>
</ul>
Notice there is no need for .inactive class. Items will be inactive by default, and active if they have .active class.
EDIT: the other answers are also correct, but I think mine is better because there is only one event listener on the parent, instead of one for each children. You can read more about event delegation.
Pure JavaScript Solution
Give all your li elements an event listener waiting for the click event.
JQuerys function closest() would be very useful here. Because in this solution we assign to nextSibling or previousSibling we always have to check if nextSibling exist to identify if it is the first or second list element.
We have the three cases:
Initial no active or inactive class else part in the function
Element we clicked on has class active
Element we clicked on has class inactive
At it's initial click give the clicked element the class active and the other element the class inactive.
Then check if the clicked element has class active remove it and add inactive to its classlist. At the other element remove inactive from it's class list and add active the same applys vice versa.
Note: Important to check if it's the next or the previous element
document.querySelectorAll('li').forEach((x) => {
x.addEventListener('click', myFunction);
})
function myFunction() {
if (this.classList.contains('active')) {
this.classList.remove('active')
this.classList.add('inactive')
if (this.nextElementSibling === null || this.nextElementSibling === undefined) {
this.previousElementSibling.classList.remove('inactive');
this.previousElementSibling.classList.add('active');
} else {
this.nextElementSibling.classList.remove('inactive');
this.nextElementSibling.classList.add('active');
}
} else if (this.classList.contains('inactive')) {
this.classList.remove('inactive')
this.classList.add('active')
if (this.nextElementSibling === null || this.nextElementSibling === undefined) {
this.previousElementSibling.classList.remove('active');
this.previousElementSibling.classList.add('inactive');
} else {
this.nextElementSibling.classList.remove('active');
this.nextElementSibling.classList.add('inactive');
}
} else {
this.classList.add('active')
if (this.nextElementSibling === null || this.nextElementSibling === undefined) {
this.previousElementSibling.classList.add('inactive');
} else {
this.nextElementSibling.classList.add('inactive');
}
}
}
.active {
background-color: yellow;
}
.inactive {
background-color: pink;
}
<ul id="myList">
<li class="top" data-pos="A">Text1</li>
<li class="top" data-pos="B">Text2</li>
</ul>
Jquery is not necesary but if you want to use it here is the solution :
$( "li" ).click(function() {
$('#myList li').removeClass('active inactive');
$('#myList li').addClass('inactive');
$(this).removeClass( "inactive" );
$(this).addClass( "active" );
});
/* Just for style purposes */
.active {
color: blue;
background-color: #E3F2FD;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="myList">
<li class="top" data-pos="A">Text1</li>
<li class="top" data-pos="B">Text2</li>
<li class="top" data-pos="C">Text3</li>
</ul>

if the LI tag has a class of active it will go to the last position

I have multiple li tag, if the user clicks one of those tags it will have a class of active and then the li tag will go to the end position of the list. but if the user decided to click the li tag it will remove the class and the li tag will go back to the previous position.
$(document).ready(function(){
$("button.move").on('click', function(){
$(".active").insertAfter("li:last-child()");
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button class="move">move</button>
<ul>
<li class="active">1st list item</li>
<li>2nd list item</li>
<li>3rd list item</li>
</ul>
You could make your list flex (or grid as david mentions in the comments) and use the order property on the li when you want to re-order them.
So no real change is happening in the DOM, but the .active element will be shown in the end.
document.querySelector('ul').addEventListener('click', (e) => {
const element = e.target;
const isLI = element.nodeName === 'LI';
if (isLI) {
element.classList.toggle('active');
removeClassFromSiblings(element, 'active');
}
})
function removeClassFromSiblings(element, classname) {
let prev = element;
let next = element;
// remove active from preceeding elements
while (prev = prev.previousElementSibling) {
prev.classList.remove('active');
}
// remove active from following elements
while (next = next.nextElementSibling) {
next.classList.remove('active');
}
}
ul {
display: flex; /* or grid */
flex-direction: column; /* if using flex */
}
.active {
order: 1;
color: red;
}
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
<li>fourth</li>
<li>fifth</li>
<ul>

JavaScript QuerySelector not returning last child

I'm trying to select the last child element of the class .nav-item and I've tried it several ways. It doesn't grab the item and just returns 'null' and applying styles will either grab the first item or return the following error:
main.js:25 Uncaught TypeError: Cannot read property 'style' of null
at main.js:25
My code :
The top part works fine and does apply the border, but I want to apply a border on the right of the last item and a border on the left of the first item. The last item is not working.
Can anyone point out my mistake?
The HTML if that helps:
let items = document.querySelectorAll('.nav-item');
for (let i = 0; i < items.length; i++) {
items[i].style.borderTop = '0.2em solid white';
items[i].style.borderBottom = '0.2em solid white';
}
// First and Last Item
// var secondItem = document.querySelector('.list-group-item:nth-child(2)');
// secondItem.style.color = 'coral';
let lastItem = document.querySelector('.nav-item:nth-child(4)');
console.log(lastItem);
lastItem.style.borderRight = '0.2em solid white';
<nav>
<h1>Item Lister</h1>
<ul class="nav" id="list">
<li class="nav-item">One</li>
<li class="nav-item">One</li>
<li class="nav-item">One</li>
<li class="nav-item">One</li>
</ul>
</nav>
So your <nav> element has one child: the <ul>. The <a> elements are children of the <ul>.
To fix this, you would need to querySelect .nav-item ul:nth-child(4).
Here's something to visualize it a little better. The <nav> is the parent to the <ul>, who is the parent to the <a>s. That makes <nav> the grandparent to the <a>s.
nav
ul
a
li
a
li
a
li
a
li
Also important like Karl-André Gagnon said to make sure the <li> is a child of the <ul>:
nav
ul
li
a
li
a
li
a
li
a
To achieve expected result, use below option of using document.querySelectorAll
Get all elements of class - nav-item irrespective parent and child relation
Convert HTML collection into Array using Array.from
Using pop() method get the last element to style
let lastItem = Array.from(document.querySelectorAll('.nav-item')).pop();
Sample working code for reference
let items = document.querySelectorAll('.nav-item');
for (let i = 0; i < items.length; i++) {
items[i].style.borderTop = '0.2em solid white';
items[i].style.borderBottom = '0.2em solid white';
}
// First and Last Item
// var secondItem = document.querySelector('.list-group-item:nth-child(2)');
// secondItem.style.color = 'coral';
let lastItem = Array.from(document.querySelectorAll('.nav-item')).pop();
console.log(lastItem);
lastItem.style.borderRight = '0.2em solid black';
<nav>
<h1>Item Lister</h1>
<ul class="nav" id="list">
<li class="nav-item">One</li>
<li class="nav-item">One</li>
<li class="nav-item">One</li>
<li class="nav-item">One</li>
</ul>
</nav>
Codepen - https://codepen.io/nagasai/pen/jOOvVjq
Issue::
:nth-child works with sibling elements, so :nth-child(4) is not available
As per MDN
The :nth-child() CSS pseudo-class matches elements based on their position in a group of siblings.
I used the options lastElementChild and firstElementChild. These worked.
Please change .nav-item to a. It serves your purpose as a is the child element of parent ul not the .nav-item.
let lastItem = document.querySelector('a:nth-child(4)');
It worked for me. Let me know if it is working for you.

How to highlight selected <li> item only?

I am trying to learn web designing and when trying to add class into it through java script ran into trouble.
html code:
<ul>
<li onclick="switchChannel(channel1)>
#Channel1
</li>
<li onclick="switchChannel(channel2) class="selected">
#Channel2
</li>
<li onclick="switchChannel(channel3)>
#Channel3
</li>
css code:
.selected{
color:blue;
border-left:4px solid blue;
}
javascript:script.js
function switchChannel(channelName) {
}
javascript:channel.js
var channel1={
name:"Channel1",
createdOn:new Date("April 1, 2016"),
starred:false
};
var channel2={
name:"Channel1",
createdOn:new Date("April 1, 2016"),
starred:false
};
I want to be able to click a channel1 from the list and apply .selected class to it but when channel2 is clicked remove .selected from channel1 and apply it to channel2 and so on...
If I have messed up anything else in the code please feel free to comment on it.
There are a lot of answers here but they don't seem to be addressing the actual issue. Here is a quick example using vanilla JavaScript to accomplish what you are asking for.
function switchChannel(el){
// find all the elements in your channel list and loop over them
Array.prototype.slice.call(document.querySelectorAll('ul[data-tag="channelList"] li')).forEach(function(element){
// remove the selected class
element.classList.remove('selected');
});
// add the selected class to the element that was clicked
el.classList.add('selected');
}
.selected{
color:blue;
border-left:4px solid blue;
}
<!-- Add the data-tag attribute to this list so you can find it easily -->
<ul data-tag="channelList">
<li onclick="switchChannel(this)">Channel 1</li>
<li onclick="switchChannel(this)" class="selected">Channel 2</li>
<li onclick="switchChannel(this)">Channel 3</li>
</ul>
You should use getElementsByIdand getElementsbyTagNameto manipulate the DOM:
function selectChannel(channelNumber) {
let listItems = document.getElementById("items").getElementsByTagName("li");
var length = listItems.length;
for (var i = 0; i < length; i++) {
listItems[i].className = i+1 == channelNumber ? "selected" : "";
}
}
.selected {
color: blue;
border-left: 4px solid blue;
}
<ul id="items">
<li onclick="selectChannel(1)">#channel1</li>
<li onclick="selectChannel(2)" class="selected">#channel2</li>
<li onclick="selectChannel(3)">#channel3</li>
</ul>
This solution uses jQuery but I thought this might help you out. A CodePen showing a version of this in action can be seen here.
jQuery(document).ready(function(){
jQuery('li').click(function(event){
//remove all pre-existing active classes
jQuery('.selected').removeClass('selected');
//add the active class to the link we clicked
jQuery(this).addClass('selected');
event.preventDefault();
});
});
I solved it. And I thank you every for your help.
$('li').removeClass('selected');
$('li:contains(' + channelName.name + ')').addClass('selected');

Activate ':hover' only when mouse moves down list and remove when mouse moves up

So I have to implement a Jquery function in which every li Element is executing the :hover pseudo class (which only changes the background color). So if I hover down to the 4th Element ALL previous li Elements (1st, 2nd, 3th) should have the changed background color from the :hover pseudo class!! But if I move up with the mouse again the :hover effect should disappear (normal background color again) up to the point where my mouse is (if it is on 2nd element only 1st and 2nd have the hover effect now) ... I have absolutely no idea how I can create such a method... I did something like
$('ul li').on('mouseenter') {
$(this).addClass('hover'); //alternatively $(this).css('background-color', 'grey');
}
but it does not remove any :hover effect and it makes failures possible like that only the first and the 5th li Element have the :hover effect but all in between remain normal which I don't want... Could you please help me?
Link to working example on jsfiddle.net
So lets start with some sample markup for a list:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
Then some css for your 'hover':
.hover {
background-color: red;
}
And some javascript to give the functionality:
$(function(){
// Our list items.
var listItems = $('ul').children();
// An event listener over all list items.
$('li').hover(hoverIn, hoverOut);
// Find the index of the current element
// and set all elements up to this element as hover.
function hoverIn() {
var index = listItems.index(this);
$.each(listItems, function(idx, ele) {
if (idx <= index) {
$(this).addClass('hover');
} else {
$(this).removeClass('hover');
}
});
}
// Remove all hover.
function hoverOut() {
$.each(listItems, function(idx, ele) {
$(this).removeClass('hover');
});
}
});
In fact this could be done entirely with css. No jQuery or JavaScript is required for this. You should consider using some html structure like this:
<ul>
<li><span>Menu item 1</span></li>
<li>
<span>Menu item 2</span>
<ul>
<li><span>Submenu item 1</span></li>
<li>
<span>Submenu item 2</span>
<ul>
<li><span>Subsubmenu item 1</span></li>
<li><span>Subsubmenu item 2</span></li>
<li><span>Subsubmenu item 3</span></li>
</ul>
</li>
<li><span>Submenu item 3</span></li>
</ul>
</li>
</ul>
Then you can use css like this
li:hover > span {
background: #9a0000;
}
See this jsfiddle: https://jsfiddle.net/aey5cusm/
Or if you ment by first, second, thirth of a single list, it can be done with css too.
ul {
list-style: none;
margin: 0;
padding: 0;
}
ul:hover li {
background: #9a0000;
}
li:hover ~ li{
background: none;
}
Just look at this jsfiddle: https://jsfiddle.net/aey5cusm/1/

Categories