четверг, 6 августа 2015 г.

RequireJS - Карта загрузки модулей и просмотр их исходного кода

Файл index.js

require.config({
      paths: {
          modulesTree: 'lib/require/modules-tree'
        , main: 'app/main/controller'
      }
    , urlArgs: '_=' + Math.random()
});

require(
    [
          'modulesTree'
        , 'main'
    ]
    , function(
          modulesTree
        , main
    ) {
   
        main();
       
        if (console && console.log) {
            console.log('Функция формирования дерева зависимостей для каждого модуля');
            window.requirejsModules.generateModuleDependenciesTree();
            console.log('Отображение имен всех загруженных модулей');
            for (var key in window.requirejsModules.list) {console.log(key);}
            console.log('Отображение списка зависимостей всех модулей');
            console.log(window.requirejsModules.generateModuleDependenciesList());
            console.log('Отображение списка путей к файлам модулей');
            console.log(window.requirejsModules.generateModulePathList());
            console.log('Отображение дерева зависимостей модулей друг от друга');
            console.log(window.requirejsModules.drawTreeGraph(window.requirejsModules.generateModuleDependenciesTreeObject())); //, '', {unicode: false}));
            console.log('Отображение вложенности файлов и папок друг в друга');
            console.log(window.requirejsModules.drawTreeGraph(window.requirejsModules.generateModulePathTreeObject()));
            console.log('Отображение зависимостей данного модуля в дереве файлов и папок');
            console.log(window.requirejsModules.drawModulePathTreeGraphWithDependencies('InterfaceBuilderController'));
            console.log('Отображение включения данного модуля в другие модули в дереве файлов и папок');
            console.log(window.requirejsModules.drawModulePathTreeGraphWithDependencies('jquery', {includeThisModule: true}));
        }
 
        var windowWidth = 1300
            , windowHeight = 500
            , top = Math.round( (window.screen.availWidth - windowWidth) / 4 )
            , left = Math.round( (window.screen.height - windowHeight) / 4 )
            , windowSpecs = [
                  'width=' + windowWidth
                , 'height=' + windowHeight
                , 'top=' + (top > 0 ? top : 0)
                , 'left=' + (left > 0 ? left : 0)
                , 'scrollbars=yes'
                , 'resizable=yes'
            ].join(', ')
        , moduleWindow;
        if (moduleWindow === undefined) {moduleWindow = window.open('modulemap.html', '_blank', windowSpecs);}

    }
);

Файл modules-tree.js

// Функция формирования списка загруженных модулей
requirejs.onResourceLoad = function (context, module, moduleDependenciesArray) {
    if (!window.requirejsModules) {
        window.requirejsModules = {
              list: {}
            , generateModuleDependenciesTree: generateModuleDependenciesTree
            , generateModuleDependenciesTreeObject: generateModuleDependenciesTreeObject
            , generateModuleDependenciesList: generateModuleDependenciesList
            , generateModulePathList: generateModulePathList
            , generateModulePathTreeObject: generateModulePathTreeObject
            , drawModulePathTreeGraphWithDependencies: drawModulePathTreeGraphWithDependencies
            , drawTreeGraph: drawTreeGraph
        };
    }
    if (!window.requirejsModules.list[module.name]) {
        window.requirejsModules.list[module.name] = {
              moduleName: module.name
            , modulePath: (function(){
                                        var url = '';
                                        if (module.url) {url = module.url.split('?')[0];}
                                        if (module.name.search('.html') === -1) {
                                            return url.replace('./', '');
                                        } else {
                                            return 'js/' + module.name;
                                        }
                                   })()
            , moduleDependenciesNames: []
            , moduleDependenciesTree: {}
        };
    }
    var i, len;
    if (moduleDependenciesArray) {
        for (
            i = 0, len = moduleDependenciesArray.length;
            i < len;
            i++
        ) {
            window.requirejsModules.list[module.name].moduleDependenciesNames.push(moduleDependenciesArray[i].name);
        }
    }
};

// Функция формирования дерева зависимостей для каждого модуля
function generateModuleDependenciesTree () {
    var modulesList = window.requirejsModules.list
        , moduleName
        , currentModule
        , i
        , len
        , currentDependencyName;
    for (moduleName in modulesList) {
        if (modulesList.hasOwnProperty(moduleName)) {
            currentModule = modulesList[moduleName];
            for (
                i = 0, len = currentModule.moduleDependenciesNames.length;
                i < len;
                i++
            ) {
                currentDependencyName = currentModule.moduleDependenciesNames[i];
                currentModule.moduleDependenciesTree[currentDependencyName] = modulesList[currentDependencyName];
            }
        }
    }
}

// Функция формирования иерархического объекта, состоящего из имен модулей, для построения дерева вложенности модулей
function generateModuleDependenciesTreeObject (mainModuleName) {
    generateModuleDependenciesTree();
   
    if (mainModuleName === undefined) {mainModuleName = 'main';}
   
    var moduleDependenciesTreeObject = buildModuleObject(window.requirejsModules.list[mainModuleName]);

    function buildModuleObject (module) {
        var moduleObject
            , moduleDependencyName;
        if (module !== undefined) {
            moduleObject = {
                  name: module.moduleName
                , path: module.modulePath
                , dependencies: []
            };
            for (moduleDependencyName in module.moduleDependenciesTree) {
                moduleObject.dependencies.push(buildModuleObject(module.moduleDependenciesTree[moduleDependencyName]));
            }
            moduleObject.dependencies.sort(function(a, b){a = a.name; b = b.name; if (a > b) {return 1;} else if (a < b) {return -1;} else {return 0;}});
        } else {
            moduleObject = {
                  name: ''
                , path: ''
                , dependencies: []
            };
        }
        return moduleObject;
    }

    return moduleDependenciesTreeObject;
}

// Функция формирования списка зависимостей всех модулей
function generateModuleDependenciesList () {
    var dependenciesList = []
        , modulesList = window.requirejsModules.list
        , moduleName
        , currentModule
        , i
        , len;
    for (moduleName in modulesList) {
        if (modulesList.hasOwnProperty(moduleName)) {
            currentModule = modulesList[moduleName];
            len = currentModule.moduleDependenciesNames.length;
            if (len) {
                for (i = 0; i < len; i++) {
                    dependenciesList.push(moduleName + ' < ' + currentModule.moduleDependenciesNames[i]);
                }
            } else {
                dependenciesList.push(currentModule.moduleName);
            }
        }
    }
    dependenciesList.sort(function(a, b){if (a > b) {return 1;} else if (a < b) {return -1;} else {return 0;}});
    return dependenciesList.join('\n');
}

// Функция формирования списка путей к файлам модулей
function generateModulePathList (mainModuleName, includeThisModule) {
    var moduleTypeMark = function () {return '';};
    if (mainModuleName !== undefined) {
        if (includeThisModule === true) {
            moduleTypeMark = function (currentModule) {
                if (currentModule.moduleName === mainModuleName) {
                    return '@'; // @ - main module
                } else if (isCurrentModuleIncludeMainModule(currentModule.moduleName)) {
                    return '*'; // * - dependency
                } else {
                    return ''; // '' - other
                }
                function isCurrentModuleIncludeMainModule (currentModuleName) {
                    var moduleDependenciesNames = window.requirejsModules.list[currentModuleName].moduleDependenciesNames
                        , i
                        , len = moduleDependenciesNames.length;
                    for (i = 0; i < len; i++) {
                        if (moduleDependenciesNames[i] === mainModuleName) {
                            return true;
                        }
                    }
                    return false;
                }
            };
        } else {
            moduleTypeMark = function (currentModule) {
                if (currentModule.moduleName === mainModuleName) {
                    return '@'; // @ - main module
                } else if (isDependency(currentModule.modulePath)) {
                    return '*'; // * - dependency
                } else {
                    return ''; // '' - other
                }
                function isDependency (currentModulePath) {
                    var moduleDependenciesTreeObject = generateModuleDependenciesTreeObject(mainModuleName)
                        , dependencies = moduleDependenciesTreeObject.dependencies
                        , i
                        , len = dependencies.length;
                    for (i = 0; i < len; i++) {
                        if (dependencies[i].path === currentModulePath) {
                            return true;
                        }
                    }
                    return false;
                }
            };
        }
    }
    var pathList = []
        , modulesList = window.requirejsModules.list
        , moduleName
        , currentModule;
    for (moduleName in modulesList) {
        if (modulesList.hasOwnProperty(moduleName)) {
            currentModule = modulesList[moduleName];
            pathList.push(currentModule.modulePath + moduleTypeMark(currentModule));
        }
    }
    pathList.sort(function(a, b){if (a > b) {return 1;} else if (a < b) {return -1;} else {return 0;}});
    return pathList.join('\n');
}

// Функция формирования иерархического объекта, состоящего из путей к модулям, для построения дерева вложенности файлов и папок
function generateModulePathTreeObject (mainModuleName, includeThisModule) {
    var pathList = generateModulePathList(mainModuleName, includeThisModule).split('\n');
    return convertArrayOfDelimitedStringsIntoHierarchicalObject(pathList, '/');
}

// Функция преобразования массива из строк в иерархический объект вложенности элементов друг в друга
function convertArrayOfDelimitedStringsIntoHierarchicalObject (input, delimiter) {
    // input = ['Fred-Jim-Bob', 'Fred-Jim', 'Fred-Thomas-Rob', 'Fred'];
    // delimiter = '-';
    var output = []
        , i, j, k
        , inputLength
        , chain, chainLength
        , currentNode, currentNodeLength
        , wantedNode
        , lastNode
        , newNode;
    for (i = 0, inputLength = input.length; i < inputLength; i++) {
        chain = input[i].split(delimiter);
        currentNode = output;
        for (j = 0, chainLength = chain.length; j < chainLength; j++) {
            wantedNode = chain[j];
            lastNode = currentNode;
            for (k = 0, currentNodeLength = currentNode.length; k < currentNodeLength; k++) {
                if (currentNode[k].name === wantedNode) {
                    currentNode = currentNode[k].dependencies;
                    break;
                }
            }
            // If we couldn't find an item in this list of dependencies
            // that has the right name, create one:
            if (lastNode === currentNode) {
                currentNode[k] = {name: wantedNode, dependencies: [], path: input[i]}; // Добавлен path для того, чтобы выделять элементы в дереве
                newNode = currentNode[k];
                currentNode = newNode.dependencies;
            }
        }
    }
    return output.length > 0 ? output[0] : {};
}

