Появилась задача — вводить в 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; }
