Javascript.RU

Грамотное javascript-дерево за 7 шагов

В этой статье описана DOM/CSS-структура дерева, которую я в свое время разработал для dojo toolkit.

Основные особенности:

  • Семантическая удобная CSS-разметка.
    Внешний вид дерева определяется исключительно CSS.
  • Скрытие/раскрытие узлов
  • Структура дерева обозначена линиями
  • Допускает многострочное HTML-содержимое в узлах
  • Оптимизация по количеству HTML-тагов
  • Легко дополняется новыми фишками

Например:

Root
  • Item 1
    • Item 1.1
      • Item 1.1.2
    • Item 1.2
  • Item 2
    title long yeah
    • Item 2.1
  • Item 3
    • Item 3.1

Основной строительный блок дерева - его узел.

Каждый узел имеет класс Node и состоит из иконки Expand, заголовка Content и контейнера для детей Container.

Визуальное представление узла:

Например, вот так выглядит разметка просто корневого узла Root, без детей:

  • Root
<ul class="Container">
  <li class="Node IsRoot ExpandOpen">
    <div class="Expand"></div>
    <div class="Content">Root</div>
  </li>
</ul>
Класс IsRoot
говорит о том, что узел является корнем дерева
Класс ExpandOpen
Обозначает, что узел раскрыт
Обратите внимание - вся используемая разметка является исключительно семантической. В данном случае CSS-класс говорит не "каким образом следует выделить элемент", а "что элемент обозначает".

А вот - внутри узла появился Container с (пока) одним потомком Item 1.

  • Root
    • Item 1
<ul class="Container">
  <li class="Node IsRoot ExpandOpen">
    <div class="Expand"></div>
    <div class="Content">Root</div>
    <ul class="Container">
      <li class="Node ExpandLeaf">
        <div class="Expand"></div>
        <div class="Content">Item 1</div>
      </li>
    </ul>
  </li>
</ul>

Семантические элементы:

Контейнер Container
В контейнере содержатся все дети, т.е 1 или больше Node. Это удобно, ведь чтобы скрыть/показать потомков - достаточно обратиться к контейнеру.

Перебрать всех детей можно, используя Container.childNodes.

Класс ExpandLeaf
Обозначает, что узел является листом дерева.

Узел-потомок уже не имеет класса IsRoot.

Для начала - немного почистим стили для UL и LI: обнулим по умолчанию заданные значения margin, padding и list-style-type.

/* контейнер просто содержит узлы.
 Узел сам будет отвечать за свой отступ */
.Container {
        padding: 0;
        margin: 0;
}

.Container li {
        list-style-type: none; /* убрать кружочки/точечки */
}

Самым базовым является стиль для собственно узла Node.
Он задает иерархическую структуру за счет свойства margin-left, которое отодвигает узел-потомок от левой стенки контейнера.

/* узел отодвинут от левой стенки контейнера на 18px
    благодаря этим отступам вложенные узлы формируют иерархию
 */
.Node {
    margin-left: 18px;
    zoom: 1; /* спецсвойство против багов IE6,7. Ставит hasLayout */
}

/* Корневой узел от родительского контейнера не отодвинут. 
   Ему же не надо демонстрировать отступом, чей он сын.
   Это правило идет после .Node, поэтому имеет более высокий приоритет 
   Так что class="Node IsRoot" дает margin-left:0
*/ 
.IsRoot {
    margin-left: 0;
}

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

Левая колонка с фиксированной шириной - Expand, правая колонка - Content.

/* иконка скрытого/раскрытого поддерева или листа
    сами иконки идут дальше, здесь общие свойства
 */
.Expand {
    width: 18px;
    height: 18px;
    /* принцип двухколоночной верстки. */
    /* float:left и width дива Expand + margin-left дива Content */
    float: left; 
}

/* содержание (заголовок) узла */
 .Content {
    /* чтобы не налезать на Expand */
    margin-left:18px;
    /* высота заголовка - как минимум равна Expand 
        Т.е правая колонка всегда выше или равна левой.
        Иначе нижний float будет пытаться разместиться на получившейся ступеньке
    */    
    min-height: 18px; 
}

 /* все правила после * html выполняет только IE6 */
* html .Content {
    height: 18px; /* аналог min-height для IE6 */
}

Получившаяся структура допускает любые данные внутри Content, включая многострочные и т.п.

/* открытое поддерево */
.ExpandOpen .Expand {  
    background-image: url(/forum/img/minus.gif);  
}

/* закрытое поддерево */
.ExpandClosed .Expand {
    background-image: url(/forum/img/plus.gif);
}

/* лист */
.ExpandLeaf .Expand {
    background-image: url(/forum/img/leaf.gif);
}

Здесь очень важен порядок, в котором следуют определения.

Поддеревья вложены, из-за этого получается такая конструкция:

<li class="...Node ExpandOpen...">
  ...
  <li class="...Node ExpandClosed...">
     <div class="Expand"></div> 
     ..
  </li>
</li>

Внутренний див Expand подходит под оба CSS-правила: и под ExpandOpen .Expand и под .ExpandClosed .Expand.

Правило .ExpandClosed .Expand идет позже, поэтому имеет более высокий приоритет, и будет (правильно) показана иконка закрытого раздела.

Структурные линии обрисовывают дерево, делая иерархию более наглядной.

В некоторых javascript-деревьях они пунктирные и используют кучу лишних тагов из-за неудачно выбранной DOM/CSS-модели.

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

И все это без добавления дополнительных тагов, исключительно средствами CSS.

Наша цель - получить дерево, которое выглядит так:

Info
  • Root
    • Item 1
      Multiline test
      • Item 1.1
    • Item 2
<div>Info</div>
<ul class="Container">
  <li class="Node IsRoot IsLast ExpandOpen">
    <div class="Expand"></div>
    <div class="Content">Root</div>
    <ul class="Container">
      <li class="Node ExpandOpen">
        <div class="Expand"></div>
        <div class="Content">Item 1<br/>Multiline test</div>
        <ul class="Container">
          <li class="Node ExpandLeaf IsLast">
            <div class="Expand"></div>
            <div class="Content">Item 1.1</div>
          </li>
        </ul>
      </li>
      <li class="Node ExpandLeaf IsLast">
        <div class="Expand"></div>
        <div class="Content">Item 2</div>
      </li>
    </ul>
  </li>
</ul>

Каркас из линий образуется дополнительными CSS-правилами.

  1. Узел Node поддерживает вертикальную линию к своему следующему соседу
    .Node {
        margin-left: 18px;
        zoom: 1;
        /* линия слева образуется повторяющимся фоновым рисунком */
        background-image : url(/forum/img/i.gif);
        background-position : top left;
        background-repeat : repeat-y;
    }
    
  2. Если соседа ниже нет, то линию вниз продолжать не надо:
    /* это правило - ниже .Node, поэтому имеет больший приоритет */
    .IsLast {
        /* добавить соединительную черточку наверх */
        background-image: url(/forum/img/i_half.gif);
        background-repeat : no-repeat;
    }
    

Получается, что все узлы на одном уровне соединены вертикальной чертой.

Размер рисунков для фоновых черточек сделан такой, чтобы вертикальная черта проходила строго посередине иконок Expand.

Поэтому получается, что эти иконки автоматически "нанизываются" на вертикальную линию.

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

Вот такие новые иконки для Expand*-классов.

Открытый узел ExpandOpen
Закрытый узел ExpandClosed
Лист ExpandLeaf
Горизонтальные коннекторы готовы.

Вертикальные линии образуют каркас, а новые иконки Expand* присоединяют узлы к каркасу. Структурные линии построены .

Для скрытия-раскрытия добавим два CSS-правила.

.ExpandOpen .Container {
	display: block;
}

.ExpandClosed .Container {
	display: none;
}

Как всегда, важен порядок. ExpandClosed идет после ExpandOpen, поэтому имеет больший приоритет, и вложенные закрытые узлы отображаются закрытыми.

Для скрытия-раскрытия javascript-функция всего лишь меняет класс узла. Остальное делает CSS.

Чтобы в дереве поддерживалось скрытие-раскрытие - достаточно повесить обработчик на самый внешний div.

И для красоты - обязательно поправить курсор при наведении на иконки скрытия/раскрытия:

.ExpandOpen .Expand, .ExpandClosed .Expand {
	cursor: pointer; /* иконки скрытия-раскрытия */
}

.ExpandLeaf .Expand {
	cursor: auto; /* листовой узел */
}