// Функция построения дерева вложенности файлов и папок с отображением зависимостей модулей друг от друга
function drawModulePathTreeGraphWithDependencies (mainModuleName, options) {
    if (options === undefined) {options = {};}
    function chr (s) {
        var chars = {
              '─' : '-'   // ─ box drawing light horizontal
            , '│' : '|'   // │ box drawing light vertical
            , '┐' : '\\'  // ┐ box drawing light down and left
            , '┘' : '/'   // ┘ box drawing light up and left
            , '┌' : '/'   // ┌ box drawing light down and right
            , '└' : '\\'  // └ box drawing light up and right
            , '┤' : '+' // ┤ box drawing light vertical and left
            , '├' : '+' // ├ box drawing light vertical and right
            , '┬' : '+' // ┬ box drawing light down and horizontal
            , '┴' : '+' // ┴ box drawing light up and horizontal
        };
        if (options.unicode === false) {return chars[s];} else {return s;}
    }
    var pathsTreeArray = (drawTreeGraph(generateModulePathTreeObject(mainModuleName, options.includeThisModule), '||')).split('||').slice(1)
        , i
        , j
        , len = pathsTreeArray.length
        , repeat
        , charactersInRow
        , maxCharactersInRow = 0
        , dependencyIndexes = []
        , dependencyIndexesLength;
    for (i = 0; i < len; i++) {
        pathsTreeArray[i] = pathsTreeArray[i].replace('\n', '');
        if (pathsTreeArray[i].indexOf('*') > -1) {
            dependencyIndexes.push(i);
        }
        if (pathsTreeArray[i].indexOf('@') > -1) {
            dependencyIndexes.push(i);
            pathsTreeArray[i] = pathsTreeArray[i].replace('@', ' ]*');
            pathsTreeArray[i] = pathsTreeArray[i].split(' ');
            pathsTreeArray[i][pathsTreeArray[i].length - 2] = '[ ' + pathsTreeArray[i][pathsTreeArray[i].length - 2];
            pathsTreeArray[i] = pathsTreeArray[i].join(' ');
        }
        charactersInRow = pathsTreeArray[i].length;
        if (charactersInRow > maxCharactersInRow) {
            maxCharactersInRow = charactersInRow;
        }
    }
    maxCharactersInRow += 2;
    dependencyIndexesLength = dependencyIndexes.length;
    if (dependencyIndexesLength > 0) {
        if (dependencyIndexesLength === 1) {
            pathsTreeArray[dependencyIndexes[0]] = pathsTreeArray[dependencyIndexes[0]].replace('*', '');
        } else if (dependencyIndexesLength === 2) {
            for (i = dependencyIndexes[0]; i <= dependencyIndexes[1]; i++) {
                if (i === dependencyIndexes[0]) {
                    pathsTreeArray[i] = pathsTreeArray[i].replace('*', '');
                    charactersInRow = pathsTreeArray[i].length;
                    repeat = maxCharactersInRow - charactersInRow;
                    for (j = 0; j < repeat; j++) {
                        if (j === 0) {
                            pathsTreeArray[i] += ' ';
                        } else if (j === repeat - 1) {
                            pathsTreeArray[i] += chr('┐');
                        } else {
                            pathsTreeArray[i] += chr('─');
                        }
                    }
                } else if (i === dependencyIndexes[1]) {
                    pathsTreeArray[i] = pathsTreeArray[i].replace('*', '');
                    charactersInRow = pathsTreeArray[i].length;
                    repeat = maxCharactersInRow - charactersInRow;
                    for (j = 0; j < repeat; j++) {
                        if (j === 0) {
                            pathsTreeArray[i] += ' ';
                        } else if (j === repeat - 1) {
                            pathsTreeArray[i] += chr('┘');
                        } else {
                            pathsTreeArray[i] += chr('─');
                        }
                    }
                } else {
                    charactersInRow = pathsTreeArray[i].length;
                    repeat = maxCharactersInRow - charactersInRow;
                    for (j = 0; j < repeat; j++) {
                        if (j === repeat - 1) {
                            pathsTreeArray[i] += chr('│');
                        } else {
                            pathsTreeArray[i] += ' ';
                        }
                    }
                }
            }
        } else if (dependencyIndexesLength > 2) {
            for (i = dependencyIndexes[0]; i <= dependencyIndexes[dependencyIndexesLength - 1]; i++) {
                if (i === dependencyIndexes[0]) {
                    pathsTreeArray[i] = pathsTreeArray[i].replace('*', '');
                    charactersInRow = pathsTreeArray[i].length;
                    repeat = maxCharactersInRow - charactersInRow;
                    for (j = 0; j < repeat; j++) {
                        if (j === 0) {
                            pathsTreeArray[i] += ' ';
                        } else if (j === repeat - 1) {
                            pathsTreeArray[i] += chr('┐');
                        } else {
                            pathsTreeArray[i] += chr('─');
                        }
                    }
                } else if (i === dependencyIndexes[dependencyIndexesLength - 1]) {
                    pathsTreeArray[i] = pathsTreeArray[i].replace('*', '');
                    charactersInRow = pathsTreeArray[i].length;
                    repeat = maxCharactersInRow - charactersInRow;
                    for (j = 0; j < repeat; j++) {
                        if (j === 0) {
                            pathsTreeArray[i] += ' ';
                        } else if (j === repeat - 1) {
                            pathsTreeArray[i] += chr('┘');
                        } else {
                            pathsTreeArray[i] += chr('─');
                        }
                    }
                } else if ((function(){
                    var middleDependencyIndexes = dependencyIndexes.slice(1, - 1);
                    for (var z = 0, len = middleDependencyIndexes.length; z < len; z++) {
                        if (i === middleDependencyIndexes[z]) {return true;}
                    }
                    return false;
                })()) {
                    pathsTreeArray[i] = pathsTreeArray[i].replace('*', '');
                    charactersInRow = pathsTreeArray[i].length;
                    repeat = maxCharactersInRow - charactersInRow;
                    for (j = 0; j < repeat; j++) {
                        if (j === 0) {
                            pathsTreeArray[i] += ' ';
                        } else if (j === repeat - 1) {
                            pathsTreeArray[i] += chr('┤');
                        } else {
                            pathsTreeArray[i] += chr('─');
                        }
                    }
                } else {
                    charactersInRow = pathsTreeArray[i].length;
                    repeat = maxCharactersInRow - charactersInRow;
                    for (j = 0; j < repeat; j++) {
                        if (j === repeat - 1) {
                            pathsTreeArray[i] += chr('│');
                        } else {
                            pathsTreeArray[i] += ' ';
                        }
                    }
                }
            }
        }
    }
   
    for (i = 0; i < len; i++) {
        pathsTreeArray[i] += '\n';
    }
   
    return pathsTreeArray.join('');
}

// Функция построения дерева потомков элемента
function drawTreeGraph (obj, prefix, options) {
    if (typeof obj === 'string') {obj = {name: obj};}
    if (prefix === undefined) {prefix = '';}
    if (options === undefined) {options = {};}

    function chr (s) {
        var chars = {
              '─' : '-'   // ─ box drawing light horizontal
            , '│' : '|'   // │ box drawing light vertical
            , '┐' : '\\'  // ┐ box drawing light down and left
            , '┘' : '/'   // ┘ box drawing light up and left
            , '┌' : '/'   // ┌ box drawing light down and right
            , '└' : '\\'  // └ box drawing light up and right
            , '┤' : '+' // ┤ box drawing light vertical and left
            , '├' : '+' // ├ box drawing light vertical and right
            , '┬' : '+' // ┬ box drawing light down and horizontal
            , '┴' : '+' // ┴ box drawing light up and horizontal
        };
        if (options.unicode === false) {return chars[s];} else {return s;}
    }

    var dependencies = obj.dependencies || []
        , lines = (obj.name || '').split('\n')
        , splitter = '\n' + prefix + (dependencies.length ? chr('│') : ' ') + ' ';
 
    return prefix
           + lines.join(splitter) + '\n'
           + dependencies.map(function (module, index) {
                                    var last = (index === dependencies.length - 1)
                                       , more = (module.dependencies && module.dependencies.length)
                                       , prefix_ = prefix + (last ? ' ' : chr('│')) + ' ';
                                    return prefix
                                           + (last ? chr('└') : chr('├')) + chr('─')
                                           + (more ? chr('┬') : chr('─')) + ' '
                                           + drawTreeGraph(module, prefix_, options).slice(prefix.length + 2);
                               }).join('');
}

Файл modulemap.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=8" />
    <meta http-equiv="cache-control" content="max-age=0" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
    <meta http-equiv="pragma" content="no-cache" />
    <link href="style/lib/modulemap/modulemap.css?v=1.0.0" rel="stylesheet" type="text/css" />
    <link href="style/lib/syntaxhighlighter/shCore.css?v=1.0.0" rel="stylesheet" type="text/css" />
    <link href="style/lib/syntaxhighlighter/shCoreDefault.css?v=1.0.0" rel="stylesheet" type="text/css" />
    <link href="style/lib/syntaxhighlighter/shCoreForModuleMap.css?v=1.0.0" rel="stylesheet" type="text/css" />
    <style type="text/css">.syntaxhighlighter {margin-bottom: 0;}</style>
    <script src="js/lib/syntaxhighlighter/shCore-with-fix.js?v=1.0.0" type="text/javascript"></script>
    <script src="js/lib/syntaxhighlighter/shBrushJScript.js?v=1.0.0" type="text/javascript"></script>
    <script src="js/lib/jquery/jquery.js?v=1.0.0" type="text/javascript"></script>
    <script src="js/lib/js-beautify/beautify.js?v=1.0.0" type="text/javascript"></script>
    <script data-main="js/lib/modulemap/modulemap.js?v=1.0.0" src="js/lib/require/require.js?v=1.0.0" type="text/javascript"></script>
    <!--<script src="js/lib/modulemap/modulemap.js?v=1.0.0" type="text/javascript"></script>-->
    <title>Аудит : Карта модулей</title>
</head>
<body>
    <table id="header"></table>
    <div id="grid" class="hidden">
        <table id="module-grid-section-control">
            <tr>
                <td id="module-grid-section-control-empty"></td>
                <td id="module-grid-section-visibility-control">
                    <div class="visibility-filter">
                        <div id="tree-section-visibility" class="visible"></div><div id="map-section-visibility" class="visible"></div><div id="file-section-visibility" class="visible"></div>
                    </div>
                </td>
                <td id="module-grid-section-control-empty"></td>
            </tr>
        </table>
        <table class="module-grid">
            <tr>
                <td class="tree-section">
                    <div class="module-filter">
                        <div>Показывать от чего зависит модуль</div><div>Показывать куда входит модуль</div>
                    </div>
                    <div class="folder-tree">
                        <ul class="list-tree"></ul>
                    </div>
                 </td>
                <td class="map-section">
                    <div class="map-filter">
                        <div>Горизонтальная схема</div><div>Вертикальная схема</div>
                    </div>
                    <div id="module-tree"></div>
                </td>
                <td class="file-section">
                    <div class="file-filter">
                        <div>Исходный код</div><div>Переформатированный код</div>
                    </div>
                    <div id="file-code">
                       <p id="file-code-hint">Кликните на модуль для отображения его кода</p>
                    </div>
                </td>
            </tr>
        </table>
    </div>
</body>
</html>

Файл modulemap.css

/*****************************************************************\
  Reset CSS
\*****************************************************************/

html {
    font-size: 100.01%;  /* Bug fix for Opera */
}

body {
    width: 99%;
    min-width: 1200px;
    margin: 10px;
    margin-left: auto;
    margin-right: auto;
    padding: 0;
    text-align: center;
    font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif;
    font-size: 62.5%;
    background-color: #ffffff;
    color: #333333;
}

div,h1,h2,h3,h4,h5,h6,p,img,ol,ul,li,table,th,td,form,select,fieldset {
    margin: 0;
    padding: 0;
    text-align: left;
}

article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
    display: block;
}

ol,ul {
    list-style: none;
}

table {
    border: 0; /* border="0" */
    border-collapse: collapse; /* cellspacing="0" */
}

table tr th {
    padding: 0; /* cellpadding="0" */
}

table tr td {
    padding: 0; /* cellpadding="0" */
}

input,textarea,select {
    font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif;
    font-size: 1.0em;
    color: #333333;
}

/* don't show the x for text inputs */
input[type=text]::-ms-clear {
    width : 0;
    height: 0;
}

/* don't show the eye for password inputs */
input[type=password]::-ms-reveal {
    width : 0;
    height: 0;
}

textarea {
    resize: none;
}

a img {
    border: none;
}

a {
    color: #333333;
    text-decoration: underline;
}

a:link {
    color: #333333;
    text-decoration: underline;
}

a:visited {
    color: #333333;
    text-decoration: none;
}

a:hover {
    color: #333333;
    text-decoration: none;
}

a:active {
    color: #333333;
    text-decoration: none;
}

a:active,a:focus,img {
    outline: 0; /* Firefox anchor border remove */
}

/*****************************************************************\
  Common classes
\*****************************************************************/

/* Hidden element */
.hidden {
    display: none !important;
}

/*****************************************************************\
  Simple button
\*****************************************************************/

div.btn {
    display: inline-block;
    min-height: 16px;
    padding: 3px 9px;
    background-color: #2497e4;
    text-align: center;
    color: #ffffff;
    white-space: nowrap;
    cursor: pointer;
}

div.btn:hover {
    background-color: #2e82ca;
}

div.btn-icon {
    padding-left: 27px;
    background-repeat: no-repeat;
    background-position: 9px 4px;
}

div.btn-disabled {
    background-color: #bfbfbf !important;
    cursor: default !important;
}

/*****************************************************************\
  List tree
\*****************************************************************/

ul.list-tree ul {
    margin: 0;
    padding: 0;
    margin-left: -24px;
    padding-left: 24px;
}

ul.list-tree ul ul {
    background: url(img/list-tree/list-item-contents.png) repeat-y left;
}

ul.list-tree li.last > ul {
    background-image: none;
}

ul.list-tree li {
    position: relative;
    margin: 0;
    padding: 0;
    line-height: 24px;
    background: url(img/list-tree/list-item-root.png) no-repeat top left;
}

