2.x cca0fd3d0d1a cached
46 files
273.1 KB
74.9k tokens
281 symbols
1 requests
Download .txt
Showing preview only (328K chars total). Download the full file or copy to clipboard to get everything.
Repository: DigitalWand/digitalwand.admin_helper
Branch: 2.x
Commit: cca0fd3d0d1a
Files: 46
Total size: 273.1 KB

Directory structure:
gitextract_k9xdrsiz/

├── .gitignore
├── LICENSE.md
├── README.md
├── admin/
│   └── route.php
├── composer.json
├── include.php
├── install/
│   ├── admin/
│   │   └── admin_helper_route.php
│   ├── index.php
│   └── version.php
├── lang/
│   └── ru/
│       ├── install/
│       │   └── index.php
│       └── lib/
│           ├── EntityManager.php
│           ├── helper/
│           │   ├── AdminBaseHelper.php
│           │   ├── AdminEditHelper.php
│           │   └── AdminListHelper.php
│           └── widget/
│               ├── CheckboxWidget.php
│               ├── ComboBoxWidget.php
│               ├── FileWidget.php
│               ├── HelperWidget.php
│               ├── IblockElementWidget.php
│               ├── NumberWidget.php
│               ├── OrmElementWidget.php
│               └── UrlWidget.php
└── lib/
    ├── EntityManager.php
    ├── EventHandlers.php
    ├── Sorting.php
    ├── helper/
    │   ├── AdminBaseHelper.php
    │   ├── AdminEditHelper.php
    │   ├── AdminInterface.php
    │   ├── AdminListHelper.php
    │   ├── AdminSectionEditHelper.php
    │   ├── AdminSectionListHelper.php
    │   └── Exception.php
    └── widget/
        ├── CheckboxWidget.php
        ├── ComboBoxWidget.php
        ├── DateTimeWidget.php
        ├── FileWidget.php
        ├── HLIBlockFieldWidget.php
        ├── HelperWidget.php
        ├── IblockElementWidget.php
        ├── NumberWidget.php
        ├── OrmElementWidget.php
        ├── StringWidget.php
        ├── TextAreaWidget.php
        ├── UrlWidget.php
        ├── UserWidget.php
        └── VisualEditorWidget.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
/.idea
/composer.lock

================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)

Copyright © 2015 DigitalWand (http://digitalwand.ru/)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# digitalwand.admin_helper
API для сборки кастомных админок в Битриксе

Документация по модулю доступна по адресу [http://api.digitalwand.ru/admin_helper/](http://api.digitalwand.ru/admin_helper/). Её же можно прочитать в комментариях в коде модуля. 

Есть хорошая вводная статья в блоге: [Генератор админок «Битрикса»](http://samokhvalov.info/blog/all/bitrix-admin-helper/).

Простой рабочий пример реализован отдельным модулем 
[demo.adminhelper](https://github.com/DigitalWand/demo.adminhelper)


## Концепция
Данный модуль реализует подход MVC для создания административного интерфейса.

Возможность построения административного интерфейса появляется благодаря наличию единого API для CRUD-операциями над
сущностями. Поэтому построение админ. интерфейса средствами данного модуля возможно только для классов, реализующих
API ORM Битрикс. При желании использовать данный модуль для сущностей, не использующих ORM Битрикс, можно
подготовить для таких сущностей класс-обёртку, реализующий необходимые функции.

Основные понятия модуля:
<ul>
<li>Модель: "model" в терминах MVC. Класс, унаследованный от DataManager или реализующий аналогичный API.</li>
<li>Хэлпер: "view" в терминах MVC. Класс, реализующий отрисовку интерфейса списка или детальной страницы.</li>
<li>Роутер: "controller" в терминах MVC. Файл, принимающий все запросы к админке данного модуля, создающий нужные
хэлперы с нужными настройками. С ним напрямую работать не придётся.</li>
<li>Виджеты: "delegate" в терминах MVC. Классы, отвечающие за отрисовку элементов управления для отдельных полей
сущностей. В списке и на детальной.</li>
</ul>

Схема работы с модулем следующая:
<ul>
<li>Реализация класса AdminListHelper - для управления страницей списка элементов</li>
<li>Реализация класса AdminEditHelper - для управления страницей просмотра/редактирования элемента</li>
<li>Создание файла Interface.php с вызовом AdminBaseHelper::setInterfaceSettings(), в которую передается
конфигурация
полей админки и классы, используемые для её построения.</li>
<li>Если не хватает возможностей виджетов, идущих с модулем, можно реализовать свой виджет, унаследованный от любого
другого готового виджета или от абстрактного класса HelperWidget</li>
</ul>

Рекомендуемая файловая структура для модулей, использующих данный функционал:
<ul>
<li>Каталог <b>admin</b>. Достаточно поместить в него файл menu.php, отдельные файлы для списка и детальной
создавать не надо благодаря единому роутингу.</li>
<li>Каталог <b>classes</b> (или lib): содержит классы модели, представлений и делегатов.</li>
<li> -- <b>classes/helper</b>: каталог, содержащий классы "view", унаследованные от AdminListHelper и
AdminEditHelper.</li>
<li> -- <b>classes/widget</b>: каталог, содержащий виджеты ("delegate"), если для модуля пришлось создавать
свои.</li>
<li> -- <b>classes/model</b>: каталог с моделями, если пришлось переопределять поведение стандартных функций getList
и т.д.</li>
</ul>

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

## Разработчики

<ul>
<li><a href="http://digitalwand.ru/">DigitalWand</a></li>
<li><a href="http://nota.media/">Notamedia</a></li>
</ul>


================================================
FILE: admin/route.php
================================================
<?php

use Bitrix\Main\Loader;
use DigitalWand\AdminHelper\Helper\AdminBaseHelper;
use DigitalWand\AdminHelper\Helper\AdminListHelper;
use DigitalWand\AdminHelper\Helper\AdminEditHelper;
use DigitalWand\AdminHelper\Helper\AdminInterface;

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_before.php');

Loader::includeModule('digitalwand.admin_helper');

function getRequestParams($param)
{
	if (!isset($_REQUEST[$param])) {
		return false;
	}
	else {
		return htmlspecialcharsbx($_REQUEST[$param]);
	}
}

$module = getRequestParams('module');
$view = getRequestParams('view');
$entity = getRequestParams('entity');

if (!$module OR !$view OR !Loader::IncludeModule($module)) {
	include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php';
}

// Собираем имя класса админского интерфейса
$moduleNameParts = explode('.', $module);
$entityNameParts = explode('_', $entity);
$interfaceNameParts = array_merge($moduleNameParts, $entityNameParts);
$interfaceNameClass = null;
$viewParts = explode('_', $view);

$count = count($viewParts);
for ($i = 0; $i < $count; $i++) {
	$interfaceName = implode('', array_map('ucfirst', $viewParts));
	$parts = $interfaceNameParts;
	$parts[] = $interfaceName . 'AdminInterface';
	$class = array_map('ucfirst', $parts);
	$interfaceNameClass = implode('\\', $class);

	if (class_exists($interfaceNameClass)) {
		break;
	}
	else {
		$className = array_pop($parts);
		$parts[] = 'AdminInterface';
		$parts[] = $className;
		$class = array_map('ucfirst', $parts);
		$interfaceNameClass = implode('\\', $class);
		if (class_exists($interfaceNameClass)) {
			break;
		}
	}
	array_pop($viewParts);
}

/**
 * @var AdminInterface $interfaceNameClass
 */

if ($interfaceNameClass && class_exists($interfaceNameClass)) {
	$interfaceNameClass::register();
}

list($helper, $interface) = AdminBaseHelper::getGlobalInterfaceSettings($module, $view);

if (!$helper OR !$interface) {
	include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php';
}

$isPopup = isset($_REQUEST['popup']) AND $_REQUEST['popup'] == 'Y';
$fields = isset($interface['FIELDS']) ? $interface['FIELDS'] : array();
$tabs = isset($interface['TABS']) ? $interface['TABS'] : array();
$helperType = false;

if (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminEditHelper')) {
	$helperType = 'edit';
	/**
	 * @var AdminEditHelper $adminHelper
	 */
	$adminHelper = new $helper($fields, $tabs);
}
elseif (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminListHelper')) {
	$helperType = 'list';
	/**
	 * @var AdminListHelper $adminHelper
	 */
	$adminHelper = new $helper($fields, $isPopup);
	$adminHelper->buildList(array($by => $order));
}
elseif (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminBaseHelper')) {
	$adminHelper = new $helper($fields, $tabs);
}
else {
	include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php';
	exit();
}

if ($isPopup) {
	require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_popup_admin.php");
}
else {
	require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_admin_after.php");
}

if ($helperType == 'list') {
	$adminHelper->createFilterForm();
}

$adminHelper->show();

if ($isPopup) {
	require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_popup_admin.php");
}
else {
	require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_admin.php");
}


================================================
FILE: composer.json
================================================
{
  "name": "digitalwand/digitalwand.admin_helper",
  "description": "API for custom admin interface in Bitrix by DigitalWand and Notamedia agency",
  "type": "bitrix-module",
  "license": "MIT",
  "keywords": [
    "bitrix",
    "admin",
    "api"
  ],
  "support": {
    "issues": "https://github.com/DigitalWand/digitalwand.admin_helper/issues",
    "source": "https://github.com/DigitalWand/digitalwand.admin_helper"
  },
  "authors": [
    {
      "name": "Alexey Volkov",
      "email": "asgalex@gmail.com"
    },
    {
      "name": "Nik Samokhvalov",
      "homepage": "http://samokhvalov.info",
      "email": "nik@samokhvalov.info"
    }
  ],
  "extra": {
    "installer-name": "digitalwand.admin_helper"
  },
  "require": {
    "php": ">=5.3.0",
    "composer/installers": "~1"
  },
  "minimum-stability": "dev"
}


================================================
FILE: include.php
================================================
<?php

use Bitrix\Main\Loader;

Loader::registerAutoLoadClasses('digitalwand.admin_helper',
    array(
        'DigitalWand\AdminHelper\EventHandlers' => 'lib/EventHandlers.php',

        'DigitalWand\AdminHelper\Helper\Exception' => 'lib/helper/Exception.php',

        'DigitalWand\AdminHelper\Helper\AdminInterface' => 'lib/helper/AdminInterface.php',
        'DigitalWand\AdminHelper\Helper\AdminBaseHelper' => 'lib/helper/AdminBaseHelper.php',
        'DigitalWand\AdminHelper\Helper\AdminListHelper' => 'lib/helper/AdminListHelper.php',
        'DigitalWand\AdminHelper\Helper\AdminSectionListHelper' => 'lib/helper/AdminSectionListHelper.php',
        'DigitalWand\AdminHelper\Helper\AdminEditHelper' => 'lib/helper/AdminEditHelper.php',
        'DigitalWand\AdminHelper\Helper\AdminSectionEditHelper' => 'lib/helper/AdminSectionEditHelper.php',

        'DigitalWand\AdminHelper\EntityManager' => 'lib/EntityManager.php',
        'DigitalWand\AdminHelper\Sorting' => 'lib/Sorting.php',

        'DigitalWand\AdminHelper\Widget\HelperWidget' => 'lib/widget/HelperWidget.php',
        'DigitalWand\AdminHelper\Widget\CheckboxWidget' => 'lib/widget/CheckboxWidget.php',
        'DigitalWand\AdminHelper\Widget\ComboBoxWidget' => 'lib/widget/ComboBoxWidget.php',
        'DigitalWand\AdminHelper\Widget\StringWidget' => 'lib/widget/StringWidget.php',
        'DigitalWand\AdminHelper\Widget\NumberWidget' => 'lib/widget/NumberWidget.php',
        'DigitalWand\AdminHelper\Widget\FileWidget' => 'lib/widget/FileWidget.php',
        'DigitalWand\AdminHelper\Widget\TextAreaWidget' => 'lib/widget/TextAreaWidget.php',
        'DigitalWand\AdminHelper\Widget\HLIBlockFieldWidget' => 'lib/widget/HLIBlockFieldWidget.php',
        'DigitalWand\AdminHelper\Widget\DateTimeWidget' => 'lib/widget/DateTimeWidget.php',
        'DigitalWand\AdminHelper\Widget\IblockElementWidget' => 'lib/widget/IblockElementWidget.php',
        'DigitalWand\AdminHelper\Widget\UrlWidget' => 'lib/widget/UrlWidget.php',
        'DigitalWand\AdminHelper\Widget\VisualEditorWidget' => 'lib/widget/VisualEditorWidget.php',
        'DigitalWand\AdminHelper\Widget\UserWidget' => 'lib/widget/UserWidget.php',
        'DigitalWand\AdminHelper\Widget\OrmElementWidget' => 'lib/widget/OrmElementWidget.php',
    )
);


================================================
FILE: install/admin/admin_helper_route.php
================================================
<?
if (!@include_once $_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/digitalwand.admin_helper/admin/route.php") {
    if (!@include_once $_SERVER["DOCUMENT_ROOT"] . "/local/modules/digitalwand.admin_helper/admin/route.php") {
        include $_SERVER['DOCUMENT_ROOT'] . '/bitrix/admin/404.php';
    }
}


================================================
FILE: install/index.php
================================================
<?php

use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);

if (class_exists('digitalwand_admin_helper')) return;

class digitalwand_admin_helper extends CModule
{
    var $MODULE_ID = 'digitalwand.admin_helper';
    var $MODULE_VERSION;
    var $MODULE_VERSION_DATE;
    var $MODULE_NAME;
    var $MODULE_DESCRIPTION;
    var $MODULE_GROUP_RIGHTS = 'Y';
    var $MODULE_CSS;
    var $PARTNER_NAME = 'DigitalWand & Notamedia';
    var $PARTNER_URI = '';

    function digitalwand_admin_helper()
    {
        include __DIR__ . '/version.php';

        $this->MODULE_VERSION = ADMIN_HELPER_VERSION;
        $this->MODULE_VERSION_DATE = ADMIN_HELPER_VERSION_DATE;
        $this->MODULE_NAME = Loc::getMessage('ADMIN_HELPER_INSTALL_NAME');
        $this->MODULE_DESCRIPTION = Loc::getMessage('ADMIN_HELPER_INSTALL_DESCRIPTION');
    }

    function DoInstall()
    {

        $eventManager = \Bitrix\Main\EventManager::getInstance();

        RegisterModule($this->MODULE_ID);
        $this->InstallFiles();

        $eventManager->registerEventHandler(
            'main',
            'OnPageStart',
            $this->MODULE_ID,
            '\DigitalWand\AdminHelper\EventHandlers',
            'onPageStart'
        );
    }

    function DoUninstall()
    {
        $eventManager = \Bitrix\Main\EventManager::getInstance();

        UnRegisterModule($this->MODULE_ID);

        $eventManager->unRegisterEventHandler(
            'main',
            'OnPageStart',
            $this->MODULE_ID,
            '\DigitalWand\AdminHelper\EventHandlers',
            'onPageStart'
        );
    }

    function InstallFiles()
    {
        CopyDirFiles(__DIR__ . '/admin', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/admin');

        return true;
    }
}

================================================
FILE: install/version.php
================================================
<?
define('ADMIN_HELPER_VERSION', '2.0.0');
define('ADMIN_HELPER_VERSION_DATE', '2016-01-22');
?>


================================================
FILE: lang/ru/install/index.php
================================================
<?php
$MESS['ADMIN_HELPER_INSTALL_NAME'] = 'API AdminHelper';
$MESS['ADMIN_HELPER_INSTALL_DESCRIPTION'] = 'API для построения административного интерфейса для highload-инфоблоков';
$MESS['ADMIN_HELPER_INSTALL_TITLE'] = 'Установка модуля "digitalwand.admin_helper"';
$MESS['ADMIN_HELPER_INSTALL_COMPLETE_OK'] = 'Установка завершена.';
$MESS['ADMIN_HELPER_INSTALL_BACK'] = 'Вернуться в управление модулями';
$MESS['ADMIN_HELPER_UNINSTALL_COMPLETE'] = 'Удаление завершено.';

================================================
FILE: lang/ru/lib/EntityManager.php
================================================
<?php
$MESS['DIGITALWAND_AH_RELATION_SHOULD_BE_MULTIPLE_FIELD'] = "Связь должна быть множественным полем";
$MESS['DIGITALWAND_AH_ARGUMENT_CANT_CONTAIN_ID'] = "Аргумент %A% не может содержать идентификатор элемента";
$MESS['DIGITALWAND_AH_ARGUMENT_SHOULD_CONTAIN_ID'] = "Аргумент %A% должен содержать идентификатор элемента";


================================================
FILE: lang/ru/lib/helper/AdminBaseHelper.php
================================================
<?php

$MESS['DIGITALWAND_ADMIN_HELPER_GETMODEL_EXCEPTION'] = 'Попыка обращение к несуществующему HL-инфоблоку #CLASS#';
$MESS['DIGITALWAND_ADMIN_HELPER_ACCESS_FORBIDDEN'] = 'Доступ запрещен';

================================================
FILE: lang/ru/lib/helper/AdminEditHelper.php
================================================
<?php
$MESS['DEFAULT_TAB'] = 'Элемент';
$MESS['DIGITALWAND_ADMIN_HELPER_RETURN_TO_LIST'] = 'Список';
$MESS['DELETE'] = 'Удалить';
$MESS['VALIDATION_ERROR'] = "Не заполнены обязательные поля:\n#FIELD_LIST#";
$MESS['VALIDATION_ERROR_FIELDS'] = "Не заполнены обязательные поля:";
$MESS['DIGITALWAND_ADMIN_HELPER_NEW_ELEMENT'] = "Новый элемент";
$MESS['DIGITALWAND_ADMIN_HELPER_EDIT_TITLE'] = "Элемент #ID#";
$MESS['DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_CONFIRM'] = "Удалить запись?";
$MESS['DIGITALWAND_ADMIN_HELPER_ACTIONS'] = "Действия";
$MESS['DIGITALWAND_ADMIN_HELPER_ADD_ELEMENT'] = "Добавить элемент";
$MESS['DIGITALWAND_ADMIN_HELPER_DELETE_ELEMENT'] = "Удалить элемент";
$MESS['DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_FORBIDDEN'] = 'Не хватает прав доступа для удаления элемента';
$MESS['DIGITALWAND_ADMIN_HELPER_EDIT_WRITE_FORBIDDEN'] = 'Не хватает прав доступа для изменения элемента';


================================================
FILE: lang/ru/lib/helper/AdminListHelper.php
================================================
<?php
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_CHECKED'] = 'Выбрано:';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_EDIT'] = 'Редактировать';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_DELETE'] = 'Удалить';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_ACTIVE'] = 'Активировать';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_DEACTIVATE'] = 'Деактивировать';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_DELETE_CONFIRM'] = 'Удалить выбранные записи?';
$MESS['DIGITALWAND_ADMIN_HELPER_SAVE_ERROR'] = 'Ошибка сохранения элемента';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_CREATE_NEW'] = 'Создать элемент';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_CREATE_NEW_SECTION'] = 'Создать раздел';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_SECTION_UP'] = 'На раздел выше';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_SECTION_DELETE'] = 'Удалить раздел';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_SELECT'] = 'Выбрать';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_DELETE_FORBIDDEN'] = 'Не хватает прав доступа для удаления элемента';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_WRITE_FORBIDDEN'] = 'Не хватает прав доступа для изменения элемента';
$MESS['DIGITALWAND_ADMIN_HELPER_LIST_SECTION_HELPER_NOT_FOUND'] = 'Хелпер для раздела не найден';

================================================
FILE: lang/ru/lib/widget/CheckboxWidget.php
================================================
<?php

$MESS['DIGITALWAND_AH_CHECKBOX_YES'] = 'Да';
$MESS['DIGITALWAND_AH_CHECKBOX_NO'] = 'Нет';

================================================
FILE: lang/ru/lib/widget/ComboBoxWidget.php
================================================
<?php

$MESS['DIGITALWAND_AH_COMBO_BOX_LIST_EMPTY'] = 'Не выбрано';
$MESS['DIGITALWAND_AH_MISSING_VARIANTS'] = 'Не удалось получить данные для выбора';

================================================
FILE: lang/ru/lib/widget/FileWidget.php
================================================
<?php

$MESS['DIGITALWAND_AH_FAIL_ADD_FILE'] = 'Не удалось добавить файл #FILE_NAME#';

================================================
FILE: lang/ru/lib/widget/HelperWidget.php
================================================
<?php

$MESS['DIGITALWAND_AH_REQUIRED_FIELD_ERROR'] = 'Обязательное поле "#FIELD#" не заполнено';
$MESS['DIGITALWAND_AH_DUPLICATE_FIELD_ERROR'] = 'Значение поля "#FIELD#" не является уникальным';
$MESS['DIGITALWAND_AH_MULTI_NOT_SUPPORT'] = 'Поле не поддерживает множественный режим';
$MESS['DIGITALWAND_AH_MULTI_ADD'] = "Добавить...";

================================================
FILE: lang/ru/lib/widget/IblockElementWidget.php
================================================
<?php
$MESS['IBLOCK_ELEMENT_NOT_FOUND'] = 'Элемент не найден';

================================================
FILE: lang/ru/lib/widget/NumberWidget.php
================================================
<?php
$MESS['VALUE_IS_NOT_NUMERIC'] = 'Поле #FIELD# должно содержать число';

================================================
FILE: lang/ru/lib/widget/OrmElementWidget.php
================================================
<?php

$MESS['TITLE_FIELD_NAME'] = 'Элемент не найден';
$MESS['DIGITALWAND_AH_ORM_MISSING_ELEMENTS'] = 'Элементы не найдены';

================================================
FILE: lang/ru/lib/widget/UrlWidget.php
================================================
<?php
$MESS['PROTOCOL_REQUIRED'] = 'Укажите протокол ссылки "#FIELD#" (например http://)';

================================================
FILE: lib/EntityManager.php
================================================
<?php

namespace DigitalWand\AdminHelper;

use Bitrix\Main\Localization\Loc;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\Entity\DataManager;
use Bitrix\Main\Entity;
use DigitalWand\AdminHelper\Helper\AdminBaseHelper;
use DigitalWand\AdminHelper\Widget\HelperWidget;

/**
 * Менеджер для управления моделью, способный анализировать её связи и сохранять данные в связанные сущности на
 * основании полученных данных от виджетов.
 *
 * Пример создания сущности:
 * ```
 * $filmManager = new EntityManager('\Vendor\Module\FilmTable', array(
 *        // Данные сущности
 *        'TITLE' => 'Монстры на каникулах 2',
 *        'YEAR' => 2015,
 *        // У сущности FilmTable есть связь с RelatedLinksTable через поле RELATED_LINKS.
 *        // Если передать ей данные, то они будут обработаны
 *        // Представим, что у сущности RelatedLinksTable есть поля ID и VALUE (в этом поле хранится ссылка), FILM_ID
 *        // В большинстве случаев, данные передаваемые связям генерируются множественными виджетами
 *        'RELATED_LINKS' => array(
 *            // Переданный ниже массив будет обработан аналогично коду RelatedLinksTable::add(array('VALUE' =>
 * 'yandex.ru')); array('VALUE' => 'yandex.ru'),
 *            // Если в массив добавить ID, то запись обновится: RelatedLinksTable::update(3, array('ID' => 3, 'VALUE'
 * => 'google.com')); array('ID' => 3, 'VALUE' => 'google.com'),
 *            // ВНИМАНИЕ: данный класс руководствуется принципом: что передано для связи, то сохранится или обновится,
 * что не передано, будет удалено
 *            // То есть, если в поле связи RELATED_LINKS передать пустой массив, то все значения связи будут удалены
 *        )
 * ));
 * $filmManager->save();
 * ```
 *
 * Пример удаления сущности
 * ```
 * $articleManager = new EntityManager('\Vendor\Module\ArticlesTable', array(), 7, $adminHelper);
 * $articleManager->delete();
 * ```
 *
 * Как работает сохранение данных? Дополнительный пример
 * Допустим, что есть модели NewsTable (новости) и NewsLinksTable (ссылки на дополнительную информацию о новости)
 *
 * У модели NewsTable есть связь с моделью NewsLinksTable через поле NEWS_LINKS:
 * ```
 * DataManager::getMap() {
 * ...
 * new Entity\ReferenceField(
 *        'NEWS_LINKS',
 *        '\Vendor\Module\NewsLinksTable',
 *        array('=this.ID' => 'ref.NEWS_ID'),
 *        'ref.FIELD' => new DB\SqlExpression('?s', 'NEWS_LINKS'),
 *        'ref.ENTITY' => new DB\SqlExpression('?s', 'news'),
 * ),
 * ...
 * }
 * ```
 *
 * Попробуем сохранить
 * ```
 * $newsManager = new EntityManager(
 *        '\Vendor\Module\NewsTable',
 *        array(
 *            'TITLE' => 'News title',
 *            'CONTENT' => 'News content',
 *            'NEWS_LINKS' => array(
 *                array('LINK' => 'test.ru'),
 *                array('LINK' => 'test2.ru'),
 *                array('ID' => 'id ссылки', 'LINK' => 'test3.ru'),
 *            )
 *        ),
 *        null,
 *        $adminHelper
 * );
 * $newsManager->save();
 * ```
 *
 * В данном примере передаются данные для новости (заголовок и содержимое) и данные для поля-связи NEWS_LINKS.
 *
 * Далее EntityManager:
 * 1. Вырезает данные, которые предназначены связям
 * 2. Подставляет в них данные из основной модели на основе условий связи
 * Например для связи с полем NEWS_LINKS подставятся данные:
 *
 * ```
 * NewsLinksTable::ENTITY_ID => NewsTable::ID,
 * NewsLinksTable::FIELD => 'NEWS_LINKS',
 * NewsLinksTable::ENTITY => 'news'
 * ```
 *
 * 3. После подстановки данных они будут переданы модели связи подобно коду ниже:
 *
 * ```
 * NewsLinksTable::add(array(
 *          'ENTITY' => 'news',
 *          'FIELD' => 'NEWS_LINKS',
 *          'ENTITY_ID' => 'id сущности, например новости',
 *          'LINK' => 'test.ru'
 * ));
 * NewsLinksTable::add(array(
 *      'ENTITY' => 'news',
 *      'FIELD' => 'NEWS_LINKS',
 *      'ENTITY_ID' => 'id сущности',
 *      'LINK' => 'test2.ru'
 * ));
 * NewsLinksTable::update('id ссылки', array(
 *      'ENTITY' => 'news',
 *      'FIELD' => 'NEWS_LINKS',
 *      'ENTITY_ID' => 'id сущности',
 *      'LINK' => 'test3.ru'
 * ));
 * ```
 *
 * Обратите внимание, что в метод EntityManager::save() были изначально передано только поле LINK, поля ENTITY,
 * ENTITY_ID и FIELD были подставлены классом EntityManager автоматически (предыдущий пункт) А так же важно, что для
 * третьей ссылки был передан идентификатор, поэтому выполнился NewsLinksTable::update, а не NewsLinksTable::add
 *
 * 4. Далее `EntityManager` удаляет данные связанной модели `NewsLinksTable`, которые не были добавлены или обновлены.
 *
 * <b>Как работает удаление?</b>
 *
 * 1. EntityManager получает из `NewsTable::getMap()` поля-связи
 * 2. Получает поля описанные в интерфейсе генератора админки
 * 3. Удаляет значения для полей-связей, которые объявлены в интерфейсе
 *
 * <i>Примечание.</i>
 * EntityManager управляет только данными, которые получает при помощи связи стандартными средставами битрикса.
 * Например, при удалении NewsTable будут удалены только NewsLinksTable, где:
 *
 * ```
 * NewsLinksTable::ENTITY_ID => NewsTable::ID,
 * NewsLinksTable::FIELD => 'NEWS_LINKS',
 * NewsLinksTable::ENTITY => 'news'
 * ```
 *
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 * @author Dmitriy Baibuhtin <dmitriy.baibuhtin@ya.ru>
 */
class EntityManager
{
	/**
	 * @var string Класс модели.
	 */
	protected $modelClass;
	/**
	 * @var Entity\Base Сущность модели.
	 */
	protected $model;
	/**
	 * @var array Данные для обработки.
	 */
	protected $data;
	/**
	 * @var integer Идентификатор записи.
	 */
	protected $itemId = null;
	/**
	 * @var string Поле модели, в котором хранится идентификатор записи.
	 */
	protected $modelPk = null;
	/**
	 * @var array Данные для связей (новые).
	 */
	protected $referencesData = array();
	/**
	 * Вспомогательный массив метода $this->ReferenceDataSet()
	 * @var array Данные для связей (то чт уже было в базе).
	 */
	protected $referenceDataSave = array();
	/**
	 * @var AdminBaseHelper Хелпер.
	 */
	protected $helper;
	/**
	 * @var array Предупреждения.
	 */
	protected $notes = array();

	/**
	 * @param string $modelClass Класс основной модели, наследника DataManager.
	 * @param array $data Массив с сохраняемыми данными.
	 * @param integer $itemId Идентификатор сохраняемой записи.
	 * @param AdminBaseHelper $helper Хелпер, инициирующий сохранение записи.
	 */
	public function __construct($modelClass, array $data = array(), $itemId = null, AdminBaseHelper $helper)
	{
		Loc::loadMessages(__FILE__);
		
		$this->modelClass = $modelClass;
		$this->model = $modelClass::getEntity();
		$this->data = $data;
		$this->modelPk = $this->model->getPrimary();
		$this->helper = $helper;

		if (!empty($itemId)) {
			$this->setItemId($itemId);
		}
	}

