jQuery, Каталог, Вложенные деревья(NESTED SETS)

test

В данной статье я завяжу в узелок такие технологии как PHP, jQuery, алгоритм NESTED SETS. В некоторых моментах могут быть пробелы, так как делаю это как часть сайта на базе framework Kohana 3. Если встретили пробел, пишите в комментариях.

Итакс, начнем. Для понимания, Вложенные деревья(они же вложенные множества) - это множества с иерархической структурой когда множество включает подмножества как ветви, которые в свою очередь включают дополнительные подмножества.

Так сказать "Вид сверху".

Вид "Сбоку". Картинки позаимствовал http://www.az-design.ru/index.shtml?Support&DataBase&DBTree2/5000

Собственно достаточно оптимальный алгоритм хранений для Меню и Каталогов, так как с помощью SELECT в базе работают эффективно выборки, но алгоритм тяжело работает с изменениями структуры дерева. Что Меню, что Каталоги редко меняют свою иерархию. Думаю основная идея понятна.

Делаем таблицу в базе:

CREATE TABLE `catalog` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `left_key` int(10) unsigned NOT NULL DEFAULT '0',
  `right_key` int(10) unsigned NOT NULL DEFAULT '0',
  `level` int(10) unsigned NOT NULL DEFAULT '0',
  `name` varchar(150) DEFAULT NULL,
  `description` text,
  PRIMARY KEY (`id`),
  KEY `left_key` (`left_key`,`right_key`,`level`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Итак, level - уровень нашей точки в нашем дереве, можно сказать номер колена. Проще взглянуть чтобы понять на картинку из другой статьи:

взятой тут( http://www.getinfo.ru/article610.html )

ID - уникальный номер узла, Level - номер уровня структуры от корня, left_key - левый ключ, right_key - правый ключ. Для глубокого понимания рассмотрим их подробней, ведь они - фундамент всего алгоритма.

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

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

Собственно как использовать выборки много где рассказанно, чтобы не описывать эти методы можете использовать статьи:

http://doc.prototypes.ru/database/trees/nestedsets/theory/use/

http://www.az-design.ru/index.shtml?Support&DataBase&SQL/CelkoJ/01h029

http://www.getinfo.ru/article610.html

Идем дальше, для отображения дерева подключаем jQuery:

http://code.google.com/intl/ru-RU/apis/libraries/devguide.html

и локально подключаем плагин jQuery для отображения и работы с деревьями:

http://bassistance.de/jquery-plugins/jquery-plugin-treeview/

Подключили скрипты(что вы должны уметь делать на ура), в Kohana 3 у меня подключение скриптов и выборка из базы идет в экшне, под обычный PHP не сложно адаптировать:

public function action_subtree(){ $this->template->scripts = array('assets/js/jquery.treeview/jquery.treeview.js');//подключение скрипта, в случае с хтмл просто через тег script $subnodes = DB::select()->from('catalog')->order_by('left_key')->execute(); //выборка из базы, легко понять что это SELECT * FROM catalog ORDER BY left_key
$this->template->content = View::factory('catalog/list')->bind('subnodes',$subnodes); //передача массива результатов в представление, в пхп вам не потребуется }      

Ну а дальше код представления в кохане, а у вас формирование хтмл-а в пхп:

<ul id="browser" class="filetree treeview-famfamfam">
<?php

foreach ($subnodes as $node) {
if (!isset($cur_lvl)) $st_pos = $cur_lvl = $node['level'];
if ($node['level']==$cur_lvl && $cur_lvl!=$st_pos) print "</li>";
if ($node['level']<$cur_lvl && $cur_lvl!=$st_pos) {print "</li></ul>"; $cur_lvl--;}
if ($node['level']>$cur_lvl) {print "<ul>"; $cur_lvl++;}
print "<li><span class='folder'>(ID: " .$node['id'].") ". $node['name']." (lvl: ". $node['level'] .")</span>";
}
?>
</ul></li></li></ul>


<script type="text/javascript">
$(document).ready(function(){
$("#browser").treeview({
toggle: function() {
console.log("%s was toggled.", $(this).find(">span").text());
}
});

 Для удобства введем в таблицу так же ID родительского элемента:

alter table catalog add (pid int unsigned not null);

В итоге отображение у нас получается:

<?php echo HTML::style('assets/js/jquery.treeview/jquery.treeview.css',array('media'=>'screen')); ?>
<?php echo HTML::style('assets/js/jquery.treeview/red-treeview.css',array('media'=>'screen')); ?>
<?php echo HTML::style('assets/js/jquery.treeview/screen.css',array('media'=>'screen')); ?>

<?php if (isset($status)) print "<div class='status'>".$status."</div>"; ?>

<ul id="browser" class="filetree treeview-famfamfam">
<?php

foreach ($subnodes as $node) {
if (!isset($cur_lvl)) $st_pos = $cur_lvl = $node['level'];
if ($node['level']==$cur_lvl && $cur_lvl!=$st_pos) print "</li>";
if ($node['level']<$cur_lvl && $cur_lvl!=$st_pos) {print "</li></ul>"; $cur_lvl--;}
if ($node['level']>$cur_lvl) {print "<ul>"; $cur_lvl++;}
print "<li><span class='folder'>(ID: " .$node['id'].") ". $node['name']." (lvl: ". $node['level'] .")</span>";
}
?>
</ul></li></li></ul>


<script type="text/javascript">
$(document).ready(function(){
$("#browser").treeview({
toggle: function() {
console.log("%s was toggled.", $(this).find(">span").text());
}
});

$("#add").click(function() {
var branches = $("<li><span class='folder'>New Sublist</span><ul>" +
"<li><span class='file'>Item1</span></li>" +
"<li><span class='file'>Item2</span></li></ul></li>").appendTo("#browser");
$("#browser").treeview({
add: branches
});
});
});
</script>

<a id='link_add' onclick='show_add_form()'>Добавить</a>
<a id='link_del' onclick='show_del_form()'>Удалить</a>
<a id='link_move' onclick='show_move_form()'>Переместить</a>

<form id='add_folder' class="popup_block" method='POST'>
ID:<input type='text' name='id_folder'>ID из приведенного выше списка<br>
Name:<input type='text' name='name_folder'> Название дирректории<br>
<input type='submit' class='btn_add' value='Add'>
</form>

<form id='move_folder' class="popup_block" method='POST'>
<input type='text' name='idfrom_folder'>ID перемещаемой дирректории<br>
<input type='text' name='idto_folder'>ID дирректории в которую перемещаем<br>
<input type='submit' class='btn_add' value='Move'>
</form>

<form id='del_folder' class="popup_block" method='POST'>
ID:<input type='text' name='id_folder'>ID из приведенного выше списка<br>
<input type='submit' class='btn_add' value='Del'>
</form>

<script>
//прячем наши формы
$('#add_folder').hide();
$('#del_folder').hide();
$('#move_folder').hide();

$('#link_move').hide();

function show_add_form(){
$('#add_folder').show();
//Формы у меня отображаются всплывающим слоем, ниже строки и есть этот слой
$('body').append('<div id="fade"></div>'); //Add the fade layer to bottom of the body tag.
$('#fade').css({'filter' : 'alpha(opacity=80)'}).fadeIn();
return false;
}

function show_move_form(){
$('#move_folder').show();
//Fade in Background
$('body').append('<div id="fade"></div>'); //Add the fade layer to bottom of the body tag.
$('#fade').css({'filter' : 'alpha(opacity=80)'}).fadeIn();
return false;
}

function show_del_form(){
$('#del_folder').show();
//Fade in Background
$('body').append('<div id="fade"></div>'); //Add the fade layer to bottom of the body tag.
$('#fade').css({'filter' : 'alpha(opacity=80)'}).fadeIn();
return false;
}
//Закрыть всплывающие окна и Fade слой при клике на слой
$('#fade').live('click', function() { //When clicking on the close or fade layer...
$('#fade , .popup_block').fadeOut(function() {
$('#fade, a.close').remove(); //fade them both out
});
return false;
});

Собственно сам контроллер:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_catalog extends Controller_DefaultTemplate {
public function action_index()
{
if (isset($_POST)){
$mcatalog = new Model_Catalog();
if (isset($_POST['id_folder']) && isset($_POST['name_folder'])) {
$id = $_POST['id_folder']; $status = "К каталогу ID: ". $id .", каталог ";
$name = $_POST['name_folder']; $status .= "Name: ". $name." успешно добавлен!";
$mcatalog->AddFolder($id, $name);
}
elseif (isset($_POST['id_folder']) && !isset($_POST['name_folder'])) {
$id = $_POST['id_folder']; $status = "Каталог ID: ". $id ." удален.";
$mcatalog->DelFolder($id);
}
elseif (isset($_POST['idfrom_folder']) && isset($_POST['idto_folder'])) {
$id = $_POST['id_folder']; $status = "Каталог ID: ". $id ." перемещен.";
$mcatalog->MoveFolder($idfrom,$idto);
}
}
$this->template->scripts = array('assets/js/jquery.treeview/jquery.treeview.js');
$subnodes = DB::select()->from('catalog')->order_by('left_key')->execute();
$this->template->content = View::factory('catalog/list')->bind('subnodes',$subnodes)->bind('status',$status);
$this->template->header = View::factory('header/header');
}

}

 Ну и собственно ф-ции для работы с самим деревом каталога, они же собственно лежат в модели:

<?php defined('SYSPATH') or die ('No direct script access.');

class Model_Catalog extends Kohana_Model{

public function AddFolder($id, $name){
$curnode = DB::select()->from('catalog')->where('id','=',$id)->execute();

//список нод которым обновляем левый и правый ключ(правая часть дерева)
$nodes = DB::select()->from('catalog')->where('left_key','>',$curnode[0]['right_key'])->execute();
foreach ($nodes as $nd)
{
$lk = $nd['left_key']+2;
$rk = $nd['right_key']+2;
$nid = $nd['id'];
DB::update('catalog')->set(array('left_key' =>$lk ,'right_key' => $rk))->where('id', '=', $nid)->execute();
}
//обновляем родительскую ветку только по правому ключу
$nodes = 0;
$nodes = DB::select()->from('catalog')->where('left_key','<=',$curnode[0]['right_key'])->and_where('right_key','>=',$curnode[0]['right_key'])->execute();
foreach ($nodes as $nd)
{
$rk = $nd['right_key']+2;
$nid = $nd['id'];
DB::update('catalog')->set(array('right_key' => $rk))->where('id', '=', $nid)->execute();
}

DB::insert('catalog',array('id','name','level','left_key','right_key','pid'))->values(array(0,$name,$curnode[0]['level']+1,$curnode[0]['right_key'],$curnode[0]['right_key']+1,$curnode[0]['id']))->execute();
}

public function DelFolder($id){
$curnode = DB::select()->from('catalog')->where('id','=',$id)->execute();
DB::delete('catalog')->where('id','=',$id)->execute();

//обновляем родительскую ветку только по правому ключу
$nodes = 0;
$nodes = DB::select()->from('catalog')->where('left_key','<=',$curnode[0]['left_key'])->and_where('right_key','>=',$curnode[0]['right_key'])->execute();
foreach ($nodes as $nd)
{
$rk = $nd['right_key']-($curnode[0]['right_key']-$curnode[0]['left_key']+1);
$nid = $nd['id'];
DB::update('catalog')->set(array('right_key' => $rk))->where('id', '=', $nid)->execute();
}

//Обновляем все остальное оставшееся дерево
$nodes = 0;
$nodes = DB::select()->from('catalog')->where('left_key','>',$curnode[0]['right_key'])->execute();
foreach ($nodes as $nd)
{
$lk = $nd['left_key'] - ($curnode[0]['right_key'] - $curnode[0]['left_key'] + 1);
$rk = $nd['right_key'] - ($curnode[0]['right_key'] - $curnode[0]['left_key'] + 1);
$nid = $nd['id'];
DB::update('catalog')->set(array('left_key' => $lk,'right_key' => $rk))->where('id', '=', $nid)->execute();
}
}

public function GetSubfolders($id){
$node = DB::select()->from('catalog')->where('id','=',$id)->execute();
$nodes = DB::select()->from('catalog')->where('left_key','>=',$node[0]['left_key'])->and_where('right_key','<=',$node[0]['right_key'])->execute();
return $nodes;
}

public function GetPathToRoot($id){
$node = DB::select()->from('catalog')->where('id','=',$id)->execute();
$nodes = DB::select()->from('catalog')->where('left_key','<=',$node[0]['left_key'])->and_where('right_key','>=',$node[0]['right_key'])->execute();
return $nodes;
}

public function GetParent($id){
$node = DB::select()->from('catalog')->where('id','=',$id)->execute();
$pnode = DB::select()->from('catalog')->where('id','=',$node[0]['pid'])->execute();
return $pnode;
}

public function GetByID($id){
$node = DB::select()->from('catalog')->where('id','=',$id)->execute();
return $node;
}

public function MoveFolder($idfrom,$idto){
//ф-ция не доработана по перемещению дерева, возможно обновлю это код когда допишу
$nodefrom = DB::select()->from('catalog')->where('id','=',$idfrom)->execute();
$nodeto = DB::select()->from('catalog')->where('id','=',$idto)->execute();
}
}

Увы, пока причесать код и весь ф-ционал в статье нет возможности и времени. Немного позже постараюсь упорядочить и адаптировать под пхп, но даже то что есть уже должно во многом вам помочь. =) Собственно если у вас есть какие-то замечания или предложения - в комментарии. =)

Категория: 
Share/Save

Делитесь с друзьями в социальных сетях! Оставляйте комментарии!

Share/Save

Это Вам так же может быть интересно!