Обязательно задать определение для листового узла тоже, иначе курсор на нем тоже станет pointer (почему? - из-за вложенности div'ов).

Root
  • Item 1
    • Item 1.1
      • Item 1.1.2
    • Item 1.2
  • Item 2
    title long yeah
    • Item 2.1
  • Item 3
    • Item 3.1
<div onclick="tree_toggle(arguments[0])">
<div>Root</div>
<ul class="Container">
  <li class="Node IsRoot ExpandClosed">
    <div class="Expand"></div>
    <div class="Content">Item 1</div>
    <ul class="Container">
      <li class="Node ExpandClosed">
        <div class="Expand"></div>
        <div class="Content">Item 1.1</div>
        <ul class="Container">
          <li class="Node ExpandLeaf IsLast">
            <div class="Expand"></div>
            <div class="Content">Item 1.1.2</div>
          </li>
        </ul>
      </li>
      <li class="Node ExpandLeaf IsLast">
        <div class="Expand"></div>
        <div class="Content">Item 1.2</div>
      </li>
    </ul>
  </li>
  <li class="Node IsRoot ExpandClosed">
    <div class="Expand"></div>
    <div class="Content">Item 2<br/>title long yeah</div>
    <ul class="Container">
      <li class="Node ExpandLeaf IsLast">
        <div class="Expand"></div>
        <div class="Content">Item 2.1</div>
      </li>
    </ul>
  </li>
  <li class="Node ExpandOpen IsRoot IsLast">
    <div class="Expand"></div>
    <div class="Content">Item 3</div>
    <ul class="Container">
      <li class="Node ExpandLeaf IsLast">
        <div class="Expand"></div>
        <div class="Content">Item 3.1</div>
      </li>
    </ul>
  </li>
</ul>

</div>

А вот и сам обработчик события onclick. После правил CSS делать ему осталось всего ничего:

  1. Определить, произошел ли клик на иконке Expand, используя event.target(или event.srcElement для IE)
  2. Получить узел Node для иконки
  3. Если узел - не лист, то поменять класс ExpandOpen <-> ExpandClosed
function tree_toggle(event) {
	event = event || window.event
	var clickedElem = event.target || event.srcElement

	if (!hasClass(clickedElem, 'Expand')) {
		return // клик не там
	}

	// Node, на который кликнули
	var node = clickedElem.parentNode
	if (hasClass(node, 'ExpandLeaf')) {
		return // клик на листе
	}

	// определить новый класс для узла
	var newClass = hasClass(node, 'ExpandOpen') ? 'ExpandClosed' : 'ExpandOpen'
	// заменить текущий класс на newClass
	// регексп находит отдельно стоящий open|close и меняет на newClass
	var re =  /(^|\s)(ExpandOpen|ExpandClosed)(\s|$)/
	node.className = node.className.replace(re, '$1'+newClass+'$3')
}


function hasClass(elem, className) {
	return new RegExp("(^|\\s)"+className+"(\\s|$)").test(elem.className)
}

Пока что мы строили дерево исключительно из HTML-разметки.

Полностью аналогично дерево работает при создании разметки при помощи Javascript. Как загружать данные с сервера в формате JSON, и многое другое Вы можете прочитать в цикле статей AJAX.

Здесь мы посмотрим, как добавить в дерево индикаторы обработки узла: .

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

Опишем его CSS-правилом:

.ExpandLoading   {
    width: 18px;
    height: 18px;
    float: left;
    background-image: url(/forum/img/expand_loading.gif);
}

Класс ExpandLoading на время операции будет заменять обычный класс Expand.

Почему нельзя добавить класс ExpandLoading к ExpandOpen/Closed/.. ?

Индикатор может понадобиться в любом месте. Среди потомков "активного" узла могут быть "неактивные" узлы, и среди его родителей - тоже.

Если поставить класс ExpandLoading в один ряд с ExpandOpen/Closed/.., то он будет либо более приоритетен - и тогда все узлы под ним получат часики, либо менее приоритетен - тогда вообще ничего не будет видно.

И тот и другой варианты - не подходят, когда индикация нужна на одном-единственном узле посередине, например, после редактирования названия узла.

Например, так может выглядеть участок дерева с активным узлом Item 1.1:

  • Item 1
    • Item 1.1
      • Item 1.1.1
<ul class="Container">
<li class="Node IsRoot IsLast ExpandOpen">
	<div class="Expand"></div>
	<div class="Content">Item 1</div>
	<ul class="Container">
		<li class="Node ExpandOpen IsLast">
			<div class="ExpandLoading"></div>
			<div class="Content">Item 1.1</div>
			<ul class="Container">
				<li class="Node ExpandOpen">
					<div class="Expand"></div>
					<div class="Content">Item 1.1.1</div>
				</li>
			</ul>
		</li>
	</ul>
</li>
</ul>

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

Для добавления, например, чекбокса <input type="checkbox"> после иконки Expand, нужно для начала вставить его в структуру сразу после иконки открытия/закрытия.

Указываем размеры, отступ и float: left:

/* Общий размер 14+2+2 = 18 - такой же как Expand */
.Node input {
    width: 14px;
    height: 14px;
    float: left; 
    margin: 2px;
}

Теперь, сохраняя двухколоночную верстку, нужно отодвинуть Content вправо уже не на 18, а на общую ширину двух float'ов - 36px.

После того как сдвинулся заголовок Content - естественно сдвинуть и сам узел Node, чтобы структурная линия шла от заголовка.

Все это осуществляется добавлением пары правил:

/* подвинем за оба float'а Node, Content */
.Node, .Content {
	margin-left: 36px;
}
/* заново переопределим .IsRoot */
.IsRoot { margin-left: 0; }
Root
  • Item 1
    • Item 1.1
      • Item 1.1.2
    • Item 1.2
  • Item 2
    title long yeah
    • Item 2.1
  • Item 3
    • Item 3.1
<div onclick="tree_toggle(arguments[0])">
<div>Root</div>
<ul class="Container">
  <li class="Node IsRoot ExpandOpen">
    <div class="Expand"></div>
    <input type="checkbox"/>
    <div class="Content">Item 1</div>
    <ul class="Container">
      <li class="Node ExpandOpen">
        <div class="Expand"></div>
        <input type="checkbox"/>
        <div class="Content">Item 1.1 </div>
        <ul class="Container">
          <li class="Node ExpandLeaf IsLast">
            <div class="Expand"></div>
            <input type="checkbox"/>
            <div class="Content">Item 1.1.2</div>
          </li>
        </ul>
      </li>
      <li class="Node ExpandLeaf IsLast">
        <div class="Expand"></div>
        <input type="checkbox"/>
        <div class="Content">Item 1.2</div>
      </li>
    </ul>
  </li>
  <li class="Node IsRoot ExpandOpen">
    <div class="Expand"></div>
    <input type="checkbox"/>
    <div class="Content">Item 2<br/>title long yeah</div>
    <ul class="Container">
      <li class="Node ExpandLeaf IsLast">
        <div class="Expand"></div>
        <input type="checkbox"/>
        <div class="Content">Item 2.1</div>
      </li>
    </ul>
  </li>
  <li class="Node ExpandOpen IsRoot IsLast">
    <div class="Expand"></div>
    <input type="checkbox"/>
    <div class="Content">Item 3</div>
    <ul class="Container">
      <li class="Node ExpandLeaf IsLast">
        <div class="Expand"></div>
        <input type="checkbox"/>
        <div class="Content">Item 3.1</div>
      </li>
    </ul>
  </li>
</ul>
</div>

В принципе, можно использовать и CSS/JS/картинки напрямую со страницы, но они содержат некоторое количество лишних классов.

Для удобства дерево все-в-одном с JS/CSS/HTML находится на отдельной странице. В этом примере полностью расписано дерево без AJAX-индикации и чекбоксов.

Кроме того, можно скачать материалы по статье:

При использовании в своем окружении Вы, наверное, захотите удлинить все классы. добавив какой-то префикс. Например, TreeContainer, TreeNode, и т.п.

Другой вариант, возможно, более удобный - ограничить классы внешним селектором. Например, .Tree .Container, .Tree .Node, * html .Tree .Content и т.п.


Автор: Dmitry A. Soshnikov, дата: 7 апреля, 2008 - 22:02
#permalink

Замечательная и полезная статья!


Автор: tenshi, дата: 7 апреля, 2008 - 23:13
#permalink

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

например, для элементов ( e - element ):
tree-e-root
tree-e-branch
tree-e-leaf

для состояний ( s - state ):
tree-s-opened
tree-s-closed
tree-s-loading

.ня


Автор: Илья Кантор, дата: 8 апреля, 2008 - 02:39
#permalink

Update: Добавил downloads и замечание о префиксах/неймспейсах в конце статьи.

Небольшие редакторские правки для лучшего раскрытия некоторых моментов.


Автор: remitmaster, дата: 7 мая, 2008 - 22:33
#permalink

А вот такой вопрос. По умолчанию дерево полностью раскрыто, как сделать его закрытым?


Автор: remitmaster, дата: 7 мая, 2008 - 22:34
#permalink

Или чтобы запоминалось, ну это наверное в куки надо закидывать...


Автор: Илья Кантор, дата: 10 мая, 2008 - 09:45
#permalink

Посмотри последний пример или скачай исходники... Там дерево полностью закрыто должно быть.


Автор: Andrzej (не зарегистрирован), дата: 8 июля, 2008 - 16:38
#permalink

Да Твоё дерево не очень, так как сразу глотает всю структуру, а если у тебя будет 5 тыщ узлов ?


Автор: Илья Кантор, дата: 9 июля, 2008 - 17:38
#permalink

Да, в статье не разобран вопрос динамической подгрузки данных.

С другой стороны, её достаточно просто реализовать самому.

Структура дерева удобна для добавления и удаления узлов, а как получить AJAX'ом с сервера данные - это уже можно, например, раздел про AJAX посмотреть.

UPDATE: В разделе по AJAX появились статьи про интеграцию AJAX в интерфейс и статья про AJAX-дерево.


Автор: Гость (не зарегистрирован), дата: 23 июля, 2008 - 13:04
#permalink

дерево и в правду сразу открывается((


Автор: Илья Кантор, дата: 24 июля, 2008 - 00:47
#permalink

Конечно, дерево в примере раскрыто. Там ведь у каждого узла класс ExpandOpen стоит. Если Вам хочется закрытое дерево - замените его на ExpandClosed.

В этом примере стоит ExpandClosed, так что дерево закрыто.


Автор: Гость (не зарегистрирован), дата: 20 августа, 2008 - 00:51
#permalink

Отличная статья! Долго искал нечто подобное. Респект автору за грамотный подход к задаче!
Большинство подобных деревьев обычно делается через Хм... пень колоду. В данном случае, все четко соответствует спецификациям и обеспечивает широкую кроссбраузерность.
Несомненный плюс данного дерева в том, что оно не генерируется скриптом, а полностью выполнено в виде HTML кода. Что в данном случае сохраняет саму логическую разметку документа, плюс позволяет свободно индексировать содержимое поисковиками.

Ну и несомненную ценность имеет не только конечный результат, но и сама статья.

Спасибо огромное автору! Добавил в избранное!


Автор: Андрей Кумыков (не зарегистрирован), дата: 30 сентября, 2008 - 12:42
#permalink

Если расширить условие функции раскурывания/закрывания таким образом, поведение дерево станет более юзабельным: для раскрытия узла не нужно целиться в крестик, а можно нажать на его имя.

if (!hasClass(clickedElem,'Expand') && !hasClass(clickedElem,'Content')) {
return // клик не там
}


Автор: Илья Кантор, дата: 24 октября, 2008 - 16:13
#permalink

В статье сделан именно крестик, т.к на клик на имени часто вешается что-то другое, например, открытие страницы с этим именем.

Хотя, конечно, в вашем случае может быть целесообразно расширить дерево именно так


Автор: Гость (не зарегистрирован), дата: 24 ноября, 2008 - 13:44
#permalink

Что-то у меня такой вариант не работает, а очень нужно, не могу понять почему?


Автор: король ящериц (не зарегистрирован), дата: 24 октября, 2008 - 13:14
#permalink

В Drupal интегрируется как пить дать, причем javascript просто дописывается к уже имеющемуся одному из файлов скриптов, а css соответственно к имеющейся таблице стилей, которые используются на этой странице и все. Нюанс: все классы, используемые деревом, желательно переименовать или хотя бы единичку дописать в имени каждого класса во избежание случайного пересечения описания классов, к примеру, класс node уже используется системой drupal, а вот node1 - нет


Автор: Вася (не зарегистрирован), дата: 26 октября, 2008 - 16:22
#permalink

Спасибо!


Автор: EugenyK, дата: 26 октября, 2008 - 23:13
#permalink

В статье http://javascript.ru/ajax/tutorial/intro приведён пример ajax-бесконечного дерева. Его просто создать, когда подгрузка узлов осуществляется без анимации.
Хотелось бы знать, как оптимально сделать, чтобы список дочерних узлов "выезжал" вниз или вверх (при сворачивании)?
Это делается через некую глобальную переменную типа width и через setTimeout вызывается функция, которая её меняет для данного тага?


Автор: Илья Кантор, дата: 27 октября, 2008 - 00:44
#permalink

В чем именно проблема? Это вопрос про AJAX или про анимацию или про то как они связаны ?


Автор: EugenyK, дата: 28 октября, 2008 - 03:43
#permalink

Именно про анимацию. Чем достигается появление результата ответа не сразу, а через "выкатывание" вниз?

У меня было предположение по поводу неё такое:
Получают результат запроса в div, далее дают ему более меньший zIndex и относительное позиционирование, смещают top на -offsetHeight и привязывают к DOM, а потом в вызывают функцию рекурсивно, которая увеличивает top до 0 и возвращают прежний zIndex.
Реализовывать это не пробовал (вижу в этом некую нерациональность)


Автор: Илья Кантор, дата: 28 октября, 2008 - 12:21
#permalink

Там изменение height + setTimeout.

То есть, появляется div, у него размер 0.
Дальше, скажем, каждые 5 ms размер увеличивается на 1px.
И так - до полного выкатывания, т.е пока height < div.scrollHeight


Автор: EugenyK, дата: 18 июня, 2009 - 15:47
#permalink

Если менять высоту div'а итерациями от 0 до scrollHeight через глобальную функцию, то на достаточно большом кол-ве содержащихся в этом div'е дочерних элементов время загрузки очень сильно возрастает (независимо от пересчёта интервала времени вызова на бОльшую высоту). Такого не наблюдается, если создать класс и нужное кол-во раз вызывать метод.

Но эффектнее это выглядит, если сделать анимацию высоты от 0 до 5-10px, а от 5-10px сразу до scrollHeigh. В обратном порядке реализовать скрытие.


Автор: yarich (не зарегистрирован), дата: 27 октября, 2008 - 15:05
#permalink

хорошо бы добавить функционал сохранения состояния дерева при обновлении страницы


Автор: kirilloid (не зарегистрирован), дата: 5 июня, 2009 - 21:10
#permalink

Проблем-то. Добавляем функции serialize/unserialize и прикручиваем метод хранения данных по желанию: cookie / DOM storage / flash LSO ...


Автор: alex_css (не зарегистрирован), дата: 7 ноября, 2008 - 15:37
#permalink

Отличная статья!
Код - предельно прост, ясен, без хаков, а главное - КОМПАКТЕН и РАБОЧИЙ!

Вот бы ещё додумать как сохранять состояние узлов в куки, а потом разворачивать.
Есть некий пример с DTHMLGoodies.com (называется folder-tree-static), там на JS написаны мудреные функции как раз для таких целей.... Однако моя башка никак не допрет как его можно применить здесь :-(

Кстати насчёт индексации поисковиками - не думаю, что в областях "display: none;" они что-то будут индексировать. Всё-таки поисковики умнеют и защищаются от спама. ИМХО, мнение что гугль\яндекс не разбирает css и не парсит эти display и none миф...


Автор: Гость (не зарегистрирован), дата: 24 ноября, 2008 - 13:46
#permalink

Хотел сделать так:
if (!hasClass(clickedElem,'Expand') && !hasClass(clickedElem,'Content')) {
return // клик не там
}
но что0-то не работает, нужно нажимать именно на крестих, а нужно ещё и чтобы при нажатии на имя нода раскрывался контейнер


Автор: Гость (не зарегистрирован), дата: 28 ноября, 2008 - 03:49
#permalink

Кто-то хотел запоминать состояние дерева. Есть вариант для простоты запоминать в куках последний активный узел, если считать, что активный узел - это раскрытый узел. При обновлении страницы JS скрипт после загрузки берет значение из кук и активирует (раскрывает) требуемый узел. Соответственно надо раскрыть и всех его предков перебором узлов родителей с классом, где нет "Leaf" в пределах дерева. Это просто сделать циклом, используя свойство .parentNode и пару проверок. Если предок не является "листом", то поставить ему стиль ExpandOpen. На мой взгляд, запоминать состояние всего дерева не практично, да и зачем это может быть нужно. Чтобы задача имела нормальное решение надо вводить разумные ограничения - иначе заколебешься.


Автор: Гость (не зарегистрирован), дата: 17 декабря, 2008 - 22:02
#permalink

а можете написать как это сделать?


Автор: Shock (не зарегистрирован), дата: 16 января, 2009 - 05:28
#permalink

Кстати, есть способ избежать использования класса IsRoot:
.Tree .Node {
background-image : url(img/i.gif);
background-position : top left;
background-repeat : repeat-y;
margin-left: 0;
zoom: 1;
}
.Tree .Node .Node {
margin-left: 18px;
}


Автор: Shock, дата: 16 января, 2009 - 06:04
#permalink

Очень понравился этот скрипт, потому незначительное улучшение от меня + класс на PHP для быстрого создания подобного списка.


Автор: Shock, дата: 16 января, 2009 - 06:09
#permalink

Файл JsTree.php

<?php
class JsTree {
	private $final_html = null;
	private $finally_generated = false;
	
	private $title = null;
	private $tree = null;

	public function __construct($array) {
		if(!is_array($array)) return;
		$this->tree = $this->_parse_array_level($array);
	}
	
	public function set_title($title) {
		$this->title = $title;
	}
	
	private function _node_generate($content, $children=null, $islast=false) {
		if(!is_array($children)) $children = null;
		$islast = $islast ? " IsLast" : "";
		$expand = $children ? " ExpandClosed" : " ExpandLeaf";
		$children = $children ? $this->_parse_array_level($children) : "";
		$node = "<ul class='Container'> <li class='Node$islast$expand'> <div class='Expand'></div> <div class='Content'>$content</div> $children</li> </ul>";
		return $node;
	}
	
	private function _parse_array_level($array) {
		$result = "";
		reset($array);
		for($i=1, $c=count($array); $i<=$c; $i++) {
			list($content, $children) = each($array);
			$islast = ($i==$c);
			$result.= $this->_node_generate($content, $children, $islast);
		}
		return $result;
	}
	
	private function _final_generate() {
		if($this->finally_generated) return;
		$header = $this->title ? "<div class='header'>" . $this->title . "</div>" : "";
		$this->final_html = "<div class='Tree' onclick='tree_toggle(arguments[0])'>" . $header . $this->tree . "</div>";
		$this->finally_generated = true;
	}
	
	public function render($output) {
		if(!$this->finally_generated) $this->_final_generate();
		if($output) echo $this->final_html;
		else return $this->final_html;
	}
}
?>

Файл index.php

<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>
<html>
	<head>
		<title>Tree Example</title>
		<link rel='stylesheet' type='text/css' href='Tree.css' />
		<script type='text/javascript' src='Tree.js'> </script>
	</head>
	<body>
		<?php
require_once("JsTree.php");
		
$tree_array = array(
	'elem.1' => null,
	'elem.2' => array (
		'elem.2.1' => null,
		'elem.2.2' => array (
			'elem.2.2.1' => null,
			'elem.2.2.2' =>  array (
				'elem.2.2.2.1' => null,
				'elem.2.2.2.2' => null,
				'elem.2.2.2.3' => null,
			),
			'elem.2.2.3' => array (
				'elem.2.2.3.1' => null,
			),
		),
		'elem.2.3' => array (
			'elem.2.3.1' => array (
				'elem.2.3.1.1' => null,
				'elem.2.3.1.2' => null,
				'elem.2.3.1.3' => null,
			),
			'elem.2.3.2' => null,
			'elem.2.3.3' => null,
		),
	),
	'elem.3' => null,
);

$tree = new JsTree($tree_array);
$tree->set_title('TreeTest');
$tree->render(true);
		?>
	</body>
</html>

Файл Tree.css

.Tree .Container {
    padding: 0;
    margin: 0;
}
.Tree .Container li {
    list-style-type: none;
}
/* indent for all tree children excepts root */
.Tree .Node {
    background-image : url(img/i.gif);
    background-position : top left;
    background-repeat : repeat-y;
    margin-left: 0;
    zoom: 1;
}
.Tree .Node .Node {
    margin-left: 18px;
}
/* left vertical line (grid) for all nodes */
.Tree .IsLast {
    background-image: url(img/i_half.gif);
    background-repeat : no-repeat;
}
.Tree .ExpandOpen .Expand {
    background-image: url(img/expand_minus.gif);
}
/* closed is higher priority than open */
.Tree .ExpandClosed .Expand {
    background-image: url(img/expand_plus.gif);
}
/* highest priority */
.Tree .ExpandLeaf .Expand {
    background-image: url(img/expand_leaf.gif);
}
.Tree .Content {
    min-height: 18px;
    margin-left:18px;
}
* html .Tree .Content {
    height: 18px;
}
.Tree .Expand {
    width: 18px;
    height: 18px;
    float: left;
}
.Tree .ExpandOpen .Container {
        display: block;
}
.Tree .ExpandClosed .Container {
        display: none;
}
.Tree .ExpandOpen .Expand, .ExpandClosed .Expand {
        cursor: pointer;
}
.Tree .ExpandLeaf .Expand {
        cursor: auto;
}

Файл Tree.js

function tree_toggle(event) {
        event = event || window.event
        var clickedElem = event.target || event.srcElement

        if (!hasClass(clickedElem, 'Expand')) {
                return // клик не там
        }

        // Node, на который кликнули
        var node = clickedElem.parentNode
        if (hasClass(node, 'ExpandLeaf')) {
                return // клик на листе
        }

        // определить новый класс для узла
        var newClass = hasClass(node, 'ExpandOpen') ? 'ExpandClosed' : 'ExpandOpen'
        // заменить текущий класс на newClass
        // регексп находит отдельно стоящий open|close и меняет на newClass
        var re =  /(^|\s)(ExpandOpen|ExpandClosed)(\s|$)/
        node.className = node.className.replace(re, '$1'+newClass+'$3')
}


function hasClass(elem, className) {
        return new RegExp("(^|\\s)"+className+"(\\s|$)").test(elem.className)
}

И картинки в папке imgs


Автор: Shock, дата: 16 января, 2009 - 06:14
#permalink
require_once("JsTree.php"); // Подключаем класс
 
$tree_array = array();// Все элементы
 
$tree = new JsTree($tree_array); // Создаем
$tree->set_title('TreeTest'); //Необязательно - генерируем шапку
$tree->render(true); // Выводим. Если необязательный аргумент указан - будет вызвано echo, иначе - return;

как строить переменную $tree_array?

$tree_array = array(
    'elem.1' => null,
    'elem.2' => null,
    'elem.3' => null,
);

если надо - заменяем нулл на еще один вложенный массив и так до бесконечности.


Автор: constantant, дата: 29 января, 2009 - 07:48
#permalink

Как-то всё очень сложно... я сделал на рекурсивной функции:
index.php

<html>
<head>
<link rel='StyleSheet' type='text/css' href='nogrid.css'/>
</head>
<body>
<?php
require_once("../config.php");
function tree($uid){
$sql = "SELECT id_folder,id_mother,id_type,pos,hide,name FROM folder WHERE id_mother=".$uid;
$a = mysql_query($sql);
for($i=0;$i<mysql_num_rows($a);$i++){
if(mysql_num_rows(mysql_query("SELECT id_folder FROM folder WHERE id_mother=".mysql_result($a,$i,0)))>0){
$class="Node ExpandClosed";
}
else{
$class="Node ExpandLeaf IsLast";
}
echo"<ul class='Container' id='".mysql_result($a,$i,0)."'><li class='".$class."'><div class='Expand'></div><input type='checkbox'/>";
echo "<div class='Input InputFolder Closed'></div><div class='Input InputNear NearOff'></div><div class='Content'>".mysql_result($a,$i,5)."</div>";
tree(mysql_result($a,$i,0));
}
echo"</li></ul>\n";
}
echo"<div id='0'><div id='root' onclick='tree_toggle(arguments[0])' onselectstart='return false' style='-moz-user-select:none;-khtml-user-select:none;user-select:none;'>";
tree(0,0);
echo"</div></div>";
?>
<script type="text/javascript" src="dnd_03.js"></script>
</body>
</html>

а ещё я извратился и сделал это дерево drag&drop-изменяемым))) писецнна! Но работает)))
dnd_03.js