    /**
     * Сохранить запись и данные связей.
     *
     * @return Entity\AddResult|Entity\UpdateResult
     */
    public function save()
    {
        $this->collectReferencesData();

        /**
         * @var DataManager $modelClass
         */
        $modelClass = $this->modelClass;
		$db = $this->model->getConnection();
		$db->startTransaction(); // начало транзакции

		if (empty($this->itemId)) {
			$result = $modelClass::add($this->data);

			if ($result->isSuccess()) {
				$this->setItemId($result->getId());
			}
		}
		else {
			$result = $modelClass::update($this->itemId, $this->data);
		}

        if ($result->isSuccess()) {
			$referencesDataResult = $this->processReferencesData();
			if($referencesDataResult->isSuccess()){
				$db->commitTransaction(); // ошибок нет - применяем изменения
			}else{
				$result = $referencesDataResult; // возвращаем ReferencesResult что бы вернуть ошибку
				$db->rollbackTransaction(); // что-то пошло не так - возвращаем все как было
			}
		} else {
			$db->rollbackTransaction();
		}

		return $result;
	}

    /**
     * Удаление запись и данные связей.
     *
     * @return Entity\DeleteResult
     */
    public function delete()
    {
        // Удаление данных зависимостей
		$db = $this->model->getConnection();
		$db->startTransaction(); // начало транзакции

		$result = $this->deleteReferencesData(); // удаляем зависимые сущности

		if(!$result->isSuccess()){ // если хотя бы одна из них не удалилась
			$db->rollbackTransaction(); // то восстанавливаем все
			return $result; // возвращаем ошибку
		}

		$model = $this->modelClass;

		//Если передается массив, то получаем ИД записи
        if (!is_null($this->itemId)) {
            $result = $model::delete($this->itemId); // удаляем основную сущность
        } elseif (!is_array($this->helper->getPk())) {
            $result = $model::delete($this->helper->getPk()); // удаляем основную сущность
        } else {
            $result = new Entity\DeleteResult();
            $error = new Entity\EntityError('Can\'t find element\'s ID');
            $result->addError($error);
        }

		if(!$result->isSuccess()){  // если не удалилась
			$db->rollbackTransaction(); // то восстанавливаем зависимые сущности
			return $result; // возвращаем ошибку
		}

		$db->commitTransaction(); // все прошло без ошибок применяем изменения
		return $result;
    }

	/**
	 * Получить список предупреждений
	 * @return array
	 */
	public function getNotes()
	{
		return $this->notes;
	}

	/**
	 * Добавить предупреждение
	 *
	 * @param $note
	 * @param string $key Ключ для избежания дублирования сообщений
	 *
	 * @return bool
	 */
	protected function addNote($note, $key = null)
	{
		if ($key) {
			$this->notes[$key] = $note;
		}
		else {
			$this->notes[] = $note;
		}

		return true;
	}

	/**
	 * Установка текущего идентификатора модели.
	 *
	 * @param integer $itemId Идентификатор записи.
	 */
	protected function setItemId($itemId)
	{
		$this->itemId = $itemId;
		$this->data[$this->modelPk] = $this->itemId;
	}

	/**
	 * Получение связей
	 *
	 * @return array
	 */
	protected function getReferences()
	{
		/**
		 * @var DataManager $modelClass
		 */
		$modelClass = $this->modelClass;
		$entity = $modelClass::getEntity();
		$fields = $entity->getFields();
		$references = array();

		foreach ($fields as $fieldName => $field) {
			if ($field instanceof Entity\ReferenceField) {
				$references[$fieldName] = $field;
			}
		}

		return $references;
	}

	/**
	 * Извлечение данных для связей
	 */
	protected function collectReferencesData()
	{
		$result = new Entity\Result();
		$references = $this->getReferences();
		// Извлечение данных управляемых связей
		foreach ($references as $fieldName => $reference) {
			if (array_key_exists($fieldName, $this->data)) {
				if (!is_array($this->data[$fieldName])) {
					$result->addError(new Entity\EntityError(Loc::getMessage('DIGITALWAND_AH_RELATION_SHOULD_BE_MULTIPLE_FIELD')));

					return $result;
				}
				// Извлечение данных для связи
				$this->referencesData[$fieldName] = $this->data[$fieldName];
				unset($this->data[$fieldName]);
			}
		}

		return $result;
	}

    /**
     * Обработка данных для связей.
     *
     * @throws ArgumentException
     */
    protected function processReferencesData()
    {
        /**
         * @var DataManager $modelClass
         */
        $modelClass = $this->modelClass;
        $entity = $modelClass::getEntity();
        $fields = $entity->getFields();
		$result = new Entity\Result(); // пустой Result у которого isSuccess вернет true

		foreach ($this->referencesData as $fieldName => $referenceDataSet) {
			if (!is_array($referenceDataSet)) {
				continue;
			}

			/**
			 * @var Entity\ReferenceField $reference
			 */
			$reference = $fields[$fieldName];
			$referenceDataSet = $this->linkDataSet($reference, $referenceDataSet);
			$referenceStaleDataSet = $this->getReferenceDataSet($reference);
			$fieldWidget = $this->getFieldWidget($fieldName);
			$variantsField = $fieldWidget->getSettings('VARIANTS');

			// Создание и обновление привязанных данных
			$processedDataIds = array();
			foreach ($referenceDataSet as $referenceData) {
				if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
					// Создание связанных данных
					if (!empty($referenceData[$fieldWidget->getMultipleField('VALUE')])) {
						$result = $this->createReferenceData($reference, $referenceData);

                        if ($result->isSuccess()) {
                            $processedDataIds[] = $result->getId();
                        } else {
							break; // ошибка, прерываем обработку данных
						}
                    }
                } else {
                    // Обновление связанных данных
					$result = $this->updateReferenceData($reference, $referenceData, $referenceStaleDataSet);

                    if ($result->isSuccess()) {
                        $processedDataIds[] = $referenceData[$fieldWidget->getMultipleField('ID')];
                    } else {
						break; // ошибка, прерываем обработку данных
					}
                }
            }

			if($result->isSuccess()){ // Удаление записей, которые не были созданы или обновлены
				foreach ($referenceStaleDataSet as $referenceData) {
					if (
                        !in_array($referenceData[$fieldWidget->getMultipleField('ID')], $processedDataIds)
                        OR
                        ($variantsField
                            AND !in_array($referenceData[$fieldWidget->getMultipleField('ID')], $processedDataIds)
                            AND array_key_exists($referenceData[$fieldWidget->getMultipleField('VALUE')], $variantsField))
					) {
                        if ($fieldWidget->getSettings('DELETE_REFERENCED_DATA')) {
                            $result = $this->deleteReferenceData($reference,
                                $referenceData[$fieldWidget->getMultipleField('ID')]);
                        } else {
                            $result = $this->deleteReference($reference,
                                $referenceData[$fieldWidget->getMultipleField('ID')]);
                        }
						if(!$result->isSuccess()) {
							break; // ошибка, прерываем удаление данных
						}
					}
				}
			}
        }

