Многоуровневое дерево с маркерами, сохраняющее состояние (HTML, CSS, jQuery, Cookies)

    Продолжается развитие темы о многоуровневом дереве с маркерами. Многоуровневое дерево с сохранением состояния узлов
    Теперь дерево выросло и окрепло, стало взрослее и помнит выбранный узел и состояние кажого узла в отдельности.
    Страницу можно перезагружать, а дерево все равно будет помнить все что вы открыли и выбрали!


    Для сохранания состояния использованы Cookies и jquery.cookies.js плагин. Работать с cookies очень просто.

    Подготовка Html


    Дерево будет работать, если выполнить базовое условие — оформить html в таком виде:
    <div id="multi-derevo">
      <h4><a href="#">Заголовок</a></h4>
      <ul>
       <li><span><a href="#1">1. Ветка</a></span>
         <ul>
          <li><span><a href="#11">1.1. Ветка</a></span>
            <ul>
             <li><span><a href="#111">1.1.1. Листик</a></span></li>
             <li><span><a href="#112">1.1.2. Цветок </a></span></li>
             <li><span><a href="#113">1.1.3. Цветок </a></span></li>
            </ul>
          </li>
         </ul>
       </li>
       <li><span><a href="#2">2. Ветка</a></span></li>
      </ul>
    </div><!-- /multi-derevo -->


    Для сосздания подуровня достатночно вставить после заголовка узла вложенный список такой же структуры.

    В принципе глубина вложения не ограничена, но так как все элементы загружаются сразу вы рискуете попасть на трафик и долгое ожидание, так что будте бдительны.

    Обновленный скрипт дерева


    Все пояснения в комментариях кода.
    /*
    (c) 2009 r3code.habrahabr.ru
    По вопросам модификации под задачу обращайтесь

    Скрипт: Построение дерева по готовому HTML списку.
    r3code.habrahabr.ru/blog/59823

    Выделяем узлы имющие поддеревья и добавляем у ним метку.
    Определяет поведение узлов дерева при клике на них.
     - Изменяет состояние маркера раскрытия (открыт/закрыт).
     - Узлы содержащие в себе другие узлы, по клику разворачиваются
      или сворачиваются, в зависимости от текущего состояния.
     - При переходе с одного узла на другой снимается выделение (.current)
      и пеходит на выбранный узел.
     - Определяет последний узел с поддеревом и скрывает соединительную
      линию до следующего узла этого уровня.
     - Сохранаяет состояние узлов (откр./закр.) и выбранный узел в cookies.
     - При установленных cookies состояние узлов восстанавливается при загрузке.

     19/05/2009
    */
    //=================================================================================

    $(document).ready(function () {
    /* Расставляем маркеры на узлах, имющих внутри себя поддерево.
      Выбираем элементы 'li' которые имеют вложенные 'ul', ставим для них
      маркер, т.е. находим в этом 'li' вложенный тег 'a'
      и в него дописываем маркер '<em class="marker"></em>'.
      a:first используется, чтобы узлам ниже 1го уровня вложенности
      маркеры не добавлялись повторно.
    */
    var root = $('#multi-derevo');
    // уникальные идентификаторы всем узлам, сквозная нумерация (Nested set)
    $('li', root).each(function (index) {
      this.id = 'n' + index;
    });
    $('li:has("ul")', root).find('a:first').prepend('<em class="marker"></em>');

    // выбрать текущий узел
    var current_id = $.cookie('current_node');
    if(current_id) $('#'+current_id).find('a:first').toggleClass('current');

    // вешаем событие на клик по ссылке
    //-----------------------------------
    $('li span', root).click(function () {
      // снимаем выделение предыдущего узла
      $('a.current', root).removeClass('current');
      var a = $('a:first',this.parentNode);
      a.toggleClass('current');
      var current_id = a.parent('li').attr('id');
      //alert(a.parents('li').get(0).tagName+"#"+a.parents('li').attr('id'));
      setCookie('current_node',a.parents('li').attr('id') || null);
      // Выделяем выбранный узел
      toggleNode(this.parentNode);
    });
    //postLoad(); // функция раскрытия по текущему url
    openNodes(); // открыть по данным cookie
    })

    //---------------------------------------------------------------------------------
    // Выделил функцию разворачивания дерева в отдельную  
    function toggleNode(Node) {// node= li
    prepareLast(Node);
    // анимация раскрытия узла и изменение состояния маркера
    var ul=$('ul:first',Node);// Находим поддерево
    if (ul.length) {// поддерево есть
      ul.slideToggle(200); //свернуть или развернуть
      // Меняем сосотояние маркера на закрыто/открыто
      var em=$('em:first',Node);// this = 'li span'
      // было em.hasClass('open')?em.removeClass('open'):em.addClass('open');
      em.toggleClass('open');
      saveTreeState();
    }  
    }

    // функция обработки последнего узла в уровне
    function prepareLast(Node) {
    /* если это последний узел уровня, то соединительную линию к следующему
    рисовать не нужно */  
    $(Node).each(function(){
      if (!$(this).next().length) {
        /* берем корень разветвления <li>, в нем находим поддерево <ul>,
         выбираем прямых потомков ul > li, назначаем им класс 'last' */
        $(this).find('ul:first > li').addClass('last');
      } 
    })
    }
    // функция разворачивания дерева до выбранной ранее ссылки
    function postLoad(){
    var url = window.location.toString();
    var max = 0;
    var a = null;
    $('#multi-derevo li span a').each(function(){
      // сравниваем адрес страницы и ссылку из атрибута
      if(url.indexOf(this.href) >= 0 && this.href.length > max){
        a = this;
        max = this.href.length;
      }
    });
    // если узел не виден, то разворачиваем дерево
    if ($(a).is(':hidden') || $(a).parents(':hidden').length) {
      var li = $(a).parents().filter('li');
      prepareLast(li);
      toggleNode(li);
    }
    // выделим выбранный узел
    if (a) {
      $(a).toggleClass('current');
    }
    else { // первый показ, выберем первую ссылку (можно убрать если не нужно)
      $('#multi-derevo li span a:first').toggleClass('current'); 
    }
    }

    // подготовка информации о сосотояниях узлов
    function GetOpenedNodes(items){ // li:has('ul')
     var str = [];
     $(items).each(function() {
      var res = $(this).attr('id');
      var state = $('em:first',this).hasClass('open') ? 1 : '';
      if(res && state){
       str.push(res);
      }
     });
     return str.join(',');
    }

    // сохранить полный список открытых узлов
    function saveTreeState(){
     var open_id = GetOpenedNodes($('#multi-derevo li:has("ul")')) || null;
     setCookie("open_nodes", open_id);
     return false;
    }

    // раскрытие узлов по указанному списку
    function openNodes(){
      // читаем куки и открываем узлы
     var open_nodes = $.cookie("open_nodes");
      if(open_nodes) {
      var nodes = open_nodes.split(',');
      
      if(nodes[0]){
       for(var node in nodes){
        nodes[node] = '#' + nodes[node]; 
       }
       var ids = nodes.join(',');
       $(ids).each(function() {
         toggleNode($(this));
       });
      }
     }
     return false;
    }

    // настройки хранить в Cookies 1 день
    function setCookie(name, value){
     var DAY = 24 * 60 * 60 * 1000;
     var date = new Date();
     date.setTime(date.getTime() + (1 * DAY)); // 1 день
     $.cookie(name, value, {expires: date});
     // alert("Cookie set: "+name+"="+value);
    }



    Мне конечно хотелось бы сделать рефакторинг кода с учетом возможностей jQuery, этот вариант написан так как я знаю. не откажусть от полезных мыслей по улучшению вида этого кода.

    Скрипт в работе


    Посмотреть работающий пример.

    Пример 2. Маркеры вынесены левее, чтобы текст был выравнен по одной вертикальной линии на одном уровне. Изменен CSS и скрипт.

    UPD 19.05.2009
    Отказался от сохранения состояния дерева при unload — Opera не поддерживает. Теперь состояние дерева сохраняется при клике на узел. Пример и скрипт обновлен.

    UPD 24.02.2012
    Сделан еще один вариант оформления как просил sanch3z
    habrahabr.ru/blogs/webdev/59823/#comment_1641197
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 48

      0
      А есть ли варианта на mootoolse? Просто у меня на сайте используеться именно этот фреймворк.
        0
        Думаю адаптировать можно.
        0
        Были такие мысли. На большие деревья размер куков может не хватить :) Придется использовать чтонибудь посерьезнее. А так — отличное дерево. В избранное — если вдруг мне деревья понадобятся.
          0
          А я придумал как поступить — буду сохранять состояние самого глубокого узла, а всех предков раскрывать просто автоматом. Тогда может кук хватить.
          Кстати какое у них ограничение я не помню?
            0
            я и сам не помню :) 2 или 4 кб на сайт вроде было.
              0
              пишут: Имеются следующие ограничения на размер cookies: всего может храниться до 300 значений cookies. каждый cookie не может превышать 4 Кб.

              У меня тут храниться 2 значения.
              Я прикинул — если сохранять все раскрытые узлы, то влезет примерно 700 узлов.

              Но можно инфу сжать, как я предложил постом выше — буду записывать только состояние самого глубокого открытого узла, так как по логике все родительские узлы должны быть тоже раскрыты.

              Количество хранимой инфы в кукуах можно сократить.
                0
                Сокращать нужно. Дерево не одно куков хочет :)

                У меня тогда проблема была более сложная. Таких деревьев на сайт могло быть и под сотню. И если запоминать состояние каждого, то куков точно не хватило бы :) Пришлось на сервере хранить. Мы еще и таблицы пользователям давали настраивать под себя(колонки добавлять/убирать) — тоже надо было сохранять.
                  0
                  Да когда деревьев много, конечно нужно сохранять на сервере, тут уж не прокатит.
                  В единичном случае как основное меню для сайта оно покатит.

                  Его еще можно дальше проработать, но там уже будет с применением AJAX и сервер еще программировать надо будет.
          +1
          может сохранять состояние в toggleNode? опера, к примеру, unload не поддерживает.
            0
            Этого не знал. Сделаю чтоб состояние по клику на узел сохранялось.
            0
            Адаптивный UI :-)
              +1
              Понимаю, что прототип. И все же, почему не вынести маркеры за лейбл? Ну не красиво же)
                0
                Поясните. Не понял что значит «почему не вынести маркеры за лейбл» — видимо мой сленг немного другого профиля )
                  0
                  Я говорю, что если вынести пиктограмму папки (треугольник) за название самого пункта, т.е. левее, то будет гораздо лучше. Тогда все названия будут на одном уровне, независимо от того, содержат они вложенные элементы или нет.
              0
              нравицо развитие идеи
                0
                А драг-н-дроп версии нет часом? Чтобы можно было узлы перетаскивать мышкой…
                  0
                  Хм… теперь есть идея её сделать.
                  Но мне особо не нужна. Какая мотивация? Ручное упорядочивание перетаскиванием?
                    0
                    да, именно. Типа как слои в фотошопе — перемещать на другое место или закидывать внутрь какого-либо раздела. Применение — например упорядочивание разделов сайта в системе управления сайтом.
                      0
                      О! Это крутая функциональность уже, тут поболее нужно будет прпрограммировать. Не обещаю этого в скором времени. Взято на заметку.
                  0
                  наблюдаю как растет дерево с самого начала. хорошая работа, спасибо.
                  скажите, а есть в планах реализовать добавление вложенности и редактирование названия вложенности?
                    0
                    Поподробнее пожалуйста разъясни, что значит добавлени «Вложенности» и «Название вложенности».
                    Как-то терминология не подобралсь универсальная, так что не сразу ясно бывает.
                      0
                      к примеру чтобы возле «Ветки» был значек "+" при нажатии на который появлялся новый «Новый лепесток»,
                      а после этого при нажатии на значок «редактировать» можно было прямо в дереве переименовать надпись «Новый листок»… дальше можно подумать чтобы «Лепестки» можно было сортировать между собой, перетаскивая мышью вверх-вниз… таким образом получим прирост функциональности дерева… хочу сразу оговориться, что я не программист, а следовательно не понимаю сложность реализации всего изложенного выше
                        0
                        Идеи хорошие, я взял на заметку.
                        Быстро это сделать не получиться, но в копилку на реализацию я пожалуй закину.
                    0
                    для вашего плагина рекомендую (делюсь) плагин jquery.namesession.js (требует jquery.js и json2.js).

                    (function($)
                    {
                    var state = {};
                    if (/^\{.*\}$/.test(window.name))
                    {
                    state = JSON.parse(window.name);
                    }
                    else
                    {
                    window.name = '{}';
                    }

                    $.namesession = {
                    'set': function(n, v)
                    {
                    state[n] = v;
                    window.name = JSON.stringify(state);
                    },
                    'get': function(n, dv)
                    {
                    if (n in state)
                    {
                    return state[n];
                    }
                    return dv;
                    }
                    };

                    })(jQuery);
                      0
                        0
                        А куда он сохраняет данные, чето не очень понял?
                        Понятно что сериализует в JSON, а куда его?

                        По ссылке инфы мало очень.
                          0
                          в свойство window.name ;)

                          оно сохраняет себя в рамках одной закладки.
                            0
                            проверять доступность jquery.namesession можно конструкцией

                            if ('namesession' in $)
                            {
                            // use namesession
                            }
                            else
                            {
                            // use cookies
                            }
                              0
                              Прочитал. Класс! Встрою и его пожалуй.
                      0
                      Не пробовали загружать дерево в несколькот тысяч элементов?
                      Недавно воспользовался jquery TreeView, с таким количеством элементов ужасно тормозит даже на скроле
                        0
                        Не было необходимости. У меня максисмум 50-100 элементов было.
                        1000 элементов сразу загружать — не вижу смысла. При таких объемах нужно подгужать данные по AJAX при раскрытии узла.
                          0
                          запаришься кликать, если надо на все сразу посмотреть…
                            0
                            Делаем кнопку «Открыть всё» и нет проблем.
                              0
                              нет, мы вернулись в начало — все ужасно тормозит :)
                                0
                                В начало? Это ж к чему?
                        0
                        записывать информацию в куки, которые передаются при каждом запросе элементов с этого каталога и ниже — это как минимум расточительно. Ограничьте видимость куки только текущей страницей, если это возможно.
                          0
                          «Ограничить видимость куки» — что это значит?
                            0
                            поставить куке path аналогичный адресу страницы. Чтобы браузер не посылал это же куку при запросе остальных элементов из этого же каталога
                              0
                              Ок. Ясно path: location.href
                          +1
                          Я разрабатывал аналогичный по назначению компонент и постараюсь дать пару дельных советов:
                          1) зачем вы намеренно усложняете жизнь пользователя, заставляя его формировать дерево со всеми этими
                          <li><span><a href="#111">1.1.1. Листик</a></span></li>
                          Все эти теги нужны только вашему скрипту, так пускай он их и расставляет сам, прозрачно для пользователя.
                          2) если на странице несколько деревьев — то каждое будет иметь одинаковый ID — парсер будет ругаться.
                          3) почему «multi-derevo», а не «multi-tree»? Или пишите на русском кириллическим алфавитом либо английский алфавитом на английском
                          4) если в ноде больше одной строки текста — чёрточки позиционируются по низу ноды — красивее было бы по середине.
                          5) ну и фич, конечно, совсем мало — как минимум не хватает подгрузки частей дерева по XHR, динамических чекбоксов итд.
                            0
                            1) согласен, можно это скрипту отдать
                            2) это разработка не завершена, никто не мешает заменить ID на класс
                            3) как хочу так и пишу — это мое личное дело. Может мне «мульти-дерево» слух ласкает.
                            4) красивее было бы, но требует дополнительной доработки скрипта — пока не критично и второстепенно
                            5) фич… ну вы конечно молодец, что идеи предалагаете, но я для себя в первую очередь делал — по необходимости. А эта жалоба — запрос на улучшение уже )
                            Динамические чекбоксы? Есть такая версия, но в личном пользовании. Доделать можно как угодно.

                            Вы ищите готовое решение? Тогда пока рано.
                              0
                              готовое решение у меня уже есть и оно меня устраивает полностью. настолько полностью, что не добавляли туда новых фич уже больше года.
                            0
                            Дерево классное! А можно подобную штуку на prototype найти?
                              0
                              Найти — нет. Можно адаптировать.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое