Reputation: 79450
I always struggle when faced with this problem, and it takes some serious work. I am currently trying to solve this problem and it will probably take me a few days. Wanted to see if you have a system or simple way of solving this problem.
Basically, say you have a flat list of DOM nodes with padding left indentation at steps of 15px. Visually, it forms a tree like a file browser. But structurally in the DOM, it is implemented as a flat list. How can you then iterate through the list and build a tree?
<div style='padding-left: 0px'>A</div>
<div style='padding-left: 15px'>AA</div>
<div style='padding-left: 15px'>AB</div>
<div style='padding-left: 30px'>ABA</div>
<div style='padding-left: 30px'>ABB</div>
<div style='padding-left: 45px'>ABBA</div>
<div style='padding-left: 45px'>ABBB</div>
<div style='padding-left: 45px'>ABBC</div>
<div style='padding-left: 30px'>ABC</div>
<div style='padding-left: 15px'>AC</div>
<div style='padding-left: 0px'>B</div>
<div style='padding-left: 0px'>C</div>
...
That should then become a JSON tree like this:
[
{
title: 'A',
children: [
{
title: 'AA',
children: []
},
{
title: 'AB',
children: [
{
title: 'ABA',
children: []
},
{
title: 'ABB',
children: [
{
title: 'ABBA',
children: []
},
{
title: 'ABBB',
children: []
},
{
title: 'ABBC',
children: []
}
]
},
{
title: 'ABC',
children: []
}
]
},
{
title: 'AC'
}
]
},
{
title: 'B',
children: []
},
{
title: 'C',
children: []
}
]
How do you do this? I get lost:
let tree = []
let path = [0]
let items = list('div')
items.forEach(item => {
let left = parseInt(item.style[`padding-left`] || 0) % 15
let set = tree
let p = path.concat()
while (left) {
let x = p.shift()
set[x] = set[x] || { children: [] }
set = set[x].children
left--
}
})
function list(s) {
return Array.prototype.slice.call(document.querySelectorAll(s))
}
Upvotes: 4
Views: 710
Reputation: 23945
It's a stack since it's sequential. Something like this?
We assume the folder structure is fully "expanded," therefore the parent of each folder (except the lowest, for which the parent is the root) must have been examined before the current one. The parent must also have a lower "padding-left" assignment.
ptrs
is a stack to which we append a reference to the next examined folder. The folder at the top (the end) of the stack is the last folder we examined. If those folders at the top of the stack have a higher than or equal "padding-left" assignment, they couldn't possibly be the parent of the current folder; and we can't possibly have more children of those after the current folder so we remove (pop) them until we find the last folder placed that had a lower "padding-left."
function getData(s){
const left = +s.match(/\d+/)[0];
const title = s.match(/[A-Z]+/)[0];
return [left, title];
}
function f(divs){
const tree = {
title: 'root',
children: []
};
const ptrs = [[0, tree]]; // stack
for (let str of divs){
const [left, title] = getData(str);
while (ptrs.length && ptrs[ptrs.length-1][0] >= left)
ptrs.pop();
parent = ptrs.length ? ptrs[ptrs.length-1][1] : tree;
const obj = {title: title, children: []};
parent.children.push(obj);
ptrs.push([left, obj]);
}
return tree;
}
var divs = [
"<div style='padding-left: 0px'>A</div>",
"<div style='padding-left: 15px'>AA</div>",
"<div style='padding-left: 15px'>AB</div>",
"<div style='padding-left: 30px'>ABA</div>",
"<div style='padding-left: 30px'>ABB</div>",
"<div style='padding-left: 45px'>ABBA</div>",
"<div style='padding-left: 45px'>ABBB</div>",
"<div style='padding-left: 45px'>ABBC</div>",
"<div style='padding-left: 30px'>ABC</div>",
"<div style='padding-left: 15px'>AC</div>",
"<div style='padding-left: 0px'>B</div>",
"<div style='padding-left: 0px'>C</div>"
]
console.log(f(divs));
Upvotes: 3
Reputation: 171690
Interesting exercise. Here's another approach that is a bit more verbose than the prior solution but also works with dom nodes rather than string html
const buildTree = (selector) => {
const elems = [...document.querySelectorAll(selector)]
.map((el,i)=>({el, title: el.textContent, idx:i, inset: parseInt(el.style.paddingLeft)}));
const getChildren = ({inset:pInset, idx:start}) => {
const nextParentIdx = elems.findIndex(({inset, idx}, i)=> inset <= pInset && i >start);
const desc = elems.slice(start, nextParentIdx+1 )
.filter(({inset})=>inset-pInset === 15);
return desc.map(getItem);
}
const getItem = (o)=>{
return {title: o.title, children: getChildren(o)}
}
return elems.filter(({inset})=>!inset).map(getItem)
}
console.log(JSON.stringify(buildTree('div'),null, 4))
.as-console-wrapper { max-height: 100%!important;top:0;}
<div style='padding-left: 0px'>A</div>
<div style='padding-left: 15px'>AA</div>
<div style='padding-left: 15px'>AB</div>
<div style='padding-left: 30px'>ABA</div>
<div style='padding-left: 30px'>ABB</div>
<div style='padding-left: 45px'>ABBA</div>
<div style='padding-left: 45px'>ABBB</div>
<div style='padding-left: 45px'>ABBC</div>
<div style='padding-left: 30px'>ABC</div>
<div style='padding-left: 15px'>AC</div>
<div style='padding-left: 0px'>B</div>
<div style='padding-left: 0px'>C</div>
Upvotes: 1