        $this->referencesData = array();
		return $result;
    }

    /**
     * Удаление данных всех связей, которые указаны в полях интерфейса раздела.
     */
    protected function deleteReferencesData()
    {
        $references = $this->getReferences();
        $fields = $this->helper->getFields();
		$result = new Entity\Result();
        /**
         * @var string $fieldName
         * @var Entity\ReferenceField $reference
         */
        foreach ($references as $fieldName => $reference) {
            // Удаляются только данные связей, которые объявлены в интерфейсе
            if (!isset($fields[$fieldName])) {
                continue;
            }

			$fieldWidget = $this->getFieldWidget($reference->getName());
			$referenceStaleDataSet = $this->getReferenceDataSet($reference);

            foreach ($referenceStaleDataSet as $referenceData) {
                $result = $this->deleteReferenceData($reference, $referenceData[$fieldWidget->getMultipleField('ID')]);
				if(!$result->isSuccess()){
					return $result;
				}
            }
        }
		return $result;
    }

    /**
     * Создание связанной записи.
     *
     * @param Entity\ReferenceField $reference
     * @param array $referenceData
     *
     * @return \Bitrix\Main\Entity\AddResult
     * @throws ArgumentException
     * @throws \Exception
     */
	protected function createReferenceData(Entity\ReferenceField $reference, array $referenceData)
	{
		$referenceName = $reference->getName();
		$fieldParams = $this->getFieldParams($referenceName);
		$fieldWidget = $this->getFieldWidget($referenceName);

		if (!empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
			throw new ArgumentException(Loc::getMessage('DIGITALWAND_AH_ARGUMENT_CANT_CONTAIN_ID', array('%A%' => 'referenceData')), 'referenceData');
		}

		$refClass = $reference->getRefEntity()->getDataClass();

        if(isset($referenceData['VALUE']) AND $fieldWidget->getMultipleField('VALUE') != 'VALUE'){
            $referenceData[$fieldWidget->getMultipleField('VALUE')] = $referenceData['VALUE'];
            unset($referenceData['VALUE']);
        }
		$createResult = $refClass::add($referenceData);

		if (!$createResult->isSuccess()) {
			$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR',
				array('#FIELD#' => $fieldParams['TITLE'])), 'CREATE_' . $referenceName);
		}

		return $createResult;
	}

    /**
     * Обновление связанной записи
     *
     * @param Entity\ReferenceField $reference
     * @param array $referenceData
     * @param array $referenceStaleDataSet
     *
     * @return Entity\UpdateResult|null
     * @throws ArgumentException
     * @throws \Exception
     */
	protected function updateReferenceData(
		Entity\ReferenceField $reference,
		array $referenceData,
		array $referenceStaleDataSet
	)
	{
		$referenceName = $reference->getName();
		$fieldParams = $this->getFieldParams($referenceName);
		$fieldWidget = $this->getFieldWidget($referenceName);

		if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
			throw new ArgumentException(Loc::getMessage('DIGITALWAND_AH_ARGUMENT_SHOULD_CONTAIN_ID', array('%A%' => 'referenceData')), 'referenceData');
		}

		// Сравнение старых данных и новых, обновляется только при различиях
		if ($this->isDifferentData($referenceStaleDataSet[$referenceData[$fieldWidget->getMultipleField('ID')]],
			$referenceData)
		) {
			$refClass = $reference->getRefEntity()->getDataClass();

			$primary = $referenceData[$fieldWidget->getMultipleField('ID')];
			if(isset($referenceData['VALUE']) AND $fieldWidget->getMultipleField('VALUE') != 'VALUE'){
                $referenceData[$fieldWidget->getMultipleField('VALUE')] = $referenceData['VALUE'];
                unset($referenceData['VALUE']);
            }
			$updateResult = $refClass::update($primary, $referenceData);

			if (!$updateResult->isSuccess()) {
				$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR',
					array('#FIELD#' => $fieldParams['TITLE'])), 'UPDATE_' . $referenceName);
			}

            return $updateResult;
        } else {
            return new Entity\Result(); // пустой Result у которого isSuccess() вернет true
        }
    }

    /**
     * Удаление данных связи.
     *
     * @param Entity\ReferenceField $reference
     * @param $referenceId
     *
     * @return \Bitrix\Main\Entity\Result
     * @throws \Exception
     */
	protected function deleteReferenceData(Entity\ReferenceField $reference, $referenceId)
	{
		$refClass = $reference->getRefEntity()->getDataClass();
		$deleteResult = $refClass::delete($referenceId);

		if (!$deleteResult->isSuccess()) {
            $fieldParams = $this->getFieldParams($reference->getName());
			$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_DELETE_ERROR',
				array('#FIELD#' => $fieldParams['TITLE'])), 'DELETE_' . $reference->getName());
		}

		return $deleteResult;
	}

    /**
     * Удаление данных связи.
     *
     * @param Entity\ReferenceField $reference
     * @param $referenceId
     *
     * @return \Bitrix\Main\Entity\Result
     * @throws \Exception
     */
    protected function deleteReference(Entity\ReferenceField $reference, $referenceId)
    {
        $refClass = $reference->getRefEntity()->getDataClass();
        $referenceName = $reference->getName();
        $fieldWidget = $this->getFieldWidget($referenceName);
        $updateResult = $refClass::update($referenceId, [$fieldWidget->getMultipleField('ENTITY_ID') => null]);

        if (!$updateResult->isSuccess()) {
            $fieldParams = $this->getFieldParams($reference->getName());
            $this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_DELETE_ERROR',
                array('#FIELD#' => $fieldParams['TITLE'])), 'DELETE_' . $reference->getName());
        }

        return $updateResult;
    }

    /**
     * Получение данных связи.
     *
     * @param Entity\ReferenceField $reference
     *
     * @return array
     * @throws ArgumentException
     */
	protected function getReferenceDataSet(Entity\ReferenceField $reference)
	{
		/**
		 * @var DataManager $modelClass
		 */
		$modelClass = $this->modelClass;
		$dataSet = array();
		$fieldWidget = $this->getFieldWidget($reference->getName());
		
		// Возможно запрос для данного поля уже делалася, получение значения из массива
		if ($this->referenceDataSave[ $reference->getName() ])
			return $this->referenceDataSave[ $reference->getName() ];

		$rsData = $modelClass::getList(array(
			'select' => array('REF_' => $reference->getName() . '.*'),
			'filter' => array('=' . $this->modelPk => $this->itemId)
		));

		while ($data = $rsData->fetch()) {
			if (empty($data['REF_' . $fieldWidget->getMultipleField('ID')])) {
				continue;
			}

			$row = array();
			foreach ($data as $key => $value) {
				$row[str_replace('REF_', '', $key)] = $value;
			}

            if (!isset($row['VALUE'])) {
                $row['VALUE'] = $row[$fieldWidget->getMultipleField('VALUE')];
            }

			$dataSet[$data['REF_' . $fieldWidget->getMultipleField('ID')]] = $row;
		}

		// Сохранить результат, для избежания повторного запроса к базе
		$this->referenceDataSave[ $reference->getName() ] = $dataSet;

		// Вернуть результат запроса
		return $dataSet;
	}

	/**
	 * В данные связи подставляются данные основной модели используя условия связи моделей из getMap().
	 *
	 * @param Entity\ReferenceField $reference
	 * @param array $referenceData Данные привязанной модели
	 *
	 * @return array
	 */
	protected function linkData(Entity\ReferenceField $reference, array $referenceData)
	{
		// Парсим условия связи двух моделей
		$referenceConditions = $this->getReferenceConditions($reference);

		// Получение ID связанного элемента
		if ($ID = $this->getLinkDataId($referenceData, $this->getReferenceDataSet($reference)))
			$referenceData['ID'] = $ID;

		foreach ($referenceConditions as $refField => $refValue) {
			// Так как в условиях связи между моделями в основном отношения типа this.field => ref.field или
			// ref.field => SqlExpression, мы можем использовать это для подстановки данных
			// this.field - это поле основной модели
			// ref.field - поле модели из связи
			// customValue - это строка полученная из new SqlExpression('%s', ...)
			if (empty($refValue['thisField'])) {
				$referenceData[$refField] = $refValue['customValue'];
			}
			else {
				$referenceData[$refField] = $this->data[$refValue['thisField']];
			}
		}

		return $referenceData;
	}

	/**
	 * Связывает набор связанных данных с основной моделю.
	 *
	 * @param Entity\ReferenceField $reference
	 * @param array $referenceDataSet
	 *
	 * @return array
	 */
	protected function linkDataSet(Entity\ReferenceField $reference, array $referenceDataSet)
	{
		foreach ($referenceDataSet as $key => $referenceData) {
			$referenceDataSet[$key] = $this->linkData($reference, $referenceData);
		}

		return $referenceDataSet;
	}
	
	/**
	 * Возвращает ID связанного элемента. Для поиска используются ранее полученные данные,
	 * Что позволяет избежать дополнительных запросов для поиска ID элементов.
	 * В звязанной таблице имя поля идентификатора всегда 'ID', а имя поля значения всегда 'VALUE'
	 *
	 * @param array $referenceDataSet устанавливаемое значение
	 * @param array $referenceStaleDataSet установленные ранее значения
	 */
	protected function getLinkDataId(array $referenceDataSet, array $referenceStaleDataSet)
	{
		foreach ($referenceStaleDataSet as $item) {
			if ($item['VALUE'] == $referenceDataSet['VALUE']) {
				return $item['ID'];
			}
		}

		return null;
	}

	/**
	 * Парсинг условий связи между моделями.
	 *
	 * Ничего сложного нет, просто определяются соответствия полей основной модели и модели из связи. Например:
	 *
	 * `FilmLinksTable::FILM_ID => FilmTable::ID (ref.FILM_ID => this.ID)`
	 *
	 * Или, например:
	 *
	 * `MediaTable::TYPE => 'FILM' (ref.TYPE => new DB\SqlExpression('?s', 'FILM'))`
	 *
	 * @param Entity\ReferenceField $reference Данные поля из getMap().
	 *
	 * @return array Условия связи преобразованные в массив вида $conditions[$refField]['thisField' => $thisField,
	 *     'customValue' => $customValue].
	 *      $customValue - это результат парсинга SqlExpression.
	 *      Если шаблон SqlExpression не равен %s, то условие исключается из результата.
	 */
	protected function getReferenceConditions(Entity\ReferenceField $reference)
	{
		$conditionsFields = array();

		foreach ($reference->getReference() as $leftCondition => $rightCondition) {
			$thisField = null;
			$refField = null;
			$customValue = null;

			// Поиск this.... в левом условии
			$thisFieldMatch = array();
			$refFieldMatch = array();
			if (preg_match('/=this\.([A-z]+)/', $leftCondition, $thisFieldMatch) == 1) {
				$thisField = $thisFieldMatch[1];
			} // Поиск ref.... в левом условии
			else {
				if (preg_match('/ref\.([A-z]+)/', $leftCondition, $refFieldMatch) == 1) {
					$refField = $refFieldMatch[1];
				}
			}

			// Поиск expression value... в правом условии
			$refFieldMatch = array();
			if ($rightCondition instanceof \Bitrix\Main\DB\SqlExpression) {
				$customValueDirty = $rightCondition->compile();
				$customValue = preg_replace('/^([\'"])(.+)\1$/', '$2', $customValueDirty);
				if ($customValueDirty == $customValue) {
					// Если значение выражения не обрамлено кавычками, значит оно не нужно нам
					$customValue = null;
				}
			} // Поиск ref.... в правом условии
			else {
				if (preg_match('/ref\.([A-z]+)/', $rightCondition, $refFieldMatch) > 0) {
					$refField = $refFieldMatch[1];
				}
			}

			// Если не указано поле, которое нужно заполнить или не найдено содержимое для него, то исключаем условие
			if (empty($refField) || (empty($thisField) && empty($customValue))) {
				continue;
			}
			else {
				$conditionsFields[$refField] = array(
					'thisField' => $thisField,
					'customValue' => $customValue,
				);
			}
		}

		return $conditionsFields;
	}

	/**
	 * Обнаружение отличий массивов
	 * Метод не сранивает наличие аргументов, сравниваются только значения общих параметров
	 *
	 * @param array $data1
	 * @param array $data2
	 *
	 * @return bool
	 */
	protected function isDifferentData(array $data1 = null, array $data2 = null)
	{
	    if(is_null($data1)) return true;
		foreach ($data1 as $key => $value) {
			if (isset($data2[$key]) && $data2[$key] != $value) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param $fieldName
	 *
	 * @return array|bool
	 */
	protected function getFieldParams($fieldName)
	{
		$fields = $this->helper->getFields();

		if (isset($fields[$fieldName]) && isset($fields[$fieldName]['WIDGET'])) {
			return $fields[$fieldName];
		}
		else {
			return false;
		}
	}

	/**
	 * Получение виджета привязанного к полю.
	 *
	 * @param $fieldName
	 *
	 * @return HelperWidget|bool
	 */
	protected function getFieldWidget($fieldName)
	{
		$field = $this->getFieldParams($fieldName);

		return isset($field['WIDGET']) ? $field['WIDGET'] : null;
	}
}

================================================
FILE: lib/EventHandlers.php
================================================
<?php

namespace DigitalWand\AdminHelper;

use Bitrix\Main\Context;
use Bitrix\Main\Loader;

/**
 * Перехватчики событий.
 *
 * Для каждого события, возникающего в системе, которе необходимо отлавливать «Админ-хелпером», создаётся
 * в данном классе одноимённый метод. Метод должен быть зарегистрирован в системе через установщик модуля.
 *
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 */
class EventHandlers
{
    /**
     * Автоматическое подключение модуля в админке.
     *
     * Таки образом, исключаем необходимость прописывать в генераторах админки своих модулей
     * подключение «Админ-хелпера».
     *
     * @throws \Bitrix\Main\LoaderException
     */
    public static function onPageStart()
    {
        if (Context::getCurrent()->getRequest()->isAdminSection())
        {
            Loader::includeModule('digitalwand.admin_helper');
        }
    }
}

================================================
FILE: lib/Sorting.php
================================================
<?php

namespace DigitalWand\AdminHelper;

use DigitalWand\AdminHelper\Helper\AdminBaseHelper;

/**
 * Оригинальный класс сохраняет в сессии выбранную сортировку по страницам админки.
 *
 * Поскольку у admin_helper всего одна страница в админке на все модели и представления,
 * нам необходимо сделать ключ сессии разным для разных классов-хелперов.
 *
 * Так мы сможем иметь свой порядок сортировки у каждого из них.
 *
 * @link:https://github.com/DigitalWand/digitalwand.admin_helper/issues/88
 */
class Sorting extends \CAdminSorting
{
	/** @noinspection PhpMissingParentConstructorInspection */
	/**
	 * Изменения относительно базового конструктора отмечены комментариями
	 *
	 * @param string $table_id ID в \CAdminList
	 * @param string|bool $by_initial Поле сортировки по умолчанию
	 * @param string|bool $order_initial Порядок сортирвоки по умолчанию
	 * @param string $by_name Параметр, содержащий поле сортировки
	 * @param string $ord_name Параметр, содержащий порядок сортировки
	 * @param AdminBaseHelper $adminHelper
	 */
	public function __construct(
		$table_id,
		$by_initial = false,
		$order_initial = false,
		$by_name = "by",
		$ord_name = "order",
		AdminBaseHelper $adminHelper = null // добавлено
	) {
		/** @global \CMain $APPLICATION */
		global $APPLICATION;

		$this->by_name = $by_name;
		$this->ord_name = $ord_name;
		$this->table_id = $table_id;
		$this->by_initial = $by_initial;
		$this->order_initial = $order_initial;

		$uniq = md5(($adminHelper ? get_class($adminHelper) : '') . '_' . $APPLICATION->GetCurPage()); // изменено

		$aOptSort = array();
		if(isset($GLOBALS[$this->by_name]))
		{
			$_SESSION["SESS_SORT_BY"][$uniq] = $GLOBALS[$this->by_name];
			$_SESSION["SESS_SORT_BY"][$uniq] = $GLOBALS[$this->by_name];
		}
		elseif(isset($_SESSION["SESS_SORT_BY"][$uniq]))
		{
			$GLOBALS[$this->by_name] = $_SESSION["SESS_SORT_BY"][$uniq];
		}
		else
		{
			$aOptSort = \CUserOptions::GetOption("list", $this->table_id, array("by"=>$by_initial, "order"=>$order_initial));
			if(!empty($aOptSort["by"]))
				$GLOBALS[$this->by_name] = $aOptSort["by"];
			elseif($by_initial !== false)
				$GLOBALS[$this->by_name] = $by_initial;
		}

		if(isset($GLOBALS[$this->ord_name]))
		{
			$_SESSION["SESS_SORT_ORDER"][$uniq] = $GLOBALS[$this->ord_name];
		}
		elseif(isset($_SESSION["SESS_SORT_ORDER"][$uniq]))
		{
			$GLOBALS[$this->ord_name] = $_SESSION["SESS_SORT_ORDER"][$uniq];
		}
		else
		{
			if(empty($aOptSort["order"]))
				$aOptSort = \CUserOptions::GetOption("list", $this->table_id, array("order"=>$order_initial));
			if(!empty($aOptSort["order"]))
				$GLOBALS[$this->ord_name] = $aOptSort["order"];
			elseif($order_initial !== false)
				$GLOBALS[$this->ord_name] = $order_initial;
		}

		$this->field = $GLOBALS[$this->by_name];
		$this->order = $GLOBALS[$this->ord_name];
	}
}

================================================
FILE: lib/helper/AdminBaseHelper.php
================================================
<?php

namespace DigitalWand\AdminHelper\Helper;

use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\Loader;
use Bitrix\Main\LoaderException;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\ModuleManager;
use DigitalWand\AdminHelper\EntityManager;
use DigitalWand\AdminHelper\Widget\HelperWidget;
use Bitrix\Main\Entity\DataManager;
use Bitrix\Highloadblock as HL;
use Bitrix\Main\Context;

Loader::includeModule('highloadblock');
Loc::loadMessages(__FILE__);

/**
 * Данный модуль реализует подход MVC для создания административного интерфейса.
 *
 * Возможность построения административного интерфейса появляется благодаря наличию единого API для CRUD-операциями над
 * сущностями. Поэтому построение админ. интерфейса средствами данного модуля возможно только для классов, реализующих
 * API ORM Битрикс. При желании использовать данный модуль для сущностей, не использующих ORM Битрикс, можно
 * подготовить для таких сущностей класс-обёртку, реализующий необходимые функции.
 *
 * Основные понятия модуля:
 * <ul>
 * <li>Мдель: "model" в терминах MVC. Класс, унаследованный от DataManager или реализующий аналогичный API.</li>
 * <li>Хэлпер: "view" в терминах MVC. Класс, реализующий отрисовку интерфейса списка или детальной страницы.</li>
 * <li>Роутер: "controller" в терминах MVC. Файл, принимающий все запросы к админке данного модуля, создающий нужные
 * хэлперы с нужными настройками. С ним напрямую работать не придётся.</li>
 * <li>Виджеты: "delegate" в терминах MVC. Классы, отвечающие за отрисовку элементов управления для отдельных полей
 * сущностей. В списке и на детальной.</li>
 * </ul>
 *
 * Схема работы с модулем следующая:
 * <ul>
 * <li>Реализация класса AdminListHelper - для управления страницей списка элементов</li>
 * <li>Реализация класса AdminEditHelper - для управления страницей просмотра/редактирования элемента</li>
 * <li>Реализация класса AdminInterface - для описания конфигурации полей админки и классы интерфейсов</li>
 * <li>Реализация класса AdminSectionListHelper - для описания странице списка разделов(если они используются)</li>
 * <li>Реализация класса AdminSectionEditHelper - для управления страницей просмотра/редактирования раздела(если они используются)</li>
 * <li>Если не хватает возможностей виджетов, идущих с модулем, можно реализовать свой виджет, унаследованный от любого
 * другого готового виджета или от абстрактного класса HelperWidget</li>
 * </ul>
 *
 * Устаревший функционал:
 * <ul>
 * <li>Файл Interface.php с вызовом AdminBaseHelper::setInterfaceSettings(), в который передается
 * конфигурация полей админки и классы.</li>
 *
 * Рекомендуемая файловая структура для модулей, использующих данный функционал:
 * <ul>
 * <li>Каталог <b>admin</b>. Достаточно поместить в него файл menu.php, отдельные файлы для списка и детальной
 * создавать не надо благодаря единому роутингу.</li>
 * <li>Каталог <b>classes</b> (или lib): содержит классы модли, представлений и делегатов.</li>
 * <li> -- <b>classes/admininterface</b>: каталог, содержащий классы "view", унаследованные от AdminListHelper,
 * AdminEditHelper, AdminInterface, AdminSectionListHelper и AdminSectionEditHelper.</li>
 * <li> -- <b>classes/widget</b>: каталог, содержащий виджеты ("delegate"), если для модуля пришлось создавать
 * свои.</li>
 * <li> -- <b>classes/model</b>: каталог с моделями, если пришлось переопределять поведение стандартынх функций getList
 * и т.д.</li>
 * </ul>
 *
 * Использовать данную структуру не обязательно, это лишь рекомендация, основанная на успешном опыте применения модуля
 * в ряде проектов.
 *
 * Единственное <b>обязательное</b> условие - расположение  всех реализуемых классов админ хелперов и админ интерфейсов
 * в одном неймспейсе
 *
 * При использовании разделов нужно обязательно прописать в модели элементов привязку к модели разделов, например:
 *
 * ```php
 * <?php
 * class ElementModel
 * {
 * 		public static function getMap()
 *  	{
 * 			return [
 * 				'CATEGORY' => [
 *					'data_type' => 'Vendor\Module\CategoryTable',
 *					'reference' => ['=this.CATEGORY_ID' => 'ref.ID'],
 *				]
 * 			];
 * 		}
 * ```
 *
 * @see AdminInterface::fields()
 * @package AdminHelper
 *
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 * @author Artem Yarygin <artx19@yandex.ru>
 */
abstract class AdminBaseHelper
{
	/**
	 * @internal
	 * @var string адрес обработчика запросов к админ. интерфейсу.
	 */
	static protected $routerUrl = '/bitrix/admin/admin_helper_route.php';

	/**
	 * @var string
	 * Имя класса используемой модели. Используется для выполнения CRUD-операций.
	 * При наследовании класса необходимо переопределить эту переменную, указав полное имя класса модели.
	 *
	 * @see DataManager
	 * @api
	 */
	static protected $model;

	/**
	 * @var string
	 * Имя класса используемого менеджера сущностей. Используется для выполнения CRUD-операций.
	 *
	 * @see DataManager
	 * @api
	 */
	static protected $entityManager = '\DigitalWand\AdminHelper\EntityManager';

	/**
	 * @var string
	 * Назвние модуля данной модели.
	 * При наследовании класса необходимо указать нзвание модуля, в котором он находится.
	 * А можно и не указывать, в этому случае он определится автоматически по namespace класса
	 * Используется для избежания конфликтов между именами представлений.
	 *
	 * @api
	 */
	static public $module = array();

	/**
	 * @var string[]
	 * Название представления.
	 * При наследовании класса необходимо указать название представления.
	 * А можно и не указывать, в этому случае оно определится автоматически по namespace класса.
	 * Оно будет использовано при построении URL к данному разделу админки.
	 * Не должно содержать пробелов и других символов, требующих преобразований для
	 * адресной строки браузера.
	 *
	 * @api
	 */
	static protected $viewName = array();

	/**
	 * @var array
	 * Настройки интерфейса
	 * @see AdminBaseHelper::setInterfaceSettings()
	 * @internal
	 */
	static protected $interfaceSettings = array();

	/**
	 * @var array
	 * Привязка класса интерфеса к классу хелпера
	 */
	static protected $interfaceClass = array();

	/**
	 * @var array
	 * Хранит список отображаемых полей и настройки их отображения
	 * @see AdminBaseHelper::setInterfaceSettings()
	 */
	protected $fields = array();

	/**
	 * @var \CMain
	 * Замена global $APPLICATION;
	 */
	protected $app;
	protected $validationErrors = array();

	/**
	 * @var string
	 * Позволяет непосредственно указать адрес страницы списка. Полезно, в случае, если такая станица реализована без
	 * использования данного модуля. В случае, если поле определено для класса, роутинг не используется.
	 *
	 * @see AdminBaseHelper::getListPageUrl
	 * @api
	 */
	static protected $listPageUrl;

	/**
	 * @var string
	 * $viewName представления, отвечающего за страницу списка. Необходимо указывать только для классов, уналедованных
	 * от AdminEditHelper.
	 * Необязательное, сгенерируется автоматически если не определено
	 *
	 * @see AdminBaseHelper::getViewName()
	 * @see AdminBaseHelper::getListPageUrl
	 * @see AdminEditHelper
	 * @api
	 */
	static protected $listViewName;

	/**
	 * @var string
	 * Позволяет непосредственно указать адрес страницы просмотра/редактирования элемента. Полезно, в случае, если
	 * такая станица реализована без использования данного модуля. В случае, если поле определено для класса,
	 * роутинг не используется.
	 *
	 * @see AdminBaseHelper::getEditPageUrl
	 * @api
	 */
	static protected $editPageUrl;

	/**
	 * @var string
	 * $viewName представления, отвечающего за страницу редактирования/просмотра элемента. Необходимо указывать только
	 *     для классов, уналедованных от AdminListHelper.
	 *
	 * @see AdminBaseHelper::getViewName()
	 * @see AdminBaseHelper::getEditPageUrl
	 * @see AdminListHelper
	 * @api
	 */
	static protected $editViewName;

	/**
	 * @var string
	 * Позволяет непосредственно указать адрес страницы просмотра/редактирования раздела. Полезно, в случае, если
	 * такая станица реализована без использования данного модуля. В случае, если поле определено для класса,
	 * роутинг не используется.
	 *
	 * @see AdminBaseHelper::getEditPageUrl
	 * @api
	 */
	static protected $sectionsEditPageUrl;

	/**
	 * @var string
	 * $viewName представления, отвечающего за страницу редактирования/просмотра раздела. Необходимо указывать только
	 * для классов, уналедованных от AdminListHelper.
	 * Необязательное, сгенерируется автоматически если не определено
	 *
	 * @see AdminBaseHelper::getViewName()
	 * @see AdminBaseHelper::getEditPageUrl
	 * @see AdminListHelper
	 * @api
	 */
	static protected $sectionsEditViewName;

	/**
	 * @var array
	 * Дополнительные параметры URL, которые будут добавлены к параметрам по-умолчанию, генерируемым автоматически
	 * @api
	 */
	protected $additionalUrlParams = array();

	/**
	 * @var string контекст выполнения. Полезен для информирования виджетов о том, какая операция в настоящий момент
	 *     производится.
	 */
	protected $context = '';

	/**
	 * Флаг использования разделов, необходимо переопределять в дочернем классе
	 * @var bool
	 */
	static protected $useSections = false;

	/**
	 * Правило именования хелперов для разделов по умолчанию
	 * @var string
	 */
	static protected $sectionSuffix = 'Sections';

	/**
	 * @param array $fields список используемых полей и виджетов для них
	 * @param array $tabs список вкладок для детальной страницы
	 * @param string $module название модуля
	 */
	public function __construct(array $fields, array $tabs = array(), $module = "")
	{
		global $APPLICATION;

		$this->app = $APPLICATION;

		$settings = array(
			'FIELDS' => $fields,
			'TABS' => $tabs
		);
		if (static::setInterfaceSettings($settings)) {
			$this->fields = $fields;
		}
		else {
			$settings = static::getInterfaceSettings();
			$this->fields = $settings['FIELDS'];
		}
	}

	/**
	 * @param string $viewName Имя вьюхи, для которой мы хотим получить натсройки
	 *
	 * @return array Возвращает настройки интерфейса для данного класса.
	 *
	 * @see AdminBaseHelper::setInterfaceSettings()
	 * @api
	 */
	public static function getInterfaceSettings($viewName = '')
	{
		if (empty($viewName)) {
			$viewName = static::getViewName();
		}

		return self::$interfaceSettings[static::getModule()][$viewName]['interface'];
	}

	/**
	 * Основная функция для конфигурации всего административного интерфейса.
	 *
	 * @param array $settings настройки полей и вкладок
	 * @param array $helpers список классов-хэлперов, используемых для отрисовки админки
	 * @param string $module название модуля
	 *
	 * @return bool false, если для данного класса уже были утановлены настройки
	 *
	 * @api
	 */
	public static function setInterfaceSettings(array $settings, array $helpers = array(), $module = '')
	{
		foreach ($helpers as $helperClass => $helperSettings) {
			if (!is_array($helperSettings)) { // поддержка старого формата описания хелперов
				$helperClass = $helperSettings; // в значении передается класс хелпера а не настройки
				$helperSettings = array(); // настроек в старом формате нет
			}
			$success = $helperClass::registerInterfaceSettings($module, array_merge($settings, $helperSettings));
			if (!$success) return false;
		}

		return true;
	}

	/**
	 * Привязывает класса хелпера из которого вызывается к интерфесу, используется при получении
	 * данных об элементах управления из интерфейса.
	 *
     * @param $class
	 */
	public static function setInterfaceClass($class)
	{
		static::$interfaceClass[get_called_class()] = $class;
	}

	/**
	 * Возвращает класс интерфейса к которому привязан хелпер из которого вызван метод.
     *
	 * @return array
	 */
	public static function getInterfaceClass()
	{
		return isset(static::$interfaceClass[get_called_class()]) ? static::$interfaceClass[get_called_class()] : false;
	}

	/**
	 * Регистрирует настройки интерфейса для текущего хелпера
	 *
	 * @param string $module имя текущего модуля
	 * @param $interfaceSettings
     *
	 * @return bool
	 * @internal
	 */
	public static function registerInterfaceSettings($module, $interfaceSettings)
	{
		if (isset(self::$interfaceSettings[$module][static::getViewName()]) || empty($module)
			|| empty($interfaceSettings)
		) {
			return false;
		}

		self::$interfaceSettings[$module][static::getViewName()] = array(
			'helper' => get_called_class(),
			'interface' => $interfaceSettings
		);

		return true;
	}

	/**
	 * Получает настройки интерфейса для данного модуля и представления. Используется при роутинге.
	 * Возвращается массив со следующими ключами:
	 *
	 * <ul>
	 * <li> helper - название класса-хэлпера, который будет рисовать страницу</li>
	 * <li> interface - настройки интерфейса для хелпера</li>
	 * </ul>
	 *
	 * @param string $module Модуль, для которого нужно получить настройки.
	 * @param string $view Название представления.
     *
	 * @return array
	 * @internal
	 */
	public static function getGlobalInterfaceSettings($module, $view)
	{
		if (!isset(self::$interfaceSettings[$module][$view])) {
			return false;
		}

		return array(
			self::$interfaceSettings[$module][$view]['helper'],
			self::$interfaceSettings[$module][$view]['interface'],
		);
	}

	/**
     * Возвращает имя текущего представления.
     *
	 * @return string
	 * @api
	 */
	public static function getViewName()
	{
		if (!is_array(static::$viewName)) {
			return static::$viewName;
		}

		$className = get_called_class();

		if (!isset(static::$viewName[$className])) {
			$classNameParts = explode('\\', trim($className, '\\'));

			if (count($classNameParts) > 2) {
				$classCaption = array_pop($classNameParts); // название класса без namespace
				preg_match_all('/((?:^|[A-Z])[a-z]+)/', $classCaption, $matches);
				$classCaptionParts = $matches[0];

				if (end($classCaptionParts) == 'Helper') {
					array_pop($classCaptionParts);
				}

				static::$viewName[$className] = strtolower(implode('_', $classCaptionParts));
			}
		}

		return static::$viewName[$className];
	}

	/**
	 * Возвращает поле модели которое используется для привязки к разделу из поля с типом совпадающим с классом модели
	 * раздела.
	 * @return string
	 * @throws Exception
	 */
	public static function getSectionField()
	{
		$sectionListHelper = static::getHelperClass(AdminSectionListHelper::className());

		if (empty($sectionListHelper))
		{
			return null;
		}

		$sectionModelClass = $sectionListHelper::getModel();
		$modelClass = static::getModel();

		foreach ($modelClass::getMap() as $field => $data) {
			if ($data instanceof ReferenceField && $data->getDataType() . 'Table' === $sectionModelClass) {
				return str_replace('=this.', '', reset($data->getReference()));
			}
			if (is_array($data) && $data['data_type'] === $sectionModelClass) {
				return str_replace('=this.', '', key($data['reference']));
			}
		}

		throw new Exception('References to section model not found');
	}

	/**
     * Возвращает имя класса используемой модели.
     *
	 * @return \Bitrix\Main\Entity\DataManager|string
	 *
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\SystemException
	 * @throws \Exception
	 * @api
	 */
	public static function getModel()
	{
		if (static::$model) {
			return static::getHLEntity(static::$model);
		}

		return null;
	}

	/**
	 * Возвращает имя модуля. Если оно не задано, то определяет автоматически из namespace класса.
     *
	 * @return string
     *
	 * @throws LoaderException
	 * @api
	 */
	public static function getModule()
	{
		if (!is_array(static::$module)) {
			return static::$module;
		}

		$className = get_called_class();

		if (!isset(static::$module[$className])) {
			$classNameParts = explode('\\', trim($className, '\\'));

			$moduleNameParts = array();
			$moduleName = false;

			while (count($classNameParts)) {
				$moduleNameParts[] = strtolower(array_shift($classNameParts));
				$moduleName = implode('.', $moduleNameParts);

				if (ModuleManager::isModuleInstalled($moduleName)) {
					static::$module[$className] = $moduleName;
					break;
				}
			}

			if (empty($moduleName)) {
				throw new LoaderException('Module name not found');
			}
		}

		return static::$module[$className];
	}

	/**
	 * Возвращает модифцированный массив с описанием элемента управления по его коду. Берет название и настройки
     * из админ-интерфейса, если они не заданы — используются значения по умолчанию.
     *
     * Если элемент управления описан в админ-интерфейсе, то дефолтные настройки и описанные в классе интерфейса
     * будут совмещены (смержены).
     *
	 * @param $code
	 * @param $params
	 * @param array $keys
     *
	 * @return array|bool
	 */
	protected function getButton($code, $params, $keys = array('name', 'TEXT'))
	{
		$interfaceClass = static::getInterfaceClass();
		$interfaceSettings = static::getInterfaceSettings();

		if ($interfaceClass && !empty($interfaceSettings['BUTTONS'])) {
			$buttons = $interfaceSettings['BUTTONS'];

			if (is_array($buttons) && isset($buttons[$code])) {
				if ($buttons[$code]['VISIBLE'] == 'N') {
					return false;
				}
				$params = array_merge($params, $buttons[$code]);

				return $params;
			}
		}

		$text = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_' . $code);

		foreach ($keys as $key) {
			$params[$key] = $text;
		}

		return $params;
	}

	/**
	 * Возвращает список полей интерфейса.
     *
	 * @see AdminBaseHelper::setInterfaceSettings()
     *
	 * @return array
     *
	 * @api
	 */
	public function getFields()
	{
		return $this->fields;
	}

	/**
	 * Окончательно выводит административную страницу.
	 */
	abstract public function show();

	/**
	 * Получает название таблицы используемой модели.
     *
	 * @return mixed
	 */
	public function table()
	{
		/**
         * @var DataManager $className
         */
		$className = static::getModel();

		return $className::getTableName();
	}

	/**
	 * Возвращает первичный ключ таблицы используемой модели
	 * Для HL-инфоблоков битрикс - всегда ID. Но может поменяться для какой-либо другой сущности.
	 * @return string
	 * @api
	 */
	public static function pk()
	{
		return 'ID';
	}

	/**
	 * Возвращает значение первичного ключа таблицы используемой модели
	 * @return array|int|null
	 * 
	 * @api
	 */
	public function getPk()
	{
		return isset($_REQUEST['FIELDS'][$this->pk()]) ? $_REQUEST['FIELDS'][$this->pk()] : $_REQUEST[$this->pk()];
	}

	/**
	 * Возвращает первичный ключ таблицы используемой модели разделов. Для HL-инфоблоков битрикс - всегда ID.
     * Но может поменяться для какой-либо другой сущности.
     *
	 * @return string
	 *
     * @api
	 */
	public function sectionPk()
	{
		return 'ID';
	}

	/**
	 * Устанавливает заголовок раздела в админке.
     *
	 * @param string $title
	 *
     * @api
	 */
	public function setTitle($title)
	{
		$this->app->SetTitle($title);
	}

	/**
	 * Функция для обработки дополнительных операций над элементами в админке. Как правило, должно оканчиваться
     * LocalRedirect после внесения изменений.
	 *
	 * @param string $action Название действия.
	 * @param null|int $id ID элемента.
     *
	 * @api
	 */
	protected function customActions($action, $id = null)
	{
		return;
	}

	/**
	 * Выполняется проверка прав на доступ к сущности.
     *
	 * @return bool
	 *
     * @api
	 */
	protected function hasRights()
	{
		return true;
	}

	/**
	 * Выполняется проверка прав на выполнение операций чтения элементов.
     *
	 * @return bool
	 *
     * @api
	 */
	protected function hasReadRights()
	{
		return true;
	}

	/**
	 * Выполняется проверка прав на выполнение операций редактирования элементов.
	 *
     * @return bool
     *
	 * @api
	 */
	protected function hasWriteRights()
	{
		return true;
	}

	/**
	 * Проверка прав на изменение определенного элемента.
     *
	 * @param array $element Массив данных элемента.
     *
	 * @return bool
     *
     * @api
	 */
	protected function hasWriteRightsElement($element = array())
	{
		if (!$this->hasWriteRights()) {
			return false;
		}

		return true;
	}

	/**
	 * Выполняется проверка прав на выполнение опреаций удаления элементов.
     *
	 * @return bool
     *
	 * @api
	 */
	protected function hasDeleteRights()
	{
		return true;
	}

	/**
	 * Выводит сообщения об ошибках.
     *
	 * @internal
	 */
	protected function showMessages()
	{
		$allErrors = $this->getErrors();
		$notes = $this->getNotes();

		if (!empty($allErrors)) {
			$errorList[] = implode("\n", $allErrors);
		}
		if ($e = $this->getLastException()) {
			$errorList[] = trim($e->GetString());
		}

		if (!empty($errorList)) {
			$errorText = implode("\n\n", $errorList);
			\CAdminMessage::ShowOldStyleError($errorText);
		}
		else {
			if (!empty($notes)) {
				$noteText = implode("\n\n", $notes);
				\CAdminMessage::ShowNote($noteText);
			}
		}
	}

	/**
	 * @return bool|\CApplicationException
     *
	 * @internal
	 */
	protected function getLastException()
	{
		if (isset($_SESSION['APPLICATION_EXCEPTION']) AND !empty($_SESSION['APPLICATION_EXCEPTION'])) {
			/** @var CApplicationException $e */
			$e = $_SESSION['APPLICATION_EXCEPTION'];
			unset($_SESSION['APPLICATION_EXCEPTION']);

			return $e;
		}
		else {
			return false;
		}
	}

	/**
	 * @param $e
	 */
	protected function setAppException($e)
	{
		$_SESSION['APPLICATION_EXCEPTION'] = $e;
	}

	/**
	 * Добавляет ошибку или массив ошибок для показа пользователю.
     *
	 * @param array|string $errors
	 *
     * @api
	 */
	public function addErrors($errors)
	{
		if (!is_array($errors)) {
			$errors = array($errors);
		}

		if (isset($_SESSION['ELEMENT_SAVE_ERRORS']) AND !empty($_SESSION['ELEMENT_SAVE_ERRORS'])) {
			$_SESSION['ELEMENT_SAVE_ERRORS'] = array_merge($_SESSION['ELEMENT_SAVE_ERRORS'], $errors);
		}
		else {
			$_SESSION['ELEMENT_SAVE_ERRORS'] = $errors;
		}
	}

	/**
	 * Добавляет уведомление или список уведомлений для показа пользователю.
     *
	 * @param array|string $notes
	 *
     * @api
	 */
	public function addNotes($notes)
	{
		if (!is_array($notes)) {
			$notes = array($notes);
		}

		if (isset($_SESSION['ELEMENT_SAVE_NOTES']) AND !empty($_SESSION['ELEMENT_SAVE_NOTES'])) {
			$_SESSION['ELEMENT_SAVE_NOTES'] = array_merge($_SESSION['ELEMENT_SAVE_NOTES'],
				$notes);
		}
		else {
			$_SESSION['ELEMENT_SAVE_NOTES'] = $notes;
		}
	}

	/**
	 * @return bool|array
     *
	 * @api
	 */
	protected function getErrors()
	{
		if (isset($_SESSION['ELEMENT_SAVE_ERRORS']) AND !empty($_SESSION['ELEMENT_SAVE_ERRORS'])) {
			$errors = $_SESSION['ELEMENT_SAVE_ERRORS'];
			unset($_SESSION['ELEMENT_SAVE_ERRORS']);

			return $errors;
		}
		else {
			return false;
		}
	}

	/**
	 * @return bool
     *
	 * @api
	 */
	protected function getNotes()
	{
		if (isset($_SESSION['ELEMENT_SAVE_NOTES']) AND !empty($_SESSION['ELEMENT_SAVE_NOTES'])) {
			$notes = $_SESSION['ELEMENT_SAVE_NOTES'];
			unset($_SESSION['ELEMENT_SAVE_NOTES']);

			return $notes;
		}
		else {
			return false;
		}
	}

	/**
	 * Возвращает класс хелпера нужного типа из всех зарегистрированных хелперов в модуле и находящихся
	 * в том же неймспейсе что класс хелпера из которого вызван этот метод
	 *
	 * Под типом понимается ближайший родитель из модуля AdminHelper.
	 *
	 * Например если нам нужно получить ListHelper для формирования ссылки на список из EditHelper,
	 * то это будет вглядеть так $listHelperClass = static::getHelperClass(AdminListHelper::getClass())
	 *
	 * @param $class
     *
	 * @return string|bool
	 */
	public static function getHelperClass($class)
	{
		$interfaceSettings = self::$interfaceSettings[static::getModule()];

		foreach ($interfaceSettings as $viewName => $settings) {
			$parentClasses = class_parents($settings['helper']);
			array_pop($parentClasses); // AdminBaseHelper

			$parentClass = array_pop($parentClasses);
			$thirdClass = array_pop($parentClasses);

			if (in_array($thirdClass, array(AdminSectionListHelper::className(), AdminSectionEditHelper::className()))) {
				$parentClass = $thirdClass;
			}

			if ($parentClass == $class && class_exists($settings['helper'])) {
				$helperClassParts = explode('\\', $settings['helper']);
				array_pop($helperClassParts);
				$helperNamespace = implode('\\', $helperClassParts);

				$сlassParts = explode('\\', get_called_class());
				array_pop($сlassParts);
				$classNamespace = implode('\\', $сlassParts);

				if ($helperNamespace == $classNamespace) {
					return $settings['helper'];
				}
			}
		}

		return false;
	}

	/**
	 * Возвращает относительный namespace до хелперов в виде URL параметра.
     *
	 * @return string
	 */
	public static function getEntityCode()
	{
		$namespaceParts = explode('\\', get_called_class());
		array_pop($namespaceParts);
		array_shift($namespaceParts);
		array_shift($namespaceParts);

		if (end($namespaceParts) == 'AdminInterface') {
			array_pop($namespaceParts);
		}

		return str_replace(
			'\\',
			'_',
			implode(
				'\\',
				array_map('lcfirst', $namespaceParts)
			)
		);
	}

	/**
	 * Возвращает URL страницы редактирования класса данного представления.
     *
	 * @param array $params
	 *
     * @return string
	 *
     * @api
	 */
	public static function getEditPageURL($params = array())
	{
		$editHelperClass = str_replace('List', 'Edit', get_called_class());
		if (empty(static::$editViewName) && class_exists($editHelperClass)) {
			return $editHelperClass::getViewURL($editHelperClass::getViewName(), static::$editPageUrl, $params);
		}
		else {
			return static::getViewURL(static::$editViewName, static::$editPageUrl, $params);
		}
	}

	/**
	 * Возвращает URL страницы редактирования класса данного представления.
     *
	 * @param array $params
	 *
     * @return string
	 *
     * @api
	 */
	public static function getSectionsEditPageURL($params = array())
	{
		$sectionEditHelperClass = str_replace('List', 'SectionsEdit', get_called_class());

        if (empty(static::$sectionsEditViewName) && class_exists($sectionEditHelperClass)) {
			return $sectionEditHelperClass::getViewURL($sectionEditHelperClass::getViewName(), static::$sectionsEditPageUrl, $params);
		}
		else {
			return static::getViewURL(static::$sectionsEditViewName, static::$sectionsEditPageUrl, $params);
		}
	}

	/**
	 * Возвращает URL страницы списка класса данного представления.
     *
	 * @param array $params
	 *
     * @return string
	 *
     * @api
	 */
	public static function getListPageURL($params = array())
	{
		$listHelperClass = str_replace('Edit', 'List', get_called_class());

        if (empty(static::$listViewName) && class_exists($listHelperClass)) {
			return $listHelperClass::getViewURL($listHelperClass::getViewName(), static::$listPageUrl, $params);
		}
		else {
			return static::getViewURL(static::$listViewName, static::$listPageUrl, $params);
		}
	}

	/**
	 * Получает URL для указанного представления
	 *
	 * @param string $viewName Название представления.
	 * @param string $defaultURL Позволяет указать URL напрямую. Если указано, то будет использовано это значение.
	 * @param array $params Дополнительные query-параметры в URL.
     *
	 * @return string
	 *
     * @internal
	 */
	public static function getViewURL($viewName, $defaultURL, $params = array())
	{
		$params['entity'] = static::getEntityCode();

		if (isset($defaultURL)) {
			$url = $defaultURL . "?lang=" . LANGUAGE_ID;
		}
		else {
			$url = static::getRouterURL() . '?lang=' . LANGUAGE_ID . '&module=' . static::getModule() . '&view=' . $viewName;
		}

		if (!empty($params)) {
			unset($params['lang']);
			unset($params['module']);
			unset($params['view']);

			$query = http_build_query($params);
			$url .= '&' . $query;
		}

		return $url;
	}

	/**
	 * Возвращает адрес обработчика запросов к админ. интерфейсу.
	 *
     * @return string
	 *
     * @api
	 */
	public static function getRouterURL()
	{
		return static::$routerUrl;
	}

    /**
     * Возвращает URL страницы с хелпером. Как правило, метод вызывается при генерации административного
     * меню (`menu.php`).
     *
     * @param array $params Дополнительные GET-параметры для подстановки в URL.
     *
     * @return string
     */
	public static function getUrl(array $params = array())
	{
		return static::getViewURL(static::getViewName(), null, $params);
	}

	/**
	 * Получает виджет для текущего поля, выполняет базовую инициализацию.
	 *
	 * @param string $code Ключ поля для данного виджета (должен быть в массиве $data).
	 * @param array $data Данные объекта в виде массива.
	 *
     * @return bool|\DigitalWand\AdminHelper\Widget\HelperWidget
     *
	 * @throws \DigitalWand\AdminHelper\Helper\Exception
	 *
     * @internal
	 */
	public function createWidgetForField($code, &$data = array())
	{
		if (!isset($this->fields[$code]['WIDGET'])) {
			$error = str_replace('#CODE#', $code, 'Can\'t create widget for the code "#CODE#"');
			throw new Exception($error, Exception::CODE_NO_WIDGET);
		}

		/** @var HelperWidget $widget */
		$widget = $this->fields[$code]['WIDGET'];

		$widget->setHelper($this);
		$widget->setCode($code);
		$widget->setData($data);
		$widget->setEntityName($this->getModel());

		$this->onCreateWidgetForField($widget, $data);

		if (!$this->hasWriteRightsElement($data)) {
			$widget->setSetting('READONLY', true);
		}

		return $widget;
	}

	/**
	 * Метод вызывается при создании виджета для текущего поля. Может быть использован для изменения настроек виджета
     * на основе передаваемых данных.
	 *
	 * @param \DigitalWand\AdminHelper\Widget\HelperWidget $widget
	 * @param array $data
	 */
	protected function onCreateWidgetForField(&$widget, $data = array())
	{
	}

	/**
	 * Если класс не объявлен, то битрикс генерирует новый класс в рантайме. Если класс уже есть, то возвращаем имя
     * как есть.
	 *
	 * @param $className
	 * @return \Bitrix\Highloadblock\DataManager
	 *
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\SystemException
	 * @throws Exception
	 */
	public static function getHLEntity($className)
	{
		if (!class_exists($className)) {
			$info = static::getHLEntityInfo($className);

			if ($info) {
				$entity = HL\HighloadBlockTable::compileEntity($info);

				return $entity->getDataClass();
			}
			else {
				$error = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_GETMODEL_EXCEPTION', array('#CLASS#' => $className));
				$exception = new Exception($error, Exception::CODE_NO_HL_ENTITY_INFORMATION);

				throw $exception;
			}
		}

		return $className;
	}

	/**
	 * Получает запись из БД с информацией об HL.
	 *
	 * @param string $className Название класса, обязательно без Table в конце и без указания неймспейса.
     *
	 * @return array|false
	 *
     * @throws \Bitrix\Main\ArgumentException
	 */
	public static function getHLEntityInfo($className)
	{
		$className = str_replace('\\', '', $className);
		$pos = strripos($className, 'Table', -5);

        if ($pos !== false) {
			$className = substr($className, 0, $pos);
		}

        $parameters = array(
			'filter' => array(
				'NAME' => $className,
			),
			'limit' => 1
		);

		return HL\HighloadBlockTable::getList($parameters)->fetch();
	}

	/**
	 * Отобразить страницу 404 ошибка
	 */
	protected function show404()
	{
		// инициализация глобальных переменных, необходимых для вывода страницы административного раздела в
		// текущей области видимости
		global $APPLICATION, $adminPage, $adminMenu, $USER;
		\CHTTP::SetStatus(404);
		include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php';
		die();
	}

	/**
	 * Выставляет текущий контекст исполнения.
     *
	 * @param $context
	 *
     * @see $context
	 */
	protected function setContext($context)
	{
		$this->context = $context;
	}

	public function getContext()
	{
		return $this->context;
	}

	public static function className()
	{
		return get_called_class();
	}
}

================================================
FILE: lib/helper/AdminEditHelper.php
================================================
<?php

namespace DigitalWand\AdminHelper\Helper;

use Bitrix\Main\Localization\Loc;
use DigitalWand\AdminHelper\EntityManager;
use DigitalWand\AdminHelper\Widget\HelperWidget;
use Bitrix\Main\Entity\DataManager;

Loc::loadMessages(__FILE__);

/**
 * Базовый класс для реализации детальной страницы админки.
 * При создании своего класса необходимо переопределить следующие переменные:
 * <ul>
 * <li> static protected $model</li>
 * </ul>
 *
 * Этого будет дастаточно для получения минимальной функциональности.
 *
 * @package AdminHelper
 * 
 * @see AdminBaseHelper::$model
 * @see AdminBaseHelper::$module
 * @see AdminBaseHelper::$listViewName
 * @see AdminBaseHelper::$viewName
 *
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 * @author Artem Yarygin <artx19@yandex.ru>
 */
abstract class AdminEditHelper extends AdminBaseHelper
{
	const OP_SHOW_TAB_ELEMENTS = 'AdminEditHelper::showTabElements';
	const OP_EDIT_ACTION_BEFORE = 'AdminEditHelper::editAction_before';
	const OP_EDIT_ACTION_AFTER = 'AdminEditHelper::editAction_after';

	/**
	 * @var array Данные сущности, редактируемой в данный момент. Ключи ассива — названия полей в БД.
	 * @api
	 */
	protected $data;
	/**
	 * @var array Вкладки страницы редактирования.
	 */
	protected $tabs = array();
	/**
	 * @var array Элементы верхнего меню страницы.
	 * @see AdminEditHelper::fillMenu()
	 */
	protected $menu = array();
	/**
	 * @var \CAdminForm
	 */
	protected $tabControl;

	/**
	 * Производится инициализация переменных, обработка запросов на редактирование
	 *
	 * @param array $fields
	 * @param array $tabs
	 */
	public function __construct(array $fields, array $tabs = array())
	{
		$this->tabs = $tabs;

		if (empty($this->tabs)) {
			$this->tabs = array(
				array(
					'DIV' => 'DEFAULT_TAB',
					'TAB' => Loc::getMessage('DEFAULT_TAB'),
					'ICON' => 'main_user_edit',
					'TITLE' => Loc::getMessage('DEFAULT_TAB'),
					'VISIBLE' => true,
				)
			);
		}
		else {
			if (!is_array(reset($this->tabs))) {
				$converted = array();

				foreach ($this->tabs as $tabCode => $tabName) {
					$tabVisible = true;

					if (is_array($tabName)) {
						$tabVisible = isset($tabName['VISIBLE']) ? $tabName['VISIBLE'] : $tabVisible;
						$tabName = $tabName['TITLE'];
					}

					$converted[] = array(
						'DIV' => $tabCode,
						'TAB' => $tabName,
						'ICON' => '',
						'TITLE' => $tabName,
						'VISIBLE' => $tabVisible,
					);
				}
				$this->tabs = $converted;
			}
		}

		parent::__construct($fields, $tabs);

		$this->tabControl = new \CAdminForm(str_replace("\\", "", get_called_class()), $this->tabs);

		if (isset($_REQUEST['apply']) OR isset($_REQUEST['save'])) {
			if (
				isset($_SERVER["HTTP_BX_AJAX"])
				||
				isset($_SERVER["HTTP_X_REQUESTED_WITH"]) && $_SERVER["HTTP_X_REQUESTED_WITH"] === "XMLHttpRequest"
			) {
				\CUtil::JSPostUnescape();
			}
			$this->data = $_REQUEST['FIELDS'];

			if (isset($_REQUEST[$this->pk()])) {
				//Первичный ключ проставляем отдельно, чтобы не вынуждать всегда указывать его в настройках интерфейса.
				$this->data[$this->pk()] = $_REQUEST[$this->pk()];
			}

			foreach ($fields as $code => $settings) {
				if (isset($_REQUEST[$code])) {
					$this->data[$code] = $_REQUEST[$code];
				}
			}

			if ($this->editAction()) {
				if (isset($_REQUEST['apply'])) {
					$id = $this->data[$this->pk()];
					$url = $this->app->GetCurPageParam($this->pk() . '=' . (is_array($id) ? $id[$this->pk()] : $id), array('ID'));
				}
				else {
					if (isset($_REQUEST['save'])) {
						$listHelperClass = static::getHelperClass(AdminListHelper::className());
						$url = $listHelperClass::getUrl(array_merge($this->additionalUrlParams,
							array(
								'restore_query' => 'Y'
							)));
					}
				}
			}
			else {
				if (isset($this->data[$this->pk()])) {
					$id = $this->data[$this->pk()];
					$url = $this->app->GetCurPageParam($this->pk() . '=' . $id);
				}
				else {
					unset($this->data);
					$this->data = $_REQUEST['FIELDS']; //Заполняем, чтобы в случае ошибки сохранения поля не были пустыми
				}
			}

			if (isset($url)) {
				if (defined('BX_PUBLIC_MODE') && BX_PUBLIC_MODE === 1 && ($errors = $this->getErrors())) {
					ob_end_clean();
					$jsMessage = \CUtil::JSEscape(implode("\n", $errors));
					echo '<script>top.BX.WindowManager.Get().ShowError("' . $jsMessage . '");</script>';
					die();
				}
				$this->setAppException($this->app->GetException());
				LocalRedirect($url);
			}
		}
		else {
			$helperFields = $this->getFields();
			$select = array_keys($helperFields);

			foreach ($select as $key => $field) {
				if (isset($helperFields[$field]['VIRTUAL'])
					AND $helperFields[$field]['VIRTUAL'] == true
					AND (!isset($helperFields[$field]['FORCE_SELECT']) OR $helperFields[$field]['FORCE_SELECT'] = false)
				) {
					unset($select[$key]);
				}
			}

			$this->data = $this->loadElement($select);

			$id = isset($_REQUEST[$this->pk()]) ? $_REQUEST[$this->pk()] : null;

			if ($this->data === false && !is_null($id)) {
				$this->show404();
			}

			if (isset($_REQUEST['action']) || isset($_REQUEST['action_button'])) {
				$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : $_REQUEST['action_button'];
				$this->customActions($action, $this->getPk());
			}
		}

		$this->setElementTitle();
	}

	/**
	 * Возвращает верхнее меню страницы.
	 * По-умолчанию две кнопки:
	 * <ul>
	 * <li> Возврат в список</li>
	 * <li> Удаление элемента</li>
	 * </ul>
	 *
	 * Добавляя новые кнопки, нужно указывать параметр URl "action", который будет обрабатываться в
	 * AdminEditHelper::customActions()
	 *
	 * @param bool $showDeleteButton Управляет видимостью кнопки удаления элемента.
     * 
     * @return array
     * 
	 * @see AdminEditHelper::$menu
	 * @see AdminEditHelper::customActions()
	 * 
     * @api
	 */
	protected function getMenu($showDeleteButton = true)
	{
		$listHelper = static::getHelperClass(AdminListHelper::className());
        
		$menu = array(
			$this->getButton('RETURN_TO_LIST', array(
				'LINK' => $listHelper::getUrl(array_merge($this->additionalUrlParams,
					array('restore_query' => 'Y')
				)),
				'ICON' => 'btn_list',
			))
		);

		$arSubMenu = array();

		if (isset($this->data[$this->pk()]) && $this->hasWriteRights()) {
			$arSubMenu[] = $this->getButton('ADD_ELEMENT', array(
				'LINK' => static::getUrl(array_merge($this->additionalUrlParams,
					array(
						'action' => 'add',
						'lang' => LANGUAGE_ID,
						'restore_query' => 'Y',
					))),
				'ICON' => 'edit'
			));
		}

		if ($showDeleteButton && isset($this->data[$this->pk()]) && $this->hasDeleteRights()) {
			$arSubMenu[] = $this->getButton('DELETE_ELEMENT', array(
				'ONCLICK' => "if(confirm('" . Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_CONFIRM') . "')) location.href='" .
					static::getUrl(array_merge($this->additionalUrlParams,
						array(
							'ID' => $this->data[$this->pk()],
							'action' => 'delete',
							'lang' => LANGUAGE_ID,
							'restore_query' => 'Y',
						))) . "'",
				'ICON' => 'delete'
			));
		}

		if (count($arSubMenu)) {
			$menu[] = array('SEPARATOR' => 'Y');
			$menu[] = $this->getButton('ACTIONS', array(
				'MENU' => $arSubMenu,
				'ICON' => 'btn_new'
			));
		}

		return $menu;
	}

    /**
     * {@inheritdoc}
     */
	public function show()
	{
		if (!$this->hasReadRights()) {
			$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_ACCESS_FORBIDDEN'));
			$this->showMessages();

			return false;
		}

		$context = new \CAdminContextMenu($this->getMenu());
		$context->Show();

		$this->tabControl->BeginPrologContent();
		$this->showMessages();
		$this->showProlog();
		$this->tabControl->EndPrologContent();

		$this->tabControl->BeginEpilogContent();
		$this->showEpilog();
		$this->tabControl->EndEpilogContent();

		$query = $this->additionalUrlParams;
        
		if (isset($_REQUEST[$this->pk()])) {
			$query[$this->pk()] = $_REQUEST[$this->pk()];
		}
		elseif (isset($_REQUEST['SECTION_ID']) && $_REQUEST['SECTION_ID']) {
			$this->data[static::getSectionField()] = $_REQUEST['SECTION_ID'];
		}

		$this->tabControl->Begin(array(
			'FORM_ACTION' => static::getUrl($query)
		));

		foreach ($this->tabs as $tabSettings) {
			if ($tabSettings['VISIBLE']) {
				$this->showTabElements($tabSettings);
			}
		}

		$this->showEditPageButtons();
		$this->tabControl->ShowWarnings('editform', array()); //TODO: дописать
		$this->tabControl->Show();
	}

	/**
	 * Отображение кнопок для управления элементом на странице редактирования.
	 */
	protected function showEditPageButtons()
	{
		$listHelper = static::getHelperClass(AdminListHelper::className());
	
        $this->tabControl->Buttons(array(
			'back_url' => $listHelper::getUrl(array_merge($this->additionalUrlParams,
				array(
					'lang' => LANGUAGE_ID,
					'restore_query' => 'Y',
				)))
		));
	}

	/**
	 * Отрисовка верхней части страницы.
     * 
	 * @api
	 */
	protected function showProlog()
	{
	}

	/**
	 * Отрисовка нижней части страницы. По-умолчанию рисует все поля, которые не попали в вывод, как input hidden.
     * 
	 * @api
	 */
	protected function showEpilog()
	{
		echo bitrix_sessid_post();
	
        $interfaceSettings = static::getInterfaceSettings();

		foreach ($interfaceSettings['FIELDS'] as $code => $settings) {
			if (!isset($settings['TAB']) AND isset($settings['FORCE_SELECT']) AND $settings['FORCE_SELECT'] == true) {
				print '<input type="hidden" name="FIELDS[' . $code . ']" value="' . $this->data[$code] . '" />';
			}
		}
	}

	/**
	 * Отрисовывает вкладку со всеми привязанными к ней полями.
	 *
	 * @param $tabSettings
     * 
	 * @internal
	 */
	private function showTabElements($tabSettings)
	{
		$this->setContext(AdminEditHelper::OP_SHOW_TAB_ELEMENTS);
		$this->tabControl->BeginNextFormTab();

		foreach ($this->getFields() as $code => $fieldSettings) {
			$widget = $this->createWidgetForField($code, $this->data);
			$fieldTab = $widget->getSettings('TAB');
			$fieldOnCurrentTab = ($fieldTab == $tabSettings['DIV'] OR $tabSettings['DIV'] == 'DEFAULT_TAB');

			if (!$fieldOnCurrentTab) {
				continue;
			}

			$fieldSettings = $widget->getSettings();

			if (isset($fieldSettings['VISIBLE']) && $fieldSettings['VISIBLE'] === false) {
				continue;
			}

			$this->tabControl->BeginCustomField($code, $widget->getSettings('TITLE'));
			$pkField = ($code == $this->pk());
			$widget->showBasicEditField($pkField);
			$this->tabControl->EndCustomField($code);
		}
	}

	/**
	 * Обработка запроса редактирования страницы. Этапы:
	 * <ul>
	 * <li> Проверка прав пользователя</li>
	 * <li> Создание виджетов для каждого поля</li>
	 * <li> Удаление значений для READONLY и HIDE_WHEN_CREATE полей</li>
	 * <li> Изменение данных модели каждым виджетом (исходя из его внутренней логики)</li>
	 * <li> Валидация значений каждого поля соответствующим виджетом</li>
	 * <li> Проверка на ошибики валидации</li>
	 * <li> В случае неудачи - выход из функции</li>
	 * <li> В случае успеха - обновление или добавление элемента в БД</li>
	 * <li> Постобработка данных модели каждым виджетом</li>
	 * </ul>
	 *
	 * @return bool
	 * 
     * @see HelperWidget::processEditAction();
	 * @see HelperWidget::processAfterSaveAction();
	 * 
     * @internal
	 */
	protected function editAction()
	{
		$this->setContext(AdminEditHelper::OP_EDIT_ACTION_BEFORE);

		if (!$this->hasWriteRights()) {
			$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_WRITE_FORBIDDEN'));

			return false;
		}

		$allWidgets = array();

		foreach ($this->getFields() as $code => $settings) {
			if ($settings['READONLY'] && $code !== $this->pk()) {
				unset($this->data[$code]);
			}
		}

		foreach ($this->getFields() as $code => $settings) {
			$widget = $this->createWidgetForField($code, $this->data);
			$widget->processEditAction();
			$this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors());
			$allWidgets[] = $widget;

			if ($widget->getSettings('READONLY') || empty($this->data[$this->pk()]) 
				&& $widget->getSettings('HIDE_WHEN_CREATE')) {
				unset($this->data[$code]);
			}
		}

		$this->addErrors($this->validationErrors);
		$success = empty($this->validationErrors);

		if ($success) {
			$this->setContext(AdminEditHelper::OP_EDIT_ACTION_AFTER);
			$existing = false;
			$id = $this->getPk();

			if ($id) {
				$existing = $this->loadElement();
			}

			if ($existing) {
				$result = $this->saveElement($id);
			}
			else {
				$result = $this->saveElement();
			}

			if ($result) {
				if (!$result->isSuccess()) {
					$this->addErrors($result->getErrorMessages());

					return false;
				}
			}
			else {
				// TODO Вывод ошибки
				return false;
			}

			$this->data[$this->pk()] = $result->getId();

			foreach ($allWidgets as $widget) {
				/** @var HelperWidget $widget */
				$widget->setData($this->data);
				$widget->processAfterSaveAction();
			}

			return true;
		}

		return false;
	}

	/**
	 * Функция загрузки элемента из БД. Можно переопределить, если требуется сложная логика и нет возможности
	 * определить её в модели.
	 *
	 * @param array $select
	 *
	 * @return bool
	 * @api
	 */
	protected function loadElement($select = array())
	{
		if ($this->getPk() !== null) {
			$className = static::getModel();
			$result = $className::getList(array(
				'filter' => array(
					$this->pk() => $this->getPk()
				),
				'select' => $select ?: array('*')
			));

			return $result->fetch();
		}

		return false;
	}

	/**
	 * Сохранение элемента. Можно переопределить, если требуется сложная логика и нет возможности определить её 
     * в модели.
     * 
     * Операциями сохранения модели занимается EntityManager.
	 *
	 * @param bool $id
	 * 
     * @return \Bitrix\Main\Entity\AddResult|\Bitrix\Main\Entity\UpdateResult
	 * 
     * @throws \Exception
     * 
     * @see EntityManager
	 * 
     * @api
	 */
	protected function saveElement($id = null)
	{
		/** @var EntityManager $entityManager */
		$entityManager = new static::$entityManager(static::getModel(), empty($this->data) ? array() : $this->data, $id, $this);
		$saveResult = $entityManager->save();
		$this->addNotes($entityManager->getNotes());

		return $saveResult;
	}

	/**
	 * Удаление элемента. Можно переопределить, если требуется сложная логика и нет возможности определить её в модели.
	 *
	 * @param $id
	 * 
     * @return bool|\Bitrix\Main\Entity\DeleteResult
	 * 
     * @throws \Exception
	 * 
     * @api
	 */
	protected function deleteElement($id)
	{
		if (!$this->hasDeleteRights()) {
			$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_FORBIDDEN'));

			return false;
		}
		
		/** @var EntityManager $entityManager */
		$entityManager = new static::$entityManager(static::getModel(), empty($this->data) ? array() : $this->data, $id, $this);

		$deleteResult = $entityManager->delete();
		$this->addNotes($entityManager->getNotes());

		return $deleteResult;
	}

	/**
	 * Выполнение кастомных операций над объектом в процессе редактирования.
	 *
	 * @param string $action Название операции.
	 * @param int|null $id ID элемента.
     * 
	 * @see AdminEditHelper::fillMenu()
	 * 
     * @api
	 */
	protected function customActions($action, $id = null)
	{
		if ($action == 'delete' AND !is_null($id)) {
			$result = $this->deleteElement($id);
            
			if(!$result->isSuccess()){
				$this->addErrors($result->getErrorMessages());
			}
			
            $listHelper = static::getHelperClass(AdminListHelper::className());
            $redirectUrl = $listHelper::getUrl(array_merge(
                $this->additionalUrlParams,
                array('restore_query' => 'Y')
            ));
			
            LocalRedirect($redirectUrl);
		}
	}

	/**
	 * Устанавливает заголовок исходя из данных текущего элемента.
	 *
	 * @see $data
	 * @see AdminBaseHelper::setTitle()
	 * 
     * @api
	 */
	protected function setElementTitle()
	{
		if (!empty($this->data)) {
			$title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_TITLE', array('#ID#' => $this->data[$this->pk()]));
		}
		else {
			$title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_NEW_ELEMENT');
		}

		$this->setTitle($title);
	}

	/**
	 * @return \CAdminForm
	 */
	public function getTabControl()
	{
		return $this->tabControl;
	}

    /**
     * @inheritdoc
     */
	public static function getUrl(array $params = array())
	{
		return static::getViewURL(static::getViewName(), static::$editPageUrl, $params);
	}
}