var dragEl=[];
var InCont=[];
var NrCont=[];
var msDw=false;
var dragHelper=null;

if(window.HTMLElement){
HTMLElement.prototype.removeNode = function(removeChildren) {
  if (Boolean(removeChildren))
    return this.parentNode.removeChild(this);
  else {
    var r=document.createRange();
    r.selectNodeContents(this);
    return this.parentNode.replaceChild(r.extractContents(),this);
  }
}}
function tree_toggle(event) {
 event = event || window.event
 var clickedElem = event.target || event.srcElement

 if (!hasClass(clickedElem, 'Expand')) {
  return // клик не там
 }

 // Node, на который кликнули
 var node = clickedElem.parentNode
 if (hasClass(node, 'ExpandLeaf')) {
  return // клик на листе
 }

 // определить новый класс для узла
 var newClass = hasClass(node, 'ExpandOpen') ? 'ExpandClosed' : 'ExpandOpen'
 // заменить текущий класс на newClass
 // регексп находит отдельно стоящий open|close и меняет на newClass
 var re =  /(^|\s)(ExpandOpen|ExpandClosed)(\s|$)/
 node.className = node.className.replace(re, '$1'+newClass+'$3')
}
function hasClass(elem, className) {
 return new RegExp("(^|\\s)"+className+"(\\s|$)").test(elem.className)
}
Number.prototype.NaN0=function(){return isNaN(this)?0:this;}
function getPosition(e){
	var left = 0;
	var top  = 0;
	while (e.offsetParent){
		left += e.offsetLeft + (e.currentStyle?(parseInt(e.currentStyle.borderLeftWidth)).NaN0():0);
		top  += e.offsetTop  + (e.currentStyle?(parseInt(e.currentStyle.borderTopWidth)).NaN0():0);
		e     = e.offsetParent;
	}
	left += e.offsetLeft + (e.currentStyle?(parseInt(e.currentStyle.borderLeftWidth)).NaN0():0);
	top  += e.offsetTop  + (e.currentStyle?(parseInt(e.currentStyle.borderTopWidth)).NaN0():0);
	return {x:left, y:top};
}
function mouseCoords(ev){
	if(ev.pageX || ev.pageY){
		return {x:ev.pageX, y:ev.pageY};
	}
	return {
		x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
		y:ev.clientY + document.body.scrollTop  - document.body.clientTop
	};
}
function ChangeClass(type,obj){
switch(type){
case 'Open':
var re =  /(^|\s)(Open|Closed)(\s|$)/;
break;
case 'Closed':
var re =  /(^|\s)(Open|Closed)(\s|$)/;
break;
case 'NearOn':
var re =  /(^|\s)(NearOn|NearOff)(\s|$)/;
break;
case 'NearOff':
var re =  /(^|\s)(NearOn|NearOff)(\s|$)/;
break;
case 'ExpandOpen':
var re =  /(^|\s)(ExpandLeaf IsLast)(\s|$)/;
break;
case 'ExpandLeaf IsLast':
var re =  /(^|\s)(ExpandOpen|ExpandClosed)(\s|$)/;
break;
}
obj.className = obj.className.replace(re, '$1'+type+'$3')
}
function mouseMove(ev){
	ev         = ev || window.event;
	var target = ev.target || ev.srcElement;
if(dragEl[3]==true){
for(var i=0; i<dragHelper.childNodes.length; i++){
dragHelper.removeChild(dragHelper.childNodes[0]);
}
dragHelper.appendChild(dragEl[0].cloneNode(true));
dragHelper.style.display = 'block';
dragHelper.childNodes[0].style.background = '#a1a1a1';
dragHelper.childNodes[0].style.visibility='visible';
var mousePos = mouseCoords(ev);
			dragHelper.style.top  = mousePos.y + 30;
			dragHelper.style.left = mousePos.x;
dragEl[0].style.visibility = 'hidden';
}}

