I'm using ColdFusion to populate a template that includes HTML unordered lists (<ul>s).
Most of these aren't that long, but a few have ridiculously long lengths and could really stand to be in 2-3 columns.
Is there an HTML, ColdFusion or perhaps JavaScript (I'm accepting jQuery solutions) way to do this easily? It's not worth some over-complicated heavyweight solution to save some scrolling.
So I dug up this article from A List Apart CSS Swag: Multi-Column Lists. I ended up using the first solution, it's not the best but the others require either using complex HTML that can't be generated dynamically, or creating a lot of custom classes, which could be done but would require loads of in-line styling and possibly a huge page.
Other solutions are still welcome though.
If Safari and Firefox support is good enough for you, there is a CSS solution:
ul {
-webkit-column-count: 3;
-moz-column-count: 3;
column-count: 3;
-webkit-column-gap: 2em;
-moz-column-gap: 2em;
column-gap: 2em;
}
I'm not sure about Opera.
There is no pure CSS/HTML way to achieve this, as far as I know. Your best bet would be to do it in pre-processing (if list length > 150, split into 3 columns, else if > 70, split into 2 columns, else 1).
The other option, using JavaScript (I'm not familiar with the jQuery library specifically) would be to iterate through lists, probably based on them being a certain class, count the number of children, and if it is a high enough number, dynamically create a new list after the first, transferring some number of list items to the new list. As far as implementing the columns, you could probably float them left, followed by an element that had the style clear: left or clear: both.
.column {
float: left;
width: 50%;
}
.clear {
clear: both;
}
<ul class="column">
<li>Item 1</li>
<li>Item 2</li>
<!-- ... -->
<li>Item 49</li>
<li>Item 50</li>
</ul>
<ul class="column">
<li>Item 51</li>
<li>Item 52</li>
<!-- ... -->
<li>Item 99</li>
<li>Item 100</li>
</ul>
<div class="clear">
I've done this with jQuery - it's cross-platform and a minimum of code.
Select the UL, clone it, and insert it after the previous UL. Something like:
$("ul#listname").clone().attr("id","listname2").after()
This will insert a copy of your list after the previous one. If the original list is styled with a float:left, they should appear side by side.
Then you can delete the even items from the left-hand list and the odd items from the right hand list.
$("ul#listname li:even").remove();
$("ul#listname2 li:odd").remove();
Now you have a left to right two column list.
To do more columns you'll want to use .slice(begin,end) and/or the :nth-child selector.
ie, for 21 LIs you could .slice(8,14) to create a new UL inserted after your original UL, then select the original UL and delete the li's selected with ul :gt(8).
Try the Bibeault/Katz book on jQuery it's a great resource.
Here is a variation on Thumbkin's example (using Jquery):
var $cat_list = $('ul#catList'); // UL with all list items.
var $cat_flow = $('div#catFlow'); // Target div.
var $cat_list_clone = $cat_list.clone(); // Clone the list.
$('li:odd', $cat_list).remove(); // Remove odd list items.
$('li:even', $cat_list_clone).remove(); // Remove even list items.
$cat_flow.append($cat_list_clone); // Append the duplicate to the target div.
Thanks Thumbkin!
The following JavaScript code works only in Spidermonkey and Rhino, and it operates on E4X nodes--i.e., this is useful only for server-side JavaScript, but it might give someone a starting point for doing a jQuery version. (It's been very useful to me on the server side, but I haven't needed it on the client badly enough to actually build it.)
function columns(x,num) {
num || (num = 2);
x.normalize();
var cols, i, j, col, used, left, len, islist;
used = left = 0;
cols = <div class={'columns cols'+num}></div>;
if((left = x.length())==1)
left = x.children().length();
else
islist = true;
for(i=0; i<num; i++) {
len = Math.ceil(left/(num-i));
col = islist ? new XMLList
: <{x.name()}></{x.name()}>;
if(!islist && x['#class'].toString())
col['#class'] = x['#class'];
for(j=used; j<len+used; j++)
islist ? (col += x[j].copy())
: (col.appendChild(x.child(j).copy()));
used += len;
left -= len;
cols.appendChild(<div class={'column'+(i==(num-1) ? 'collast' : '')}>{col}</div>);
}
return cols;
}
You call it like columns(listNode,2) for two columns, and it turns:
<ul class="foo">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
into:
<div class="columns cols2">
<div class="column">
<ul class="foo">
<li>a</li>
<li>b</li>
</ul>
</div>
<div class="column collast">
<ul class="foo">
<li>c</li>
</ul>
</div>
</div>
It's meant to be used with CSS like this:
div.columns {
overflow: hidden;
_zoom: 1;
}
div.columns div.column {
float: left;
}
div.cols2 div.column {
width: 47.2%;
padding: 0 5% 0 0;
}
div.cols3 div.column {
width: 29.8%;
padding: 0 5% 0 0;
}
div.cols4 div.column {
width: 21.1%;
padding: 0 5% 0 0;
}
div.cols5 div.column {
width: 15.9%;
padding: 0 5% 0 0;
}
div.columns div.collast {
padding: 0;
}
The thing that most people are forgetting is that when floating <li/> items, all of the items have to be the same height, or the columns start getting out of whack.
Since you're using a server side language, my recommendation would be to use CF to split the list into 3 arrays. Then you can use an outer ul to wrap the 3 inner ul like so:
<cfset thelist = "1,2,3,4,5,6,7,8,9,10,11,12,13">
<cfset container = []>
<cfset container[1] = []>
<cfset container[2] = []>
<cfset container[3] = []>
<cfloop list="#thelist#" index="i">
<cfif i mod 3 eq 0>
<cfset arrayappend(container[3], i)>
<cfelseif i mod 2 eq 0>
<cfset arrayappend(container[2], i)>
<cfelse>
<cfset arrayappend(container[1], i)>
</cfif>
</cfloop>
<style type="text/css">
ul li { float: left; }
ul li ul li { clear: left; }
</style>
<cfoutput>
<ul>
<cfloop from="1" to="3" index="a">
<li>
<ul>
<cfloop array="#container[a]#" index="i">
<li>#i#</li>
</cfloop>
</ul>
</li>
</cfloop>
</ul>
</cfoutput>
Using a modulo operation, you can quickly split your list into multiple lists by inserting a </ul><ul> during your loop.
<cfset numberOfColumns = 3 />
<cfset numberOfEntries = 34 />
<ul style="float:left;">
<cfloop from="1" to="#numberOfEntries#" index="i">
<li>#i#</li>
<cfif NOT i MOD ceiling(numberOfEntries / numberOfColumns)>
</ul>
<ul style="float:left;">
</cfif>
</cfloop>
</ul>
Use ceiling() instead of round() to ensure that you don't have extra values at the end of the list and that the last column is shortest.
Flexbox can be used to wrap items in both row and column directions.
The main idea is to set the flex-direction on the container to either row or column.
NB: Nowadays browser support is pretty good.
FIDDLE
(Sample markup taken from this old 'list apart' article)
ol {
display: flex;
flex-flow: column wrap; /* flex-direction: column */
height: 100px; /* need to specify height :-( */
}
ol ~ ol {
flex-flow: row wrap; /* flex-direction: row */
max-height: auto; /* override max-height of the column direction */
}
li {
width: 150px;
}
a {
display: inline-block;
padding-right: 35px;
}
<p>items in column direction</p>
<ol>
<li>Aloe
</li>
<li>Bergamot
</li>
<li>Calendula
</li>
<li>Damiana
</li>
<li>Elderflower
</li>
<li>Feverfew
</li>
<li>Ginger
</li>
<li>Hops
</li>
<li>Iris
</li>
<li>Juniper
</li>
<li>Kava kava
</li>
<li>Lavender
</li>
<li>Marjoram
</li>
<li>Nutmeg
</li>
<li>Oregano
</li>
<li>Pennyroyal
</li>
</ol>
<hr/>
<p>items in row direction</p>
<ol>
<li>Aloe
</li>
<li>Bergamot
</li>
<li>Calendula
</li>
<li>Damiana
</li>
<li>Elderflower
</li>
<li>Feverfew
</li>
<li>Ginger
</li>
<li>Hops
</li>
<li>Iris
</li>
<li>Juniper
</li>
<li>Kava kava
</li>
<li>Lavender
</li>
<li>Marjoram
</li>
<li>Nutmeg
</li>
<li>Oregano
</li>
<li>Pennyroyal
</li>
</ol>
To output the list into multiple grouped tag you can loop in this fashion.
<cfset list="1,2,3,4,5,6,7,8,9,10,11,12,13,14">
<cfset numberOfColumns = "3">
<cfoutput>
<cfloop from="1" to="#numberOfColumns#" index="col">
<ul>
<cfloop from="#col#" to="#listLen(list)#" index="i" step="#numberOfColumns#">
<li>#listGetAt(list,i)#</li>
</cfloop>
</ul>
</cfloop>
</cfoutput>
Here is another solution that allows for columned lists in the following style:
1. 4. 7. 10.
2. 5. 8. 11.
3. 6. 9. 12.
(but it's pure javascript, and requires jQuery, with no fallback)
The following contains a some code that modifies the Array prototype to give a new function called 'chunk' that breaks any given Array into chunks of a given size. Next is a function called 'buildColumns' that takes a UL selector string and a number used to designate how many rows your columns may contain. (Here is a working JSFiddle)
$(document).ready(function(){
Array.prototype.chunk = function(chunk_size){
var array = this,
new_array = [],
chunk_size = chunk_size,
i,
length;
for(i = 0, length = array.length; i < length; i += chunk_size){
new_array.push(array.slice(i, i + chunk_size));
}
return new_array;
}
function buildColumns(list, row_limit) {
var list_items = $(list).find('li').map(function(){return this;}).get(),
row_limit = row_limit,
columnized_list_items = list_items.chunk(row_limit);
$(columnized_list_items).each(function(i){
if (i != 0){
var item_width = $(this).outerWidth(),
item_height = $(this).outerHeight(),
top_margin = -((item_height * row_limit) + (parseInt($(this).css('margin-top')) * row_limit)),
left_margin = (item_width * i) + (parseInt($(this).css('margin-left')) * (i + 1));
$(this[0]).css('margin-top', top_margin);
$(this).css('margin-left', left_margin);
}
});
}
buildColumns('ul#some_list', 5);
});
Since I had the same problem and couldn't find anything "clean" I thought I'd posted my solution. In this example I use a reversed while loop so I can use splice instead of slice. The advantage now is splice() only needs an index and a range where slice() needs an index and the total. The latter tends to become difficult while looping.
Disadvantage is I need to reverse the stack while appending.
Example:
cols = 4;
liCount = 35
for loop with slice = [0, 9]; [9, 18]; [18, 27]; [27, 35]
reversed while with splice = [27, 8]; [18, 9]; [9, 9]; [0, 9]
Code:
// #param (list): a jquery ul object
// #param (cols): amount of requested columns
function multiColumn (list, cols) {
var children = list.children(),
target = list.parent(),
liCount = children.length,
newUl = $("<ul />").addClass(list.prop("class")),
newItems,
avg = Math.floor(liCount / cols),
rest = liCount % cols,
take,
stack = [];
while (cols--) {
take = rest > cols ? (avg + 1) : avg;
liCount -= take;
newItems = children.splice(liCount, take);
stack.push(newUl.clone().append(newItems));
}
target.append(stack.reverse());
list.remove();
}
You can try this to convert in cols.
CSS:
ul.col {
width:50%;
float:left;
}
div.clr {
clear:both;
}
Html Part :
<ul class="col">
<li>Number 1</li>
<li>Number 2</li>
<li>Number 19</li>
<li>Number 20</li>
</ul>
<ul class="col">
<li>Number 21</li>
<li>Number 22</li>
<li>Number 39</li>
<li>Number 40</li>
</ul>
Related
I'm relatively new to JavaScript, so I'm not sure if I'm doing things conventionally here, of if there's a better way of doing what I'm trying to do.
I have a JavaScript function that takes about 3,600 sentences from a JSON document and inserts them automatically into my HTML code. A unique id is generated for each once in the HTML.
I want to create an onclick event for each sentence so that when it's clicked more information appears underneath about the sentence. This means I have to declare thousands of variables, one for each sentence and one for each information div associated with that sentence:
var sent1 = document.getElementById('s1');
var sent1info = document.getElementById('s1info');
var sent2 = document.getElementById('s2');
var sent2info = document.getElementById('s2info');
var sent3 = document.getElementById('s3');
var sent3info = document.getElementById('s3info');
...
This is way too much to do manually. Is there a way to automate the process of declaring these variables, or is there a better way to do what I'm doing?
For context, my intention with each variable is to feed it into this function:
sent1.onclick = function(){
if(sent1info.className == 'open'){
sent1info.className = 'close';
} else{
sent1info.className = 'close';
}
};
From here the CSS will reduce the info box to a hight of 0 when the className is 'close' and expand it when the className is 'open'. But, again, this will require me writing out this function thousands of times.
Is there a way to do this automatically also? Or am I going about this all wrong?
Edit to show HTML:
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<div id="everything">
<header id="theheader" class="clearfix">...</header>
<div id="thebody" class="box clearfix">
<aside id="page" class="side">...</aside>
<div class="items">
<article id="content" class="article">
<img id="sentpic" src="sentpic.jpg">
<h1>Sentences</h1>
<div id="sentences">
*** This is where the JS inserts sentences and information ***
<ul id='sent1' class='sentcontent'><li class='number'>1.</li><li class='thesent'>...</li></ul>
<div id='sent1info' class='infobox'>
<ul class='sentinfo'><li class='information'>Info:</li><li class='infotext'><em>...</em></li></ul>
<ul class='sentinfo'><li class='information'>Line:</li><li class='line'>...</li></ul>
</div>
<ul id='sent2' class='sentcontent'><li class='number'>2.</li><li class='thesent'>...</li></ul>"
<div id='sent2info' class='infobox'>
<ul class='sentinfo'><li class='information'>Info:</li><li class='infotext'><em>...</em></li></ul>
<ul class='sentinfo'><li class='information'>Line:</li><li class='line'>...</li></ul>
</div>
*** it goes on like this for each sent inserted ***
</div>
</article>
</div>
</div>
<div class="associates clearfix">...</div>
<footer class="foot">...</footer>
</div>
<script type="text/javascript" src="index.js"></script>
</body>
</html>
Using HTML <details> element:
const json = [
{thesent:"Lol", info:"This is some info 1", line:"Whatever 1..."},
{thesent:"Lorem", info:"Some info 2", line:"Something here 2..."},
];
const template_sentence = (ob, i) => `
<details class="sentence">
<summary>${i+1} ${ob.thesent}</summary>
<h3>${ob.info}</h3>
<div>${ob.line}</div>
</details>`;
document.querySelector("#sentences").innerHTML = json.map(template_sentence).join('');
<div id="sentences"></div>
Otherwise, by using your current non-semantic markup:
Targeting by ID (in your specific case) is not needed. There's other methods like the + Next Adjacent sibling selector in CSS.
And here's a JS example - should be self-explanatory, but feel free to ask.
Use JS to toggle a class (.active in this example) to the clickable UL element
Use CSS and the Next adjacent sibling selector + to make the info DIV display: block
/* Just a sample... you'll know how to modify this with the right properties I hope */
const json = [
{thesent:"Lol", info:"This is some info 1", line:"Whatever 1..."},
{thesent:"Lorem", info:"Some info 2", line:"Something here 2..."},
];
// The toggle function:
const toggleInfobox = ev => ev.currentTarget.classList.toggle("active");
// A single sentcontent template
const template_sentence = (ob, i) =>
`<ul class='sentcontent'>
<li class='number'>${i+1}</li>
<li class='thesent'>${ob.thesent}</li>
</ul>
<div class='infobox'>
<ul class='sentinfo'>
<li class='information'>Info:</li>
<li class='infotext'><em>${ob.info}</em></li>
</ul>
<ul class='sentinfo'>
<li class='information'>Line:</li>
<li class='line'>${ob.line}</li>
</ul>
</div>`;
// Get target element
const el_sentences = document.querySelector("#sentences");
// Loop JSON data and create HTML
el_sentences.innerHTML = json.map(template_sentence).join('');
// Assign listeners
const el_sentcontent = el_sentences.querySelectorAll(".sentcontent");
el_sentcontent.forEach(el => el.addEventListener('click', toggleInfobox));
/* BTW, why do you use <ul> ? That's not a semantic list! */
.sentcontent { padding: 0; cursor: pointer;}
.sentcontent li { display: inline-block; }
/* Arrows are cool, right? */
.sentcontent:before { content: "\25BC"; }
.sentcontent.active:before { content: "\25B2"; }
/* Hide adjacent .infobox initially,
/* and show adjacent .infobox on JS click */
.sentcontent + .infobox { display: none; }
.sentcontent.active + .infobox { display: block; }
<div id="sentences"></div>
In this Stack overflow answer you can find out more about toggling an element on some button click.
This question is more of an architectural issue than a need for creating dynamic variables. Consider this example:
ids are removed (existing class names used)
This pattern scales for n sentence instances
In handleClick, we toggle the open class on the clicked element, which lets us leverage the adjacent sibling selector via CSS
No need for a close class, since the absence of the open class represents the closed state.
let outerUL = document.querySelectorAll('.sentcontent')
function handleClick() {
this.classList.toggle('open');
}
outerUL.forEach(ul => {
ul.addEventListener('click', handleClick);
})
.sentcontent {
cursor: pointer;
}
.sentcontent.open + .infobox {
display: block;
}
.infobox {
background-color: #eee;
display: none;
padding: .25em .5em;
}
<ul class='sentcontent'>
<li class='number'>1.</li>
<li class='thesent'>Sent</li>
</ul>
<div class='infobox'>
<ul class='sentinfo'>
<li class='information'>Info</li>
<li class='infotext'><em>Info text</em></li>
</ul>
<ul class='sentinfo'>
<li class='information'>Line info</li>
<li class='line'>Line</li>
</ul>
</div>
<ul class='sentcontent'>
<li class='number'>2.</li>
<li class='thesent'>Sent</li>
</ul>
<div class='infobox'>
<ul class='sentinfo'>
<li class='information'>Info</li>
<li class='infotext'><em>Info text</em></li>
</ul>
<ul class='sentinfo'>
<li class='information'>Line info</li>
<li class='line'>Line</li>
</ul>
</div>
https://jsfiddle.net/d91va7tq/2/
When you have a very large json data then its good idee to keep in mind too not render the whole data at once, it will effect the webbrowser performance. Instead render when needed. And that is when the user click for more information.
I did some example below, make sure too read the comment
const json = [
{thesent:"Lol", info:"This is some info 1", line:"Whatever 1..."},
{thesent:"Lorem", info:"Some info 2", line:"Something here 2..."},
];
const container = document.querySelector(".container");
json.forEach((item)=> {
let x= item;
let el = document.createElement("li");
el.innerHTML = x.thesent;
container.appendChild(el);
el.addEventListener("click",()=> {
var infoContainer= el.querySelector(".info");
// dont create all html element at once, instead create them
//when the user click on it. this is better when you have a very large data.
if (!infoContainer){ // not created, then create
infoContainer = document.createElement("div");
infoContainer.className="info";
var info = document.createElement("div");
var line = document.createElement("div");
info.innerHTML = x.info;
line.innerHTML = x.line;
infoContainer.appendChild(info);
infoContainer.appendChild(line);
el.appendChild(infoContainer);
} else if (infoContainer.style.display == "none") // created and hidden, then display it
infoContainer.style.display = "block";
else infoContainer.style.display= "none"; // already displayed then hide it
});
})
.container li >div.info >div:first-child{
font-size: 12px;
}
.container li >div.info >div:last-child{
font-size: 10px;
}
<ul class="container">
</ul>
I have a menu which opens a sub-navigation on clicking a header which I am trying to get to close by clicking anywhere on the page except an open element.
My Code Snippet is as follows:
function showSubMenu(show, hide1, hide2, hide3, hide4) {
document.getElementById(show).className = "subNavShow";
document.getElementById(hide1).className = "subNavHide";
document.getElementById(hide2).className = "subNavHide";
document.getElementById(hide3).className = "subNavHide";
document.getElementById(hide4).className = "subNavHide";
}
.subNavHide {
display: none;
}
.subNavShow {
display: block;
}
<ul class="topnavList" id="siteTopnavList">
<li>
<a onclick="showSubMenu('text1','text2','text3','text4','text5')" href="javascript:void(0);">Nav 1</a>
<article id="text1" class="subNavHide">
<ul>
<li>Sub Nav 1</li>
</ul>
</article>
</li>
<li>
<a onclick="showSubMenu('text2','text1','text3','text4','text5')" href="javascript:void(0);">Nav 2</a>
<article id="text2" class="subNavHide"> text2 </article>
</li>
<li>
<a onclick="showSubMenu('text3','text1','text2','text4','text5')" href="javascript:void(0);">Nav 3</a>
<article id="text3" class="subNavHide"> text3 </article>
</li>
<li>
<a onclick="showSubMenu('text4','text1','text2','text3','text5')" href="javascript:void(0);">Nav 4</a>
<article id="text4" class="subNavHide"> text4 </article>
</li>
<li>
<a onclick="showSubMenu('text5','text1','text2','text3','text4')" href="javascript:void(0);">Nav 5</a>
<article id="text5" class="subNavHide"> text5 </article>
</li>
</ul>
Ideally I would like to use pure Javascript for this but if Jquery is absolutely necessary then I would be OK with that too
The easiest way to do this with your current implementation, in my opinion, is to add a click event listener to the document and use .closest to determine if the element clicked is the element open:
document.addEventListener(`click`, hideSubMenus);
function hideSubMenus(event) {
if (!event.target.closest(`.topnavList li a, .subNavShow`)) {
document.getElementById(`text1`).className = `subNavHide`;
document.getElementById(`text2`).className = `subNavHide`;
document.getElementById(`text3`).className = `subNavHide`;
document.getElementById(`text4`).className = `subNavHide`;
document.getElementById(`text5`).className = `subNavHide`;
}
}
closest is however not compatible with older browsers: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
But I would probably add classes to the links and add event listeners to them instead of using the "onclick" attribute. That way, for example, if you add the "subNavLink" class to each link, you can use a loop to deal with the links, instead of repeating the same line for each link:
let links, i, n;
links = document.getElementsByClassName(`subNavLink`);
for (i = 0, n = links.length; i < n; i++) {
links[i].addEventListener(`click`, showSubMenu);
}
function showSubMenu(event) {
let currentLink, i, link, n;
currentLink = event.currentTarget;
for (i = 0, n = links.length; i < n; i++) {
link = links[i];
if (link === currentLink) {
// this link was clicked, so we have to show its submenu
link.nextElementSibling.className = `subNavShow`;
} else {
// this link was not clicked, so we have to hide its submenu
link.nextElementSibling.className = `subNavHide`;
}
}
}
By doing this you can change the hideSubMenus function to:
function hideSubMenus(event) {
let i, n;
if (!event.target.closest(`.subNavLink, .subNavShow`)) {
for (i = 0, n = links.length; i < n; i++) {
links[i].nextElementSibling.className = `subNavHide`;
}
}
}
I've found that the easiest way to pull this off is to create a layer, underneath the menu (or more commonly a modal window). And then use that layer as the element to test if it has been clicked (versus the element sitting on top of it).
(The example uses a grayed out background to show the overlay's presence, but it could just as easily be a transparent DIV and still have the same effect)
// Get the elements that will show/hide
const overlay = document.getElementById('overlay');
const menu = document.getElementById('menu');
// Change the className to have the CSS that will hide
// the elements
// Since the 'menu' element is on top of the 'overlay'
// element, clicking on the 'menu' should not click
// through the 'overlay' -- thus ignoring this section
// of code to hide things
overlay.onclick = function(){
menu.className = 'hide';
overlay.className = 'hide';
};
// Quick and dirty code to reset the page and display
// the 'menu' and 'overlay' DIVs
function open(){
menu.className = '';
overlay.className = '';
}
#overlay{
display: block;
position: fixed;
top: 0; left: 0;
height: 100%; height: 100vh;
width: 100%; width: 100vw;
background-color: rgba( 0, 0, 0, 0.25 );
}
#overlay.hide{ display: none; }
#menu{
position: absolute;
background-color: white;
padding: 15px; border-radius: 5px;
}
#menu ul, #menu li{
margin: 0; padding: 0;
list-style: none;
}
#menu.hide{ display: none; }
OPEN
<div id="overlay"></div>
<div id="menu">
<ul>
<li>Menu Item</li>
<li>Menu Item</li>
<li>Menu Item</li>
<li>Menu Item</li>
</ul>
</div>
With the bubble and how elements are stacked, clicking on the menu won't close it -- but clicking anywhere outside of it will.
The more general the code is, the better.
Using an eventListener set on the document lets you listen to all "click" events (that bubbles up the DOM tree) on the page. You can close all articles no matter what, then display the clicked entry (and its ancestors) if appropriate.
The code below, yet short as many benefits:
It is dynamic. Meaning it can handle any amount of sub-levels. article elements neither require id attributes nor show/hide classes at first render. The code becomes loosly coupled.
Only a single handler function will live in memory instead of one per menu entry.
It will handle entries added later (after eventListener registration) to the menu.
Your code is factorized which makes it easier to read and reuse.
let topNavList = document.querySelector('#siteTopnavList');
document.addEventListener('click', function (e) {
let t = e.target;
// At this point, close menu entries anyway
topNavList.querySelectorAll('a ~ article').forEach(el => {
el.classList.add('subNavHide'); el.classList.remove('subNavShow');
});
// Drop clicks on the "active" link or any element that is outside the `#siteTopnavList` menu
if (!t.nextElementSibling || t.nextElementSibling.classList.contains('subNavShow')) {
return;
}
if (t.nodeName.toLowerCase() == 'a' && topNavList.contains(t)) {
topNavList.querySelectorAll('article').forEach(x => {
if(x.contains(t) || x === t.nextElementSibling) {
x.classList.remove('subNavHide');
x.classList.add('subNavShow');
}
});
// Prevent the browser to process the anchor href attribute
e.preventDefault();
}
});
#siteTopnavList article {display:none}
#siteTopnavList .subNavShow {display:block}
<ul class="topnavList" id="siteTopnavList">
<li>
Nav 1
<article>
<ul>
<li>Sub Nav 1</li>
</ul>
</article>
</li>
<li>
Nav 2
<article> TEXT2 </article>
</li>
<li>
Multi level
<article>
<ul>
<li>
Sub Nav 1
<article>
<ul>
<li>Deep 1</li>
<li>Deep 2</li>
<li>
Even deeper 3
<article>
<ul>
<li>Even deeper 1</li>
</ul>
</article>
</li>
</ul>
</article>
</li>
</ul>
</article>
</li>
</ul>
Need to align my odd number list iteam in center. Mean if i have five list iteam like below then last li should align center others should align side by side in mobile device.
<ul>
<li> name1</li>
<li> name2</li>
<li> name3</li>
<li> name4</li>
<li> name5</li>
</ul>
So fist i need to validate li count whether it's ending witj odd number or even number then i need to align last li center of the screen.
As JS just set's the CSS attribs to fit anyway, you might as well check out if a pure css solution fit's your problem.
See: https://stackoverflow.com/a/5080748/1614903
Assuming that the data will not be presented in ascending order, then a JavaScript solution may be required. See my example for a proof of concept. The CSS solutions will be fine when the data is presented in ascending order.
document.addEventListener('DOMContentLoaded', function() {
var list1 = document.querySelector('#list1');
var listItems = list1.querySelectorAll('li');
for (var li of listItems) {
addCenterAlignToListItem(li);
}
});
function addCenterAlignToListItem(li) {
var txt = li.textContent;
// RegEx matches a number at the end of the string.
var num = txt.match(/\d+$/);
if (!num) {
// There was no number at the
// end of the string.
return;
} else {
// Get the string from the
// regex match.
num = num.pop();
}
if (num % 2 === 1) {
// This is odd
li.classList.add('centered');
}
}
.centered {
text-align: center;
}
<!--
The list is intentionally
out of order.
-->
<ul id="list1">
<li>name2</li>
<li>name5</li>
<li>name1</li>
<li>name3</li>
<li>name4</li>
</ul>
Hope this works for you!
.list li {
text-align: inherit;
}
.list li:nth-child(odd) {
text-align: center;
}
.list li:nth-child(even) {
text-align: inherit;
}
<ul class="list">
<li> name1</li>
<li> name2</li>
<li> name3</li>
<li> name4</li>
<li> name5</li>
</ul>
I have 20 templates that are set out like the code below. The text gets added in via a database so can't change the style of the ul/li in there. I want to write 1 function that will change it in all.
Is it possible to only change the bullet list color (not the actual text) in a external js file?
<div id="container">
<h1 id="head1">Header</h1>
<p id="p1">
<ul>
<li>Test 1</li>
<li>Test 2</li>
<li>Test 3</li>
</ul>
</p>
Any help would be appreciated.
You can use css to do it.
You can create a class with the code below and then use javascript to apply that class to the bullet points you need.
This example was propose by Evan Mulwaski in a question similar to yours.
ul
{
list-style-type: square;
}
ul > li
{
color: green;
}
ul > li > span
{
color: black;
}
This is the link to the original question:
how to set ul/li bullet point color?
To change the bullet:
use
list-style-type: "\1F44D"; // thumbs up sign
li{
list-style-type: "\1F44D"; /* thumbs up sign */
}
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li></li>
<li>Item 4</li>
</ul>
https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
And yes, its CSS
To change the color of the bullet use CSS "content":
li {
list-style: none;
position: relative;
}
li::before {
color: #ff2211; /*bullet color*/
content: "\2022"; /* bullet char */
position:absolute;
left:-1.2em; /* indent of the bullet to the text */
}
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li></li>
<li>Item 4</li>
</ul>
The short answer is no, not in a pure JavaScript way can you manipulate lists like that. You would need to add a class to the HTML and change that via JS, or have span tags with •that you would style with CSS. There is a bit of a hack, but make sure you adjust the margin of the list items as this will throw it off a bit, also the bullets are a bit smaller so eh. Your call, but here's a take on it:
var addRule = function(selector, styles, sheet) {
styles = (function(styles) {
if(typeof styles === 'string') {
return(styles);
}
var clone = '';
for(var p in styles) {
if(styles.hasOwnProperty(p)) {
var val = styles[p];
p = p.replace(/([A-Z])/g, "-$1").toLowerCase(); // convert to dash-case
clone += p + ":" + (p === "content" ? '"' + val + '"' : val) + "; ";
}
}
return(clone);
}(styles));
sheet = sheet || document.styleSheets[document.styleSheets.length - 1];
if(sheet.insertRule) {
sheet.insertRule(selector + ' {' + styles + '}', sheet.cssRules.length);
} else if(sheet.addRule) {
sheet.addRule(selector, styles);
}
};
var uls = document.querySelectorAll('ul'), ul = null;
for(var i = 0, len = uls.length; i < len; i++) {
ul = uls[i];
ul.style.listStyle = 'none';
}
addRule('li:before', {
'content': '• ',
'color': 'red'
});
Using the addRule function I found over here, you first strip all the ul elements of the list-style property and use li:before pseudo selection to mimic a bullet point.
Using the li:before selector
Edit list style attribute list-style:none at css of list. And add cutom item inside li.
<li>
<span style = "color :red">
◉ item 1
</span>
</li>
i have some troubles implementing this layout:
http://deconcini.net/maurer-schneebacher/projekte/elisabethinum.html
Basically i would like to display the div with the pictures as soon as the user clicks on one of the links.
The problem is, that the centered navigation is in a container
#maincol{
width:940px;
padding: 0 auto;
border: 0px solid;
margin:0 auto;
background-color: #FFFFFF;
position: relative;
/* other attributes */
}
while the div i have to display has its own (grey) backgound which reaches through the whole width of the screen.
To implement it i had to close the maincol and open a new div called "selection" with the grey background which contains another div called "container", which is centered and contains the images.
#selection {
background: #DDDDDD;
margin: 10px 0 10px 0;
padding: 0;
width: 100%;
}
.container {
width: 940px;
margin: 0 auto;
overflow: hidden;
}
After this div's i re-open the maincol.
Now i would like to display this div's with javascript or AJAX. The problem is that i find it pretty hard to close a div, re-open and close a div again and than re-open the maincol div.
So i was wondering if it is possible to center the whole layout with padding, so i can set the grey background there and simply insert a new div with javascript without the need to close and re-open the maincol div.
I'm not very experienced with html and I wasn't able to find an answer on the web. So I would be very grateful if someone could give me a hint :-)
Best regards
Phew, okay here you go: http://jsfiddle.net/chricholson/25Yew/46/
Half way through I realised that you hadn't specified jQuery, but by then I had nearly done it. You can translate it to traditional Javascript if you like but I would highly recommend a library as it will save you many hours.
So onto the script, this detects the list item that has been clicked and moves it from the list before/after the "selection" div to the list after/before respectively. This keeps the lists clean and tidy and means you only have the one grey box in the middle.
You can animate as you see fit.
Notice that I have simple set the "selection" box to be filled with the contents of the list item clicked. In reality, you may want to put an id on each list item and have that load the content into the "selection" box, either with an array or an ajax call.
Good luck! :)
HTML:
<div id="mainCol1" class="col">
<ul>
<li>hello world 1</li>
<li>hello world 2</li>
<li>hello world 3</li>
</ul>
</div>
<div id="selection">
selection
</div>
<div id="mainCol2" class="col">
<ul>
<li>hello world 4</li>
<li>hello world 5</li>
<li>hello world 6</li>
</ul>
</div>
Javascript (actually jQuery):
$('#mainCol1 li').click(function(){
var item = $('#mainCol1 li').index(this);
var total = $('#mainCol1 li').length - 1;
fillSelection($('#mainCol1 li:eq(' + item + ')').html());
for(i = total; i > item; i--){
$('#mainCol2 ul').prepend($('#mainCol1 li:eq(' + i + ')'));
}
});
$('#mainCol2 li').click(function(){
var item = $('#mainCol2 li').index(this);
var total = $('#mainCol2 li').length - 1;
fillSelection($('#mainCol2 li:eq(' + item + ')').html());
for(i = 0; i < item; i++){
$('#mainCol1 ul').append($('#mainCol2 li:eq(0)'));
}
});
function fillSelection(html){
$('#selection').text(html);
}