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 Битрикс, можно подготовить для таких сущностей класс-обёртку, реализующий необходимые функции. Основные понятия модуля: Схема работы с модулем следующая: Рекомендуемая файловая структура для модулей, использующих данный функционал: Использовать данную структуру не обязательно, это лишь рекомендация, основанная на успешном опыте применения модуля в ряде проектов. ## Разработчики ================================================ FILE: admin/route.php ================================================ 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 ================================================ '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 ================================================ 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 ================================================ ================================================ FILE: lang/ru/install/index.php ================================================ 'Монстры на каникулах 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`, которые не были добавлены или обновлены. * * Как работает удаление? * * 1. EntityManager получает из `NewsTable::getMap()` поля-связи * 2. Получает поля описанные в интерфейсе генератора админки * 3. Удаляет значения для полей-связей, которые объявлены в интерфейсе * * Примечание. * EntityManager управляет только данными, которые получает при помощи связи стандартными средставами битрикса. * Например, при удалении NewsTable будут удалены только NewsLinksTable, где: * * ``` * NewsLinksTable::ENTITY_ID => NewsTable::ID, * NewsLinksTable::FIELD => 'NEWS_LINKS', * NewsLinksTable::ENTITY => 'news' * ``` * * @author Nik Samokhvalov * @author Dmitriy Baibuhtin */ 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 ================================================ */ class EventHandlers { /** * Автоматическое подключение модуля в админке. * * Таки образом, исключаем необходимость прописывать в генераторах админки своих модулей * подключение «Админ-хелпера». * * @throws \Bitrix\Main\LoaderException */ public static function onPageStart() { if (Context::getCurrent()->getRequest()->isAdminSection()) { Loader::includeModule('digitalwand.admin_helper'); } } } ================================================ FILE: lib/Sorting.php ================================================ 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 ================================================ *
  • Мдель: "model" в терминах MVC. Класс, унаследованный от DataManager или реализующий аналогичный API.
  • *
  • Хэлпер: "view" в терминах MVC. Класс, реализующий отрисовку интерфейса списка или детальной страницы.
  • *
  • Роутер: "controller" в терминах MVC. Файл, принимающий все запросы к админке данного модуля, создающий нужные * хэлперы с нужными настройками. С ним напрямую работать не придётся.
  • *
  • Виджеты: "delegate" в терминах MVC. Классы, отвечающие за отрисовку элементов управления для отдельных полей * сущностей. В списке и на детальной.
  • * * * Схема работы с модулем следующая: * * * Устаревший функционал: * * * Этого будет дастаточно для получения минимальной функциональности. * * @package AdminHelper * * @see AdminBaseHelper::$model * @see AdminBaseHelper::$module * @see AdminBaseHelper::$listViewName * @see AdminBaseHelper::$viewName * * @author Nik Samokhvalov * @author Artem Yarygin */ 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 ''; 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(); } /** * Возвращает верхнее меню страницы. * По-умолчанию две кнопки: *
      *
    • Возврат в список
    • *
    • Удаление элемента
    • *
    * * Добавляя новые кнопки, нужно указывать параметр 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 ''; } } } /** * Отрисовывает вкладку со всеми привязанными к ней полями. * * @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); } } /** * Обработка запроса редактирования страницы. Этапы: *
      *
    • Проверка прав пользователя
    • *
    • Создание виджетов для каждого поля
    • *
    • Удаление значений для READONLY и HIDE_WHEN_CREATE полей
    • *
    • Изменение данных модели каждым виджетом (исходя из его внутренней логики)
    • *
    • Валидация значений каждого поля соответствующим виджетом
    • *
    • Проверка на ошибики валидации
    • *
    • В случае неудачи - выход из функции
    • *
    • В случае успеха - обновление или добавление элемента в БД
    • *
    • Постобработка данных модели каждым виджетом
    • *
    * * @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 ================================================ * @author Artem Yarygin */ 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' * ) * ``` * * Где: *
      *
    • `Vendor\Module\Entity\AdminInterface` - namespace до реализованных классов AdminHelper. *
    • `BUTTONS` - ключ для массива с описанием элементов управления (подробнее в методе getButton() * класса AdminBaseHelper). *
    • `LIST_CREATE_NEW`, `LIST_CREATE_NEW_SECTION`, `RETURN_TO_LIST`, `ADD_ELEMENT` - символьные код элементов * управления. *
    • `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 ================================================ *
    • static protected $model
    • *
    * * Этого будет дастаточно для получения минимальной функциональности * Также данный класс может использоваться для отображения всплывающих окон с возможностью выбора элемента из списка * * @see AdminBaseHelper::$model * @see AdminBaseHelper::$module * @see AdminBaseHelper::$editViewName * @see AdminBaseHelper::$viewName * @package AdminHelper * * @author Nik Samokhvalov * @author Artem Yarygin */ 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')); } } } /** * Сохранение полей для отной записи, отредактированной в списке. * Этапы: *
      *
    • Выборка элемента по ID, чтобы удостовериться, что он существует. В противном случае возвращается * ошибка
    • *
    • Создание виджета для каждой ячейки, валидация значений поля
    • *
    • TODO: вывод ошибок валидации
    • *
    • Сохранение записи
    • *
    • Вывод ошибок сохранения, если таковые появились
    • *
    • Модификация данных сроки виджетами.
    • *
    * * @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 = ''; } /** * Основной цикл отображения списка. Этапы: *
      *
    • Вывод заголовков страницы
    • *
    • Определение списка видимых колонок и колонок, участвующих в выборке.
    • *
    • Создание виджета для каждого поля выборки
    • *
    • Модификация параметров запроса каждым из виджетов
    • *
    • Выборка данных
    • *
    • Вывод строк таблицы. Во время итерации по строкам возможна модификация данных строки.
    • *
    • Отрисовка футера таблицы, добавление контекстного меню
    • *
    * * @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); } /** * Возвращает массив со списком действий при клике правой клавишей мыши на строке таблицы * По-умолчанию: *
      *
    • Редактировать элемент
    • *
    • Удалить элемент
    • *
    • Если это всплывающее окно - запустить кастомную JS-функцию.
    • *
    * * @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 '
    '; $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 '
    '; } /** * Возвращает 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 ================================================ * @author Artem Yarygin */ class AdminSectionEditHelper extends AdminEditHelper { } ================================================ FILE: lib/helper/AdminSectionListHelper.php ================================================ * @author Artem Yarygin */ class AdminSectionListHelper extends AdminListHelper { } ================================================ FILE: lib/helper/Exception.php ================================================ *
  • FIELD_TYPE - Тип данных для хранения булевых значений (строка, целые числа, булево)
  • * */ 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 = ' '; $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 = ''; $filterHtml .= '' . $this->getSettings('TITLE') . ''; $filterHtml .= ' '; $filterHtml .= ''; 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 = ''; $html .= ''; break; } case static::TYPE_INT: case static::TYPE_BOOLEAN: { $checked = $this->getValue() == self::TYPE_INT_YES ? 'checked' : ''; $html = ''; $html .= ''; 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 ================================================ *
  • STYLE - inline-стили
  • *
  • VARIANTS - массив с вариантами значений или функция для их получения в формате ключ=>заголовок * Например: * [ * 1=>'Первый пункт', * 2=>'Второй пункт' * ] *
  • *
  • DEFAULT_VARIANT - ID варианта по-умолчанию
  • * */ 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 = ''; } 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) . '
    '; } } } return $result; } /** * Возвращает массив в следующем формате: * * array( * '123' => array('ID' => 123, 'TITLE' => 'ololo'), * '456' => array('ID' => 456, 'TITLE' => 'blablabla'), * '789' => array('ID' => 789, 'TITLE' => 'pish-pish'), * ) * * * Результат будет выводиться в комбобоксе. * @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 ''; print '' . $this->getSettings('TITLE') . ''; print '' . $this->getComboBox(false, true) . ''; print ''; } /** * @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 ================================================ '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 ''; print '' . $this->settings['TITLE'] . ''; print '' . CalendarPeriod($inputNameFrom, $$inputNameFrom, $inputNameTo, $$inputNameTo, "find_form") . ''; } /** * Сконвертируем дату в формат 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 ================================================ *
  • DESCRIPTION_FIELD - bool нужно ли поле описания
  • *
  • MULTIPLE - bool является ли поле множественным
  • *
  • IMAGE - bool отображать ли изображение файла, для старого вида отображения
  • * */ 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 .= ''; } 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 .= ' '; } } 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 = '' . $file['FILE_NAME'] . ' (' . $file['FILE_DESCRIPTION'] . ')' . ''; } $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 ================================================ *
  • MODEL - Название модели, из которой будет производиться выборка данных. По-умолчанию - модель текущего * хэлпера
  • * * 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 == AdminListHelper::OP_ADMIN_VARIABLES_HEADER AND isset($info['LIST_COLUMN_LABEL']) AND !empty($info['LIST_COLUMN_LABEL']) ) { $value = $info['LIST_COLUMN_LABEL']; } else if ($context == AdminEditHelper::OP_SHOW_TAB_ELEMENTS AND isset($info['EDIT_FORM_LABEL']) AND !empty($info['EDIT_FORM_LABEL']) ) { $value = $info['EDIT_FORM_LABEL']; } else { $value = $info['FIELD_NAME']; } } } return $value; } /** * Генерирует HTML для поля в списке * Копипаст из API Битрикса, бессмысленного и беспощадного... * * @see AdminListHelper::addRowCell(); * * @param \CAdminListRow $row * @param array $data - данные текущей строки * * @return mixed */ public function generateRow(&$row, $data) { $info = $this->getUserFieldInfo(); if ($info) { /** @var \CAllUserTypeManager $USER_FIELD_MANAGER */ global $USER_FIELD_MANAGER; $FIELD_NAME = $this->getCode(); $GLOBALS[$FIELD_NAME] = isset($GLOBALS[$FIELD_NAME]) ? $GLOBALS[$FIELD_NAME] : $this->data[$this->getCode()]; $info["VALUE_ID"] = intval($this->data['ID']); if (isset($_REQUEST['def_' . $FIELD_NAME])) { $info['SETTINGS']['DEFAULT_VALUE'] = $_REQUEST['def_' . $FIELD_NAME]; } $USER_FIELD_MANAGER->AddUserField($info, $data[$this->getCode()], $row); } } /** * Генерирует HTML для поля фильтрации * * @see AdminListHelper::createFilterForm(); * @return mixed */ public function showFilterHtml() { $info = $this->getUserFieldInfo(); if ($info) { /** @var \CAllUserTypeManager $USER_FIELD_MANAGER */ global $USER_FIELD_MANAGER; $FIELD_NAME = $this->getCode(); $GLOBALS[$FIELD_NAME] = isset($GLOBALS[$FIELD_NAME]) ? $GLOBALS[$FIELD_NAME] : $this->data[$this->getCode()]; $info["VALUE_ID"] = intval($this->data['ID']); $info['LIST_FILTER_LABEL'] = $this->getSettings('TITLE'); print $USER_FIELD_MANAGER->GetFilterHTML($info, $this->getFilterInputName(), $this->getCurrentFilterValue()); } } public function getUserFieldInfo() { $id = $this->getHLId(); $fields = static::getUserFields($id, $this->data); if (isset($fields[$this->getCode()])) { return $fields[$this->getCode()]; } return false; } /** * Получаем ID HL-инфоблока по имени его класса * @return mixed */ protected function getHLId() { static $id = false; if ($id === false) { $model = $this->getSettings('MODEL'); $info = AdminBaseHelper::getHLEntityInfo($model); if ($info AND isset($info['ID'])) { $id = $info['ID']; } } return $id; } static public function getUserFields($iblockId, $data) { /** @var \CAllUserTypeManager $USER_FIELD_MANAGER */ global $USER_FIELD_MANAGER; $iblockId = 'HLBLOCK_' . $iblockId; if (!isset(static::$userFieldsCache[$iblockId][$data['ID']])) { $fields = $USER_FIELD_MANAGER->getUserFieldsWithReadyData($iblockId, $data, LANGUAGE_ID, false, 'ID'); self::$userFieldsCache[$iblockId][$data['ID']] = $fields; } return self::$userFieldsCache[$iblockId][$data['ID']]; } /** * Заменяем оригинальную функцию, т.к. текст ошибки приходит от битрикса, причем название поля там почему-то не * проставлено * *@param string $messageId */ protected function addError($messageId) { if (is_array($messageId)) { foreach ($messageId as $key => $error) { if (isset($error['text'])) { //FIXME: почему-то битрикс не подхватывает корректное название поля, поэтому запихиваем его сами. if (isset($error['id']) AND strpos($error['text'], '""')) { $messageId[$key] = str_replace('""', '"' . $this->getSettings('TITLE') . '"', $error['text']); } else { $messageId[$key] = $error['text']; } } } } $messageId = implode("\n", $messageId); $this->validationErrors[$this->getCode()] = $messageId; } } ================================================ FILE: lib/widget/HelperWidget.php ================================================ *
  • Отображение поля на странице редактирования
  • *
  • Отображение ячейки поля в таблице списка - при просмотре и редактировании
  • *
  • Отображение фильтра по данному полю
  • *
  • Валидацию значения поля
  • * * * Также виджетами осуществляется предварительная обработка данных: *
      *
    • Перед сохранением значения поля в БД
    • *
    • После получения значения поля из БД
    • *
    • Модификация запроса перед фильтрацией
    • *
    • Модификация пуеДшые перед выборкой данных
    • *
    * * Для получения минимальной функциональности достаточно переопределить основные методы, отвечающие за отображение * виджета в списке и на детальной. * * Каждый виджет имеет ряд специфических настроек, некоторые из которых обязательны для заполнения. Подробную * документацию по настройкам стоит искать в документации к конкретному виджету. Настройки могут быть переданы в * виджет как при описании всего интерфейса в файле Interface.php, так и непосредственно во время исполнения, * внутри Helper-классов. * * При указании настроек типа "да"/"нет", нельзя использовать строковые обозначения "Y"/"N": * для этого есть булевы true и false. * * Настройки базового класса: *
      *
    • HIDE_WHEN_CREATE - скрывает поле в форме редактирования, если создаётся новый элемент, а не открыт * существующий на редактирование.
    • *
    • TITLE - название поля. Если не задано то возьмется значение title из DataManager::getMap() * через getField($code)->getTitle(). Будет использовано в фильтре, заголовке таблицы и в качестве подписи поля * на * странице редактирования.
    • *
    • REQUIRED - является ли поле обязательным.
    • *
    • READONLY - поле нельзя редактировать, предназначено только для чтения
    • *
    • FILTER - позволяет указать способ фильтрации по полю. В базовом классе возможен только вариант "BETWEEN" * или "><". И в том и в другом случае это будет означать фильтрацию по диапазону значений. Количество возможных * вариантов этого параметра может быть расширено в наследуемых классах
    • *
    • UNIQUE - поле должно содержать только уникальные значения
    • *
    • VIRTUAL - особая настройка, отражается как на поведении виджета, так и на поведении хэлперов. Поле, * объявленное виртуальным, отображается в графическом интерфейче, однако не участвоует в запросах к БД. Опция * может быть полезной при реализации нестандартной логики, когда, к примеру, под именем одного поля могут * выводиться данные из нескольких полей сразу.
    • *
    • EDIT_IN_LIST - параметр не обрабатывается непосредственно виджетом, однако используется хэлпером. * Указывает, можно ли редактировать данное поле в спискке
    • *
    • MULTIPLE - bool является ли поле множественным
    • *
    • MULTIPLE_FIELDS - array поля используемые в хранилище множественных значений и их алиасы
    • *
    • LIST - отображать ли поле в списке доступных в настройках столбцов таблицы (по-умолчанию true)
    • *
    • HEADER - является ли столбец отображаемым по-умолчанию, если вывод столбцов таблицы не настроен (по-умолчанию true)
    • *
    * * Как сделать виджет множественным? *
      *
    • Реализуйте метод genMultipleEditHTML(). Метод должен выводить множественную форму ввода. Для реализации формы * ввода есть JS хелпер HelperWidget::jsHelper()
    • *
    • Опишите поля, которые будут переданы связи в EntityManager. Поля описываются в настройке "MULTIPLE_FIELDS" * виджета. По умолчанию множественный виджет использует поля ID, ENTITY_ID, VALUE
    • *
    • Полученные от виджета данные будут переданы в EntityManager и сохранены как связанные данные
    • *
    * Пример реализации можно увидеть в виджете StringWidget * * Как использовать множественный виджет? *
      *
    • * Создайте таблицу и модель, которая будет хранить данные поля * - Таблица обязательно должна иметь поля, которые требует виджет. * Обязательные поля виджета по умолчанию описаны в: HelperWidget::$settings['MULTIPLE_FIELDS'] * Если у виджета нестандартный набор полей, то они хранятся в: SomeWidget::$settings['MULTIPLE_FIELDS'] * - Если поля, которые требует виджет есть в вашей таблице, но они имеют другие названия, * можно настроить виджет для работы с вашими полями. * Для этого переопределите настройку MULTIPLE_FIELDS при объявлении поля в интерфейсе следующим способом: * ``` * 'RELATED_LINKS' => array( * 'WIDGET' => new StringWidget(), * 'TITLE' => 'Ссылки', * 'MULTIPLE' => true, * // Обратите внимание, именно тут переопределяются поля виджета * 'MULTIPLE_FIELDS' => array( * 'ID', // Должны быть прописаны все поля, даже те, которые не нужно переопределять * 'ENTITY_ID' => 'NEWS_ID', // ENTITY_ID - поле, которое требует виджет, NEWS_ID - пример поля, которое * будет использоваться вместо ENTITY_ID * 'VALUE' => 'LINK', // VALUE - поле, которое требует виджет, LINK - пример поля, которое будет * использоваться вместо VALUE * ) * ), * ``` *
    • * *
    • * Далее в основной модели (та, которая указана в AdminBaseHelper::$model) нужно прописать связь с моделью, * в которой вы хотите хранить данные поля * Пример объявления связи: * ``` * new Entity\ReferenceField( * 'RELATED_LINKS', * 'namespace\NewsLinksTable', * array('=this.ID' => 'ref.NEWS_ID'), * // Условия FIELD и ENTITY не обязательны, подробности смотрите в комментариях к классу @see EntityManager * 'ref.FIELD' => new DB\SqlExpression('?s', 'NEWS_LINKS'), * 'ref.ENTITY' => new DB\SqlExpression('?s', 'news'), * ), * ``` *
    • * *
    • * Чтобы виджет работал во множественном режиме, нужно при описании интерфейса поля указать параметр MULTIPLE => true * ``` * 'RELATED_LINKS' => array( * 'WIDGET' => new StringWidget(), * 'TITLE' => 'Ссылки', * // Включаем режим множественного ввода * 'MULTIPLE' => true, * ) * ``` *
    • * *
    • * Готово :) *
    • *
    * * О том как сохраняются данные множественных виджетов можно узнать из комментариев * класса \DigitalWand\AdminHelper\EntityManager. * * @see EntityManager * @see HelperWidget::getEditHtml() * @see HelperWidget::generateRow() * @see showFilterHtml::showFilterHTML() * @see HelperWidget::setSetting() * * @author Nik Samokhvalov * @author Dmitriy Baibuhtin */ abstract class HelperWidget { const LIST_HELPER = 1; const EDIT_HELPER = 2; /** * @var string Код поля. */ protected $code; /** * @var array $settings Настройки виджета для данной модели. */ protected $settings = array( // Поля множественного виджета по умолчанию (array('ОРИГИНАЛЬНОЕ НАЗВАНИЕ', 'ОРИГИНАЛЬНОЕ НАЗВАНИЕ' => 'АЛИАС')) 'MULTIPLE_FIELDS' => array('ID', 'VALUE', 'ENTITY_ID') ); /** * @var array Настройки "по-умолчанию" для модели. */ static protected $defaults; /** * @var DataManager Название класса модели. */ protected $entityName; /** * @var array Данные модели. */ protected $data; /** @var AdminBaseHelper|AdminListHelper|AdminEditHelper $helper Экземпляр хэлпера, вызывающий данный виджет. */ protected $helper; /** * @var bool Статус отображения JS хелпера. Используется для исключения дублирования JS-кода. */ protected $jsHelper = false; /** * @var array $validationErrors Ошибки валидации поля. */ protected $validationErrors = array(); /** * @var string Строка, добавляемая к полю name полей фильтра. */ protected $filterFieldPrefix = 'find_'; /** * Эксемпляр виджета создаётся всего один раз, при описании настроек интерфейса. При создании есть возможность * сразу указать для него необходимые настройки. * * @param array $settings */ public function __construct(array $settings = array()) { Loc::loadMessages(__FILE__); $this->settings = $settings; } /** * Генерирует HTML для редактирования поля. * * @return string * * @api */ abstract protected function getEditHtml(); /** * Генерирует HTML для редактирования поля в мульти-режиме. * * @return string * * @api */ protected function getMultipleEditHtml() { return Loc::getMessage('DIGITALWAND_AH_MULTI_NOT_SUPPORT'); } /** * Оборачивает поле в HTML код, который в большинстве случаев менять не придется. Далее вызывается * кастомизируемая часть. * * @param bool $isPKField Является ли поле первичным ключом модели. * * @see HelperWidget::getEditHtml(); */ public function showBasicEditField($isPKField) { if ($this->getSettings('HIDE_WHEN_CREATE') AND !isset($this->data[$this->helper->pk()])) { return; } // JS хелперы $this->jsHelper(); if ($this->getSettings('USE_BX_API')) { $this->getEditHtml(); } else { print ''; $title = $this->getSettings('TITLE'); if ($this->getSettings('REQUIRED') === true) { $title = '' . $title . ''; } print '' . $title . ':'; $field = $this->getValue(); if (is_null($field)) { $field = ''; } $readOnly = $this->getSettings('READONLY'); if (!$readOnly AND !$isPKField) { if ($this->getSettings('MULTIPLE')) { $field = $this->getMultipleEditHtml(); } else { $field = $this->getEditHtml(); } } else { if ($readOnly) { if ($this->getSettings('MULTIPLE')) { $field = $this->getMultipleValueReadonly(); } else { $field = $this->getValueReadonly(); } } } print '' . $field . ''; print ''; } } /** * Возвращает значение поля в форме "только для чтения" для не множественных свойств. * * @return mixed */ protected function getValueReadonly() { return static::prepareToOutput($this->getValue()); } /** * Возвращает значения множественного поля. * * @return array */ protected function getMultipleValue() { $rsEntityData = null; $values = array(); if (!empty($this->data[$this->helper->pk()])) { $entityName = $this->entityName; $rsEntityData = $entityName::getList(array( 'select' => array('REFERENCE_' => $this->getCode() . '.*'), 'filter' => array('=ID' => $this->data[$this->helper->pk()]) )); if ($rsEntityData) { while ($referenceData = $rsEntityData->fetch()) { if (empty($referenceData['REFERENCE_' . $this->getMultipleField('ID')])) { continue; } $values[] = $referenceData['REFERENCE_' . $this->getMultipleField('VALUE')]; } } } else { if ($this->data[$this->code]) { $values = $this->data[$this->code]; } } return $values; } /** * Возвращает значение поля в форме "только для чтения" для множественных свойств. * * @return string */ protected function getMultipleValueReadonly() { $values = $this->getMultipleValue(); foreach ($values as &$value) { $value = static::prepareToOutput($value); } return join('
    ', $values); } /** * Обработка строки для безопасного отображения. Если нужно отобразить текст как аттрибут тега, * используйте static::prepareToTag(). * * @param string $string * @param bool $hideTags Скрыть теги: * * - true - вырезать теги оставив содержимое. Результат обработки: text = text * * - false - отобразаить теги в виде текста. Результат обработки: text = <b>text</b> * * @return string */ public static function prepareToOutput($string, $hideTags = true) { if ($hideTags) { return preg_replace('/<.+>/mU', '', $string); } else { return htmlspecialchars($string, ENT_QUOTES, SITE_CHARSET); } } /** * Подготовка строки для использования в аттрибутах тегов. Например: * ``` * * ``` * * @param string $string * * @return string */ public static function prepareToTagAttr($string) { // Не используйте addcslashes в этом методе, иначе в тегах будут дубли обратных слешей return htmlspecialchars($string, ENT_QUOTES, SITE_CHARSET); } /** * Подготовка строки для использования в JS. * * @param string $string * * @return string */ public static function prepareToJs($string) { $string = htmlspecialchars($string, ENT_QUOTES, SITE_CHARSET); $string = addcslashes($string, "\r\n\"\\"); return $string; } /** * Генерирует HTML для поля в списке. * * @param \CAdminListRow $row * @param array $data Данные текущей строки. * * @return void * * @see AdminListHelper::addRowCell() * * @api */ abstract public function generateRow(&$row, $data); /** * Генерирует HTML для поля фильтрации. * * @return void * * @see AdminListHelper::createFilterForm() * * @api */ abstract public function showFilterHtml(); /** * Возвращает массив настроек данного виджета, либо значение отдельного параметра, если указано его имя. * * @param string $name Название конкретного параметра. * * @return array|mixed * * @api */ public function getSettings($name = '') { if (empty($name)) { return $this->settings; } else { if (isset($this->settings[$name])) { return $this->settings[$name]; } else { if (isset(static::$defaults[$name])) { return static::$defaults[$name]; } else { return false; } } } } /** * Передаёт в виджет ссылку на вызывающий его объект. * * @param AdminBaseHelper $helper */ public function setHelper(&$helper) { $this->helper = $helper; } /** * Возвращает текукщее значение поля фильтрации (спец. символы экранированы). * * @return bool|string */ protected function getCurrentFilterValue() { if (isset($GLOBALS[$this->filterFieldPrefix . $this->code])) { return htmlspecialcharsbx($GLOBALS[$this->filterFieldPrefix . $this->code]); } else { return false; } } /** * Проверяет корректность введенных в фильтр значений * * @param string $operationType тип операции * @param mixed $value значение фильтра * * @see AdminListHelper::checkFilter(); * @return bool */ public function checkFilter($operationType, $value) { return true; } /** * Позволяет модифицировать опции, передаваемые в getList, непосредственно перед выборкой. * Если в настройках явно указан способ фильтрации, до добавляет соответствующий префикс в $arFilter. * Если фильтр BETWEEN, то формирует сложную логику фильтрации. * * @param array $filter $arFilter целиком * @param array $select * @param $sort * @param array $raw $arSelect, $arFilter, $arSort до примененных к ним преобразований. * * @see AdlinListHelper::getData(); */ public function changeGetListOptions(&$filter, &$select, &$sort, $raw) { if ($this->isFilterBetween()) { $field = $this->getCode(); $from = $to = false; if (isset($_REQUEST['find_' . $field . '_from'])) { $from = $_REQUEST['find_' . $field . '_from']; if (is_a($this, 'DateWidget')) { $from = date('Y-m-d H:i:s', strtotime($from)); } } if (isset($_REQUEST['find_' . $field . '_to'])) { $to = $_REQUEST['find_' . $field . '_to']; if (is_a($this, 'DateWidget')) { $to = date('Y-m-d 23:59:59', strtotime($to)); } else if ( is_a($this, '\DigitalWand\AdminHelper\Widget\DateTimeWidget') && !preg_match('/\d{2}:\d{2}:\d{2}/', $to) ) { $to = date('d.m.Y 23:59:59', strtotime($to)); } } if ($from !== false AND $to !== false) { $filter['><' . $field] = array($from, $to); } else { if ($from !== false) { $filter['>' . $field] = $from; } else { if ($to !== false) { $filter['<' . $field] = $to; } } } } else { if ($filterPrefix = $this->getSettings('FILTER') AND $filterPrefix !== true AND isset($filter[$this->getCode()])) { $filter[$filterPrefix . $this->getCode()] = $filter[$this->getCode()]; unset($filter[$this->getCode()]); } } } /** * Проверяет оператор фильтрации. * * @return bool */ protected function isFilterBetween() { return $this->getSettings('FILTER') === '><' OR $this->getSettings('FILTER') === 'BETWEEN'; } /** * Действия, выполняемые над полем в процессе редактирования элемента, до его сохранения. * По-умолчанию выполняется проверка обязательных полей и уникальности. * * @see AdminEditHelper::editAction(); * @see AdminListHelper::editAction(); */ public function processEditAction() { if (!$this->checkRequired()) { $this->addError('DIGITALWAND_AH_REQUIRED_FIELD_ERROR'); } if ($this->getSettings('UNIQUE') && !$this->isUnique()) { $this->addError('DIGITALWAND_AH_DUPLICATE_FIELD_ERROR'); } } /** * В совсем экзотических случаях может потребоваться моджифицировать значение поля уже после его сохраненния в БД - * для последующей обработки каким-либо другим классом. */ public function processAfterSaveAction() { } /** * Добавляет строку ошибки в массив ошибок. * * @param string $messageId Код сообщения об ошибке из лэнг-файла. Плейсхолдер #FIELD# будет заменён на значение * параметра TITLE. * @param array $replace Данные для замены. * * @see Loc::getMessage() */ protected function addError($messageId, $replace = array()) { $this->validationErrors[$this->getCode()] = Loc::getMessage( $messageId, array_merge(array('#FIELD#' => $this->getSettings('TITLE')), $replace) ); } /** * Проверка заполненности обязательных полей. * Не должны быть null или содержать пустую строку. * * @return bool */ public function checkRequired() { if ($this->getSettings('REQUIRED') == true) { $value = $this->getValue(); return !is_null($value) && !empty($value); } else { return true; } } /** * Выставляет код для данного виджета при инициализации. Перегружает настройки. * * @param string $code */ public function setCode($code) { $this->code = $code; $this->loadSettings(); } /** * @return mixed */ public function getCode() { return $this->code; } /** * Устанавливает настройки интерфейса для текущего поля. * * @param string $code * * @return bool * * @see AdminBaseHelper::getInterfaceSettings() * @see AdminBaseHelper::setFields() */ public function loadSettings($code = null) { $interface = $this->helper->getInterfaceSettings(); $code = is_null($code) ? $this->code : $code; if (!isset($interface['FIELDS'][$code])) { return false; } unset($interface['FIELDS'][$code]['WIDGET']); $this->settings = array_merge($this->settings, $interface['FIELDS'][$code]); $this->setDefaultValue(); return true; } /** * Возвращает название сущности данной модели. * * @return string|DataManager */ public function getEntityName() { return $this->entityName; } /** * @param string $entityName */ public function setEntityName($entityName) { $this->entityName = $entityName; $this->setDefaultValue(); } /** * Устанавливает значение по-умолчанию для данного поля */ public function setDefaultValue() { if (isset($this->settings['DEFAULT']) && is_null($this->getValue())) { $this->setValue($this->settings['DEFAULT']); } } /** * Передает ссылку на данные сущности в виджет * * @param $data */ public function setData(&$data) { $this->data = &$data; //FIXME: нелепый оверхэд ради того, чтобы можно было централизованно преобразовывать значение при записи $this->setValue($data[$this->getCode()]); } /** * Возвращает текущее значение, хранимое в поле виджета * Если такого поля нет, возвращает null * * @return mixed|null */ public function getValue() { $code = $this->getCode(); return isset($this->data[$code]) ? $this->data[$code] : null; } /** * Устанавливает значение поля * * @param $value * * @return bool */ protected function setValue($value) { $code = $this->getCode(); $this->data[$code] = $value; return true; } /** * Получения названия поля таблицы, в которой хранятся множественные данные этого виджета * * @param string $fieldName Название поля * * @return bool|string */ public function getMultipleField($fieldName) { $fields = $this->getSettings('MULTIPLE_FIELDS'); if (empty($fields)) { return $fieldName; } // Поиск алиаса названия поля if (isset($fields[$fieldName])) { return $fields[$fieldName]; } // Поиск оригинального названия поля $fieldsFlip = array_flip($fields); if (isset($fieldsFlip[$fieldName])) { return $fieldName; } return $fieldName; } /** * Выставляет значение отдельной настройки * * @param string $name * @param mixed $value */ public function setSetting($name, $value) { $this->settings[$name] = $value; } /** * Возвращает собранные ошибки валидации * @return array */ public function getValidationErrors() { return $this->validationErrors; } /** * Возвращает имена для атрибута name полей фильтра. * Если это фильтр BETWEEN, то вернёт массив с вариантами from и to. * * @return array|string */ protected function getFilterInputName() { if ($this->isFilterBetween()) { $baseName = $this->filterFieldPrefix . $this->code;; $inputNameFrom = $baseName . '_from'; $inputNameTo = $baseName . '_to'; return array($inputNameFrom, $inputNameTo); } else { return $this->filterFieldPrefix . $this->code; } } /** * Возвращает текст для атрибута name инпута редактирования. * * @param null $suffix опциональное дополнение к названию поля * * @return string */ protected function getEditInputName($suffix = null) { return 'FIELDS[' . $this->getCode() . $suffix . ']'; } /** * Уникальный ID для DOM HTML * @return string */ protected function getEditInputHtmlId() { $htmlId = end(explode('\\', $this->entityName)) . '-' . $this->getCode(); return strtolower(preg_replace('/[^A-z-]/', '-', $htmlId)); } /** * Возвращает текст для атрибута name инпута редактирования поля в списке * @return string */ protected function getEditableListInputName() { $id = $this->data[$this->helper->pk()]; return 'FIELDS[' . $id . '][' . $this->getCode() . ']'; } /** * Определяет тип вызывающего хэлпера, от чего может зависить поведение виджета. * * @return bool|int * @see HelperWidget::EDIT_HELPER * @see HelperWidget::LIST_HELPER */ protected function getCurrentViewType() { if (is_a($this->helper, 'DigitalWand\AdminHelper\Helper\AdminListHelper')) { return self::LIST_HELPER; } else { if (is_a($this->helper, 'DigitalWand\AdminHelper\Helper\AdminEditHelper')) { return self::EDIT_HELPER; } } return false; } /** * Проверяет значение поля на уникальность * @return bool */ private function isUnique() { if ($this->getSettings('VIRTUAL')) { return true; } $value = $this->getValue(); if (empty($value)) { return true; } /** @var DataManager $class */ $class = $this->entityName; $field = $this->getCode(); $idField = 'ID'; $id = $this->data[$idField]; $filter = array( $field => $value, ); if (!empty($id)) { $filter["!=" . $idField] = $id; } $count = $class::getCount($filter); if (!$count) { return true; } return false; } /** * Проверяет, не является ли текущий запрос попыткой выгрузить данные в Excel * @return bool */ protected function isExcelView() { if (isset($_REQUEST['mode']) && $_REQUEST['mode'] == 'excel') { return true; } return false; } /** * @todo Вынести в ресурс (\CJSCore::Init()). * @todo Описать. */ protected function jsHelper() { if ($this->jsHelper == true) { return true; } $this->jsHelper = true; \CJSCore::Init(array("jquery")); ?> *
  • IBLOCK_ID - (int) ID инфоблока *
  • INPUT_SIZE - (int) значение атрибута size для input
  • *
  • WINDOW_WIDTH - (int) значение width для всплывающего окна выбора элемента
  • *
  • WINDOW_HEIGHT - (int) значение height для всплывающего окна выбора элемента
  • * * * @author Nik Samokhvalov */ class IblockElementWidget extends NumberWidget { static protected $defaults = array( 'FILTER' => '=', 'INPUT_SIZE' => 5, 'WINDOW_WIDTH' => 600, 'WINDOW_HEIGHT' => 500, ); public function __construct(array $settings = array()) { Loc::loadMessages(__FILE__); Loader::includeModule('iblock'); parent::__construct($settings); } /** * {@inheritdoc} */ public function getEditHtml() { $iblockId = (int) $this->getSettings('IBLOCK_ID'); $inputSize = (int) $this->getSettings('INPUT_SIZE'); $windowWidth = (int) $this->getSettings('WINDOW_WIDTH'); $windowHeight = (int) $this->getSettings('WINDOW_HEIGHT'); $name = 'FIELDS'; $key = $this->getCode(); $elementId = $this->getValue(); if (!empty($elementId)) { $rsElement = ElementTable::getById($elementId); if (!$element = $rsElement->fetch()) { $element['NAME'] = Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); } } else { $elementId = ''; } return '' . '' . ' ' . static::prepareToOutput($element['NAME']) . ''; } /** * {@inheritdoc} */ public function getValueReadonly() { $elementId = $this->getValue(); if (!empty($elementId)) { $rsElement = ElementTable::getList(array( 'filter' => array( 'ID' => $elementId ), 'select' => array( 'ID', 'NAME', 'IBLOCK_ID', 'IBLOCK.IBLOCK_TYPE_ID', ) )); $element = $rsElement->fetch(); return '[' . $elementId . '] ' . static::prepareToOutput($element['NAME']) . ''; } } /** * {@inheritdoc} */ public function generateRow(&$row, $data) { $elementId = $this->getValue(); if (!empty($elementId)) { $rsElement = ElementTable::getList(array( 'filter' => array( 'ID' => $elementId ), 'select' => array( 'ID', 'NAME', 'IBLOCK_ID', 'IBLOCK.IBLOCK_TYPE_ID', ) )); $element = $rsElement->fetch(); $html = '[' . $elementId . '] ' . static::prepareToOutput($element['NAME']) . ''; } else { $html = ''; } $row->AddViewField($this->getCode(), $html); } } ================================================ FILE: lib/widget/NumberWidget.php ================================================ '=', 'EDIT_IN_LIST' => true ); public function checkFilter($operationType, $value) { return $this->isNumber($value); } public function checkRequired() { if ($this->getSettings('REQUIRED') == true) { $value = $this->getValue(); return !is_null($value) && $value !== ''; } else { return true; } } public function processEditAction() { if (!$this->checkRequired()) { $this->addError('DIGITALWAND_AH_REQUIRED_FIELD_ERROR'); } else if (!$this->isNumber($this->getValue())) { $this->addError('VALUE_IS_NOT_NUMERIC'); } } protected function isNumber($value) { return is_numeric($value) OR is_null($value) OR empty($value); } } ================================================ FILE: lib/widget/OrmElementWidget.php ================================================ */ class OrmElementWidget extends NumberWidget { public function processEditAction() { if (!$this->getSettings('MULTIPLE')) { parent::processEditAction(); } else { if (!$this->checkRequired()) { $this->addError('DIGITALWAND_AH_REQUIRED_FIELD_ERROR'); } } } protected static $defaults = array( 'FILTER' => '=', 'INPUT_SIZE' => 5, 'WINDOW_WIDTH' => 600, 'WINDOW_HEIGHT' => 500, 'TITLE_FIELD_NAME' => 'TITLE', 'TEMPLATE' => 'select', 'ADDITIONAL_URL_PARAMS' => array(), 'DELETE_REFERENCED_DATA' => false ); /** * @inheritdoc */ public function loadSettings($code = null) { $load = parent::loadSettings($code); if (!is_subclass_of($this->getSettings('HELPER'), '\DigitalWand\AdminHelper\Helper\AdminBaseHelper')) { throw new ArgumentTypeException('HELPER', '\DigitalWand\AdminHelper\Helper\AdminBaseHelper'); } if (!is_array($this->getSettings('ADDITIONAL_URL_PARAMS'))) { throw new ArgumentTypeException('ADDITIONAL_URL_PARAMS', 'array'); } return $load; } /** * @inheritdoc */ public function getEditHtml() { if ($this->getSettings('TEMPLATE') == 'radio') { $html = $this->genEditHtmlInputs(); } else { $html = $this->getEditHtmlSelect(); } return $html; } /** * Генерирует HTML с выбором элемента во вcплывающем окне, шаблон select. * * @return string */ protected function getEditHtmlSelect() { /** @var AdminBaseHelper $linkedHelper */ $linkedHelper = $this->getSettings('HELPER'); $inputSize = (int) $this->getSettings('INPUT_SIZE'); $windowWidth = (int) $this->getSettings('WINDOW_WIDTH'); $windowHeight = (int) $this->getSettings('WINDOW_HEIGHT'); $name = 'FIELDS'; $key = $this->getCode(); $entityData = $this->getOrmElementData(); if (!empty($entityData)) { $elementId = $entityData[$linkedHelper::pk()]; $elementName = $entityData[$this->getSettings('TITLE_FIELD_NAME')] ? $entityData[$this->getSettings('TITLE_FIELD_NAME')] : Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); } else { $elementId = ''; } $popupUrl = $linkedHelper::getUrl(array_merge( array( 'popup' => 'Y', 'eltitle' => $this->getSettings('TITLE_FIELD_NAME'), 'n' => $name, 'k' => $key ), $this->getSettings('ADDITIONAL_URL_PARAMS') )); return '' . '' . ' ' . static::prepareToOutput($elementName) . ''; } /** * Генерирует HTML с выбором элемента в виде радио инпутов. * * @return string */ public function genEditHtmlInputs() { $return = ''; $elementList = $this->getOrmElementList(); if (!is_null($elementList)) { /** @var \DigitalWand\AdminHelper\Helper\AdminBaseHelper $linkedHelper */ $linkedHelper = $this->getSettings('HELPER'); foreach ($elementList as $key => $element) { $return .= InputType("radio", $this->getEditInputName(), $element[$linkedHelper::pk()], $this->getValue(), false, $element['TITLE']); } } else { $return = Loc::getMessage('DIGITALWAND_AH_ORM_MISSING_ELEMENTS'); } return $return; } /** * @inheritdoc */ public function getMultipleEditHtml() { /** @var AdminBaseHelper $linkedHelper */ $linkedHelper = $this->getSettings('HELPER'); $inputSize = (int)$this->getSettings('INPUT_SIZE'); $windowWidth = (int)$this->getSettings('WINDOW_WIDTH'); $windowHeight = (int)$this->getSettings('WINDOW_HEIGHT'); $name = 'FIELDS'; $key = $this->getCode(); $uniqueId = $this->getEditInputHtmlId(); $entityListData = $this->getOrmElementData(); $popupUrl = $linkedHelper::getUrl(array_merge( array( 'popup' => 'Y', 'eltitle' => $this->getSettings('TITLE_FIELD_NAME'), 'n' => $name, 'k' => '{{field_id}}' ), $this->getSettings('ADDITIONAL_URL_PARAMS') )); $popupUrl = str_replace(urlencode('{{field_id}}'), '{{field_id}}', $popupUrl); ob_start(); ?>
    getOrmElementData(); if (!empty($entityData)) { $entityName = $entityData[$this->getSettings('TITLE_FIELD_NAME')] ? $entityData[$this->getSettings('TITLE_FIELD_NAME')] : Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); return '[' . $entityData['ID'] . ']' . static::prepareToOutput($entityName); } return ''; } /** * @inheritdoc */ public function getMultipleValueReadonly() { $entityListData = $this->getOrmElementData(); if (!empty($entityListData)) { $multipleData = array(); foreach ($entityListData as $entityData) { $entityName = $entityData[$this->getSettings('TITLE_FIELD_NAME')] ? $entityData[$this->getSettings('TITLE_FIELD_NAME')] : Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); $multipleData[] = '[' . $entityData['ID'] . ']' . static::prepareToOutput($entityName); } return implode('
    ', $multipleData); } return ''; } /** * @inheritdoc */ public function generateRow(&$row, $data) { if ($this->getSettings('MULTIPLE')) { $strElement = $this->getMultipleValueReadonly(); } else { $strElement = $this->getValueReadonly(); } $row->AddViewField($this->getCode(), $strElement); } /** * @inheritdoc */ public function showFilterHtml() { /** @var AdminBaseHelper $linkedHelper */ $linkedHelper = $this->getSettings('HELPER'); if ($this->getSettings('MULTIPLE')) { } else { $inputSize = (int) $this->getSettings('INPUT_SIZE'); $windowWidth = (int) $this->getSettings('WINDOW_WIDTH'); $windowHeight = (int) $this->getSettings('WINDOW_HEIGHT'); $name = 'FIND'; $key = $this->getCode(); print ''; print '' . $this->getSettings('TITLE') . ''; $popupUrl = $linkedHelper::getUrl(array_merge( array( 'popup' => 'Y', 'eltitle' => $this->getSettings('TITLE_FIELD_NAME'), 'n' => $name, 'k' => $key ), $this->getSettings('ADDITIONAL_URL_PARAMS') )); $editStr = '' . ''; print '' . $editStr . ''; print ''; } } /** * Получает информацию о записях, к которым осуществлена привязка. * * @return array * @throws \Bitrix\Main\ArgumentException */ protected function getOrmElementData() { $refInfo = array(); $valueList = null; /** @var \DigitalWand\AdminHelper\Helper\AdminBaseHelper $linkedHelper */ $linkedHelper = $this->getSettings('HELPER'); $linkedModel = $linkedHelper::getModel(); if ($this->getSettings('MULTIPLE')) { $entityName = $this->entityName; $rsMultEntity = $entityName::getList(array( 'select' => array('REFERENCE_' => $this->getCode() . '.*'), 'filter' => array('=' . $this->getCode() . '.' . $this->getMultipleField('ENTITY_ID') => $this->data[$this->helper->pk()]) )); while ($multEntity = $rsMultEntity->fetch()) { $valueKey = $this->getMultipleField('VALUE'); if (isset($multEntity['REFERENCE_' . $valueKey])) { $valueList[$multEntity['REFERENCE_' . $linkedHelper::pk()]] = $multEntity['REFERENCE_' . $valueKey]; } } } else { $value = $this->getValue(); if (!empty($value)) { $valueList[$value] = $value; } } if ($valueList) { if ($this->getSettings('MULTIPLE')) { $filter = array(); foreach ($valueList as $id => $val){ $filter['ID'][] = $id; } } else { $filter = array($linkedHelper::pk() => $valueList); } $rsEntity = $linkedModel::getList(array( 'filter' => $filter )); while ($entity = $rsEntity->fetch()) { if ($this->getSettings('MULTIPLE')) { if (in_array($entity[$linkedHelper::pk()], array_keys($valueList))) { unset($valueList[$entity[$linkedHelper::pk()]]); } $refInfo[] = $entity; } else { if (in_array($entity[$linkedHelper::pk()], $valueList)) { unset($valueList[$entity[$linkedHelper::pk()]]); } $refInfo = $entity; break; } } foreach ($valueList as $entityId) { if ($this->getSettings('MULTIPLE')) { $refInfo[] = array($linkedHelper::pk() => $entityId); } else { $refInfo = array($linkedHelper::pk() => $entityId); break; } } } return $refInfo; } /** * Получает информацию о всех активных элементах для их выбора в виджете. * * @return array * * @throws \Bitrix\Main\ArgumentException */ protected function getOrmElementList() { $valueList = null; /** @var \DigitalWand\AdminHelper\Helper\AdminBaseHelper $linkedHelper */ $linkedHelper = $this->getSettings('HELPER'); $linkedModel = $linkedHelper::getModel(); $rsEntity = $linkedModel::getList(array( 'filter' => array( 'ACTIVE' => 1 ), 'select' => array( $linkedHelper::pk(), 'TITLE' ) )); while ($entity = $rsEntity->fetch()) { $valueList[] = $entity; } return $valueList; } } ================================================ FILE: lib/widget/StringWidget.php ================================================ *
  • EDIT_LINK - отображать в виде ссылки на редактирование элемента
  • *
  • STYLE - inline-стили для input
  • *
  • SIZE - значение атрибута size для input
  • *
  • TRANSLIT - true, если поле будет транслитерироваться в символьный код
  • *
  • MULTIPLE - поддерживается множественный ввод. В таблице требуется наличие поля VALUE
  • * */ class StringWidget extends HelperWidget { static protected $defaults = array( 'FILTER' => '%', //Фильтрация по подстроке, а не по точному соответствию. 'EDIT_IN_LIST' => true ); /** * @inheritdoc */ protected function getEditHtml() { $style = $this->getSettings('STYLE'); $size = $this->getSettings('SIZE'); $link = ''; if ($this->getSettings('TRANSLIT')) { //TODO: refactor this! $uniqId = get_class($this->entityName) . '_' . $this->getCode(); $nameId = 'name_link_' . $uniqId; $linkedFunctionName = 'set_linked_' . get_class($this->entityName) . '_CODE';//FIXME: hardcode here!!! if (isset($this->entityName->{$this->entityName->pk()})) { $pkVal = $this->entityName->{$this->entityName->pk()}; } else { $pkVal = '_new_'; } $nameId .= $pkVal; $linkedFunctionName .= $pkVal; $link = ''; } return '' . $link; } protected function getMultipleEditHtml() { $style = $this->getSettings('STYLE'); $size = $this->getSettings('SIZE'); $uniqueId = $this->getEditInputHtmlId(); $rsEntityData = null; if (!empty($this->data['ID'])) { $entityName = $this->entityName; $rsEntityData = $entityName::getList(array( 'select' => array('REFERENCE_' => $this->getCode() . '.*'), 'filter' => array('=ID' => $this->data['ID']) )); } ob_start(); ?>
    data['ID'])) { $entityName = $this->entityName; $rsEntityData = $entityName::getList(array( 'select' => array('REFERENCE_' => $this->getCode() . '.*'), 'filter' => array('=ID' => $this->data['ID']) )); } $result = ''; if ($rsEntityData) { while ($referenceData = $rsEntityData->fetch()) { if (empty($referenceData['REFERENCE_VALUE'])) { continue; } $result .= '
    ' . static::prepareToOutput($referenceData['REFERENCE_VALUE']) . '
    '; } } return $result; } /** * Генерирует HTML для поля в списке * @see AdminListHelper::addRowCell(); * @param \CAdminListRow $row * @param array $data - данные текущей строки */ public function generateRow(&$row, $data) { if ($this->getSettings('MULTIPLE')) { } else { if ($this->getSettings('EDIT_LINK') || $this->getSettings('SECTION_LINK')) { $pk = $this->helper->pk(); if ($this->getSettings('SECTION_LINK')) { $params = $this->helper->isPopup() ? $_GET : array(); $params['ID'] = $this->data[$pk]; $listHelper = $this->helper->getHelperClass($this->helper->isPopup() ? AdminSectionListHelper::className() : AdminListHelper::className()); $pageUrl = $listHelper::getUrl($params); $value = ''; } else { $editHelper = $this->helper->getHelperClass(AdminEditHelper::className()); $pageUrl = $editHelper::getUrl(array( 'ID' => $this->data[$pk] )); } $value .= '' . static::prepareToOutput($this->getValue()) . ''; } else { $value = static::prepareToOutput($this->getValue()); } if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { $row->AddInputField($this->getCode(), array('style' => 'width:90%')); } $row->AddViewField($this->getCode(), $value); } } /** * @inheritdoc */ public function showFilterHtml() { if ($this->getSettings('MULTIPLE')) { } else { print ''; print '' . $this->getSettings('TITLE') . ''; if ($this->isFilterBetween()) { list($from, $to) = $this->getFilterInputName(); print '
    От:
    До:
    '; } else { print ''; } print ''; } } } ================================================ FILE: lib/widget/TextAreaWidget.php ================================================ *
  • COLS - ширина
  • *
  • ROWS - высота
  • * */ class TextAreaWidget extends StringWidget { /** * количество отображаемых символов в режиме списка. */ const LIST_TEXT_SIZE = 150; static protected $defaults = array( 'COLS' => 65, 'ROWS' => 5, 'EDIT_IN_LIST' => false ); /** * @inheritdoc */ protected function getEditHtml() { $cols = $this->getSettings('COLS'); $rows = $this->getSettings('ROWS'); return ''; } /** * @inheritdoc */ public function generateRow(&$row, $data) { $text = $this->getValue(); if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { $row->AddInputField($this->getCode(), array('style' => 'width:90%')); } else { if (strlen($text) > self::LIST_TEXT_SIZE && !$this->isExcelView()) { $pos = false; $pos = $pos === false ? stripos($text, " ", self::LIST_TEXT_SIZE) : $pos; $pos = $pos === false ? stripos($text, "\n", self::LIST_TEXT_SIZE) : $pos; $pos = $pos === false ? stripos($text, "AddViewField($this->code, $text); } } } ================================================ FILE: lib/widget/UrlWidget.php ================================================ *
  • PROTOCOL_REQUIRED - ссылка должна иметь протокол
  • *
  • STYLE - inline-стили
  • *
  • SIZE - значение атрибута size для input
  • *
  • MAX_URL_LEN - длина отображаемого URL
  • * * * @author Nik Samokhvalov */ class UrlWidget extends StringWidget { static protected $defaults = array( 'MAX_URL_LEN' => 256, 'PROTOCOL_REQUIRED' => false, ); /** * @inheritdoc */ public function generateRow(&$row, $data) { $value = $this->getValue(); if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { $row->AddInputField($this->getCode(), array('style' => 'width:90%')); } $row->AddViewField($this->getCode(), $value); } /** * @inheritdoc */ public function getValue() { $code = $this->getCode(); $value = isset($this->data[$code]) ? $this->data[$code] : null; if ($value !== null) { $urlText = static::prepareToOutput($value); $urlText = preg_replace('/^javascript:/i', '', $urlText); if (strlen($urlText) > $this->getSettings('MAX_URL_LEN')) { $urlText = substr($urlText, 0, $this->getSettings('MAX_URL_LEN')); } if (($this->getSettings('READONLY') && $this->getCurrentViewType() == static::EDIT_HELPER) || $this->getCurrentViewType() == static::LIST_HELPER) { $value = '' . $urlText . ''; } else { $value = $urlText; } } return $value; } /** * @inheritdoc */ protected function getValueReadonly() { return $this->getValue(); } /** * @inheritdoc */ public function processEditAction() { $value = $this->getValue(); if ( $this->getSettings('PROTOCOL_REQUIRED') && !empty($value) && preg_match('/^https?:\/\//', $value) == 0 ) { $this->addError('PROTOCOL_REQUIRED'); } } } ================================================ FILE: lib/widget/UserWidget.php ================================================ *
  • STYLE - inline-стили *
  • SIZE - значение атрибута size для input * * * @author Nik Samokhvalov */ class UserWidget extends NumberWidget { /** * @inheritdoc */ public function getEditHtml() { $style = $this->getSettings('STYLE'); $size = $this->getSettings('SIZE'); $userId = $this->getValue(); $htmlUser = ''; if (!empty($userId) && $userId != 0) { $rsUser = UserTable::getById($userId); $user = $rsUser->fetch(); $htmlUser = '[' . $user['ID'] . '] (' . $user['EMAIL'] . ') ' . $user['NAME'] . ' ' . $user['LAST_NAME']; } return '' . $htmlUser; } /** * @inheritdoc */ public function getValueReadonly() { $userId = $this->getValue(); $htmlUser = ''; if (!empty($userId) && $userId != 0) { $rsUser = UserTable::getById($userId); $user = $rsUser->fetch(); $htmlUser = '[' . $user['ID'] . ']'; if ($user['EMAIL']) { $htmlUser .= ' (' . $user['EMAIL'] . ')'; } $htmlUser .= ' ' . static::prepareToOutput($user['NAME']) . ' ' . static::prepareToOutput($user['LAST_NAME']); } return $htmlUser; } /** * @inheritdoc */ public function generateRow(&$row, $data) { $userId = $this->getValue(); $strUser = ''; if (!empty($userId) && $userId != 0) { $rsUser = UserTable::getById($userId); $user = $rsUser->fetch(); $strUser = '[' . $user['ID'] . ']'; if ($user['EMAIL']) { $strUser .= ' (' . $user['EMAIL'] . ')'; } $strUser .= ' ' . static::prepareToOutput($user['NAME']) . ' ' . static::prepareToOutput($user['LAST_NAME']); } if ($strUser) { $row->AddViewField($this->getCode(), $strUser); } else { $row->AddViewField($this->getCode(), ''); } } } ================================================ FILE: lib/widget/VisualEditorWidget.php ================================================ '100%', 'HEIGHT' => 450, 'EDITORS' => array( 'EDITOR' ), 'DEFAULT_EDITOR' => 'EDITOR', 'LIGHT_EDITOR_MODE' => 'N', 'EDITOR_TOOLBAR_CONFIG_SET' => 'FULL', // SIMPLE 'EDITOR_TOOLBAR_CONFIG' => false, ); /** * @inheritdoc */ protected function getEditHtml() { if (\CModule::IncludeModule('fileman')) { ob_start(); $codeType = $this->getContentTypeCode(); /** @var string $className Имя класса без неймспейса */ $className = $this->getEntityShortName(); $entityClass = $this->entityName; $modelPk = $entityClass::getEntity()->getPrimary(); $id = isset($this->data[$modelPk]) ? $this->data[$modelPk] : false; $bxCode = $this->code . '_' . $className; $bxCodeType = $codeType . '_' . $className; if ($this->forceMultiple) { if ($id) { $bxCode .= '_' . $id; $bxCodeType .= '_' . $id; } else { $bxCode .= '_new_'; $bxCodeType .= '_new_'; } } // TODO Избавиться от данного костыля if ($_REQUEST[$bxCode]) { $this->data[$this->code] = $_REQUEST[$bxCode]; } $editorToolbarSets = array( 'FULL' => array( 'Bold', 'Italic', 'Underline', 'Strike', 'RemoveFormat', 'CreateLink', 'DeleteLink', 'Image', 'Video', 'BackColor', 'ForeColor', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyFull', 'InsertOrderedList', 'InsertUnorderedList', 'Outdent', 'Indent', 'StyleList', 'HeaderList', 'FontList', 'FontSizeList' ), 'SIMPLE' => array( 'Bold', 'Italic', 'Underline', 'Strike', 'RemoveFormat', 'CreateLink', 'DeleteLink', 'Video', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyFull', 'InsertOrderedList', 'InsertUnorderedList', 'Outdent', 'Indent', 'FontList', 'FontSizeList', ) ); if ($this->getSettings('LIGHT_EDITOR_MODE') == 'Y') { // Облегченная версия редактора global $APPLICATION; $editorToolbarConfig = $this->getSettings('EDITOR_TOOLBAR_CONFIG'); if (!is_array($editorToolbarConfig)) { $editorToolbarSet = $this->getSettings('EDITOR_TOOLBAR_CONFIG_SET'); if (isset($editorToolbarSets[$editorToolbarSet])) { $editorToolbarConfig = $editorToolbarSets[$editorToolbarSet]; } else { $editorToolbarConfig = $editorToolbarSets['FULL']; } } $APPLICATION->IncludeComponent('bitrix:fileman.light_editor', '', array( 'CONTENT' => $this->data[$this->code], 'INPUT_NAME' => $bxCode, 'INPUT_ID' => $bxCode, 'WIDTH' => $this->getSettings('WIDTH'), 'HEIGHT' => $this->getSettings('HEIGHT'), 'RESIZABLE' => 'N', 'AUTO_RESIZE' => 'N', 'VIDEO_ALLOW_VIDEO' => 'Y', 'VIDEO_MAX_WIDTH' => $this->getSettings('WIDTH'), 'VIDEO_MAX_HEIGHT' => $this->getSettings('HEIGHT'), 'VIDEO_BUFFER' => '20', 'VIDEO_LOGO' => '', 'VIDEO_WMODE' => 'transparent', 'VIDEO_WINDOWLESS' => 'Y', 'VIDEO_SKIN' => '/bitrix/components/bitrix/player/mediaplayer/skins/bitrix.swf', 'USE_FILE_DIALOGS' => 'Y', 'ID' => 'LIGHT_EDITOR_' . $bxCode, 'JS_OBJ_NAME' => $bxCode, 'TOOLBAR_CONFIG' => $editorToolbarConfig ) ); } else { // Полная версия редактора \CFileMan::AddHTMLEditorFrame( $bxCode, $this->data[$this->code], $bxCodeType, $this->data[$codeType], array( 'width' => $this->getSettings('WIDTH'), 'height' => $this->getSettings('HEIGHT'), ) ); $defaultEditors = array( static::CONTENT_TYPE_TEXT => static::CONTENT_TYPE_TEXT, static::CONTENT_TYPE_HTML => static::CONTENT_TYPE_HTML, 'editor' => 'editor' ); $editors = $this->getSettings('EDITORS'); $defaultEditor = strtolower($this->getSettings('DEFAULT_EDITOR')); $contentType = $this->getContentType(); $defaultEditor = isset($contentType) && $contentType == static::CONTENT_TYPE_TEXT ? static::CONTENT_TYPE_TEXT : $defaultEditor; $defaultEditor = isset($contentType) && $contentType == static::CONTENT_TYPE_HTML ? "editor" : $defaultEditor; if (count($editors) > 1) { foreach ($editors as &$editor) { $editor = strtolower($editor); if (isset($defaultEditors[$editor])) { unset($defaultEditors[$editor]); } } } $script = ''; echo $script; } $html = ob_get_clean(); return $html; } else { return parent::getEditHtml(); } } /** * @inheritdoc */ public function showBasicEditField($isPKField) { if (!\CModule::IncludeModule('fileman')) { parent::showBasicEditField($isPKField); } else { $title = $this->getSettings('TITLE'); if ($this->getSettings('REQUIRED') === true) { $title = '' . $title . ''; } print '' . $title . ''; print ''; $readOnly = $this->getSettings('READONLY'); if (!$readOnly) { print $this->getEditHtml(); } else { print $this->getValueReadonly(); } print ''; } } /** * @inheritdoc */ public function processEditAction() { $entityClass = $this->entityName; $modelPk = $entityClass::getEntity()->getPrimary(); $className = $this->getEntityShortName(); $currentView = $this->getCurrentViewType(); switch ($currentView) { case HelperWidget::EDIT_HELPER: $id = isset($this->data[$modelPk]) ? $this->data[$modelPk] : false; $codeType = $this->getContentTypeCode(); $bxCode = $this->getCode() . '_' . $className; $bxCodeType = $codeType . '_' . $className; if ($this->forceMultiple AND $id) { $bxCode .= '_' . $id; $bxCodeType .= '_' . $id; } if (!$_REQUEST[$bxCode] && $this->getSettings('REQUIRED') == true) { $this->addError('DIGITALWAND_AH_REQUIRED_FIELD_ERROR'); } $this->data[$this->code] = $_REQUEST[$bxCode]; $this->data[$codeType] = $_REQUEST[$bxCodeType]; break; case HelperWidget::LIST_HELPER: default: parent::processEditAction(); break; } } /** * @inheritdoc */ protected function getValueReadonly() { return $this->getContentType() == static::CONTENT_TYPE_HTML ? $this->data[$this->code] : parent::getValueReadOnly(); } /** * @inheritdoc */ public function generateRow(&$row, $data) { $text = trim(strip_tags($data[$this->code])); if (strlen($text) > self::LIST_TEXT_SIZE && !$this->isExcelView()) { $pos = false; $pos = $pos === false ? stripos($text, " ", self::LIST_TEXT_SIZE) : $pos; $pos = $pos === false ? stripos($text, "\n", self::LIST_TEXT_SIZE) : $pos; $pos = $pos === false ? stripos($text, "AddViewField($this->code, $text); } /** * Тип текста (text/html). По умолчанию html. * * @return string */ public function getContentType() { $contentType = $this->data[$this->getContentTypeCode()]; return empty($contentType) ? static::CONTENT_TYPE_HTML : $contentType; } /** * Поле, в котором хранится тип текста. * * @return string */ public function getContentTypeCode() { return $this->code . '_TEXT_TYPE'; } /** * Название класса без неймспейса. * * @return string */ protected function getEntityShortName() { return end(explode('\\', $this->entityName)); } }