ul.list-tree li li {
    padding-left: 24px;
    background-image: url(img/list-tree/list-item.png);
}

ul.list-tree li.last {
    background-image: url(img/list-tree/list-item-last.png);
}

ul.list-tree li.opened {
    background-image: url(img/list-tree/list-item-open.png);
}

ul.list-tree li.closed {
    background-image: url(img/list-tree/list-item.png);
}

ul.list-tree li.last.opened {
    background-image: url(img/list-tree/list-item-last-open.png);
}

ul.list-tree li.last.closed {
    background-image: url(img/list-tree/list-item-last.png);
}

ul.list-tree li.semiclosed {
    background-image: url(img/list-tree/list-item-open.png);
}

ul.list-tree li.last.semiclosed {
    background-image: url(img/list-tree/list-item-last-open.png);
}

ul.list-tree li.unclosed  {
    background-image: url(img/list-tree/list-item-open.png);
}

ul.list-tree li.last.unclosed  {
    background-image: url(img/list-tree/list-item-last-open.png);
}

ul.list-tree li.root {
    background-image: url(img/list-tree/list-item-root.png);
}

ul.list-tree li.root.closed {
    background-image: none;
}

ul.list-tree li.root.single {
    background-image: none;
}

ul.list-tree span.element {
    display: inline-block;
    margin-left: 15px;
    padding: 0px 5px;
    vertical-align: top;
    cursor: pointer;
    white-space: nowrap;
}

ul.list-tree span.element:hover {
    background: #2b7fed;
    color: #ffffff;
}

ul.list-tree span.active {
    background-color: #3c90fe !important;
    color: #ffffff;
    cursor: default;
}

ul.list-tree li span.selected {
    background-color: #59adfe;
    color: #ffffff;
}

ul.list-tree li span.button {
    position: absolute;
    display: block;
    width: 11px;
    height: 11px;
    margin-top: 7px;
    background-image: url(img/list-tree/button.png);
    background-repeat:  no-repeat;
    background-position: top left;
}

ul.list-tree li.opened > span.button {
    background-image: url(img/list-tree/button-open.png);
    cursor: pointer;
}

ul.list-tree li.closed > span.button {
    background-image: url(img/list-tree/button-closed.png);
    cursor: pointer;
}

ul.list-tree li.semiclosed > span.button {
    background-image: url(img/list-tree/button-closed.png);
    cursor: pointer;
}

ul.list-tree li span.text {
    margin-left: 2px;
}

/*****************************************************************\
  Header
\*****************************************************************/

table#header {
    width: 100%;
}

/*****************************************************************\
  Grid
\*****************************************************************/

 div#grid {
    position: relative;
    background-color: #ffffff;
    font-size: 1.2em;
 }

/*****************************************************************\
  Module grid section control
\*****************************************************************/

table#module-grid-section-control {
    width: 100%;
}

table#module-grid-section-control tr td {
    padding: 5px;
    border: 1px solid #0c6ed5;
}

table#module-grid-section-control tr td#module-grid-section-control-empty {
    width: auto;
}

table#module-grid-section-control tr td#module-grid-section-visibility-control {
    width: 100px;
    text-align: center;
}

/*****************************************************************\
  Module grid section control > Visibility filter
\*****************************************************************/

div.visibility-filter {
    width: 100px;
    height: 23px;
}

div.visibility-filter div {
    display: inline-block;
    width: 30px;
    height: 23px;
    background-image: url(img/icons/sections.gif);
    background-repeat: no-repeat;
    cursor: pointer;
}

div.visibility-filter div#tree-section-visibility {
    background-position: 0px 0px;
}

div.visibility-filter div#map-section-visibility {
    margin-left: 5px;
    background-position: -30px 0px;
}

div.visibility-filter div#file-section-visibility {
    margin-left: 5px;
    background-position: -60px 0px;
}

div.visibility-filter div.visible {
    background-color: #0c6ed5;
}

div.visibility-filter div.invisible {
    background-color: #c0c0c0;
}

div.visibility-filter div:hover {
    background-color: #0055b5;
}

/*****************************************************************\
  Module grid
\*****************************************************************/

table.module-grid {
    width: 100%;
}

table.module-grid tr td {
    border: 1px solid #0c6ed5;
    border-top: none;
    vertical-align: top;
}

table.module-grid tr td.tree-section {
    width: auto;
    text-align: center;
}

table.module-grid tr td.map-section {
    width: 40%;
    text-align: center;
}

table.module-grid tr td.file-section {
    width: 30%;
    text-align: center;
}

table.module-grid tr td.width-auto {
    width: auto !important;
}

table.module-grid tr td.width-50 {
    width: 50% !important;
}

table.module-grid tr td.width-70  {
    width: 100% !important;
}

table.module-grid tr td.width-100  {
    width: 100% !important;
}

/*****************************************************************\
  Module grid > Module filter
\*****************************************************************/

div.module-filter {
    display: inline-block;
    margin-top: 15px;
    margin-left: 5px;
    margin-right: 5px;
    border-left: 1px solid #0c6ed5;
    white-space: nowrap;
}

div.module-filter div {
    display: inline-block;
    padding: 4px 15px;
    border-top: 1px solid #0c6ed5;
    border-bottom: 1px solid #0c6ed5;
    border-right: 1px solid #0c6ed5;
    text-align: center;
    white-space: nowrap;
    color: #2497e4;
    cursor: pointer;
}

div.module-filter div:hover {
    color: #0c6ed5;
}

div.module-filter div.active {
    background-color: #0c6ed5;
    color: #ffffff;
    cursor: default;
}

div.module-filter div.active:hover {
    background-color: #0c6ed5;
    color: #ffffff;
}

/*****************************************************************\
  Module grid > Folder tree
\*****************************************************************/

div.folder-tree {
    overflow: auto;
    margin-top: 15px;
    margin-left: 5px;
    margin-right: 5px;
}

div.folder-tree ul.list-tree span.element span.icon {
    display: inline-block;
    width: 15px;
    height: 15px;
    line-height: 0;
    margin-right: 2px;
    vertical-align: text-top;
}

div.folder-tree ul.list-tree span.element span.folder {
    background-image: url(img/icons/folder-small.gif);
}

div.folder-tree ul.list-tree span.element span.file {
    background-image: url(img/icons/file-small.gif);
}

div.folder-tree ul.list-tree span.associated-file {
    background-color: #5bafed;
    color: #ffffff;
}

div.folder-tree ul.list-tree span.element:hover {
    background-color: #ffffff;
    color: #333333;
    cursor: default;
}

div.folder-tree ul.list-tree span.selectable:hover {
    background-color: #2b7fed;
    color: #ffffff;
    cursor: pointer;
}

/*****************************************************************\
  Module grid > File connection line
\*****************************************************************/

div.connection-line {
    overflow: hidden;
    position: absolute;
    width: 1px;
    height: 1px;
    top: 0px;
    left: 0px;
    background-color: #c0c0c0;
    cursor: default;
}

/*****************************************************************\
  Module grid > Map filter
\*****************************************************************/

div.map-filter {
    display: inline-block;
    margin-top: 15px;
    margin-left: 5px;
    margin-right: 5px;
    border-left: 1px solid #0c6ed5;
    white-space: nowrap;
}

div.map-filter div {
    display: inline-block;
    padding: 4px 15px;
    border-top: 1px solid #0c6ed5;
    border-bottom: 1px solid #0c6ed5;
    border-right: 1px solid #0c6ed5;
    text-align: center;
    white-space: nowrap;
    color: #2497e4;
    cursor: pointer;
}

div.map-filter div:hover {
    color: #0c6ed5;
}

div.map-filter div.active {
    background-color: #0c6ed5;
    color: #ffffff;
    cursor: default;
}

div.map-filter div.active:hover {
    background-color: #0c6ed5;
    color: #ffffff;
}

/*****************************************************************\
  Module grid > Module tree
\*****************************************************************/

div#module-tree {
    position: relative;
    overflow: auto;
    margin-top: 15px;
    margin-left: 5px;
    margin-right: 5px;
}

/* Стиль оформления развернутого блока */
div#module-tree div.node {
    position: absolute;
    overflow: hidden;
    line-height: 30px;
    background-color: #ffffff;
    border: 1px solid #c0c0c0;
    vertical-align: middle;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
    z-index: 100;
}

/* Стиль оформления свернутого блока */
div#module-tree div.node-collapsed {
    position: absolute;
    overflow: hidden;
    line-height: 30px;
    background-color: #ffffff;
    border: 1px solid #c0c0c0;
    vertical-align: middle;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
    z-index: 100;
    border: 1px solid #0c6ed5;
}

/* Соединительная линия между блоками */
div#module-tree div.node-line {
    position: absolute;
    overflow: hidden;
    background-color: #c0c0c0;
}

/* Стиль выделенного блока */
div#module-tree div.active {
    background-color: #3c90fe !important;
    color: #ffffff;
}

/* Стиль блока с наведенным на него крусором */
div#module-tree div.node:hover {
    background: #2b7fed;
    color: #ffffff;
}

div#module-tree div.node-collapsed:hover {
    background: #2b7fed;
    color: #ffffff;
}

/*****************************************************************\
  Module grid > File filter
\*****************************************************************/

div.file-filter {
    display: inline-block;
    margin-top: 15px;
    margin-left: 5px;
    margin-right: 5px;
    border-left: 1px solid #0c6ed5;
    white-space: nowrap;
}

div.file-filter div {
    display: inline-block;
    padding: 4px 15px;
    border-top: 1px solid #0c6ed5;
    border-bottom: 1px solid #0c6ed5;
    border-right: 1px solid #0c6ed5;
    text-align: center;
    white-space: nowrap;
    color: #2497e4;
    cursor: pointer;
}

div.file-filter div:hover {
    color: #0c6ed5;
}

div.file-filter div.active {
    background-color: #0c6ed5;
    color: #ffffff;
    cursor: default;
}

div.file-filter div.active:hover {
    background-color: #0c6ed5;
    color: #ffffff;
}

/*****************************************************************\
  Module grid > File code
\*****************************************************************/

div#file-code {
    position: relative;
    overflow: auto;
    margin-top: 15px;
    margin-left: 5px;
    margin-right: 5px;
}

div#file-code p#file-code-hint {
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}

/* Временные стили */

div.folder-tree,
div#module-tree,
div#file-code {height: 650px;}

Файл modulemap.js

require.config({
      baseUrl: 'js'
    , paths: {
          text: 'lib/require/text'
    }
});

