Начало · Справочники · Курсы · Разговоры

leechy.ru · Сайт почти придуман

Делаем дерево из таблицы (часть 2)

После того, как выяснил как именно скрывать и показывать элементы дерева, нужно написать и сам скрипт, который позволит третировать табличные строки как элементы дерева. Для начала будет нужно присвоит всем «пунктам» дерева свои имена (id'шки) и сделать массив, который будет содержать всю необходимую информацию о них.

Массив из объектов

Легче всего хранить всю информацию о данном элементе, если представить этот элемент как объект. Для тех, кто не знаком с принципами объектного программирования скажу только, что объект — это в каком-то смысле просто переменная, которая может иметь свои свойства (собственные переменные) и может содержать собственные методы (функций).

Для элементов (для строк дерева) интересны собственно: имена, уровень вложенности, родительский элемент, «дети» и открыт или закрыт элемент (нужно ли показывать «детей» или нет). В JavaScript'е объекты создаются с помощью отдельной функции, которая в итоге возвращает объект. В нашем случае она должна иметь вот такой вид:

function treeItem(id,level,parent,children,isOpen) {
   this.id = id;
   this.level = level;
   this.parent = parent;
   this.children = children;
   this.isOpen = isOpen;
   return this;
}]

Этот код был бы идеален, если не факт, что в качестве параметра children нужно передать уже готовый массив, который содержит всех под-элементов. Не нужно забывать однако, что вполне возможно создать этот самый масив «налету». Просто каждый новый элемент будет «говорить» своему «папе», что он его «ребенок» и таким образом будет заполнять его массив children. Для этого в функцию treeItem() нужно заменить строку this.children = children; и не передавать значение children:

function treeItem(id,level,parent,isOpen) {
   ...
   this.children = new Array();
   if (parent != '') treeItems[parent].children[treeItems[parent].children.length] = id;
   ...
}

А потом и задать весь массив treeItems, который упомянули в этих строках:

treeItems = new Array();
treeItems['item1'] = new treeItem('item1', 0, '', false);
treeItems['item2'] = new treeItem('item2', 1, 'item1', false);
treeItems['item3'] = new treeItem('item3', 2, 'item2', false);
treeItems['item4'] = new treeItem('item4', 1, 'item1', false);

В итоге, создается массив treeItems, состоящий из четырех элементов, которые в свою очередь образуют одну «ветку» дерева. На его основе дальше буду строит функции показывания/скрывания отдельных веток и веточек. Но сначала...

Видимые элементы

Для того, чтобы придать необходимый первоначальный внешний вид дерева, нужно еще во время загрузки страницы скрыть тех элементов дерева, которые должны показываться только если их специально «откроют». Проще всего это можно сделать, если задать класс скрытых элементов и присвоит его нужным элементам в соответствии со значениями isOpen, которые задали, создавая treeItems. Чтобы NN4 и MSIE3 (которые тоже понимают стили), а также Opera показывали эти элементы, укажем стиль с помощью JavaScript:

if (dom || ie) {
   document.writeln('<style type="text/css">');
   document.writeln('.treeElem \{ display: none; \}');
   document.writeln('</style>');
}

Ну и конечно создадим еще один массив, который содержит всех видимых элементав... не бойтесь, не в ручную:

visible = new Array();
for (i in treeItems) if (treeItems[i].level == 0) {
   visible[visible.length] = treeItems[i].id;
   if (treeItems[i].isOpen) visible.concat(visibleChildren(treeItems[i].id));
}

Как видно к массиву visible добавляются все элементы нулевого уровня (с них ветки начинаются и без них нельзя), а если нулевой уровень «открыть», то добавляются и все видимые под-элементы дерева. Спрашиваете что за функция visibleChildren()? Читайте дальше.

Логика работы

Дошли до того момента, с которого наверное и следовало начинать эту часть статьи. Это то как именно будет работать дерево.

Принцип прост. При открытие элемента, его переменная isOpen становится true, а все его «дети» и их открытые ветки попадают в массив visible. Если элемент закрывается, то соответственно isOpen = false и все его дети и их ветки выкидываются из visible. После чего все что в visible — показывается, а остальное — скрывается.