================================================
FILE: lib/helper/AdminInterface.php
================================================
<?php

namespace DigitalWand\AdminHelper\Helper;

/**
 * Базовый класс для описания админского интерфейса.
 * Включает в себя методы описывающие элементы управления, названия столбцов, типы полей и т.д.
 *
 * Есть 2 метода которые обязательно должны быть описаны в реализуемых классах:
 *
 * getFields()  - должен возвращать массив со списком табов и описанием полей для каждого таба
 * getHelpers() - должен возваращать массив со списком классов хелперов, также может включать
 * описание настроек элементов управления для хелпера.
 *
 * Для того что бы модуль мог корректна работать необходима регистрация классов унаследованных от AdminInterface.
 * Это можно сделтаь в include.php другого модуля(не рекомендуется) или AdminInterface зарегистрируется
 * автоматически если при генерации ссылок на страницы админского интерфейса использовался статический
 * метод getLink из соответствующего хелпера (ListHelper для списка элементов и EditHelper для страницы редактирования)
 *
 * При использовании разделов необходимо уведомить AdminInterface элементов и AdminInterface разделов о существовании
 * друг друга, что бы каждый из них регистрировал другого в момент собственной регистрации. Для этого достаточно указать полное
 * имя класса в методе getDependencies(), это нужно сделать как для AdminInterface элементов так и для AdminInterface разделов.
 *
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 * @author Artem Yarygin <artx19@yandex.ru>
 */
abstract class AdminInterface
{
	/**
	 * Список зарегистрированных интерфейсов
	 * @var string
	 */
	public static $registeredInterfaces = array();

	/**
	 * Описание интерфейса админки: списка табов и полей. Метод должен вернуть массив вида:
	 *
	 * ```
	 * array(
	 *    'TAB_1' => array(
	 *        'NAME' => Loc::getMessage('VENDOR_MODULE_ENTITY_TAB_1_NAME'),
	 *        'FIELDS' => array(
	 *            'FIELD_1' => array(
	 *                'WIDGET' => new StringWidget(),
	 *                'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_1_TITLE'),
	 *                ...
	 *            ),
	 *            'FIELD_2' => array(
	 *                'WIDGET' => new NumberWidget(),
	 *                'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_2_TITLE'),
	 *                ...
	 *            ),
	 *            ...
	 *        )
	 *    ),
	 *    'TAB_2' => array(
	 *        'NAME' => Loc::getMessage('VENDOR_MODULE_ENTITY_TAB_2_NAME'),
	 *        'FIELDS' => array(
	 *            'FIELD_3' => array(
	 *                'WIDGET' => new DateTimeWidget(),
	 *                'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_3_TITLE'),
	 *                ...
	 *            ),
	 *            'FIELD_4' => array(
	 *                'WIDGET' => new UserWidget(),
	 *                'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_4_TITLE'),
	 *                ...
	 *            ),
	 *            ...
	 *        )
	 *    ),
	 *  ...
	 * )
	 * ```
	 *
	 * Где TAB_1..2 - символьные коды табов, FIELD_1..4 - название столбцов в таблице сущности. TITLE для поля задавать
	 * не обязательно, в этому случае он будет запрашиваться из модели.
	 *
	 * Более подробную информацию о формате описания настроек виджетов см. в классе HelperWidget.
	 *
	 * @see DigitalWand\AdminHelper\Widget\HelperWidget
	 *
	 * @return array[]
	 */
	abstract public function fields();

	/**
	 * Список классов хелперов с настройками. Метод должен вернуть массив вида:
	 *
	 * ```
	 * array(
	 *    '\Vendor\Module\Entity\AdminInterface\EntityListHelper' => array(
	 *        'BUTTONS' => array(
	 *            'RETURN_TO_LIST' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_RETURN_TO_LIST')),
	 *            'ADD_ELEMENT' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_ADD_ELEMENT'),
	 *            ...
	 *        )
	 *    ),
	 *    '\Vendor\Module\Entity\AdminInterface\EntityEditHelper' => array(
	 *        'BUTTONS' => array(
	 *            'LIST_CREATE_NEW' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_LIST_CREATE_NEW')),
	 *            'LIST_CREATE_NEW_SECTION' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_LIST_CREATE_NEW_SECTION'),
	 *            ...
	 *        )
	 *    )
	 * )
	 * ```
	 *
	 * или
	 *
	 * ```
	 * array(
	 *    '\Vendor\Module\Entity\AdminInterface\EntityListHelper',
	 *    '\Vendor\Module\Entity\AdminInterface\EntityEditHelper'
	 * )
	 * ```
	 *
	 * Где:
	 * <ul>
	 * <li> `Vendor\Module\Entity\AdminInterface` - namespace до реализованных классов AdminHelper.
	 * <li> `BUTTONS` - ключ для массива с описанием элементов управления (подробнее в методе getButton()
	 *          класса AdminBaseHelper).
	 * <li> `LIST_CREATE_NEW`, `LIST_CREATE_NEW_SECTION`, `RETURN_TO_LIST`, `ADD_ELEMENT` - символьные код элементов
	 *          управления.
	 * <li> `EntityListHelper` и `EntityEditHelper` - реализованные классы хелперов.
	 *
	 * Оба формата могут сочетаться друг с другом.
	 *
	 * @see \DigitalWand\AdminHelper\Helper\AdminBaseHelper::getButton()
	 *
	 * @return string[]
	 */
	abstract public function helpers();

	/**
	 * Список зависимых админских интерфейсов, которые будут зарегистрированы при регистраци админского интерфейса,
	 * например, админские интерфейсы разделов.
	 *
	 * @return string[]
	 */
	public function dependencies()
	{
		return array();
	}

	/**
	 * Регистрируем поля, табы и кнопки.
	 */
	public function registerData()
	{
		$fieldsAndTabs = array('FIELDS' => array(), 'TABS' => array());
		$tabsWithFields = $this->fields();

		// приводим массив хелперов к формату класс => настройки
		$helpers = array();

		foreach ($this->helpers() as $key => $value) {
			if (is_array($value)) {
				$helpers[$key] = $value;
			}
			else {
				$helpers[$value] = array();
			}
		}

		$helperClasses = array_keys($helpers);
		/**
		 * @var \Bitrix\Main\Entity\DataManager
		 */
		$model = $helperClasses[0]::getModel();
		foreach ($tabsWithFields as $tabCode => $tab) {
			$fieldsAndTabs['TABS'][$tabCode] = $tab['NAME'];

			foreach ($tab['FIELDS'] as $fieldCode => $field) {
				if (empty($field['TITLE']) && $model) {
				    //Битрикс не использует параметр title при создании экземпляра ReferenceField.
                    if (is_a($model::getEntity()->getField($fieldCode), 'Bitrix\Main\Entity\ReferenceField')) {
                        $map = $model::getMap();
                        if(isset($map[$fieldCode]['title'])){
                            $field['TITLE'] = $map[$fieldCode]['title'];
                        }
                    } else {
                        $field['TITLE'] = $model::getEntity()->getField($fieldCode)->getTitle();
                    }
				}

				$field['TAB'] = $tabCode;
				$fieldsAndTabs['FIELDS'][$fieldCode] = $field;
			}
		}

		AdminBaseHelper::setInterfaceSettings($fieldsAndTabs, $helpers, $helperClasses[0]::getModule());

		foreach ($helperClasses as $helperClass) {
			/**
			 * @var AdminBaseHelper $helperClass
			 */
			$helperClass::setInterfaceClass(get_called_class());
		}
	}

	/**
	 * Регистрация интерфейса и его зависимостей.
	 */
	public static function register()
	{
		if (!in_array(get_called_class(), static::$registeredInterfaces)) {
			static::$registeredInterfaces[] = get_called_class();

			$adminInterface = new static();
			$adminInterface->registerData();

			foreach ($adminInterface->dependencies() as $adminInterfaceClass) {
				$adminInterfaceClass::register();
			}
		}
	}
}

================================================
FILE: lib/helper/AdminListHelper.php
================================================
<?php

namespace DigitalWand\AdminHelper\Helper;

use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Entity\DataManager;
use Bitrix\Main\DB\Result;
use DigitalWand\AdminHelper\EntityManager;
use DigitalWand\AdminHelper\Sorting;

Loc::loadMessages(__FILE__);

/**
 * Базовый класс для реализации страницы списка админки.
 * При создании своего класса необходимо переопределить следующие переменные:
 * <ul>
 * <li> static protected $model </Li>
 * </ul>
 *
 * Этого будет дастаточно для получения минимальной функциональности
 * Также данный класс может использоваться для отображения всплывающих окон с возможностью выбора элемента из списка
 *
 * @see AdminBaseHelper::$model
 * @see AdminBaseHelper::$module
 * @see AdminBaseHelper::$editViewName
 * @see AdminBaseHelper::$viewName
 * @package AdminHelper
 *
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 * @author Artem Yarygin <artx19@yandex.ru>
 */
abstract class AdminListHelper extends AdminBaseHelper
{
	const OP_GROUP_ACTION = 'AdminListHelper::__construct_groupAction';
	const OP_ADMIN_VARIABLES_FILTER = 'AdminListHelper::prepareAdminVariables_filter';
	const OP_ADMIN_VARIABLES_HEADER = 'AdminListHelper::prepareAdminVariables_header';
	const OP_GET_DATA_BEFORE = 'AdminListHelper::getData_before';
	const OP_ADD_ROW_CELL = 'AdminListHelper::addRowCell';
	const OP_CREATE_FILTER_FORM = 'AdminListHelper::createFilterForm';
	const OP_CHECK_FILTER = 'AdminListHelper::checkFilter';
	const OP_EDIT_ACTION = 'AdminListHelper::editAction';

	
	/**
     	* @var bool
     	* Показывать ли кнопки добавления раздела и элемента в списке  
     	*/
	protected $showAdd = true;
	/**
	 * @var bool
	 * Выводить кнопку экспорта в Excel
	 * @api
	 */
	protected $exportExcel = true;
	/**
	 * @var bool
	 * Выводить в списке кол-во элементов пункт Все
	 */
	protected $showAll = true;
	/**
	 * @var bool
	 * Является ли список всплывающим окном для выбора элементов из списка.
	 * В этой версии не должно быть операций удаления/перехода к редактированию.
	 */
	protected $isPopup = false;
	/**
	 * @var string
	 * Название поля, в котором хранится результат выбора во всплывающем окне
	 */
	protected $fieldPopupResultName = '';
	/**
	 * @var string
	 * Уникальный индекс поля, в котором хранится результат выбора во всплывающем окне
	 */
	protected $fieldPopupResultIndex = '';
	protected $sectionFields = array();
	/**
	 * @var string
	 * Название столбца, в котором хранится название элемента
	 */
	protected $fieldPopupResultElTitle = '';
	/**
	 * @var string
	 * Название функции, вызываемой при даблклике на строке списка, в случае, если список выводится в режиме
	 *     всплывающего окна
	 */
	protected $popupClickFunctionName = 'selectRow';
	/**
	 * @var string
	 * Код функции, вызываемой при клике на строке списка
	 * @see AdminListHelper::genPopupActionJS()
	 */
	protected $popupClickFunctionCode;
	/**
	 * @var array
	 * Массив с заголовками таблицы
	 * @see \CAdminList::AddHeaders()
	 */
	protected $arHeader = array();
	/**
	 * @var array
	 * параметры фильтрации списка в классическим битриксовом формате
	 */
	protected $arFilter = array();
	/**
	 * @var array
	 * Массив, хранящий тип фильтра для данного поля. Позволяет избежать лишнего парсинга строк.
	 */
	protected $filterTypes = array();
	/**
	 * @var array
	 * Поля, предназначенные для фильтрации
	 * @see \CAdminList::InitFilter();
	 */
	protected $arFilterFields = array();
	/**
	 * Список полей, для которых доступна фильтрация
	 * @var array
	 * @see \CAdminFilter::__construct();
	 */
	protected $arFilterOpts = array();
	/**
	 * @var \CAdminList
	 */
	protected $list;
	/**
	 * @var string
	 * Префикс таблицы. Нужен, чтобы обеспечить уникальность относительно других админ. интерфейсов.
	 * Без его добавления к конструктору таблицы повычается вероятность, что возникнет конфликт с таблицей из другого
	 * административного интерфейса, в результате чего неправильно будет работать паджинация, фильтрация. Вероятны
	 * ошибки запросов к БД.
	 */
	static protected $tablePrefix = "digitalwand_admin_helper_";
	/**
	 * @var array
	 * @see \CAdminList::AddGroupActionTable()
	 */
	protected $groupActionsParams = array();
	/**
	 * Текущие параметры пагинации,
	 * требуются для составления смешанного списка разделов и элементов
	 * @var array
	 */
	protected $navParams = array();
	/**
	 * Количество элементов смешанном списке
	 * @see AdminListHelper::CustomNavStart
	 * @var int
	 */
	protected $totalRowsCount = 0;
	/**
	 * Массив для слияния столбцов элементов и разделов
	 * @var array
	 */
	protected $tableColumnsMap = array();
	/**
	 * @var string
	 * HTML верхней части таблицы
	 * @api
	 */
	public $prologHtml;

	/**
	 * @var string
	 * HTML нижней части таблицы
	 * @api
	 */
	public $epilogHtml;