$(document).ready(function(){

    // Содержимое модулей
    var files = {};
   
    loadAllFilesSourceCode();
   
    // Функция загрузки кода всех файлов модуля
    function loadAllFilesSourceCode () {
        var modulesList = window.opener.requirejsModules.list
            , key
            , path;
        for (key in modulesList) {
            path = modulesList[key].modulePath;
            if (!files.hasOwnProperty(path)) {
                files[path] = $.ajax({type: 'GET', url: path, async: false}).responseText;
            }
            /*
            path = path.substring(3);
            if (!files.hasOwnProperty(path)) {
                require(['text!' + path], function(code){
                    files[path] = code;
                });
            }
            */
        }
    }
   
    // Функция загрузки кода файла модуля
    /* Функция устарела
    function loadFileSourceCode (path) {
        path = path.substring(3);
        if (!files.hasOwnProperty(path)) {
            require(['text!' + path], function(code){
                files[path] = code;
                showFileSourceCode(path);
            });
        } else {
            showFileSourceCode(path);
        }
    }
    */
   
    // Функция отображения кода модуля
    function showFileSourceCode () {
        var selectedFilePath
            , fileCode;
        if ($('.folder-tree .selectable').hasClass('active')) {
            selectedFilePath = $('.folder-tree .active').attr('path');
            fileCode = files[selectedFilePath];
            if ($('.file-filter div').first().hasClass('active')) {
                /*
                fileCode = fileCode.replace(/</gmi, '&lt;').replace(/\r\n/gmi, '<br/>').replace(/ /gmi, '&nbsp;');
                $('#file-code').empty().html(fileCode);
                */
                fileCode = fileCode.replace(/</gmi, '&lt;');
                fileCode = '<pre class="brush: javascript">' + fileCode + '</pre>';
                $('#file-code').empty().html(fileCode);
                SyntaxHighlighter.highlight(); // SyntaxHighlighter загружен в файле index.html перед загрузкой всех модулей
                $('.syntaxhighlighter .toolbar').remove();
                $('.syntaxhighlighter').attr('style', 'width: 1500px !important;');
            } else {
                fileCode = js_beautify(fileCode, {
                                                                      indent_size: 4
                                                                    , indent_char: ' '
                                                                    , max_preserve_newlines: -1
                                                                    , preserve_newlines: false
                                                                    , keep_array_indentation: false
                                                                    , break_chained_methods: false
                                                                    , indent_scripts: 'normal'
                                                                    , brace_style: 'collapse'
                                                                    , space_before_conditional: true
                                                                    , unescape_strings: false
                                                                    , jslint_happy: false
                                                                    , end_with_newline: false
                                                                    , wrap_line_length: 0
                                                                    , indent_inner_html: false
                                                                    , comma_first: true
                                                                    , e4x: false
                                                                });
                fileCode = fileCode.replace(/</gmi, '&lt;');
                fileCode = '<pre class="brush: javascript">' + fileCode + '</pre>';
                $('#file-code').empty().html(fileCode);
                SyntaxHighlighter.highlight(); // SyntaxHighlighter загружен в файле index.html перед загрузкой всех модулей
                $('.syntaxhighlighter .toolbar').remove();
                $('.syntaxhighlighter').attr('style', 'width: 1500px !important;');          
            }
        }
    }

    // Показать содержимое страницы
    $('#grid').removeClass('hidden');
   
    // Выделить первую кнопку фильтра модулей
    $('.module-filter div:first').addClass('active');

    // Изменить размеры блоков согласно размеру окна пользователя
    function resize () {
        var windowHeight = $(window).height()
            , bodyMarginTop = 11
            , bodyMarginBottom = 11
            , moduleGridSectionControlHeight = $('#module-grid-section-control').height()
            , gridContentHeight = 0
            , gridContentMinHeight = 245
            , moduleFilterMarginTop = 15
            , moduleFilterHeight = 0
            , folderTreeHeight = 0
            , folderTreeMarginTop = 15
            , folderTreeMarginBottom = 5
            , mapFilterMarginTop = 15
            , mapFilterHeight = 0
            , moduleTreeHight = 0
            , moduleTreeMarginTop = 15
            , moduleTreeMarginBottom = 5
            , fileFilterMarginTop = 15
            , fileFilterHeight = 0
            , fileCodeHight = 0
            , fileCodeMarginTop = 15
            , fileCodeMarginBottom = 5;
        gridContentHeight = windowHeight - bodyMarginTop - bodyMarginBottom - moduleGridSectionControlHeight;
        if (gridContentHeight < gridContentMinHeight) {gridContentHeight = gridContentMinHeight;}
        $('#grid').height(gridContentHeight);
        $('table.module-grid tr td.tree-section').height(gridContentHeight);
        moduleFilterHeight = $('.module-filter').first().height();
        mapFilterHeight = $('.map-filter').first().height();
        fileFilterHeight = $('.file-filter').first().height();
        folderTreeHeight = gridContentHeight
                                    - moduleFilterMarginTop
                                    - moduleFilterHeight
                                    - folderTreeMarginTop
                                    - folderTreeMarginBottom;
        moduleTreeHight = gridContentHeight
                                    - mapFilterMarginTop
                                    - mapFilterHeight
                                    - moduleTreeMarginTop
                                    - moduleTreeMarginBottom;
        fileCodeHight = gridContentHeight
                                    - fileCodeMarginTop
                                    - fileFilterHeight
                                    - fileCodeMarginTop
                                    - fileCodeMarginBottom;        
        $('.folder-tree').height(folderTreeHeight);
        $('#module-tree').height(moduleTreeHight);
        $('#file-code').height(fileCodeHight);
       
        var treeSectionWidth = 0
            , mapSectionWidth = 0
            , marginRight = 2
            , result;
           
        if (!$('.tree-section').hasClass('hidden')) {treeSectionWidth = $('#grid .tree-section').width();}
        if (!$('.map-section').hasClass('hidden')) {mapSectionWidth = $('#grid .map-section').width();}
       
        if (treeSectionWidth === 0 && mapSectionWidth === 0) {marginRight = -12;}
        if (treeSectionWidth === 0 && mapSectionWidth !== 0) {marginRight = -12;}
        if (treeSectionWidth !== 0 && mapSectionWidth === 0) {marginRight = -13;}
        if (treeSectionWidth !== 0 && mapSectionWidth !== 0) {marginRight = 2;}
       
        result = $('#header').width() - treeSectionWidth - mapSectionWidth + marginRight;
       
        $('#file-code').css({'width': result + 'px'});
    }

    resize();
   
    $(window).resize(resize);
   
    /*
    // Активировать изменение ширины колонок таблицы
    function initColumnWidthResize (id) {
   
        var border = $('#' + id + ' .border-section')
            , borderWidth = border.width()
            , treeSection = $('#' + id + ' .tree-section')
            , mapSection = $('#' + id + ' .map-section')
            , treeSectionMinWidth = 457
            , mapSectionMinWidth = 385
            , tableOffsetLeft
            , folderTree = $('#' + id + ' .folder-tree')
            , folderTreeSumOfMarginLeftAndRight = 10;
       
        border.on('mousedown.border', startBorderMove);
       
        function startBorderMove () {
            setTableOffsetLeft();
            $(document).on('mousemove.border', moveBorder).on('mouseup.border', endBorderMove);
            return false;
        }
       
        function setTableOffsetLeft () {
            tableOffsetLeft = border.parent().parent().offset().left;
        }
       
        function moveBorder (event) {
            setBorderPosition(event.pageX - tableOffsetLeft - borderWidth / 2);
            return false;
        }
       
        function endBorderMove () {
            $(document).off('mousemove.border', moveBorder).off('mouseup.border', endBorderMove);
            return false;
        }
       
        function setBorderPosition (x) {
            if (x < treeSectionMinWidth) {
                border.prev().css({'width': treeSectionMinWidth + 'px'});
                folderTree.css({'width': (treeSectionMinWidth - folderTreeSumOfMarginLeftAndRight) + 'px'});
            } else if (x > treeSection.width() + borderWidth + mapSection.width() - mapSectionMinWidth) {
                border.prev().css({'width': treeSection.width() + borderWidth + mapSection.width() - mapSectionMinWidth  + 'px'});
                folderTree.css({'width': (treeSection.width() + borderWidth + mapSection.width() - mapSectionMinWidth - folderTreeSumOfMarginLeftAndRight) + 'px'});
            } else {
                border.prev().css({'width': x + 'px'});
                folderTree.css({'width': (x - folderTreeSumOfMarginLeftAndRight) + 'px'});
            }
        }

    }
   
    initColumnWidthResize('grid');
    */
   
    // Активировать фильтр дерева папок и файлов
    function initFolderFilter () {
        $('.module-filter div').first().addClass('active');
       
        // Клик по любой кнопке фильтра
        $('.module-filter div').click(function(){
            // Выделение кнопки и отображение соответствующего ей блока с деревом
            if ($(this).hasClass('active')) {return;}
            $('.module-filter div').removeClass('active');
            $(this).addClass('active');
            var selectedFilePath
                , selectedModuleName
                , moduleList = window.opener.requirejsModules.list
                , moduleDependenciesTreeObject
                , moduleDependecies
                , moduleDependeciesLength
                , moduleDependenciesNames
                , moduleDependenciesNamesLength
                , key
                , i;
            if ($('.folder-tree .selectable').hasClass('active')) {
                $('.folder-tree .selectable').removeClass('associated-file');
                selectedFilePath = $('.folder-tree .active').attr('path');
                showFileSourceCode();
                for (key in moduleList) {
                    if (moduleList[key].modulePath === selectedFilePath) {
                        selectedModuleName = moduleList[key].moduleName;
                        break;
                    }
                }
                moduleDependenciesTreeObject = window.opener.requirejsModules.generateModuleDependenciesTreeObject(selectedModuleName);
                if ($(this).index() === 0) {
                    // Выделить модули, от которых зависит данный модуль
                    moduleDependecies = moduleDependenciesTreeObject.dependencies;
                    moduleDependeciesLength = moduleDependecies.length;
                    for (i = 0; i < moduleDependeciesLength; i++) {
                        $('.folder-tree .selectable').filter('[path="' + moduleDependecies[i].path + '"]').addClass('associated-file');
                    }
                } else if ($(this).index() === 1) {
                    // Выделить модули, куда входит данный модуль
                    for (key in moduleList) {
                        moduleDependenciesNames = moduleList[key].moduleDependenciesNames;
                        moduleDependenciesNamesLength = moduleDependenciesNames.length;
                        for (i = 0; i < moduleDependenciesNamesLength; i++) {
                            if (moduleDependenciesNames[i] === selectedModuleName) {
                                $('.folder-tree .selectable').filter('[path="' + moduleList[key].modulePath + '"]').addClass('associated-file');
                                break;
                            }
                        }
                    }
                }
                // Нарисовать соединительные линии для выделенных файлов
                drawFileConnectionLines();
            }
        });
       
    }

    initFolderFilter();

    // Обновить блок с деревом
    function updateFolderTree (data) {
        if ($.isEmptyObject(data)) {return;}
        var tree = '<ul class="list-tree">';
        if (data.hasOwnProperty('name')) {
            tree += '<li class="' + (data.dependencies.length > 0 ? 'unclosed ' : '') + 'root"><span class="button"></span>'
                                + '<span class="element' + (data.dependencies.length > 0 ? '' : ' selectable') + '"' + (data.dependencies.length > 0 ? '' : ' path="' + data.path + '"') + '>'
                                    + '<span class="icon ' + (data.dependencies.length > 0 ? 'folder' : 'file') + '"></span>'
                                    + '<span class="text">' + data.name + '</span>'
                                + '</span>';
            if (data.dependencies.length > 0) {
                tree += buildTree(data.dependencies);
            }
            tree += '</li>';
        }
        tree += '</ul>';
        $('.folder-tree').empty().append(tree);
        $('.list-tree li:last-child').not('.root').addClass('last');
        if (data.dependencies.length === 0) {
            $('.list-tree li').addClass('single');
        }
        function buildTree (data) {
            var tree = '<ul>'
                , length = data.length
                , i
                , last = length - 1;
            for (i = 0; i < length; i++) {
                tree += '<li class="' + (data[i].dependencies.length > 0 ? 'unclosed ' : '') + (i === last ? 'last' : '') + '"><span class="button"></span>'
                                    + '<span class="element' + (data[i].dependencies.length > 0 ? '' : ' selectable') + '"' + (data[i].dependencies.length > 0 ? '' : ' path="' + data[i].path + '"') + '>'
                                        + '<span class="icon ' + (data[i].dependencies.length > 0 ? 'folder' : 'file') + '"></span>'
                                        + '<span class="text">' + data[i].name + '</span>'
                                    + '</span>';
                if (data[i].dependencies.length > 0) {
                    tree += buildTree(data[i].dependencies);
                }
                tree += '</li>';
            }
            tree += '</ul>';
            return tree;
        }
    }

    var modulePathTreeObject = window.opener.requirejsModules.generateModulePathTreeObject();
    //window.opener.console.log(window.opener.requirejsModules.generateModulePathTreeObject());
   
    updateFolderTree(modulePathTreeObject);
   
    // Клик по файлам в дереве папок
    $(document).on('click', '.folder-tree .selectable', function(){
        if ($(this).hasClass('active')) {return;}
        $('.folder-tree .selectable').removeClass('active');
        $(this).addClass('active');
        var selectedFilePath
            , selectedModuleName
            , moduleList = window.opener.requirejsModules.list
            , moduleDependenciesTreeObject
            , moduleDependecies
            , moduleDependeciesLength
            , moduleDependenciesNames
            , moduleDependenciesNamesLength
            , key
            , i;
        if ($('.folder-tree .selectable').hasClass('active')) {
            $('.folder-tree .selectable').removeClass('associated-file');
            selectedFilePath = $('.folder-tree .active').attr('path');
            showFileSourceCode();
            for (key in moduleList) {
                if (moduleList[key].modulePath === selectedFilePath) {
                    selectedModuleName = moduleList[key].moduleName;
                    break;
                }
            }
            moduleDependenciesTreeObject = window.opener.requirejsModules.generateModuleDependenciesTreeObject(selectedModuleName);
            if ($('.module-filter .active').index() === 0) {
                // Выделить модули, от которых зависит данный модуль
                moduleDependecies = moduleDependenciesTreeObject.dependencies;
                moduleDependeciesLength = moduleDependecies.length;
                for (i = 0; i < moduleDependeciesLength; i++) {
                    $('.folder-tree .selectable').filter('[path="' + moduleDependecies[i].path + '"]').addClass('associated-file');
                }
            } else if ($('.module-filter .active').index() === 1) {
                // Выделить модули, куда входит данный модуль
                for (key in moduleList) {
                    moduleDependenciesNames = moduleList[key].moduleDependenciesNames;
                    moduleDependenciesNamesLength = moduleDependenciesNames.length;
                    for (i = 0; i < moduleDependenciesNamesLength; i++) {
                        if (moduleDependenciesNames[i] === selectedModuleName) {
                            $('.folder-tree .selectable').filter('[path="' + moduleList[key].modulePath + '"]').addClass('associated-file');
                            break;
                        }
                    }
                }
            }
            // Нарисовать соединительные линии для выделенных файлов
            drawFileConnectionLines();
            // Выделить модуль в дереве модулей
            $('#module-tree div').removeClass('active');
            $('#module-tree div').filter('[path="' + selectedFilePath + '"]').addClass('active');
        }
    });
   
    // Функция для определения положения элемента относиттльно заданного элемента
    // Пример использования: $('#innerElement').offsetRelativeTo('#outerElement').left;
    jQuery.fn.offsetRelativeTo = function (el) {
        var $el = $(el)
            , o1 = this.offset()
            , o2 = $el.offset();
        o1.top = o1.top - o2.top - $el.scrollTop();
        o1.left = o1.left - o2.left - $el.scrollLeft();
        return o1;
    };
   
    // Функция отрисовки соединительных линиц для выделенных файлов в дереве папок
    function drawFileConnectionLines () {
        // Удалить все соединительные линии
        $('.folder-tree .connection-line').remove();
        // Рассчитать отступ слева для каждого выделенного файла
        var allElements = $('.folder-tree .element')
            , allElementsLength = allElements.length
            , offsetTop
            , offsetLeft
            , width
            , horizontalConnectionLineBeginCoordinate
            , horizontalConnectionLineEndCoordinate
            , maxHorizontalConnectionLineEndCoordinate = 0
            , associatedFilesParameters = []
            , associatedFilesParametersLength
            , i;
        for (i = 0; i < allElementsLength; i++) {
            offsetTop = allElements.eq(i).offsetRelativeTo('.folder-tree:first').top; // Рассчитать отступ сверху каждого выделенного файла
            offsetLeft = allElements.eq(i).offsetRelativeTo('.folder-tree:first').left; // Рассчитать отступ слева каждого выделенного файла
            width = allElements.eq(i).width(); // Рассчитать ширину каждого выделенного файла
            horizontalConnectionLineBeginCoordinate = offsetLeft + width + 10; // Сложить отступ слева, ширину каждого выделенного файла и расстояние до начальной точки горизонтальной соединительной линии, получив тем самым начальную точку
            horizontalConnectionLineEndCoordinate = horizontalConnectionLineBeginCoordinate + 20; // Сложить отступ слева, ширину каждого выделенного файла и расстояние до конечной точки горизонтальной соединительной линии, получив тем самым конечную точку
            // Выбрать самую дальнюю координату конечной точки
            if (maxHorizontalConnectionLineEndCoordinate < horizontalConnectionLineEndCoordinate) {
                maxHorizontalConnectionLineEndCoordinate = horizontalConnectionLineEndCoordinate;
            }
            if (
                    allElements.eq(i).hasClass('associated-file')
                || allElements.eq(i).hasClass('active')
            ) {
                associatedFilesParameters.push({
                      offsetTop: offsetTop
                    , offsetLeft: offsetLeft
                    , width: width
                    , horizontalConnectionLineBeginCoordinate: horizontalConnectionLineBeginCoordinate
                    , horizontalConnectionLineEndCoordinate: horizontalConnectionLineEndCoordinate
                    , index: i
                });
            }
        }
        // Для каждого выделенного файла рассчиать ширину горизонтальной соединительной линии, отступ её от текста элемента и добавить горизонтальную линию в выделенный файл
        associatedFilesParametersLength = associatedFilesParameters.length;
        if (associatedFilesParametersLength > 1) {
            for (i = 0; i < associatedFilesParametersLength; i++) {
                allElements.eq(associatedFilesParameters[i].index).append('<div class="connection-line" style="width: ' + (maxHorizontalConnectionLineEndCoordinate - associatedFilesParameters[i].horizontalConnectionLineBeginCoordinate) + 'px; top: 12px; left: ' + (associatedFilesParameters[i].width + 53) + 'px;"></div>');
            }
            // Рассчитать расстояние по высоте между первым и последним выделенным файлом
            // Добавить в первый выделенный файл элемент div с высотой от первого до последнего выделенного файла
            allElements.eq(associatedFilesParameters[0].index).append('<div class="connection-line" style="height: ' + (associatedFilesParameters[associatedFilesParameters.length - 1].offsetTop - associatedFilesParameters[0].offsetTop + 1) + 'px; top: 12px; left: ' + (associatedFilesParameters[0].width + 53 + maxHorizontalConnectionLineEndCoordinate - associatedFilesParameters[0].horizontalConnectionLineBeginCoordinate) + 'px;"></div>');
        }
    }

    // Активировать фильтр карты модулей
    function initMapFilter () {
        $('.map-filter div').first().addClass('active');
       
        // Клик по любой кнопке фильтра
        $('.map-filter div').click(function(){
            // Выделение кнопки и отображение соответствующего ей блока в дереве
            if ($(this).hasClass('active')) {return;}
            $('.map-filter div').removeClass('active');
            $(this).addClass('active');
            changeModuleMapLayout();
            var selectedFilePath;
            if ($('.folder-tree .selectable').hasClass('active')) {
                selectedFilePath = $('.folder-tree .active').attr('path');
                showFileSourceCode();
                // Выделить модуль в дереве модулей
                $('#module-tree div').filter('[path="' + selectedFilePath + '"]').addClass('active');
            }
        });
       
    }

    initMapFilter();
   
    var moduleDependenciesTreeObject = window.opener.requirejsModules.generateModuleDependenciesTreeObject();
    //window.opener.console.log(window.opener.requirejsModules.generateModuleDependenciesTreeObject());

    // Нарисовать дерево в первый раз
    drawModuleMap();
   
    // Изменить отрисовку дерева модулей
    function changeModuleMapLayout () {
        drawModuleMap();
    }

    // Обновить дерево на экране
    function drawModuleMap () {
        drawTree({
              container: document.getElementById('module-tree')
            , rootNode: moduleDependenciesTreeObject
            , layout: (function(){
                             if ($('.map-filter div').eq(0).hasClass('active')) {
                                 return 'horizontal';
                              } else {
                                 return 'vertical';
                              }
                         })() // 'horizontal' or 'vertical'
            , onNodeClick: nodeSingleAndDoubleClick
        });
    }
   
    // Определение одиночного или двойного
    function nodeSingleAndDoubleClick () {
        var clicks = 0;
        return function () {
            var self = this;
            clicks++;
            if (clicks === 1) {
                setTimeout(function(){
                    if (clicks === 1) {
                        nodeClick(self);
                    } else {
                        nodeDoubleClick(self);
                    }
                    clicks = 0;
                }, 300);
            }
        };
    }
   
    // Одиночный клик на блоке элемента
    function nodeClick (self) {
        if ($(self).hasClass('active')) {return;}
        $('.node').removeClass('active');
        $('.node-collapsed').removeClass('active');
        $(self).addClass('active');
        var selectedFilePath
            , selectedModuleName
            , moduleList = window.opener.requirejsModules.list
            , moduleDependenciesTreeObject
            , moduleDependecies
            , moduleDependeciesLength
            , moduleDependenciesNames
            , moduleDependenciesNamesLength
            , key
            , i;
        if ($('.node').hasClass('active') || $('.node-collapsed').hasClass('active')) {
            selectedFilePath = $('#module-tree .active').attr('path');
            // Выделить модуль в дереве модулей
            $('#module-tree div').filter('[path="' + selectedFilePath + '"]').addClass('active');
            // Выделить файлы в дереве папок
            $('.folder-tree .selectable').removeClass('active').removeClass('associated-file');
            $('.folder-tree .selectable').filter('[path="' + selectedFilePath + '"]').addClass('active');
            showFileSourceCode();
            for (key in moduleList) {
                if (moduleList[key].modulePath === selectedFilePath) {
                    selectedModuleName = moduleList[key].moduleName;
                    break;
                }
            }
            moduleDependenciesTreeObject = window.opener.requirejsModules.generateModuleDependenciesTreeObject(selectedModuleName);
            if ($('.module-filter .active').index() === 0) {
                // Выделить модули, от которых зависит данный модуль
                moduleDependecies = moduleDependenciesTreeObject.dependencies;
                moduleDependeciesLength = moduleDependecies.length;
                for (i = 0; i < moduleDependeciesLength; i++) {
                    $('.folder-tree .selectable').filter('[path="' + moduleDependecies[i].path + '"]').addClass('associated-file');
                }
            } else if ($('.module-filter .active').index() === 1) {
                // Выделить модули, куда входит данный модуль
                for (key in moduleList) {
                    moduleDependenciesNames = moduleList[key].moduleDependenciesNames;
                    moduleDependenciesNamesLength = moduleDependenciesNames.length;
                    for (i = 0; i < moduleDependenciesNamesLength; i++) {
                        if (moduleDependenciesNames[i] === selectedModuleName) {
                            $('.folder-tree .selectable').filter('[path="' + moduleList[key].modulePath + '"]').addClass('associated-file');
                            break;
                        }
                    }
                }
            }
            // Нарисовать соединительные линии для выделенных файлов
            drawFileConnectionLines();
        }
    }

    // Двойной клик на блоке элемента
    function nodeDoubleClick (self) {
        if (self.node.dependencies && self.node.dependencies.length > 0) { // Если блок имеет потомков
            self.node.collapsed = !self.node.collapsed;
            drawModuleMap();
            var selectedFilePath;
            if ($('.folder-tree .selectable').hasClass('active')) {
                selectedFilePath = $('.folder-tree .active').attr('path');
                showFileSourceCode();
                // Выделить модуль в дереве модулей
                $('#module-tree div').filter('[path="' + selectedFilePath + '"]').addClass('active');
            }
        }
    }

    // Нарисовать дерево
    function drawTree (args) {

        // Подготовить элементы дерева для отрисовки на экране
        prepareNode({node: args.rootNode});

        // Построить вертикальное или горизонтальное дерево
        if (args.layout === 'vertical') {
            performLayoutV(args.rootNode); // Построить вертикальное дерево
        } else {
            performLayoutH(args.rootNode); // Построить горизонтальное дерево
        }

        // Нарисовать контейнеры элементов дерева
        args.container.innerHTML = ''; // Удалить все элементы из блока DIV, в который будет вставлено наше дерево
        drawNode(args.rootNode, args.container, args);

        // Нарисовать соединительные линии между блоками элементов
        drawLines(args.rootNode, args.container);
    }

    // Проверить раскрыт ли узел и имеет ли он потомков.
    function isNodeCollapsedAndHasChildren (node) {
    if (!node.collapsed && node.dependencies && node.dependencies.length > 0) { // Элемент имеет потомков и раскрыт
            return true;
        }
        return false;
    }

    // Подготовить элементы дерева для отрисовки на экране
    function prepareNode (args) {

        var node = args.node // Узел
            , level = args.level // Уровень вложенности узла
            , parentNode = args.parentNode // Родительский узел
            , leftNode = args.leftNode // Самый левый (первый) узел потомок
            , rightLimits = args.rightLimits // Массив узлов потомков
            , nodesLength // Количество потомков в данному узле
            , i;

        if (level === undefined) {level = 0;}
        if (parentNode === undefined) {parentNode = null;}
        if (leftNode === undefined) {leftNode = null;} // Самый левый (первый) узел потомок
        if (rightLimits === undefined) {rightLimits = [];} // Массив элементов потомков

        node.level = level;
        node.parentNode = parentNode;
        node.leftNode = leftNode;

        if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт
            nodesLength = node.dependencies.length;
            for (i = 0; i < nodesLength; i++) {
                leftNode = null;
                if (i === 0 && rightLimits[level] !== undefined) {
                    leftNode = rightLimits[level];
                }
                if (i > 0) {
                    leftNode = node.dependencies[i - 1]; // Получить самый левый (первый) узел потомок
                }
                if (i === (nodesLength - 1)) {
                    rightLimits[level] = node.dependencies[i]; // Получить самый правый (последний) узел потомок
                }
                // Приступить к обработке следующего элемента дерева
                prepareNode({
                      node: node.dependencies[i]
                    , level: level + 1
                    , parentNode: node
                    , leftNode: leftNode
                    , rightLimits: rightLimits
                });
            }
        }
    }

    // Построить вертикальное дерево
    function performLayoutV (node) {

        var nodeHeight = 30 // Высота блока узла в "px"
            , nodeWidth = 170 // Ширина блока узла в "px"
            , nodeMarginLeft = 50 // Отступ слева для блока узла в "px"
            , nodeMarginTop = 15 // было 30 // Отступ сверху для блока узла в "px"
            , nodeTop = 0 // Начальная координат верхней стороны родительского узла
            , nodesLength // Количество потомков в данному узле
            , childrenHeight = 0  // Высота, занимаемая потомками данного узла
            , i;

        // Перед посроением данного узла мы предварительно строим узлы всех его мотомков
        if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт

            nodesLength = node.dependencies.length;

            // Сначала построить вертикальное дерево для потомков данного узла
            for (i = 0; i < nodesLength; i++) {
                performLayoutV(node.dependencies[i]); // Построить вертикальное дерево для потомков данного узла
            }

            // Затем построить вертикальное дерево для самого родительского узла

            // Родительский узел всегда находится в центре перед своими потомками
            // Высота, занимаемая потомками, равна сумме координаты верхней стороны крайнего нижнего (последнего) потомка с его высотой
            // минус координата верхней стороны крайнего верхнего (первого) потомка
            childrenHeight = (node.dependencies[nodesLength - 1].top + node.dependencies[nodesLength - 1].height) - node.dependencies[0].top;

            // Начальная координат верхней стороны родительского узла, размещаемого в центре перед потомками,
            // равна сумме начальной координаты верхней стороны крайнего верхнего (первого) потомка и половине высоты, занимаемой
            // всеми потомками данного узла минус половина высоты самого родительского узла
            nodeTop = (node.dependencies[0].top + (childrenHeight / 2)) - (nodeHeight / 2);

            // Верний элемент находится над предыдущим сестринским элементом?
            // В этом случае переместить его вниз
             var newTop
                 , diff;
            if (node.leftNode && ((node.leftNode.top + node.leftNode.height + nodeMarginTop) > nodeTop)) {
                newTop = node.leftNode.top + node.leftNode.height + nodeMarginTop;
                diff = newTop - nodeTop;
                // Также переместить моих потомков вниз
                moveBottom(node.dependencies, diff);
                nodeTop = newTop;
            }

        } else {
            // Мой верх находится за верхним сестринским элементом
            if (node.leftNode) {
                nodeTop = node.leftNode.top + node.leftNode.height + nodeMarginTop;
            }
        }

        node.top = nodeTop;

        // Left зависит только от уровня вложенности level
        node.left = (nodeMarginLeft * (node.level + 1)) + (nodeWidth * (node.level + 1));
        // Размер блока всегда постоянен
        node.height = nodeHeight;
        node.width = nodeWidth;

        // Рассчитать соединительные точки
        // Потомок: Где линии выходят для соединения узла с его потомками
        var pointX = node.left + nodeWidth
            , pointY = nodeTop + (nodeHeight / 2);
        node.childrenConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'vertical'
        };
        // Родитель: Где линия, которая соединяет это узел с его родительским концом
        pointX = node.left;
        pointY = nodeTop + (nodeHeight / 2);
        node.parentConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'vertical'
        };
    }

    // Переместить всех потомков узла вниз
    function moveBottom (nodes, distance) {
        var nodesLength = nodes.length
            , node
            , i;
        for (i = 0; i < nodesLength; i++) {
            node = nodes[i];
            node.top += distance;
            if (node.parentConnectorPoint) {node.parentConnectorPoint.y += distance;}
            if (node.childrenConnectorPoint) {node.childrenConnectorPoint.y += distance;}
            if (node.dependencies) {
                moveBottom(node.dependencies, distance);
            }
        }
    }

    // Построить горизонтальное дерево
    function performLayoutH (node) {

        var nodeHeight = 30 // Высота блока узла в "px"
            , nodeWidth = 170 // Ширина блока узла в "px"
            , nodeMarginLeft = 10 // было 50 // Отступ слева для блока узла в "px"
            , nodeMarginTop = 50 // было 30 // Отступ сверху для блока узла в "px"
            , nodeLeft = 0 // Начальная координат левой стороны родительского узла
            , nodesLength // Количество потомков в данному узле
            , childrenWidth = 0 // Ширина, занимаемая потомками данного узла
            , i;

        // Перед построением данного узла мы предварительно строим узлы всех его мотомков
        if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт

            nodesLength = node.dependencies.length;

            // Сначала построить горизонтальное дерево для потомков данного узла
            for (i = 0; i < nodesLength; i++) {
                performLayoutH(node.dependencies[i]);  // Построить горизонтальное дерево для потомков данного узла
            }

            // Затем построить горизонтальное дерево для самого родительского узла

            // Родительский узел всегда находится в центре над своими потомками
            // Ширина, занимаемая потомками, равна сумме координаты левой стороны крайнего правого (последнего) потомка с его шириной
            // минус координата левой стороны крайнего левого (первого) потомка
            childrenWidth = (node.dependencies[nodesLength - 1].left + node.dependencies[nodesLength - 1].width) - node.dependencies[0].left;

            // Начальная координат левой стороны родительского узла, размещаемого в центре над потомками,
            // равна сумме начальной координаты левой стороны крайнего левого (первого) потомка и половине ширины, занимаемой
            // всеми потомками данного узла минус половина ширина самого родительского узла
            nodeLeft = (node.dependencies[0].left + (childrenWidth / 2)) - (nodeWidth / 2);

            // Левый элемент находится перед моим левый узлом?
            // В этом случае переместить его вправо
            var newLeft
                , diff;
            if (node.leftNode && ((node.leftNode.left + node.leftNode.width + nodeMarginLeft) > nodeLeft)) {
                newLeft = node.leftNode.left + node.leftNode.width + nodeMarginLeft;
                diff = newLeft - nodeLeft;
                // Также переместить моих потомков вправо
                moveRight(node.dependencies, diff);
                nodeLeft = newLeft;
            }
        } else {
            // Мое лево находится за левым сестринским элементом
            if (node.leftNode) {
                nodeLeft = node.leftNode.left + node.leftNode.width + nodeMarginLeft;
            }
        }

        node.left = nodeLeft;

        // Top зависит только от уровня вложенности level
        node.top = (nodeMarginTop * (node.level + 1)) + (nodeHeight * (node.level + 1));
        // Размер блока всегда постоянен
        node.height = nodeHeight;
        node.width = nodeWidth;

        // Рассчитать соединительные точки
        // Потомок: Где линии выходят для соединения узла с его потомками
        var pointX = nodeLeft + (nodeWidth / 2)
            , pointY = node.top + nodeHeight;
        node.childrenConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'horizontal'
        };
        // Родитель: Где линия, которая соединяет это узел с его родительским концом
        pointX = nodeLeft + (nodeWidth / 2);
        pointY = node.top;
        node.parentConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'horizontal'
        };
    }

    // Переместить всех потомков узла вправо
    function moveRight (nodes, distance) {
        var nodesLength = nodes.length
            , node
            , i;
        for (i = 0; i < nodesLength; i++) {
            node = nodes[i];
            node.left += distance;
            if (node.parentConnectorPoint) {node.parentConnectorPoint.x += distance;}
            if (node.childrenConnectorPoint) {node.childrenConnectorPoint.x += distance;}
            if (node.dependencies) {
                moveRight(node.dependencies, distance);
            }
        }
    }

    // Нарисовать контейнеры элементов
    function drawNode (node, container, options) {

        // Создать блок DIV для узла и задать ему значение ширины, высоты и положения на экране
        var nodeDiv = document.createElement('div');
        nodeDiv.style.top = node.top + 'px';
        nodeDiv.style.left = node.left + 'px';
        nodeDiv.style.width = node.width + 'px';
        nodeDiv.style.height = node.height + 'px';
        if (node.collapsed) {
            nodeDiv.className = 'node-collapsed'; // Если узел свернут
        } else {
            nodeDiv.className = 'node'; // Если узел развернут
        }
        nodeDiv.setAttribute('path', node.path);

        // Добавить в блок DIV узла индивидуальный класс и содержимое, если они были заданы
        if (node.className) {nodeDiv.className = node.className;}
        if (node.name) {nodeDiv.innerHTML = node.name;}
        nodeDiv.node = node;

        // Навесить на блок DIV события клика мыши по блоку
        if (options.onNodeClick) {nodeDiv.onclick = options.onNodeClick();}

        // Добавить созданный блок в контейнер
        container.appendChild(nodeDiv);

        // Нарисовать потомков
        var nodesLength // Количество потомков в данному узле
            , i;
        if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт
            nodesLength = node.dependencies.length;
            for (i = 0; i < nodesLength; i++) {
            drawNode(node.dependencies[i], container, options);
            }
        }
       
    }

    // Нарисовать соединительные линии между блоками элементов
    function drawLines (node, container) {
        var nodesLength // Количество потомков в данному узле
            , j;
        if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт
            nodesLength = node.dependencies.length;
            if (node.childrenConnectorPoint.layout === 'vertical') { // Вертикальная схема расположения блоков
                for (j = 0; j < nodesLength; j++) {
                    // Нарисовать вертикальную соединительную линию
                    drawLineV(container, node.childrenConnectorPoint, node.dependencies[j].parentConnectorPoint);
                    drawLines(node.dependencies[j], container); // Нарисовать соединительные линии для потомков данного блока
                }
            } else {
                for (j = 0; j < nodesLength; j++) { //Горизонтальная схема расположения блоков
                    // Нарисовать горизонтальную соединительную линию
                    drawLineH(container, node.childrenConnectorPoint, node.dependencies[j].parentConnectorPoint);
                    drawLines(node.dependencies[j], container); // Нарисовать соединительные линии для потомков данного блока
                }
            }
        }
    }

    // Нарисовать вертикальную соединительную линию
    function drawLineV (container, startPoint, endPoint) {
        var midX = (startPoint.x + ((endPoint.x - startPoint.x) / 2)); // Середина - это половина пути между началом и концом точки X

        // Начальный сегмент вертикальной соединительной линии
        drawLineSegment(container, startPoint.x, startPoint.y, midX, startPoint.y, 1);

        // Промежуточный сегмент вертикальной соединительной линии
        var imsStartY = startPoint.y < endPoint.y ? startPoint.y : endPoint.y // Самое нижнее значение будет стартовой точкой
            , imsEndY = startPoint.y > endPoint.y ? startPoint.y : endPoint.y; // Самое высокое значение будет конечной точкой
        drawLineSegment(container, midX, imsStartY, midX, imsEndY, 1);

        // Конечный сегмент вертикальной соединительной линии
        drawLineSegment(container, midX, endPoint.y, endPoint.x, endPoint.y, 1);
    }

    // Нарисовать горизонтальную соединительную линию
    function drawLineH (container, startPoint, endPoint) {
            var midY = (startPoint.y + ((endPoint.y - startPoint.y) / 2)); // Середина - это половина пути между началом и концом точки Y

            // Начальный сегмент горизонтальной соединительной линии
            drawLineSegment(container, startPoint.x, startPoint.y, startPoint.x, midY, 1);

            // Промежуточный сегмент горизонтальной соединительной линии
            var imsStartX = startPoint.x < endPoint.x ? startPoint.x : endPoint.x // Самое нижнее значение будет стартовой точкой
                , imsEndX = startPoint.x > endPoint.x ? startPoint.x : endPoint.x; // Самое высокое значение будет конечной точкой
            drawLineSegment(container, imsStartX, midY, imsEndX, midY, 1);

            // Конечный сегмент горизонтальной соединительной линии
            drawLineSegment(container, endPoint.x, midY, endPoint.x, endPoint.y, 1);
    }

    // Нарисовать сегмент соединительной линии
    function drawLineSegment (container, startX, startY, endX, endY, lineWidth) {

        // Создать блок DIV, который будет изображать соединительную линию
        var lineDiv = document.createElement('div');
        lineDiv.style.top = startY + 'px';
        lineDiv.style.left = startX + 'px';

        if (startX === endX) { // Вертикальная линия
            lineDiv.style.width = lineWidth + 'px';
            lineDiv.style.height = (endY - startY) + 'px';
        } else{ // Горизонтальная линия
            lineDiv.style.width = (endX - startX) + 'px';
            lineDiv.style.height = lineWidth + 'px';
        }

        lineDiv.className = 'node-line';
        container.appendChild(lineDiv); // Добавить соединительную линию к блоку элемента
    }
   
    // Активировать фильтр кода файлов
    function initFileFilter () {
        $('.file-filter div').first().addClass('active');
       
        // Клик по любой кнопке фильтра
        $('.file-filter div').click(function(){
            // Выделение кнопки и отображение соответствующего ей формата кода
            if ($(this).hasClass('active')) {return;}
            $('.file-filter div').removeClass('active');
            $(this).addClass('active');
            if ($('.folder-tree .selectable').hasClass('active')) {
                showFileSourceCode();
            }
        });
       
    }

    initFileFilter();
   
    // Активировать фильтр секций
    function initSectionsFilter () {
        $('#tree-section-visibility').click(function(){
            if ($(this).hasClass('visible')) {
                $(this).removeClass('visible').addClass('invisible');
                $('.tree-section').addClass('hidden');
            } else if ($(this).hasClass('invisible')) {
                $(this).removeClass('invisible').addClass('visible');
                $('.tree-section').removeClass('hidden');
            }
            setSectionsWidth();
        });
        $('#map-section-visibility').click(function(){
            if ($(this).hasClass('visible')) {
                $(this).removeClass('visible').addClass('invisible');
                $('.map-section').addClass('hidden');
            } else if ($(this).hasClass('invisible')) {
                $(this).removeClass('invisible').addClass('visible');
                $('.map-section').removeClass('hidden');
            }
            setSectionsWidth();
        });
        $('#file-section-visibility').click(function(){
            if ($(this).hasClass('visible')) {
                $(this).removeClass('visible').addClass('invisible');
                $('.file-section').addClass('hidden');
            } else if ($(this).hasClass('invisible')) {
                $(this).removeClass('invisible').addClass('visible');
                $('.file-section').removeClass('hidden');
            }
            setSectionsWidth();
        });
        function setSectionsWidth () {
            // Показаны все секции
            $('.tree-section').removeClass('width-auto').removeClass('width-50').removeClass('width-70').removeClass('width-100');
            $('.map-section').removeClass('width-auto').removeClass('width-50').removeClass('width-70').removeClass('width-100');
            $('.file-section').removeClass('width-auto').removeClass('width-50').removeClass('width-70').removeClass('width-100');
            if (
                     !$('.tree-section').hasClass('hidden')
                && (($('.map-section').hasClass('hidden') && !$('.file-section').hasClass('hidden')) || (!$('.map-section').hasClass('hidden') && $('.file-section').hasClass('hidden')))
            ) { // Показана только секция с файлам и любая другая секция
                $('.tree-section').addClass('width-auto');
                $('.map-section').addClass('width-70');
                $('.file-section').addClass('width-70');
            } else if (
                       $('.tree-section').hasClass('hidden')
                && !$('.map-section').hasClass('hidden')
                && !$('.file-section').hasClass('hidden')
            ) { // Cекция с файлам скрыта и показы секции с картой и кодом
                $('.map-section').addClass('width-50');
                $('.file-section').addClass('width-50');
            } else if (
                    (!$('.tree-section').hasClass('hidden') && $('.map-section').hasClass('hidden') && $('.file-section').hasClass('hidden'))
                || ($('.tree-section').hasClass('hidden') && !$('.map-section').hasClass('hidden') && $('.file-section').hasClass('hidden'))
                || ($('.tree-section').hasClass('hidden') && $('.map-section').hasClass('hidden') && !$('.file-section').hasClass('hidden'))
            ) { // Показана только одна любая секция
                $('.tree-section').addClass('width-100');
                $('.map-section').addClass('width-100');
                $('.file-section').addClass('width-100');
            }
            resize();
            // Скрыты все секции
        }
    }

    initSectionsFilter();
   
   
});