Это был принцип работы дерева, которое я писал давно и про которое я говорил в прошлой части. Чтобы ускорит его работу, нужно было выкинуть лишние циклы в конце, которые обрабатывали массивы treeItems и visible и лучшее решение оказалось просто показывать/скрывать те элементы, которые появляются/изчезают из visible. Конечно это стало возможно и потому, что изменился принцип «прятания» на основе свойства display.

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

Добавляем и убираем элементы из visible.

Так как будем добавлять и удалять больше, чем один элемент, то лучше всего добавляемые/удаляемые элементы передавать в массиве. У массивов есть очень удобный метод splice(), который делает именно то, что нужно, но к сожалению в майкрософтской интерпретации JavaScript он отсутствует.

Это проблема. И самое красивое решение написал Peter Balesis в серии статей Array Power на webreference.com, а точнее в Array Power III. У меня однако цель немного другая, а именно показать как можно сделать все «своими руками», да и в данном случае нам не нужна вся «мощь» всех методов, поэтому напишем две простеньких функции сами.

Первой передаем два массива — основной (main) и под-массив (sub), а также говорим после какого элемента именно нужно вставить под-массив (where).

function visibleAdd(main, where, sub) {
   var resArr = new Array();
   for (i in main) {
      resArr[resArr.length] = main[i];
      if (main[i] == where) for (j in sub) resArr[resArr.length] = sub[j];
   }
   return resArr;
}

Работает просто: создаем временный массив resArr; пробегаем по main, добавляя ее элементы к resArr; если случайно достигли до указанное место в where, то добавляем элементы из sub. В конце возвращаем то, что получилось.

В принципе, вместо цикла по sub можно написать и resArr.concat(sub) — если честно, то не проверял какой из двух вариантов быстрее. В приведенном варианте все выглядит логичнее и к тому-же неопытные JavaScript'еры смогут привыкнут быстрее к циклам (по собственным наблюдениям — они их почему-то недолюбливают ;-).

Вторая функция — удаление из массива — еще проще. Передается только основной (main) и под-массив (sub):

function visibleRemove(main, sub) {
   for (i in main) for (j in sub) if (main[i] == sub[j]) delete main[i];
   return main;
}

Двойной вложенный цикл и все элементы, которые есть в sub удаляются из main.

Какие вложенные элементы «видимы»?

Функция, которая определяет это не намного сложнее, чем предыдущие две. Особенность — она вызывает сама себя. За последние пол-года я понял, что такие функции называются рекурсивные, если этого не знали — запомните, а то над вами будут смеятся как и надо мной.

Функция определяет есть ли «дети» у запрашиваемого элемента, если есть, то добавляет их к временному массиву, а если дети isOpen и у них есть свои дети — запускает себя-же передавая как параметр имя дитя не забывая добавить результат к временному массиву. Возвращает как и прежде массив, состоящийся из всех «видимых детей» запрашиваемого элемента:

function visibleChildren(id) {
   var vChildren = new Array();
   if (treeItems[id].children.length > 0) {
      for (i in treeItems[id].children) {
         vChildren[vChildren.length] = treeItems[id].children[i];
         if (treeItems[treeItems[id].children[i]].children.length > 0 && treeItems[treeItems[id].children[i]].isOpen) {
            vChildren = vChildren.concat(visibleChildren(treeItems[id].children[i]));
         }
      }
   }
   return vChildren;
}

где id — имя запрашиваемого элемента, а vChildren — временный массив.

Финальный штрих

В конце нужно добавить и саму функцию, которая открывает и закрывает ветки дерева. Принцип ее действия я уже объяснял, все составные функции тоже, поэтому без лишних слов:

function changeTree(which) {
   var changingElems = visibleChildren(which);
   if (treeItems[which].isOpen) {
      visible = visibleRemove(visible,changingElems);
      for (i in changingElems) removeElem(changingElems[i]);
      treeItems[which].isOpen = false;
   } else {
      visible = visibleAdd(visible,which,changingElems);
      for (i in changingElems) putElem(changingElems[i],"table-row");
      treeItems[which].isOpen = true;
   }
}

Хотя придется чуть-чуть объяснить. Функции changeTree() передается параметр which — это имя элемента, которого нужно открыть или закрыть. Т. к. его нынешнее состояние скрипту неизвестно, то нужно его проверить, а потом уже добавляем или убираем из visible его «видимые дети» попутно добавляя или убирая их со страницы. И очень важно в конце поставить нужное значение флажку isOpen.

Полный скрипт напишу, если кто-то из вас не справится с написанием собственного.