Контекстное меню

В CS-Cart есть контекстное меню — механизм для массовых действий над сущностями. Он позволяет выделять элементы из списка и массово работать с ними. Чтобы посмотреть, как выглядит контекстное меню, достаточно зайти на список товаров и выделить один из них. Все действия над объектами в списке должны быть реализованы через контекстное меню.

Для реализации контекстного меню необходимо:

  1. Создать схему конкретной сущности;
  2. Адаптировать таблицу, в которой выводится список объектов этой сущности;
  3. Реализовать бекенд-обработчики для массовых действий (массовое удаление, изменение статуса и т.п.).

Примеры контекстных меню находятся в директории app/schemas/context_menu/.

Схема контекстного меню

Схема контекстного меню имеет следующую структуру:

[
  // Список статусов для выделения элементов.
  'selectable_statuses' => array<string, string>
  // Список элементов контекстного меню.
  'items'               => [
      // Идентификатор элемента.
      'status'  => [
          // Название элемента.
          'name'              => ['template' => 'status'],
          // Тип элемента (ActionItem/GroupItem/ComponentItem).
          'type'              => GroupItem::class,
          // Данные для передачи в шаблон. В шаблоне обращаться через $data.
          'data'              => [
              // menu_item_attributes, menu_item_class, action_attributes, action_class - используется в общих шаблонах.
   // menu_item_attributes, menu_item_class - добавляют атрибуты или классы на элементы меню. Например cm-no-hide-input или mobile-hide (скрывает элемент на мобильных устройствах)
   // action_attributes, action_class - добавляют атрибуты или классы на элементы-действия. Например cm-ajax, cm-post, cm-confirm или заменить ссылку действия, указав в массиве action_attributes, элемент с ключом href и ссылкой
             // cm-no-hide-input - оставляет input-теги активными если на форме есть класс cm-hide-input
              'menu_item_class' => 'cm-no-hide-input',
              ...
          ],
          // Список вложенных элементов в GroupItem.
          'items'             => [
              // Элемент-ссылка
              'm_activate' => [
                  'name'          => [
                      'template' => 'change_to_status',
                      'params'   => [
                          '[status]' => __('active'),
                          ...
                      ],
                  ],
                   // Диспатч, на который будут переданы идентификаторы выделенных элементов; диспатч также используется при проверке доступности элемента текущему пользователю.
                  'dispatch' => 'profiles.m_activate',
                  // Позиция элемента в списке.
                  'position' => 10,
              ],
              // Элемент-разделитель.
              'actions_divider'   => [
                   // Тип вложенного в GroupItem элемента (GroupActionItem/ComponentItem/DividerItem). По умолчанию тип элемента GroupActionItem.
                  'type'     => DividerItem::class,
                  'position' => 20,
              ],
              // Элемент с собственным шаблоном.
              'notify_checkboxes' => [
                  'type'          => ComponentItem::class,
                  // Собственный шаблон элемента ComponentItem.
                  'template'      => 'views/profiles/components/context_menu/notify_checkboxes.tpl',
                  // Callback-функция для формирования данных для передачи в шаблон. В шаблоне обращаться через $data.
                  'data_provider' => static function () {
                      return [
                          'param1' => 'value2',
                          ...
                      ];
                  },
                  'position'      => 30,
              ],
              ...
          ],
          // Callback-функция для проверки доступности элемента.
          'permission_callback' => static function ($request, $auth, $runtime) {
              return !(
                  UserTypes::isVendor($auth['user_type'])
                  && UserTypes::isCustomer($request['user_type'])
                  && fn_check_permissions('profiles', 'm_activate', 'admin', 'POST', ['user_type' => $request['user_type']])
                  && fn_check_permissions('profiles', 'm_disable', 'admin', 'POST', ['user_type' => $request['user_type']])
              );
          },
          'position'            => 20,
      ],
      ...
  ],
]
Параметр Обязательность Описание
items обязательный Массив элементов контекстного меню
selectable_statuses опциональный Массив статусов, которые можно использовать для выбора элементов. Ключи массива — коды статусов, значения — описание статуса уже на нужном языке, полученное через функцию __().

