Появилась задача — вводить в web-приложение элементы иерархического списка (например КЛАДР).
Когда работал с drupal, видел там модуль Hierarchical Select, реализующий эту функциональность.
Но хотелось сделать самому — для того, чтоб не привязываться к системам, которые мне полностью не понятны и вообще, интересно.
Поиск готового решения, не завязанного на какую-то существующую систему, дал что-то вроде
этого, что не устроило.
Топик не претендует на новизну и особую сложность исполнения.
Сделал за 2 часа.
Захотел поделится.
Возможно, кому-то пригодится.
Максимально все документировал и функции, описывающие источник данных, вынес в 2 метода:
— поиск детей (один уровень) по родителю
— поиск всех родителей ребенка по иерархии вверх
чтобы желающие могли использовать это для своих нужд.
Возможно, что-то подобное где-то и было, но быстро я это не нашел.
Итак, приступим.
Сразу ссылка на работающий пример и исходные коды
Пример
Можно быстро переписать на свой источник данных (достаточно изменить 2 метода в классе)
Постановка задачи:
Функционал по модулям:
Последовательность изложения:
1. Что нужно сделать с приложением
2. Описание javaScript
3. Класс, обслуживающий запросы ajax
4. Как все это работает
При загрузке страницы, когда модель документа полностью сформирована,
срабатывает инициализирующий код javascript
и для каждого контейнера загружает инициализирующие данные
с помощью функции selecHierarhyInit(объект_контейнера)
Эта функция запрашивает контент, передавая
— идентификатор контейнера, который равен названию элемента ввода
— инициализирующий идентификатор узла иерархии
PHP-класс, получая в функцию getInit($id) этот идентификатор, обрабатывает его.
Если он пуст, то загружает один уровень со списком узлов, подчиненных корню иерархии (я использую «заглушку» -1)
Если нет, то сканирует иерархию вверх от инициализирующего узла, а потом инвертирует полученный словарь,
получая «сверзу вниз» и передает массив на распечатывание
Далее это содержимое (список списков или один список) возвращается javaScript-у, который записывает его в контейнер,
удаляя инициализирующее значение, которое нам больше не нужно
Если оператор изменил select, то вызывается функция selecHierarhygetChilds, которая получает параметром
объект изменившегося списка, выбирает из него параметры:
Удаляет все списки, которые находятся после изменившегося,
потому что при изменении родителя, его детей нужно пересобрать заново
запрашивает у PHP getContent(id) список дочерних, по отношению к этому, узлов и добавляет полученное после (все дети были удалены)
изменившегося списка
5. Обработка результата
обойдусь без комментариев
Когда работал с drupal, видел там модуль Hierarchical Select, реализующий эту функциональность.
Но хотелось сделать самому — для того, чтоб не привязываться к системам, которые мне полностью не понятны и вообще, интересно.
Поиск готового решения, не завязанного на какую-то существующую систему, дал что-то вроде
этого, что не устроило.
Топик не претендует на новизну и особую сложность исполнения.
Сделал за 2 часа.
Захотел поделится.
Возможно, кому-то пригодится.
Максимально все документировал и функции, описывающие источник данных, вынес в 2 метода:
— поиск детей (один уровень) по родителю
— поиск всех родителей ребенка по иерархии вверх
чтобы желающие могли использовать это для своих нужд.
Возможно, что-то подобное где-то и было, но быстро я это не нашел.
Итак, приступим.
Сразу ссылка на работающий пример и исходные коды
Пример
Можно быстро переписать на свой источник данных (достаточно изменить 2 метода в классе)
Постановка задачи:
- Использовать набор динамически генерируемых select-ов
- При выборе узла, подгружается очередной элемент с детьми.
- Дать возможность пользователю возможность двигаться по дереву дальше или остановить свой выбор на этом узле, не выбирая дочерний
- Если элемент уже выбран, и его нужно отредактировать, необходимо загрузить набор элементов, содержащих путь к выбранному узлу
то есть, инициализировать - Генерация без перезагрузки страницы — ajax
- Иерархических списков разного вида может быть много и они не должны конфликтовать
Функционал по модулям:
- Представление контейнера, содержащее все необходимые данные
- javaScript, реагирующий на изменения и запрашивающий элементы, генерируемые «на лету»
- Модуль сборки динамического содержимого
Последовательность изложения:
- Описать необходимые и достаточные данные, которые должно генерировать приложение, к которому это подключается
- Описание javaScript
- Класс, обслуживающий запросы ajax
- Как все это работает
- Обработка результата
1. Что нужно сделать с приложением
- Иметь подключенный javascript jQuery
и скрипт обработки иерархии
<script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="script.js"></script>
- Для каждого списка на странице:
<!-- * Контейнер должен содержать class=select_hierarhy_container * иметь уникальное id контейнера * иметь внутри скрытый элемент ввода, содержащий текущее значение (для инициализации) * old и new нужно для того, чтоб отслеживать изменившееся и операторы не перезатирали друг у друга то, что они не меняли --> <div class=select_hierarhy_container id=field_1> <input type=hidden name=field_1[old] value=""/> </div>
- Скопировать в корень сайта файл ajax.php
2. Описание javaScript
// инициализаторы
$(document).ready(function() {
/**
* Перебрать все контейнеры и инициализировать их элементами первого уровня
*/
$('.select_hierarhy_container').each(function() {
selecHierarhyInit(this);
});
});
/**
* Инициализировать контейнер, содержащий в себе иерархический список. Получает:
* id (containerId), значение по умолчанию (initValue) и загружает в контейнер
* все, что нужно динамически
*
* @param v_container
* объект-контейнер
*/
function selecHierarhyInit(v_container) {
var container = $(v_container);
var containerId = container.attr('id');
var initValue = container.children('input').val();
container.load('ajax.php?container_id=' + containerId + '&init_id='
+ initValue);
}
/**
* Загрузить следующий узел со списком детей для выбранного в v_obj, родителя
* Сначала из изменившегося списка получить параметры: id контейнера
* (containerId) текущий уровень иерархии узла с выбранным элементом (level)
* идентификатор выбранного элемента (parent_id) Удаляет всех детей ниже по
* иерархии, чем измененный (если они были, значит нужно уничтожить для
* уточнения выбора) Запросить контент со списком детей
*
* @param v_obj
* объект со списком select, который изменился
*/
function selecHierarhygetChilds(v_obj) {
obj = $(v_obj);
var containerId = obj.parents('.select_hierarhy_container').attr('id');
var parent_id = obj.val();
obj.nextAll().remove();
$.ajax({
url : 'ajax.php?container_id=' + containerId + '&parent_id='
+ parent_id,
success : function(data) {
$('#' + containerId).append(data);
}
});
}
3. Класс, обслуживающий запросы ajax
/**
* Класс, обслуживающий механизм рекурсивных элементов ввода.
*
* После отправки формы можете взять последний элемент в массиве
* $_POST[$selectName] если он не пуст. Если пуст - предпоследний.
* Реализация такого источника данных (массив, который вшит внутрь файла)
* приводится исключительно в качестве примера и использовать
* на практике довольно бессмысленно (на мой взгляд)
* Скорее всего, источником данных, будет база данных
*
* @author dibrovsd
*/
class HerarhySelect {
// имя select-а
private $selectName;
/**
* @param string $selectName название select-а, который нужно генерировать
*/
public function __construct($selectName) {
$this->selectName = $selectName;
}
/**
* Получить один уровень
* @param int $parentId идентификатор родительского узла
*/
public function getContent($parentId) {
return $this->generateSelect($parentId);
}
/**
* Получить инициализированноые значения списков
* Начиная с указанной ноды, загружаются все родительский узлы
* (снизу вверх)
* Если инициализирующее значение пусто, загружает подчиненные
* корню узлы (один уровень) и добавляет дочерний, по отношению к инициализирующему, уровень
* @param int $nodeId инициализирующее значение
*/
public function getInit($nodeId) {
if($nodeId == '') {
return $this->getContent(-1);
}
else {
$s = null;
$parents = $this->getDataParentsList($nodeId);
foreach($parents as $parentId => $childId) {
$s .= $this->generateSelect($parentId, $childId);
}
$s .= $this->generateSelect($nodeId);
return $s;
}
}
/**
* Генерировать список из детей $parentId, отметив выбранным узел $childId
* @param int $parentId идентификатор родительского узла
* @param int $childId
*/
private function generateSelect($parentId, $childId = null) {
// если детей нет - выйти
$childs = $this->getDataChilds($parentId);
if($childs == null) {
return;
}
$s = '<select
name='.$this->selectName.'[new][]
onChange="selecHierarhygetChilds(this);">
<option></option>';
foreach($childs as $id => $value) {
$s .= '<option value="'.$id.'" '.
($id == $childId ? 'selected' : '').'>'.
$value.'</option>';
}
$s .='</select>';
return $s;
}
/**
* Источник данных. Возвращает ассоциативную хэш-таблицу
* со списком детей
* для переданого родительского узла
*
* Поместите сюда ваш метод получения данных.
* Это может быть любая база данных, web-сервис
* или какой-то массив (как в этом примере)
*
* @param int $parentId Идентификатор родительского узла
*/
private function getDataChilds($parentId) {
$dataSource = $this->getDataArray();
if(!isset($dataSource[$parentId])) {
return null;
}
else {
return $dataSource[$parentId];
}
}
/**
* Получить список родителей по иерархии сверху вниз,
* пока не дойдем до $childId
* Каждый элемент массива - это очередной вниз уровень иерархии
* В ключе идентификатор родителя, в значении - ребенок этого уровня
*
* Делаем (для этого источника данных) за счет раскручивания иерархии снизу
* вверх и потом реверс полученого хэша
* Сделайте для своего источника данных свою реализацию:
* Для Oracle иерархическим запросом,
* Для PostgreSQL рекурсивным запросом
* Для других БД рекурсивной функцией, запрашивающей узел за узлом
* (если они не содержат механизма работы с иерархией)
*
* @param int $child
* @return int:int id родителя : id ребенка,
* которого нужно пометить выбраным в списке элементов,
* подчиненных родителю
*/
private function getDataParentsList($childId) {
$dataSource = $this->getDataArray();
$result = array();
$currentChild = $childId;
// Ищем в цикле родителя за родителем.
// Если родитель не нашелся, мы достигли вершины иерархии
do {
$isParentFind = false;
foreach($dataSource as $idParent => $childs) {
if(isset($childs[$currentChild])) {
$result[$idParent] = $currentChild;
$isParentFind = true;
$currentChild = $idParent;
break;
}
}
}
while($isParentFind);
return array_reverse($result, true);
}
/**
* Функция - поставщик статического набора данных.
* Нужно только для примера, так как, скорее всего,
* источником данных будет база данных,
* а не это извращение
*/
private function getDataArray() {
return array(
-1 => array(1 => 'test1', 2 => 'test2', 3 => 'test3'),
1 => array(4 => 'test1_1', 5 => 'test1_2', 6 => 'test1_3'),
4 => array(7 => 'test2_1', 8 => 'test2_2', 9 => 'test2_3'),
9 => array(10 => 'test2_1_1', 11 => 'test2_1_2', 12 => 'test2_1_3')
);
}
}
4. Как все это работает
При загрузке страницы, когда модель документа полностью сформирована,
срабатывает инициализирующий код javascript
и для каждого контейнера загружает инициализирующие данные
с помощью функции selecHierarhyInit(объект_контейнера)
Эта функция запрашивает контент, передавая
— идентификатор контейнера, который равен названию элемента ввода
— инициализирующий идентификатор узла иерархии
PHP-класс, получая в функцию getInit($id) этот идентификатор, обрабатывает его.
Если он пуст, то загружает один уровень со списком узлов, подчиненных корню иерархии (я использую «заглушку» -1)
Если нет, то сканирует иерархию вверх от инициализирующего узла, а потом инвертирует полученный словарь,
получая «сверзу вниз» и передает массив на распечатывание
Далее это содержимое (список списков или один список) возвращается javaScript-у, который записывает его в контейнер,
удаляя инициализирующее значение, которое нам больше не нужно
Если оператор изменил select, то вызывается функция selecHierarhygetChilds, которая получает параметром
объект изменившегося списка, выбирает из него параметры:
Удаляет все списки, которые находятся после изменившегося,
потому что при изменении родителя, его детей нужно пересобрать заново
запрашивает у PHP getContent(id) список дочерних, по отношению к этому, узлов и добавляет полученное после (все дети были удалены)
изменившегося списка
5. Обработка результата
обойдусь без комментариев
if(isset($_POST['sendForm'])) {
$data1 = array_reverse($_POST['field_1']['new']);
$data2 = array_reverse($_POST['field_2']['new']);
$val1 = $data1[0] == '' ? $data1[1] : $data1[0];
$val2 = $data2[0] == '' ? $data2[1] : $data2[0];
}
else {
$val1 = null;
$val2 = 12;
}