javascript changing text of multiple elements while hovering only one element - javascript

Having an issue with replacing text in multiple elements while hovering only one element
I tried doing document.getElementsbyClassName() but it didnt seem to work
function replace(text) {
var display = document.getElementById('head');
display.innerHTML = "";
display.innerHTML = text;
}
function revert(text) {
var display = document.getElementById('head');
display.innerHTML = "";
display.innerHTML = text;
}
<h2 id="head" onmouseover="replace('oh no!! the heading is gone')" onmouseout="revert('e8')"> e8 </h2>
<p id="pp"> :) </p>
I can replace the header but not the paragraph.

Personally I'd recommend a more extensible route revolving around data attributes.
The benefit to this approach is that you don't need to modify the JavaScript for each new item. You simply add attributes to the HTML elements themselves.
See the examples below - comments in the code.
If each element has it's own hover and replace information:
const elems = document.querySelectorAll('[data-replace]');
elems.forEach(elem => {
elem.addEventListener("mouseover", function() {
this.dataset["original"] = this.innerHTML; // store the text as data-original
this.innerHTML = this.dataset["replace"]; // update the html to be data-replace
});
elem.addEventListener("mouseout", function() {
this.innerHTML = this.dataset["original"]; // revert back to data-original
});
});
<h2 id="head" data-replace="'oh no!! the heading is gone'">e8</h2>
<p id="pp" data-replace="Something else"> :) </p>
If one item being hovered affects others, check out the example below instead.
You can group items by giving them the same data-group attribute. The one with the data-replace attribute is the one that triggers the replacement and defines the text.
const elems = document.querySelectorAll('[data-replace]');
elems.forEach(elem => {
//Get all elements that have the same data-group as the item being hovered
let group = document.querySelectorAll(`[data-group='${elem.dataset.group}']`);
elem.addEventListener("mouseover", function() {
group.forEach(e => { //For all group elements
e.dataset.original = e.innerHTML; //Store the original text
e.innerHTML = this.dataset.replace; //Replace the current text
});
});
elem.addEventListener("mouseout", function() {
group.forEach(e => e.innerHTML = e.dataset.original); //Rever to original text
});
});
<h2 data-group="group1" data-replace="Hello World">heading</h2>
<p data-group="group1">p</p>
<h2 data-group="group2" data-replace="Goodbye World">heading 2</h2>
<p data-group="group2">p 2</p>

Once option would be to pass the mouseover/mouseout event into the function. then you could use one function for multiple places.
<h2 id="head" onmouseover="replace(this, 'oh no!! the heading is gone', 'pp', 'moused over the heading')" onmouseout="replace(this, 'e8', 'pp', ':)')"> e8 </h2>
<p id="pp"> :) </p>
then use one function that will update the content of whatever element is passed to it.
function replace(e, text, secondElmID, secondElmText) {
// update the first element
e.innerHTML = "";
e.innerHTML = text;
// update the second element if is exists
var secondElm = document.getElementById(secondElmID);
if (secondElm) {
secondElm.innerHTML = secondElmText;
}
}

You just need to make sure that you reference both the elements that need to be affected and change both of them when one of them initiates the event.
Now, for few notes:
Don't use inline HTML event attributes.
Don't use .getElementsByClassName().
Don't use .innerHTML when you aren't setting any HTML as it has security and performance implications. Use .textContent instead.
let head2 = document.getElementById("head2");
let display = document.getElementById("pp");
let origHead2 = null;
let origPP = null;
head2.addEventListener("mouseover", function(){ replace("oh no!! the heading is gone"); });
head2.addEventListener("mouseout", revert);
function replace(text) {
// Store the original values before changing them
origHead2 = head2.textContent;
origPP = pp.textContent;
// Set the new values to what was passed into the function
head2.textContent = text;
display.textContent = text;
}
function revert() {
// Set the values back to the originals
head2.textContent = origHead2;
display.textContent = origPP;
}
<h2 id="head2"> e8 </h2>
<p id="pp"> :) </p>