Элементы контекстного меню

Контекстное меню может содержать элементы типа ActionItem, GroupItem и ComponentItem.

ActionItem

Кнопка в контекстном меню — действие над элементами без раскрывающегося списка или всплывающего окна рядом с меню:

Параметр Обязательность Описание
type обязательный ActionItem::class
dispatch обязательный Диспатч (например, protucts.update); по диспатчу проверяется доступность текущему пользователю.
name опциональный Массив из двух элементов: template и params. template — название языковой переменной; params — массив плейсхолдеров (опционально).
data опциональный Массив с данными.
position опциональный Позиция элемента в меню.
permission_callback опциональный Функция для пользовательской проверки доступности элемента, должна вернуть булевое значение. Работает совместно с dispatch.

GroupItem

Выпадающий список, содержащий действия:

Параметр Обязательность Описание
type обязательный ActionItem::class
items обязательный Массив из других элементов контекстного меню.
name опциональный Массив из двух элементов: template и params. template — название языковой переменной; params — массив плейсхолдеров (опционально).
data опциональный Массив с данными; данные доступны в шаблонах через $data.
position опциональный Позиция элемента в меню.
permission_callback опциональный Функция для пользовательской проверки доступности элемента, должна вернуть булевое значение.

GroupActionItem

Действие, находящееся в GroupItem:

Параметр Обязательность Описание
dispatch обязательный Диспатч (например, protucts.update); по диспатчу проверяется доступность текущему пользователю.
name опциональный Массив из двух элементов: template и params. template — название языковой переменной; params — массив плейсхолдеров (опционально).
type опциональный GroupActionItem::class
data опциональный Массив с данными.
position опциональный Позиция элемента в меню.
permission_callback опциональный Функция для пользовательской проверки доступности элемента, должна вернуть булевое значение. Работает совместно с dispatch.

DividerItem

Разделитель, находящийся в GroupItem:

Параметр Обязательность Описание
type обязательный DividerItem::class
position опциональный Позиция элемента в меню

ComponentItem

Сложный пользовательский компонент с произвольным наполнением и поведением. Например, смена цены и количества на списке товаров:

Параметр Обязательность Описание
type обязательный ComponentItem::class
template обязательный Путь до подключаемого шаблона.
name опциональный Массив из двух элементов: template и params. template — название языковой переменной; params — массив плейсхолдеров (опционально).
data опциональный Массив с данными.
data_provider опциональный Функция, возвращающая данные для использования в подключаемом шаблоне.
position опциональный Позиция элемента в меню.
permission_callback опциональный Функция для пользовательской проверки доступности элемента, должна вернуть булевое значение.

Компоненты отображения контекстного меню

Для вывода таблицы с контекстным меню используется компонент common/context_menu_wrapper.tpl. Параметры при подключении в шаблоне:

Параметр Обязательность Описание
object обязательный Имя объекта для которого нужно сформировать контекстное меню (название схемы).
form обязательный Идентификатор формы которая будет отправляться при каком-либо действии.
id опциональный Идентификатор контекстного меню, для размещения нескольких контекстных меню на одной странице.
class опциональный Класс для блока обертки контекстного меню.
attributes опциональный Атрибут/атрибуты для блока обертки контекстного меню.
hook опциональный Название хука в котором будет находиться контекстное меню; если не указано, название хука формируется из “название_объекта:context_menu”.
has_permission опциональный Булевое значения: “true” для отображения контекстного меню; при “false” контекстное меню не будет отрисовываться.
context_menu_class опциональный Класс для блока контекстного меню.
is_check_all_shown опциональный Булевое значение. Прокидывается в шаблон common/check-items.tpl.

ComponentItem позволяет подключать собственный шаблон. В этот шаблон передаются и могут быть использованы следующие параметры:

Параметр Описание
item_id Идентификатор элемента (ключ элемента из схемы).
item Объект класса ComponentItem.
data Данные из схемы (data, data_provider).
params Параметры обертки (form, object, items …)