/*

Код из HTML-файла

    <div>Shift+Click to Add Node</div>
    <div>Double Click to Expand or Collapse</div>
    <div><select id="dlLayout" onchange="ChangeLayout()">
                <option value="horizontal">Horizontal</option>
                <option value="vertical">Vertical</option>
            </select></div>

    <div class="Container" id="dvTreeContainer"></div>

*/
/*

// Корневой элемент
var rootNode = {Content: 'Корневой элемент', Nodes:[]};

// Первый уровень
rootNode.Nodes[0] = {Content: 'N1.1'};
rootNode.Nodes[1] = {Content: 'N1.15', ToolTip: 'Всплывающая подсказка 1.15'};
rootNode.Nodes[2] = {Content: 'N1.2'};
rootNode.Nodes[3] = {Content: 'N1.3'};
rootNode.Nodes[4] = {Content: 'N1.4'};

// Второй уровень
rootNode.Nodes[2].Nodes = [
      {Content : 'N1.2.1', Collapsed: true} // Этот узел отрисовывается свернутым
    , {Content : 'N1.2.2'}
    , {Content : 'N1.2.3', Class : 'SpecialNode', ToolTip: 'Кликни!'} // Этот узел раскрашен подругому
    , {Content : 'N1.2.4'}
];

rootNode.Nodes[3].Nodes = [
      {Content : 'N1.2.1', Collapsed: true} // Этот узел отрисовывается свернутым
    , {Content : 'N1.2.2'}
    , {Content : 'N1.2.3', Class : 'SpecialNode', ToolTip: 'Кликни!'} // Этот узел раскрашен подругому
    , {Content : 'N1.2.4'}
];

// Третий уровень
rootNode.Nodes[2].Nodes[0].Nodes = [
      {Content: 'N1.2.1.1' }
    , {Content: 'N1.2.1.2'}
    , {Content: 'N1.2.1.3'}
    , {Content: 'N1.2.1.4'}
    , {Content: 'N1.2.1.5'}
    , {Content: 'N1.2.1.6'}
    , {Content: 'N1.2.1.7'}
];

rootNode.Nodes[2].Nodes[1].Nodes = [
      {Content: 'N1.2.1.1' }
    , {Content: 'N1.2.1.2'}
    , {Content: 'N1.2.1.3'}
    , {Content: 'N1.2.1.4'}
    , {Content: 'N1.2.1.5'}
    , {Content: 'N1.2.1.6'}
    , {Content: 'N1.2.1.7'}
];

rootNode.Nodes[2].Nodes[2].Nodes = [
      {Content: 'N1.2.4.1'}
    , {Content: 'N1.2.4.2'}
    , {Content: 'N1.2.4.3'}
    , {Content: 'N1.2.4.4'}
    , {Content: 'N1.2.4.5'}
    , {Content: 'N1.2.4.6'}
    , {Content: 'N1.2.4.7'}
];

rootNode.Nodes[2].Nodes[3].Nodes = [
      {Content: 'N1.2.4.1'}
    , {Content: 'N1.2.4.2'}
    , {Content: 'N1.2.4.3'}
    , {Content: 'N1.2.4.4'}
    , {Content: 'N1.2.4.5'}
    , {Content: 'N1.2.4.6'}
    , {Content: 'N1.2.4.7'}
];

// Нарисовать дерево в первый раз
refreshTree();

// Изменить отрисовку дерева при изменении опции в селекторе
document.getElementById('dlLayout').onchange = function () {
    changeLayout();
};

// Изменить отрисовку дерева
function changeLayout () {
    refreshTree();
}

// Обновить дерево на экране
function refreshTree () {
    drawTree({
          container: document.getElementById('dvTreeContainer')
        , rootNode: rootNode
        , layout: document.getElementById('dlLayout').value
        , onNodeClick: nodeClick
        , onNodeDoubleClick: nodeDoubleClick
    });
}

// Одиночный клик на блоке элемента
function nodeClick (event) {
    var newNodeIndex; // Индекс добавленного потомка в блок
    if (this.node.name === 'N1.2.3') {alert(this.node.nontent);}
    // Добавить в блок нового потомка
    if (event && event.shiftKey) {
        // Добавить в блок нового потомка, если блок раскрыт
        if (!this.node.collapsed) {
            if (!this.node.dependencies) {this.node.dependencies = [];}
            newNodeIndex = this.node.dependencies.length;
            this.node.dependencies[newNodeIndex] = {};
            this.node.dependencies[newNodeIndex].name = this.node.name + '.' + (newNodeIndex + 1);
            refreshTree();
        }
    }
}

// Двойной клик на блоке элемента
function nodeDoubleClick () {
    if (this.node.dependencies && this.node.dependencies.length > 0) { // Если блок имеет потомков
        this.node.collapsed = !this.node.collapsed;
        refreshTree();
    }
}

// Нарисовать дерево
function drawTree (args) {

    // Подготовить элементы дерева для отрисовки на экране
        prepareNode({node: args.rootNode});

    // Построить вертикальное или горизонтальное дерево
        if (args.layout === 'vertical') {
            performLayoutV(args.rootNode); // Построить вертикальное дерево
    } else {
            performLayoutH(args.rootNode); // Построить горизонтальное дерево
    }

    // Нарисовать контейнеры элементов дерева
        args.container.innerHTML = ''; // Удалить все элементы из блока DIV, в который будет вставлено наше дерево
        drawNode(args.rootNode, args.container, args);

    // Нарисовать соединительные линии между блоками элементов
        drawLines(args.rootNode, args.container);
}

// Проверить раскрыт ли узел и имеет ли он потомков.
function isNodeCollapsedAndHasChildren (node) {
    if (!node.collapsed && node.dependencies && node.dependencies.length > 0) { // Элемент имеет потомков и раскрыт
        return true;
    }
    return false;
}

// Подготовить элементы дерева для отрисовки на экране
function prepareNode (args) {

    var node = args.node // Узел
        , level = args.level // Уровень вложенности узла
        , parentNode = args.parentNode // Родительский узел
        , leftNode = args.leftNode // Самый левый (первый) узел потомок
        , rightLimits = args.rightLimits // Массив узлов потомков
        , nodesLength // Количество потомков в данному узле
        , i;

    if (level === undefined) {level = 0;}
    if (parentNode === undefined) {parentNode = null;}
    if (leftNode === undefined) {leftNode = null;} // Самый левый (первый) узел потомок
    if (rightLimits === undefined) {rightLimits = [];} // Массив элементов потомков

        node.level = level;
        node.parentNode = parentNode;
        node.leftNode = leftNode;

    if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт
            nodesLength = node.dependencies.length;
        for (i = 0; i < nodesLength; i++) {
            leftNode = null;
            if (i === 0 && rightLimits[level] !== undefined) {
                leftNode = rightLimits[level];
            }
            if (i > 0) {
                    leftNode = node.dependencies[i - 1]; // Получить самый левый (первый) узел потомок
            }
            if (i === (nodesLength - 1)) {
                    rightLimits[level] = node.dependencies[i]; // Получить самый правый (последний) узел потомок
            }
            // Приступить к обработке следующего элемента дерева
            prepareNode({
                      node: node.dependencies[i]
                , level: level + 1
                , parentNode: node
                , leftNode: leftNode
                , rightLimits: rightLimits
            });
        }
    }
}

// Построить вертикальное дерево
function performLayoutV (node) {

    var nodeHeight = 30 // Высота блока узла в "px"
            , nodeWidth = 170 // Ширина блока узла в "px"
        , nodeMarginLeft = 50 // Отступ слева для блока узла в "px"
        , nodeMarginTop = 30 // Отступ сверху для блока узла в "px"
        , nodeTop = 0 // Начальная координат верхней стороны родительского узла
        , nodesLength // Количество потомков в данному узле
        , childrenHeight = 0  // Высота, занимаемая потомками данного узла
        , i;

    // Перед посроением данного узла мы предварительно строим узлы всех его мотомков
    if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт

            nodesLength = node.dependencies.length;

        // Сначала построить вертикальное дерево для потомков данного узла
        for (i = 0; i < nodesLength; i++) {
                performLayoutV(node.dependencies[i]); // Построить вертикальное дерево для потомков данного узла
        }

        // Затем построить вертикальное дерево для самого родительского узла

        // Родительский узел всегда находится в центре перед своими потомками
        // Высота, занимаемая потомками, равна сумме координаты верхней стороны крайнего нижнего (последнего) потомка с его высотой
        // минус координата верхней стороны крайнего верхнего (первого) потомка
            childrenHeight = (node.dependencies[nodesLength - 1].top + node.dependencies[nodesLength - 1].height) - node.dependencies[0].top;

        // Начальная координат верхней стороны родительского узла, размещаемого в центре перед потомками,
        // равна сумме начальной координаты верхней стороны крайнего верхнего (первого) потомка и половине высоты, занимаемой
        // всеми потомками данного узла минус половина высоты самого родительского узла
            nodeTop = (node.dependencies[0].top + (childrenHeight / 2)) - (nodeHeight / 2);

        // Верний элемент находится над предыдущим сестринским элементом?
        // В этом случае переместить его вниз
         var newTop
             , diff;
            if (node.leftNode && ((node.leftNode.top + node.leftNode.height + nodeMarginTop) > nodeTop)) {
                newTop = node.leftNode.top + node.leftNode.height + nodeMarginTop;
            diff = newTop - nodeTop;
            // Также переместить моих потомков вниз
                moveBottom(node.dependencies, diff);
            nodeTop = newTop;
        }

    } else {
        // Мой верх находится за верхним сестринским элементом
            if (node.leftNode) {
                nodeTop = node.leftNode.top + node.leftNode.height + nodeMarginTop;
        }
    }

        node.top = nodeTop;

    // Left зависит только от уровня вложенности level
        node.left = (nodeMarginLeft * (node.level + 1)) + (nodeWidth * (node.level + 1));
    // Размер блока всегда постоянен
        node.height = nodeHeight;
        node.width = nodeWidth;

    // Рассчитать соединительные точки
    // Потомок: Где линии выходят для соединения узла с его потомками
        var pointX = node.left + nodeWidth
        , pointY = nodeTop + (nodeHeight / 2);
        node.childrenConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'vertical'
    };
    // Родитель: Где линия, которая соединяет это узел с его родительским концом
        pointX = node.left;
    pointY = nodeTop + (nodeHeight / 2);
        node.parentConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'vertical'
    };
}

// Переместить всех потомков узла вниз
function moveBottom (nodes, distance) {
    var nodesLength = nodes.length
        , node
        , i;
    for (i = 0; i < nodesLength; i++) {
        node = nodes[i];
            node.top += distance;
            if (node.parentConnectorPoint) {node.parentConnectorPoint.y += distance;}
            if (node.childrenConnectorPoint) {node.childrenConnectorPoint.y += distance;}
            if (node.dependencies) {
                moveBottom(node.dependencies, distance);
        }
    }
}

// Построить горизонтальное дерево
function performLayoutH (node) {

    var nodeHeight = 30 // Высота блока узла в "px"
            , nodeWidth = 170 // Ширина блока узла в "px"
        , nodeMarginLeft = 50 // Отступ слева для блока узла в "px"
        , nodeMarginTop = 30 // Отступ сверху для блока узла в "px"
        , nodeLeft = 0 // Начальная координат левой стороны родительского узла
        , nodesLength // Количество потомков в данному узле
        , childrenWidth = 0 // Ширина, занимаемая потомками данного узла
        , i;

    // Перед построением данного узла мы предварительно строим узлы всех его мотомков
    if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт

            nodesLength = node.dependencies.length;

        // Сначала построить горизонтальное дерево для потомков данного узла
        for (i = 0; i < nodesLength; i++) {
                performLayoutH(node.dependencies[i]);  // Построить горизонтальное дерево для потомков данного узла
        }

        // Затем построить горизонтальное дерево для самого родительского узла

        // Родительский узел всегда находится в центре над своими потомками
        // Ширина, занимаемая потомками, равна сумме координаты левой стороны крайнего правого (последнего) потомка с его шириной
        // минус координата левой стороны крайнего левого (первого) потомка
            childrenWidth = (node.dependencies[nodesLength - 1].left + node.dependencies[nodesLength - 1].width) - node.dependencies[0].left;

        // Начальная координат левой стороны родительского узла, размещаемого в центре над потомками,
        // равна сумме начальной координаты левой стороны крайнего левого (первого) потомка и половине ширины, занимаемой
        // всеми потомками данного узла минус половина ширина самого родительского узла
            nodeLeft = (node.dependencies[0].left + (childrenWidth / 2)) - (nodeWidth / 2);

        // Левый элемент находится перед моим левый узлом?
        // В этом случае переместить его вправо
        var newLeft
            , diff;
            if (node.leftNode && ((node.leftNode.left + node.leftNode.width + nodeMarginLeft) > nodeLeft)) {
                newLeft = node.leftNode.left + node.leftNode.width + nodeMarginLeft;
            diff = newLeft - nodeLeft;
            // Также переместить моих потомков вправо
                moveRight(node.dependencies, diff);
            nodeLeft = newLeft;
        }
    } else {
        // Мое лево находится за левым сестринским элементом
            if (node.leftNode) {
                nodeLeft = node.leftNode.left + node.leftNode.width + nodeMarginLeft;
        }
    }

    node.left = nodeLeft;

    // Top зависит только от уровня вложенности level
    node.top = (nodeMarginTop * (node.level + 1)) + (nodeHeight * (node.level + 1));
    // Размер блока всегда постоянен
    node.height = nodeHeight;
    node.width = nodeWidth;

    // Рассчитать соединительные точки
    // Потомок: Где линии выходят для соединения узла с его потомками
    var pointX = nodeLeft + (nodeWidth / 2)
            , pointY = node.top + nodeHeight;
        node.childrenConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'horizontal'
    };
    // Родитель: Где линия, которая соединяет это узел с его родительским концом
    pointX = nodeLeft + (nodeWidth / 2);
        pointY = node.top;
        node.parentConnectorPoint = {
              x: pointX
            , y: pointY
            , layout: 'horizontal'
    };
}

// Переместить всех потомков узла вправо
function moveRight (nodes, distance) {
    var nodesLength = nodes.length
        , node
        , i;
    for (i = 0; i < nodesLength; i++) {
        node = nodes[i];
            node.left += distance;
            if (node.parentConnectorPoint) {node.parentConnectorPoint.x += distance;}
            if (node.childrenConnectorPoint) {node.childrenConnectorPoint.x += distance;}
            if (node.dependencies) {
                moveRight(node.dependencies, distance);
        }
    }
}

// Нарисовать контейнеры элементов
function drawNode (node, container, options) {

    // Создать блок DIV для узла и задать ему значение ширины, высоты и положения на экране
    var nodeDiv = document.createElement('div');
    nodeDiv.style.top = node.top + 'px';
    nodeDiv.style.left = node.left + 'px';
    nodeDiv.style.width = node.width + 'px';
    nodeDiv.style.height = node.height + 'px';
    if (node.collapsed) {
        nodeDiv.className = 'node-collapsed'; // Если узел свернут
    } else {
        nodeDiv.className = 'node'; // Если узел развернут
    }

    // Добавить в блок DIV узла класс, содержимое и вспылвающую подсказку title, если они были заданы
    if (node.className) {nodeDiv.className = node.className;}
    if (node.tooltip) {nodeDiv.setAttribute('title', node.tooltip);}
    if (node.name) {nodeDiv.innerHTML = node.name;}
    nodeDiv.Node = node;

    // Навесить на блок DIV события клика мыши по блоку
    if (options.onNodeClick) {nodeDiv.onclick = options.onNodeClick;}
    if (options.onNodeDoubleClick) {nodeDiv.ondblclick = options.onNodeDoubleClick;}

   // Навесить на блок DIV события наведения курсора мыши на блок
    nodeDiv.onmouseover = function () { // Курсор мыши над элементом
        this.prevClassName = this.className;
        this.className = 'node-hover';
    };
 
    nodeDiv.onmouseout = function () { // Курсор мыши ушел с элемента
        if (this.prevClassName) {
            this.className = this.prevClassName;
            this.prevClassName = null;
        }
    };

    // Добавить созданный блок в контейнер
    container.appendChild(nodeDiv);

    // Нарисовать потомков
    var nodesLength // Количество потомков в данному узле
        , i;
    if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт
        nodesLength = node.dependencies.length;
        for (i = 0; i < nodesLength; i++) {
            drawNode(node.dependencies[i], container, options);
        }
    }
       
}

// Нарисовать соединительные линии между блоками элементов
function drawLines (node, container) {
    var nodesLength // Количество потомков в данному узле
        , j;
    if (isNodeCollapsedAndHasChildren(node)) { // Элемент имеет потомков и раскрыт
            nodesLength = node.dependencies.length;
            if (node.childrenConnectorPoint.layout === 'vertical') { // Вертикальная схема расположения блоков
            for (j = 0; j < nodesLength; j++) {
                // Нарисовать вертикальную соединительную линию
                    drawLineV(container, node.childrenConnectorPoint, node.dependencies[j].parentConnectorPoint);
                    drawLines(node.dependencies[j], container); // Нарисовать соединительные линии для потомков данного блока
            }
        } else {
            for (j = 0; j < nodesLength; j++) { //Горизонтальная схема расположения блоков
                // Нарисовать горизонтальную соединительную линию
                    drawLineH(container, node.childrenConnectorPoint, node.dependencies[j].parentConnectorPoint);
                    drawLines(node.dependencies[j], container); // Нарисовать соединительные линии для потомков данного блока
            }
        }
    }
}

// Нарисовать вертикальную соединительную линию
function drawLineV (container, startPoint, endPoint) {
        var midX = (startPoint.x + ((endPoint.x - startPoint.x) / 2)); // Середина - это половина пути между началом и концом точки X

    // Начальный сегмент вертикальной соединительной линии
        drawLineSegment(container, startPoint.x, startPoint.y, midX, startPoint.y, 1);

    // Промежуточный сегмент вертикальной соединительной линии
        var imsStartY = startPoint.y < endPoint.y ? startPoint.y : endPoint.y // Самое нижнее значение будет стартовой точкой
            , imsEndY = startPoint.y > endPoint.y ? startPoint.y : endPoint.y; // Самое высокое значение будет конечной точкой
    drawLineSegment(container, midX, imsStartY, midX, imsEndY, 1);

    // Конечный сегмент вертикальной соединительной линии
        drawLineSegment(container, midX, endPoint.y, endPoint.x, endPoint.y, 1);
}

// Нарисовать горизонтальную соединительную линию
function drawLineH (container, startPoint, endPoint) {
            var midY = (startPoint.y + ((endPoint.y - startPoint.y) / 2)); // Середина - это половина пути между началом и концом точки Y

        // Начальный сегмент горизонтальной соединительной линии
            drawLineSegment(container, startPoint.x, startPoint.y, startPoint.x, midY, 1);

        // Промежуточный сегмент горизонтальной соединительной линии
            var imsStartX = startPoint.x < endPoint.x ? startPoint.x : endPoint.x // Самое нижнее значение будет стартовой точкой
                , imsEndX = startPoint.x > endPoint.x ? startPoint.x : endPoint.x; // Самое высокое значение будет конечной точкой
        drawLineSegment(container, imsStartX, midY, imsEndX, midY, 1);

        // Конечный сегмент горизонтальной соединительной линии
            drawLineSegment(container, endPoint.x, midY, endPoint.x, endPoint.y, 1);
}

// Нарисовать сегмент соединительной линии
function drawLineSegment (container, startX, startY, endX, endY, lineWidth) {

    // Создать блок DIV, который будет изображать соединительную линию
    var lineDiv = document.createElement('div');
    lineDiv.style.top = startY + 'px';
    lineDiv.style.left = startX + 'px';

    if (startX === endX) { // Вертикальная линия
        lineDiv.style.width = lineWidth + 'px';
        lineDiv.style.height = (endY - startY) + 'px';
    } else{ // Горизонтальная линия
        lineDiv.style.width = (endX - startX) + 'px';
        lineDiv.style.height = lineWidth + 'px';
    }

        lineDiv.className = 'node-line';
    container.appendChild(lineDiv); // Добавить соединительную линию к блоку элемента
}
*/

Комментариев нет:

Отправить комментарий