I want text parts to appear, and disappear, on click.
Before the first click you can only see the banner, and no verses yet; on the first click the first verse appears, on second click the second verse appears in place of the first, and so on.
I am trying to achieve this with hiding the elements, placing them in an array, and let them display when the number of times the function gets called fits the index of the verse.
I am new to JavaScript, and don't understand the exceptions thrown. If i try to inspect my code online, I get this Exception when O try to call the function by clicking on the website:
Uncaught TypeError: Cannot read properties of null (reading 'style')
This is my code so far:
const text = document.querySelector(".banner")
document.addEventListener('click', myFunction);
const verse1 = document.querySelector(".verse1")
const verse2 = document.querySelector(".verse2")
const verse3 = document.querySelector(".verse3")
const verse4 = document.querySelector(".verse4")
const verse5 = document.querySelector(".verse5")
const verses = [verse1, verse2, verse3, verse4, verse5]
let versesLength = verses.length;
function myFunction() {
for (let i = 0; i < versesLength; i++) {
text.innerHTML = verses[i].style.display = 'block';
}
}
<div class="banner">
<script src="main.js"></script>
<img src="files/SomeLogo.jpg" alt="We are still building on our Website:-)">
</div>
<div id="verses">
<div class="verse1" style="display: none">Lorem Ipsum</div>
<div class="verse2" style="display: none">Lorem Ipsum2</div>
<div class="verse3" style="display: none">Lorem Ipsum3</div>
<div class="verse4" style="display: none">Lorem Ipsum4</div>
<div class="verse5" style="display: none">Lorem Ipsum5</div>
</div>
I am stuck, and clicked through similar questions for the last hours. Thanks in advance for any help
without changing anything in the HTML, you can do something like this in javascript
const text = document.querySelector(".banner")
document.addEventListener('click', myFunction);
let verses = document.querySelector("#verses").children
let count = 0
function myFunction() {
Array.from(verses).forEach(el=> el.style.display="none")
if(count < verses.length){
verses[count].style.display = 'block'
count ++
if(count===verses.length) count =0
}
}
You can remove the need for an array by giving all the verse elements the same class: verse. We can grab them with querySelectorAll.
Add a data attribute to each verse to identify them.
In order to limit the number of global variables we can use a closure - in the addEventListener we call the handleClick function which initialises the count, and then returns a function that will be assigned to the listener. This is a closure. It maintains a copy of its outer lexical environment (ie variables) that it can use when it's returned.
// Cache the elements with the verse class
const banner = document.querySelector('.banner');
const verses = document.querySelectorAll('.verse');
// Call `handleClick` and assign the function it
// returns to the listener
document.addEventListener('click', handleClick());
function handleClick() {
// Initialise `count`
let count = 1;
// Return a function that maintains a
// copy of `count`
return function () {
// If the count is 5 or less
if (count < verses.length + 1) {
// Remove the banner
if (count === 1) banner.remove();
// Remove the previous verse
if (count > 1) {
const selector = `[data-id="${count - 1}"]`;
const verse = document.querySelector(selector);
verse.classList.remove('show');
}
// Get the new verse
const selector = `[data-id="${count}"]`;
const verse = document.querySelector(selector);
// And show it
verse.classList.add('show');
// Increase the count
++count;
}
}
}
.verse { display: none; }
.show { display: block; margin-top: 1em; padding: 0.5em; border: 1px solid #787878; }
[data-id="1"] { background-color: #efefef; }
[data-id="2"] { background-color: #dfdfdf; }
[data-id="3"] { background-color: #cfcfcf; }
[data-id="4"] { background-color: #bfbfbf; }
[data-id="5"] { background-color: #afafaf; }
<div class="banner">
<img src="https://dummyimage.com/400x75/404082/ffffff&text=We+are+still+building+our+website" alt="We are still building on our Website:-)">
</div>
<div>
<div data-id="1" class="verse">Lorem Ipsum 1</div>
<div data-id="2" class="verse">Lorem Ipsum 2</div>
<div data-id="3" class="verse">Lorem Ipsum 3</div>
<div data-id="4" class="verse">Lorem Ipsum 4</div>
<div data-id="5" class="verse">Lorem Ipsum 5</div>
</div>
Additional documentation
Template/string literals
classList
This should make it:
const verses = document.querySelectorAll('.verse');
const banner = document.querySelector('.banner');
const length = verses.length;
let counter = 0;
document.onclick = () => {
if (counter === 0) banner.classList.add('hide');
if (counter >= length) return;
verses[counter].classList.add('show');
if (verses[counter - 1]) verses[counter - 1].classList.remove('show');
counter++;
};
body {
background: orange;
}
.hide {
display: none !important;
}
.show {
display: block !important;
}
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body>
<div class="banner">
<img
src="files/SomeLogo.jpg"
alt="We are still building on our Website:-)"
/>
</div>
<div id="verses">
<div class="verse1 verse hide">Lorem Ipsum</div>
<div class="verse2 verse hide">Lorem Ipsum2</div>
<div class="verse3 verse hide">Lorem Ipsum3</div>
<div class="verse4 verse hide">Lorem Ipsum4</div>
<div class="verse5 verse hide">Lorem Ipsum5</div>
</div>
<script src="main.js"></script>
</body>
</html>
Fixing the error
The "Cannot read properties of null" error occurs because you try to access the properties of null. Your array holds nulls because you queried for the elements before the browser has inserted them into its DOM.
The browser parses the HTML the same way you would read it: From left to right, and top to bottom.
If the browser encounters a regular <script> element, it halts parsing and first executes the JavaScript. Naturally, some elements may not yet be available in the DOM.
There are multiple ways to defer script execution:
Add attribute defer to <script>: Will execute once the DOM is fully built.
Add attribute type="module" to <script>: Similar to defer, but will also make your code be treated as a JS module. This will also make your code run in strict mode.
Use JS event DOMContentLoaded: Similar to defer, but encapsulated in your JS-file.
Use JS event load: Similar to DOMContentLoaded, but will additionally wait until all resources (e.g. images, videos) have loaded. Prefer DOMContentLoaded if applicable.
Move <script> to the bottom of the HTML: Effectively like defer. Scripts with defer will still load after scripts at the bottom.
The simplest solution would be to use defer, as with it you wouldn't have to change your JS code:
<script src="main.js" defer></script>
By the way: Don't be fooled by the StackOverflow snippets; when using the on-site snippets, the <script> for the code is moved to the bottom of the HTML!
The feature!
Variable lifetimes
Variables in JS only persist for as long as they are used.
Using a variable that is declared outside a function will create a closure around your function and variable. That means, that variable will persist for as long as the function exists that uses it:
let someVariable = 0;
function someFunction() {
// `someVariable` is used, so it will persist across calls!
return someVariable;
}
This means, to have the variable for "keeping track of what verse to show" persist, it has to be declared outside your function.
Show and hide!
By calculating the previous verse's index with the index of the next-to-show verse, we only have to keep one counter. With two counters, they might get out of sync if we don't handle them correctly.
let nextToShow = 0;
function showNextVerse() {
const previousIndex = nextToShow - 1;
// ...
++nextToShow; // Increase counter for next call
}
In our case, the user (or rather, their clicks) will play the role of the loop. They will cause our click handler (the function) to run occasionally, at which point we have to swap the verses.
Swapping the verses can be done in many ways, but we'll stick to your "inline style" way: (Final code)
document.addEventListener("click", showNextVerse);
const banner = document.querySelector(".banner");
const verses = document.getElementById("verses").children; // More on this later
let nextToShow = 0;
function showNextVerse() {
const previousIndex = nextToShow - 1;
// On every call, hide the banner
banner.style.display = "none"; // Use `.style` instead of `.innerHTML` to preserve its HTML!
// Hide previous if valid index
if (previousIndex >= 0 && previousIndex < verses.length) {
verses[previousIndex].style.display = "none";
}
// Show next if valid index
if (nextToShow >= 0 && nextToShow < verses.length) {
verses[nextToShow].style.display = "block";
}
++nextToShow;
}
<div class="banner">
<script src="main.js" defer></script>
<img alt="We are still building on our Website:-)">
</div>
<div id="verses">
<div style="display:none">Lorem Ipsum1</div>
<div style="display:none">Lorem Ipsum2</div>
<div style="display:none">Lorem Ipsum3</div>
<div style="display:none">Lorem Ipsum4</div>
<div style="display:none">Lorem Ipsum5</div>
</div>
Improvements?!
There is no need for the variable versesLength; you can directly replace each of its occurences with verses.length. It doesn't improve on the original name, and is one more potential source for bugs if not synchronized with the original variable.
Correctly use class and id
Currently, your verses use class as if it was id; they each use a different class. This is not wrong, but semantically I would use id for this purpose.
To use the class attribute effectively, you should give each verse the class verse. This way, you can select them more easily via JS (see next section).
Easier getting of elements
As with everything in the coding world, there are many solutions to a problem. You solved getting the elements in a rather tedious way, but there are alternatives: (Non-exhaustive list)
Use document.querySelectorAll().
Rename verses to use same class, and use document.getElementsByClassName().
Use Element.children.
You may have already noticed how I get all the verses. In fact, verses (in the final code) doesn't even reference an array, but an HTMLCollection. It is very similar to an array, with the exception of it updating live to changes:
const elementsWrapper = document.getElementById("elements");
const collection = elementsWrapper.children;
const array = Array.from(elementsWrapper.children);
document.getElementById("bt-add").addEventListener("click", function() {
const newElement = document.createElement("div");
newElement.textContent = "Added later";
elementsWrapper.appendChild(newElement);
});
document.getElementById("bt-log").addEventListener("click", function() {
console.log("collection.length:", collection.length);
console.log("array.length:", array.length);
});
<button id="bt-add">Add new element</button>
<button id="bt-log">Log <code>.length</code></button>
<p>
Try logging first! Then try to add elements, and log again! See the difference?
</p>
<div>Elements:</div>
<div id="elements">
<div>Initially existent</div>
<div>Initially existent</div>
</div>
Alternative way of hiding
Here are ways of hiding the elements:
Use inline styling (this is what you did!).
Use CSS classes.
Use the HTML attribute hidden.
For small style changes I too would use inline styling. But for only hiding elements I would use the hidden attribute.
Also, there are multiple CSS ways of hiding elements:
Using display: none: Will hide the element as if it doesn't exist.
Using opacity: 0: Will hide the element by making it invisible; it still takes up space, and should still be part of the accessibility tree (opinionated).
Moving it off-site with position: fixed and top, left, etc. properties: (Please don't.)
Will move the element off-site, visually. It will still be part of the accessibility tree, and will only work for languages with the intended writing direction (e.g. it won't work for right-to-left languages).
Setting width, height, margin, padding and border to 0: Will hide the element only visually; it will still be part of the accessibility tree, and will stop margin collapse. Screen-reader only classes use this for non-visual elements, very useful.
Related
As a novice Javascript programmer, I'd like to create an html document presenting a feature very similar to the "reveal spoiler" used extensively in the Stack Exchange sites.
My document therefore has a few <div> elements, each of which has an onClick event listner which, when clicked, should reveal a hiddent text.
I already know that this can be accomplished, e.g., by
<div onclick="this.innerHTML='Revealed text'"> Click to reveal </div>
However, I would like the text to be revealed to be initially stored in a variable, say txt, which will be used when the element is clicked, as in:
<div onclick="this.innerHTML=txt"> Click to reveal </div>
Since there will be many such <div> elements, I certainly cannot store the text to be revealed in a global variable. My question is then:
Can I declare a variable that is local to a specific html element?
Yes you can. HTML elements are essentially just Javascript Objects with properties/keys and values. So you could add a key and a value to an HTML element object.
But you have to add it to the dataset object that sits inside the element, like this:
element.dataset.txt = 'This is a value' // Just like a JS object
A working example of what you want could look like this:
function addVariable() {
const myElement = document.querySelector('div')
myElement.dataset.txt = 'This is the extended data'
}
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
addVariable() // Calling this one immediately to add variables on initial load
<div onclick="showExtendedText(event)">Click to see more </div>
Or you could do it by adding the variable as a data-txt attribute right onto the element itself, in which case you don't even need the addVariable() function:
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
<div onclick="showExtendedText(event)" data-txt="This is the extended data">Click to see more </div>
To access the data/variable for the specific element that you clicked on, you have to pass the event object as a function paramater. This event object is given to you automatically by the click event (or any other event).
Elements have attributes, so you can put the information into an attribute. Custom attributes should usually be data attributes. On click, check if a parent element has one of the attributes you're interested in, and if so, toggle that parent.
document.addEventListener('click', (e) => {
const parent = e.target.closest('[data-spoiler]');
if (!parent) return;
const currentMarkup = parent.innerHTML;
parent.innerHTML = parent.dataset.spoiler;
parent.dataset.spoiler = currentMarkup;
});
<div data-spoiler="foo">text 1</div>
<div data-spoiler="bar">text 2</div>
That's the closest you'll get to "a variable that is local to a specific html element". To define the text completely in the JavaScript instead, one option is to use an array, then look up the clicked index of the spoiler element in the array.
const spoilerTexts = ['foo', 'bar'];
const spoilerTags = [...document.querySelectorAll('.spoiler')];
document.addEventListener('click', (e) => {
const parent = e.target.closest('.spoiler');
if (!parent) return;
const currentMarkup = parent.innerHTML;
const index = spoilerTags.indexOf(parent);
parent.innerHTML = spoilerTexts[index];
spoilerTexts[index] = currentMarkup;
});
<div class="spoiler">text 1</div>
<div class="spoiler">text 2</div>
There are also libraries that allow for that sort of thing, by associating each element with a component (a JavaScript function/object used by the library) and somehow sending a variable to that component.
// for example, with React
const SpoilerElement = ({ originalText, spoilerText }) => {
const [spoilerShown, setSpoilerShown] = React.useState(false);
return (
<div onClick={() => setSpoilerShown(!spoilerShown)}>
{ spoilerShown ? spoilerText : originalText }
</div>
);
};
const App = () => (
<div>
<SpoilerElement originalText="text 1" spoilerText="foo" />
<SpoilerElement originalText="text 2" spoilerText="bar" />
</div>
)
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
Thanks everybody for your answers, which helped immensely! However, as a minimalist, I took all that I learned from you and came up with what I believe is the simplest possible code achieving my goal:
<div spoiler = "foo" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
<div spoiler = "bar" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
as I said in title I have problem with HTML elements created with Element.insertAdjacentHTML() method, I'm trying about an hour to solve this but can't. I have button that create new HTML elements, couple of that elements is new buttons with same class or id, it's no matter, that I need to catch in some variable and than again use for event listener, for some reason the class or id for these new created button doesn't exist, is there any way to catch it and use it later, I need Vanila Javascript?
There is over 500 lines of code, this is only for mentioned method
btnClaim.addEventListener("click", () => {
rewardCurrent.style.display = "none";
claimedRewards.push(currentReward);
rewardsList.innerHTML = ``;
claimedRewards.forEach(function (rew, i) {
const html = `
<div class="reward" id="${i}">
<div class="img-text-cont">
<img src="${rew.imgUrl}" alt="">
<div class="text-cont">
<p class="claimed-reward-title">${rew.title}</p>
<p class="claimed-reward-price">$${rew.price}</p>
</div>
</div>
<div class="claimed-rewards-action">
<button id="btn-sell2">Sell</button>
<button id="btn-ship">Ship</button>
</div>
</div>
`;
rewardsList.insertAdjacentHTML("afterbegin", html);
I need that btn-sell2 and btn-ship buttons in variables.
your element is going to be created and doesn't exist at the time page loads, so js addeventlistener will throw an error. to solve you have 2 ways.
1- use parent node that element will be created inside.
addevenlistener to parent and use
parent.addeventlistener( event, function (event){
if(event.target.classList.contains("childClass") {}
}
2- give addeventlistener when creating the element :
function createElement () {
const elem = -craete elemnt-
elem.addeventlistener(event, function);
}
I'm trying to use Svelte to do some conditional styling and highlighting to equations. While I've been successful at applying a global static style to a class, I cannot figure out how to do this when an event occurs (like one instance of the class is hovered over).
Do I need to create a stored value (i.e. some boolean that gets set to true when a class is hovered over) to use conditional styling? Or can I write a function as in the example below that will target all instances of the class? I'm a bit unclear why targeting a class in styling requires the :global(classname) format.
App.svelte
<script>
// import Component
import Katex from "./Katex.svelte"
// math equations
const math1 = "a\\htmlClass{test}{x}^2+bx+c=0";
const math2 = "x=-\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}";
const math3 = "V=\\frac{1}{3}\\pi r^2 h";
// set up array and index for reactivity and initialize
const mathArray = [math1, math2, math3];
let index = 0;
$: math = mathArray[index];
// changeMath function for button click
function changeMath() {
// increase index
index = (index+1)%3;
}
function hoverByClass(classname,colorover,colorout="transparent")
{
var elms=document.getElementsByClassName(classname);
console.log(elms);
for(var i=0;i<elms.length;i++)
{
elms[i].onmouseover = function()
{
for(var k=0;k<elms.length;k++)
{
elms[k].style.backgroundColor=colorover;
}
};
elms[i].onmouseout = function()
{
for(var k=0;k<elms.length;k++)
{
elms[k].style.backgroundColor=colorout;
}
};
}
}
hoverByClass("test","pink");
</script>
<h1>KaTeX svelte component demo</h1>
<h2>Inline math</h2>
Our math equation: <Katex {math}/> and it is inline.
<h2>Displayed math</h2>
Our math equation: <Katex {math} displayMode/> and it is displayed.
<h2>Reactivity</h2>
<button on:click={changeMath}>
Displaying equation {index}
</button>
<h2>Static math expression within HTML</h2>
<Katex math={"V=\\pi\\textrm{ m}^3"}/>
<style>
:global(.test) {
color: red
}
</style>
Katex.svelte
<script>
import katex from "katex";
export let math;
export let displayMode = false;
const options = {
displayMode: displayMode,
throwOnError: false,
trust: true
}
$: katexString = katex.renderToString(math, options);
</script>
<svelte:head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex#0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
</svelte:head>
{#html katexString}
If I understand it correctly you have a DOM structure with arbitrary nested elements and you would want to highlight parts of the structure that share the same class.
So you would have a structure like this:
<div>
<p>This is some text <span class="a">highlight</span></p>
<span class="a">Another highlight</span>
<ul>
<li>Some listitem</li>
<li class="a">Some listitem</li>
<li class="b">Some listitem</li>
<li class="b">Some listitem</li>
</ul>
</div>
And if you select an element with class="a" all elements should be highlighted regardles where they are in the document. This arbitrary placement makes using the sibling selector in css not possible.
There is no easy solution to this, but I will give you my attempt:
This is the full code with some explanation
<script>
import { onMount } from 'svelte'
let hash = {}
let wrapper
onMount(() => {
[...wrapper.querySelectorAll('[class]')].forEach(el => {
if (hash[el.className]) return
else hash[el.className] = [...wrapper.querySelectorAll(`[class="${el.className}"]`)]
})
Object.values(hash).forEach(nodes => {
nodes.forEach(node => {
node.addEventListener('mouseover', () => nodes.forEach(n => n.classList.add('hovered')))
node.addEventListener('mouseout', () => nodes.forEach(n => n.classList.remove('hovered')))
})
})
})
</script>
<div bind:this={wrapper}>
<p>
Blablabla <span class="a">AAA</span>
</p>
<span class="a">BBBB</span>
<ul>
<li>BBB</li>
<li class="a b">BBB</li>
<li class="b">BBB</li>
<li class="b">BBB</li>
</ul>
</div>
<style>
div :global(.hovered) {
background-color: red;
}
</style>
The first thing I did was use bind:this to get the wrapping element (in your case you would put this around the {#html katexString}, this will make that the highlight is only applied to this specific subtree.
Doing a querySelector is a complex operation, so we will gather all the related nodes in a sort of hashtable during onMount (this kind of assumes the content will never change, but since it's rendered with #html I believe it's safe to do so).
As you can see in onMount, I am using the wrapper element to restrict the selector to this section of the page, which is a lot faster than checking the entire document and is probably what you want anyway.
I wasn't entirely sure what you want to do, but for simplicity I am just grabbing every descendant that has a class and make a hash section for each class. If you only want certain classes you could write out a bunch of selectors here instead:
hash['selector-1'] = wrapper.querySelectorAll('.selector-1');
hash['selector-2'] = wrapper.querySelectorAll('.selector-2')];
hash['selector-3'] = wrapper.querySelectorAll('.selector-3');
Once this hashtable is created, we can loop over each selector, and attach two event listeners to all of the elements for that selector. One mouseover event that will then again apply a new class to each of it's mates. And a mouseout that removes this class again.
This still means you have to add hovered class. Since the class is not used in the markup it will be removed by Svelte unless you use :global() as you found out yourself. It is indeed not that good to have global classes because you might have unintended effect elsewhere in your code, but you can however scope it as I did in the code above.
The line
div > :global(.hovered) { background-color: red; }
will be processed into
div.svelte-12345 .hovered { background-color: red; }
So the red background will only be applied to .hovered elements that are inside this specific div, without leaking all over the codebase.
Demo on REPL
Here is the same adapted to use your code and to use a document-wide querySelector instead (you could probably still restrict if wanted by having the bind one level higher and pass this node into the component)
Other demo on REPL
I have DOM elements as shown below. I want to sort it on the basis of href attribute.
This is what I have tried in JS but more need to be done.
document.addEventListener("DOMContentLoaded", function () {
let elems = Array.from(document.querySelectorAll(".house-senate a"));
elems.sort((a, b) => a.textContent.localeCompare(b.textContent));
});
Problem Statement:
I am wondering what JS code I need to add so that it sorts everything on the basis of href attributes.
You're close, but:
You need to actually move them in the DOM.
You're potentially sorting ones that aren't in the same parent (though they all are in your example HTML).
blex pointed out to me that you want to sort by the category in the href, not by the href itself. In your example, it comes to the same thing because the text prior to the category in all the hrefs is the same, but still, perhaps better to extract it.
This is blex's function for extracting it:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
Or if you want to be more rigorous about extracting that parameter from the query string, this collaborative answer originally by Code Spy shows how to do that.
See comments for more:
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
If you need to add more sorting criteria (per your comment under the question), just add them in the sort callback; this question has answers showing how to sort an array of objects on multiple criteria.
I've assumed above that there aren't hundreds of these links. If there are, and if you see a performance problem with the above, you can remove the container from the DOM before moving the links around within it, then put it back:
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Remember its parent and following sibling and remove it
const parent = container.parentNode;
const sibling = container.nextSibling;
parent.removeChild(container);
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
// Put the container back -- note this works even if the
// container was the last child in the parent
// and `sibling` is `null`.
parent.insertBefore(container, sibling);
});
<div>This is before the <code>div</code> with the links in it.</div>
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
<div>This is after the <code>div</code> with the links in it.</div>
Note: You're using modern language features, but the above relies on a modern browser feature (NodeList being iterable). If you're transpiling, it may not be that all the browsers you're targeting have the necessary feature, but for anything even vaguely modern, you can polyfill it; see my answer here for details.
On this coding exercise, tt took me a while to figure out how to reference the class that gets hidden here, and now that it's working, it would be a shame to break it, however, it was my hope to refactor this thing so the reference to the class that needs to be hidden is cleaner.
As you can see, doubling up on nextElementSibling to traverse the tree costs in hard coding the solution for each adaptive reuse. My hope was to simply change the classes queried. To do this, at first, it looked like using this.querySelector('* +') might be the answer, but after many attempts (maybe not the right ones b/c it still might work unless you tell me otherwise) it failed.
The following does work:
let el = this.parentNode.firstChild
el = el.nextElementSibling
el = el.nextElementSibling
So, how can the class .acc-body be referenced in a simpler way given the HTML outline it acts on?
And, since the approach to event handing put into force here is decidedly non-standard, and probably not recommended, is that reason enough to avoid using this in production?
<div class="acc">
<div class="acc-item">
<!-- <div class="acc-title"> -->
<h1 class="acc-title" id="btn_hide">Click to hide the Body</h1>
<!-- </div> -->
<div class="acc-body hideable">
<p id="head1">Why innerHTML is so great...</p>
</div>
</div>
<div class="acc-item">
<h1 class="acc-title" id="other_btn_hide_xxxx">Click to hide the Body</h1>
<div class="acc-body hideable">
<p id="head2">Why innerHTML is so great...</p>
</div>
</div>
</div>
Implemented as an IIFE; recommend it be placed in its own .js file and referenced from a script tag in the HTML document.
(function() {
console.log('ready to assign event handler to chosen nodelist of elements')
// returned as a nodelist
all = document.querySelectorAll(".acc-item .acc-title")
console.log(all);
// document[selectionMethod(element)]
// element.selectionMethod.classList
console.log('setAttribute \'clickable\' received on: -----------');
all.forEach ( (j) => {
console.log(j);
// j.setAttribute('onclick', 'showhide(this)')
j.setAttribute('clickable', 'true')
j.classList.add("clickable")
})
console.log('listen for onclick on: ----------');
all.forEach( (j) => {
console.log(j);
j.onclick = function () {
// alert(this);
// let el = this.querySelector('* +')
let el = this.parentNode.firstChild
el = el.nextElementSibling
el = el.nextElementSibling
console.log(el);
if (el.hidden){
el.hidden = false
} else {
el.hidden = true
}
}
})
})()