function mouseUp(){
if(dragEl[3]==true){
if((InCont[2]==true && InCont[1]!=dragEl[2]) || NrCont[3]==true){
if(InCont[2]==true){
dragEl[0].style.visibility='visible';
InCont[0].appendChild(dragEl[0]);
dragEl[3]=false;
dragHelper.style.display = 'none';
InCont[2]=false;
ChangeClass('Closed',InCont[3]);
ChangeClass('ExpandOpen',InCont[0]);
if(dragEl[4].getElementsByTagName('ul').length==0){
ChangeClass('ExpandLeaf IsLast',dragEl[4]);
}

}
if(NrCont[3]==true){
dragEl[0].style.visibility='visible';
NrCont[4].insertBefore(dragEl[0], NrCont[0]);
dragEl[3]=false;
dragHelper.style.display = 'none';
NrCont[3]=false;
ChangeClass('NearOff',NrCont[5]);
if(dragEl[4].getElementsByTagName('ul').length==0){
ChangeClass('ExpandLeaf IsLast',dragEl[4]);
}
}
}else{
if(dragEl[3]=true){
dragEl[0].style.visibility = 'visible';
dragHelper.style.display = 'none';
dragEl[3]=false;
if(InCont[2]==true){
ChangeClass('Closed',InCont[3]);
}}}}}
function PutInThis(){
if(dragEl[3]==true){
if(hasClass(this, 'InputFolder')){
ChangeClass('Open',this);
var InFolderCont=this.parentNode;
InCont[0]=InFolderCont;
InCont[1]=InFolderCont.parentNode.id;
InCont[2]=true;
InCont[3]=this;
}
if(hasClass(this, 'InputNear')){
ChangeClass('NearOn',this);
var NearCont=this.parentNode.parentNode;
NrCont[0]=NearCont;
NrCont[1]=NearCont.id;
NrCont[2]=NearCont.parentNode.parentNode.id;
NrCont[3]=true;
NrCont[4]=NearCont.parentNode;
NrCont[5]=this;
}}}
function NotThis(){
if(dragEl[3]==true){
if(hasClass(this, 'InputFolder')){
ChangeClass('Closed',this);
InCont[2]=false;
}
if(hasClass(this, 'InputNear')){
ChangeClass('NearOff',this);
NrCont[3]=false;
}}}
function GetThis(ev){
var dragElprs=this.parentNode.parentNode;
dragEl[0]=dragElprs;
dragEl[1]=dragElprs.id;
dragEl[2]=dragElprs.parentNode.parentNode.id;
dragEl[3]=true;
dragEl[4]=dragElprs.parentNode;
}

