Pull to refresh

Ввод в программу иерархического списка

Reading time7 min
Views4.2K
Появилась задача — вводить в web-приложение элементы иерархического списка (например КЛАДР).

Когда работал с drupal, видел там модуль Hierarchical Select, реализующий эту функциональность.
Но хотелось сделать самому — для того, чтоб не привязываться к системам, которые мне полностью не понятны и вообще, интересно.

Поиск готового решения, не завязанного на какую-то существующую систему, дал что-то вроде
этого, что не устроило.

Топик не претендует на новизну и особую сложность исполнения.
Сделал за 2 часа.
Захотел поделится.
Возможно, кому-то пригодится.
Максимально все документировал и функции, описывающие источник данных, вынес в 2 метода:
— поиск детей (один уровень) по родителю
— поиск всех родителей ребенка по иерархии вверх
чтобы желающие могли использовать это для своих нужд.

Возможно, что-то подобное где-то и было, но быстро я это не нашел.
Итак, приступим.


Сразу ссылка на работающий пример и исходные коды
Пример

Можно быстро переписать на свой источник данных (достаточно изменить 2 метода в классе)

Постановка задачи:
  • Использовать набор динамически генерируемых select-ов
  • При выборе узла, подгружается очередной элемент с детьми.
  • Дать возможность пользователю возможность двигаться по дереву дальше или остановить свой выбор на этом узле, не выбирая дочерний
  • Если элемент уже выбран, и его нужно отредактировать, необходимо загрузить набор элементов, содержащих путь к выбранному узлу
    то есть, инициализировать
  • Генерация без перезагрузки страницы — ajax
  • Иерархических списков разного вида может быть много и они не должны конфликтовать


Функционал по модулям:
  • Представление контейнера, содержащее все необходимые данные
  • javaScript, реагирующий на изменения и запрашивающий элементы, генерируемые «на лету»
  • Модуль сборки динамического содержимого


Последовательность изложения:
  1. Описать необходимые и достаточные данные, которые должно генерировать приложение, к которому это подключается
  2. Описание javaScript
  3. Класс, обслуживающий запросы ajax
  4. Как все это работает
  5. Обработка результата


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;
}
Tags:
Hubs:
Total votes 19: ↑9 and ↓10-1
Comments15

Articles