	/**
	 * Производится инициализация переменных, обработка запросов на редактирование
	 *
	 * @param array $fields
	 * @param bool $isPopup
	 * @throws \Bitrix\Main\ArgumentException
	 */
	public function __construct(array $fields, $isPopup = false)
	{
		$this->isPopup = $isPopup;

		if ($this->isPopup) {
			$this->fieldPopupResultName = preg_replace("/[^a-zA-Z0-9_:\\[\\]]/", "", $_REQUEST['n']);
			$this->fieldPopupResultIndex = preg_replace("/[^a-zA-Z0-9_:]/", "", $_REQUEST['k']);
			$this->fieldPopupResultElTitle = $_REQUEST['eltitle'];
		}

		parent::__construct($fields);

		$this->restoreLastGetQuery();
		$this->prepareAdminVariables();

		$className = static::getModel();
		$oSort = $this->initSortingParameters();
		$this->list = new \CAdminList($this->getListTableID(), $oSort);
		$this->list->InitFilter($this->arFilterFields);

		if ($this->list->EditAction() AND $this->hasWriteRights()) {
			global $FIELDS;
			foreach ($FIELDS as $id => $fields) {
				if (!$this->list->IsUpdated($id)) {
					continue;
				}
				$this->editAction($id, $fields);
			}
		}
		if ($IDs = $this->list->GroupAction() AND $this->hasWriteRights()) {

		    //Элементы выбраны галочкой "Для всех". Нужно собрать все элементы и разделы,
            //попадающие под текущий фильтр, и передать их ID на удаление

			if ($_REQUEST['action_target'] == 'selected') {
                $this->setContext(AdminListHelper::OP_GROUP_ACTION);

                //Если находимся в подразделе, то его нужно учесть при фильтрации
                if (isset($_GET['SECTION_ID'])) {
                    $sectionField = static::getSectionField();
                    $this->arFilter[$sectionField] = $_GET['SECTION_ID'];
                }

                $IDs = array();

                //Текущий фильтр должен быть модифицирован виждтами
                //для соответствия результатов фильтрации тому, что видит пользователь в интерфейсе.
                $raw = array(
                    'SELECT' => $this->pk(),
                    'FILTER' => $this->arFilter,
                    'SORT' => array()
                );

                foreach ($this->fields as $code => $settings) {
                    $widget = $this->createWidgetForField($code);
                    $widget->changeGetListOptions($this->arFilter, $raw['SELECT'], $raw['SORT'], $raw);
                }

                $res = $className::getList(array(
                    'filter' => $this->arFilter,
                    'select' => array($this->pk()),
                ));

                while ($el = $res->Fetch()) {
                    $IDs[] = $el[$this->pk()];
                }

                //Собираем ID разделов, если они используются
                $sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
                if ($sectionEditHelperClass) {
                    $sectionFilter = $this->arFilter;
                    $sectionsInterfaceSettings = static::getInterfaceSettings($sectionEditHelperClass::getViewName());

                    if ($sectionEditHelperClass && !isset($_REQUEST['model-section'])) {
                        $sectionClassName = $sectionEditHelperClass::getModel();
                    } else {
                        $sectionClassName = $_REQUEST['model-section'];
                    }

                    foreach ($sectionFilter as $elementFieldName => $elementFilterValue) {
                        $elementFieldNameEscaped = $this->escapeFilterFieldName($elementFieldName);
                        if (!isset($sectionsInterfaceSettings['FIELDS'][$elementFieldNameEscaped])) {
                            unset($sectionFilter[$elementFieldName]);
                        }
                    }
                    if (isset($_GET['SECTION_ID'])) {
                        $sectionField = $sectionEditHelperClass::getSectionField();
                        $sectionFilter[$sectionField] = $_GET['SECTION_ID'];
                    }

                    $res = $sectionClassName::getList(array(
                        'filter' => $sectionFilter,
                        'select' => array($this->pk()),
                    ));

                    while ($el = $res->Fetch()) {
                        $IDs[] = 's' . $el[$this->pk()];
                    }
                }
            }

			$filteredIDs = array();

			foreach ($IDs as $id) {
				if (strlen($id) <= 0) {
					continue;
				}
				$filteredIDs[] = IntVal($id);
			}
			$this->groupActions($IDs, $_REQUEST['action']);
		}

		if (isset($_REQUEST['action']) || isset($_REQUEST['action_button']) && count($this->getErrors()) == 0) {
			$listHelperClass = $this->getHelperClass(AdminListHelper::className());
			$id = isset($_GET['ID']) ? $_GET['ID'] : null;
			$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : $_REQUEST['action_button'];
			if ($action != 'edit' && $_REQUEST['cancel'] != 'Y') {
				$params = $_GET;
				unset($params['action']);
				unset($params['action_button']);
				$this->customActions($action, $id);
				LocalRedirect($listHelperClass::getUrl($params));
			}
		}

		if ($this->isPopup()) {
			$this->genPopupActionJS();
		}

		// Получаем параметры навигации
		$navUniqSettings = array(
			'nPageSize' => 20,
			'sNavID' => $this->getListTableID()
		);
		$this->navParams = array(
			'nPageSize' => \CAdminResult::GetNavSize($this->getListTableID(), $navUniqSettings),
			'navParams' => \CAdminResult::GetNavParams($navUniqSettings)
		);
	}

	/**
	 * Инициализирует параметры сортировки на основании запроса
	 * @return \CAdminSorting
	 */
	protected function initSortingParameters()
	{
		$sortByParameter = 'by';
		$sortOrderParameter = 'order';

		return new Sorting($this->getListTableID(), $this->pk(), 'desc', $sortByParameter, $sortOrderParameter, $this);
	}

	/**
	 * Подготавливает переменные, используемые для инициализации списка.
	 *
	 * - добавляет поля в список фильтра только если FILTER не задано false по умолчанию для виджета и поле не является
	 * полем связи сущностью разделов
	 */
	protected function prepareAdminVariables()
	{
		$this->arHeader = array();
		$this->arFilter = array();
		$this->arFilterFields = array();
		$arFilter = array();
		$this->filterTypes = array();
		$this->arFilterOpts = array();

		$sectionField = static::getSectionField();

		foreach ($this->fields as $code => $settings) {
			$widget = $this->createWidgetForField($code);

			if (
				($sectionField != $code && $widget->getSettings('FILTER') !==false)
				&&
				((isset($settings['FILTER']) AND $settings['FILTER'] != false) OR !isset($settings['FILTER']))
			) {

				$this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_FILTER);
				$filterVarName = 'find_' . $code;
				$this->arFilterFields[] = $filterVarName;
				$filterType = '';

				if (is_string($settings['FILTER'])) {
					$filterType = $settings['FILTER'];
				}

				if (isset($_REQUEST[$filterVarName])
					AND !isset($_REQUEST['del_filter'])
					AND $_REQUEST['del_filter'] != 'Y'
				) {
					$arFilter[$filterType . $code] = $_REQUEST[$filterVarName];
					$this->filterTypes[$code] = $filterType;
				}

				$this->arFilterOpts[$code] = $widget->getSettings('TITLE');
			}

			if (!isset($settings['LIST']) || $settings['LIST'] === true) {
				$this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_HEADER);
				$mergedColumn = false;
				// проверяем есть ли столбец раздела с таким названием
				if ($widget->getSettings('LIST_TITLE')) {
					$sectionHeader = $this->getSectionsHeader();
					foreach ($sectionHeader as $sectionColumn) {
						if ($sectionColumn['content'] == $widget->getSettings('LIST_TITLE')) {
							// добавляем столбец элементов в карту столбцов
							$this->tableColumnsMap[$code] = $sectionColumn['id'];
							$mergedColumn = true;
							break;
						}
					}
				}
				if (!$mergedColumn) {
					$this->arHeader[] = array(
						"id" => $code,
						"content" => $widget->getSettings('LIST_TITLE') ? $widget->getSettings('LIST_TITLE') : $widget->getSettings('TITLE'),
						"sort" => $code,
						"default" => !isset($settings['HEADER']) || $settings['HEADER'] === true,
						'admin_list_helper_sort' => $widget->getSettings('LIST_COLUMN_SORT') ? $widget->getSettings('LIST_COLUMN_SORT') : 100
					);
				}
			}
		}

		if ($this->checkFilter($arFilter)) {
			$this->arFilter = $arFilter;
		}

		if (static::getHelperClass(AdminSectionEditHelper::className())) {
			$this->arFilter[static::getSectionField()] = $_GET['ID'];
		}
	}

	/**
	 * Возвращает список столбцов для разделов
	 * @return array
	 */
	public function getSectionsHeader()
	{
		$arSectionsHeaders = array();
		$sectionHelper = static::getHelperClass(AdminSectionEditHelper::className());
		$sectionsInterfaceSettings = static::getInterfaceSettings($sectionHelper::getViewName());
		$this->sectionFields = $sectionsInterfaceSettings['FIELDS'];

		foreach ($sectionsInterfaceSettings['FIELDS'] as $code => $settings) {

			if (!isset($settings['LIST']) || $settings['LIST'] === true) {
				$arSectionsHeaders[] = array(
					"id" => $code,
					"content" => isset($settings['LIST_TITLE']) ? $settings['LIST_TITLE'] : $settings['TITLE'],
					"sort" => $code,
					"default" => !isset($settings['HEADER']) || $settings['HEADER'] === true,
					'admin_list_helper_sort' => isset($settings['LIST_COLUMN_SORT']) ? $settings['LIST_COLUMN_SORT'] : 100
				);
			}
			unset($settings['WIDGET']);

			foreach ($settings as $c => $v) {
				$sectionsInterfaceSettings['FIELDS'][$code]['WIDGET']->setSetting($c, $v);
			}
		}

		return $arSectionsHeaders;
	}

	/**
	 * Производит проверку корректности данных (в массиве $_REQUEST), переданных в фильтр
	 * @TODO: нужно сделать вывод сообщений об ошибке фильтрации.
	 * @param $arFilter
	 * @return bool
	 */
	protected function checkFilter($arFilter)
	{
		$this->setContext(AdminListHelper::OP_CHECK_FILTER);
		$filterValidationErrors = array();
		foreach ($this->filterTypes as $code => $type) {
			$widget = $this->createWidgetForField($code);
			$value = $arFilter[$type . $code];
			if (!$widget->checkFilter($type, $value)) {
				$filterValidationErrors = array_merge($filterValidationErrors,
					$widget->getValidationErrors());
			}
		}

		return empty($filterValidationErrors);
	}

	/**
	 * Подготавливает массив с настройками контекстного меню. По-умолчанию добавлена кнопка "создать элемент".
	 *
	 * @see $contextMenu
	 *
	 * @api
	 */
	protected function getContextMenu()
	{
		$contextMenu = array();
		/** @var AdminSectionEditHelper $sectionEditHelper */
		$sectionEditHelper = static::getHelperClass(AdminSectionEditHelper::className());
		if ($sectionEditHelper) {
			$sectionId = $_GET['SECTION_ID'] ?: $_GET['ID'] ?: null;
			$this->additionalUrlParams['SECTION_ID'] = $sectionId = $sectionId > 0 ? (int)$sectionId : null;
		}

		/**
		 * Если задан для разделов добавляем кнопку создать раздел и
		 * кнопку на уровень вверх если это не корневой раздел
		 */
		if (isset($sectionId)) {
			$params = $this->additionalUrlParams;
			$sectionModel = $sectionEditHelper::getModel();
			$sectionField = $sectionEditHelper::getSectionField();
			$section = $sectionModel::getById(
				$this->getCommonPrimaryFilterById($sectionModel, null, $sectionId)
			)->Fetch();
			if ($this->isPopup()) {
				$params = array_merge($_GET);
			}
			if ($section[$sectionField]) {
				$params['ID'] = $section[$sectionField];
			}
			else {
				unset($params['ID']);
			}
			unset($params['SECTION_ID']);
			$contextMenu[] = $this->getButton('LIST_SECTION_UP', array(
				'LINK' => static::getUrl($params),
				'ICON' => 'btn_list'
			));
		}

		/**
		 * Добавляем кнопку создать элемент и создать раздел
		 */
		if (!$this->isPopup() && $this->hasWriteRights() && $this->showAdd) {
			$editHelperClass = static::getHelperClass(AdminEditHelper::className());
			if ($editHelperClass) {
				$contextMenu[] = $this->getButton('LIST_CREATE_NEW', array(
					'LINK' => $editHelperClass::getUrl($this->additionalUrlParams),
					'ICON' => 'btn_new'
				));
			}
			$sectionsHelperClass = static::getHelperClass(AdminSectionEditHelper::className());
			if ($sectionsHelperClass) {
				$contextMenu[] = $this->getButton('LIST_CREATE_NEW_SECTION', array(
					'LINK' => $sectionsHelperClass::getUrl($this->additionalUrlParams),
					'ICON' => 'btn_new'
				));
			}
		}

		return $contextMenu;
	}

	/**
	 * Возвращает массив с настройками групповых действий над списком.
	 *
	 * @return array
	 *
	 * @api
	 */
	protected function getGroupActions()
	{
		$result = array();

		if (!$this->isPopup()) {
			if ($this->hasDeleteRights()) {
				$result = array('delete' => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE"));
			}
		}

		return $result;
	}

	/**
	 * Обработчик групповых операций. По-умолчанию прописаны операции активации / деактивации и удаления.
	 *
	 * @param array $IDs
	 * @param string $action
	 *
	 * @api
	 */
	protected function groupActions($IDs, $action)
	{
		$sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
		$listHelperClass = $this->getHelperClass(AdminListHelper::className());

		$className = static::getModel();
		if (isset($_REQUEST['model'])) {
			$className = $_REQUEST['model'];
		}

		if ($sectionEditHelperClass && !isset($_REQUEST['model-section'])) {
			$sectionClassName = $sectionEditHelperClass::getModel();
		}
		else {
			$sectionClassName = $_REQUEST['model-section'];
		}

		if ($action == 'delete') {
			if ($this->hasDeleteRights()) {
				$complexPrimaryKey = is_array($className::getEntity()->getPrimary());
				if ($complexPrimaryKey) {
					$IDs = $this->getIds();
				}

				// ищем правильный урл для перехода
				if (!empty($IDs[0])) {

					$id = $complexPrimaryKey ? $IDs[0][$this->pk()] : $IDs[0];
					$model = $className;

					if (strpos($id, 's') === 0) {
						$model = $sectionClassName;
						$listHelper = $this->getHelperClass(AdminSectionListHelper::className());
						if (!$listHelper) {
							$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_SECTION_HELPER_NOT_FOUND'));
							unset($_GET['ID']);
							return;
						}
						$id = substr($id, 1);
					} else {
						$listHelper = $listHelperClass;
					}

					if ($listHelper) {
						$id = $this->getCommonPrimaryFilterById($model, null, $id);
						$element = $model::getById($id)->Fetch();
						$sectionField = $listHelper::getSectionField();
						if ($element[$sectionField]) {
							$_GET[$this->pk()] = $element[$sectionField];
						} else {
							unset($_GET['ID']);
						}
					}
				}

				foreach ($IDs as $id) {
					$model = $className;
					$id = $complexPrimaryKey ? $id[$this->pk()] : $id;
					if (strpos($id, 's') === 0) {
						$model = $sectionClassName;
						$id = substr($id, 1);
					}
					/** @var EntityManager $entityManager */
					$entityManager = new static::$entityManager($model, empty($this->data) ? array() : $this->data, $id,
						$this);
					$result = $entityManager->delete();
					$this->addNotes($entityManager->getNotes());
					if (!$result->isSuccess()) {
						$this->addErrors($result->getErrorMessages());
						break;
					}
				}
			}
			else {
				$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_FORBIDDEN'));
			}
		}

		if ($action == 'delete-section') {
			if ($this->hasDeleteRights()) {

				// ищем правильный урл для перехода
				if (!empty($IDs[0])) {
					$id = $this->getCommonPrimaryFilterById($sectionClassName, null, $IDs[0]);
					$sectionListHelperClass = $this->getHelperClass(AdminSectionListHelper::className());
					if ($sectionListHelperClass) {
						$element = $sectionClassName::getById($id)->Fetch();
						$sectionField = $sectionListHelperClass::getSectionField();
						if ($element[$sectionField]) {
							$_GET[$this->pk()] = $element[$sectionField];
						} else {
							unset($_GET['ID']);
						}
					} else {
						unset($_GET['ID']);
						$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_SECTION_HELPER_NOT_FOUND'));
						return;
					}
				}

				foreach ($IDs as $id) {
					$id = $this->getCommonPrimaryFilterById($sectionClassName, null, $id);
					$entityManager = new static::$entityManager($sectionClassName, array(), $id, $this);
					$result = $entityManager->delete();
					$this->addNotes($entityManager->getNotes());
					if(!$result->isSuccess()){
						$this->addErrors($result->getErrorMessages());
						break;
					}
				}
			}
			else {
				$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_FORBIDDEN'));
			}
		}
	}

	/**
	 * Сохранение полей для отной записи, отредактированной в списке.
	 * Этапы:
	 * <ul>
	 * <li> Выборка элемента по ID, чтобы удостовериться, что он существует. В противном случае  возвращается
	 * ошибка</li>
	 * <li> Создание виджета для каждой ячейки, валидация значений поля</li>
	 * <li> TODO: вывод ошибок валидации</li>
	 * <li> Сохранение записи</li>
	 * <li> Вывод ошибок сохранения, если таковые появились</li>
	 * <li> Модификация данных сроки виджетами.</li>
	 * </ul>
	 *
	 * @param int $id ID записи в БД
	 * @param array $fields Поля с изменениями
	 *
	 * @see HelperWidget::processEditAction();
	 * @see HelperWidget::processAfterSaveAction();
	 */
	protected function editAction($id, $fields)
	{
		$this->setContext(AdminListHelper::OP_EDIT_ACTION);
		if(strpos($id, 's')===0){ // для раделов другой класс модели
			$editHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
			$sectionsInterfaceSettings = static::getInterfaceSettings($editHelperClass::getViewName());
			$className = $editHelperClass::getModel();
			$id = str_replace('s','',$id);
		}else{
			$className = static::getModel();
			$sectionsInterfaceSettings = false;
		}

		$idForLog = $id;
		$complexPrimaryKey = is_array($className::getEntity()->getPrimary());
		if ($complexPrimaryKey) {
			$oldRequest = $_REQUEST;
			$_REQUEST = array($this->pk() => $id);
			$id = $this->getCommonPrimaryFilterById($className, null, $id);
			$idForLog = json_encode($id);
			$_REQUEST = $oldRequest;
		}

		$el = $className::getById($id);
		if ($el->getSelectedRowsCount() == 0) {
			$this->list->AddGroupError(Loc::getMessage("MAIN_ADMIN_SAVE_ERROR"), $idForLog);
			return;
		}

		// замена кодов для столбцов элементов соединенных со столбцами разделов
		if($sectionsInterfaceSettings==false){
			$tableColumnsMap = array_flip($this->tableColumnsMap);
			$replacedFields = array();
			foreach($fields as $key => $value){
				if(!empty($tableColumnsMap[$key])) {
					$key = $tableColumnsMap[$key];
				}
				$replacedFields[$key] = $value;
			}
			$fields = $replacedFields;
		}

		$allWidgets = array();
		foreach ($fields as $key => $value) {
			if($sectionsInterfaceSettings!==false){ // для разделов свои виджеты
				$widget = $sectionsInterfaceSettings['FIELDS'][$key]['WIDGET'];
			}else{
				$widget = $this->createWidgetForField($key, $fields); // для элементов свои
			}

			$widget->processEditAction();
			$this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors());
			$allWidgets[] = $widget;
		}
		//FIXME: может, надо добавить вывод ошибок ДО сохранения?..
		$this->addErrors($this->validationErrors);

		$result = $className::update($id, $fields);
		$errors = $result->getErrorMessages();
		if (empty($this->validationErrors) AND !empty($errors)) {
			$fieldList = implode("\n", $errors);
			$this->list->AddGroupError(Loc::getMessage("MAIN_ADMIN_SAVE_ERROR") . " " . $fieldList, $idForLog);
		}

		if (!empty($errors)) {
			foreach ($allWidgets as $widget) {
				/** @var \DigitalWand\AdminHelper\Widget\HelperWidget $widget */
				$widget->setData($fields);
				$widget->processAfterSaveAction();
			}
		}
	}

	/**
	 * Является ли список всплывающим окном для выбора элементов из списка.
	 * В этой версии не должно быть операций удаления/перехода к редактированию.
	 *
	 * @return boolean
	 */
	public function isPopup()
	{
		return $this->isPopup;
	}

	/**
	 * Функция определяет js-функцию для двойонго клика по строке.
	 * Вызывается в том случае, если окно открыто в режиме попапа.
	 *
	 * @api
	 */
	protected function genPopupActionJS()
	{
		$this->popupClickFunctionCode = '<script>
			function ' . $this->popupClickFunctionName . '(data){
				var input = window.opener.document.getElementById("' . $this->fieldPopupResultName . '[' . $this->fieldPopupResultIndex . ']");
				if(!input)
					input = window.opener.document.getElementById("' . $this->fieldPopupResultName . '");
				if(input)
				{
					input.value = data.ID;
					if (window.opener.BX)
						window.opener.BX.fireEvent(input, "change");
				}
				var span = window.opener.document.getElementById("sp_' . md5($this->fieldPopupResultName) . '_' . $this->fieldPopupResultIndex . '");
				if(!span)
					span = window.opener.document.getElementById("sp_' . $this->fieldPopupResultName . '");
				if(!span)
					span = window.opener.document.getElementById("' . $this->fieldPopupResultName . '_link");
				if(span)
					span.innerHTML = data["' . $this->fieldPopupResultElTitle . '"];
				window.close();
			}
		</script>';
	}

	/**
	 * Основной цикл отображения списка. Этапы:
	 * <ul>
	 * <li> Вывод заголовков страницы </li>
	 * <li> Определение списка видимых колонок и колонок, участвующих в выборке. </li>
	 * <li> Создание виджета для каждого поля выборки </li>
	 * <li> Модификация параметров запроса каждым из виджетов </li>
	 * <li> Выборка данных </li>
	 * <li> Вывод строк таблицы. Во время итерации по строкам возможна модификация данных строки. </li>
	 * <li> Отрисовка футера таблицы, добавление контекстного меню </li>
	 * </ul>
	 *
	 * @param array $sort Настройки сортировки.
	 *
	 * @see AdminListHelper::getList();
	 * @see AdminListHelper::getMixedData();
	 * @see AdminListHelper::modifyRowData();
	 * @see AdminListHelper::addRowCell();
	 * @see AdminListHelper::addRow();
	 * @see HelperWidget::changeGetListOptions();
	 */
	public function buildList($sort)
	{
		$this->setContext(AdminListHelper::OP_GET_DATA_BEFORE);

		$headers = $this->arHeader;

		$sectionEditHelper = static::getHelperClass(AdminSectionEditHelper::className());

		if ($sectionEditHelper) { // если есть реализация класса AdminSectionEditHelper, значит используются разделы
			$sectionHeaders = $this->getSectionsHeader();
			foreach ($sectionHeaders as $sectionHeader) {
				$found = false;
				foreach ($headers as $i => $elementHeader) {
					if ($sectionHeader['content'] == $elementHeader['content'] || $sectionHeader['id'] == $elementHeader['id']) {
						if (!$elementHeader['default'] && $sectionHeader['default']) {
							$headers[$i] = $sectionHeader;
						} else {
							$found = true;
						}
						break;
					}
				}
				if (!$found) {
					$headers[] = $sectionHeader;
				}
			}
		}

		// сортировка столбцов с сохранением исходной позиции в
		// массиве для развнозначных элементов
		// массив $headers модифицируется
		$this->mergeSortHeader($headers);

		$this->list->AddHeaders($headers);
		$visibleColumns = $this->list->GetVisibleHeaderColumns();

		$modelClass = $this->getModel();
		$elementFields = array_keys($modelClass::getEntity()->getFields());

		if ($sectionEditHelper) {
			$sectionsVisibleColumns = array();
			foreach ($visibleColumns as $k => $v) {
				if (isset($this->sectionFields[$v])) {
					if(!in_array($v, $elementFields)){
						unset($visibleColumns[$k]);
					}
					if (!isset($this->sectionFields[$v]['LIST']) || $this->sectionFields[$v]['LIST'] !== false) {
						$sectionsVisibleColumns[] = $v;
					}
				}
			}
			$visibleColumns = array_values($visibleColumns);
			$visibleColumns = array_merge($visibleColumns, array_keys($this->tableColumnsMap));
		}

		$className = static::getModel();
		$visibleColumns[] = $this->pk();
		$sectionsVisibleColumns[] = $this->sectionPk();

		$raw = array(
			'SELECT' => $visibleColumns,
			'FILTER' => $this->arFilter,
			'SORT' => $sort
		);

		foreach ($this->fields as $name => $settings) {
			$key = array_search($name, $visibleColumns);
			if ((isset($settings['VIRTUAL']) AND $settings['VIRTUAL'] == true)) {
				unset($visibleColumns[$key]);
				unset($this->arFilter[$name]);
				unset($sort[$name]);
			}
			if (isset($settings['LIST']) && $settings['LIST'] === false) {
				unset($visibleColumns[$key]);
			}
			if (isset($settings['FORCE_SELECT']) AND $settings['FORCE_SELECT'] == true) {
				$visibleColumns[] = $name;
			}
		}

		$visibleColumns = array_unique($visibleColumns);
		$sectionsVisibleColumns = array_unique($sectionsVisibleColumns);

		foreach ($this->fields as $code => $settings) {
            if($_REQUEST['del_filter'] !== 'Y') {
                $widget = $this->createWidgetForField($code);
                $widget->changeGetListOptions($this->arFilter, $visibleColumns, $sort, $raw);
            }
			// Множественные поля не должны быть в селекте
			if (!empty($settings['MULTIPLE'])) {
            	$visibleColumns = array_diff($visibleColumns, array($code));
			}
		}

		if ($sectionEditHelper) // Вывод разделов и элементов в одном списке
		{
			$mixedData = $this->getMixedData($sectionsVisibleColumns, $visibleColumns, $sort, $raw);
			$res = new \CDbResult;
			$res->InitFromArray($mixedData);
			$res = new \CAdminResult($res, $this->getListTableID());
			$res->nSelectedCount = $this->totalRowsCount;
			// используем кастомный NavStart что бы определить правильное количество страниц и элементов в списке
			$this->customNavStart($res);
			$this->list->NavText($res->GetNavPrint(Loc::getMessage("PAGES")));
			while ($data = $res->NavNext(false)) {
				$this->modifyRowData($data);
				if ($data['IS_SECTION']) // для разделов своя обработка
				{
					list($link, $name) = $this->getRow($data, $this->getHelperClass(AdminSectionEditHelper::className()));
					$row = $this->list->AddRow('s' . $data[$this->pk()], $data, $link, $name);
					foreach ($this->sectionFields as $code => $settings) {
						if (in_array($code, $sectionsVisibleColumns)) {
							$this->addRowSectionCell($row, $code, $data);
						}
					}
					$row->AddActions($this->getRowActions($data, true));
				}
				else // для элементов своя
				{
					$this->modifyRowData($data);
					list($link, $name) = $this->getRow($data);
					// объединение полей элемента с полями раздела
					foreach ($this->tableColumnsMap as $elementCode => $sectionCode) {
						if (isset($data[$elementCode])) {
							$data[$sectionCode] = $data[$elementCode];
						}
					}
					$row = $this->list->AddRow($data[$this->pk()], $data, $link, $name);
					foreach ($this->fields as $code => $settings) {
						if(in_array($code, $visibleColumns)) {
							$this->addRowCell($row, $code, $data,
							isset($this->tableColumnsMap[$code]) ? $this->tableColumnsMap[$code] : false);
						}
					}
					$row->AddActions($this->getRowActions($data));
				}
			}
		}
		else // Обычный вывод элементов без использования разделов
		{
			$this->totalRowsCount = $className::getCount($this->getElementsFilter($this->arFilter));
			$res = $this->getData($className, $this->arFilter, $visibleColumns, $sort, $raw);
			$res = new \CAdminResult($res, $this->getListTableID());
			$this->customNavStart($res);
			// отключаем отображение всех элементов, если установлено св-во
			$res->bShowAll = $this->showAll;
			$this->list->NavText($res->GetNavPrint(Loc::getMessage("PAGES")));
			while ($data = $res->NavNext(false)) {
				$this->modifyRowData($data);
				list($link, $name) = $this->getRow($data);
				$row = $this->list->AddRow($data[$this->pk()], $data, $link, $name);
				foreach ($this->fields as $code => $settings) {
					if(in_array($code, $visibleColumns)) {
						$this->addRowCell($row, $code, $data);
					}
				}
				$row->AddActions($this->getRowActions($data));
			}
		}

		$this->list->AddFooter($this->getFooter($res));
		$this->list->AddGroupActionTable($this->getGroupActions(), $this->groupActionsParams);
		$this->list->AddAdminContextMenu($this->getContextMenu(), $this->exportExcel);

		$this->list->BeginPrologContent();
		echo $this->prologHtml;
		$this->list->EndPrologContent();

		$this->list->BeginEpilogContent();
		echo $this->epilogHtml;
		$this->list->EndEpilogContent();

		// добавляем ошибки в CAdminList для режимов list и frame
		$errors = $this->getErrors();
		if(in_array($_GET['mode'], array('list','frame')) && is_array($errors)) {
			foreach($errors as $error) {
				$this->list->addGroupError($error);
			}
		}

		$this->list->CheckListMode();
	}

	/**
	 * Функция сортировки столбцов c сохранением порядка равнозначных элементов
	 * @param $array
	 */
	protected function mergeSortHeader(&$array)
	{
		// для сортировки нужно хотя бы 2 элемента
		if (count($array) < 2) return;

		// делим массив пополам
		$halfway = count($array) / 2;
		$array1 = array_slice($array, 0, $halfway);
		$array2 = array_slice($array, $halfway);

		// реукрсивно сортируем каждую половину
		$this->mergeSortHeader($array1);
		$this->mergeSortHeader($array2);

		// если последний элемент первой половины меньше или равен первому элементу
		// второй половины, то просто соединяем массивы
		if ($this->mergeSortHeaderCompare(end($array1), $array2[0]) < 1) {
			$array = array_merge($array1, $array2);
			return;
		}

		// соединяем 2 отсортированных половины в один отсортированный массив
		$array = array();
		$ptr1 = $ptr2 = 0;
		while ($ptr1 < count($array1) && $ptr2 < count($array2)) {
			// собираем в 1 массив последовательную цепочку
			// элементов из 2-х отсортированных половинок
			if ($this->mergeSortHeaderCompare($array1[$ptr1], $array2[$ptr2]) < 1) {
				$array[] = $array1[$ptr1++];
			}
			else {
				$array[] = $array2[$ptr2++];
			}
		}

		// если в исходных массивах что-то осталось забираем в основной массив
		while ($ptr1 < count($array1)) $array[] = $array1[$ptr1++];
		while ($ptr2 < count($array2)) $array[] = $array2[$ptr2++];

		return;
	}

	/**
	 * Функция сравнения столбцов по их весу в сортировке
	 * @param $a
	 * @param $b
	 * @return int
	 */
	public function mergeSortHeaderCompare($a, $b)
	{
		$a = $a['admin_list_helper_sort'];
		$b = $b['admin_list_helper_sort'];
		if ($a == $b) {
			return 0;
		}

		return ($a < $b) ? -1 : 1;
	}

	/**
	 * Получение смешанного списка из разделов и элементов.
	 *
	 * @param $sectionsVisibleColumns
	 * @param $elementVisibleColumns
	 * @param $sort
	 * @param $raw
	 * @return array
	 */
	protected function getMixedData($sectionsVisibleColumns, $elementVisibleColumns, $sort, $raw)
	{
		$sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
		$elementEditHelperClass = $this->getHelperClass(AdminEditHelper::className());
		$sectionField = $sectionEditHelperClass::getSectionField();
		$sectionId = $_GET['SECTION_ID'] ? $_GET['SECTION_ID'] : $_GET['ID'];
		$returnData = array();
		/**
		 * @var DataManager $sectionModel
		 */
		$sectionModel = $sectionEditHelperClass::getModel();
		$sectionFilter = array();

		// добавляем из фильтра те поля которые есть у разделов
		foreach ($this->arFilter as $field => $value) {
			$fieldName = $this->escapeFilterFieldName($field);

			if(!empty($this->tableColumnsMap[$fieldName])) {
				$field = str_replace($fieldName, $this->tableColumnsMap[$fieldName], $field);
				$fieldName = $this->tableColumnsMap[$fieldName];
			}

			if (isset($this->sectionFields[$fieldName])) {
				$sectionFilter[$field] = $value;
			}
		}

		$sectionFilter[$sectionField] = $sectionId;

		$raw['SELECT'] = array_unique($raw['SELECT']);

		// при использовании в качестве popup окна исключаем раздел из выборке
		// что бы не было возможности сделать раздел родителем самого себя
		if (!empty($_REQUEST['self_id'])) {
			$sectionFilter['!' . $this->sectionPk()] = $_REQUEST['self_id'];
		}

		$sectionSort = array();
		$limitData = $this->getLimits();
		// добавляем к общему количеству элементов количество разделов
		$this->totalRowsCount = $sectionModel::getCount($this->getSectionsFilter($sectionFilter));
		foreach ($sort as $field => $direction) {
			if (in_array($field, $sectionsVisibleColumns)) {
				$sectionSort[$field] = $direction;
			}
		}
		// добавляем к выборке разделы
		$rsSections = $sectionModel::getList(array(
			'filter' => $this->getSectionsFilter($sectionFilter),
			'select' => $sectionsVisibleColumns,
			'order' => $sectionSort,
			'limit' => $limitData[1],
			'offset' => $limitData[0],
		));

		while ($section = $rsSections->fetch()) {
			$section['IS_SECTION'] = true;
			$returnData[] = $section;
		}

		// расчитываем offset и limit для элементов
		if (count($returnData) > 0) {
			$elementOffset = 0;
		}
		else {
			$elementOffset = $limitData[0] - $this->totalRowsCount;
		}

		// для списка разделов элементы не нужны
		if (static::getHelperClass(AdminSectionListHelper::className()) == static::className()) {
			return $returnData;
		}

		$elementLimit = $limitData[1] - count($returnData);
		$elementModel = static::$model;
		$elementFilter = $this->arFilter;
		if(get_called_class() != static::getHelperClass(AdminSectionListHelper::className())) {
            $elementFilter[$elementEditHelperClass::getSectionField()] = $sectionId;
        }
		// добавляем к общему количеству элементов количество элементов
		$this->totalRowsCount += $elementModel::getCount($this->getElementsFilter($elementFilter));

		// возвращае данные без элементов если разделы занимают всю страницу выборки
		if (!empty($returnData) && $limitData[0] == 0 && $limitData[1] == $this->totalRowsCount) {
			return $returnData;
		}

		$elementSort = array();
		foreach ($sort as $field => $direction) {
			if (in_array($field, $elementVisibleColumns)) {
				$elementSort[$field] = $direction;
			}
		}

		$elementParams = array(
			'filter' => $this->getElementsFilter($elementFilter),
			'select' => $elementVisibleColumns,
			'order' => $elementSort,
		);
		if ($elementLimit > 0 && $elementOffset >= 0) {
			$elementParams['limit'] = $elementLimit;
			$elementParams['offset'] = $elementOffset;
			// добавляем к выборке элементы
			$rsSections = $elementModel::getList($elementParams);

			while ($element = $rsSections->fetch()) {
				$element['IS_SECTION'] = false;
				$returnData[] = $element;
			}
		}

		/**
		 * Вернем результат с первой страницы если на текущей нет элементов.
		 * Для списка элементов аналогичная проверка есть в $this->getLimits()
		 */
		if (!count($returnData) && $this->totalRowsCount > 0)
		{
			$this->navParams['navParams']['PAGEN'] = 1;
			return $this->getMixedData($sectionsVisibleColumns, $elementVisibleColumns, $sort, $raw);
		}

		return $returnData;
	}

	/**
	 * Огранчения выборки из CAdminResult
	 * @return array
	 */
	protected function getLimits()
	{
		if ($this->navParams['navParams']['SHOW_ALL']) {
			return array();
		}
		else {
			if (!intval($this->navParams['navParams']['PAGEN']) OR !isset($this->navParams['navParams']['PAGEN'])) {
				$this->navParams['navParams']['PAGEN'] = 1;
			}
			$from = $this->navParams['nPageSize'] * ((int)$this->navParams['navParams']['PAGEN'] - 1);

			/**
			 * Вернем результат с первой страницы если на текущей нет элементов.
			 *
			 * $this->totalRowsCount еще не заполнен при смешанном отображении элементов и разделов,
			 * в $this->>getMixedData() есть отдельная проверка на этот счет
			 */
			if ($this->totalRowsCount && $from >= $this->totalRowsCount)
			{
				$this->navParams['navParams']['PAGEN'] = 1;
				$from = 0;
			}

			return array($from, $this->navParams['nPageSize']);
		}
	}

	/**
	 * Очищает название поля от операторов фильтра
	 * @param string $fieldName названия поля из фильтра
	 * @return string название поля без без операторов фильтра
	 */
	protected function escapeFilterFieldName($fieldName)
	{
		return str_replace(array('!','<', '<=', '>', '>=', '><', '=', '%'), '', $fieldName);
	}

	/**
	 * Выполняет CDBResult::NavNext с той разницей, что общее количество элементов берется не из count($arResult),
	 * а из нашего параметра, полученного из SQL-запроса.
	 * array_slice также не делается.
	 *
	 * @param \CAdminResult $res
	 */
	protected function customNavStart(&$res)
	{
		$res->NavStart($this->navParams['nPageSize'],
			$this->navParams['navParams']['SHOW_ALL'],
			(int)$this->navParams['navParams']['PAGEN']
		);
		// отключаем отображение всех элементов
		$res->bShowAll = $this->showAll;

		$res->NavRecordCount = $this->totalRowsCount;
		if ($res->NavRecordCount < 1)
			return;

		if ($res->NavShowAll)
			$res->NavPageSize = $res->NavRecordCount;

		$res->NavPageCount = floor($res->NavRecordCount / $res->NavPageSize);
		if ($res->NavRecordCount % $res->NavPageSize > 0)
			$res->NavPageCount++;

		$res->NavPageNomer =
			($res->PAGEN < 1 || $res->PAGEN > $res->NavPageCount
				?
				(\CPageOption::GetOptionString("main", "nav_page_in_session", "Y") != "Y"
				|| $_SESSION[$res->SESS_PAGEN] < 1
				|| $_SESSION[$res->SESS_PAGEN] > $res->NavPageCount
					?
					1
					:
					$_SESSION[$res->SESS_PAGEN]
				)
				:
				$res->PAGEN
			);
	}

	/**
	 * Преобразует данные строки, перед тем как добавлять их в список.
	 *
	 * @param $data
	 *
	 * @see AdminListHelper::getList()
	 *
	 * @api
	 */
	protected function modifyRowData(&$data)
	{
	}

	/**
	 * Настройки строки таблицы.
	 *
	 * @param array $data Данные текущей строки БД.
	 * @param bool|string $class Класс хелпера через метод getUrl которого идет получение ссылки.
	 *
	 * @return array Возвращает ссылку на детальную страницу и её название.
	 *
	 * @api
	 */
	protected function getRow($data, $class = false)
	{
		if (empty($class)) {
			$class = static::getHelperClass(AdminEditHelper::className());
		}
		if ($this->isPopup()) {
			return array();
		}
		else {
			$query = array_merge($this->additionalUrlParams, array(
				'lang' => LANGUAGE_ID,
				$this->pk() => $data[$this->pk()]
			));

			return array($class::getUrl($query));
		}
	}

	/**
	 * Для каждой ячейки(раздела) таблицы создаёт виджет соответствующего типа.
	 * Виджет подготавливает необходимый HTML для списка.
	 *
	 * @param \CAdminListRow $row
	 * @param $code Сивольный код поля.
	 * @param $data Данные текущей строки.
	 *
	 * @throws Exception
	 *
	 * @see HelperWidget::generateRow()
	 */
	protected function addRowSectionCell($row, $code, $data)
	{
		$sectionEditHelper = $this->getHelperClass(AdminSectionEditHelper::className());
		if (!isset($this->sectionFields[$code]['WIDGET'])) {
			$error = str_replace('#CODE#', $code, 'Can\'t create widget for the code "#CODE#"');
			throw new Exception($error, Exception::CODE_NO_WIDGET);
		}

		/**
		 * @var \DigitalWand\AdminHelper\Widget\HelperWidget $widget
		 */
		$widget = $this->sectionFields[$code]['WIDGET'];

		$widget->setHelper($this);
		$widget->setCode($code);
		$widget->setData($data);
		$widget->setEntityName($sectionEditHelper::getModel());

		$this->setContext(AdminListHelper::OP_ADD_ROW_CELL);
		$widget->generateRow($row, $data);
	}

	/**
	 * Возвращает массив со списком действий при клике правой клавишей мыши на строке таблицы
	 * По-умолчанию:
	 * <ul>
	 * <li> Редактировать элемент </li>
	 * <li> Удалить элемент </li>
	 * <li> Если это всплывающее окно - запустить кастомную JS-функцию. </li>
	 * </ul>
	 *
	 * @param $data Данные текущей строки.
	 * @param $section Признак списка для раздела.
	 *
	 * @return array
	 *
	 * @see CAdminListRow::AddActions
	 *
	 * @api
	 */
	protected function getRowActions($data, $section = false)
	{
		$actions = array();

		if ($this->isPopup()) {
			$jsData = \CUtil::PhpToJSObject($data);
			$actions['select'] = array(
				'ICON' => 'select',
				'DEFAULT' => true,
				'TEXT' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_SELECT'),
				"ACTION" => 'javascript:' . $this->popupClickFunctionName . '(' . $jsData . ')'
			);
		}
		else {
			$viewQueryString = 'module=' . static::getModule() . '&view=' . static::getViewName() . '&entity=' . static::getEntityCode();
			$query = array_merge($this->additionalUrlParams,
				array($this->pk() => $data[$this->pk()]));
			if ($this->hasWriteRights()) {
				$sectionHelperClass = static::getHelperClass(AdminSectionEditHelper::className());
				$editHelperClass = static::getHelperClass(AdminEditHelper::className());

				$actions['edit'] = array(
					'ICON' => 'edit',
					'DEFAULT' => true,
					'TEXT' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_EDIT'),
					'ACTION' => $this->list->ActionRedirect($section ? $sectionHelperClass::getUrl($query) : $editHelperClass::getUrl($query))
				);
			}
			if ($this->hasDeleteRights()) {
				$actions['delete'] = array(
					'ICON' => 'delete',
					'TEXT' => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE"),
					'ACTION' => "if(confirm('" . Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_CONFIRM') . "')) " . $this->list->ActionDoGroup($data[$this->pk()],
							$section ? "delete-section" : "delete", $viewQueryString)
				);
			}
		}

		return $actions;
	}

	/**
	 * Для каждой ячейки таблицы создаёт виджет соответствующего типа. Виджет подготавливает необходимый HTML-код
	 * для списка.
	 *
	 * @param \CAdminListRow $row Объект строки списка записей.
	 * @param string $code Сивольный код поля.
	 * @param array $data Данные текущей строки.
	 * @param bool $virtualCode
	 *
	 * @throws Exception
	 *
	 * @see HelperWidget::generateRow()
	 */
	protected function addRowCell($row, $code, $data, $virtualCode = false)
	{
		$widget = $this->createWidgetForField($code, $data);
		$this->setContext(AdminListHelper::OP_ADD_ROW_CELL);

		// устанавливаем виртуальный код ячейки, используется при слиянии столбцов
		if ($virtualCode) {
			$widget->setCode($virtualCode);
		}

		$widget->generateRow($row, $data);

		if ($virtualCode) {
			$widget->setCode($code);
		}
	}

	/**
	 * Производит выборку данных. Функцию стоит переопределить в случае, если необходима своя логика, и её нельзя
	 * вынести в класс модели.
	 *
	 * @param DataManager $className
	 * @param array $filter
	 * @param array $select
	 * @param array $sort
	 * @param array $raw
	 *
	 * @return Result
	 *
	 * @api
	 */
	protected function getData($className, $filter, $select, $sort, $raw)
	{
		$limits = $this->getLimits();
		$parameters = array(
			'filter' => $this->getElementsFilter($filter),
			'select' => $select,
			'order' => $sort,
			'offset' => $limits[0],
			'limit' => $limits[1],
		);

		/** @var Result $res */
		$res = $className::getList($parameters);

		return $res;
	}

	/**
	 * Подготавливает массив с настройками футера таблицы Bitrix
	 * @param \CAdminResult $res - результат выборки данных
	 * @see \CAdminList::AddFooter()
	 * @return array[]
	 */
	protected function getFooter($res)
	{
		return array(
			$this->getButton('MAIN_ADMIN_LIST_SELECTED', array("value" => $res->SelectedRowsCount())),
			$this->getButton('MAIN_ADMIN_LIST_CHECKED', array("value" => $res->SelectedRowsCount()), array(
				"counter" => true,
				"value" => "0",
			)),
		);
	}

	/**
	 * Выводит форму фильтрации списка
	 */
	public function createFilterForm()
	{
		//нужно пробрасывать параметр popup в форму, если она является таковой
		if($this->isPopup())
		{
			$this->additionalUrlParams['popup'] = 'Y';
		}

		$this->setContext(AdminListHelper::OP_CREATE_FILTER_FORM);
		print ' <form name="find_form" method="GET" action="' . static::getUrl($this->additionalUrlParams) . '?">';

		$sectionHelper = $this->getHelperClass(AdminSectionEditHelper::className());
		if($sectionHelper) {
			$sectionsInterfaceSettings = static::getInterfaceSettings($sectionHelper::getViewName());
			foreach($this->arFilterOpts as $code => $name) {
				if(!empty($this->tableColumnsMap[$code])) {
                    $newName = $sectionsInterfaceSettings['FIELDS'][$this->tableColumnsMap[$code]]['WIDGET']
                        ->getSettings('TITLE');
                    $this->arFilterOpts[$code] = $newName;
				}
			}
		}

		$oFilter = new \CAdminFilter($this->getListTableID() . '_filter', $this->arFilterOpts);
		$oFilter->Begin();

		foreach ($this->arFilterOpts as $code => $name) {
			$widget = $this->createWidgetForField($code);
			if($widget->getSettings('TITLE') != $this->arFilterOpts[$code]) {
				$widget->setSetting('TITLE', $this->arFilterOpts[$code]);
			}
			$widget->showFilterHtml();
		}

		$oFilter->Buttons(array(
			"table_id" => $this->getListTableID(),
			"url" => static::getUrl($this->additionalUrlParams),
			"form" => "find_form",
		));
		$oFilter->End();

		print '</form>';
	}

	/**
	 * Возвращает ID таблицы, который не должен конфликтовать с ID в других разделах админки, а также нормально
	 * парситься в JS
	 *
	 * @return string
	 */
	protected function getListTableID()
	{
		return str_replace('.', '', static::$tablePrefix . $this->table());
	}

	/**
	 * Выводит сформированный список.
	 * Сохраняет обработанный GET-запрос в сессию
	 */
	public function show()
	{
		if (!$this->hasReadRights()) {
			$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_ACCESS_FORBIDDEN'));
			$this->showMessages();

			return false;
		}
		$this->showMessages();
		$this->list->DisplayList();

		if ($this->isPopup()) {
			print $this->popupClickFunctionCode;
		}

		$this->saveGetQuery();
	}

	/**
	 * Сохраняет параметры запроса для поторного использования после возврата с других страниц (к примеру, после
	 * перехода с детальной обратно в список - чтобы вернуться в точности в тот раздел, с которого ранее ушли)
	 */
	private function saveGetQuery()
	{
		$_SESSION['LAST_GET_QUERY'][get_called_class()] = $_GET;
	}

	/**
	 * Восстанавливает последний GET-запрос, если в текущем задан параметр restore_query=Y
	 */
	private function restoreLastGetQuery()
	{
		if (!isset($_SESSION['LAST_GET_QUERY'][get_called_class()])
			OR !isset($_REQUEST['restore_query'])
			OR $_REQUEST['restore_query'] != 'Y'
		) {
			return;
		}

		$_GET = array_merge($_GET, $_SESSION['LAST_GET_QUERY'][get_called_class()]);
		$_REQUEST = array_merge($_REQUEST, $_SESSION['LAST_GET_QUERY'][get_called_class()]);
	}

	/**
	 * @inheritdoc
	 */
	public static function getUrl(array $params = array())
	{
		return static::getViewURL(static::getViewName(), static::$listPageUrl, $params);
	}

	/**
	 * Кастомизация фильтра разделов
	 * @param $filter
	 * @return mixed
	 */
	protected function getSectionsFilter(array $filter)
	{
		return $filter;
	}

	/**
	 * Кастомизация фильтра элементов
	 * @param $filter
	 * @return mixed
	 */
	protected function getElementsFilter($filter)
	{
		return $filter;
	}

	/**
	 * Список идентификаторов для групповых операций
	 *
	 * @return array
	 */
	protected function getIds()
	{
		$className = static::getModel();
		if (isset($_REQUEST['model'])) {
			$className = $_REQUEST['model'];
		}

		$sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
		if ($sectionEditHelperClass && !isset($_REQUEST['model-section'])) {
			$sectionClassName = $sectionEditHelperClass::getModel();
		}
		else {
			$sectionClassName = $_REQUEST['model-section'];
		}

        $pkValue = $this->getPk();
        if (isset($pkValue[$this->pk()]) && is_array($pkValue[$this->pk()])) {
			foreach ($pkValue[$this->pk()] as $id) {
				$class = strpos($id, 's') === 0 ? $sectionClassName : $className;
				$ids[] = $this->getCommonPrimaryFilterById($class, null, $id);
			}
		} else {
			$ids = array($this->getPk());
		}

		return $ids;
	}

	/**
	 * Получить оставшуюся часть составного первичного ключа
	 *
	 * @param $className
	 * @param null $sectionClassName
	 * @param $id
	 * @return array
	 */
	protected function getCommonPrimaryFilterById($className, $sectionClassName = null, $id)
	{
		if ($this->getHelperClass($sectionClassName) && strpos($id, 's') === 0) {
			$primary = $sectionClassName::getEntity()->getPrimaryArray();
		} else {
			$primary = $className::getEntity()->getPrimaryArray();
		}

		if (count($primary) === 1) {
			return array($this->pk() => $id);
		}

		$key = $this->getPk();
		$key[$this->pk()] = $id;

		return $key;
	}
}