You can proceed by grouping the elements which have the hover and replace text functionality under the same class and give each one a data-txt (where you can change txt part per your requirements) that holds the text that will be shown on hover and also will hold the old text each time an element gets hovered.
Here's a demo :
/**
* txtChange: the elements to be changed (their text) on mouse enter.
* doTextChange: a function that handles changing the text back and forth (replace and return to the original) for each element on both mouseenter and mouseleave events.
**/
const txtChange = [...document.querySelectorAll('.text-change')],
doTextChange = () => {
/** loop through the elements **/
txtChange.forEach(el => {
/** save the old text **/
const oldTxt = el.textContent;
/** replace the old text with the one in the data-txt **/
el.textContent = el.dataset.txt;
/** store the old text in the data-txt so that we return to the original text on mouse leave **/
el.dataset.txt = oldTxt;
});
};
/** cycle through the elements and attach the events **/
txtChange.forEach(el => {
/** the "doTextChange" can handle both the events **/
el.addEventListener('mouseenter', doTextChange);
el.addEventListener('mouseleave', doTextChange);
});
/** for demo purposes **/
.text-change {
margin: 35px 0;
padding: 8px;
background-color: #eee;
transition: all .4s 0s ease;
}
.text-change:hover {
background-color: #c0c0c0;
}
<!-- the hoverable elements share the same class "text-change" and each one has its own "data-txt" -->
<h2 class="text-change" data-txt="new text !">e8</h2>
<p class="text-change" data-txt="yet another new text">:)</p>

Related

Why is my code not affecting entire content?