var container=document.getElementById('root');
for(var i=0; i<container.getElementsByTagName('div').length; i++){
var tElem=container.getElementsByTagName('div')[i];
if(hasClass(tElem, 'InputFolder')){
tElem.onmouseover=PutInThis;
tElem.onmouseout=NotThis;
}
if(hasClass(tElem, 'InputNear')){
tElem.onmousedown=GetThis;
tElem.onmouseover=PutInThis;
tElem.onmouseout=NotThis;
}}
if(dragHelper==null){
dragHelper = document.createElement('div');
dragHelper.style.cssText = 'position:absolute;display:none;';
document.body.appendChild(dragHelper);
}
document.onmouseup = mouseUp;
document.onmousemove = mouseMove;

просьба к тем, кто силём в яваскрипте, посоветовать чего и как тут можно оптимизировать))
nogrid.css

img{margin: 0px;padding: 0px;border: 0}

.Container {
	padding: 0;
	margin: 0;
}

.Container li {
	list-style-type: none;
}

/* indent for all tree children excepts root */
.Node {
    margin-left: 18px;
   /* zoom: 1;*/

}

.IsRoot {
    margin-left: 0;
}
 
.ExpandOpen .Expand {
    background-image: url(http://javascript.ru/forum/img/minus.gif);
}
 
/* closed is higher priority than open */
.ExpandClosed .Expand {
    background-image: url(http://javascript.ru/forum/img/plus.gif);
}
 
/* highest priority */
.ExpandLeaf .Expand {
    background-image: url(http://javascript.ru/forum/img/leaf.gif);
}


* html  .Content {
	float: left;
   /* height: 18px;*/
}

.Expand {
    width: 18px;
    height: 18px;
    float: left;
}


.ExpandOpen .Container {
        display: block;
}

.ExpandClosed .Container {
        display: none;
}

.ExpandOpen .Expand, .ExpandClosed .Expand {
        cursor: pointer;
}
.ExpandLeaf .Expand {
        cursor: auto;
}
.Closed {
    background-image: url(http://javascript.ru/forum/img/closed.gif);
}
.Open {
    background-image: url(http://javascript.ru/forum/img/open.gif);
}
.NearOff {
    background-image: url(http://javascript.ru/forum/img/closed.gif);
}
.NearOn {
    background-image: url(http://javascript.ru/forum/img/open.gif);
}
.Input {
    width: 18px;
    height: 18px;
    float: left;
    cursor: pointer;
}
.Node {
        margin-left: 36px;
}
.Content {
        /*margin-left: 36px;*/
}
.IsRoot { margin-left: 0; }

.Container input {
    width: 14px;
    height: 14px;
    float: left;
    margin: 2px;
}

Автор: Кристина (не зарегистрирован), дата: 8 марта, 2009 - 20:51
#permalink

Огромное спасибо!!! Долго искала образец дерева, это самое удобное!


Автор: nim (не зарегистрирован), дата: 10 марта, 2009 - 03:26
#permalink

В таком дереве есть недостаток. Нельзя добавить колонку(и) кнопок, которые были бы выровнены по правому краю. пришлось дерево сделать в таблице.


Автор: Илья Кантор, дата: 11 марта, 2009 - 18:47
#permalink

Да, и еще оно на баяне не играет.


Автор: Гость (не зарегистрирован), дата: 23 марта, 2009 - 15:31
#permalink

+1


Автор: Гость (не зарегистрирован), дата: 24 апреля, 2009 - 18:42
#permalink

Взаимно, +1 ))


Автор: Илья Кантор, дата: 25 апреля, 2009 - 23:38
#permalink

Впрочем, на самом деле добавить колонку таких кнопок можно, CSS-структура это замечательно позволяет.

Сейчас узел построен по принципу двухколоночной верстки: [fixed width] + [left margin]. Добавить правую колонку - это всего лишь перейти на трехколоночную верстку дивами: [fixed width] + [left/right margin] + [fixed width].


Автор: Ursus Drew (не зарегистрирован), дата: 21 марта, 2009 - 00:56
#permalink

Стоит заменить zoom: 1 на height: 1% - чтобы пройти валидацию.


Автор: Илья Кантор, дата: 30 марта, 2009 - 17:39
#permalink

В разделе по AJAX появились статьи про интеграцию AJAX в интерфейс и статья про AJAX-дерево.


Автор: Yojik, дата: 31 марта, 2009 - 05:47
#permalink

Как раз делал дерево, только для jQuery UI, вот это, мой подход оказался очень похож на ваш, это клево. Значит я на верном пути.


Автор: exxxtremist (не зарегистрирован), дата: 9 апреля, 2009 - 14:57
#permalink

Очень полезная статья, как в качестве примера работы с CSS, так и в качестве действительно полезного практического материала. Автору огромный респект. Аффтар, пеши исчо!!!


Автор: Ira (не зарегистрирован), дата: 20 мая, 2009 - 11:13
#permalink

Здравствуйте! огромное спасибо за статью от всех новичков!
а не будет ли кто-нибудь так добр подсказать, как добавить к дереву кнопки "свернуть / развернуть все"?
заранее спасибо :-)


Автор: Mirok (не зарегистрирован), дата: 5 июня, 2009 - 13:44
#permalink

СПАСИБО!


Автор: Serg7 (не зарегистрирован), дата: 12 августа, 2009 - 01:00
#permalink

Присоеденяюсь к вопросу Ira:
"Здравствуйте! огромное спасибо за статью от всех новичков!
а не будет ли кто-нибудь так добр подсказать, как добавить к дереву кнопки "свернуть / развернуть все"?
заранее спасибо :-)"

Спасибо за удобное решение!


Автор: Klimashkin (не зарегистрирован), дата: 18 августа, 2009 - 18:58
#permalink

В IE8 отображается криво...


Автор: Гость (не зарегистрирован), дата: 19 августа, 2009 - 08:43
#permalink

Нормально отображается, вообще все ок


Автор: Илья Кантор, дата: 19 августа, 2009 - 08:56
#permalink

Отвечаю на вопрос по "открыть все - свернуть все".

1) Без AJAX все достаточно просто. Функция рекурсивно пробегает по узлам дерева от корня к детям и все разворачивает/сворачивает. Придется использовать рекурсию или стек.

2) С AJAX отсутствующие узлы для операции "развернуть все" придется подгрузить. Но практика показывает, что функционал "развернуть все" к AJAX-узлам применяется редко.
Если вам действительно нужен этот код - я его писал и могу портировать в статью.
А так можно развернуть все загруженные узлы, используя простую рекурсию из п. 1.


Автор: Митяй (не зарегистрирован), дата: 19 августа, 2009 - 12:07
#permalink

Отлично все описано, есть пара идей по оптимизации

function hasClass(elem, className) {
return new RegExp("(^|\\s)"+className+"(\\s|$)").test(elem.className)
}

превращаем в

function hasClass(elem, className) {
return elem.className.search("\\b"+className+"\\b")+1
}

а вот это -

// определить новый класс для узла
var newClass = hasClass(node, 'ExpandOpen') ? 'ExpandClosed' : 'ExpandOpen'
// заменить текущий класс на newClass
// регексп находит отдельно стоящий open|close и меняет на newClass
var re = /(^|\s)(ExpandOpen|ExpandClosed)(\s|$)/
node.className = node.className.replace(re, '$1'+newClass+'$3')

превращем в

node.className = node.className.replace(/(\bExpand)([\w]+\b)/,
function (m,s1,s2) {return s1+(s2!='Open'?'Open':'Closed')});

Вполне выразительно и короче, разве что не так наглядно.


Автор: Митяй (не зарегистрирован), дата: 19 августа, 2009 - 14:11
#permalink

Последний листинг читать как
node.className = node.className.replace(/(\bExpand)(\w+\b)/,
function (m,s1,s2) {return s1+(s2!='Open'?'Open':'Closed')});

убрал обертку символьного класса за ее ненужностью.


Автор: alex39x, дата: 5 сентября, 2009 - 19:07
#permalink

Подскажите пожалуйста.
Если много элементов дерева - и раскрыть их - как сделать чтобы при закрытии одного из них страница не обновлялась?


Автор: Wolf11 (не зарегистрирован), дата: 7 октября, 2009 - 11:39
#permalink

Хотелось выразить автору большую благодарность за статью. Даже я, совсем не зная JS, умудрился реализовать то, что мне надо. А точнее меню, в котором подменю появляется и исчезает по щелчку. Только оч. нужна ещё помощь: как сделать, что бы, скажем, открытый node1 первого уровня, становился закрытым при щелчке на node2 первого уровня?


Автор: Климашкин Павел (не зарегистрирован), дата: 9 октября, 2009 - 15:11
#permalink

В класс .Node необходимо добавить свойство:

clear:left;

В противном случае при изменении масштаба страницы в firefox 3.5 и IE8 весь список уедет вправо из-за свойства float:left, так как они думают что все следующие списки должны обтекать предидущий!


Автор: 4vanger, дата: 13 октября, 2009 - 11:58
#permalink

для hasClass вместо

new RegExp("(^|\\s)"+className+"(\\s|$)")

лучше использовать "\b" для отделения слова от других. Делает то же, но выглядит чище. Плюс className надо экранировать (ну конечно если захочется использовать эту функцию чаще)


Автор: Гость (не зарегистрирован), дата: 2 ноября, 2009 - 23:51
#permalink

Спасибо большое!!! давно искала


Автор: Хыиуду (не зарегистрирован), дата: 3 ноября, 2009 - 18:07
#permalink

Еще одна реализация этого же дерева. CSS не менял, реализацию функций hasClass и tree_toggle тоже.

var offchange="    "; offset=offchange;
level=1;
function nodes_run(arr)
{
	
	var cnt=0;
	for (var key in arr)
		cnt++; //А что еще делать, если яваскрипт такой убогий?
	for (var key in arr)
	{
		val=arr[key];
		cnt--;
		if ((typeof(val)=='string')||(typeof(val)=='number')) // Это лист, финальный
		{
			result+=offset+'<li class="Node ExpandLeaf';
			if (level==1) result+=' IsRoot ';			
			if (cnt==0) result+=' IsLast';
			result+='">\n';
			offset=offset+offchange;
	        result+=offset+'<div class="Expand"></div>\n'+
	        offset+'<div class="Content"><a href="'+val+'">'+key+'</a></div>\n';
			offset=offset.slice(0, -offchange.length);			
	        result+=offset+'</li>\n';
		}
		else if (typeof(val)=='object') //У этого рута есть еще дети
		{
			result+=offset+'<li class="Node ExpandClosed ';
			if (level==1) result+=' IsRoot';
			if (cnt==0) result+=' IsLast';
			result+='">\n';
			offset=offset+offchange;
			result+=offset+'<div class="Expand"></div>\n'+
			offset+'<div class="Content">'+key+'</div>\n'+
			offset+'<ul class="Container">\n';
			offset+=offchange;
			level+=1;
			nodes_run(val);
			level-=1;
			offset=offset.slice(0, -offchange.length);
			result+=offset+'</ul>\n';
			offset=offset.slice(0, -offchange.length);			
			result+=offset+'</li>\n';
		}
	} 
	return result;
}

function tree(arr, name)
{
    result="";
	var ret='<div onclick="tree_toggle(arguments[0])"> \n'+
    '<div>'+name+'</div>\n'+
	offset+'<ul class="Container">\n';
	offset+=offchange;
    ret+=nodes_run(arr);
	offset=offset.slice(0, -offchange.length);			
   ret+=offset+"</ul>\n</div>\n";
   
   return ret; 

}

Использование - скормить функции tree два параметра: ассоциативный массив с деревом и заголовок. Массив может выглядеть, к примеру, так:

var pump={
	"111": 
		{
			"foo": '1_1',
			"bar": '1_2'
		},
	"foobar":
		{
			"qwerty": 
                                 {"test1" : 'test2'},
			"йцукен": '2_2'
		},
	"temp": 
		{
	                "aaa": '3_1',
			"bbb": '3_2'
		},
	"фываолдж": '4'
};

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


Автор: RadChand (не зарегистрирован), дата: 5 ноября, 2009 - 14:30
#permalink

Минималистический вариант
Функциональность таже что и у оригинала.

Удалены не нужные классы и правила css .Container, .Node, .Content, .Expand, .ExpandLeaf
.IsRoot тоже не нужен т.к. маргин заменен на паддинг, т.е. теперь, при построении отступов, не сам элемент дерева отодвигается в право относительно своего родителя, а отодвигает своих детей
.ExpandOpen, .ExpandClosed и .IsLast переименованы в .Expanded, .Contracted и .Last соответственно.

.Tree ul {
    padding: 0;
    margin: 0;
}

.Tree li {
    list-style-position: inside;
    list-style-image: url(/forum/img/expand_leaf.gif);
    background-image : url(/forum/img/i.gif);
    background-position : top left;
    background-repeat : repeat-y;
    padding-left: 18px;
    text-indent: -18px;
    cursor: pointer;
}

.Tree .Last {
    background-image: url(/forum/img/i_half.gif);
    background-repeat : no-repeat;
}

.Tree .Expanded {
    list-style-image: url(/forum/img/expand_minus.gif);
}

.Tree .Contracted {
    list-style-image: url(/forum/img/expand_plus.gif);
}

.Tree .Expanded ul {
    display: block;
}

.Tree .Contracted ul {
    display: none;
}

Также сокращен html. Удалены ненужные div-ы. Теперь выглядит так:

<div class="Tree" onclick="tree_toggle(arguments[0])">
<div>Tree</div>
<ul>
  <li class="Last Expanded">
    Root
    <ul>
      <li class="Contracted">Item 1
        <ul>
          <li>Item 1.1<br />second line</li>
          <li class="Last">Item 1.2</li>
        </ul>
      </li>
      <li class="Last">Item 2</li>
    </ul>
  </li>
</ul>
</div>

Ну и js тоже не избежал кастрации

function tree_toggle(event) 
{
    event = event || window.event;
    var node = event.target || event.srcElement;

    if(node.className.search("\\bExpanded\\b") != -1)
        node.className = node.className.replace('Expanded','Contracted');
    else if(node.className.search("\\bContracted\\b") != -1)
        node.className = node.className.replace('Contracted','Expanded');
}

Единственный минус по сравнении с оригиналом, курсор всегда pointer. Хотя для меня это плюс, т.к. листья у меня - ссылки, т.е. тоже кликабельные.


Автор: Geb (не зарегистрирован), дата: 7 сентября, 2010 - 17:52
#permalink

Вот только не надо про функционал заливать. Он конкретно урезан. Автор молодец предусмотрел все варианты. Мне например надо кнопочки на каждый нод и еще подсветка бзкграундом, что в вашем случае недостижимо.

Я с JS на вы, кто-то может выложить готовый код для Toggle All Nodes. Должно быть просто изменение Close <=> Open с перебором всех узлов. Респект автору!


Автор: RadChand (не зарегистрирован), дата: 5 ноября, 2009 - 14:42
#permalink

PS Если дерево без линий, то и .Last не нужен.


Автор: Sera (не зарегистрирован), дата: 6 ноября, 2009 - 15:10
#permalink

попробовал твой сокращённый вариант, работает отлично... на IE, а вот на firefox дерево получается кривое, в чём может быть проблемма?


Автор: RadChand (не зарегистрирован), дата: 9 ноября, 2009 - 14:37
#permalink

Да, действительно, FF делает отступ между маркером списка и текстом. Хотя, я бы не сказал, что дерево сильно кривое, просто лишний пробел перед текстом.
Но, тем не менее, пофиксить можно так:

Придется завернуть текст в спаны.

<li><span>Item 1.1</span></li>

И в CSS добавить хак для FF.

/*FF hack*/
@-moz-document url-prefix() {
  .Tree span {margin-left:-8px;}
}

Автор: Sera (не зарегистрирован), дата: 10 ноября, 2009 - 10:46
#permalink

Да, это помогло, спасибо!


Автор: Гость (не зарегистрирован), дата: 1 декабря, 2009 - 19:37
#permalink

Вы пишите, что (цитата):

Обратите внимание - вся используемая разметка является исключительно семантической. В данном случае CSS-класс говорит не "каким образом следует выделить элемент", а "что элемент обозначает".

На самом деле у Вас разметка говорит не только что элемент обозначает, но и хранит его состояние - ExpandOpen, что не есть хорошо, т.к. расплывается понятие "узел дерева" до "узел дерева в состоянии" * на количество состояний. А если добавить чекбоксы, селектор, ещё что нибудь, то будет бо-бо

Состояние узла и применяемый в этом состоянии стиль следует хранить отдельно. Я использую для этого statemap (хэш-таблица, где ключ статус, значение - доп. стиль) и функцию switch(statename). При небольшом расширении это позволяет менять не только стиль, но любые свойства элемента DOM при переходе между состояниями, а так же при необходимости отслеживать валидность переходов и сохранять состояние страницы, чтобы оно восстанавливалось при следующем заходе пользователя.


Автор: Роман (не зарегистрирован), дата: 5 февраля, 2010 - 18:45
#permalink

Обратите внимание - вся используемая разметка является исключительно семантической. В данном случае CSS-класс говорит не "каким образом следует выделить элемент", а "что элемент обозначает".

-- Ну начнем с того что пустой див ни как не может быть семантической разметкой - по определению.

-- Два - именование классов к семантике имеет слабое отношение, боюсь что даже никакое.


Автор: xxen (не зарегистрирован), дата: 4 марта, 2010 - 01:02
#permalink

А у меня выполнено в виде плагина к jQuery, который хавает кошерный DOM-элемент из списков и строит древовидный javascript объект jsTree (или сразу из json).

Потом скрывает исходное дерево и вставляет на это место свое, построенное на основе jsTree. Там уже и события привязаны, и иконки нужные вставлены, и состояние узлов восстановлено(открытый/закрытый).
//Кстати, тут я решил не париться с колоночными div просто таблицами обошелся.
у такого подхода есть один большой плюс - можно легко менять способ отображения или даже cделать что-то вроде MVC

На стандарты пришлось забить в одном месте - у узлов в исходном html-дереве есть атрибут "c", где хранится конфиг узла. Выглядит примерно так:
<li c="{isOpen:true, icon:['image.png', 1, 0]}"> ...
плевать что валидатор ругается. Везде работает.

про хранение состояния..
Первое, что приходит в голову - cookie.
Я придумал вычислять хеш-сумму из строки заголовка узла, соединенной с заголовками узлов-предков. То есть для каждого узла строка должна быть разная и хеш будет уникальным(не всегда конечно). При сохранении, хеши открытых узлов кладутся блоками(по три символа для узла) в строку cookie. Это сделано, чтоб минимизировать длину cookie

хеш-функция - простая самодельная=) и намного быстрее какого-нибудь CRC. На входе - любая строка произвольной длины, на выходе - строка из трех символов, по сути 32-значное число от "000" до "vvv".