================================================
FILE: lib/helper/AdminSectionEditHelper.php
================================================
<?php

namespace DigitalWand\AdminHelper\Helper;

/**
 * Класс-обертка для хелпера редактирования разделов.
 * 
 * Все хелперы отвечающие за редактирование разделов должны наследовать от этого класса. Название класса используется 
 * для определения к какому типу принадлежит хелпер:
 * - список элементов,
 * - редактирования элементов,
 * - список разделов,
 * - редактирование раздела.
 * 
 * @see AdminBaseHelper::getHelperClass
 * 
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 * @author Artem Yarygin <artx19@yandex.ru>
 */
class AdminSectionEditHelper extends AdminEditHelper
{
}

================================================
FILE: lib/helper/AdminSectionListHelper.php
================================================
<?php

namespace DigitalWand\AdminHelper\Helper;

/**
 * Класс-обертка для хелпера списка разделов.
 * 
 * Все хелперы отвечающие за вывод списка разделов должны наследовать от этого класса.
 * 
 * @see AdminSectionEditHelper
 * 
 * @author Nik Samokhvalov <nik@samokhvalov.info>
 * @author Artem Yarygin <artx19@yandex.ru>
 */
class AdminSectionListHelper extends AdminListHelper
{
}

================================================
FILE: lib/helper/Exception.php
================================================
<?php

namespace DigitalWand\AdminHelper\Helper;

class Exception extends \Exception
{
	const CODE_NO_WIDGET = 1;
	const CODE_NO_HL_ENTITY_INFORMATION = 2;

}

================================================
FILE: lib/widget/CheckboxWidget.php
================================================
<?php

namespace DigitalWand\AdminHelper\Widget;

use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);

/**
 * Виджет "галочка"
 *
 * Доступные опции:
 * <ul>
 * <li> <b>FIELD_TYPE</b> - Тип данных для хранения булевых значений (строка, целые числа, булево) </li>
 * </ul>
 */
class CheckboxWidget extends HelperWidget
{
    /**
     * Строковый тип чекбокса (Y/N)
     * FIXME: если верить битриксу, мжет быть ещё и экзотичный случай со строками "true" и "false"!
     */
    const TYPE_STRING = 'string';
    /**
     * Целочисленный тип чекбокса (1/0)
     */
    const TYPE_INT = 'integer';
    /**
     * Булевый тип чекбокса
     */
    const TYPE_BOOLEAN = 'boolean';
    /**
     * Значение положительного варианта для строкового чекбокса
     */
    const TYPE_STRING_YES = 'Y';
    /**
     * Значение отрицательного варианта для строкового чекбокса
     */
    const TYPE_STRING_NO = 'N';
    /**
     * Значение положительного варианта для целочисленного чекбокса
     */
    const TYPE_INT_YES = 1;
    /**
     * Значение отрицательного варианта для целочисленного чекбокса
     */
    const TYPE_INT_NO = 0;

    protected static $defaults = array(
        'EDIT_IN_LIST' => true
    );

    /**
     * @inheritdoc
     */
    public function generateRow(&$row, $data)
    {
        $modeType = $this->getCheckboxType();

        $globalYes = '';
        $globalNo = '';

        switch ($modeType) {
            case self::TYPE_STRING: {
                $globalYes = self::TYPE_STRING_YES;
                $globalNo = self::TYPE_STRING_NO;
                break;
            }
            case self::TYPE_INT:
            case self::TYPE_BOOLEAN: {
                $globalYes = self::TYPE_INT_YES;
                $globalNo = self::TYPE_INT_NO;
                break;
            }
        }

        if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) {
            $checked = intval($this->getValue() == $globalYes) ? 'checked' : '';
            $js = 'var input = document.getElementsByName(\'' . $this->getEditableListInputName() . '\')[0];
                   input.value = this.checked ? \'' . $globalYes . '\' : \'' . $globalNo . '\';';
            $editHtml = '<input type="checkbox"
                                value="' . static::prepareToTagAttr($this->getValue()) . '" ' . $checked . '
                                onchange="' . $js . '"/>
                         <input type="hidden"
                                value="' . static::prepareToTagAttr($this->getValue()) . '"
                                name="' . $this->getEditableListInputName() . '" />';
            $row->AddEditField($this->getCode(), $editHtml);
        }

        if (intval($this->getValue() == $globalYes)) {
            $value = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES');
        } else {
            $value = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO');
        }

        $row->AddViewField($this->getCode(), $value);
    }

    /**
     * @inheritdoc
     */
    public function showFilterHtml()
    {
        $filterHtml = '<tr>';
        $filterHtml .= '<td>' . $this->getSettings('TITLE') . '</td>';
        $filterHtml .= '<td> <select  name="' . $this->getFilterInputName() . '">';
        $filterHtml .= '<option value=""></option>';

        $modeType = $this->getCheckboxType();

        $langYes = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES');
        $langNo = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO');

        switch ($modeType) {
            case self::TYPE_STRING: {
                $filterHtml .= '<option value="' . self::TYPE_STRING_YES . '">' . $langYes . '</option>';
                $filterHtml .= '<option value="' . self::TYPE_STRING_NO . '">' . $langNo . '</option>';
                break;
            }
            case self::TYPE_INT:
            case self::TYPE_BOOLEAN: {
                $filterHtml .= '<option value="' . self::TYPE_INT_YES . '">' . $langYes . '</option>';
                $filterHtml .= '<option value="' . self::TYPE_INT_NO . '">' . $langNo . '</option>';
                break;
            }
        }

        $filterHtml .= '</select></td>';
        $filterHtml .= '</tr>';

        print $filterHtml;
    }

    /**
     * @inheritdoc
     */
    public function getValueReadonly()
    {
        $code = $this->getCode();
        $value = isset($this->data[$code]) ? $this->data[$code] : null;
        $modeType = $this->getCheckboxType();

        switch ($modeType) {
            case static::TYPE_STRING: {
                $value = $value == 'Y' ? Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES') : Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO');
                break;
            }
            case static::TYPE_INT:
            case static::TYPE_BOOLEAN: {
                $value = $value ? Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES') : Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO');
                break;
            }
        }

        return static::prepareToOutput($value);
    }

    /**
     * @inheritdoc
     */
    public function processEditAction()
    {
        parent::processEditAction();

        if ($this->getCheckboxType() === static::TYPE_BOOLEAN) {
            $this->data[$this->getCode()] = (bool)$this->data[$this->getCode()];
        }
    }

    /**
     * @inheritdoc
     */
    protected function getEditHtml()
    {
        $html = '';

        $modeType = $this->getCheckboxType();

        switch ($modeType) {
            case static::TYPE_STRING: {
                $checked = $this->getValue() == self::TYPE_STRING_YES ? 'checked' : '';

                $html = '<input type="hidden" name="' . $this->getEditInputName() . '" value="' . self::TYPE_STRING_NO . '" />';
                $html .= '<input type="checkbox" name="' . $this->getEditInputName() . '" value="' . self::TYPE_STRING_YES . '" ' . $checked . ' />';
                break;
            }
            case static::TYPE_INT:
            case static::TYPE_BOOLEAN: {
                $checked = $this->getValue() == self::TYPE_INT_YES ? 'checked' : '';

                $html = '<input type="hidden" name="' . $this->getEditInputName() . '" value="' . self::TYPE_INT_NO . '" />';
                $html .= '<input type="checkbox" name="' . $this->getEditInputName() . '" value="' . self::TYPE_INT_YES . '" ' . $checked . ' />';
                break;
            }
        }

        return $html;
    }

    /**
     * Получить тип чекбокса по типу поля.
     * По-умолчанию возвращает TYPE_STRING
     * @return mixed
     */
    public function getCheckboxType()
    {
        $settingsFieldType = $this->getSettings('FIELD_TYPE');
        $checkTypes = array(static::TYPE_STRING, static::TYPE_BOOLEAN, static::TYPE_INT);
        $columnName = $this->getCode();

        if ($settingsFieldType AND in_array($settingsFieldType, $checkTypes)) {
            return $settingsFieldType;

        } else {
            $entity = $this->getEntityName();
            $entityMap = $entity::getMap();

            if (!isset($entityMap[$columnName])) {
                foreach ($entityMap as $field/** @var \Bitrix\Main\Entity\ScalarField $field */) {
                    if($field instanceof \Bitrix\Main\Entity\ReferenceField)
                        continue;
                    if (is_object($field) AND $field->getColumnName() === $columnName) {
                        return $field->getDataType(); //FIXME: deprecated? На что нужно заменить?
                    }
                }

            } elseif (isset($entityMap[$columnName]['values']) AND
                is_array($entityMap[$columnName]['values']) AND
                count($entityMap[$columnName]['values']) == 2
            ) {
                $value = reset($entityMap[$columnName]['values']);
                if (is_string($value)) {
                    return static::TYPE_STRING;
                } elseif (is_bool($value) OR is_integer($value)) {
                    return static::TYPE_BOOLEAN;
                }

            } elseif (isset($entityMap[$columnName]['data_type'])) {
                return $entityMap[$columnName]['data_type'];

            }

            /**
             * Теоретически, рзработчик мог ввести полную хрень, указывая варианты значений для сущности
             * В этом случае ни одна проверка выше не сработает.
             * FIXME: а нужен ли эксепшн?
             */
//            throw new \Bitrix\Main\ArgumentTypeException("Unknown checkbox type");
        }

        return static::TYPE_STRING;
    }
}


================================================
FILE: lib/widget/ComboBoxWidget.php
================================================
<?php

namespace DigitalWand\AdminHelper\Widget;

use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);

/**
 * Выпадающий список.
 *
 * Доступные опции:
 * <ul>
 * <li> STYLE - inline-стили</li>
 * <li> VARIANTS - массив с вариантами значений или функция для их получения в формате ключ=>заголовок
 *        Например:
 *            [
 *                1=>'Первый пункт',
 *                2=>'Второй пункт'
 *            ]
 * </li>
 * <li> DEFAULT_VARIANT - ID варианта по-умолчанию</li>
 * </ul>
 */
class ComboBoxWidget extends HelperWidget
{
    static protected $defaults = array(
        'EDIT_IN_LIST' => true
    );

    /**
     * @inheritdoc
     *
     * @see AdminEditHelper::showField();
     *
     * @param bool $forFilter
     *
     * @return mixed
     */
    protected function getEditHtml()
    {
        return $this->getComboBox();
    }

    /**
     * @inheritdoc
     */
    protected function getMultipleEditHtml()
    {
        return $this->getComboBox(true);
    }

    /**
     * Возвращает ХТМЛ-код с комбобоксом.
     *
     * @param bool $multiple Множественный режим.
     * @param bool $forFilter Комбобокс будет выводиться в блоке с фильтром.
     *
     * @return string
     */
    protected function getComboBox($multiple = false, $forFilter = false)
    {
        if ($multiple) {
            $value = $this->getMultipleValue();
        } else {
            $value = $this->getValue();
        }

        $style = $this->getSettings('STYLE');

        $variants = $this->getVariants();

        if (!$multiple)
        {
            array_unshift($variants, array(
                'ID' => null,
                'TITLE' => null
            ));
        }

        if (empty($variants)) {
            $comboBox = Loc::getMessage('DIGITALWAND_AH_MISSING_VARIANTS');
        } else {
            $name = $forFilter ? $this->getFilterInputName() : $this->getEditInputName();
            $comboBox = '<select name="' . $name . ($multiple ? '[]' : null) . '"
                '. ($multiple ? 'multiple="multiple"' : null) . '
                style="' . $style . '">';

            foreach ($variants as $variant) {
                $selected = false;

                if ($variant['ID'] == $value) {
                    $selected = true;
                }

                if ($multiple && in_array($variant['ID'], $value)) {
                    $selected = true;
                } elseif ($variant['ID'] === $value) {
                    $selected = true;
                }

                $comboBox .= "<option value='" . static::prepareToTagAttr($variant['ID']) . "' " . ($selected ? "selected" : "") . ">"
                    . static::prepareToTagAttr($variant['TITLE']) . "</option>";
            }

            $comboBox .= '</select>';
        }

        return $comboBox;
    }