I have written the following JS for my chrome project which allows you to bold random letters in a word or sentence.
The problem is that whenever there is a hyperlink in a paragraph the code only bolds random letters up until that point and the rest of the text is unaffected.
let all = document.querySelectorAll("*");
all.forEach(a => a.childNodes.forEach(b => makeRandomBold(b)));
function makeRandomBold(node) {
if (node.nodeType !== 3) {
return;
}
let text = node.textContent;
node.textContent = "";
text.split('').forEach(s => {
if (s !== " " && Math.random() > .49) {
let strong = document.createElement("strong");
strong.textContent = s;
node.parentNode.insertBefore(strong, node);
} else {
node.parentNode.insertBefore(document.createTextNode(s), node);
}
I have tried to change from the universal selector tag, to individually selecting HTML elements such as p a span. Which did not work.
Might finding random intervals and putting them in a single strong tag work?
What might be causing this?
Here you have one way to do it.
I added comments to the code:
// The selector * will pull everything: html, head, style, script, body, etc.
// Let's indicate that we only want elements inside the body that aren't scripts,
// styles, img, or any element that wont hold a string.
let allElements = document.querySelectorAll("body > *:not(style):not(script):not(img)");
// Now, lets iterate:
allElements.forEach(elem => {
traverseDOMToBold(elem);
});
// We want to visit all the futher children first,
// then we can change the content
function traverseDOMToBold(elem){
if (elem.hasChildNodes()){
[...elem.children].forEach(child => {
traverseDOMToBold(child);
});
}
boldInnerHTML(elem);
}
// I like to try to create function that have a Single Responsability if possible.
function boldInnerHTML(elem){
if (elem.innerHTML.length > 0){
let currentString = elem.innerHTML;
let newString = `<strong>${currentString}</strong>`;
elem.innerHTML = currentString.replace(currentString, newString)
}
}
<h1>Title</h1>
<p>Paragraph</p>
<div>Div</div>
<span id="parent">
Parent Span
<span id="child">
Child Span
</span>
</span>
<div>
1.
<div>
2.
<div>
3.
</div>
</div>
</div>
<ul>
<li>A.</li>
<li>B.</li>
<li>
<ul>C
<li>C1.</li>
<li>C2.</li>
<li>C3.</li>
</ul>
</li>
</ul>

onclick function doesn't execute

I'm trying to update the number of participants by increasing it by one every time the submit button gets clicked, and by doing so I added an add() inside my script tag and increased the participant number every time it gets clicked. But the number of participants doesn't get changed for some reason.
Btw I'm new to DOM and JS
let Agenda_style = document.querySelector('.agenda'); //to add style you must acess the class name/ID using querySelector
Agenda_style.style.color = "red "; // set color to red to change the color of the class agenda
let NewElement = document.createElement("li "); //create new element of type <li>
NewElement.innerText = "Here "; // add a text inside the <li> element
Agenda_style.append(NewElement); // append a tet to the agendaa class
let participant = 0;
function add() {
participant++;
document.getElementById("submit").innerText = participant;
};
<h5>Number of participant:<span id="submit">0</span></h5>
<button type="button" onclick="add()">Submit</button>
</div>
It fails for me, as I have now .agenda element.. do you have that in your HTML?
If I put a null check around that section of the script, the remaining piece works.
<h5>Number of participants: <span id="submit">0</span></h5>
<button type="button" onclick="add()">Submit</button>
</div>
<script>
let Agenda_style = document.querySelector('.agenda'); // to add style you must access the class name/ID using querySelector
if(Agenda_style != null) { // only proceed if Agenda_style exists
Agenda_style.style.color = "red "; // set color to red to change the color of the class agenda
let NewElement = document.createElement("li "); // create new element of type <li>
NewElement.innerText = "Here "; // add a text inside the <li> element
Agenda_style.append(NewElement); // append a tet to the agendaa class
}
let participant = 0;
function add() {
participant++;
document.getElementById("submit").innerText = participant;
}
</script>
These days we tend to not use inline JavaScript, so it would be best to grab your button with querySelector, and then use addEventListener to call your function when it's clicked. This way there's a "separation of concerns" between your mark-up, your CSS, and your code.
const number = document.querySelector('#number');
const button = document.querySelector('button');
button.addEventListener('click', add, false);
let participant = 0;
function add() {
number.textContent = ++participant;
}
<h5>Number of participant:
<span id="number">0</span>
</h5>
<button type="button">Submit</button>
This should be working
function add() {
let val = document.querySelector('#submit').innerText;
val = parseInt(val)+1;
}

Delete element in JS

I was trying to prototype a site for a To-Do List to experiment with something new using JavaScript.
function task() {
//Create checkbox
var x = document.createElement("INPUT");
x.setAttribute("type", "checkbox");
//Create <br>
lineBreak = document.createElement("br");
//Create <p> element
var todo = document.createElement("p");
//Insert in <p> the text in the input box
todo.innerText = document.getElementById("task").value;
//Create the <p>checkbox+text</p><br> on every botton click
return document.body.appendChild(x) + document.body.appendChild(todo) + document.body.appendChild(lineBreak);
document.querySelector('#reset').addEventListener('click', () => {
document.getElementById('reset').clicked
});
}
//Show Reset button on task generated
document.querySelector('#go').addEventListener('click', () => {
document.getElementById("reset").style.visibility = "visible";
});
p {
display: inline;
}
img {
width: 30px;
display: inline;
}
#reset {
visibility: hidden;
}
<h1>To-Do List</h1>
<input type="text" placeholder="Write a Task" id="task"><button id="go" onclick="task()">GO</button>
<hr>
<body>
<section>
<button id="reset">RESET</button>
</section>
</body>
As you can see from the code and the indicated if statement I was able to generate for each click on the go button (defined in HTML) new <p></p>.
It successfully generates a checkbox, next to a text typed in a text box and then wraps with the <br>.
I was trying to eliminate the elements generated by pressing the reset button, but despite having tried several solutions the only one that seems to work is the one that deletes all the contents of the body.
Could you suggest a solution to allow it to work?
Just make the adjustments to your javascript code with the following steps and it should work as your expectation:
Steps to fix the code:
Step 1: AddEventListener should be called before return so it would be called whenever the task() is executed with the click of the Go button.
Step 2: Firstly, remove the className "go-element" from the previously added elements if they exist.
Step 3: Add the class "go-element" to newly added elements so they can be identified easily while resetting them.
Step 4: on reset click, it should remove all the elements with the class "go-element"
Note: If you just want to remove all the elements which are added through the Go button, just skip step 2. Also, to simplify you can wrap your all elements in a div element and just follow all the steps as shown above with the div instead of elements.
function task() {
// Step 2: removing go-element class from previously added elements
const elements = document.getElementsByClassName("go-element");
while(elements.length > 0) {
elements[0].classList.remove("go-element");
}
// Step 3: add the class name to new elements
//Create checkbox
var x = document.createElement("INPUT");
x.setAttribute("type", "checkbox");
x.classList.add("go-element"); // step 3
//Create <br>
lineBreak = document.createElement("br");
lineBreak.classList.add("go-element"); // step 3
//Create <p> element
var todo = document.createElement("p");
todo.classList.add("go-element"); // step 3
//Insert in <p> the text in the input box
todo.innerText = document.getElementById("task").value;
// Step 1: moved this code before return so it will execute
document.querySelector('#reset').addEventListener('click', () => {
// Step 4: removing elements with class name "go-element"
const elements = document.getElementsByClassName("go-element");
while (elements.length > 0) {
elements[0].parentNode.removeChild(elements[0]);
}
});
//Create the <p>checkbox+text</p><br> on every botton click
return document.body.appendChild(x) + document.body.appendChild(todo) + document.body.appendChild(lineBreak);
}
//Show Reset button on task generated
document.querySelector('#go').addEventListener('click', () => {
document.getElementById("reset").style.visibility = "visible";
});

getSelection add html, but prevent nesting of html

I have a content editable field, in which I can enter and html format som text (select text and click a button to add <span class="word"></span> around it).
I use the following function:
function highlightSelection() {
if (window.getSelection) {
let sel = window.getSelection();
if (sel.rangeCount > 0) {
if(sel.anchorNode.parentElement.classList.value) {
let range = sel.getRangeAt(0).cloneRange();
let newParent = document.createElement('span');
newParent.classList.add("word");
range.surroundContents(newParent);
sel.removeAllRanges();
sel.addRange(range);
} else {
console.log("Already in span!");
// end span - start new.
}
}
}
}
But in the case where I have:
Hello my
<span class="word">name is</span>
Benny
AND I select "is" and click my button I need to prevent the html from nesting, so instead of
Hello my
<span class="word">name <span class="word">is</span></span> Benny
I need:
Hello my
<span class="word">name</span>
<span class="word">is</span>
Benny
I try to check the parent class, to see if it is set, but how do I prevent nested html - close span tag at caret start position, add and at the end of caret position add so the html will not nest?
It should also take into account if there are elements after selection which are included in the span:
So:
Hello my
<span class="word">name is Benny</span>
selecting IS again and clicking my button gives:
Hello my
<span class="word">name</span>
<span class="word">is</span>
<span class="word">Benny</span>
Any help is appreciated!
Thanks in advance.
One way would be to do this in multiple pass.
First you wrap your content, almost blindly like you are currently doing.
Then, you check if in this content there were some .word content. If so, you extract its content inside the new wrapper you just created.
Then, you check if your new wrapper is itself in a .word container.
If so, you get the content that was before the selection and wrap it in its own new wrapper. You do the same with the content after the selection.
At this stage we may have three .word containers inside the initial one. We thus have to extract the content of the initial one, and remove it. Our three wrappers are now independent.
function highlightSelection() {
if (window.getSelection) {
const sel = window.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0).cloneRange();
if (range.collapsed) { // nothing to do
return;
}
// first pass, wrap (almost) carelessly
wrapRangeInWordSpan(range);
// second pass, find the nested .word
// and wrap the content before and after it in their own spans
const inner = document.querySelector(".word .word");
if (!inner) {
// there is a case I couldn't identify correctly
// when selecting two .word start to end, where the empty spans stick around
// we remove them here
range.startContainer.parentNode.querySelectorAll(".word:empty")
.forEach((node) => node.remove());
return;
}
const parent = inner.closest(".word:not(:scope)");
const extractingRange = document.createRange();
// wrap the content before
extractingRange.selectNode(parent);
extractingRange.setEndBefore(inner);
wrapRangeInWordSpan(extractingRange);
// wrap the content after
extractingRange.selectNode(parent);
extractingRange.setStartAfter(inner);
wrapRangeInWordSpan(extractingRange);
// finally, extract all the contents from parent
// to its own parent and remove it, now that it's empty
moveContentBefore(parent)
}
}
}
document.querySelector("button").onclick = highlightSelection;
function wrapRangeInWordSpan(range) {
if (
!range.toString().length && // empty text content
!range.cloneContents().querySelector("img") // and not an <img>
) {
return; // empty range, do nothing (collapsed may not work)
}
const content = range.extractContents();
const newParent = document.createElement('span');
newParent.classList.add("word");
newParent.appendChild(content);
range.insertNode(newParent);
// if our content was wrapping .word spans,
// move their content in the new parent
// and remove them now that they're empty
newParent.querySelectorAll(".word").forEach(moveContentBefore);
}
function moveContentBefore(parent) {
const iterator = document.createNodeIterator(parent);
let currentNode;
// walk through all nodes
while ((currentNode = iterator.nextNode())) {
// move them to the grand-parent
parent.before(currentNode);
}
// remove the now empty parent
parent.remove();
}
.word {
display: inline-block;
border: 1px solid red;
}
[contenteditable] {
white-space: pre; /* leading spaces will get ignored once highlighted */
}
<button>highlight</button>
<div contenteditable
>Hello my <span class="word">name is</span> Benny</div>
But beware, this is just a rough proof of concept, I didn't do any heavy testings and there may very well be odd cases where it will just fail (content-editable is a nightmare).
Also, this doesn't handle cases where one would copy-paste or drag & drop HTML content.
I modified your code to a working approach:
document.getElementById('btn').addEventListener('click', () => {
if (window.getSelection) {
var sel = window.getSelection(),
range = sel.getRangeAt(0).cloneRange();
sel.anchorNode.parentElement.className != 'word' ?
addSpan(range) : console.log('Already tagged');
}
});
const addSpan = (range) => {
var newParent = document.createElement('span');
newParent.classList.add("word");
range.surroundContents(newParent);
}
.word{color:orange}button{margin:10px 0}
<div id="text">lorem ipsum Benny</div>
<button id="btn">Add span tag to selection</button>