Автор: Гость (не зарегистрирован), дата: 25 мая, 2010 - 12:57
#permalink

Статья превосходна в техническом плане. Но в плане практики у меня остались вопросы. Ведь мне например понадобится не только показать это дерево, но и произвести с ним какие-либо операции, например отметить какой то элемент дерева галкой и отправить данные из моей формы с учетом выбранной галки на сервер. Таким образом получаем новое требование: массив дерева должен содержать не только "Имя_элемента", но и "ID_элемента"!!! Без ID никак нельзя, сами понимаете, ведь в имени могут быть и русские буквы, к которым сервер может иметь неприязнь, так еще и имена из соседних веток дерева могут совпадать. Надеюсь доступно объяснил свою мысль. Вобщем нужно прикрутить массив, в котором не один ключ, а два и более: [id] и [name]


Автор: Гость (не зарегистрирован), дата: 25 мая, 2010 - 13:23
#permalink

PS.:

интересует реализация такого дерева на php + javascript


Автор: Гость (не зарегистрирован), дата: 22 июня, 2010 - 11:10
#permalink

Уважаемые, построил дерево, используя указанный код (мой опыт JS близок к нулю).
Задача: передать в скрытую форму значение, содержащееся в классе 'Content' для дальнейшей обработки.
Проблема: не передает IE 6.0 и Опера 9.61, но с Фоксом и Сафари работает.

