Javascript create recursive unordered list from ordered lists - javascript

I have the following HTML
<ol data-level="0">
<li class="active">This is the data1</li>
</ol>
<ol data-level="1">
<li class="active">This is the data2</li>
</ol>
<ol data-level="2">
<li class="active">This is the data3</li>
</ol>
<ol data-level="3">
<li class="active">This is the data4</li>
</ol>
<ol data-level="2">
<li class="active">This is the data5</li>
</ol>
<ol data-level="2">
<li class="active">This is the data6</li>
</ol>
<ol data-level="0">
<li class="active">This is the data7</li>
</ol>
Below you can see the outcome of the above HTML (I stripped out some html tags for better readability!).
I have to create a valid unordered list from this outcome. I've searched around stackoverflow and I came across multiple solutions. E.g. making use of a recursive function. The problem with this is that I don't know how to pass in the objects that belong to the 'root' object (see: How can I recursively create a UL/LI's from JSON data - multiple layers deep).
At the moment I'm stuck with this code:
var $root = $items.first();
var rootLvl = $root.data('level');
var prevLvl = rootLvl;
function recursiveCheck($next) {
prevLvl = $next.data('level');
var nextItem = $next.next();
if (nextItem.data('level') > prevLvl) {
console.log('check');
recursiveCheck(nextItem);
} else {
console.log('break');
}
}
recursiveCheck($root);
At the 'break' I don't know how to go back to the previous root element.. Can someone pull me in the right direction?
Thanks in advance!
Edit: My desired outcome is like this:
<ul>
<li>Data 1
<ul>
<li> Data 2
<ul>
<li> Data 3
<ul>
<li>
Data 4
</li>
</ul>
</li>
<li>Data 5
</li>
<li>Data 6
</li>
</ul>
</li>
</ul>
</li>
<li>Data 7
</li>
</ul>
More information
- level 1
- - level 2 // this belongs to the first level 1
- - - level 3 // this belongs to level 2
- - - level 3 // this belongs to level 2
- - - level 3 // this belongs to level 2
- - level 2 // this belongs to the first level 1
- level 1
- - level 2 // this belongs to the second level 1
in this example the first level 1 has two level 2 subitems, but one of them also has three level 3 subitems. So I have to find out how to get the next items with a higher level. I'm able to produce above string with '- - -'. I just can't convert this to an unordered list.
Solved, final code:
edu.saveSummary = function() {
var dataLevel = 'edu-draggable-level';
var node = $('[data-edu-draggable]').first();
function parseUL() {
var level = node.data(dataLevel);
var ul = document.createElement("ul");
while (node && node.data(dataLevel) === level) {
var li = document.createElement("li");
li.textContent = node.text();
ul.appendChild(li);
node = node.next();
if (node && +node.data(dataLevel) > level) {
li.appendChild(parseUL());
}
}
return ul;
}
return parseUL();
};

Something along these lines should work:
var node = first_ol_node;
function parseUL() {
var level = +node.children().first().data('level');
var ul = document.createElement("ul");
while (node && +node.children().first().data('level') === level) {
var li = document.createElement("li");
li.textContent = node.text();
ul.appendChild(li);
node = node.next();
if (node && +node.children().first().data(level) > level) {
li.appendChild(parseUL());
}
}
return ul;
}
var ul = parseUL();
One problem is that you need both to return the parsed ul node from the function and you also need to advance the parsing pointer over the source items (and in Javascript there's no way to pass a variable by reference, you can only pass by value).
In this solution I'm using an external variable instead of a parameter to keep the current pointer in the source items (advanced in node = node.next()) and the return value is the parsed UL.

Related

Angular - Create component tree menu

I am creating a tree menu, visually it looks like this:
The tree has been created based on an array of objects obtained from a service, extracted from a date property.
Now, I have to get the tree menu to allow displaying and collapsing the: years, months and complete dates, in the style of this component:
https://angular2-tree.readme.io/
Ideally, I'd do this with typescript, but if it's hard for me, I'd try using an external component.
This is the html code:
<ul>
<li *ngFor="let y of tree | keyvalue">{{y.key}}
<ul>
<li *ngFor="let m of tree[y.key] | keyvalue">{{m.key}}
<ul>
<li *ngFor="let d of tree[y.key][m.key] | keyvalue">{{d.key}}
<ul>
<li *ngFor="let h of tree[y.key][m.key][d.key]"><a [ngClass]="{'hourSelected': (idSchedule === h.id || lastId === h.id),'hourUnSelected': idSchedule !== h.id}" (click)="loadMacroProcesses(h.id)">{{h.hour}}</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
This would be the solution, now I will refine it:
<ul>
<li id="year" *ngFor="let y of tree | keyvalue; let i=index" (click)="listClick($event, i)">{{y.key}}
<ul [hidden]="indexExpandedYear!=i?true:null">
<li id="month" *ngFor="let m of tree[y.key] | keyvalue; let j=index" (click)="listClick($event, j)">{{m.key}}
<ul [hidden]="indexExpandedMonth!=j?true:null">
<li id="day" *ngFor="let d of tree[y.key][m.key] | keyvalue; let k=index" (click)="listClick($event, k)">{{d.key}}
<ul [hidden]="indexExpandedDay!=k?true:null">
<li *ngFor="let h of tree[y.key][m.key][d.key]"><a [ngClass]="{'hourSelected': (idSchedule === h.id || lastId === h.id),'hourUnSelected': idSchedule !== h.id && lastId !== h.id}" (click)="loadMacroProcesses(h.id)">{{h.hour}}</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
This is the typescript method:
listClick(event, i) {
switch (event.srcElement.id) {
case "year":
this.indexExpandedYear = this.indexExpandedYear==i?-1:i;
event.stopPropagation();
break;
case "month":
this.indexExpandedMonth = this.indexExpandedMonth==i?-1:i;
event.stopPropagation();
break;
case "day":
this.indexExpandedDay = this.indexExpandedDay==i?-1:i;
event.stopPropagation();
break;
default:
break;
}
}
Can you recommend me a good one external component? Thanks.
NOTE: I am working with version 11 of Angular
NOTE: If you deploy one year, the rest of the years should be collpased back.
NOTE: Angular material is not valid for me
You could add a parameter for visibility and click event to the parent ul. How it would work is that they would have a boolean value on them for visibility that would change when you click the top ul element. You would have a method that would just switch between true/false and display if true hidden if false. Click event should be on the top li element and visibility on its child.
You should checkout the tree component provided by primeng. It has its own data format and can do your own customisation on top it.

Get index of an element from a NodeList

I have a simple list:
<ul id="list">
<li id="item-1">1</li>
<li id="item-2" style="display: none">2</li>
<li id="item-3">3</li>
<li id="item-4">4</li>
<li id="item-5">5</li>
</ul>
And need to get index of a specific item disregarding hidden items.
var list = document.getElementById('list');
var items = list.querySelectorAll('li:not([style*="display: none"])');
I try to convert NodeList in Array:
var list_items = Array.from(items);
But don't known how to run something like that: list_items.indexOf('item-3')
https://codepen.io/marcelo-villela-gusm-o/pen/RwNEVVB?editors=1010
You can make a function to find the id you need in a list you want, passing two parameters, that way you can use this function dynamically.
Based on id, inside the function just need to use .findIndex() that returns the index or -1 if not found.
See here:
var list = document.getElementById('list');
var items = list.querySelectorAll('li:not([style*="display: none"])');
var list_items = Array.from(items);
function getIndexById(idToSearch, list){
//ES6 arrow function syntax
return list.findIndex(elem => elem.id == idToSearch)
//normal syntax
//return list.findIndex(function(elem) {return elem.id == idToSearch})
}
console.log("found at index: ", getIndexById("item-3", list_items))
<ul id="list">
<li id="item-1">1</li>
<li id="item-2" style="display: none">2</li>
<li id="item-3">3</li>
<li id="item-4">4</li>
<li id="item-5">5</li>
</ul>
Not exactly related to the question, but if possible, I would suggest you to change your HTML to remove that inline style of display: none and change it to a class, (e.g: class='hidden'), it would be better for your .querySelector when using :not, for example: li:not(.hidden), since any space in your inline style can break your selector. ("display:none" != "display: none", spot the space)
Maybe like this:
var item = list_items.find(function(item) {
return item.id === "item-3";
});
I would recommend using :not(.hidden) instead of "grepping" for a match on the style tag. Then, simply find the index after casting the NodeList to an array.
For the Vue.js inclined, see this fiddle: https://jsfiddle.net/634ojdq0/
let items = [...document.querySelectorAll('#list li:not(.hidden)')]
let index = items.findIndex(item => item.id == 'item-4')
console.log('item-4 index in visible list is', index)
.hidden {
display: none;
}
<ul id="list">
<li id="item-1">1</li>
<li id="item-2" class="hidden">2</li>
<li id="item-3">3</li>
<li id="item-4">4</li>
<li id="item-5">5</li>
</ul>
Maybe you can use map. First you can create an object with id and value. Then use map function to create array of this object. Then you can access it with foreach, when id = 'item-3'.

Array methods for DOM HTML? [duplicate]

This question already has answers here:
How to convert a DOM node list to an array in Javascript?
(7 answers)
Closed 5 years ago.
I need to make an array from li tags. The array must include their inner texts. Eache index in array with each inner texts of li tags.
I'm trying to call the array method slice by Array.prototype.slice(). But probably I making some wrong...
The result must be like:
arr = ["Animals", "0_", .... , "fish__"]
var bodyd = document.getElementsByTagName('li');
for (var i = 0; i < bodyd.length; i++) {
bodyd = Array.prototype.slice.call(bodyd, 1);
console.log(bodyd);
}
<ul>
<li>Animals
<ul>
<li>0_
<ul>
<li>1__</li>
<li>2__</li>
<li>3__</li>
<li>4__</li>
</ul>
</li>
<li>Other_
<ul>
<li>Slis__</li>
<li>Bird__</li>
<li>Repti__</li>
</ul>
</li>
</ul>
</li>
<li>Fish
<ul>
<li>Aqua
<ul>
<li>Aqua__</li>
<li>Aqua__</li>
</ul>
</li>
<li>fish_
<ul>
<li>fish__</li>
</ul>
</li>
</ul>
</li>
</ul>
try this
var bodyd = document.getElementsByTagName('li');
bodyd = Array.prototype.slice.call(bodyd).map(function(val) {
return val.firstChild.data.trim();
});
console.log(bodyd);

How can I create json output from deep li structure

I have ol li structure as html and I want to create JSON from that but my code doesn't create that JSON I need. Can any one please help me to solve it?
I need to create JSON like that
[
{"en":"Menu1","enlink":"#enlink1","tr":"Menü 1","trlink":"#trlink1","data":[
{"en":"Menu1-1","enlink":"#enlink1-1","tr":"Menü 1-1","trlink":"#trlink1-1","data":[
{"en":"Menu1-1-1","enlink":"#enlink1-1-1","tr":"Menü 1-1-1","trlink":"#trlink1-1-1"},
{"en":"Menu1-1-2","enlink":"#enlink1-1-2","tr":"Menü 1-1-2","trlink":"#trlink1-1-2"}
]},
]},
{"en":"Menu2","enlink":"#enlink2","tr":"Menü 2","trlink":"#trlink2","data":[
{"en":"Menu2-1","enlink":"#enlink2-1","tr":"Menü 2-1","trlink":"#trlink2-1"},
{"en":"Menu2-1","enlink":"#enlink2-1","tr":"Menü 2-1","trlink":"#trlink2-1"}
]},
{"en":"Menu3","enlink":"#enlink3","tr":"Menü 3","trlink":"#trlink3"}
]
And my sample codes are..
var buildJson = function (root){
if(!root){
root='#domenu-en';
}
var result = [];
$(' ol > li ',root).each(function() {
if($(this).children("ol").length){
result.push({"en":$(this).attr("data-en"),"data":buildJson($(this))});
//return false;
} else{
result.push({"en":$(this).attr("data-en")});
}
});
return result;
}
$('#results').val(JSON.stringify(buildJson()))
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="domenu-en">
<ol>
<li data-enlink="#enlink1" data-en="Menu1" data-trlink="#trlink1" data-tr="Menü 1">
<ol>
<li data-enlink="#enlink1-1" data-en="Menu1-1" data-trlink="#trlink1-1" data-tr="Menü 1-1">
<ol>
<li data-enlink="#enlink1-1-1" data-en="Menu1-1-1" data-trlink="#trlink1-1-1" data-tr="Menü 1-1-1">
</li>
<li data-enlink="#enlink1-1-2" data-en="Menu1-1-2" data-trlink="#trlink1-1-2" data-tr="Menü 1-1-2">
</li>
</ol>
</li>
</ol>
</li>
<li data-enlink="#enlink2" data-en="Menu2" data-trlink="#trlink2" data-tr="Menü 2">
<ol>
<li data-enlink="#enlink2-1" data-en="Menu2-1" data-trlink="#trlink2-1" data-tr="Menü 2-1">
</li>
<li data-enlink="#enlink2-1" data-en="Menu2-1" data-trlink="#trlink2-1" data-tr="Menü 2-1">
</li>
</ol>
</li>
<li data-enlink="#enlink3" data-en="Menu3" data-trlink="#trlink3" data-tr="Menü 3">
</li>
</ol>
</div>
<textarea id="results" style=" height: 279px;"></textarea>
I found the solution its very simple, in case any one needed I like to share it. If we tell the jQuery select first li as ol:first >li then its generate same hierarchy as li structure.
var buildJson = function (root){
if(!root){
root='#domenu-en';
}
var result = [];
$('ol:first > li ',root).each(function() {
var itemdata = {};
$.each($(this).data(), function(key, value) {
itemdata[key] = value;
});
if($(this).children("ol").length){
itemdata["data"] = buildJson($(this));
}
result.push(itemdata);
});
return result;
}
The main difference seem to be that you don't include the other data- attributes other than the data-en. You can iterate all of the data- attributes on an element with $.each($(this).data(), function(key, value) {}).
In your case that would be something like:
$(' ol > li ',root).each(function() {
var itemdata = {};
$.each($(this).data(), function(key, value) {
itemdata[key] = value;
});
if($(this).children("ol").length){
itemdata["data"] = buildJson($(this));
}
result.push(itemdata);
});
That should give you an equal JSON result. The properties may be in a different order, but that shouldn't matter for JSON (for example: your JSON example had the order en, enlink, tr and trlink, while I got tr, trlink, en and enlink in my example. The order of the menu items in the list is still the same (1, 1-1, 1-2, etc.).
A working example at https://jsfiddle.net/gpctm9d1/

Jquery Mobile Duplicating Autodividers

I have a list but when I try to generate autodividers for that list I'm getting duplicate dividers. Here is the code for the ul and the relevant script:
<div data-role="content">
<ul data-role="listview" id="ScheduleList" data-autodividers="true">
<li time="3:30PM">Event 1</li>
<li time="3:30PM">Event 2</li>
<li time="4:30PM">Event 3</li>
<li time="3:30PM">Event 4</li>
<li time="3:30PM">Event 5</li>
<li time="4:30PM">Event 6</li>
</ul>
</div>
</div>
<script>
$(document).on("pageinit", "#ScheduleDay", function(){
$("#ScheduleList").listview({
autodividers: true,
autodividersSelector: function (li) {
var out = li.attr('time');
return out;
}
}).listview('refresh');
});
</script>
Here is the code in JSFiddle: http://jsfiddle.net/4fGT6/65/
I know that I could reorder the list items in the html and that would eliminate the duplicate autodividers, but if I made the list to be generated dynamically from user inputs then I couldn't manually reorder the html.
Is there a way to solve this if the list had been generated dynamically?
Thanks.
First step, sort list items based on data-time attribute (I added data to facilitate reading values - data attribute is ignored by user agent, thus it won't mess up your code).
I used the below simple code, yet genius, made by #undefined.
Update:
Thanks to #Keir Lavelle for reviewing the code of sorting li elements.
var listview = $('#ScheduleList'),
listitems = listview.children('li');
listitems.detach().sort(function (a, b) {
var adata = $(a).data('time');
var bdata = $(b).data('time');
/* return (adata > bdata) ? (adata > bdata) ? 1 : 0 : -1; */
return (adata > bdata) ? 1 : -1;
});
listview.append(listitems);
Second step, apply autodividers dynamically.
$("#ScheduleList").listview({
autodividers: true,
autodividersSelector: function (li) {
var out = li.jqmData('time');
return out;
}
}).listview('refresh');
Demo
Credits to #undefined and #Keir Lavelle

Categories

Resources