![](https://site-cloud-files.bitrix.info/resize_cache/2308293/7acf4cadf975128573a8b1c2766af5d8/blog/avatar/23f/23fc4454600353a6e119b173ca0c8408/ava_g.png)
Продолжаем учиться правильно кастомизировать Битрикс24
![С улыбкой :)](/upload/main/smiles/3/bx_smile_smile.png)
Статьи по кастомизации коробочной версии Битрикс24:
-
-
-
-
Сегодня же рассмотрим варианты решения одной из самых частых задач в коробке - как изменить пользовательский интерфейс. Добавить кнопку или дополнительную информацию, изменить поведение штатных элементов управления.
Казалось бы, что тут сложного?
Скопировали шаблон компонента в свое пространство имен и кастомизировали как душе угодно. Но это, мягко говоря, не самый лучший подход. Использование папки local - так же "не идеально".
Почему же так и какие есть варианты решения задач - мы вам и расскажем.
В этой статье демонстрируются 4 подхода на нескольких практических примерах. Они позволят вам "не ходить по граблям" самому, перенять полезный опыт и решать задачи эффективно. С нами снова делятся опытом коллеги из
В изучении этой темы вам поможет
Под катом подробная статья, слово коллегам из Интерволги:
[spoiler]
Отличие подходов к доработке Б24 и сайта на БУС
Считайте Битрикс24 сайтом, разработанным на БУС – платформа, технологии – те же. Битрикс24, как и любой другой сайт, включает шаблоны сайта, публичную часть (разделы и страницы), компоненты и шаблоны компонентов. Однако при установке обновлений публичная часть тоже обновляется. Поэтому публичную часть, шаблоны сайта и компонентов так же можно считать ядром Битрикс24.
Как известно, вносить изменения в ядро нельзя. В Битрикс24 это означает, что нельзя изменять не только файлы Bitrix Framework, но и публичную часть, шаблоны сайта и компонентов. Как минимум потому, что ваши изменения будут удалены при очередном обновлении.
С копированием шаблонов компонентов ситуация еще интереснее.
Во-первых, даже если вы скопировали шаблон в каталог local с тем же именем, что и оригинальный, система будет подключать его всегда, вне зависимости от обновлений. Это, как минимум, означает, что после очередного обновления клиент не увидит новых функций, а также не будут исправлены ошибки в этом шаблоне, если они есть. Но это не самое страшное.
Во-вторых, компоненты Б24 представляют собой целостную систему и их код написан исходя из предположения, что во всей системе используются оригинальные шаблоны. Это означает, что кастомизированные шаблоны не ожидаются системой. Кастомизированный шаблон может привести к информационной несовместимости с остальными частями системы и стать источником странных, трудноуловимых ошибок.
![image11.png](https://site-cloud-files.bitrix.info/resize_cache/7027881/04dbd6b4c314fe48f655aad84339ad15/main/69e/69eaf0fb4110740e54d1cc58b29f8f71/image11.png)
Как же быть? Нужны иные способы внесения изменений, предусмотренные системой или не нарушающие логику работы Б24.
Рассмотрим их далее.
Способы встраивания элементов интерфейса
Наиболее употребимыми способами встраивания элементов интерфейса являются следующие:
№ | СПОСОБ | ПЛЮСЫ | МИНУСЫ |
1 | REST-приложения |
|
|
2 | Отложенные функции |
|
|
3 | Типы пользовательских полей |
|
|
4 | Модификации на стороне клиента (JavaScript) |
|
|
1. REST-приложения и типы пользовательских полей
В REST-интерфейсе Б24 предусмотрена функция встраивания приложений в различные части интерфейса. Далее приводится лишь перечень этих мест (плейсментов). За дополнительной информацией обращайтесь
Контекстное меню сущности CRM в списке
![image8.png](https://site-cloud-files.bitrix.info/main/817/8178938c8c92edb31548601064a654bb/image8.png)
Плейсменты:
CRM_LEAD_LIST_MENU
CRM_DEAL_LIST_MENU
CRM_CONTACT_LIST_MENU
CRM_COMPANY_LIST_MENU
CRM_INVOICE_LIST_MENU
CRM_QUOTE_LIST_MENU
CRM_ACTIVITY_LIST_MENU
Вкладка в карточке сущности CRM
![image5.png](https://site-cloud-files.bitrix.info/main/646/6464c7903a7febcf398bdfbf4fe035e7/image5.png)
Плейсменты:
CRM_LEAD_DETAIL_TAB
CRM_DEAL_DETAIL_TAB
CRM_CONTACT_DETAIL_TAB
CRM_COMPANY_DETAIL_TAB
Действие в ленте сущности CRM
![image9.png](https://site-cloud-files.bitrix.info/main/730/73048010ae87c25faf1d249ee74b7492/image9.png)
Плейсменты:
CRM_LEAD_DETAIL_ACTIVITY
CRM_DEAL_DETAIL_ACTIVITY
CRM_CONTACT_DETAIL_ACTIVITY
CRM_COMPANY_DETAIL_ACTIVITY
Карточка звонка
![image12.png](https://site-cloud-files.bitrix.info/resize_cache/7027873/04dbd6b4c314fe48f655aad84339ad15/main/1f9/1f97cc7b2683dedc087dbcdacdfa8203/image12.png)
Плейсмент: CALL_CARD
Типы пользовательских полей
Также REST-приложения могут регистрировать собственные типы пользовательских полей. Они могут быть использованы везде, где есть возможность добавить такие поля.
Пример поля REST-приложения в карточке CRM. Просмотр:
![2018-02-23_12-01-36.png](https://site-cloud-files.bitrix.info/main/287/287adbe54d6e00c044ee8b521653f344/2018-02-23_12-01-36.png)
Редактирование:
![2018-02-23_12-01-07.png](https://site-cloud-files.bitrix.info/main/587/587cdf454598297a4c660a50b4f3cd4e/2018-02-23_12-01-07.png)
2. Отложенные функции
Разработчики сайтов на платформе БУС хорошо знакомы с отложенными функциями. Если вкратце, то в любом месте (шаблоне сайта, компонента и т. п.) можно вызвать $APPLICATION->ShowViewContent, затем, используя метода шаблона SetViewTarget/EndViewTarget или $APPLICATION->AddViewContent, добавить верстку в отложенную область. Причем не важно, в какой момент будет добавлена верстка, главное, чтобы это было сделано в рамках одного хита.
Если вы посмотрите на код шаблона сайта (/bitrix/templates/bitrix24), то увидите некоторое количество вызовов ShowViewContent. Штатные компоненты системы используют эти отложенные области для встраивания своих элементов. Но и вы тоже можете добавлять свою верстку!
Важно! Это «недокументированная возможность». Названия областей могут измениться со временем без предупреждения.
В качестве примера добавим свою кнопку в строку заголовка [
use Bitrix\Main\UI\Extension; Extension::load('ui.buttons'); Extension::load('ui.buttons.icons'); // Пример 1: Добавляем кнопку. ob_start(); ?> <div class="pagetitle-container"> <a href="#" class="ui-btn ui-btn-light-border ui-btn-icon-info">Отчет</a> </div> <? $customHtml = ob_get_clean(); $APPLICATION->AddViewContent('inside_pagetitle', $customHtml, 20000); |
Кнопку мы создаем с помощью модуля UI-библотека (ui). Сначала подключаем расширения «стили кнопок» (ui.buttons) и «иконки для кнопок» (ui.buttons.icons) с помощью класса Extension главного модуля. После этого необходимые стили будут загружены браузером.
Саму кнопку «Отчет» формируем с помощью HTML. Она ничего не делает, цель — научиться встраивать элементы.
Кнопку добавляем с помощью $APPLICATION->AddViewContent. Обратите внимание на третий аргумент. Это значение сортировки. Задавать его необязательно, но с его помощью вы можете регулировать положение вашего элемента относительно других в этой области.
У нас получится следующее:
![image14.png](https://site-cloud-files.bitrix.info/resize_cache/7027921/04dbd6b4c314fe48f655aad84339ad15/main/1e8/1e8a06c73adf10e3241aba7e6d34d51b/image14.png)
3. Типы пользовательских полей
Можно использовать возможность типов пользовательских полей встраивания в карточки сущностей. Не обязательно делать настоящее поле, можно вывести свою верстку.
Однако этот способ работает только для карточек и списков. При этом само поле нужно создать в системе и следить за тем, чтобы его не удалили.
Чтобы создать свой тип пользовательского поля в коробке, необходимо объявить класс этого поля и зарегистрировать его в системе с помощью события OnUserTypeBuildList [
class MyUserType extends TypeBase { const USER_TYPE_ID = 'myusertype'; function GetUserTypeDescription () { return array( 'USER_TYPE_ID' => static::USER_TYPE_ID, 'CLASS_NAME' => __CLASS__, 'DESCRIPTION' => 'Кастомное поле', 'BASE_TYPE' => \CUserTypeManager::BASE_TYPE_STRING, 'EDIT_CALLBACK' => array( __CLASS__, 'GetPublicEdit' ), 'VIEW_CALLBACK' => array( __CLASS__, 'GetPublicView' ) ); } function GetDBColumnType ($arUserField) { global $DB; switch (strtolower($DB->type)) { case "mysql": return "text"; case "oracle": return "varchar2(2000 char)"; case "mssql": return "varchar(2000)"; } } public static function GetPublicView ($arUserField, $arAdditionalParameters = array()) { Extension::load('ui.buttons'); return '<a href="#" class="ui-btn ui-btn-danger">Button From Custom Field</a>'; } public static function GetPublicEdit ($arUserField, $arAdditionalParameters = array()) { Extension::load('ui.buttons'); $name = static::getFieldName($arUserField, $arAdditionalParameters); return '<input type="hidden" name="' . $name . '" value="1"/>' . '<a href="#" class="ui-btn ui-btn-success">Button From Custom Field</a>'; } } |
Наследовать класс своего поля от класса другого поля может быть удобно, однако вы рискуете получить неправильную форму наследования. Поэтому, рекомендуется наследовать свой класс от TypeBase — базового класса всех типов пользовательских полей.
Важно определить константу USER_TYPE_ID (именно с таким именем). Она используется системой для различения типов полей.
Метод GetUserTypeDescription должен возвращать описание поля для системы: ID типа поля, класс-обработчик поля, название в интерфейсе управления пользовательскими полями, методы визуализации поля. Этот метод должен являться обработчиком события OnUserTypeBuildList.
$eventManager = EventManager::getInstance(); $eventManager->addEventHandlerCompatible('main', 'OnUserTypeBuildList', array( 'MyUserType', 'GetUserTypeDescription' ) ); |
Нас интересуют методы GetPublicView и GetPublicEdit. С помощью них происходит визуализация поля в карточках сущностей в публичной части. Они оба должны возвращать HTML. Выведем кнопки разных цветов с помощью модуля ui.
Помните, что некоторые карточки сущностей не отображают поле в режиме просмотра, если оно не заполнено. Чтобы быть уверенным, что при сохранении данных поле оказалось заполненным, в методе GetPublicEdit также выводится скрытый input.
Вот, что у нас получилось:
В режиме просмотра сделки | В режиме редактирования сделки |
![]() | ![]() |
4. Модификации на стороне клиента (JavaScript)
С помощью JavaScript можно произвольным образом менять внешний вид КП на стороне клиента, в том числе добавлять свои элементы элементы управления с нужным вам поведением.
- Однако этот способ обладает рядом ограничений, которые вы должны принимать во внимание:
- При включенном объединении JS в настройках главного модуля появление вашего скрипта на клиенте не полностью под вашим контролем. Требуется организовывать свой код определенным образом.
- При очередном обновлении верстка может измениться. Вам необходимо следить за тем, чтобы обновление не привело к ошибкам в вашем коде и своевременно его актуализировать.
- Код исполняется на стороне клиента. Но вы точно не знаете какой именно на этой стороне браузер. Ваш код должен быть кроссбраузерным.
- С различными фрагментами DOM-дерева может быть связана штатная клиентская логика, например, канбан сам обновляется. Это приводит к динамическому изменению DOM-дерева, ваш код должен корректно реагировать на эти изменения.
- В целях обеспечения эргономичности, некоторыми штатными функциями КП можно воспользоваться из нескольких мест (например, задачу можно завершить с детальной страницы, из списка и пр.). Встраиваемые вами элементы (в лучшем случае) должны подчиняться той же логике (то есть быть продублированы в разных интерфейсах). Это особенно актуально, если задача связана с ограничением доступа.
Общий подход
Где создать JS-файл и как его правильно подключить со всеми зависимостями?
Для следующих примеров нам понадобится несколько файлов (JS, стили, языковые константы, AJAX-обработчик). Разместим их следующим образом (считайте пока, что они пустые):
- /local/js/custom_stuff.js
- /local/css/custom_stuff.css
- /local/lang/ru/custom_stuff.js.php
- /local/tools/custom_stuff/disk_templates.php
CJSCore::RegisterExt('custom_stuff', array( 'js' => '/local/js/custom_stuff.js', 'lang' => '/local/lang/' . LANGUAGE_ID . '/custom_stuff.js.php', 'css' => '/local/css/custom_stuff.css', 'rel' => array( 'ajax', 'popup' ) ) ); CJSCore::Init('custom_stuff'); |
Для проверки напишем в custom_stuff.js функцию helloWorld, чтобы проверить, что все корректно подключилось. Весь код поместим в пространство имен CustomStuff.
var CustomStuff = BX.namespace('CustomStuff'); CustomStuff.helloWorld = function() { alert(BX.message('CUSTOM_STUFF_HELLO_WORLD')); }; |
А в файл языковых констант добавим строку:
$MESS['CUSTOM_STUFF_HELLO_WORLD'] = 'Привет, Мир!'; |
Обратите внимание, что CJSCore автоматически передает на клиент необходимые языковые строки в зависимости от текущего языка. Получить к ним доступ можно с помощью JS-функции BX.message.
Перезагрузите страницу, откройте панель инструментов разработчика в браузере и выполните следующий JS-код в консоли:
BX.CustomStuff.helloWorld(); |
Вы должны увидеть приветственное сообщение.
Важно! Весь JS-код, добавленный с помощью классов CJSCore, Asset или функций AddHeadScript, SetAdditionalCSS, будет объединен в один файл, если это включено в настройках главного модуля. При этом удаление вызова CJSCore::Init не приведет к удалению вашего кода из файла с объединенным кодом. Это значит, что вы не можете управлять загрузкой вашего кода на сторону клиента путем вызова или не вызова CJSCore::Init. Следует применять другой подход.
В custom_stuff.js следует лишь объявить все необходимые функции и классы, которые вам понадобятся для решения задачи (например, «добавить кнопку», «показать диалог подтверждения»).
Если же необходим запуск каких либо функций при загрузке страницы, то следует добавить на страницу еще один небольшой скрипт, который это сделает. Добавить этот скрипт можно с помощью метода Asset::addString (на замену AddHeadString). Он не будет объединен с другим JS-кодом, таким образом, вы можете контролировать запуск кода из custom_stuff.js со стороны сервера.
Например, чтобы метод helloWorld автоматически вызывался после загрузки каждой страницы, нужно сделать следующее:
$asset = Asset::getInstance(); $asset->addString('<sc ript>BX.ready(function () { BX.CustomStuff.helloWorld(); });</sc ript>'); |
Теперь рассмотрим несколько примеров.
Пример 1. Кнопка «поблагодарить сотрудника» на странице профиля
[
На странице профиля есть небольшой блок действий. Добавим в него пункт «Поблагодарить сотрудника», вот сюда
![image17.png](https://site-cloud-files.bitrix.info/main/ec1/ec1304f75be9db6e2b7e24370bf99cd9/image17.png)
Для начала добавим в custom_stuff.js функцию, которая создает кнопку.
CustomStuff.createThankButton = function() { var panelWidget = BX.findChildByClassName(document, 'user-profile-events',true); var panelCategoryWidget = BX.findChildByClassName(panelWidget, 'user-profile-events-cont', true); var thankButton = BX.create('a', { attrs : { className : 'user-profile-events-item', href : 'jav * ascript:void(0);' }, html : '<i></i> ' + BX.message('CUSTOM_STUFF_THANK_BUTTON') }); BX.bind(thankButton, 'click', CustomStuff.showThankPopup); panelCategoryWidget.appendChild(thankButton); }; CustomStuff.showThankPopup = function() {}; |
Здесь узел, представляющий кнопку, создается с помощью функции BX.create (тег a с соответствующим текстом). Также к нему сразу привязывается обработчик клика.
С помощью функции BX.findChildByClassName выполняется поиск узла DOM-дерева — блока действий на странице профиля, а, затем, в нем выполняется поиск контейнера (группы действий). Мы берем первый найденный контейнер и добавляем в него созданную кнопку с помощью метода appendChild.
Теперь добавим вызов написанной функции на странице профиля.
$profileTemplates = array( 'profile' => ltrim(Option::get('intranet', 'path_user', '', SITE_ID), '/') ); if (CComponentEngine::parseComponentPath('/', $profileTemplates, $arVars) == 'profile') { $asset->addString('<sc ript>BX.ready(function () { BX.CustomStuff.createThankButton(); });</sc ript>'); } |
Посмотрим, что получилось.
![image2.png](https://site-cloud-files.bitrix.info/main/1b5/1b504c3a4e650de202aeeb710e8c4e23/image2.png)
Теперь реализуем обработчик клика по этому пункту. Выведем всплывающее окно с полем ввода текста.
CustomStuff.showThankPopup = function() { if (CustomStuff.thankPopup) { CustomStuff.thankPopup.close(); CustomStuff.thankPopup.destroy(); } CustomStuff.thankPopup = new BX.PopupWindow('thank', null, { width : 600, height : 400, closeByEsc : true, closeIcon : true, overlay : { opacity : 50, backgroundColor : '#000' }, titleBar : BX.message('CUSTOM_STUFF_POPUP_TITLE'), content : BX.create('div', { html : '<p>' + BX.message('CUSTOM_STUFF_POPUP_DESCR') + '</p><textarea></textarea>' }), buttons : [ new BX.PopupWindowButton({ text : BX.message('CUSTOM_STUFF_POPUP_BUTTON'), className : 'popup-window-button-accept', events : { click : CustomStuff.helloWorld } }) ] }); CustomStuff.thankPopup.show(); }; |
Про параметры всплывающего окна можно узнать из записи вебинара.
Посмотрим, что получилось.
![image19.png](https://site-cloud-files.bitrix.info/main/ce8/ce848f37609b7ec37ae6b482113472d1/image19.png)
На событие клика по кнопке «Отправить начальнику» запускается функция helloWorld. При выполнении реальной задачи, вы, например, могли бы сделать AJAX-запрос, который выполняет настоящую отправку сообщения.
Пример 2. Создание документа на диске из шаблона
[
При нажатии на кнопку «Создать документ» в диске появляется окно, в котором можно выбрать, создать документ на компьютере или в облачном сервисе. Добавим в это окно выбор шаблона документа.
![image10.png](https://site-cloud-files.bitrix.info/resize_cache/7027893/04dbd6b4c314fe48f655aad84339ad15/main/e2f/e2f5ebdf1bbcd24654070670542e7b0f/image10.png)
В данном случае мы не можем добавить нужную верстку сразу после загрузки страницы, т. к. всплывающее окно появляется в DOM-дереве в момент его открытия. В этом случае мы можем проверить, существуют ли какие-нибудь события, связанные с открытием этого окна.
Выполните в консоли браузера следующий код.
// Лог событий. var originalBxOnCustomEvent = BX.onCustomEvent; BX.onCustomEv ent = function(eventObject, eventName, eventParams, secureParams) { var logData = { eventObject : eventObject, eventName : eventName, eventParams : eventParams, eventParamsClassNames : [], secureParams : secureParams }; for ( var i in eventParams) { var param = eventParams; if (param !== null && typeof param == 'object' && param.constructor) { logData['eventParamsClassNames'].push(param.constructor.name); } else { logData['eventParamsClassNames'].push(null); } } console.log(logData); originalBxOnCustomEvent.apply(null, [ eventObject, eventName, eventParams, secureParams ]); }; |
![image7.png](https://site-cloud-files.bitrix.info/resize_cache/7027895/04dbd6b4c314fe48f655aad84339ad15/main/8df/8df50645d2a36913603e9a55637a7927/image7.png)
Значит, существуют события, связанные со всплывающим окном, одним из которых мы воспользуемся.
На самом деле фрагмент DOM-дерева для всплывающего окна будет готов уже к моменту onPopupWindowIsInitialized. В целях демонстрации мы подпишемся на событие onAfterPopupShow, чтобы добавить верстку для выбора шаблона.
Создадим функцию в custom_stuff.js, которая регистрирует обработчик события onAfterPopupShow с помощью функции BX.addCustomEvent.
CustomStuff.enableDiskTemplates = function() { BX.addCustomEvent('onAfterPopupShow', CustomStuff.addTemplatesToPopup); }; |
Теперь реализуем обработчик события — функцию CustomStuff.addTemplatesToPopup.
/** * @param {BX.PopupWindow} * popup */ CustomStuff.addTemplatesToPopup = function(popup) { if (popup.uniquePopupId != 'bx-disk-create-file') { return; } if (popup.templatesAdded) { return; } BX.ajax.get('/local/tools/custom_stuff/disk_templates.php', { sessid : BX.bitrix_sessid() }, function(response) { var diskContentDiv = BX.findChildByClassName(popup.contentContainer, 'bx-disk-popup-content', true); BX.adjust(diskContentDiv, { style : { 'padding-bottom' : 0, 'padding-top' : 0 } }); var templatesDiv = BX.create('div', { html : response }); popup.contentContainer.appendChild(templatesDiv); }); popup.templatesAdded = true; }; |
Обратите внимание, что обработчик события onAfterPopupShow будет вызываться при каждом открытии любого всплывающего окна. Поэтому в самом начале мы проверяем, обрабатываем ли мы открытие окна создания документа или нет. Узнать ID всплывающего окна можно из лога событий.
![image4.png](https://site-cloud-files.bitrix.info/main/372/372b5ea6054cbd8260a08b1e094b044c/image4.png)
На этот раз HTML-код выбора шаблона довольно объемный. Получим его с сервера с помощью AJAX. Мы используем функцию BX.ajax.get, чтобы выполнить запрос.
Важно! Не забывайте передавать параметр sessid и проверять его в обработчике запроса. Это необходимо для предотвращения некоторых атак.
Полученный код добавим во всплывающее окно. Ссылка на узел DOM-дерева хранится в поле contentContainer объекта всплывающего окна.
В целях демонстрации обработчик AJAX-запроса просто отдает верстку для шаблонов. При решении реальной задачи будет уместно вызвать компонент, генерирующий эту верстку.
<?php define('PUBLIC_AJAX_MODE', true); define('STOP_STATISTICS', true); define('NO_AGENT_CHECK', true); require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php'); check_bitrix_sessid() || die; $APPLICATION->ShowAjaxHead(); ?> <div class="disk-templates-title"> Или создайте из шаблона: </div> <div class="disk-templates"> <a class="template" oncl ick="BX.CustomStuff.helloWorld();"> <span class="bx-file-icon-container-small bx-disk-file-icon icon-doc"></span> Шаблон документа 1 </a> <!-- И так далее. --> </div> <? require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php'); ?> |
Осталось сделать запускающий скрипт. Поскольку код на клиенте проверяет, открывается ли окно создания документа или другое. Значит мы можем подписываться на событие открытия окна на любой странице. Поэтому просто будем выполнять следующий код на каждом хите.
$asset->addString( '<sc ript>BX.ready(function () { BX.CustomStuff.enableDiskTemplates(); });</sc ript>' ); |
Теперь при открытии окна создания документа выводится список шаблонов.
![image18.png](https://site-cloud-files.bitrix.info/main/a98/a98faa284fb86b5c39a1ba7173806f59/image18.png)
Пример 3. Дополнительные поля в карточках ранней версии канбана
[
Не так давно данный пример стал устаревшим, т. к. в канбане появились события. Тем не менее, описываемый подход полезно рассмотреть, на случай, если вы попадете в ситуацию, когда части страницы строятся динамически, а событий, за которые можно «зацепиться» нет. Например, при доработке свежей функциональности.
Весь интерфейс канбана строится на стороне клиента с помощью JS. В этом легко убедиться, отключив выполнение JS в браузере. Перед нами стоит задача добавления поля в карточку канбана.
Прежде чем переходить к «грубым» хакам, рассмотрим пример того, как можно дождаться появления на странице карточек, а затем выполнить свой код.
Реализуем функцию, которая выполняется через равные промежутки времени и проверяет наличие нужного нам элемента в DOM-дереве.
CustomStuff.extendKanbanOnFirstLoad = function() { if (CustomStuff.kanbanTimer > 0) { return; } CustomStuff.kanbanTimer = setInterval(function() { var mainKanbanDiv = BX.findChildByClassName(document, 'main-kanban', true); if (mainKanbanDiv) { clearInterval(CustomStuff.kanbanTimer); CustomStuff.addHelloWorldToKanban(); } }, 100); }; |
Таким образом, когда сетка канбана появится на странице, будет выполнена функция CustomStuff.addHelloWorldToKanban.
Существует и другой метод ожидания элемента DOM-дерева: с помощью MutationObserver.
Теперь реализуем функцию, которая добавит «Hello World!» в каждую карточку.
CustomStuff.addHelloWorldToKanban = function() { var kanbanCards = BX.findChildrenByClassName(document, 'main-kanban-item', true); for ( var cardIndex in kanbanCards) { var date = BX.findChildByClassName(kanbanCards[cardIndex], 'crm-kanban-item-date', true); var helloWorld = BX.create('span', { text : 'Hello World!' }); BX.insertAfter(helloWorld, date); } }; |
Функцию CustomStuff.extendKanbanOnFirstLoad будем вызывать, когда открывается любая страница раздела канбана лидов.
if (CSite::InDir('/crm/lead/kanban/')) { $asset->addString( '<sc ript>BX.ready(function () { BX.CustomStuff.extendKanbanOnFirstLoad();'.'});</sc ript> '); } |
Посмотрим, что получилось.
![image6.png](https://site-cloud-files.bitrix.info/main/4e4/4e42beb31a91ec686f2baefc057e4135/image6.png)
Также нужно учитывать, что канбан имеет свойство обновлять свое состояние при изменениях на сервере. Например, при добавлении нового лида, его карточка появится без перезагрузки страницы. Чтобы дополнительные поля появились и в ней, момент ее добавления также придется обработать тем или иным образом.
Пример 4. Подтверждение на получение публичной ссылки в диске
[
Существуют задачи, которые предыдущим способом делать либо дорого, либо вообще не возможно.
JS — особенный язык
![С улыбкой :-)](/upload/main/smiles/3/bx_smile_smile.png)
Например, в контекстном меню объекта диска есть пункт «Получить публичную ссылку». Никаких событий при этом не происходит. Рассмотрим обработчик события клика по этому пункту меню.
BX.Disk['FolderListClass_p7T8Vm'].openExternalLinkDetailSettingsWithEditing(46); |
Видим, что у некоторого объекта вызывается метод openExternalLinkDetailSettingsWithEditing, который создает публичную ссылку и показывает ее во всплывающем окне. Мы хотим запросить подтверждение на получение ссылки перед тем, как она будет показана. Для этого нам не надо знать реализацию метода получения ссылки. Мы заменим оригинальный метод на свой, который будет сначала запрашивать подтверждение, а потом просто вызывать оригинал.
Прежде всего нам нужно узнать, к какому классу относится метод и какая у него сигнатура.
Название класса поможет узнать следующий код, который нужно выполнить в консоли.
BX.Disk['FolderListClass_p7T8Vm'].constructor.name // Вернет "FolderListClass" |
Далее можно сделать поиск по коду и узнать, что полное имя класса (с пространством имен) будет BX.Disk.FolderListClass. Ну или интуицию включить
![С улыбкой :-)](/upload/main/smiles/3/bx_smile_smile.png)
Далее необходимо найти сигнатуру метода, который мы хотим заменить, чтобы узнать, какие аргументы он принимает. Для этого нужно открыть исходный код этого класса.
FolderListClass.prototype.openExternalLinkDetailSettingsWithEditing = function (objectId) { |
Теперь мы можем просто присвоить BX.Disk.FolderListClass.prototype.openExternalLinkDetailSettingsWithEditing новую версию метода. Напишем для этого функцию в custom_stuff.js.
CustomStuff.overrideDiskShare = function() { if (!BX.Disk || !BX.Disk.FolderListClass) { return; } var originalMethod = BX.Disk.FolderListClass.prototype.openExternalLinkDetailSettingsWithEditing; BX.Disk.FolderListClass.prototype.openExternalLinkDetailSettingsWithEditing = function( objectId) { var self = this; CustomStuff.showShareConfirmationDialog(function() { originalMethod.apply(self, [ objectId ]) }); }; }; |
Осталось реализовать функцию, которая выводит запрос на подтверждение действия.
CustomStuff.showShareConfirmationDialog = function(onAgree) { var confirmationPopup = new BX.PopupWindow('shareConfirmation', null, { width : 400, height : 100, closeByEsc : true, closeIcon : true, overlay : { opacity : 50, backgroundColor : '#000' }, titleBar : BX.message('CUSTOM_STUFF_SHARE_CONFIRMATION_TITLE'), content : BX.create('div', { html : BX.message('CUSTOM_STUFF_SHARE_CONFIRMATION') }), buttons : [ new BX.PopupWindowButton({ text : BX.message('CUSTOM_STUFF_POPUP_BUTTON_YES'), className : 'popup-window-button-accept', events : { click : function() { confirmationPopup.close(); confirmationPopup.destroy(); onAgree(); } } }), new BX.PopupWindowButton({ text : BX.message('CUSTOM_STUFF_POPUP_BUTTON_NO'), events : { click : function() { confirmationPopup.close(); confirmationPopup.destroy(); } } }) ] }); confirmationPopup.show(); }; |
CustomStuff.overrideDiskShare сама проверяет существование класса на клиенте, поэтому вызовем ее на каждой странице.
$asset->addString( '<sc ript>BX.ready(function () { BX.CustomStuff.overrideDiskShare(); });</sc ript>' ); |
Посмотрим, что получилось.
![image1.png](https://site-cloud-files.bitrix.info/main/ab0/ab04cbdf27819332f14d9cea662994c1/image1.png)
Теперь, если нажать на этот пункт меню, появится всплывающее окно для подтверждения. Публичная ссылка появится только если нажать «Да».
Бонус: работа с боковой панелью-слайдером
[
В последнее время все чаще используется панель-слайдер в интерфейсе КП. Например, при создании задачи, открытии карточки CRM и т. п.
Дело в том, что клик по кнопке, которая открывает слайдер, обрабатывается не на этапе всплытия, а на этапе перехвата, при чем на элементе html. Это значит, что если понадобится выполнить какой-то код перед открытием слайдера, обработчик клика уже не подойдет.
В данном примере мы сделаем окно с подтверждением при создании новой группы соц. сети.
Открытие страницы в слайдере сопровождается событием, на которое мы подпишем свой обработчик и воспользуемся функцией отмены открытия страницы.
CustomStuff.enableCreateGroupConfirmation = function() { BX.addCustomEvent('SidePanel.Slider:onOpen', CustomStuff.showCreateGroupConfirmation); }; CustomStuff.showCreateGroupConfirmation = function(event) { var slider = event.getSlider(); if (!slider.getUrl().endsWith('/groups/create/')) { return; } if (CustomStuff.isConfirmingGroupCreate) { CustomStuff.isConfirmingGroupCreate = false; } else { event.denyAction(); CustomStuff.showGroupCreateConfirmationDialog(function() { BX.SidePanel.Instance.open(slider.getUrl(), slider.options); }); CustomStuff.isConfirmingGroupCreate = true; } }; |
Код всплывающего окна почти не отличается от предыдущего примера.
CustomStuff.showGroupCreateConfirmationDialog = function(onAgree) { var confirmationPopup = new BX.PopupWindow('groupCreateConfirmation', null, { width : 400, height : 100, closeByEsc : true, closeIcon : true, overlay : { opacity : 50, backgroundColor : '#000' }, titleBar : BX.message('CUSTOM_STUFF_GROUP_CREATE_CONFIRMATION_TITLE'), content : BX.create('div', { html : BX.message('CUSTOM_STUFF_GROUP_CREATE_CONFIRMATION') }), buttons : [ new BX.PopupWindowButton({ text : BX.message('CUSTOM_STUFF_POPUP_BUTTON_YES'), className : 'popup-window-button-accept', events : { click : function() { onAgree(); confirmationPopup.close(); confirmationPopup.destroy(); } } }), new BX.PopupWindowButton({ text : BX.message('CUSTOM_STUFF_POPUP_BUTTON_NO'), events : { click : function() { confirmationPopup.close(); confirmationPopup.destroy(); } } }) ], events : { onPopupClose : function() { CustomStuff.isConfirmingGroupCreate = false; } } }); confirmationPopup.show(); }; |
Теперь при нажатии кнопки «Создать» на странице групп, сначала появляется такое окно.
![image16.png](https://site-cloud-files.bitrix.info/main/ddc/ddcdd4fc1cbac56c3ed43dc540f9fd91/image16.png)
Также, иногда бывает полезно узнать, открывается ли текущая страница в слайдере или нет. В этом случае достаточно посмотреть на параметр запроса IFRAME. Он будет равен Y, если страница открывается в слайдере.
$context = Context::getCurrent(); $request = $context->getRequest(); $isInSlider = $request->get('IFRAME') == 'Y'; |
-----
В комментариях пишите ваши подходы в кастомизации коробки, решения. А так же какие еще темы вы хотели бы видеть на наших вебинарах.
Материалы:
Автор статьи:
Фото:
Типичный пример:
1. Создание в /local/templates/.default/... "шаблона компонента", который выглядит почти как обычный кастомизированный шаблон компонента (
2. Создаете result_modifier.php
3. Манипулируете $this->__folder и $this->__file
4. После чего пишите свой код result_modifier.php и затем при необходимости вручную подключаете оригинальный result_modfifier.php
5. Profit
Результат вы видели на скриншоте выше.
PS. хочется верить, что немногие знали о такой фиче =)
Ничего радикально нового явно не будет логику компонентов принципиально вендор не переделает. Хотя кастомный result_modifier, который никому не мешает - уже хорошо
и из за этого вылазят ошибки js
копировать их конечно не хочется - иначе теряется смысл данного способа
я пока добавляю так в result_modifier.php
// подключить скрипты и стили стандартного шаблона
$this->__component->getTemplate()->addExternalJS($this->__folder . '/script.js');
$this->__component->getTemplate()->addExternalCss($this->__folder . '/style.css');
вроде все ок - но может есть способ лучше?
а как вы решаете данную проблему?
$this->__hasCSS = true;
$this->__hasJS = true;
Необходимо ограничение количества цифр и разделение слэшом.
сейчас в боковом слайдере нельзя открыть стороннюю страницу (c другого хостинга) - блокируется содержимое
делаю так - BX.SidePanel.Instance.open("http://my-page.com/");
или нужно https?
Не выводится кнопка при просмотре страницы сущности, в моем случае компания.
Почему бы не использовать MutationObserver?
При этом при клике на редактирование кнопка появляется.
3. Типы пользовательских полей
не работает, пишет:
Class 'TypeBase' not found (0)
надо сверху
Но кнопки красная и зелёная не выводятся в карточке
Всё работает.
Из того что отсутствует в видео:
Сначала поле нужно выбрать - через кнопочку "Выбрать поле" и только потом оно будет отображаться в карточке.
Вот что у меня в итоге получилось:
Инструкция больше не актуальна?
В документации есть такой пассаж:
Все uf-классы должны являться наследниками класса BaseType и располагаться по пути <modulename>/lib/usefield/types/<classname>/ соответствующего модуля, например, /main/lib/userfield/types/stringtype.
Подскажи пожалуйста, где я тут допускаю ошибку?
Хочу создать костомное тип поля, но при создании в админке тип поля не отображается, при этом ошибка тоже нету
<?php
use Bitrix\Main\EventManager;
use Bitrix\Main\UI\Extension;
use Bitrix\Main\UserField\TypeBase;
Extension::load("ui.buttons";);
class MyUserType extends TypeBase
{
const USER_TYPE_ID = 'myusertype';
public static function GetUserTypeDescription()
{
return [
'USER_TYPE_ID' => static::USER_TYPE_ID,
'CLASS_NAME' => __CLASS__,
'DESCRIPTION' => 'Кастомное поле',
'BASE_TYPE' => \CUserTypeManager::BASE_TYPE_STRING,
'EDIT_CALLBACK' => [__CLASS__, 'GetPublicEdit'],
'VIEW_CALLBACK' => [__CLASS__, 'GetPublicView'],
];
}
public static function GetDBColumnType($arUserField)
{
global $DB;
switch(strtolower($DB->type))
{
case "mysql":
return "text";
case "oracle":
return "varchar2(2000 char)";
case "mssql":
return "varchar(2000)";
}
}
public static function GetPublicView($arUserField, $arAdditionalParameters = [])
{
Extension::load('ui.buttons';);
return '
}
public static function GetPublicEdit($arUserField, $arAdditionalParameters = [])
{
Extension::load('ui.buttons';);
$name = static::getFieldName($arUserField, $arAdditionalParameters);
return '<input type="hidden" name="' . $name . '" value="1"/>
}
}
$eventManager = EventManager::getInstance();
$eventManager->addEventHandlerCompatible('main', 'OnUserTypeBuildList', ['MyUserType', 'GetUserTypeDescription']);