<?php …

$classCont="<ul class='Container'><li class='Node ExpandLeaf IsLast'><div class='Expand'></div><div class='Content'>";
…
$Cont="<a href='#'>".$classCont.$N. "</a></div></li></ul>";
…?>

f

unction tree_toggle(event) {
        event = event || window.event
        var clickedElem = event.target || event.srcElement
        var node = clickedElem.parentNode

	if (hasClass(node, 'Content')) {
	       document.getElementById('TTT').value = clickedElem.parentNode.textContent
	       document.getElementById('myform').submit()
	    return // клик на листе
           }
…
}


Подскажите как правильно.

Автор: Гость (не зарегистрирован), дата: 28 июля, 2010 - 13:11
#permalink

я в java плохо разбераюсь. Подскажите как сделать сворачивание открытой ветки при при открытии другой ветки, что бы у дерева постоянно была открыта только одна ветка.

Спасибо.


Автор: ivanmfan (не зарегистрирован), дата: 12 августа, 2010 - 13:12
#permalink

Парни подскажите плиз как сделать автоперенос текста на новую строку? Ну ограничить ширину меню. Использую все стандартное...


Автор: Cibeboing (не зарегистрирован), дата: 4 февраля, 2011 - 11:11
#permalink

да, наверно так и есть


Автор: tratatun tratatun4k (не зарегистрирован), дата: 14 марта, 2011 - 06:09
#permalink

автору огромное спасибо за статью!!

от себя бы дополнил, что класс IsLast можно смело заменить на следующее и не засорять ваш js код ненужной ф-ностью :

.IsLast

ul:last-child>li:last-child
{
    background-image: url(../img/i_half.gif);
    background-repeat: no-repeat;
}

Автор: Jungle Dread (не зарегистрирован), дата: 16 марта, 2011 - 22:37
#permalink

Обьясните пожалуйста эту строку:

function tree_toggle(event) {
event = event || window.event // Здесь ясно создаем обьек собитие
var clickedElem = event.target || event.srcElement // Здесь получаем имя инициатора события

if (!hasClass(clickedElem, 'Expand')) { Смотрим ниже(в энде кода) и что !значит перед вызовом функции(возврат фэлсе что ли?)
return // клик не там
}

// Node, на который кликнули
var node = clickedElem.parentNode
if (hasClass(node, 'ExpandLeaf')) {
return // клик на листе
}

// определить новый класс для узла
var newClass = hasClass(node, 'ExpandOpen') ? 'ExpandClosed' : 'ExpandOpen'
// заменить текущий класс на newClass
// регексп находит отдельно стоящий open|close и меняет на newClass
var re = /(^|\s)(ExpandOpen|ExpandClosed)(\s|$)/
node.className = node.className.replace(re, '$1'+newClass+'$3')
}

