'20', 40 => '40', 100 => '100']; /** @var string ключ кол-ва записей на странице в GET/POST запросе */ public $perPageVar = 'perpage'; /** @var string Макрос ссылки на страницу */ protected $linkHref = ''; /** @var string Событие нажатия на ссылку перехода на страницу */ protected $linkOnClick = ''; /** @var string HTML код формы */ public $formHTML = ''; /** @var BlockFormsTabs */ protected $formsTabs = null; /** @var string */ public $titleAddLink = ''; /** @var array Данные для вывода списка как nestedSetsTree */ public $nestedSetsTree = []; /** @var array Для генерации экшенов событий onFordev */ protected $onFordevCounter = 0; /** @var int время автоматического обновления списка в секундах 0 - отключено */ protected $refreshTimeout = 0; /** @var Model Объект модели для работы с записью */ protected $model = null; /** @var string доп. параметры в ajax запросах */ public $_ajaxParams = ''; /** @var array обработчики javascript событий */ public $_jsParams = []; /** @var bool показывать заголовок столбцов */ public $showHeader = true; /** @var array атрибуты столбца с кнопками */ public $rowActionsAttr = []; /** @var string префикс для tableDnD */ public $dndID = 'dnd-'; /** @var array список сообщений */ protected $alerts = []; /** @var bool дублировать название столбцов для мобильной версии */ public $mobileRowsTitle = true; /** @var BlockListMass массовая обработки строк */ protected $mass = null; /** @var bool $paginationMore использовать пагинацию типа more */ protected $paginationMore = false; /** @var bool $paginationMoreAllowed отображать кнопку more (еще не достигнуто окончание выборки) */ protected $paginationMoreAllowed = false; /** @var string переменная фильтра содержащая смещение для more пагинации */ public $paginationMoreOffsetVar = 'offset'; /** * Конструктор * @param Module $controller объект контроллера * @param string $action названием метода контроллера работающего со списком * @param string $id id списка */ public function __construct(Module $controller, $action, $id = '') { $this->setController($controller, $action); $this->id = $id; } public function init() { if ( ! parent::init()) { return false; } $this->setTemplateName('block.list'); $this->wrapper()->title(_t('@', 'List')); $this->formsHeader([ 'title.add' => _t('@', 'Add'), 'title.edit' => _t('@', 'Edit'), 'title.add.link' => _t('@', 'Add'), ]); $this->detectAjaxInit(); $this->trigger('init'); return true; } /** * Fire list event * @param string $event * @param array $data * @return static */ public function trigger($event, array $data = []) { $this->app->hook($this->id . '.admin.list.' . $event, $this, $data); return $this; } /** * Добавление параметров к ajax запросам * @param string|array $data * @return static */ public function ajaxParams($data) { if (is_array($data)) { $data = Url::query($data, [], '&'); } if ( ! empty($data) && mb_strpos($data, '&') === false) { $data = '&'.$data; } $this->_ajaxParams = $data; return $this; } /** * Формирование Url * @param string $action * @param array $opts параметры * @return string */ public function urlAjax($action, array $opts = []) { return tpl::adminLink($this->getControllerAction().$this->_ajaxParams.'&'.$this->subActionKey.'='.$action.Url::query($opts, [], '&')); } /** * Url для добавления строки * @param array $opts параметры * @return string */ public function urlAdd(array $opts = []) { return $this->urlAjax(static::ACTION_ADD, $opts); } /** * Url для редактирования строки * @param int $id ID строки * @param array $opts параметры * @return string */ public function urlEdit($id, array $opts = []) { $opts['id'] = $id; return $this->urlAjax(static::ACTION_EDIT, $opts); } /** * Url для дин. свойств * @param int $id ID строки * @param string $action название метода дин. свойств * @param array $opts параметры * @return string */ public function urlDynprops($id, $action = 'listing', array $opts = array()) { $opts['dp_act'] = $action; $opts['owner'] = $id; return $this->urlAjax(static::ACTION_DYNPROPS, $opts); } public function getTemplateJsObject() { if (empty($this->templateJsObject)) { $controller = $this->getControllerName(); $controller = preg_replace('/[^a-zA-Z0-9_]/', '', $controller); $action = $this->getControllerAction(); $action = preg_replace('/[^a-zA-Z0-9_]/', '', $action); $this->templateJsObject = 'j_'.$controller.'_'.$action; } return $this->templateJsObject; } /** * Устанавливаем заголовок списка * @param string $title текст заголовка * @param string|bool $icon иконка */ public function title($title, $icon = true) { $this->wrapper()->title($title)->icon($icon); } /** * Добавляем столбец списка * @param string $id ID столбца * @param string|Closure $title название столбца * @param string|int $width ширина столбца * @param array $extra [ * int 'type' тип отображения: false или COLUMN_TYPE_ * string 'align' выравнивание: false или COLUMN_ALIGN_ * bool|string 'order' сортировка по столбцу: false или COLUMN_ORDER_ * callable 'showIf' return false - столбец скрыт * callable 'attrRow' возможность управления атрибутами строки (может быть только в одном из столбцов), * аргументы($attr - массив атрибутов, & $row - ссылка на массив данные строки) * должна вернуть новый массив атрибутов * $attrRow = call_user_func_array($attrRowCallable, ['attr' => $attrRow, 'row' => & $row ]); * ] * @param string|callable $render ($value, $row, $opts) * @return static */ public function column($id, $title, $width = false, array $extra = [], $render = null) { static $defaults = array( # Тип данных 'type' => ['default'=>self::COLUMN_TYPE_TEXT, 'allowed'=>[ self::COLUMN_TYPE_ID, self::COLUMN_TYPE_TEXT, self::COLUMN_TYPE_DATE, self::COLUMN_TYPE_CUSTOM, ]], # Порядок сортировки 'order' => ['default'=>false, 'allowed'=>[ self::COLUMN_ORDER_ASC, self::COLUMN_ORDER_DESC, ]], # Выравнивание текста 'align' => ['default'=>false, 'allowed'=>[ self::COLUMN_ALIGN_CENTER, self::COLUMN_ALIGN_LEFT, self::COLUMN_ALIGN_RIGHT, ]], ); static $idTypes = array( 'id' => self::COLUMN_TYPE_ID, 'created' => self::COLUMN_TYPE_DATE, 'modified' => self::COLUMN_TYPE_DATE, 'title' => self::COLUMN_TYPE_TEXT, ); if ( ! is_string($id)) { return $this; } if (empty($extra['type']) && array_key_exists($id, $idTypes)) { $extra['type'] = $idTypes[$id]; } foreach ($defaults as $k=>$v) { if ( ! array_key_exists($k, $extra)) { $extra[$k] = $v['default']; continue; } if ($extra[$k] instanceof Closure) { $extra[$k] = call_user_func(Closure::bind($extra[$k], $this), $v); } if ( ! in_array($extra[$k], $v['allowed'], true)) { $extra[$k] = $v['default']; } } $column = array_merge(array( 'id' => $id, 'title' => $title, 'width' => $width, 'render' => ['callable' => $render], 'attr.head' => [], 'attr.cell' => [], 'showIf' => null, 'attrRow' => null, 'actionsRow' => null, 'priority' => (count($this->columns) + 1) * 10, ), $extra); if (array_key_exists('showIf', $column) && $column['showIf'] instanceof Closure) { $column['showIf'] = Closure::bind($column['showIf'], $this); } else { $column['showIf'] = false; } if (empty($render) || ! is_callable($render, true)) { unset($column['render']); } $this->columns[$id] = $column; return $this; } /** * Получение или установка свойства столбца * @param string $id ID столбца * @param string $name название свойства * @param mixed|null $value значение, если null - вернуть текуще значение * @return static|mixed */ public function columnProp($id, $name, $value = null) { if (is_null($value)) { if (isset($this->columns[$id][$name])) { return $this->columns[$id][$name]; } } else { if (isset($this->columns[$id][$name])) { $this->columns[$id][$name] = $value; } } return $this; } /** * Удаление столбца * @param $id * @return static */ public function columnUnset($id) { unset($this->columns[$id]); return $this; } /** * Праобразовать список в nestedSetsTree * @param string $id ID столбца, содержащего название элементов * @param array $opt параметры ['rootID', 'fieldPID', 'fieldNumlevel', 'session', 'classes'] * @return array фильтр для модели */ public function nestedSetsTree($id, array $opt = []) { $default = [ 'rootID' => 1, # ID корневого элемента 'fieldPID' => 'pid', # зазвание поля, содержащего PID записи 'fieldNumlevel' => 'numlevel', # зазвание поля, содержащего numlevel записи 'session' => '', # название session переменной для хранения открытых списков 'classes' => [['but','but-text','folder'], ['but','but-text','folder_ua']], # классы для ноды и элемента ]; if ($this->model) { if (method_exists($this->model, 'nodeGetRootNodeID')) { $default['rootID'] = $this->model->nodeGetRootNodeID(); $default['fieldPID'] = $this->model->nodeGetParentKey(); $default['fieldNumlevel'] = $this->model->nodeGetLevelKey(); $this->onRotate(function () { $this->model->nodesRotate(); }); } } $opt = $this->defaults($opt, $default); $loverName = function($str) { $str = preg_replace('/[^a-zA-Z0-9_]/', '_', $str); return mb_strtolower($str); }; $filter = []; do { if ( ! isset($this->columns[$id])) break; $opt['id'] = $id; if (empty($opt['session'])) { $opt['session'] = $loverName($this->getControllerName()).'_'.$loverName($this->getControllerAction()).'_expand'; } $this->jsInclude('tablednd'); $this->nestedSetsTree = $opt; if (! Session::has($opt['session']) || ! is_array(Session::get($opt['session']))) { Session::put($opt['session'], []); } $expand = array_keys(Session::get($opt['session'])); $expand = array_map('intval', $expand); $expand[] = $opt['rootID']; $expand = array_unique($expand); $filter[ $opt['fieldPID'] ] = $expand; $this->columns[$id]['type'] = static::COLUMN_TYPE_CUSTOM; $oldRender = null; if (isset($this->columns[$id]['render']) && is_callable($this->columns[$id]['render']['callable'])){ $oldRender = $this->columns[$id]['render']; } $this->columns[$id]['render'] = ['callable' => function($value, $row, $options) use (& $opt, & $oldRender, & $expand) { if (isset($options['column']['attr.cell']) && ! empty($row[ $opt['fieldNumlevel'] ])) { static::attrAdd($options['column']['attr.cell'], 'style', 'padding-left:'.($row[ $opt['fieldNumlevel'] ] * 15 - 10).'px;'); } $node = isset($row['numright']) && isset($row['numleft']) && ($row['numright'] - $row['numleft'] > 1); $attr = [ 'class' => 'j-nested-set-expand', 'data-id' => $row['id'], 'data-pid' => $row[ $opt['fieldPID'] ] ?? 0, 'data-numlevel' => $row[ $opt['fieldNumlevel'] ] ?? 0, 'href' => 'javascript:', ]; $expanded = in_array($row['id'], $expand); $data = [ 'attr' => $attr, 'node' => $node, 'value' => $value, 'opt' => $opt, 'expanded' => $expanded, 'noJsRender'=>true, ]; $result = $this->render($data, 'block.list.nestedsets'); if ( ! is_null($oldRender)) { $html = ''; Block::obCallable($html, $oldRender, function($callable) use(& $value, & $row, & $options, & $result, & $attr, & $opt, $node, $expanded) { return call_user_func_array($callable, array( 'value' => & $value, 'row' => & $row, 'options' => & $options, 'html' => & $result, 'extra' => [ 'attr' => & $attr, 'opt' => & $opt, 'node' => $node, 'expanded' => $expanded, ] )); }); return $html; } return $result; }]; if ($this->subAction() === static::ACTION_NESTEDSETS) { $id = $this->input->postget('id', TYPE_UINT); $pid = $this->input->postget('pid', TYPE_UINT); $session = Session::get($opt['session']); if ($this->input->post('session', TYPE_UINT)) { $visible = $this->input->post('visible', TYPE_UINT); if ($visible) { $session[$id] = $pid; } else { $unset = function ($id) use (& $session, & $unset) { foreach ($session as $k => $v) { if ($v == $id) { $unset($k); unset($session[$k]); } } unset($session[$id]); }; $unset($id); } Session::put($opt['session'], $session); $this->ajaxResponseForm([]); } $this->nestedSetsTree['expand'] = true; $filter[ $opt['fieldPID'] ] = $id; $session[$id] = $pid; Session::put($opt['session'], $session); } } while(false); return $filter; } /** * Ключ порядка сортировки * @param string|null $key устанавливаем новое значение ключа, null - возвращаем текущее * @return string */ public function orderKey($key = null) { if ( ! is_null($key) && ! empty($key) && is_string($key)) { $this->orderKey = $key; } return $this->orderKey; } /** * Текущее значение порядка сортировки * @param string|bool $default ID столбца по умолчанию * @param bool $sqlFormat в SQL формате * @return string */ public function order($default = false, $sqlFormat = true) { if ($this->orderActive === null) { $this->orderActive = ['column'=>'']; $value = $this->input->postget($this->orderKey, TYPE_NOTAGS); if (empty($value) && $default !== false) { $value = $default; } if (mb_stripos($value, $this->orderSeparator) > 0) { list($column, $direction) = explode($this->orderSeparator, $value, 2); if (empty($direction) || !in_array($direction, [ static::COLUMN_ORDER_ASC, static::COLUMN_ORDER_DESC ] ) ) { $direction = static::COLUMN_ORDER_ASC; } } else { $column = $value; } if (array_key_exists($column, $this->columns)) { if ( ! isset($direction)) { $direction = $this->columns[$column]['order']; if (empty($direction)) { $direction = static::COLUMN_ORDER_ASC; } } $this->orderActive['column'] = $column; $this->orderActive['direction'] = $direction; $this->orderActive['direction_next'] = ($direction === static::COLUMN_ORDER_ASC ? static::COLUMN_ORDER_DESC : static::COLUMN_ORDER_ASC ); $this->columns[$column]['orderActive'] = $this->orderActive; } } if ($this->orderActive['column']) { if ($sqlFormat) { return $this->orderActive['column'] . ' ' . mb_strtoupper($this->orderActive['direction']); } else { return $this->orderActive['column'] . $this->orderSeparator . $this->orderActive['direction']; } } return ''; } /** * Добавление действия со строками * @param string $key * @param callable|null $callable * @param int $priority * @param array $opts * @return static */ public function rowsActionAdd($key, callable $callable = null, $priority = 0,array $opts = []) { if ( ! empty($priority)) { $opts['priority'] = $priority; } else { switch ($key) { case $this::ACTION_FAV: $opts['priority'] = 10; break; case $this::ACTION_TOGGLE: $opts['priority'] = 20; break; case $this::ACTION_DYNPROPS: $opts['priority'] = 30; break; case $this::ACTION_EDIT: $opts['priority'] = 40; break; case $this::ACTION_DELETE: $opts['priority'] = 50; break; default: $opts['priority'] = count($this->rowsActions) + 10; } } $opts['key'] = $key; if (is_callable($callable)) { $opts['callable'] = $callable; } $this->rowsActions[] = $opts; return $this; } /** * Список действий, которые можно скрыть в '...' * @param array|null $actions список * @return static|array */ public function rowsActionsMore(?array $actions = null) { if (is_null($actions)) { return $this->rowsActionsMore; } $this->rowsActionsMore = $actions; return $this; } /** * Устанавливаем функцию фильтрации списка записей * Функция принимает данные о записи и возвращает false если запись не следует отображать * @param Closure $filter callback($row) return bool */ public function rowsFilter(Closure $filter) { $this->rowsFilter = $filter->bindTo($this); } /** * Добавляем записи в список * @param array $rows набор записей * @param bool|int $total всего записей */ public function rows(array $rows, $total = false) { if ($this->isPaginationMore()) { if (count($rows) > $this->perPage) { $this->paginationMoreAllowed = true; array_pop($rows); } } $this->rows = $rows; if (is_numeric($total)) { $this->total($total); } } /** * Переопределяем ключ ID записи * @param string $key ключ * @return static */ public function rowsIdKey($key) { if ( ! empty($key)) { $this->rowsIdKey = $key; } return $this; } /** * Вызов события перед отрисовкой блока * @param callable $callable аргументы (['list' => объект списока, 'form' => объект первой формы, 'forms' => массив объектов всех форм ]) * должна вернуть string HTML или false - выполнить стандартную отрисовку * @param array $opts доп. параметры ['ob'] * bool 'ob' - форсировать использование буферизации вывода * @return static */ public function onBeforeView(callable $callable, array $opts = []) { $opts['callable'] = $callable; $this->onBeforeView = $opts; return $this; } /** * Отрисовываем блок * @param array $data доп. данные шаблона * @return string HTML */ public function view(array & $data = []) { if ($this->isMassActions()) { $this->massActions()->initActions(); } $html = false; if (BlockList::obCallable($html, $this->onBeforeView, function($callable) { $params = [ 'list' => $this, ]; if ($this->formsTabs) { $params['forms'] = $this->formsTabs->forms(); $params['form'] = reset($params['forms']); } return call_user_func($callable, $params); }) !== false) { if ($html !== false) { return $html; } } $html = $this->render($data); $this->ajaxInitResponse($html); return $html; } /** * Вызов события после отрисовки всех полей списка * @param callable $callable аргументы ($html) $html HTML код полей списка * должна вернуть string HTML * @param array $opts доп. параметры ['ob'] * bool 'ob' - форсировать использование буферизации вывода * @return static */ public function afterFilterRender(callable $callable, array $opts = []) { $opts['callable'] = $callable; $this->afterFilterRender = $opts; return $this; } /** * Вызов события после отрисовки всех полей списка * @param callable $callable аргументы ($html) $html HTML код полей списка * должна вернуть string HTML * @param array $opts доп. параметры ['ob'] * bool 'ob' - форсировать использование буферизации вывода * @return static */ public function afterRowsRender(callable $callable, array $opts = []) { $opts['callable'] = $callable; $this->afterRowsRender = $opts; return $this; } /** * Вызов события после отрисовки списка * @param callable $callable аргументы ($html) $html HTML код списка * должна вернуть string HTML * @param array $opts доп. параметры ['ob'] * bool 'ob' - форсировать использование буферизации вывода * @return static */ public function afterListRender(callable $callable, array $opts = []) { $opts['callable'] = $callable; $this->afterListRender = $opts; return $this; } /** * Вызов события после отрисовки списка * @param callable $callable аргументы ($data) $data массив с ajax ответом * должна вернуть array * @return static */ public function onAjaxResponse(callable $callable) { $this->onAjaxResponse = $callable; return $this; } /** * Рендеринг списка * @param array $options доп. параметры: * bool 'columns' - ренедрить заголовок таблицы * int 'rowId' - ID строки * @return string HTML */ protected function rowsRender(array $options = []) { if ($this->table) { $data = array_merge([ 'columns' => true, 'rowId' => 0, 'rowActions' => $this->rowsActions, 'rowActionsMore' => $this->rowsActionsMore, 'noJsRender' => true, ], $options); $html = $this->render($data, 'block.list.table'); } else { $data = array_merge([ 'rowId' => 0, 'rowActions' => $this->rowsActions, 'rowActionsMore' => $this->rowsActionsMore, 'noJsRender' => true, ], array_merge($this->customRenderer['rows'] ?? [], $options)); $html = $this->render($data, 'block.list.custom'); } if ($this->afterRowsRender) { static::obCallable($html, $this->afterRowsRender, function($callable) use( & $html) { return call_user_func($callable, $html, $this); }); } return $html; } /** * Рендеринг отдельной строки * @param int $id ID строки * @return string HTML */ protected function rowRender($id, array $options = array()) { $options['rowId'] = $id; $options['rowActions'] = $this->rowsActions; if ($this->table) { if ( ! array_key_exists('columns', $options)) { $options['columns'] = false; } } return $this->rowsRender($options); } /** * Отрисовка списка отличная от табличной * @param callable $renderer функция отрисовки принимающая данные о строке * @param array $opts * @return static */ public function custom(callable $renderer, $opts = []) { if (is_callable($renderer)) { $this->table = false; $opts['callable'] = $renderer; $this->customRenderer = $opts; } return $this; } /** * Ключ текущего вложенного действия или false * @return string|bool */ public function subAction() { $subAction = $this->input->postget($this->subActionKey, TYPE_NOTAGS); if (empty($subAction)) { return false; } return $subAction; } /** * Получение название GET переменной - параметра содержащей ajax действие * @return string */ public function subActionKey() { return $this->subActionKey; } /** * */ protected function formsTabsInit() { if ( ! $this->formsTabs) { $this->formsTabs = new BlockFormsTabs($this); $this->formsTabs->init(); } } /** * @return BlockFormsTabs */ public function getFormsTabs() { $this->formsTabsInit(); return $this->formsTabs; } /** * Добавление CRUD формы, обрабатываемой в пределах списка * @param Form|array $forms форма или массив форм * @param array $opt доп. опции * @return BlockList */ public function formAdd($forms, array $opt = []) { $opt = array_merge([ 'add' => true, 'edit' => true, 'pills' => false, 'noWrapper' => false, 'popup' => false, ], $opt); $this->formsTabsInit(); $this->formsTabs->pills = $opt['pills']; $this->formsTabs->popup = $opt['popup']; if ( ! is_array($forms)) { $forms = array($forms); } foreach ($forms as $v) { /** @var $v Form */ $this->formsTabs->formAdd($v, $opt); if (is_null($this->model)) { $model = $v->getModel(); if ( ! is_null($model)) { $this->useModel($model); } } } if ($opt['noWrapper']) { $this->formsTabs->wrapper()->invisible(); } switch ($this->subAction()) { case static::ACTION_ADD: $this->formHTML = function() { $this->getTemplateJsObject(); return $this->formsTabs->content(0); }; break; case static::ACTION_EDIT: $this->formHTML = function() { $this->getTemplateJsObject(); $id = $this->input->postget('id', TYPE_STR); return $this->formsTabs->content($id); }; break; } if ( ! empty($opt['add'])) { $this->wrapper()->link($this->titleAddLink, $this->urlAdd(is_array($opt['add']) ? $opt['add'] : []), ($opt['popup'] ? 'j-form-popup' : 'j-form-url')); } if ( ! empty($opt['edit'])) { $params = ['popup' => $opt['popup']]; $this->rowsActionAdd(static::ACTION_EDIT, null, 0, $params); } return $this; } /** * Добавление произвольной формы, обрабатываемой в пределах списка * @param Form $form * @param string $action уникальный keyword action формы в пределах списка * @param array $opt доп. опции * 'idKey' - название ключа, содержащего recordID для формы * 'popup' => true - открывать форму в попапе * 'popup' => [width => 500] - ширина попапа 500px */ public function formCustom(Form $form, $action, array $opt = []) { if ( ! $form instanceof Form) return; if (empty($action)) return; func::array_defaults($opt, [ 'idKey' => 'id', 'popup' => false, ]); if ($this->subAction() == $action) { $recordID = $this->input->postget($opt['idKey'], TYPE_STR); $record = ['recordID' => $recordID]; if (empty($opt['popup'])) { $this->formHTML = function() use ($form, $action, $recordID, & $record, & $opt){ $jsObjectList = $this->getTemplateJsObject(); $form->jsHistory($jsObjectList, $this->urlAjax($action, [$opt['idKey'] => $recordID])); return $form->view($record); }; } else { if ($this->isAJAX()) { $form->wrapper()->popup($opt['popup']); $html = $form->view($record); $this->ajaxResponseForm($this->appendIncludedStatic(['popup' => $html])); } } } } /** * Установка заголовков для формы * @param array $opt ключи * 'title.add' => Заголовок формы добавления 'title.edit' => Заголовок формы редактирования 'title.add.link' => Заголовок кнопки добавить * @return BlockList */ public function formsHeader(array $opt = []) { $this->formsTabsInit(); if ( ! empty($opt['title.add'])) { $this->formsTabs->titleAdd = $opt['title.add']; } if ( ! empty($opt['title.edit'])) { $this->formsTabs->titleEdit = $opt['title.edit']; } if ( ! empty($opt['title.add.link'])) { $this->titleAddLink = $opt['title.add.link']; } return $this; } /** * Событие * @param Dynprops $dp */ public function onDynprops(Dynprops $dp) { $dp->setSettings('act_listing', $this->getControllerAction().'&list_action='.static::ACTION_DYNPROPS.'&dp_act=listing'); $dp->setSettings('act_action', $this->getControllerAction().'&list_action='.static::ACTION_DYNPROPS.'&dp_act=action'); $this->rowsActionAdd(static::ACTION_DYNPROPS); if ($this->subAction() === static::ACTION_DYNPROPS) { $method = $this->input->getpost('dp_act', TYPE_STR); if (method_exists($dp, $method)) { $html = call_user_func([$dp, $method]); if ($method != 'listing') { $wrapper = new BlockWrapper(); $wrapper->setTemplateDir($this->getTemplateDir()); $wrapper->init(); $wrapper->title(_t('@', 'Properties')); $data = ['content' => $html]; $html = $wrapper->view($data); } if ($this->isAJAX()) { $this->ajaxResponseForm(['form' => $html]); } $this->formHTML = $html; } } } /** * Указание модели для работы с записью * @param $model Model * @return self */ public function useModel(Model $model) { $this->model = $model; return $this; } /** * Получение модели для работы с записью * @return Model|null */ public function getModel() { return $this->model; } /** * Событие удаления * @param callable|Model|bool $onDelete функция вызываемая по событию, если нет ошибок или объект модели или true - использовать модель по умолчанию * @param callable|null $beforeDelete функция вызываемая по событию для проверки возможности удаления записи * @return BlockList * @throws AdminFormsException */ public function onDelete($onDelete = true, callable $beforeDelete = null) { if (is_bool($onDelete)) { if ($onDelete === false) return $this; if (is_null($this->model)) { throw new AdminFormsException(static::class.': Model not set. Use '.static::class.'::useModel first.'); } $onDelete = $this->getModel(); } if ($onDelete instanceof Model) { $onDelete = function($id) use ($onDelete){ if ( ! $id) { $this->errors->impossible(); return; } $onDelete->find($id)->delete(); }; } else if ( ! is_callable($onDelete)) { throw new AdminFormsException(static::class.': The $onDelete must be instance of Model or callable.'); } $this->rowsActionAdd(static::ACTION_DELETE); if ($this->subAction() === static::ACTION_DELETE) { $id = $this->input->postget('id', TYPE_STR); if (is_callable($beforeDelete)) { $this->response( call_user_func($beforeDelete, $id) ); } if ($this->formsTabs) { $this->formsTabs->onBeforeDelete($id); } if ($this->errors->no()) { if ($this->formsTabs) { $this->formsTabs->onDelete($id); } $this->response( call_user_func($onDelete, $id) ); } $this->ajaxResponseForm(); } return $this; } /** * Событие переключения состояния * @param callable|Model|bool $onToggle функция вызываемая по событию или объект модели или true - использовать модель по умолчанию * @param string $type тип переключателя: 'enabled', 'fav' * @return BlockList * @throws AdminFormsException */ public function onToggle($onToggle = true, $type = 'enabled') { if (is_bool($onToggle)) { if ($onToggle === false) return $this; if (is_null($this->model)) { throw new AdminFormsException(static::class.': Model not set. Use '.static::class.'::useModel first.'); } $onToggle = $this->getModel(); } if ($onToggle instanceof Model) { $onToggle = function($id) use ($onToggle, $type){ if ( ! $id) return; $onToggle->find($id)->toggle($type); }; } else if ( ! is_callable($onToggle)) { throw new AdminFormsException(static::class.': The $onToggle must be instance of Model or callable.'); } if ($type === 'enabled') { $this->rowsActionAdd(static::ACTION_TOGGLE); } else if ($type === 'fav') { $this->rowsActionAdd(static::ACTION_FAV); } if ($this->subAction() === 'toggle') { $id = $this->input->postget('id', TYPE_UINT); if ($this->input->get('type', TYPE_STR) === $type) { $response = $this->response(call_user_func($onToggle, $id, $type)); if (is_null($response)) { $response = []; } $this->ajaxResponseForm($response); } } return $this; } /** * Событие ротации * @param callable|Model|bool $onRotate функция вызываемая по событию или объект модели или true - использовать модель по умолчанию * @param string $field поле сортировки * @param array $opts параметры * @return BlockList * @throws AdminFormsException */ public function onRotate($onRotate = true, $field = 'num', $opts = []) { if (is_bool($onRotate)) { if ($onRotate === false) return $this; if (is_null($this->model)) { throw new AdminFormsException(static::class.': Model not set. Use '.static::class.'::useModel first.'); } $onRotate = $this->getModel(); } if ($onRotate instanceof Model) { $onRotate = function($tab) use ($onRotate, $field, $opts){ $onRotate->rotateTableDnd($field, $opts); }; } else if ( ! is_callable($onRotate)) { throw new AdminFormsException(static::class.': The $onRotate must be instance of Model or callable.'); } if ($this->subAction() === 'rotate') { $tab = $this->tab(); $this->response(call_user_func($onRotate, $tab, $field)); $this->ajaxResponseForm(); } return $this; } /** * Проверка активности функциональности перетягивания строк * @return bool */ public function isActiveRotation() { if (is_callable($this->isRotation)) { return call_user_func($this->isRotation); } return $this->tabs()->isActiveRotation(); } /** * Установить функцию, определяющую необходимость перетягивания строк * @param callable $callable * @return static */ public function isRotation(callable $callable) { $this->isRotation = $callable; return $this; } /** * Добавление события, доступного в режиме разработки * @param string $title заголовок * @param callable $callback обработчик * @param bool|string $confirm спросить подтверждение (текст подтверждения) * @param string $icon класс иконки * @param array $attr атрибуты ['debug-only', 'action'] * @return static */ public function onFordev($title, callable $callback, $confirm = true, $icon = 'icon-refresh', array $attr = []) { func::array_defaults($attr, [ 'debug-only' => false, # доступно только в режиме отладки 'action' => '', # название экшена (если не указан генерится автоматически) ]); if (empty($attr['action'])) { $this->onFordevCounter++; $index = $this->getControllerName().'-'.$this->getControllerAction(); $action = $index.'-dev-'.$this->onFordevCounter; } else { $action = $attr['action']; } unset($attr['action']); $this->wrapper()->fordev($title, function() use ($action) { return $this->urlAjax($action); }, $confirm, $icon, $attr); if ($this->subAction() == $action) { $wrapper = new BlockWrapper(); $wrapper->setTemplateDir($this->getTemplateDir()); $wrapper->init(); $wrapper->title($title); $wrapper->icon($icon); $response = $this->response(call_user_func($callback, $wrapper)); if ($this->isAJAX()) { $this->ajaxResponseForm($response); } $html = ''; if (is_array($response)) { if (isset($response['html'])) { if ( ! empty($response['noWrapper'])) { $this->formHTML = $response['html']; return $this; } $html = $response['html']; } } else { $html = $response; } $data = ['content' => $html]; $this->formHTML = $wrapper->view($data); } return $this; } /** * Добавление обработчика обработчики javascript событий * @param string $name имя события (onInit, onSubmit, onReset, onProgress, onPopstate, onFormPopstate, onTabChange, onBeforeSubmitQueryPlus) * @param string|callable $action действие string html или callable, возвращающий string html * @param array $opts доп. параметры ['ob', 'js', function'] * bool 'ob' - форсировать использование буферизации вывода * bool 'js' = true - вырезать тег