Адаптация списка сущностей для работы с контекстным меню

  1. Блок, которой выводит список элементов сущности, нужно обернуть в capture:

    {capture name=”entity_table”}
        %блок с таблицей со списком элементов%
    {/capture}
    
  2. Содержимого этого capture затем нужно вывести в с помощью компонента common/context_menu_wrapper.tpl:

    {include file="common/context_menu_wrapper.tpl"
       form="entity_form"
       object="entity"
       items=$smarty.capture.entity_table
    }
    
  3. Блоку, в котором находится таблица со списком сущностей, необходимо добавить класс longtap-selection.

  4. В тег thead таблицы с сущностями нужно добавить атрибуты:

    • data-ca-bulkedit-default-object — чтобы скрывать заголовок таблицы при отображении панели контекстного меню;
    • data-ca-bulkedit-component — для включения возможности массового выделения.
  5. В первую ячейку заголовка таблицы (первый th) добавить служебный скрытый input, который будет включать или отключать режим массового выделения для таблицы:

    <input type="checkbox"
          class="bulkedit-toggler hide"
          data-ca-bulkedit-disable="[data-ca-bulkedit-default-object]" // в атрибуте указывается селектор элементов, которые нужно скрыть при отображении контекстного меню — например, заголовок таблицы
          data-ca-bulkedit-enable="[data-ca-bulkedit-expanded-object]" // в атрибуте указывается селектор элементов, которые нужно отобразить при активации контекстного меню — панель управления элементами
    />
    
  6. В тег tbody нужно добавить:

    • Класс cm-longtap-target.
    • Атрибуты:
      • data-ca-longtap-action="setCheckBox" — задает тип действия при нажатии на элемент, добавляется на элемент с тегом <tr>(обычно) в котором находится чекбокс с идентификатором элемента.
      • data-ca-longtap-target="input.cm-item" — указывает селектор, по которому у найденных элементов внутри data-ca-longtap-action переключится атрибут selected.
      • data-ca-id="{$product.product_id}" — содержит идентификатор элемента, например, ID товара.
  7. Каждому элементу списка в таблице нужно добавить чекбокс, отвечающий за его выделение и хранящий ID сущности:

    <input type="checkbox"
          name="product_ids[]"//имя для примера
          value="{$product.product_id}"
          class="cm-item cm-item-status-{$product.status|lower} hide"
    />
    
  8. Класс cm-status-* используется для выделения элементов с конкретным статусом через выпадающий список в заголовке таблицы (All, None, Active, Disabled, Hidden …).

    Класс hide используется для скрытия чекбокса.

Пример адаптации

Пример адаптации на GitHub

Было

{capture name="mainbox"}

   <form action="{""|fn_url}" method="post" id="states_form" name="states_form">
       {include file="common/pagination.tpl" save_current_page=true save_current_url=true}

       {if $states}
           <div class="table-responsive-wrapper">
               <table width="100%" class="table table-middle table--relative table-responsive state-table">
               <thead>
                   <tr>
                       <th width="6%" class="mobile-hide">
                           {include file="common/check_items.tpl" check_statuses=$state_statuses}
                       </th>
                       <th width="10%">{__("code")}</th>
                       <th>{__("state")}</th>
                   </tr>
               </thead>
               {foreach $states as $state}
                   <tr class="cm-row-status-{$state.status|lower} cm-longtap-target">
                       <td width="6%" class="mobile-hide">
                           <input type="checkbox" name="state_ids[]" value="{$state.state_id}" class="cm-item cm-item-status-{$state.status|lower}" />
                       </td>
                       <td width="10%" class="left nowrap row-status" data-th="{__("code")}">
                           <span>{$state.code}</span>
                       </td>
                       <td data-th="{__("state")}">
                           <input type="text" name="states[{$state.state_id}][state]" size="55" value="{$state.state}" class="input-hidden"/>
                       </td>
                   </tr>
               {/foreach}
               </table>
          </div>
       {else}
           <p class="no-items">{__("no_data")}</p>
       {/if}

       {include file="common/pagination.tpl"}
   </form>

   {capture name="buttons"}
       {capture name="tools_list"}
           {hook name="states:manage_tools_list"}
               <li>{btn type="delete_selected" dispatch="dispatch[states.m_delete]" form="states_form"}</li>
           {/hook}
       {/capture}
       {dropdown content=$smarty.capture.tools_list}

       {if $states}
           {include file="buttons/save.tpl" but_name="dispatch[states.m_update]" but_role="action" but_target_form="states_form" but_meta="cm-submit"}
       {/if}
   {/capture}

   {capture name="adv_buttons"}
       {include file="common/popupbox.tpl" id="new_state" action="states.add" text=$title content=$smarty.capture.add_new_picker title=__("add_state") act="general" icon="icon-plus"}
   {/capture}