function hasClass(elem, className) {
return new RegExp("(^|\\s)"+className+"(\\s|$)").test(elem.className) А вот тут не соасем ясно что это?"(^|\\s)" "(\\s|$)" .test(elem.className) что значит слово test? я так понял тестирует на совпадение классов? почему бы не написать если класс инициатора = классу Expand то ... Зачем создавать функцию?
}

Спасибо, только в гугл не посылайте! А то раньше на х... ,а теперь в гугл!)


Автор: 0931454574, дата: 16 марта, 2011 - 22:54
#permalink

return new RegExp("(^|\\s)"+className+"(\\s|$)").test(elem.className)

нашел стать по этим регекспам я так понимаю что это вариант поиска текста в строке имени класса или названии класса? но так у нас есть class="Expand" ExpandOpen ExpandLeaf ExpandClosed . А понял щелчок должен быть по голой картинке . Ок

Есть еще строка
var re = /(^|\s)(ExpandOpen|ExpandClosed)(\s|$)/
node.className = node.className.replace(re, '$1'+newClass+'$3') что значит символ доллара с 1 и 3

var newClass = hasClass(node, 'ExpandOpen') ? 'ExpandClosed' : 'ExpandOpen' Это я так понял если стаит класс а то меняется на б и наоборот что то вроде toggle в jquery? Что значит ? и двоеточие?


Автор: 0931454574, дата: 16 марта, 2011 - 23:36
#permalink

function hasClass(elem, className) {
return new RegExp("(^|\\s)"+className+"(\\s|$)").test(elem.className) А вот тут не соасем ясно что это?"(^|\\s)" "(\\s|$)" .test(elem.className)

ОБЬЯСНИТЕ ПОЖАЛУЙСТА как и что значит в этой функции, плииз, нашел все в нете но в голове каша! я понимаю что меняется что т оместами но как задается условие немогу понять,СПАСИБО СПАСИБО


Автор: Дмитрий Синюков (не зарегистрирован), дата: 22 апреля, 2011 - 20:03
#permalink

Вопрос к автору. Можно ли данный код использовать в коммерческом проекте? Есть ли лицензия?


Автор: Гость (не зарегистрирован), дата: 10 мая, 2011 - 15:35
#permalink

Спасибо за исходник! Очень выручает!
вот пример кода на JQuery для отметки всех дочерних checkbox'ов

window.onload = function(){
	
	tree("tree", "index.php?s=Tree");
	
        $(".myCheckbox").change(function(eventData, eventObject){
            var status = $(this).attr("checked");
            var id = $(this).parent().parent().attr("id");
            $("#" + id + " :checkbox").each(function(k, v){
                $(this).attr("checked", status);
            });
        });
    }

Автор: Иваннн, дата: 31 августа, 2011 - 17:32
#permalink

Когда писал свое дерево не додумался использовать классы для изменения картинок и закрытия и открытия LI, но как убирается продолжение бекгроунда первого (коренного LI) элемента, если последний из его детей сам имеет детей и раскрыт. Ведь коренной LI охватывает все элементы, и соответсвенно его бекгроунд тоже продолжается до конца. хотя должен закончится на последнем ребенке?


Автор: deivan (не зарегистрирован), дата: 5 декабря, 2011 - 14:59
#permalink

Люди добрые, а как сделать, чтобы сам корень Root не отображался?.. Т.е. дерево сразу показывало ветки, без корня?..


Автор: Юрий 123456789 (не зарегистрирован), дата: 23 декабря, 2011 - 15:23
#permalink

Сделаю закладочку. Очень толково написано.


Автор: Doctor Death (не зарегистрирован), дата: 26 декабря, 2011 - 06:40
#permalink

Спасибо за хороший и простой и понятный скрипт

использовал версию с чекбоксами
не могли бы посоветовать как реализовать такю фунцию
нужно поставить чекбокс на верхние уровни папок с таким расчетом чтобы при его выбори выбирались все в ветке


Автор: aее (не зарегистрирован), дата: 2 февраля, 2012 - 10:19
#permalink

Как сделать все элементы в сетке, пробовал в container style=border:1px solid black
обводятся элементы, а дерева в сетке не получается


Автор: Гость (не зарегистрирован), дата: 22 апреля, 2012 - 22:17
#permalink

В таком виде всё открывается в Mozilla Firefox, а в Internet Explorer 2й список не открывается. Подскажите пожалуйста как это исправить?

< div onClick="tree_toggle (arguments[0])">
< ul class="Container">
< li class="ExpandClosed">
< div class="Expand">

< div class="Content Text">Внешний тюнинг

< ul class="Container">
< li class="Node ExpandLeaf">
< div class="Expand">

< div class="Content">< a href="AirOb2107.html" title="Список обвесов">Аэродинамические обвесы< /a>

< div class="Expand">

< div class="ExpandLeaf">< a href="Schetki2107.html" title="Список щёток стеклоочистителя">Щётки стеклоочистителя< /a>

< /li>
< /ul>
< /li>
< /ul>

< div onClick="tree_toggle (arguments[1])">
< ul class="Container">
< li class="ExpandClosed">
< div class="Expand">

< div class="Content Text">Выхлопная система

< ul class="Container">
< li class="Node ExpandLeaf">
< div class="Expand">

< div class="Content">< a href="Kollektor2107.html" title="Список коллекторов">Коллекторый< /a>

< li class="Node ExpandLeaf IsLast">
< div class="Expand">

< div class="ExpandLeaf">< a href="Prochee.html" title="Список дополнительных деталей">Прочее< /a>

< /li>
< /ul>
< /li>
< /ul>


Автор: Зуек (не зарегистрирован), дата: 8 июля, 2012 - 14:01
#permalink

Добрый день!

Подскажите, как выделить цветом выделенный элемент, соответственно, сбросить цвет предыдущего выделенного, т.е. щелкаю Item1 - он подсвечивается.
Спасибо!


Автор: Довольный (не зарегистрирован), дата: 10 августа, 2012 - 12:33
#permalink

Огромное спасибо за статью, жаль что не могу ссылку повесить


Автор: Гость (не зарегистрирован), дата: 7 сентября, 2012 - 12:57
#permalink

Автору - в Firefox 15 идет баг с checkbox. Надо убрать из стилей width и height для input. Тогда все нормально.


Автор: Гость (не зарегистрирован), дата: 7 сентября, 2012 - 13:04
#permalink

Или надо выставить width и height по 13px;


Автор: Гость (не зарегистрирован), дата: 1 ноября, 2012 - 00:46
#permalink

Илья, скажите пожалуйста, что означает передача arguments[0] в функцию tree_toggle ?
Заранее спасибо


Автор: Гость (не зарегистрирован), дата: 1 ноября, 2012 - 00:59
#permalink

Наверное, сам понял:
передаётся только объект, по которому кликнули, игнорируя всплывание клика


Автор: Гость (не зарегистрирован), дата: 19 ноября, 2012 - 10:17
#permalink

Статья очень помогла бы, если бы вместе с кодом не копировались номера строк кода!!!


Автор: Гость (не зарегистрирован), дата: 3 декабря, 2012 - 10:16
#permalink

Там кнопки есть, нажи и откроется новое окно с кодом без номеров строк.


Автор: .* (не зарегистрирован), дата: 22 марта, 2013 - 13:39
#permalink

xml-овский минимализьм (-|+) пользую покамест, оный у разработчиков степенно взяв.) оказывается html + css + js + skin это xml.


Автор: mirt (не зарегистрирован), дата: 7 сентября, 2013 - 12:27
#permalink

Подскажите, под какой лицензией вы выпускаете этот код?


Автор: Гость (не зарегистрирован), дата: 12 ноября, 2013 - 18:09
#permalink

Подскажите, пожалуйста, как можно реализовать это дерево от дочерних ссылок к родительским. Спасибо


Автор: Гость (не зарегистрирован), дата: 3 апреля, 2014 - 08:43
#permalink

я ваш рот наоборот


Автор: Виталя (не зарегистрирован), дата: 12 мая, 2014 - 12:09
#permalink

Да, это хорошо


Автор: Гость (не зарегистрирован), дата: 3 июля, 2014 - 15:37
#permalink

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


Автор: Гость (не зарегистрирован), дата: 2 июля, 2015 - 13:10
#permalink

И все равно не понял (даже прочитав комментарии), как грузить подобный список из бд (ms sql) с помощью php =/


Автор: Ирина86 (не зарегистрирован), дата: 2 марта, 2016 - 14:04
#permalink

Большое спасибо за статью! А можете подсказать, как это дерево убрать в выпадающий список, т.е. чтобы изначально кроме "- Выберите значение-" ничего не было?


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

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
18 + 1 =
Введите результат. Например, для 1+3, введите 4.
 
Текущий раздел
Поиск по сайту
Реклама
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние комментарии
Последние темы на форуме
Forum