id = $id; $this->setController($controller, $action); } /** * Инициализация */ public function init() { parent::init(); $this->setTemplateDir( $this->app->corePath('tpl/admin/' . $this->app->adminTheme() . '/form') ); $this->wrapper()->icon(false); $this->setTemplateName('layout'); $this->detectAjaxInit(); $this->trigger('init'); } /** * Fire form event * @param string $event * @param array $data * @return static */ public function trigger(string $event, array $data = []) { $this->app->hook($this->id . '.admin.form.' . $event, $this, $data); return $this; } /** * Получаем ID формы * @return int */ public function id() { return $this->id; } /** * Установить ID записи * @param int|string $id * @return static */ public function setRecordID($id) { $this->recordID = $id; if ($this->_onRecordID && is_callable($this->_onRecordID)) { $this->recordID = call_user_func($this->_onRecordID, $id); } return $this; } /** * Получаем ID записи * @return string */ public function recordID() { return $this->recordID; } /** * Установка обработчика события установки ID записи * @param callable $callback функция обработчика, аргументы ($recordID) $recordID - ID записи, * функция должна вернуть ID записи * @return static */ public function onRecordID(callable $callback) { $this->_onRecordID = $callback; return $this; } /** * Указание модели для работы с данными формы * @param Model $model * @param array $opts [ * 'with' => relations * 'tag' => string * 'beforeSave' => callable ($data, $id) return array data to save * 'afterLoad' => callable ($data, $id, $fields) return array data * ] * @return static */ public function useModel(Model $model, array $opts = []) { $opts = $this->defaults($opts, [ 'with' => [], 'tag' => false, 'beforeSave' => null, 'afterLoad' => null, ]); $this->model = $model; if (is_null($this->_onLoad)) { $this->onLoad(function($id, $fields) use ( & $opts) { $model = $this->getModel(); if (! $opts['tag']) { $opts['tags'] = 'admin.form.load.'.$this->getControllerName().'.'.$this->getControllerAction(); } $opts['form'] = $this; $data = $model->onAdminFormLoad($id, $opts); if (is_callable($opts['afterLoad'])) { $data = call_user_func($opts['afterLoad'], $data, $id, $fields); } return $data; }); } if (is_null($this->_onSave)) { $this->onSave(function($id, $data) use ( & $opts) { $model = $this->getModel(); if (is_callable($opts['beforeSave'])) { $data = call_user_func($opts['beforeSave'], $data, $id); } if ($id) { return $model->find($id)->fill($data)->save() ? $id : false; } else { $model->fill($data)->save(); return $model->getKey(); } }, false); } return $this; } /** * Получение модели для работы с данными формы * @return Model|null */ public function getModel() { return $this->model; } /** * Установка обработчика события получения данных о записи * @param callable $callback функция обработчика, аргументы ($recordID, array $fields) $recordID - ID записи, $fields - массив со списком необходимых полей * функция должна вернуть массив с данными * @return static */ public function onLoad(callable $callback) { $this->_onLoad = $callback; $this->checkAjax(); return $this; } public function issetLoad() { return is_callable($this->_onLoad); } /** * Установка обработчика события сохранения данных о записи * @param callable $callback функция обработчика, аргументы ($recordID, $data) $recordID - ID записи, $data - массив данных в формате [имяполя => значение] * если $recordID == 0 - добавление новой записи, должна вернуть ID добавленной записи * функция должна вернуть результат сохранения * @param bool $toJSON преобразовывать данные типа TYPE_ARRAY к JSON строке * @return static */ public function onSave(callable $callback, $toJSON = true) { $this->_onSave = $callback; $this->toJSON = $toJSON; $this->checkAjax(); return $this; } /** * @return bool */ public function issetSave() { return is_callable($this->_onSave); } /** * Приводить или нет сохраняемые массивы к JSON строкам * @return bool */ public function _toJSON() { return $this->toJSON; } /** * Проверка возможности запуска обработчика ajax запросов * @return void */ protected function checkAjax() { if ($this->issetSave() && $this->issetLoad()) { bff::hook('admin.form.init.'.$this->getControllerName().'.'.$this->getControllerAction(), $this); $this->ajax(); } } /** * Построение контента формы * @param array $data * @return string HTML */ public function content($data = []) { $data = $this->defaults($data, [ 'form' => false, 'settingsForm' => false, ]); $priority = []; $i = 0; foreach ($this->tabs as $k => & $v) { $i++; $priority[$k] = $v['priority'] ? $v['priority'] : $i; $v['value'] = $this->value($k, false); } unset($v); asort($priority, SORT_NUMERIC); $data['priority'] = $priority; $data['tabs'] = & $this->tabs; $data['fields'] = & $this->fields; $data['cntTabs'] = 0; $tab = ''; $ftab = $this->input->getpost('ftab', TYPE_STR); if (! empty($ftab)) { foreach ($this->tabs as $v) { if ($v['name'] == $ftab) { $tab = $v['name']; break; } } } else { foreach ($this->tabs as $v) { if ($v['default']) { $tab = $v['name']; break; } } } if (empty($tab)) { $t = array_keys($priority); $t = reset($t); if (isset($this->tabs[$t])){ $tab = $this->tabs[$t]['name']; } } $data['tab'] = $tab; foreach ($this->tabs as & $v) { $v['counter'] = static::obCallableAuto($v['counter']); $v['style'] = static::obCallableAuto($v['style']); } unset($v); $data['tabsBlock'] = $this->render($data, 'tabs'); $priority = []; foreach ($this->fields as $k => & $v) { $priority[$k] = $v['priority']; $v['value'] = $this->value($k, false); } unset($v); asort($priority, SORT_NUMERIC); $data['priority'] = $priority; $content = $this->render($data, 'content'); static::obCallable($content, $this->_contentWrapper, function($callable) use ( & $content) { return call_user_func($callable, $content); }); if ($data['form']) { $priority = []; if (! is_array($this->buttons)) { $this->buttons = []; } foreach ($this->buttons as $k => $v) { $priority[$k] = $v['priority']; } asort($priority, SORT_NUMERIC); $data['priority'] = $priority; $data['buttons'] = $this->buttons; $data['iframe'] = $this->iframe; $data['buttons'] = $this->render($data, 'buttons'); } static::obCallable($this->_contentRight['html'], $this->_contentRight, function($callable) { return call_user_func($callable, $this); }); static::obCallable($this->_contentTop['html'], $this->_contentTop, function($callable) { return call_user_func($callable, $this); }); static::obCallable($this->_contentBottom['html'], $this->_contentBottom, function($callable) { return call_user_func($callable, $this); }); $layout = [ 'form' => $data['form'], 'content' => $content, 'tabs' => $data['tabsBlock'], 'buttons' => isset($data['buttons']) ? $data['buttons'] : '', 'alerts' => $this->_alerts, 'right' => $this->_contentRight, 'top' => $this->_contentTop, 'bottom' => $this->_contentBottom, 'iframe' => $this->iframe, ]; return $this->render($layout); } /** * Построение формы * @param array $data доп. данные шаблона * @return string HTML * @throws AdminFormsException */ public function view(array & $data = []) { $this->ajax(); if (isset($data['recordID'])) { $this->setRecordID($data['recordID']); $this->applyWhen(); $this->load(); if ($this->buttonsDefault()) { $this->buttonSubmit('', null, ['group' => true, 'priority' => 10]); if (! empty($data['recordID'])) { $this->buttonSubmitReturn('', ['group' => true, 'priority' => 11]); } $this->buttonCancel('', ['priority' => 100]); } } else { $this->applyWhen(); } $html = $this->content(['form' => true]); $this->ajaxInitResponse($html); return $html; } /** * Валидация данных формы * @param array $post данные для для сабмита, [] - взять из $_POST * @return array данные для созранения */ public function _validate(array $post = []) { $isPost = empty($post); $result = []; foreach ($this->fields as $v) { /** @var Input $field */ $field = $v['field']; $name = $field->name(); $clean = $field->clean(); if ($isPost) { if ($clean != TYPE_BOOL && ! isset($_POST[$name])) { continue; } $value = $this->input->post($name, $clean); } else { if ($clean != TYPE_BOOL && ! isset($post[$name])) { continue; } $value = $this->input->clean($post[$name], $clean); } if (! $field->validate(['value' => & $value, 'result' => & $result])) { continue; } $result[$name] = $value; } return $result; } /** * Дополнительная валидация формы после сохранения (перенос удаление файлов и т.д.) * @param bool $isInserted флаг добавления записи * @param array $saved @ref сохраненные данные * @return array|bool */ public function _validateAfter($isInserted = false, array & $saved = []) { $result = []; foreach ($this->fields as $v) { /** @var Input $field */ $field = $v['field']; $field->validateAfter($isInserted, ['saved' => & $saved, 'result' => & $result]); } return $result; } /** * Вернуть значение для поля * @param string $fieldName имя поля или "таб/имя поля" * @param string|false|null $lang если указанно вернуть для языка (false - для всех языков) * @param bool $default значение по умолчанию * @return mixed */ public function value($fieldName = '', $lang = null, $default = false) { $result = $default; if (is_null($lang)) { $lang = $this->locale->current(); } $fieldActive = $this->fieldActive; if (! empty($fieldName)) { $fieldActive = $this->_fieldSelector($fieldName); } do { if (! isset($this->fields[$fieldActive])) { # нет активного поля break; } /** @var Input $field */ $field = $this->fields[$fieldActive]['field']; # сохраненные значения $result = $this->_fieldData($fieldActive, $field->getDefault($default)); $field->setValue($result); $result = $field->value($lang); } while (false); return $result; } /** * Формирование значения атрибута name для тегов * @param int $fieldID ID поля * @param string $prefix префикс * @return string */ public function _inputNameGenerator($fieldID, $prefix = '') { if (isset($this->fields[ $fieldID ])) { /** @var Input $field */ $field = $this->fields[ $fieldID ]['field']; if (empty($prefix)) { return $field->name(); } else { return $prefix . '[' . $field->name() . ']'; } } return ''; } /** * Получение значения поля из данных * @param int $fieldID ID поля * @param mixed $default значение по умолчанию * @return mixed */ public function _fieldData($fieldID, $default = '') { do { if (! isset($this->fields[ $fieldID ])) { break; } /** @var Input $field */ $field = $this->fields[ $fieldID ]['field']; $name = $field->name(); if (! isset($this->data[ $name ])) { break; } return $this->data[ $name ]; } while (false); return $default; } /** * Получение данных о записи * @return array */ public function _data() { return $this->data; } /** * Получение данных о записи * @param string|null $key * @param mixed $default * @return mixed */ public function getData(?string $key, $default = null) { if (! is_null($key)) { return $this->data[$key] ?? $default; } return $this->data; } /** * Установка данных о записи * @param string $name ключ поля * @param mixed $value значение * @return static */ public function setData($name, $value) { $this->data[$name] = $value; return $this; } /** * Получение данных в ajax запросах * @param int $fieldID ID поля * @param mixed $default значение по умолчанию * @param bool $reset сбросить кеш * @return mixed */ public function _ajaxData($fieldID, $default = '', $reset = false) { do { if (! isset($this->fields[ $fieldID ])) { break; } /** @var Input $field */ $field = $this->fields[ $fieldID ]['field']; $name = $field->name(); if (! $this->recordID) { break; } if (array_key_exists($name, $this->data) && ! $reset) { return $this->data[ $name ]; } $this->load([ $name ]); if (array_key_exists($name, $this->data)) { return $this->data[ $name ]; } } while (false); return $default; } /** * Сохранение данных в ajax запросах * @param int $fieldID ID поля * @param mixed $value заначение * @throws AdminFormsException */ public function _ajaxSave($fieldID, $value) { do { if (! isset($this->fields[ $fieldID ])) { break; } /** @var Input $field */ $field = $this->fields[ $fieldID ]['field']; $name = $field->name(); if (! $this->recordID) { break; } $this->save([$name => $value]); unset($this->data[$name]); } while (false); } /** * Сохранение данных формы * @param array $data данные для сохранения * @return bool * @throws AdminFormsException */ public function save($data) { if (empty($data)) { return false; } if (! $this->issetSave()) { throw new AdminFormsException('Error initialization '.static::class.'. Require to initialise method onSave or useModel'); } foreach ($data as $k => $v) { if (! isset($this->data[$k])) { continue; } $this->data[$k] = $v; } if ($this->toJSON) { foreach ($this->fields as $v) { /** @var Input $field */ $field = $v['field']; $name = $field->name(); if (! isset($data[$name])) { continue; } if (! is_array($data[$name])) { continue; } $lang = $field->lang(); if ($lang === static::LANG_ARRAY) { continue; } $data[$name] = json_encode($data[$name], JSON_UNESCAPED_UNICODE); } } $inserted = empty($this->recordID); $this->trigger('saving', ['data' => &$data, 'recordID' => $this->recordID]); $response = call_user_func($this->_onSave, $this->recordID, $data); if (! $this->recordID && $response) { $this->recordID = $response; } $this->trigger('saved', ['data' => &$data, 'response' => &$response, 'recordID' => $this->recordID, 'inserted' => $inserted]); return $this->response($response); } /** * Загрузка данных для формы * @param array $fields массив требуемых полей * @throws AdminFormsException * @return void */ public function load(array $fields = []) { if (! $this->issetLoad()) { throw new AdminFormsException('Error initialization '.static::class.'. Require to initialise method onLoad or useModel'); } if (! $this->recordID) { $this->data = []; return; } if (empty($fields)) { foreach ($this->fields as $v) { /** @var Input $field */ $field = $v['field']; $fields = array_merge($fields, $field->fieldsList()); } $fields = array_unique($fields); } $this->trigger('loading', ['fields' => &$fields]); $data = $this->response( call_user_func($this->_onLoad, $this->recordID, $fields) ); if (! is_array($data)) { $data = []; } $this->trigger('loaded', ['data' => &$data]); foreach ($this->fields as $v) { /** @var Input $field */ $field = $v['field']; $name = $field->name(); if (! isset($data[$name])) { continue; } $clean = $field->clean(); if (($clean == TYPE_ARRAY || $clean >= TYPE_ARRAY_BOOL) && is_string($data[$name])) { $data[$name] = json_decode($data[$name], true); if (! is_array($data[$name])) { $data[$name] = []; } } } $this->data = array_merge($this->data, $data); } /** * Получение URL для обработки ajax запросов * @param string $action название события * @return string */ public function ajaxUrl($action = '') { return Url::admin($this->getControllerName() . '/' . $this->getControllerAction()) . $this->_ajaxParams . '&form_id=' . $this->id . '&record_id=' . HTML::escape($this->recordID()) . '&' . $this->ajaxAction . '=' . $action; } /** * Добавление параметров к 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; } /** * Название GET переменной - параметра содержащей ajax действие * @return string */ public function ajaxAction() { return $this->ajaxAction; } /** * Обработчик ajax запросов * @return \bff\http\Response|void */ public function ajax() { if ($this->input->getpost('form_id', TYPE_STR) != $this->id) { return; } if ($this->ajaxProcessed) { return; } $this->ajaxProcessed = true; $recordID = $this->input->getpost('record_id', TYPE_STR); if ($recordID) { $this->setRecordID($recordID); } $act = $this->input->getpost($this->ajaxAction, TYPE_STR); $response = []; switch ($act) { case 'save': # сабмит формы $id = $this->input->postget('id', TYPE_STR); $this->recordSubmit($id, [], $response); if (! isset($response['reload']) && $this->reload) { $response['reload'] = $this->reload; } break; case 'tab-init': # инициализация формы в табе $this->applyWhen(); $id = $this->input->postget('recordID', TYPE_UINT); $this->wrapper()->hide(); $recordData = ['recordID' => $id]; $response['form'] = $this->view($recordData); break; case 'field_action': $this->applyWhen(); $unionID = $this->input->getpost('union_id', TYPE_UINT); if ($unionID && isset($this->fields[$unionID])) { /** @var Fields\Union */ $union = $this->fields[$unionID]['field']; if ($union instanceof Fields\Union) { $response = $union->ajax(); break; } } $fieldID = $this->input->getpost('field_id', TYPE_UINT); if ($fieldID && isset($this->fields[$fieldID])) { /** @var Input $field */ $field = $this->fields[$fieldID]['field']; $response = $field->ajax(); break; } break; case 'reloadif': $response = $this->ajaxHandlerReloadIf(); break; default: $this->applyWhen(); $response = false; if (isset($this->ajaxHandlers[$act])) { $response = call_user_func($this->ajaxHandlers[$act]); } } if ($response === false) { return; } $this->ajaxResponseForm($response); } /** * Установка обработчика ajax запроса для формы поля * @param string $action * @param callable|null $callable * @return static|callable|null */ public function formAjaxHandler($action, $callable) { if (is_null($callable)) { return $this->ajaxHandlers[$action] ?? null; } $this->ajaxHandlers[$action] = $callable; return $this; } /** * Обработчик ajax запроса ReloadIf * @return array */ protected function ajaxHandlerReloadIf() { $response = []; do { $this->applyWhen(); $masterID = $this->input->post('master', TYPE_UINT); $value = $this->input->post('value', TYPE_NOCLEAN); $formData = $this->input->post('formData', TYPE_STR); if (! $masterID || ! isset($this->fields[$masterID])) { break; } /** @var Input $master */ $master = $this->fields[$masterID]['field']; if (empty($master->reloadIF['master'])) { break; } # Загрузим данные в форму parse_str($formData, $formData); $formData = $this->_validate($formData); $this->errors->clear(); # Вызовем все функции 'condition' привязанне к полю 'master', для возможности изменения $formData foreach ($master->reloadIF['master'] as $v) { if (empty($v['condition'])) { continue; } call_user_func($v['condition'], $value, $master, ['data' => & $formData, 'response' => & $response]); } $this->data = array_merge($this->data, $formData); # Рендерим контент формы $this->reloadIfContent = []; $this->content(); $reload = []; $allowOther = function ($slave) use ($master) { /** @var Input $slave */ if (count($slave->reloadIF['slave']) == 1) { return true; } foreach ($slave->reloadIF['slave'] as $v) { if (! in_array($v['mode'], ['present', 'absent'])) { continue; } /** @var Input $field */ $field = $v['master']; if ($field->id() == $master->id()) { continue; } $value = $field->value(); switch ($v['mode']) { case 'present': if (! call_user_func($v['condition'], $value, $field)) { return false; } break; case 'absent': if (call_user_func($v['condition'], $value, $field)) { return false; } break; } } return true; }; foreach ($master->reloadIF['master'] as $v) { if (empty($v['condition'])) { continue; } /** @var Input $slave */ $slave = $v['slave']; $id = $slave->id(); if (! array_key_exists($id, $this->reloadIfContent)) { continue; } switch ($v['mode']) { case 'reload': if (! call_user_func($v['condition'], $value, $slave)) { break; } $reload[$id] = $this->reloadIfContent[$id]; break; case 'present': $reload[$id] = ''; if (call_user_func($v['condition'], $value, $slave)) { if ($allowOther($slave)) { $reload[$id] = $this->reloadIfContent[$id]; } } break; case 'absent': $reload[$id] = ''; if (! call_user_func($v['condition'], $value, $slave)) { if ($allowOther($slave)) { $reload[$id] = $this->reloadIfContent[$id]; } } break; } } $response['reload'] = $reload; $response['master'] = [ 'id' => $masterID, 'name' => $master->name(), 'value' => $value, ]; $this->appendIncludedStatic($response); } while(false); return $response; } /** * @param Input $field * @param string $content * @return string */ public function _reloadIfSlaveContent($field, $content) { if (is_array($this->reloadIfContent)) { $this->reloadIfContent[$field->id()] = $content; } else if (is_null($this->reloadIfContent)) { if (! empty($field->reloadIF['slave'])) { foreach ($field->reloadIF['slave'] as $v) { if (! in_array($v['mode'], ['present', 'absent'])) { continue; } /** @var Input $master */ $master = $v['master']; $value = $master->value(); switch ($v['mode']) { case 'present': if (! call_user_func($v['condition'], $value, $field)) { $content = ''; break 2; } break; case 'absent': if (call_user_func($v['condition'], $value, $field)) { $content = ''; break 2; } break; } } } } return $content; } /** * Управление флагом обработки ajax запросов * @param bool|null $processed * @return static|bool */ public function ajaxProcessed($processed = null) { if (is_null($processed)) { return $this->ajaxProcessed; } $this->ajaxProcessed = $processed; return $this; } /** * Ключ текущего вложенного действия или false * @return string|bool */ public function subAction() { $subAction = $this->input->postget($this->ajaxAction, TYPE_NOTAGS); if (empty($subAction)) { return false; } return $subAction; } /** * Установка обработчика события проверки целосности данных по событию сабмита формы * @param callable $callback функция обработчика, аргументы ($recordID, array('data' => & $data) $recordID - ID записи, $data - ссылка массив с данными для сохранения в формате [имяполя => значение] * обработчик должен быть установлен перед обработчиком события сохранения данных onSave * возвращаемые значения: * false - не корректные данные сохранение не происходит, если не срабатывает проверка $this->errors->no() сохранение не происходит * array - массив с данными для ajax ответа * @param array $opts доп. параметры * @return static */ public function onSubmit(callable $callback, array $opts = []) { $opts['callable'] = $callback; $this->_onSubmit = $opts; return $this; } /** * Установка обработчика события проверки целосности данных по событию сабмита формы после сохранения * @param callable $callback функция обработчика, аргументы * ($recordID, - ID записи * array( * 'data' => & @ref, - ссылка на массив с данными для сохранения, в формате [имяполя => значение] * 'saved' => & @ref, - ссылка на массив сохраненных данных, в формате [имяполя => значение] * 'response' => & @ref, - ссылка на массив с данными для ajax ответа форме * 'inserted' => bool - флаг добавления новой записи * ) * ) * обработчик должен быть установлен перед обработчиком события сохранения данных onSave * @param array $opts доп. параметры * @return static */ public function onSubmitAfter(callable $callback, array $opts = []) { $opts['callable'] = $callback; $this->_onSubmitAfter = $opts; return $this; } /** Событие сабмита формы * @param int $recordID * @param array $post данные для для сабмита, [] - взять из $_POST * @param array $response @ref массив для аякс ответа * @throws * @return void */ public function recordSubmit($recordID, array $post = [], array & $response = []) { $this->setRecordID($recordID); $this->applyWhen(); # валидируем данные $data = $this->_validate($post); # событие onSubmit if (isset($this->_onSubmit['callable']) && is_callable($this->_onSubmit['callable'])) { $result = $this->response( call_user_func($this->_onSubmit['callable'], $recordID, ['data' => & $data, 'response' => & $response]) ); if ($result === false) { return; } if (is_array($result)) { $response = $result; } } if (! $this->errors->no()) { return; } # сохраняем данные $res = $this->save($data); $inserted = empty($recordID) && is_int($res) && $res > 0; if (empty($this->recordID)) { $this->setRecordID($res); } # действия после сохранения (перенос картинок и файлов) $after = []; if ($this->recordID) { $after = $this->_validateAfter($inserted, $data); } # событие onSubmitAfter if (isset($this->_onSubmitAfter['callable']) && is_callable($this->_onSubmitAfter['callable'])) { $this->response(call_user_func($this->_onSubmitAfter['callable'], $this->recordID, [ 'data' => & $after, 'saved' => & $data, 'response' => & $response, 'inserted' => $inserted, ])); } } /** * Событие перед удалением записи, если есть ошибка метод recordDelete не вызывается * @param int $recordID * @return void */ public function recordBeforeDelete($recordID) { if (empty($recordID)) { return; } $this->setRecordID($recordID); $this->applyWhen(); foreach ($this->fields as $v) { /** @var Input $field */ $field = $v['field']; if (method_exists($field, 'recordBeforeDelete')) { $field->recordBeforeDelete($recordID); } } } /** * Событие удаления записи * @param int $recordID * @return void */ public function recordDelete($recordID) { if (empty($recordID)) { return; } $this->setRecordID($recordID); $this->applyWhen(); foreach ($this->fields as $v) { /** @var Input $field */ $field = $v['field']; if (method_exists($field, 'recordDelete')) { $field->recordDelete($recordID); } } } /** * Установка названия таба формы, если в списке используется несколько форм * @param string|callable $title * @return static */ public function setTitle($title) { $this->title = $title; $this->wrapper()->title($title); return $this; } /** * Установка названия JS объекта списка, и урл для открытия формы для возможности управления историей ($_GET['gtab']) * @param string $jsObject имя JS объекта (false - отключить) * @param string $url * @return static */ public function jsHistory($jsObject, $url = '') { if ($this->_history !== false) { if ($jsObject == false) { $this->_history = false; } else { $this->_history = ['object' => $jsObject, 'url' => $url]; } } return $this; } /** * Выбор поля по имени или "таб/имя поля" * @param string|int $fieldName имя поля или fieldID * @return int ID поля */ public function _fieldSelector($fieldName) { $fieldID = false; do { if (empty($fieldName)) { break; } if (is_int($fieldName) && isset($this->fields[$fieldName])) { $fieldID = $fieldName; break; } $tabActive = $this->tabActive; $fieldName = explode('/', $fieldName); if (count($fieldName) == 2) { # "таб/имя поля" foreach ($this->tabs as $k => $v) { if ($v['name'] == $fieldName[0]) { $tabActive = $k; break; } } $name = $fieldName[1]; } else { $name = $fieldName[0]; } if (empty($name)) { break; } # название поля в активном табе foreach ($this->fields as $k => $v) { if ($v['tab'] != $tabActive) { continue; } /** @var Input $field */ $field = $v['field']; if ($field->name() == $name) { $fieldID = $k; break 2; } } # если не нашли и не указан таб, ищем во всех табах if (count($fieldName) == 1) { foreach ($this->fields as $k => $v) { /** @var Input $field */ $field = $v['field']; if ($field->name() == $name) { $fieldID = $k; break 2; } } } } while (false); return $fieldID; } /** * Сделать поле активным, выбор поля по имени или "таб/имя поля" * @param string|int $fieldName имя поля или fieldID * @param string|int $fieldInUnion имя поля или fieldID внутри группы если $fieldName - объединение полей * @return static */ public function _fieldActivate($fieldName, $fieldInUnion = 0) { $fieldID = $this->_fieldSelector($fieldName); if (isset($this->fields[$fieldID])) { # сделаем поле активным $field = $this->fields[$fieldID]['field']; $this->fieldActive = $fieldID; $this->tabActive = $this->fields[$fieldID]['tab']; $this->unionActive = false; if ($field instanceof Fields\Union) { $this->unionActive = $fieldID; if ($fieldInUnion) { $field->field($fieldInUnion); } } } return $this; } /** * Получения объекта поля по имени или "таб/имя поля" * @param string|int $fieldName имя поля или fieldID * @return Input|bool */ public function field($fieldName) { $fieldID = $this->_fieldSelector($fieldName); if (isset($this->fields[$fieldID])) { return $this->fields[$fieldID]['field']; } return false; } /** * Получения объекта активного поля * @param bool $inUnion true для объединения полей, возвращаем активное поле внутри объединения * @return Input|bool */ public function _activeField($inUnion = true) { do { if ($inUnion && $this->unionActive && isset($this->fields[$this->unionActive])) { /** @var Fields\Union */ $union = $this->fields[$this->unionActive]['field']; if ($union instanceof Fields\Union) { $result = $union->_activeField(); if ($result !== false) { return $result; } } } if (! isset($this->fields[$this->fieldActive])) { break; } /** @var Input $field */ return $this->fields[$this->fieldActive]['field']; } while (false); return false; } /** * Получение активного поля * @param bool $name * @return mixed */ public function _getField($name = false) { if ($name) { return $this->field($name); } return $this->_activeField(); } /** * Выполнить callback, если условие выполняется * @param bool|callable $condition условие или функция ($form) возвращающая bool * @param callable $action действие ($form), выполняется если $condition === true * @param array $opts параметры * @return static */ public function when($condition, callable $action, array $opts = []) { $opts['condition'] = $condition; $opts['action'] = $action; $this->when[] = $opts; return $this; } /** * Выполнить callback, если добавляется новая запись * @param callable $action действие ($form) * @param array $opts параметры * @return static */ public function whenNew(callable $action, array $opts = []) { $this->when(function () { $id = $this->recordID(); return empty($id); }, $action, $opts); return $this; } /** * Выполнить callback, если запись редактируется * @param callable $action действие ($form) * @param array $opts параметры * @return static */ public function whenEdit(callable $action, array $opts = []) { $this->when(function () { $id = $this->recordID(); return ! empty($id); }, $action, $opts); return $this; } /** * Проверить when-условия и выполнить callback */ protected function applyWhen() { if ($this->whenApplied) { return; } $this->whenApplied = true; foreach ($this->when as $v) { if (! isset($v['condition'])) { continue; } if (! is_callable($v['action'])) { continue; } if (is_callable($v['condition'])) { $condition = call_user_func($v['condition'], $this); } else { $condition = $v['condition']; } if ($condition !== true) { continue; } $this->response(call_user_func($v['action'], $this)); } } /** * Определить количество элементов формы * @param string $name * @return array|int */ public function count($name = '') { $result = [ 'fields' => count($this->fields), 'tabs' => count($this->tabs), 'buttons' => count($this->buttons), ]; if (! empty($name)) { return $result[$name] ?? 0; } return $result; } # === добавление табов и полей === /** * Добавление или выбор таба * @param string $name имя таба * @param string $title заголовок * @param array $opts доп.параметры * @return static */ public function tab($name, $title = '', array $opts = []) { do { foreach ($this->tabs as $k => $v) { if ($v['name'] == $name) { $this->tabActive = $k; $this->unionActive = false; break 2; } } $id = sizeof($this->tabs) + 1; $opts = $this->defaults($opts, [ 'id' => $id, 'name' => $name, 'title' => $title, 'hidden' => false, 'table' => true, 'stretch' => false, 'url' => false, 'buttons' => true, 'ajax' => false, 'counter' => null, 'style' => 'default', 'icon' => '', 'collapsed' => false, 'default' => false, 'priority' => false, 'wrapperA' => null, # static function($html, $o) { $o['attr']['foo'] = 'bar'; return 'foo'.$html;} 'wrapperLI' => null, # static function($html, $o) { $o['attr']['foo'] = 'bar'; return 'foo'.$html;} ]); $this->tabs[$id] = $opts; $this->unionActive = false; $this->tabActive = $id; } while (false); return $this; } /** * Проверка существования активного таба и возможности добавления в него не специальных полей * @return void */ protected function checkActiveTab() { if (! isset($this->tabs[ $this->tabActive ])) { # поля могут добавлятся только внутри таба if (empty($this->tabs)) { # если нет табов создадим один скрытый таб $this->tab('h', 'noname', ['hidden' => true]); } else { foreach ($this->tabs as $k => $v) { $this->tabActive = $k; break; } } } # проверим возможность добавления полей в таб if (! $this->tabs[ $this->tabActive ]['table']) { foreach ($this->tabs as $k => $v) { if ($v['table']) { $this->tabActive = $k; break; } } } if (! $this->tabs[ $this->tabActive ]['table']) { $this->tab('h', ' '); } } /** * Добавление SEO таба * @param string $group группа страницы * @param string $key имя страницы * @param string $title заголовок таба * @param array $opts доп. параметры * string 'prefix' - префикс SEO таба, если используется более одного SEO таба в форме * @return static */ public function tabSEO(string $group, string $key, $title = 'SEO', array $opts = []) { $opts = $this->defaults($opts, [ 'prefix' => '', ]); do { $tabActive = $this->tabActive; $this->tab('seo_' . $opts['prefix'] . $group . $key, $title); SEO::templateForm($group, $key, $this, $opts); $this->tabActive = $tabActive; } while (false); return $this; } /** * Добавление таба-ссылки на страницу * @param string $name имя таба * @param string $title заголовок * @param string $url ссылка на страницу * @param array $opts доп.параметры * @return static */ public function tabURL($name, $title, $url, array $opts = []) { $tabActive = $this->tabActive; $opts['table'] = false; $opts['url'] = $url; $this->tab($name, $title, $opts); $this->tabActive = $tabActive; return $this; } /** * Добавление таба с ajax загрузкой контента по Url * @param string $name имя таба * @param string $title заголовок * @param string $url ссылка для получения url, в ответе должен быть ключ html * @param array $opts доп.параметры * @return static */ public function tabAjax($name, $title, $url, array $opts = []) { $tabActive = $this->tabActive; $opts['table'] = false; $opts['ajax'] = $url; $opts['buttons'] = false; $this->tab($name, $title, $opts); $this->tabActive = $tabActive; return $this; } /** * Установка обработчика события добавления поля * @param callable $callback функция обработчика, аргументы (Fields\Input $field) * @return int index добавленного обработчика (для возможности удаления) */ public function onFieldAdd(callable $callback) { $this->_onFieldAdd[] = $callback; return array_key_last($this->_onFieldAdd); } /** * Удаление обработчика события добавления поля * @param int $index index добавленного ранее обработчика * @return static */ public function removeOnFieldAdd(int $index) { unset($this->_onFieldAdd[$index]); return $this; } /** * Добавление поля * @param Fields\Input $field * @param bool $checkTab выполнить проверку на таб * @return void */ protected function fieldAdd($field, $checkTab = true) { $field->setProperties(['form' => $this]); $field->init(); # add field handlers: before foreach($this->_onFieldAdd as $callback) { $callback($field, false/* before */); } if ($this->unionActive && isset($this->fields[$this->unionActive]) && $field->inUnionAllowed()) { /** @var Fields\Union $union */ $union = $this->fields[$this->unionActive]['field']; $union->fieldAdd($field); # add field handlers: after foreach($this->_onFieldAdd as $callback) { $callback($field, true/* after */); } return; } $checkTab && $this->checkActiveTab(); $id = sizeof($this->fields) + 1; $field->setID($id); $max = 0; foreach ($this->fields as $v) { if ($max < $v['priority']) { $max = $v['priority']; } } $this->fields[$id] = [ 'id' => $id, 'field' => $field, 'priority' => $max + 10, 'tab' => $this->tabActive, ]; $this->fieldActive = $id; # add field handlers: after foreach($this->_onFieldAdd as $callback) { $callback($field, true/* after */); } } /** * Дополнение дефолтного значения $default для lang инпутов text, textarea и т.д. * @param string|array $value значение * @param bool $lang использовать мультиязычность * true - использовать - вернет массив со всеми языками * false - не использовать - вернет строку на default языке или для первого языка * @return array|string */ public function defaultLang($value = '', $lang = false) { if ($lang) { return Lang::fill([], $value); } else { if (is_array($value)) { return Lang::value($value); } return $value; } } /** * Сделать поле активным, выбор поля по имени или "таб/имя поля" alias для функции _fieldActivate * @param string|int $fieldName имя поля или fieldID * @return static */ public function to($fieldName) { return $this->_fieldActivate($fieldName); } /** * Добавление поля input text * @param string $name имя * @param string $title заголовок * @param array|string $default значение по умолчанию * @param bool $lang использовать мультиязычность * @param array $opts доп.параметры * @return static */ public function text($name, $title = '', $default = [], $lang = true, array $opts = []) { if (empty($default) && $lang === static::LANG_NONE) { $default = ''; } $opts['title'] = $title; $opts['default'] = $this->defaultLang($default, $lang); $opts['lang'] = $lang; if (! isset($opts['stretch'])) { $opts['stretch'] = true; } $opts['clean'] = ($lang === static::LANG_NONE ? TYPE_STR : TYPE_ARRAY_STR); $f = new Fields\Text($name, $opts); $this->fieldAdd($f); return $this; } /** * Получение активного поля типа Text * @param string|bool $name имя поля false - активное поле * @return Fields\Text */ public function getText($name = false) { return $this->_getField($name); } /** * Добавление поля ввода многострочного текста