    /**
     * @inheritdoc
     */
    protected function getValueReadonly()
    {
        $variants = $this->getVariants();
        $value = $variants[$this->getValue()]['TITLE'];

        return static::prepareToOutput($value);
    }

    /**
     * @inheritdoc
     */
    protected function getMultipleValueReadonly()
    {
        $variants = $this->getVariants();
        $values = $this->getMultipleValue();
        $result = '';

        if (empty($variants)) {
            $result = Loc::getMessage('DIGITALWAND_AH_MISSING_VARIANTS');
        } else {
            foreach ($variants as $id => $data) {
                $name = strlen($data["TITLE"]) > 0 ? $data["TITLE"] : "";

                if (in_array($id, $values)) {
                    $result .= static::prepareToOutput($name) . '<br/>';
                }
            }
        }

        return $result;
    }

    /**
     * Возвращает массив в следующем формате:
     * <code>
     * array(
     *      '123' => array('ID' => 123, 'TITLE' => 'ololo'),
     *      '456' => array('ID' => 456, 'TITLE' => 'blablabla'),
     *      '789' => array('ID' => 789, 'TITLE' => 'pish-pish'),
     * )
     * </code>
     * 
     * Результат будет выводиться в комбобоксе.
     * @return array
     */
    protected function getVariants()
    {
        $variants = $this->getSettings('VARIANTS');

        if (is_callable($variants)) {
            $var = $variants();

            if (is_array($var)) {
                return $this->formatVariants($var);
            }
        }elseif (is_array($variants) AND !empty($variants)) {
            return $this->formatVariants($variants);
        }

        return array();
    }

    /**
     * Приводит варианты к нужному формату, если они заданы в виде одномерного массива.
     *
     * @param $variants
     *
     * @return array
     */
    protected function formatVariants($variants)
    {
        $formatted = array();

        foreach ($variants as $id => $data) {
            if (!is_array($data)) {
                $formatted[$id] = array(
                    'ID' => $id,
                    'TITLE' => $data
                );
            }
        }

        return $formatted;
    }

    /**
     * @inheritdoc
     */
    public function generateRow(&$row, $data)
    {
        if ($this->settings['EDIT_IN_LIST'] AND !$this->settings['READONLY']) {
            $row->AddInputField($this->getCode(), array('style' => 'width:90%'));
        } else {
            $row->AddViewField($this->getCode(), $this->getValueReadonly());
        }
    }

    /**
     * @inheritdoc
     */
    public function showFilterHtml()
    {
        print '<tr>';
        print '<td>' . $this->getSettings('TITLE') . '</td>';
        print '<td>' . $this->getComboBox(false, true) . '</td>';
        print '</tr>';
    }

    /**
     * @inheritdoc
     */
    public function processEditAction()
    {
        if ($this->getSettings('MULTIPLE')) {
            $sphere = $this->data[$this->getCode()];
            unset($this->data[$this->getCode()]);

            foreach ($sphere as $sphereKey) {
                $this->data[$this->getCode()][] = array('VALUE' => $sphereKey);
            }
        }

        parent::processEditAction();
    }
}


================================================
FILE: lib/widget/DateTimeWidget.php
================================================
<?php

namespace DigitalWand\AdminHelper\Widget;

class DateTimeWidget extends HelperWidget
{
	static protected $defaults = array(
		'FILTER' => 'BETWEEN',
	);
	
	/**
	 * Генерирует HTML для редактирования поля
	 * @see AdminEditHelper::showField();
	 * @return mixed
	 */
	protected function getEditHtml()
	{
		return \CAdminCalendar::CalendarDate($this->getEditInputName(), ConvertTimeStamp(strtotime($this->getValue()), "FULL"), 10, true);
	}

	/**
	 * Генерирует HTML для поля в списке
	 * @see AdminListHelper::addRowCell();
	 * @param CAdminListRow $row
	 * @param array $data - данные текущей строки
	 * @return mixed
	 */
	public function generateRow(&$row, $data)
	{
		if (isset($this->settings['EDIT_IN_LIST']) AND $this->settings['EDIT_IN_LIST'])
		{
			$row->AddCalendarField($this->getCode());
		}
		else
		{
			$arDate = ParseDateTime($this->getValue());

			if ($arDate['YYYY'] < 10)
			{
				$stDate = '-';
			}
			else
			{
				$stDate = ConvertDateTime($this->getValue(), "DD.MM.YYYY HH:MI:SS", "ru");
			}

			$row->AddViewField($this->getCode(), $stDate);
		}
	}

	/**
	 * Генерирует HTML для поля фильтрации
	 * @see AdminListHelper::createFilterForm();
	 * @return mixed
	 */
	public function showFilterHtml()
	{
		list($inputNameFrom, $inputNameTo) = $this->getFilterInputName();
		print '<tr>';
		print '<td>' . $this->settings['TITLE'] . '</td>';
		print '<td width="0%" nowrap>' . CalendarPeriod($inputNameFrom, $$inputNameFrom, $inputNameTo, $$inputNameTo, "find_form") . '</td>';
	}

	/**
	 * Сконвертируем дату в формат Mysql
	 * @return boolean
	 */
	public function processEditAction()
	{
		try
		{
			$this->setValue(new \Bitrix\Main\Type\Datetime($this->getValue()));
		} catch (\Exception $e)
		{
		}
		if (!$this->checkRequired())
		{
			$this->addError('REQUIRED_FIELD_ERROR');
		}
	}
}


================================================
FILE: lib/widget/FileWidget.php
================================================
<?php

namespace DigitalWand\AdminHelper\Widget;

use Bitrix\Main\Localization\Loc;
use Bitrix\Main\UI\FileInput;
use Bitrix\Main\Application;
use CTempFile;

/**
 * Для множественного поля в таблице должен быть столбец FILE_ID.
 * Настройки класса:
 * <ul>
 * <li><b>DESCRIPTION_FIELD</b> - bool нужно ли поле описания</li>
 * <li><b>MULTIPLE</b> - bool является ли поле множественным</li>
 * <li><b>IMAGE</b> - bool отображать ли изображение файла, для старого вида отображения</li>
 * </ul>
 */
class FileWidget extends HelperWidget
{
    protected static $defaults = array(
        'IMAGE' => false,
        'DESCRIPTION_FIELD' => false,
        'EDIT_IN_LIST' => false,
        'FILTER' => false,
        'UPLOAD' => true,
        'MEDIALIB' => true,
        'FILE_DIALOG' => true,
        'CLOUD' => true,
        'DELETE' => true,
        'EDIT' => true,
    );

    /**
     * {@inheritdoc}
     */
    public function __construct(array $settings = array())
    {
        Loc::loadMessages(__FILE__);
        
        parent::__construct($settings);
    }

    /**
     * {@inheritdoc}
     */
    protected function getEditHtml()
    {
        if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) {
            $html = FileInput::createInstance(array(
                'name' => $this->getEditInputName('_FILE'),
                'description' => $this->getSettings('DESCRIPTION_FIELD'),
                'upload' => $this->getSettings('UPLOAD'),
                'allowUpload' => 'I',
                'medialib' => $this->getSettings('MEDIALIB'),
                'fileDialog' => $this->getSettings('FILE_DIALOG'),
                'cloud' => $this->getSettings('CLOUD'),
                'delete' => $this->getSettings('DELETE'),
                'edit' => $this->getSettings('EDIT'),
                'maxCount' => 1
            ))->show($this->getValue());
        } else {
            $html = \CFileInput::Show($this->getEditInputName('_FILE'), ($this->getValue() > 0 ? $this->getValue() : 0),
                array(
                    'IMAGE' => $this->getSettings('IMAGE') === true ? 'Y' : 'N',
                    'PATH' => 'Y',
                    'FILE_SIZE' => 'Y',
                    'ALLOW_UPLOAD' => 'I',
                ), array(
                    'upload' => $this->getSettings('UPLOAD'),
                    'medialib' => $this->getSettings('MEDIALIB'),
                    'file_dialog' => $this->getSettings('FILE_DIALOG'),
                    'cloud' => $this->getSettings('CLOUD'),
                    'del' => $this->getSettings('DELETE'),
                    'description' => $this->getSettings('DESCRIPTION_FIELD'),
                )
            );
        }

        if ($this->getValue()) {
            $html .= '<input type="hidden" name="' . $this->getEditInputName() . '" value=' . $this->getValue() . '>';
        }

