Многоуровневый динамический список на Javascript

Создание многуровненого списка (или по сути дерева) хорошо подходит для визализации сложных объектов. Например отображения папок на сервере. Пример данной реализации подразумевает предварительную загрузку дерева и потом только перерисовку нод. index.html
1
2
3
<div id="tree"></div>
<script src="tree.js" />
<link rel="stylesheet" href="style.css" />
tree.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//Функция создаётся и сразу вызывается
(function initTree (treeNode) {
    const FOLDER = 'FOLDER';
    const FILE = 'FILE';
    const clickFolder = folder => render(folder);
 
    //Создание DOM элемента
    function initChild(child) {
        const entity = document.createElement('li');
        entity.innerHTML = (child.type === FOLDER ? './' : '') + child.name;
        entity.setAttribute('class', child.type);
        entity.addEventListener(
            'click',
            () => child.type === FOLDER && clickFolder(child)
         );
 
        return entity;
    }
 
    //Создание всех DOM-элементов в папке
    function initFolder(parent) {
        const entity = document.createElement('ul');
 
        parent.children
            .sort(child => child.type !== FOLDER)
            .forEach(child => entity.appendChild(initChild(child)));
 
        return entity;
    }
 
    //Функция создающая возврающая дерево списка с "родителями"
    function getRoot() {
        const withParents = (child, parent) => {
            child.parent = parent;
 
            if (child && child.children) {
                child.children = child.children.map(
                    subchild => withParents(subchild, child)
                );
            }
 
            return child;
        };
         
        return withParents(getMockFolderData());
    }
 
   //Генерируем дерево списка для примера
   function getMockFolderData() {
       const rootFolder = { type: FOLDER, children: [] };
       let currentFolder = rootFolder;
       
       for (let size= 0; size < 25; size++) {
         currentFolder  = (
            [
               currentFolder.parent || currentFolder,
               ...currentFolder.children.filter(f => f.type === FOLDER)
            ]
               .sort(() => 0.5 - Math.random())
               .pop()
         );
 
         const FILE_NAMES = ["Фотки", "Любимая папка", "Разное"];
         const FOLDER_NAMES = ["photo1.png", "backup.zip", "42.jpg"];
         currentFolder.children.push(
            Math.random() < 0.5  ? {
               type:  FOLDER,
               name: FILE_NAMES.sort(() => Math.random() - 0.5).pop()
               children: []
            }  : {
               name: FOLDER_NAMES.sort(() => Math.random() - 0.5).pop(),
               type: FILE
            }
         );
       }
       
      return rootFolder;
   }
    
   //Рекурсивно по родителям получаем путь к папке
    function getBreadcrumbs(child, arr = []) {
        return (
         child.parent
            ? getBreadcrumbs(child.parent, [child, ...arr])
             : [child, ...arr]
         );
    }
 
    //Перерисовываем текущую папку
    function render(root) {
        const folder = initFolder(root);
        const breadcrumbs = document.createElement('span');
 
        getBreadcrumbs(root).forEach(child => {
            const breadcrumb = document.createElement('a');
            breadcrumb.href = "javascript:void(0)";
            breadcrumb.innerHTML = `${child.name || ""} ${child.type === FOLDER ? "/" : ""}`;
 
            breadcrumb.addEventListener(
               'click',
               () => child.type === FOLDER && render(child)
            );
 
            breadcrumbs.appendChild(breadcrumb);
        });
         
        treeNode.innerHTML = "";
        treeNode.appendChild(breadcrumbs);
        treeNode.appendChild(folder);
    }
 
    render(getRoot());
})(document.getElementById('tree'));
style.css
1
2
3
4
5
6
7
8
9
#tree { background: #003 }
#tree > span { margin-left: 16px; display: block;}
#tree > span > a { color: #aaf;  }
#tree > span > a:hover { color: #aaf; background: rgba(255, 255, 255, 0.3); }
#tree > ul > li { color: #fff; padding: 4px; display: block; }
#tree > ul > li.FOLDER:hover {
   cursor: pointer;
   background: rgba(255, 255, 255, 0.3);
}
Результат:
Для добаваления динамической загрузки достачно немного переписать метод render. Нужно добавить запрос на сервер c путём к текущей папке Что-то вроде такого:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function render(root) {
   const breadcrumbs = document.createElement('span');
   const breadcrumbsArray = getBreadcrumbs(root);
   breadcrumbsArray .forEach(child => {
      const breadcrumb = document.createElement('a');
      breadcrumb.href = "javascript:void(0)";
      breadcrumb.innerHTML = (child.name || "") + (child.type === FOLDER ? "/" : "");
      breadcrumb.addEventListener('click', () => child.type === FOLDER && render(child));
      breadcrumbs.appendChild(breadcrumb);
   });
 
   treeNode.innerHTML = "";
   treeNode.appendChild(breadcrumbs);
 
   $.getJSON(
      "/get-folder-content?uri=" + breadcrumbsArray.map(child => child.name).join('/'),
      rootData => treeNode.appendChild(initFolder(rootData))
   );
}
Спасибо за внимание.
Если статья Вам показалась незаконченной или Вы знаете как её улучшить, пожалуйста сообщите мне e@gohtml.ru