Добрый день!
Хочу поделиться реализацией пользовательского поля с типом "Привязка к складу" Битрикса. Тип создавался для HL-блоков
Структура файлов, участвующих в построении поля такая:
Сейчас разберем содержимое каждого файла. Я опущу их расположение в проекте, т.к. это не важно: разместить их можно в любом удобном месте (программисты поймут)
Начнем с главного файла с классом, описывающим новый тип поля - CUserTypeStoreLink.php
Главная особенность - формирование списка значений и фильтра для поиска. Обратите внимание на метод getItemFieldHtml. Именно в нем формируется HTML с фильтром и выводом значений
AJAX запросы обрабатывает файл ajax.php. В нем содержится класс CUserTypeStoreLinkAction по обработке AJAX запросов на формирование списка вариантов складов, удовольтворяющих условиям фильтра. Его код представлен ниже:
Весь JS расположен в файле script.js
Файл в основном шлет запросы на ajax.php поиск складов и формирует список вариантов. А также небольшие правки для удобства пользования
Есть файл style.css со стилями для поля. Содержит минимальный набор стилей для корректной работы
Завершающий этап - подключить наш новый тип поля "привязка к складам" через хендлер OnUserTypeBuildList
И, наконец, немного картинок как это выглядет:
Вид в списке:
Вид в фильтре:
Надеюсь кому-то пригодится!
Хочу поделиться реализацией пользовательского поля с типом "Привязка к складу" Битрикса. Тип создавался для HL-блоков
Структура файлов, участвующих в построении поля такая:
Сейчас разберем содержимое каждого файла. Я опущу их расположение в проекте, т.к. это не важно: разместить их можно в любом удобном месте (программисты поймут)
Начнем с главного файла с классом, описывающим новый тип поля - CUserTypeStoreLink.php
<?php namespace Project\UserTypes\StoreLink; use Bitrix\Catalog\StoreTable; use Bitrix\Main\ArgumentException; use Bitrix\Main\Loader; use Bitrix\Main\LoaderException; use Bitrix\Main\ObjectPropertyException; use Bitrix\Main\SystemException; use CJSCore; /** * Class CUserTypeStoreLink * * Класс описывает пользовательское свойство привязки к складу * * @package Project\UserTypes\StoreLink */ class CUserTypeStoreLink { const USER_TYPE = "mercool_store_link"; /** * Формируем массив значение * * @param $mxValue * @return array */ private static function getValuesArray($mxValue) { $arValues = array(); if (!empty($mxValue)) { if (is_array($mxValue)) { $arValues = $mxValue; } else { $arValues = [$mxValue]; } } return $arValues; } /** * Функция регистрируется в качестве обработчика события OnUserTypeBuildList * @return array */ public function GetUserTypeDescription() { return [ "USER_TYPE_ID" => self::USER_TYPE, "CLASS_NAME" => __CLASS__, "DESCRIPTION" => "Привязка к складу", "BASE_TYPE" => "int", ]; } /** * Функция вызывается при добавлении нового свойства для конструирования SQL запроса * создания столбца значений свойства * * @param $arUserField * @return string */ public function GetDBColumnType($arUserField) { $sNum = ""; switch (strtolower($GLOBALS["DB"]->type)) { case "mysql": $sNum = "int(18)"; break; case "oracle": $sNum = "number(18)"; break; case "mssql": $sNum = "int"; break; } return $sNum; } /** * Функция вызывается перед сохранением метаданных (настроек) свойства в БД * * @param $arUserField * @return array */ public function PrepareSettings($arUserField) { return []; } /** * Функция вызывается при выводе формы метаданных (настроек) свойства * * @param bool $arUserField * @param $arHtmlControl * @param $bVarsFromForm - флаг отправки формы * @return string - HTML для вывода */ public function GetSettingsHTML($arUserField = false, $arHtmlControl, $bVarsFromForm) { return ""; } /** * Функция валидатор значений свойства вызвается в $GLOBALS["USER_FIELD_MANAGER"]->CheckField() * при добавлении/изменении * * @param $arUserField * @param $value - начение для проверки на валидность * @return array - массив массивов ("id","text") ошибок */ public function CheckFields($arUserField, $value) { return []; } /** * Получение склада по ID * * @param int $iStoreId * @return array * @throws ArgumentException * @throws LoaderException * @throws ObjectPropertyException * @throws SystemException */ private static function getStore(int $iStoreId) { Loader::includeModule("sale"); $arFilter["=ACTIVE"] = "Y"; $arFilter["=ID"] = $iStoreId; $arStore = StoreTable::getList(array( "filter" => $arFilter, "select" => array( "ID", "TITLE", "ADDRESS" ), "limit" => 1, "cache" => array( "ttl" => TTL, "cache_joins" => true ) ))->fetch(); return self::makeStoreArray($arStore); } /** * формируем нужный формат массива Склада * * @param array $arStore * @return array */ public static function makeStoreArray(array $arStore) { return array( "ID" => $arStore["ID"], "TITLE" => $arStore["TITLE"], "ADDRESS" => $arStore["ADDRESS"] ); } /** * Формы редактирования значения свойства * * @param $arUserField * @param $arHtmlControl * @return string - HTML для вывода */ public function GetEditFormHTML($arUserField, $arHtmlControl) { self::registrExt(); return self::getItemFieldHtml($arHtmlControl["NAME"], $arHtmlControl["VALUE"]); } /** * Вывод в форме редактирования множественного поля * * @param $arUserField * @param $arHtmlControl * @return string * @throws ArgumentException * @throws LoaderException * @throws ObjectPropertyException * @throws SystemException */ public function GetEditFormHTMLMulty($arUserField, $arHtmlControl) { self::registrExt(); return self::getItemFieldHtml($arHtmlControl["NAME"], $arHtmlControl["VALUE"], true); } /** * Генерируем HTML код экземпляра поля * * @param string $sFieldName * @param $mxValue * @param bool $bIsMultiple * @return string * @throws ArgumentException * @throws LoaderException * @throws ObjectPropertyException * @throws SystemException */ private function getItemFieldHtml(string $sFieldName, $mxValue, bool $bIsMultiple = false) { $sFieldName = str_replace('[]', '', $sFieldName); $sPathFromContentRoot = str_replace($_SERVER["DOCUMENT_ROOT"], "", __DIR__); $arValues = self::getValuesArray($mxValue); list($sReturn, $iKey) = self::generateListHtml($arValues, $sFieldName, $bIsMultiple); $sReturn .= "<div class='".self::USER_TYPE."-filter' data-code='".$sFieldName."' data-ajaxurl='".$sPathFromContentRoot."/ajax.php' >"; $sReturn .= "<input class='".self::USER_TYPE."' type='text' data-code='".$sFieldName."' name='".$sFieldName."_TITLE' value='' placeholder='Название'>"; $sReturn .= "<input class='".self::USER_TYPE."' type='text' data-code='".$sFieldName."' name='".$sFieldName."_ADDRESS' value='' placeholder='Адрес склада'>"; $sReturn .= "</div>"; $sReturn .= "<div class='store-search-result' data-count='".($iKey + 1)."' data-multiple='".($bIsMultiple ? "true" : "false")."' data-code='".$sFieldName."'></div>"; return $sReturn; } /** * Функция вызывается при выводе фильтра на странице списка * * @param $arUserField * @param $arHtmlControl * @return string - HTML для вывода */ public function GetFilterHTML($arUserField, $arHtmlControl) { return self::GetEditFormHTML($arUserField, $arHtmlControl); } /** * Функция должна вернуть представление значения поля для поиска * * @param $arUserField * @return string - посковое содержимое */ public function OnSearchIndex($arUserField) { if (is_array($arUserField["VALUE"])) { return implode("\r\n", $arUserField["VALUE"]); } else { return $arUserField["VALUE"]; } } /** * Функция вызывается перед сохранением значений в БД * * @param $arUserField * @param $iValue * @return int - значение для вставки в БД */ public function OnBeforeSave($arUserField, $iValue) { if (intval($iValue) > 0) { return intval($iValue); } } /** * Функция вызывается при выводе значения свойства в списке элементов * * @param $arUserField * @param $arHtmlControl * @return string - HTML для вывода */ public function GetAdminListViewHTML($arUserField, $arHtmlControl) { $sReturn = ""; if (!empty($arHtmlControl["VALUE"])) { $arStore = self::getStore($arHtmlControl["VALUE"]); $sReturn .= "<p>[".$arStore["ID"]."] ".$arStore["TITLE"]." | ".$arStore["ADDRESS"]."</p>"; } return $sReturn; } /** * Подключаем стили и скрипты к полю */ private function registrExt() { $sPathFromContentRoot = str_replace($_SERVER["DOCUMENT_ROOT"], "", __DIR__); CJSCore::RegisterExt("store_link", [ "js" => $sPathFromContentRoot."/js/script.js", "css" => $sPathFromContentRoot."/css/style.css", "rel" => ["jquery"] ]); CJSCore::Init(["jquery", "store_link"]); } /** * Генерируем HTML списка значений * * @param array $arValues * @param $sFieldName * @param bool $bIsMultiple * @return array * @throws ArgumentException * @throws LoaderException * @throws ObjectPropertyException * @throws SystemException */ private function generateListHtml(array $arValues, $sFieldName, bool $bIsMultiple): array { $iKey = 0; $sReturn = "<ul class='".self::USER_TYPE."-list".(empty($arValues) ? " hidden" : "")."' data-code='".$sFieldName."'>"; if (!empty($arValues)) { foreach ($arValues as $iKey => $sVal) { if ($arStore = self::getStore($sVal)) { $sReturn .= "<li>[".$arStore["ID"]."] ".$arStore["TITLE"]." | ". $arStore["ADDRESS"]." <a class=\"delete\" href=\"#\">[удалить]</a>"; if ($bIsMultiple) { $sReturn .= "<input type='hidden' name='".$sFieldName."[".$iKey."]' value='".(!empty($sVal) ? $sVal : "")."'></li>"; } else { $sReturn .= "<input type='hidden' name='".$sFieldName."' value='".(!empty($sVal) ? $sVal : "")."'></li>"; } } } } $sReturn .= "</ul>"; return array($sReturn, $iKey); } } |
AJAX запросы обрабатывает файл ajax.php. В нем содержится класс CUserTypeStoreLinkAction по обработке AJAX запросов на формирование списка вариантов складов, удовольтворяющих условиям фильтра. Его код представлен ниже:
<? namespace Project\UserTypes\StoreLink; require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php"); use Bitrix\Catalog\StoreTable; use Bitrix\Main\ArgumentException; use Bitrix\Main\Loader; use Bitrix\Main\Application; use Bitrix\Main\LoaderException; use Bitrix\Main\ObjectPropertyException; use Bitrix\Main\SystemException; use Bitrix\Main\Web\Json; final class CUserTypeStoreLinkAction { private $sTitle; private $sAddress; /** * CUserTypeStoreLinkAction constructor. * @throws LoaderException * @throws SystemException */ public function __construct() { Loader::includeModule("sale"); $obRequest = Application::getInstance()->getContext()->getRequest(); $this->sTitle = $obRequest->get("title"); $this->sAddress = $obRequest->get("address"); } /** * Получить список складов * * @return array * @throws SystemException * @throws ArgumentException * @throws ObjectPropertyException */ public function getStores() { $arStores = array(); $arFilter["=ACTIVE"] = "Y"; if (!empty($this->sTitle)) { $arFilter["%TITLE"] = $this->sTitle; } if (!empty($this->sAddress)) { $arFilter["%ADDRESS"] = $this->sAddress; } $rsStores = StoreTable::getList(array( "filter" => $arFilter, "select" => array( "ID", "TITLE", "ADDRESS" ), "cache" => array( "ttl" => TTL, "cache_joins" => true ) )); while ($arStore = $rsStores->fetch()) { $arStores[] = CUserTypeStoreLink::makeStoreArray($arStore); } return $arStores; } } header("Content-Type: application/json"); $obUserTypeStoreLinkAction = new CUserTypeStoreLinkAction; $arStores = $obUserTypeStoreLinkAction->getStores(); echo Json::encode($arStores); ?> |
Весь JS расположен в файле script.js
$(document).on("click", ".hover-variant", function () { var sFieldCode = $(this).attr("data-code"); var iStoreId = $(this).attr("data-id"); var obList = $("ul[data-code='" + sFieldCode + "']"); var obSearchResults = $(".store-search-result[data-code='" + sFieldCode + "']"); var bIsMultilple = (obSearchResults.attr("data-multiple") === "true"); var sHiddenValueField = "<input type='hidden' name='" + sFieldCode + "' value='" + iStoreId + "' >"; var iKey = Number.parseInt($(this).parent().attr("data-count")); var obFilter = $(".mercool_store_link-filter[data-code='" + sFieldCode + "']"); if (bIsMultilple) { sHiddenValueField = "<input type='hidden' name='" + sFieldCode + "[" + iKey + "]' value='" + iStoreId + "' >"; } var sValue = "<li>" + $(this).html().replace(/<[^>]+>/g, '') + sHiddenValueField + " <a class='delete' href='#'>[удалить]</a></li>"; if (bIsMultilple) { obList.append(sValue); $(this).parent().attr("data-count", iKey + 1); } else { obList.html(sValue); } obList.removeClass("hidden"); obSearchResults.hide(); obFilter.find("input").val(""); }); $(document).on("click", ".mercool_store_link-list .delete", function () { var obListItem = $(this).parent(); obListItem.hide(); obListItem.find("input").val(""); return false; }); /** * AJAX-поиск на сервере * * @param {string} sFieldCode */ var searchStores = function (sFieldCode) { var obSearchResults = $(".store-search-result[data-code='" + sFieldCode + "']"); var sTitle = $('input[name="' + sFieldCode + '_' + 'TITLE' + '"').val(); var sAddress = $('input[name="' + sFieldCode + '_' + 'ADDRESS' + '"').val(); var sAjaxUrl = $(".mercool_store_link-filter[data-code='" + sFieldCode + "']").attr("data-ajaxurl"); obSearchResults.html("Загрузка..."); obSearchResults.show(); BX.ajax.post( sAjaxUrl, { "title": sTitle, "address": sAddress }, function (obData) { obData = JSON.parse(obData); var sVariants = ""; $.each(obData, function (iIndex, obStore) { sVariants = sVariants + "<div class='hover-variant' data-code='" + sFieldCode + "' data-id='" + obStore.ID + "'>[" + obStore.ID + "] " + displayMatches(obStore.TITLE, sTitle) + " | " + displayMatches(obStore.ADDRESS, sAddress) + "</div>"; }); if (sVariants.length == 0) { sVariants = "Ничего не найдено. Попробуйте изменить параметры поиска."; } obSearchResults.html(sVariants); obSearchResults.removeClass("preload"); } ); } /** * Отложенный вызов */ var delayExec = (function () { var iTimer = 0; return function (callback, ms) { clearTimeout(iTimer); iTimer = setTimeout(callback, ms); }; })(); $(document).on("keyup", ".mercool_store_link", function () { if ($(this).val().length >= 3) { var sFieldCode = $(this).attr("data-code"); delayExec(function () { searchStores(sFieldCode); }, 600); } }); /** * Выделение подстроки в строке * * @param {string} sFullStr * @param {string} sSearchSubStr */ var displayMatches = function (sFullStr, sSearchSubStr) { var obRegex = new RegExp(sSearchSubStr, 'gi') var sStr = sFullStr.replace(obRegex, function (sSearchSubStr) { return "<b>" + sSearchSubStr + "</b>" }) return sStr; } |
Есть файл style.css со стилями для поля. Содержит минимальный набор стилей для корректной работы
.store-search-result { background: white; border: 1px solid gray; padding: 10px; position: absolute; z-index: 50; height: 150px; overflow-y: scroll; display: none; } .hover-variant { padding: 5px 0; cursor: pointer; } .hover-variant:hover { background: lightgray; } .mercool_store_link-list.hidden { display: none; } .mercool_store_link-filter { display: flex; justify-content: space-between; } .mercool_store_link-filter input { width: 48%; } |
$eventManager = \Bitrix\Main\EventManager::getInstance(); $eventManager->addEventHandler("main", "OnUserTypeBuildList", array( "\\Project\\UserTypes\\StoreLink\\CUserTypeStoreLink", "GetUserTypeDescription" )); |
И, наконец, немного картинок как это выглядет:
Вид в списке:
Вид в фильтре:
Надеюсь кому-то пригодится!