{/capture}

{include file="common/mainbox.tpl"
   title=__("states")
   content=$smarty.capture.mainbox
   adv_buttons=$smarty.capture.adv_buttons
   buttons=$smarty.capture.buttons
   select_languages=true
}

Стало

{capture name="mainbox"}

   <form action="{""|fn_url}" method="post" id="states_form" name="states_form">
       {include file="common/pagination.tpl" save_current_page=true save_current_url=true}

       {if $states}
           {capture name="states_table"}
               <div class="table-responsive-wrapper longtap-selection">
                   <table width="100%" class="table table-middle table--relative table-responsive state-table">
                   <thead
                       data-ca-bulkedit-default-object
                       data-ca-bulkedit-component
                   >
                       <tr>
                           <th width="6%" class="mobile-hide">
                               {include file="common/check_items.tpl" check_statuses=$state_statuses}

                               <input type="checkbox"
                                   class="bulkedit-toggler hide"
                                   data-ca-bulkedit-disable="[data-ca-bulkedit-default-object]"
                                   data-ca-bulkedit-enable="[data-ca-bulkedit-expanded-object]"
                               />
                           </th>
                           <th width="10%">{__("code")}</th>
                           <th>{__("state")}</th>
                       </tr>
                   </thead>
                   {foreach $states as $state}
                       <tr class="cm-row-status-{$state.status|lower} cm-longtap-target"
                           data-ca-longtap-action="setCheckBox"
                           data-ca-longtap-target="input.cm-item"
                           data-ca-id="{$state.state_id}"
                       >
                           <td width="6%" class="mobile-hide">
                               <input type="checkbox" name="state_ids[]" value="{$state.state_id}" class="cm-item cm-item-status-{$state.status|lower} hide" />
                           </td>
                           <td width="10%" class="left nowrap row-status" data-th="{__("code")}">
                               <span>{$state.code}</span>
                           </td>
                           <td data-th="{__("state")}">
                               <input type="text" name="states[{$state.state_id}][state]" size="55" value="{$state.state}" class="input-hidden"/>
                           </td>
                       </tr>
                   {/foreach}
                   </table>
               </div>
           {/capture}

           {include file="common/context_menu_wrapper.tpl"
               form="states_form"
               object="states"
               items=$smarty.capture.states_table
           }
       {else}
           <p class="no-items">{__("no_data")}</p>
       {/if}

       {include file="common/pagination.tpl"}
   </form>

   {capture name="buttons"}
       {capture name="tools_list"}
           {hook name="states:manage_tools_list"}
           {/hook}
       {/capture}
       {dropdown content=$smarty.capture.tools_list}

       {if $states}
           {include file="buttons/save.tpl" but_name="dispatch[states.m_update]" but_role="action" but_target_form="states_form" but_meta="cm-submit"}
       {/if}
   {/capture}

   {capture name="adv_buttons"}
       {include file="common/popupbox.tpl" id="new_state" action="states.add" text=$title content=$smarty.capture.add_new_picker title=__("add_state") act="general" icon="icon-plus"}
   {/capture}

{/capture}

{include file="common/mainbox.tpl"
   title=__("states")
   content=$smarty.capture.mainbox
   adv_buttons=$smarty.capture.adv_buttons
   buttons=$smarty.capture.buttons
   select_languages=true
}

Примеры схем

app/schemas/context_menu/products.php — категории, цены и массовое изменение

app/schemas/context_menu/payouts.php — notify_checkboxes

app/schemas/context_menu/product_features.php — group, category