JavaScript innerHTML manipulation with "onclick" handler

I wanna know how I can change the innerHtml to switch back and forth between two states.
I have this html
<div class="test" id="test">
<p>this is a test</p>
</div>
<p id="js" class="test" >Change</p>
and this is the JavaScript I have
let button = document.getElementById("js");
button.onclick = function() {
document.getElementById("test").innerHTML = "test";
};
how can I change the innerHTML from "test" to "another test" and vice versa ?
It's better not to store state in the HTML - don't test against what's currently in the HTML, keep a variable in your Javascript instead.
You should also only use let when you want to warn other programmers that you're going to reassign the variable in question - otherwise, use const.
Also, if you're changing the text of an element, assign to textContent instead - it's safer, faster, and more predictable.
const button = document.getElementById("js");
const test = document.getElementById("test");
let clicked = false;
button.onclick = function() {
clicked = !clicked;
if (clicked) test.textContent = 'another test';
else test.textContent = 'test';
};
<div class="test" id="test">
<p>this is a test</p>
</div>
<p id="js" class="test" >Change</p>
First, .innerHTML is only for when you are setting/getting a string that contains HTML. When you are not, use .textContent, which is more efficient and reduces security risks.
Then, you only need a toggling if statement, which can be done with a JavaScript ternary operator.
Also, rather than using event properties, like onclick. Use .addEventListener(), which is more robust and follows the modern standard.
// Get a reference to the <p> that is inside of the element who's id is "test"
let output = document.querySelector("#test p");
// Set up an event handler for the "Change" element
document.getElementById("js").addEventListener("click", function() {
// Check the current textContent and set it to the other value
output.textContent === "test" ? output.textContent = "another test" : output.textContent = "test";
});
<div class="test" id="test">
<p>this is a test</p>
</div>
<p id="js" class="test" >Change</p>
Just toggle between the two with an if statement like this:
let button = document.getElementById("js");
let toggleText = document.getElementById("test");
button.addEventListener('click', function() {
if (toggleText.innerHTML !== "another test") {
toggleText.innerHTML = "another test";
} else {
toggleText.innerHTML = "test";
}
});
ALso, as #CertainPerformance mentioned in the other answer, I would recommend that you use textContent rather than innerHTML since you're checking and toggling the element's string content and not the element itself.
I'm going to expand on a couple of excellent answers here.
What if you had more options and wanted to iterate through a list?
In that case, I'd use a data attribute to hold the button state. This is still a valid approach with just two options.
I am going to use Scott's answer as the basis of mine, so everything he says is pertinent.
// Get a reference to the <p> that is inside of the element who's id is "test"
let output = document.querySelector("#test p");
// Set up an event handler for the "Change" element
document.getElementById("js").addEventListener("click", function() {
//Here is out list of options
var options = ["test", "another test", "yet another test", "Really? Another Test?"];
//get the current state value and increment it
var index = parseInt(this.dataset.state, 10) + 1;
//if index is out of bounds set it to 0
if(index > options.length -1 || index < 0)
{
index = 0;
}
//Set the text
output.textContent = options[index];
//Set the new state
this.dataset.state = index;
});
<div class="test" id="test">
<p>this is a test</p>
</div>
<p id="js" class="test" data-state="-1" >Change</p>

Categories

Resources