        return $html;
    }

    /**
     * {@inheritdoc}
     */
    protected function getMultipleEditHtml()
    {
        $inputHidden = array();
        $inputName = array();
        
        if (!empty($this->data['ID'])) {
            $entityName = $this->entityName;
            
            $rsEntityData = $entityName::getList(array(
                'select' => array('REFERENCE_' => $this->getCode() . '.*'),
                'filter' => array('=ID' => $this->data['ID'])
            ));

            while ($referenceData = $rsEntityData->fetch()) {
                $inputName[$this->code . '[' . $referenceData['REFERENCE_ID'] . ']'] = $referenceData['REFERENCE_VALUE'];
                $inputHidden[$referenceData['REFERENCE_ID']] = $referenceData['REFERENCE_VALUE'];
            }
        }

        if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) {
            $html = \Bitrix\Main\UI\FileInput::createInstance(array(
                'name' => $this->code . '[n#IND#]',
                'description' => $this->getSettings('DESCRIPTION_FIELD'),
                'upload' => $this->getSettings('UPLOAD'),
                'allowUpload' => 'I',
                'medialib' => $this->getSettings('MEDIALIB'),
                'fileDialog' => $this->getSettings('FILE_DIALOG'),
                'cloud' => $this->getSettings('CLOUD'),
                'delete' => $this->getSettings('DELETE'),
                'edit' => $this->getSettings('EDIT')
            ))->show($inputName);
        } else {
            $html = \CFileInput::ShowMultiple($inputName, $this->code . '[n#IND#]',
                array(
                    'IMAGE' => $this->getSettings('IMAGE') === true ? 'Y' : 'N',
                    'PATH' => 'Y',
                    'FILE_SIZE' => 'Y',
                    'DIMENSIONS' => 'Y',
                    'IMAGE_POPUP' => 'Y',
                ), 
                false, 
                array(
                    'upload' => $this->getSettings('UPLOAD'),
                    'medialib' => $this->getSettings('MEDIALIB'),
                    'file_dialog' => $this->getSettings('FILE_DIALOG'),
                    'cloud' => $this->getSettings('CLOUD'),
                    'del' => $this->getSettings('DELETE'),
                    'description' => $this->getSettings('DESCRIPTION_FIELD'),
                )
            );
        }

        foreach ($inputHidden as $key => $input) {
            if (!empty($input)) {
                $html .= '<input type="hidden" name="' . $this->code . '[' . $key . '][ID]" value=' . $key . '>
					<input type="hidden" name="' . $this->code . '[' . $key . '][VALUE]" value=' . $input . '>';
            }
        }

        return $html;
    }

    /**
     * {@inheritdoc}
     */
    public function generateRow(&$row, $data)
    {
        $html = '';
        
        if ($this->getSettings('MULTIPLE')) {
            
        } else {
            $path = \CFile::GetPath($data[$this->code]);
            $rsFile = \CFile::GetByID($data[$this->code]);
            $file = $rsFile->Fetch();

            if ($path) {
                $html = '<a href="' . $path . '" >' . $file['FILE_NAME'] . ' (' . $file['FILE_DESCRIPTION'] . ')' . '</a>';
            }
            
            $row->AddViewField($this->code, $html);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function showFilterHtml()
    {
        // TODO: Implement genFilterHTML() method.
    }

    /**
     * {@inheritdoc}
     */
    public function processEditAction()
    {
        if ($this->getSettings('MULTIPLE')) {
            if ($this->getSettings('READONLY') === true) {
                //удаляем все добавленные файлы в режиме только для чтения
                foreach ($this->data[$this->code] as $key => $value) {
                    if (!is_array($value)) {
                        unset($this->data[$this->code][$key]);
                    }
                }
                return false;
            }

            if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) {
                foreach ($this->data[$this->code] as $key => $value) {
                    if (is_array($value) && ($value['name'] || $value['tmp_name'])) {
                        $_FILES[$this->code]['name'][$key] = $value['name'];
                        $_FILES[$this->code]['type'][$key] = $value['type'];
                        $_FILES[$this->code]['tmp_name'][$key] = $this->correctTmpName($value['tmp_name']);
                        $_FILES[$this->code]['error'][$key] = $value['error'];
                        $_FILES[$this->code]['size'][$key] = $value['size'];
                        unset($this->data[$this->code][$key]);
                    } else {
                        $_FILES[$this->code]['name'][$key] = '';
                    }
                }
                if (!count($this->data[$this->code])) {
                    unset($this->data[$this->code]);
                }
            }

            if (!empty($_FILES[$this->getCode()])) {
                foreach ($_FILES[$this->getCode()]['name'] as $key => $fileName) {
                    if (empty($fileName)
                        || empty($_FILES[$this->getCode()]['tmp_name'][$key])
                        || !empty($_FILES[$this->getCode()]['error'][$key])
                    ) {
                        if (isset($_REQUEST[$this->getCode() . '_del'][$key])) {
                            if (is_array($this->data[$this->getCode()][$key]) &&
                                !empty($this->data[$this->getCode()][$key]['VALUE'])
                            ) {
                                \CFile::Delete(intval($this->data[$this->getCode()][$key]['VALUE']));
                            } else {
                                \CFile::Delete(intval($this->data[$this->getCode()][$key]));
                            }
                            unset($this->data[$this->getCode()][$key]);
                        } elseif ($this->data[$this->getCode()][$key]['VALUE']) {
                            \CFile::UpdateDesc($this->data[$this->getCode()][$key]['VALUE'],
                                $_REQUEST[$this->getCode() . '_descr'][$key]);
                        }
                        continue;
                    } elseif (is_int($key)) {
                        //Удаляем старый файл при замене
                        if (is_array($this->data[$this->getCode()][$key]) &&
                            !empty($this->data[$this->getCode()][$key]['VALUE'])
                        ) {
                            \CFile::Delete(intval($this->data[$this->getCode()][$key]['VALUE']));
                        } else {
                            \CFile::Delete(intval($this->data[$this->getCode()][$key]));
                        }
                    }

                    $description = null;

                    if (isset($_REQUEST[$this->getCode() . '_descr'][$key])) {
                        $description = $_REQUEST[$this->getCode() . '_descr'][$key];
                    }

                    if (empty($this->data[$this->getCode()][$key])) {
                        unset($this->data[$this->getCode()][$key]);
                    }

                    $fileId = $this->saveFile($fileName, $_FILES[$this->getCode()]['tmp_name'][$key], false, $description);

                    if ($fileId) {
                        $this->data[$this->getCode()][$key] = array('VALUE' => $fileId);
                    } else {
                        $this->addError('DIGITALWAND_AH_FAIL_ADD_FILE', array(
                            'FILE_NAME' => $_FILES[$this->getCode()]['name'][$key]
                        ));
                    }
                }
            }
        } else {
            if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) {
                if (is_array($this->data[$this->code . '_FILE']) && ($this->data[$this->code . '_FILE']['name'] ||
                        $this->data[$this->code . '_FILE']['tmp_name'])
                ) {
                    $_FILES['FIELDS']['name'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['name'];
                    $_FILES['FIELDS']['type'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['type'];
                    $_FILES['FIELDS']['tmp_name'][$this->code . '_FILE']
                        = $this->correctTmpName($this->data[$this->code . '_FILE']['tmp_name']);
                    $_FILES['FIELDS']['error'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['error'];
                    $_FILES['FIELDS']['size'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['size'];
                }
            }

            unset($this->data[$this->code . '_FILE']);
            
            if ($this->getSettings('READONLY') === true) {
                return false;
            }

            if (empty($_FILES['FIELDS']['name'][$this->code . '_FILE'])
                || empty($_FILES['FIELDS']['tmp_name'][$this->code . '_FILE'])
                || !empty($_FILES['FIELDS']['error'][$this->code . '_FILE'])
            ) {
                if (isset($_REQUEST['FIELDS_del'][$this->code . '_FILE']) AND $_REQUEST['FIELDS_del'][$this->code . '_FILE'] == 'Y') {
                    \CFile::Delete(intval($this->data[$this->code]));
                    $this->data[$this->code] = 0;
                } elseif ($this->data[$this->code] && isset($_REQUEST['FIELDS_descr'][$this->code . '_FILE'])) {
                    \CFile::UpdateDesc($this->data[$this->code],
                        $_REQUEST['FIELDS_descr'][$this->code . '_FILE']);
                }
                return false;
            }

            $description = null;

            if (isset($_REQUEST['FIELDS_descr'][$this->code . '_FILE'])) {
                $description = $_REQUEST['FIELDS_descr'][$this->code . '_FILE'];
            }

            $name = $_FILES['FIELDS']['name'][$this->code . '_FILE'];
            $path = $_FILES['FIELDS']['tmp_name'][$this->code . '_FILE'];
            $type = $_FILES['FIELDS']['type'][$this->code . '_FILE'];

            $this->saveFile($name, $path, $type, $description);
        }
        parent::processEditAction();
    }

    protected function saveFile($name, $path, $type = false, $description = null)
    {
        if (!$path) {
            return false;
        }

        $file = \CFile::MakeFileArray($path, $type);

        if (!$file) {
            return false;
        }

        if (!empty($description)) {
            $file['description'] = $description;
        }

        if ($this->getSettings('IMAGE') === true && stripos($file['type'], "image") === false) {
            $this->addError('FILE_FIELD_TYPE_ERROR');

            return false;
        }

        $file['name'] = $name;

        $moduleId = $this->helper->getModule();
        $file['MODULE_ID'] = $moduleId;

        $fileId = \CFile::SaveFile($file, $moduleId);

        if (!$this->getSettings('MULTIPLE')) {
            $code = $this->code;
            
            if (isset($this->data[$code])) {
                \CFile::Delete($this->data[$code]);
            }

            $this->data[$code] = $fileId;
        }

        return $fileId;
    }

    /**
     * {@inheritdoc}
     */
    protected function getValueReadonly()
    {
        $this->setSetting('UPLOAD', false);
        $this->setSetting('MEDIALIB', false);
        $this->setSetting('FILE_DIALOG', false);
        $this->setSetting('CLOUD', false);
        $this->setSetting('DELETE', false);
        $this->setSetting('EDIT', false);

        return $this->getEditHtml();
    }

    /**
     * {@inheritdoc}
     */
    protected function getMultipleValueReadonly()
    {
        $this->setSetting('UPLOAD', false);
        $this->setSetting('MEDIALIB', false);
        $this->setSetting('FILE_DIALOG', false);
        $this->setSetting('CLOUD', false);
        $this->setSetting('DELETE', false);
        $this->setSetting('EDIT', false);

        return $this->getMultipleEditHtml();
    }

    /**
     * Корректирует путь до временного файла
     * т.к. в новых версиях ядра путь до файла передается относительно временной папки загрузки, а не корня сайта.
     *
     * @param string $tmpName
     * @return string
     */
    protected function correctTmpName($tmpName = '')
    {
        if(!$tmpName) {
            return '';
        }

        static $relativeTempFolder = false;
        if(!$relativeTempFolder){
            $relativeTempFolder = str_replace(Application::getDocumentRoot(), '', CTempFile::GetAbsoluteRoot());
        }
        if (strpos($tmpName, $relativeTempFolder) === false) {
            $tmpName = $relativeTempFolder . $tmpName;
        }

        return $tmpName;
    }
}

================================================
FILE: lib/widget/HLIBlockFieldWidget.php
================================================
<?php
namespace DigitalWand\AdminHelper\Widget;

use DigitalWand\AdminHelper\Helper\AdminBaseHelper;
use Bitrix\Main\Entity\DataManager;
use Bitrix\Main\Entity\EntityError;
use Bitrix\Main\Entity\Result;
use Bitrix\Highloadblock as HL;
use Bitrix\Main\Localization\Loc;
use DigitalWand\AdminHelper\Helper\AdminEditHelper;
use DigitalWand\AdminHelper\Helper\AdminListHelper;

Loc::loadMessages(__FILE__);

/**
 * Виджет, отображающий стандартные поля, создаваемые в HL-инфоблоке в админке.
 *
 * Настройки:
 * <ul>
 * <li><b>MODEL</b> - Название модели, из которой будет производиться выборка данных. По-умолчанию - модель текущего
 * хэлпера</li>
 * </ul>
 * Class HLIBlockFieldWidget
 * @package DigitalWand\AdminHelper\Widget
 */
class HLIBlockFieldWidget extends HelperWidget
{
    static protected $userFieldsCache = array();
    static protected $defaults = array(
        'USE_BX_API' => true
    );

    /**
     * Генерирует HTML для редактирования поля
     *
     * @see \CAdminForm::ShowUserFieldsWithReadyData
     * @return mixed
     */
    protected function getEditHtml()
    {
        $info = $this->getUserFieldInfo();
        if ($info) {

            /** @var \CAllUserTypeManager $USER_FIELD_MANAGER */
            global $USER_FIELD_MANAGER;
            $GLOBALS[$this->getCode()] = isset($GLOBALS[$this->getCode()]) ? $GLOBALS[$this->getCode()] : $this->data[$this->getCode()];
            $bVarsFromForm = false;

            $info["VALUE_ID"] = intval($this->data['ID']);
            $info['EDIT_FORM_LABEL'] = $this->getSettings('TITLE');

            if (isset($_REQUEST['def_' . $this->getCode()])) {
                $info['SETTINGS']['DEFAULT_VALUE'] = $_REQUEST['def_' . $this->getCode()];
            }
            print $USER_FIELD_MANAGER->GetEditFormHTML($bVarsFromForm, $GLOBALS[$this->getCode()], $info);

        }
    }

    /**
     * Конвертирует данные при сохранении так, как это делали бы пользовательские свойства битрикса.
     * Выполняет валидацию с помощью CheckFields() пользовательских полей.
     *
     * @see Bitrix\Highloadblock\DataManager
     * @see /bitrix/modules/highloadblock/admin/highloadblock_row_edit.php
     *
     * @throws \Bitrix\Main\ArgumentException
     * @throws \Bitrix\Main\SystemException
     */
    public function processEditAction()
    {
        /** @var \CAllUserTypeManager $USER_FIELD_MANAGER */
        global $USER_FIELD_MANAGER;
        $iblockId = 'HLBLOCK_' . $this->getHLId();

        //Чтобы не терялись старые данные
        if (!isset($this->data[$this->getCode()]) AND isset($_REQUEST[$this->getCode() . '_old_id'])) {
            $this->data[$this->getCode()] = $_REQUEST[$this->getCode() . '_old_id'];
        }

        //Функция работает для всех полей, так что запускаем её только один раз, результат кешируем.
        static $data = array();
        if (empty($data)) {
            $data = $this->data;
            $USER_FIELD_MANAGER->EditFormAddFields($iblockId, $data);
        }

        $value = $data[$this->getCode()];

        $entity_data_class = AdminBaseHelper::getHLEntity($this->getSettings('MODEL'));

        $oldData = $this->getOldFieldData($entity_data_class);
        $fieldsInfo = $USER_FIELD_MANAGER->getUserFieldsWithReadyData($iblockId, $oldData, LANGUAGE_ID, false, 'ID');
        $fieldInfo = $fieldsInfo[$this->getCode()];

        $className = $fieldInfo['USER_TYPE']['CLASS_NAME'];
        if (is_callable(array($className, 'CheckFields'))) {
            $errors = $className::CheckFields($fieldInfo, $value);
            if (!empty($errors)) {
                $this->addError($errors);
                return;
            }
        }

        // use save modifiers
        $field = $entity_data_class::getEntity()->getField($this->getCode());
        $value = $field->modifyValueBeforeSave($value, $data);

        //Типоспецифичные хаки
        if ($unserialized = unserialize($value)) {
            //Список значений прилетает сериализованным
            $this->data[$this->getCode()] = $unserialized;

        } else if ($className == 'CUserTypeFile' AND !is_array($value)) {
            //Если не сделать intval, то при сохранении с ранее добавленным файлом будет выскакивать ошибка
            $this->data[$this->getCode()] = intval($value);

        } else {
            //Все остальные поля - сохраняем как есть.
            $this->data[$this->getCode()] = $value;
        }
    }

    /**
     * Битриксу надо получить поля, кторые сохранены в базе для этого пользовательского свойства.
     * Иначе множественные свойства он затрёт.
     * Проблема в том, что пользовательские свойства могут браться из связанной сущности.
     * @param HL\DataManager $entity_data_class
     *
     * @return mixed
     */
    protected function getOldFieldData($entity_data_class)
    {
        if (is_null($this->data) OR !isset($this->data[$this->helper->pk()])) return false;
        return $entity_data_class::getByPrimary($this->data[$this->helper->pk()])->fetch();
    }

    /**
     * Если запрашивается модель, и если модель явно не указана, то берется модель текущего хэлпера, сохраняется для
     * последующего использования и возарвщвется пользователю.
     *
     * @param string $name
     * @return array|\Bitrix\Main\Entity\DataManager|mixed|string
     */
    public function getSettings($name = '')
    {
        $value = parent::getSettings($name);
        if (!$value) {
            if ($name == 'MODEL') {
                $value = $this->helper->getModel();
                $this->setSetting($name, $value);

            } else if ($name == 'TITLE') {

                $context = $this->helper->getContext();
                $info = $this->getUserFieldInfo();

                if (($context == AdminListHelper::OP_ADMIN_VARIABLES_FILTER OR $context == AdminListHelper::OP_CREATE_FILTER_FORM)
                    AND (isset($info['LIST_FILTER_LABEL']) AND !empty($info['LIST_FILTER_LABEL']))
                ) {
                    $value = $info['LIST_FILTER_LABEL'];

                } else if ($context == AdminList
Download .txt
gitextract_k9xdrsiz/

├── .gitignore
├── LICENSE.md
├── README.md
├── admin/
│   └── route.php
├── composer.json
├── include.php
├── install/
│   ├── admin/
│   │   └── admin_helper_route.php
│   ├── index.php
│   └── version.php
├── lang/
│   └── ru/
│       ├── install/
│       │   └── index.php
│       └── lib/
│           ├── EntityManager.php
│           ├── helper/
│           │   ├── AdminBaseHelper.php
│           │   ├── AdminEditHelper.php
│           │   └── AdminListHelper.php
│           └── widget/
│               ├── CheckboxWidget.php
│               ├── ComboBoxWidget.php
│               ├── FileWidget.php
│               ├── HelperWidget.php
│               ├── IblockElementWidget.php
│               ├── NumberWidget.php
│               ├── OrmElementWidget.php
│               └── UrlWidget.php
└── lib/
    ├── EntityManager.php
    ├── EventHandlers.php
    ├── Sorting.php
    ├── helper/
    │   ├── AdminBaseHelper.php
    │   ├── AdminEditHelper.php
    │   ├── AdminInterface.php
    │   ├── AdminListHelper.php
    │   ├── AdminSectionEditHelper.php
    │   ├── AdminSectionListHelper.php
    │   └── Exception.php
    └── widget/
        ├── CheckboxWidget.php
        ├── ComboBoxWidget.php
        ├── DateTimeWidget.php
        ├── FileWidget.php
        ├── HLIBlockFieldWidget.php
        ├── HelperWidget.php
        ├── IblockElementWidget.php
        ├── NumberWidget.php
        ├── OrmElementWidget.php
        ├── StringWidget.php
        ├── TextAreaWidget.php
        ├── UrlWidget.php
        ├── UserWidget.php
        └── VisualEditorWidget.php
Download .txt
SYMBOL INDEX (281 symbols across 26 files)

FILE: admin/route.php
  function getRequestParams (line 13) | function getRequestParams($param)

FILE: install/index.php
  class digitalwand_admin_helper (line 9) | class digitalwand_admin_helper extends CModule
    method digitalwand_admin_helper (line 21) | function digitalwand_admin_helper()
    method DoInstall (line 31) | function DoInstall()
    method DoUninstall (line 48) | function DoUninstall()
    method InstallFiles (line 63) | function InstallFiles()

FILE: lib/EntityManager.php
  class EntityManager (line 143) | class EntityManager
    method __construct (line 189) | public function __construct($modelClass, array $data = array(), $itemI...
    method save (line 209) | public function save()
    method delete (line 251) | public function delete()
    method getNotes (line 290) | public function getNotes()
    method addNote (line 303) | protected function addNote($note, $key = null)
    method setItemId (line 320) | protected function setItemId($itemId)
    method getReferences (line 331) | protected function getReferences()
    method collectReferencesData (line 353) | protected function collectReferencesData()
    method processReferencesData (line 379) | protected function processReferencesData()
    method deleteReferencesData (line 460) | protected function deleteReferencesData()
    method createReferenceData (line 498) | protected function createReferenceData(Entity\ReferenceField $referenc...
    method updateReferenceData (line 535) | protected function updateReferenceData(
    method deleteReferenceData (line 582) | protected function deleteReferenceData(Entity\ReferenceField $referenc...
    method deleteReference (line 605) | protected function deleteReference(Entity\ReferenceField $reference, $...
    method getReferenceDataSet (line 629) | protected function getReferenceDataSet(Entity\ReferenceField $reference)
    method linkData (line 679) | protected function linkData(Entity\ReferenceField $reference, array $r...
    method linkDataSet (line 713) | protected function linkDataSet(Entity\ReferenceField $reference, array...
    method getLinkDataId (line 730) | protected function getLinkDataId(array $referenceDataSet, array $refer...
    method getReferenceConditions (line 759) | protected function getReferenceConditions(Entity\ReferenceField $refer...
    method isDifferentData (line 820) | protected function isDifferentData(array $data1 = null, array $data2 =...
    method getFieldParams (line 837) | protected function getFieldParams($fieldName)
    method getFieldWidget (line 856) | protected function getFieldWidget($fieldName)

FILE: lib/EventHandlers.php
  class EventHandlers (line 16) | class EventHandlers
    method onPageStart (line 26) | public static function onPageStart()

FILE: lib/Sorting.php
  class Sorting (line 17) | class Sorting extends \CAdminSorting
    method __construct (line 30) | public function __construct(

FILE: lib/helper/AdminBaseHelper.php
  class AdminBaseHelper (line 95) | abstract class AdminBaseHelper
    method __construct (line 274) | public function __construct(array $fields, array $tabs = array(), $mod...
    method getInterfaceSettings (line 301) | public static function getInterfaceSettings($viewName = '')
    method setInterfaceSettings (line 321) | public static function setInterfaceSettings(array $settings, array $he...
    method setInterfaceClass (line 341) | public static function setInterfaceClass($class)
    method getInterfaceClass (line 351) | public static function getInterfaceClass()
    method registerInterfaceSettings (line 365) | public static function registerInterfaceSettings($module, $interfaceSe...
    method getGlobalInterfaceSettings (line 396) | public static function getGlobalInterfaceSettings($module, $view)
    method getViewName (line 414) | public static function getViewName()
    method getSectionField (line 447) | public static function getSectionField()
    method getModel (line 481) | public static function getModel()
    method getModule (line 498) | public static function getModule()
    method getButton (line 543) | protected function getButton($code, $params, $keys = array('name', 'TE...
    method getFields (line 579) | public function getFields()
    method show (line 587) | abstract public function show();
    method table (line 594) | public function table()
    method pk (line 610) | public static function pk()
    method getPk (line 621) | public function getPk()
    method sectionPk (line 634) | public function sectionPk()
    method setTitle (line 646) | public function setTitle($title)
    method customActions (line 660) | protected function customActions($action, $id = null)
    method hasRights (line 672) | protected function hasRights()
    method hasReadRights (line 684) | protected function hasReadRights()
    method hasWriteRights (line 696) | protected function hasWriteRights()
    method hasWriteRightsElement (line 710) | protected function hasWriteRightsElement($element = array())
    method hasDeleteRights (line 726) | protected function hasDeleteRights()
    method showMessages (line 736) | protected function showMessages()
    method getLastException (line 765) | protected function getLastException()
    method setAppException (line 782) | protected function setAppException($e)
    method addErrors (line 794) | public function addErrors($errors)
    method addNotes (line 815) | public function addNotes($notes)
    method getErrors (line 835) | protected function getErrors()
    method getNotes (line 853) | protected function getNotes()
    method getHelperClass (line 879) | public static function getHelperClass($class)
    method getEntityCode (line 917) | public static function getEntityCode()
    method getEditPageURL (line 947) | public static function getEditPageURL($params = array())
    method getSectionsEditPageURL (line 967) | public static function getSectionsEditPageURL($params = array())
    method getListPageURL (line 988) | public static function getListPageURL($params = array())
    method getViewURL (line 1011) | public static function getViewURL($viewName, $defaultURL, $params = ar...
    method getRouterURL (line 1041) | public static function getRouterURL()
    method getUrl (line 1054) | public static function getUrl(array $params = array())
    method createWidgetForField (line 1071) | public function createWidgetForField($code, &$data = array())
    method onCreateWidgetForField (line 1102) | protected function onCreateWidgetForField(&$widget, $data = array())
    method getHLEntity (line 1117) | public static function getHLEntity($className)
    method getHLEntityInfo (line 1147) | public static function getHLEntityInfo($className)
    method show404 (line 1169) | protected function show404()
    method setContext (line 1186) | protected function setContext($context)
    method getContext (line 1191) | public function getContext()
    method className (line 1196) | public static function className()

FILE: lib/helper/AdminEditHelper.php
  class AdminEditHelper (line 31) | abstract class AdminEditHelper extends AdminBaseHelper
    method __construct (line 62) | public function __construct(array $fields, array $tabs = array())
    method getMenu (line 213) | protected function getMenu($showDeleteButton = true)
    method show (line 268) | public function show()
    method showEditPageButtons (line 316) | protected function showEditPageButtons()
    method showProlog (line 334) | protected function showProlog()
    method showEpilog (line 343) | protected function showEpilog()
    method showTabElements (line 363) | private function showTabElements($tabSettings)
    method editAction (line 411) | protected function editAction()
    method loadElement (line 495) | protected function loadElement($select = array())
    method saveElement (line 528) | protected function saveElement($id = null)
    method deleteElement (line 549) | protected function deleteElement($id)
    method customActions (line 576) | protected function customActions($action, $id = null)
    method setElementTitle (line 603) | protected function setElementTitle()
    method getTabControl (line 618) | public function getTabControl()
    method getUrl (line 626) | public static function getUrl(array $params = array())

FILE: lib/helper/AdminInterface.php
  class AdminInterface (line 27) | abstract class AdminInterface
    method fields (line 85) | abstract public function fields();
    method helpers (line 133) | abstract public function helpers();
    method dependencies (line 141) | public function dependencies()
    method registerData (line 149) | public function registerData()
    method register (line 205) | public static function register()

FILE: lib/helper/AdminListHelper.php
  class AdminListHelper (line 32) | abstract class AdminListHelper extends AdminBaseHelper
    method __construct (line 177) | public function __construct(array $fields, $isPopup = false)
    method initSortingParameters (line 321) | protected function initSortingParameters()
    method prepareAdminVariables (line 335) | protected function prepareAdminVariables()
    method getSectionsHeader (line 415) | public function getSectionsHeader()
    method checkFilter (line 449) | protected function checkFilter($arFilter)
    method getContextMenu (line 472) | protected function getContextMenu()
    method getGroupActions (line 539) | protected function getGroupActions()
    method groupActions (line 560) | protected function groupActions($IDs, $action)
    method editAction (line 696) | protected function editAction($id, $fields)
    method isPopup (line 775) | public function isPopup()
    method genPopupActionJS (line 786) | protected function genPopupActionJS()
    method buildList (line 832) | public function buildList($sort)
    method mergeSortHeader (line 1019) | protected function mergeSortHeader(&$array)
    method mergeSortHeaderCompare (line 1067) | public function mergeSortHeaderCompare($a, $b)
    method getMixedData (line 1087) | protected function getMixedData($sectionsVisibleColumns, $elementVisib...
    method getLimits (line 1215) | protected function getLimits()
    method escapeFilterFieldName (line 1247) | protected function escapeFilterFieldName($fieldName)
    method customNavStart (line 1259) | protected function customNavStart(&$res)
    method modifyRowData (line 1304) | protected function modifyRowData(&$data)
    method getRow (line 1318) | protected function getRow($data, $class = false)
    method addRowSectionCell (line 1348) | protected function addRowSectionCell($row, $code, $data)
    method getRowActions (line 1388) | protected function getRowActions($data, $section = false)
    method addRowCell (line 1442) | protected function addRowCell($row, $code, $data, $virtualCode = false)
    method getData (line 1473) | protected function getData($className, $filter, $select, $sort, $raw)
    method getFooter (line 1496) | protected function getFooter($res)
    method createFilterForm (line 1510) | public function createFilterForm()
    method getListTableID (line 1560) | protected function getListTableID()
    method show (line 1569) | public function show()
    method saveGetQuery (line 1591) | private function saveGetQuery()
    method restoreLastGetQuery (line 1599) | private function restoreLastGetQuery()
    method getUrl (line 1615) | public static function getUrl(array $params = array())
    method getSectionsFilter (line 1625) | protected function getSectionsFilter(array $filter)
    method getElementsFilter (line 1635) | protected function getElementsFilter($filter)
    method getIds (line 1645) | protected function getIds()
    method getCommonPrimaryFilterById (line 1681) | protected function getCommonPrimaryFilterById($className, $sectionClas...

FILE: lib/helper/AdminSectionEditHelper.php
  class AdminSectionEditHelper (line 20) | class AdminSectionEditHelper extends AdminEditHelper

FILE: lib/helper/AdminSectionListHelper.php
  class AdminSectionListHelper (line 15) | class AdminSectionListHelper extends AdminListHelper

FILE: lib/helper/Exception.php
  class Exception (line 5) | class Exception extends \Exception

FILE: lib/widget/CheckboxWidget.php
  class CheckboxWidget (line 17) | class CheckboxWidget extends HelperWidget
    method generateRow (line 56) | public function generateRow(&$row, $data)
    method showFilterHtml (line 102) | public function showFilterHtml()
    method getValueReadonly (line 137) | public function getValueReadonly()
    method processEditAction (line 161) | public function processEditAction()
    method getEditHtml (line 173) | protected function getEditHtml()
    method getCheckboxType (line 205) | public function getCheckboxType()

FILE: lib/widget/ComboBoxWidget.php
  class ComboBoxWidget (line 25) | class ComboBoxWidget extends HelperWidget
    method getEditHtml (line 40) | protected function getEditHtml()
    method getMultipleEditHtml (line 48) | protected function getMultipleEditHtml()
    method getComboBox (line 61) | protected function getComboBox($multiple = false, $forFilter = false)
    method getValueReadonly (line 115) | protected function getValueReadonly()
    method getMultipleValueReadonly (line 126) | protected function getMultipleValueReadonly()
    method getVariants (line 160) | protected function getVariants()
    method formatVariants (line 184) | protected function formatVariants($variants)
    method generateRow (line 203) | public function generateRow(&$row, $data)
    method showFilterHtml (line 215) | public function showFilterHtml()
    method processEditAction (line 226) | public function processEditAction()

FILE: lib/widget/DateTimeWidget.php
  class DateTimeWidget (line 5) | class DateTimeWidget extends HelperWidget
    method getEditHtml (line 16) | protected function getEditHtml()
    method generateRow (line 28) | public function generateRow(&$row, $data)
    method showFilterHtml (line 56) | public function showFilterHtml()
    method processEditAction (line 68) | public function processEditAction()

FILE: lib/widget/FileWidget.php
  class FileWidget (line 19) | class FileWidget extends HelperWidget
    method __construct (line 37) | public function __construct(array $settings = array())
    method getEditHtml (line 47) | protected function getEditHtml()
    method getMultipleEditHtml (line 90) | protected function getMultipleEditHtml()
    method generateRow (line 155) | public function generateRow(&$row, $data)
    method showFilterHtml (line 177) | public function showFilterHtml()
    method processEditAction (line 185) | public function processEditAction()
    method saveFile (line 317) | protected function saveFile($name, $path, $type = false, $description ...
    method getValueReadonly (line 362) | protected function getValueReadonly()
    method getMultipleValueReadonly (line 377) | protected function getMultipleValueReadonly()
    method correctTmpName (line 396) | protected function correctTmpName($tmpName = '')

FILE: lib/widget/HLIBlockFieldWidget.php
  class HLIBlockFieldWidget (line 26) | class HLIBlockFieldWidget extends HelperWidget
    method getEditHtml (line 39) | protected function getEditHtml()
    method processEditAction (line 70) | public function processEditAction()
    method getOldFieldData (line 132) | protected function getOldFieldData($entity_data_class)
    method getSettings (line 145) | public function getSettings($name = '')
    method generateRow (line 196) | public function generateRow(&$row, $data)
    method showFilterHtml (line 222) | public function showFilterHtml()
    method getUserFieldInfo (line 238) | public function getUserFieldInfo()
    method getHLId (line 252) | protected function getHLId()
    method getUserFields (line 267) | static public function getUserFields($iblockId, $data)
    method addError (line 286) | protected function addError($messageId)

FILE: lib/widget/HelperWidget.php
  class HelperWidget (line 147) | abstract class HelperWidget
    method __construct (line 197) | public function __construct(array $settings = array())
    method getEditHtml (line 211) | abstract protected function getEditHtml();
    method getMultipleEditHtml (line 220) | protected function getMultipleEditHtml()
    method showBasicEditField (line 233) | public function showBasicEditField($isPKField)
    method getValueReadonly (line 286) | protected function getValueReadonly()
    method getMultipleValue (line 296) | protected function getMultipleValue()
    method getMultipleValueReadonly (line 329) | protected function getMultipleValueReadonly()
    method prepareToOutput (line 353) | public static function prepareToOutput($string, $hideTags = true)
    method prepareToTagAttr (line 372) | public static function prepareToTagAttr($string)
    method prepareToJs (line 385) | public static function prepareToJs($string)
    method generateRow (line 405) | abstract public function generateRow(&$row, $data);
    method showFilterHtml (line 416) | abstract public function showFilterHtml();
    method getSettings (line 427) | public function getSettings($name = '')
    method setHelper (line 449) | public function setHelper(&$helper)
    method getCurrentFilterValue (line 459) | protected function getCurrentFilterValue()
    method checkFilter (line 477) | public function checkFilter($operationType, $value)
    method changeGetListOptions (line 494) | public function changeGetListOptions(&$filter, &$select, &$sort, $raw)
    method isFilterBetween (line 542) | protected function isFilterBetween()
    method processEditAction (line 554) | public function processEditAction()
    method processAfterSaveAction (line 568) | public function processAfterSaveAction()
    method addError (line 581) | protected function addError($messageId, $replace = array())
    method checkRequired (line 595) | public function checkRequired()
    method setCode (line 611) | public function setCode($code)
    method getCode (line 620) | public function getCode()
    method loadSettings (line 635) | public function loadSettings($code = null)
    method getEntityName (line 656) | public function getEntityName()
    method setEntityName (line 664) | public function setEntityName($entityName)
    method setDefaultValue (line 673) | public function setDefaultValue()
    method setData (line 685) | public function setData(&$data)
    method getValue (line 698) | public function getValue()
    method setValue (line 712) | protected function setValue($value)
    method getMultipleField (line 727) | public function getMultipleField($fieldName)
    method setSetting (line 755) | public function setSetting($name, $value)
    method getValidationErrors (line 764) | public function getValidationErrors()
    method getFilterInputName (line 775) | protected function getFilterInputName()
    method getEditInputName (line 795) | protected function getEditInputName($suffix = null)
    method getEditInputHtmlId (line 804) | protected function getEditInputHtmlId()
    method getEditableListInputName (line 815) | protected function getEditableListInputName()
    method getCurrentViewType (line 829) | protected function getCurrentViewType()
    method isUnique (line 846) | private function isUnique()
    method isExcelView (line 884) | protected function isExcelView()
    method jsHelper (line 897) | protected function jsHelper()

FILE: lib/widget/IblockElementWidget.php
  class IblockElementWidget (line 22) | class IblockElementWidget extends NumberWidget
    method __construct (line 31) | public function __construct(array $settings = array())
    method getEditHtml (line 42) | public function getEditHtml()
    method getValueReadonly (line 81) | public function getValueReadonly()
    method generateRow (line 109) | public function generateRow(&$row, $data)

FILE: lib/widget/NumberWidget.php
  class NumberWidget (line 12) | class NumberWidget extends StringWidget
    method checkFilter (line 19) | public function checkFilter($operationType, $value)
    method checkRequired (line 24) | public function checkRequired()
    method processEditAction (line 34) | public function processEditAction()
    method isNumber (line 44) | protected function isNumber($value)

FILE: lib/widget/OrmElementWidget.php
  class OrmElementWidget (line 27) | class OrmElementWidget extends NumberWidget
    method processEditAction (line 29) | public function processEditAction()
    method loadSettings (line 54) | public function loadSettings($code = null)
    method getEditHtml (line 74) | public function getEditHtml()
    method getEditHtmlSelect (line 90) | protected function getEditHtmlSelect()
    method genEditHtmlInputs (line 139) | public function genEditHtmlInputs()
    method getMultipleEditHtml (line 161) | public function getMultipleEditHtml()
    method getValueReadonly (line 234) | public function getValueReadonly()
    method getMultipleValueReadonly (line 252) | public function getMultipleValueReadonly()
    method generateRow (line 276) | public function generateRow(&$row, $data)
    method showFilterHtml (line 290) | public function showFilterHtml()
    method getOrmElementData (line 339) | protected function getOrmElementData()
    method getOrmElementList (line 421) | protected function getOrmElementList()

FILE: lib/widget/StringWidget.php
  class StringWidget (line 24) | class StringWidget extends HelperWidget
    method getEditHtml (line 34) | protected function getEditHtml()
    method getMultipleEditHtml (line 67) | protected function getMultipleEditHtml()
    method getMultipleValueReadonly (line 123) | protected function getMultipleValueReadonly()
    method generateRow (line 155) | public function generateRow(&$row, $data)
    method showFilterHtml (line 191) | public function showFilterHtml()

FILE: lib/widget/TextAreaWidget.php
  class TextAreaWidget (line 19) | class TextAreaWidget extends StringWidget
    method getEditHtml (line 35) | protected function getEditHtml()
    method generateRow (line 47) | public function generateRow(&$row, $data)

FILE: lib/widget/UrlWidget.php
  class UrlWidget (line 22) | class UrlWidget extends StringWidget
    method generateRow (line 32) | public function generateRow(&$row, $data)
    method getValue (line 46) | public function getValue()
    method getValueReadonly (line 72) | protected function getValueReadonly()
    method processEditAction (line 80) | public function processEditAction()

FILE: lib/widget/UserWidget.php
  class UserWidget (line 18) | class UserWidget extends NumberWidget
    method getEditHtml (line 23) | public function getEditHtml()
    method getValueReadonly (line 50) | public function getValueReadonly()
    method generateRow (line 75) | public function generateRow(&$row, $data)

FILE: lib/widget/VisualEditorWidget.php
  class VisualEditorWidget (line 11) | class VisualEditorWidget extends TextAreaWidget
    method getEditHtml (line 37) | protected function getEditHtml()
    method showBasicEditField (line 180) | public function showBasicEditField($isPKField)
    method processEditAction (line 204) | public function processEditAction()
    method getValueReadonly (line 240) | protected function getValueReadonly()
    method generateRow (line 248) | public function generateRow(&$row, $data)
    method getContentType (line 270) | public function getContentType()
    method getContentTypeCode (line 282) | public function getContentTypeCode()
    method getEntityShortName (line 292) | protected function getEntityShortName()
Condensed preview — 46 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (341K chars).
[
  {
    "path": ".gitignore",
    "chars": 21,
    "preview": "/.idea\n/composer.lock"
  },
  {
    "path": "LICENSE.md",
    "chars": 1101,
    "preview": "The MIT License (MIT)\n\nCopyright © 2015 DigitalWand (http://digitalwand.ru/)\n\nPermission is hereby granted, free of char"
  },
  {
    "path": "README.md",
    "chars": 3202,
    "preview": "# digitalwand.admin_helper\nAPI для сборки кастомных админок в Битриксе\n\nДокументация по модулю доступна по адресу [http:"
  },
  {
    "path": "admin/route.php",
    "chars": 3413,
    "preview": "<?php\n\nuse Bitrix\\Main\\Loader;\nuse DigitalWand\\AdminHelper\\Helper\\AdminBaseHelper;\nuse DigitalWand\\AdminHelper\\Helper\\Ad"
  },
  {
    "path": "composer.json",
    "chars": 825,
    "preview": "{\n  \"name\": \"digitalwand/digitalwand.admin_helper\",\n  \"description\": \"API for custom admin interface in Bitrix by Digita"
  },
  {
    "path": "include.php",
    "chars": 2322,
    "preview": "<?php\r\n\r\nuse Bitrix\\Main\\Loader;\r\n\r\nLoader::registerAutoLoadClasses('digitalwand.admin_helper',\r\n    array(\r\n        'Di"
  },
  {
    "path": "install/admin/admin_helper_route.php",
    "chars": 307,
    "preview": "<?\r\nif (!@include_once $_SERVER[\"DOCUMENT_ROOT\"] . \"/bitrix/modules/digitalwand.admin_helper/admin/route.php\") {\r\n    if"
  },
  {
    "path": "install/index.php",
    "chars": 1823,
    "preview": "<?php\r\n\r\nuse Bitrix\\Main\\Localization\\Loc;\r\n\r\nLoc::loadMessages(__FILE__);\r\n\r\nif (class_exists('digitalwand_admin_helper"
  },
  {
    "path": "install/version.php",
    "chars": 102,
    "preview": "<?\r\ndefine('ADMIN_HELPER_VERSION', '2.0.0');\r\ndefine('ADMIN_HELPER_VERSION_DATE', '2016-01-22');\r\n?>\r\n"
  },
  {
    "path": "lang/ru/install/index.php",
    "chars": 471,
    "preview": "<?php\n$MESS['ADMIN_HELPER_INSTALL_NAME'] = 'API AdminHelper';\n$MESS['ADMIN_HELPER_INSTALL_DESCRIPTION'] = 'API для постр"
  },
  {
    "path": "lang/ru/lib/EntityManager.php",
    "chars": 325,
    "preview": "<?php\n$MESS['DIGITALWAND_AH_RELATION_SHOULD_BE_MULTIPLE_FIELD'] = \"Связь должна быть множественным полем\";\n$MESS['DIGITA"
  },
  {
    "path": "lang/ru/lib/helper/AdminBaseHelper.php",
    "chars": 192,
    "preview": "<?php\n\n$MESS['DIGITALWAND_ADMIN_HELPER_GETMODEL_EXCEPTION'] = 'Попыка обращение к несуществующему HL-инфоблоку #CLASS#';"
  },
  {
    "path": "lang/ru/lib/helper/AdminEditHelper.php",
    "chars": 888,
    "preview": "<?php\n$MESS['DEFAULT_TAB'] = 'Элемент';\n$MESS['DIGITALWAND_ADMIN_HELPER_RETURN_TO_LIST'] = 'Список';\n$MESS['DELETE'] = '"
  },
  {
    "path": "lang/ru/lib/helper/AdminListHelper.php",
    "chars": 1148,
    "preview": "<?php\n$MESS['DIGITALWAND_ADMIN_HELPER_LIST_CHECKED'] = 'Выбрано:';\n$MESS['DIGITALWAND_ADMIN_HELPER_LIST_EDIT'] = 'Редакт"
  },
  {
    "path": "lang/ru/lib/widget/CheckboxWidget.php",
    "chars": 96,
    "preview": "<?php\n\n$MESS['DIGITALWAND_AH_CHECKBOX_YES'] = 'Да';\n$MESS['DIGITALWAND_AH_CHECKBOX_NO'] = 'Нет';"
  },
  {
    "path": "lang/ru/lib/widget/ComboBoxWidget.php",
    "chars": 154,
    "preview": "<?php\r\n\r\n$MESS['DIGITALWAND_AH_COMBO_BOX_LIST_EMPTY'] = 'Не выбрано';\r\n$MESS['DIGITALWAND_AH_MISSING_VARIANTS'] = 'Не уд"
  },
  {
    "path": "lang/ru/lib/widget/FileWidget.php",
    "chars": 86,
    "preview": "<?php\n\n$MESS['DIGITALWAND_AH_FAIL_ADD_FILE'] = 'Не удалось добавить файл #FILE_NAME#';"
  },
  {
    "path": "lang/ru/lib/widget/HelperWidget.php",
    "chars": 334,
    "preview": "<?php\n\n$MESS['DIGITALWAND_AH_REQUIRED_FIELD_ERROR'] = 'Обязательное поле \"#FIELD#\" не заполнено';\n$MESS['DIGITALWAND_AH_"
  },
  {
    "path": "lang/ru/lib/widget/IblockElementWidget.php",
    "chars": 63,
    "preview": "<?php\r\n$MESS['IBLOCK_ELEMENT_NOT_FOUND'] = 'Элемент не найден';"
  },
  {
    "path": "lang/ru/lib/widget/NumberWidget.php",
    "chars": 76,
    "preview": "<?php\n$MESS['VALUE_IS_NOT_NUMERIC'] = 'Поле #FIELD# должно содержать число';"
  },
  {
    "path": "lang/ru/lib/widget/OrmElementWidget.php",
    "chars": 125,
    "preview": "<?php\n\n$MESS['TITLE_FIELD_NAME'] = 'Элемент не найден';\n$MESS['DIGITALWAND_AH_ORM_MISSING_ELEMENTS'] = 'Элементы не найд"
  },
  {
    "path": "lang/ru/lib/widget/UrlWidget.php",
    "chars": 91,
    "preview": "<?php\r\n$MESS['PROTOCOL_REQUIRED'] = 'Укажите протокол ссылки \"#FIELD#\" (например http://)';"
  },
  {
    "path": "lib/EntityManager.php",
    "chars": 26991,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper;\n\nuse Bitrix\\Main\\Localization\\Loc;\nuse Bitrix\\Main\\ArgumentException;\nuse Bitr"
  },
  {
    "path": "lib/EventHandlers.php",
    "chars": 876,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper;\n\nuse Bitrix\\Main\\Context;\nuse Bitrix\\Main\\Loader;\n\n/**\n * Перехватчики событий"
  },
  {
    "path": "lib/Sorting.php",
    "chars": 2826,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper;\n\nuse DigitalWand\\AdminHelper\\Helper\\AdminBaseHelper;\n\n/**\n * Оригинальный клас"
  },
  {
    "path": "lib/helper/AdminBaseHelper.php",
    "chars": 31477,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Helper;\n\nuse Bitrix\\Main\\Entity\\ReferenceField;\nuse Bitrix\\Main\\Loader;\nuse Bit"
  },
  {
    "path": "lib/helper/AdminEditHelper.php",
    "chars": 16405,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Helper;\n\nuse Bitrix\\Main\\Localization\\Loc;\nuse DigitalWand\\AdminHelper\\EntityMa"
  },
  {
    "path": "lib/helper/AdminInterface.php",
    "chars": 7403,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Helper;\n\n/**\n * Базовый класс для описания админского интерфейса.\n * Включает в"
  },
  {
    "path": "lib/helper/AdminListHelper.php",
    "chars": 52518,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Helper;\n\nuse Bitrix\\Main\\Localization\\Loc;\nuse Bitrix\\Main\\Entity\\DataManager;\n"
  },
  {
    "path": "lib/helper/AdminSectionEditHelper.php",
    "chars": 591,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Helper;\n\n/**\n * Класс-обертка для хелпера редактирования разделов.\n * \n * Все х"
  },
  {
    "path": "lib/helper/AdminSectionListHelper.php",
    "chars": 384,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Helper;\n\n/**\n * Класс-обертка для хелпера списка разделов.\n * \n * Все хелперы о"
  },
  {
    "path": "lib/helper/Exception.php",
    "chars": 158,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Helper;\n\nclass Exception extends \\Exception\n{\n\tconst CODE_NO_WIDGET = 1;\n\tconst"
  },
  {
    "path": "lib/widget/CheckboxWidget.php",
    "chars": 8535,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\Localization\\Loc;\n\nLoc::loadMessages(__FILE__);\n\n/**\n "
  },
  {
    "path": "lib/widget/ComboBoxWidget.php",
    "chars": 6079,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\Localization\\Loc;\n\nLoc::loadMessages(__FILE__);\n\n/**\n "
  },
  {
    "path": "lib/widget/DateTimeWidget.php",
    "chars": 1823,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nclass DateTimeWidget extends HelperWidget\n{\n\tstatic protected $default"
  },
  {
    "path": "lib/widget/FileWidget.php",
    "chars": 15729,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\Localization\\Loc;\nuse Bitrix\\Main\\UI\\FileInput;\nuse Bi"
  },
  {
    "path": "lib/widget/HLIBlockFieldWidget.php",
    "chars": 10701,
    "preview": "<?php\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse DigitalWand\\AdminHelper\\Helper\\AdminBaseHelper;\nuse Bitrix\\Main\\Ent"
  },
  {
    "path": "lib/widget/HelperWidget.php",
    "chars": 33832,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\Localization\\Loc;\nuse DigitalWand\\AdminHelper\\Helper\\A"
  },
  {
    "path": "lib/widget/IblockElementWidget.php",
    "chars": 4327,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Iblock\\ElementTable;\nuse Bitrix\\Main\\Loader;\nuse Bitrix\\Mai"
  },
  {
    "path": "lib/widget/NumberWidget.php",
    "chars": 1145,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\Localization\\Loc;\n\nLoc::loadMessages(__FILE__);\n\n/**\n "
  },
  {
    "path": "lib/widget/OrmElementWidget.php",
    "chars": 14446,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\ArgumentTypeException;\nuse Bitrix\\Main\\Localization\\Lo"
  },
  {
    "path": "lib/widget/StringWidget.php",
    "chars": 8191,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\Localization\\Loc;\nuse DigitalWand\\AdminHelper\\Helper\\A"
  },
  {
    "path": "lib/widget/TextAreaWidget.php",
    "chars": 1906,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\nuse Bitrix\\Main\\Localization\\Loc;\n\nLoc::loadMessages(__FILE__);\n\n/**\n "
  },
  {
    "path": "lib/widget/UrlWidget.php",
    "chars": 2426,
    "preview": "<?php\r\n\r\nnamespace DigitalWand\\AdminHelper\\Widget;\r\n\r\nuse Bitrix\\Main\\Localization\\Loc;\r\n\r\nLoc::loadMessages(__FILE__);\r"
  },
  {
    "path": "lib/widget/UserWidget.php",
    "chars": 2808,
    "preview": "<?php\r\n\r\nnamespace DigitalWand\\AdminHelper\\Widget;\r\n\r\nuse Bitrix\\Main\\UserTable;\r\n\r\n/**\r\n * Виджет для вывода пользовате"
  },
  {
    "path": "lib/widget/VisualEditorWidget.php",
    "chars": 10940,
    "preview": "<?php\n\nnamespace DigitalWand\\AdminHelper\\Widget;\n\n/**\n * Визуальный редактор.\n *\n * В отличии от виджета TextAreaWidget,"
  }
]

About this extraction

This page contains the full source code of the DigitalWand/digitalwand.admin_helper GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 46 files (273.1 KB), approximately 74.9k tokens, and a symbol index with 281 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!