name = $name; } $this->setProperties($properties); } /** * Инициализация поля * @return bool */ public function init() { if (! parent::init()) { return false; } $default = $this->app->corePath('tpl/admin/' . $this->app->adminTheme() . '/form'); if (empty($this->templateDir)) { $this->setTemplateDir($default); } if (empty($this->wrapperTemplateDir)) { $this->wrapperTemplateDir = $default; } $this->ajaxAction = 'action_'.$this->name; return true; } /** * Установка свойств * @param array $properties * @return static */ public function setProperties(array $properties) { foreach ($properties as $k => $v) { if (property_exists($this, $k)) { $this->$k = $v; } } return $this; } /** * Получение значения свойства * @param $name * @return mixed */ public function getProperty($name) { if (property_exists($this, $name)) { return $this->$name; } return null; } /** * Получение значения ID поля * @return int */ public function id() { return $this->id; } /** * Установим ID поля * @param $id */ public function setID($id) { $this->id = $id; } /** * Получение значения имени поля * @return string */ public function name() { return $this->name; } /** * Завершение определения свойств поля в шаблоне формы (для постороения chain -> -> ...) * @return Form */ public function end() { return $this->form; } /** * Список полей при построении запроса к БД * @return array */ public function fieldsList() { if (is_callable($this->onFieldsList)) { return call_user_func($this->onFieldsList, $this); } return [$this->name()]; } /** * Определение функции для списка полей при построении запроса к БД * @param callable $callback функция * @return static */ public function onFieldsList(callable $callback) { $this->onFieldsList = $callback; return $this; } /** * Указываем на осутствие столбцов соответствующих данному полю при построении запроса к БД * @return static */ public function noFieldsList() { $this->onFieldsList = function(){ return[]; }; return $this; } /** * Получение значения тип для очистки * @return int|array */ public function clean() { return $this->clean; } /** * Получение значения заголовка * @return string */ public function title() { return $this->title . ( ! empty($this->title) ? $this->form->fieldsTitleSuffix : ''); } /** * Возвращения ID, если поле находится внутри объединения полей иначе false * @return bool|int */ public function unionID() { if ($this->union) { return $this->union->id(); } return false; } /** * Установить поле внутрь объединения полей * @param $union */ public function setUnion($union) { if ($union instanceof Union) { $this->union = $union; } } /** * Получить значение по умолчанию * @param bool $default * @return bool */ public function getDefault($default = null) { if (isset($this->onDefault['callable']) && is_callable($this->onDefault['callable'])) { return call_user_func($this->onDefault['callable'], $this); } if (is_null($this->default)) return $default; return $this->default; } /** * Возможность добавления поля в объединение полей * @return bool */ public function inUnionAllowed() { return $this->isUnionAllowed; } /** * Проверить является ли поле обязательным для заполнения * @return bool */ public function isRequired() { return ! empty($this->required); } /** * Установить или вернуть параметры для обязательного заполнения * @param array|null * @return array */ public function required($data = null) { if (! is_null($data) && isset($data['class'])) { if ($this->unionID()) { $data['class'] .= '-' . $this->id(); } $this->required = $data; $this->classAdd($data['class']); } return $this->required; } /** * Необходимость растягивания элемента на всю ширину * @param null|bool|string $value null - вернуть текущее значение, иначе установить ('', 'sm', 'md', 'lg', '100', 'mini') * @return static|string текущее значение или $this для chain */ public function stretch($value = null) { if (is_null($value)) { return $this->stretch; } elseif ($value === false) { $value = empty($this->stretch) ? 'lg' : $this->stretch; } elseif ($value === true) { $value = '_'; } $this->stretch = $value; return $this; } /** * Установка класс для растягивания инпута * @param array $attr атрибуты * @param string $default класс по умолчанию * @return $this */ public function stretchClass(array & $attr, string $default = '') { $data = [ 'attr' => &$attr, 'stretch' => $this->stretch, 'default' => $default, ]; $this->render($data, 'stretch', $this->wrapperTemplateDir); return $this; } /** * Указываем правило валидации * TYPE_UINT, 'required', ... * @param int|string|Rule|Closure $rule * @param array $opts */ public function rule($rule, $opts = []) { if (is_numeric($rule)) { $this->clean = $rule; } else if (! empty($rule)) { $rule = Rule::factory($rule, $opts); if ($rule !== false) { $this->rules[] = $rule; } } } /** * Проверка значения поля * @param array $data ['value' => &$value, 'index' => 0], 'value' - ссылка на значение поля, 'index' - индекс поля (если поле внутри группы полей) * @return bool true - значение корректно, false - значение некорректно, пропустить (не сохраняется в БД) */ public function validate($data) { foreach ($this->rules as $rule) { if (! $rule->check($data['value'])) { $this->errors->set($rule->getMessage($this->title()), $this->name()); return false; } } if (isset($this->onValidate['callable']) && is_callable($this->onValidate['callable'])) { $result = call_user_func($this->onValidate['callable'], $data, $this); if (! $result) return false; } if ($this->isRequired()) { if (empty($data['value'])) { $inputName = false; if ($this->unionID()) { $prefix = $this->form->_inputNameGenerator($this->unionID()); if ($this->group && isset($data['index'])) { $prefix .= '[' . $data['index'] . ']'; } $inputName = $this->union->_inputNameGenerator($this->id(), $prefix); } else { $inputName = $this->form->_inputNameGenerator($this->id()); } $this->errors->set($this->required['message'], $inputName); return false; } } return true; } /** * Проверка значения или выполнение действий после сохранения (точно есть recordID) * @param bool $isInserted - была добавлена новая запись false - редактирование существующей * @param array $data ['saved' => &$saved, 'result' => &$result], 'saved' - сохраненные данные, 'result' - данные для обновления В БД */ public function validateAfter($isInserted, $data) { } /** * Установка значения поля при загрузке данных из БД * @param mixed $value */ public function setValue($value) { if (isset($this->onValue['callable']) && is_callable($this->onValue['callable'])) { $value = call_user_func($this->onValue['callable'], $value, $this); } $this->value = $value; } /** * Получение значения поля * @param bool $lang только для указанного языка, если применимо * @return mixed */ public function value($lang = false) { if (is_null($this->value)) { $data = $this->getDefault(); } else { $data = $this->value; } if ($lang && $this->lang !== false) { if (isset($data[$lang])) { $data = $data[$lang]; } else if (is_array($data)) { $data = reset($data); } } return $data; } /** * Выполнение события перед началом отрисовки поля * @param array $opt */ public function beforeView(array &$opt) { $html = ''; static::obCallable($html, $this->beforeView, function($callable) use (&$opt) { return call_user_func($callable, ['data' => &$opt], $this); }); } /** * Генерация основного контента поля * @param array $data @ref данные * string 'name' - название поля, * mixed 'wrapper' - данные для врапера, если установить $data['wrapper'] = ['colspan' => 2]; - врапер объединит строку и скроет title * @return string HTML */ public function view(array &$data = []) { return ''; } /** * Получение настроек мультиязывности * @return bool */ public function lang() { return $this->lang; } /** * Установка HTML атрибута * @param string $key ключ атрибута * @param mixed $value значение * @return $this */ public function attr($key, $value) { do { if (isset($this->attr[$key])) { $values = $this->attr[$key]; if (is_array($values)) { if (in_array($value, $values)) break; } else { if ($this->attr[$key] == $value) break; } } static::attrAdd($this->attr, $key, $value); } while(false); return $this; } /** * Добавление класса для инпута * @param string|array $name название класса или классов * @return Input */ public function classAdd($name) { $this->attr('class', $name); return $this; } /** * Удаление класса для инпута * @param string|array $name название класса или классов * @return Input */ public function classRemove($name) { static::attrRemove($this->attr, 'class', $name); return $this; } /** * Установка HTML атрибута для врапера * @param string $key ключ атрибута * @param mixed $value значение * @return $this */ public function wrapperAttr($key, $value) { do { if (isset($this->wrapper[$key])) { $values = $this->wrapper[$key]; if (is_array($values)) { if (in_array($value, $values)) { break; } } else { if ($this->wrapper[$key] == $value) { break; } } } static::attrAdd($this->wrapper, $key, $value); } while(false); return $this; } /** * Добавление класса для врапера инпута * @param string|array $name название класса или классов * @return Input */ public function wrapperClassAdd($name) { $this->wrapperAttr('class', $name); return $this; } /** * Удаление класса для врапера инпута * @param string|array $name название класса или классов * @return Input */ public function wrapperClassRemove($name) { static::attrRemove($this->wrapper, 'class', $name); return $this; } /** * Получение настроек соединения полей (размещение в одну строку) * @return array */ public function together() { return $this->together; } /** * Добавление к полю контента другого поля * @param string $content добавляемый контент * @param int $id ID поля * @param array $opts */ public function togetherAdd($content, $id, $opts = []) { if (! isset($this->together['content'])) { $this->together['content'] = []; } if (! isset($this->together['first'])) { $this->together['first'] = []; } if (! isset($this->together['hidden'])) { $this->together['hidden'] = []; } if (! empty($opts['first'])) { $this->together['first'][$id] = $content; } else if (! empty($opts['isHidden'])) { $this->together['hidden'][$id] = $content; } else { $this->together['content'][$id] = $content; } if (! isset($this->together['newLine'])) { $this->together['newLine'] = false; } $this->together['newLine'] |= $opts['newLine'] ?? false; } /** * Получение настроек соединения полей (общая граница) * @return array */ public function boundary() { return $this->boundary; } /** * Добавление к полю контента другого поля * @param string $content добавляемый контент * @param int $id ID поля */ public function boundaryAdd($content, $id) { if (! isset($this->boundary['content'])) { $this->boundary['content'] = []; } $this->boundary['content'][$id] = $content; } /** * Формирование HTML tr td врапера для поля, реализация функциональности toggleIf, together, boundary * @param string $content контент, который необходимо обвернуть * @param array $opt параметры [ * 'wrapper' => ['colspan' => 2, 'tr' => [], 'td' => []]; 'colspan' => 2 - объединить td, 'tr' и 'td' - атрибуты для tr и td * 'no_content' => true - не выводить контент поля * ] * @return string HTML */ public function wrapper($content, array &$opt = []) { if (! empty($opt['no_content'])) { return ''; } if (! isset($opt['wrapper'])) { $opt['wrapper'] = []; } if (! isset($opt['wrapper']['tr'])) { $opt['wrapper']['tr'] = $this->wrapper; } else { $opt['wrapper']['tr'] = array_merge($this->wrapper, $opt['wrapper']['tr']); } static::attrAdd($opt['wrapper']['tr'], 'class', 'j-field-block'); if (! isset($opt['wrapper']['td'])) { $opt['wrapper']['td'] = ['class' => 'row2']; } if (! empty($this->reloadIF['slave'])) { static::attrAdd($opt['wrapper']['tr'], 'class', 'j-reloadif-slave-' . $this->id()); $opt['wrapper']['reloadIf'] = [$this->form, '_reloadIfSlaveContent']; } if ($this->isTip()) { static::attrAdd($opt['wrapper']['tr'], 'class', 'hasTip'); } $this->toggleIFClasses($opt['wrapper']['tr']); # проверка на скрытое поле if (isset($this->isHidden['callable']) && is_callable($this->isHidden['callable']) && call_user_func($this->isHidden['callable'], $this)) { static::attrAdd($opt['wrapper']['tr'], 'class', 'displaynone'); } $opt['content'] = $content; $opt['together'] = & $this->together; # Together if (isset($this->together['target'])) { static::obCallable($content, $this->beforeRender, function($callable) use (&$content) { return call_user_func($callable, $content, $this); }); /** @var Input $target */ $target = $this->together['target']; $attr = $opt['wrapper']['tr']; if (isset($this->together['attr']) && is_array($this->together['attr'])) { foreach ($this->together['attr'] as $k => $v) { static::attrAdd($attr, $k, $v); } } $this->together['attr'] = $attr; $this->together['html'] = $content; if (empty($this->together['isHidden']) && ! empty($content)) { $content = $this->render($this->together, 'together.target', $this->wrapperTemplateDir); } $target->togetherAdd($content, $this->id(), $this->together); return ''; } if (! empty($this->together['content']) || ! empty($this->together['first'])) { $this->together['html'] = $content; $content = $this->render($this->together, 'together.result', $this->wrapperTemplateDir); } if (! empty($this->together['hidden'])) { $content .= join('', $this->together['hidden']); } $opt['title'] = $this->title(); $opt['isReqiured'] = $this->isRequired(); static::obCallable($content, $this->beforeRender, function($callable) use (&$content, & $opt) { return call_user_func($callable, $content, $this, ['opt' => &$opt]); }); $opt['content'] = $content; # Wrapper HTML $html = $this->render($opt, 'wrapper', $this->wrapperTemplateDir); static::obCallable($html, $this->afterRender, function($callable) use (&$html) { return call_user_func($callable, $html, $this); }); # Boundary if (isset($this->boundary['target'])) { /** @var Input $target */ $target = $this->boundary['target']; $target->boundaryAdd($html, $this->id()); return ''; } if (isset($this->boundary['title'])) { $data = $this->boundary; $data['current'] = $html; $data['wrapper'] = $opt['wrapper']; return $this->render($data, 'boundary', $this->wrapperTemplateDir); } return $html; } /** * Формирование подсказки для поля * @return string */ public function tip() { if (! $this->isTip()) { return ''; } $tip = $this->tip; $jsName = $this->form->jsObjectName('popover'); $attr = [ 'href' => 'javascript:', 'class' => 'descr disabled j-popover', ]; $content = $tip['content']; static::obCallable($content, $tip, function($callable) { return call_user_func($callable, $this); }); $tip['content'] = $content; foreach (['placement', 'trigger', 'content'] as $v) { if (! isset($tip[$v])) continue; $attr['data-'.$v] = $tip[$v]; } $data = [ 'attr' => $attr, 'jsName' => $jsName, 'noJsRender' => 1, ]; $html = $this->render($data, 'tip', $this->wrapperTemplateDir); $this->form->initionsJavascript($jsName, $this->jsRender()); return $html; } /** * Наличие подсказки у поля */ public function isTip() { return ! empty($this->tip); } /** * Добавление кода после основного контента поля * @return string */ public function htmlAfter() { $html = isset($this->htmlAfter['html']) ? $this->htmlAfter['html'] : ''; static::obCallable($html, $this->htmlAfter, function($callable) { return call_user_func($callable, $this); }); return $html; } /** * Формирования ajax урл для вызова функции ajax из javascript * @param string $action название события * @return string */ public function ajaxUrl($action = '') { $url = 'field_action&field_id='.$this->id();; if ($this->ajaxAction) { $url .= '&'.$this->ajaxAction.'='; } $url .= $action; return $this->unionID() ? $this->union->ajaxUrl() . $url : $this->form->ajaxUrl() . $url; } /** * Обработка ajax запросов * @return array|bool данные для ajax ответа */ public function ajax() { if (empty($this->ajaxHandlers)) { return []; } $act = $this->input->getpost($this->ajaxAction, TYPE_STR); if (isset($this->ajaxHandlers[$act])) { return call_user_func($this->ajaxHandlers[$act]); } return []; } /** * Установка обработчика ajax запроса * @param $action * @param callable|null $callable * @return static|callable */ public function ajaxHandler($action, callable $callable = null) { if (is_null($callable)) { return $this->ajaxHandlers[$action] ?? null; } $this->ajaxHandlers[$action] = $callable; return $this; } /** * Установка javascript обработчика, при изменении значения поля * @param array $opts параметры * @param callable $callback */ public function jsOnChange(callable $callback, array $opts = []) { $union = $this->unionID(); $class = 'j-on-change-'.$this->id().($union ? '-'.$union : ''); $this->classAdd($class); $opts['class'] = $class; $opts['callable'] = $callback; $this->jsOnChange[] = $opts; } /** * Формирование атрибутов функциональности toggleIF, другие поля могут скрыватся в зависимости от значения поля * @param int $toggle способ отображения static::TOGGLE_VISIBLE или static::TOGGLE_HIDDEN */ public function toggleIFmaster($toggle) { if (! in_array($toggle, [static::TOGGLE_HIDDEN, static::TOGGLE_VISIBLE])) { return; } $this->classAdd('j-toggle-if'); $id = $this->id(); $union = $this->unionID(); if ($union) { $id .= '_' . $union; if ($this->group) { $this->classAdd('j-toggle-gr'); } } $this->attr('data-' . ($toggle == static::TOGGLE_HIDDEN ? 'hiddenif' : 'visibleif'), $id); $this->toggleIF[static::TOGGLE_MASTER] = 1; } /** * Установить видимость поля в зависимости от значений другого поля * @param int $toggle способ отображения static::TOGGLE_VISIBLE или static::TOGGLE_HIDDEN * @param Input $master от значения какого поля зависит видимость * @param array $values масив значений */ public function toggleIFAdd($toggle, $master, $values) { if (! in_array($toggle, [static::TOGGLE_HIDDEN, static::TOGGLE_VISIBLE])) return; if (! isset($this->toggleIF[$toggle])) { $this->toggleIF[$toggle] = []; } $values = array_map(function ($v){ if (is_bool($v)) { $v = (int)$v; } return $v; }, $values); $this->toggleIF[$toggle][] = ['master' => $master, 'values' => $values]; } /** * Формирование списка классов для функциональности toggleIF * @param array $attr атрибуты элемента для которого сформировать необходимые классы */ public function toggleIFClasses(& $attr) { if (empty($this->toggleIF)) return; $classes = []; if (! empty($this->toggleIF[static::TOGGLE_VISIBLE])) { foreach ($this->toggleIF[static::TOGGLE_VISIBLE] as $v) { /** @var Input $master */ $master = $v['master']; $id = $master->id(); $union = $master->unionID(); if ($union) { $id .= '_' . $union; } $classes[] = 'j-visible-' . $id; foreach ($v['values'] as $vv) { $classes[] = 'j-visible-' . $id . '-' . $vv; } $value = $master->value(); if (! in_array($value, $v['values'])) { $classes[] = 'j-hide-' . $id; } } } if (! empty($this->toggleIF[static::TOGGLE_HIDDEN])) { foreach ($this->toggleIF[static::TOGGLE_HIDDEN] as $v) { /** @var Input $master */ $master = $v['master']; $id = $master->id(); $union = $master->unionID(); if ($union) { $id .= '_' . $union; } $classes[] = 'j-hidden-' . $id; foreach ($v['values'] as $vv) { $classes[] = 'j-hidden-' . $id . '-' . $vv; } $value = $master->value(); if (in_array($value, $v['values'])) { $classes[] = 'j-hide-' . $id; } } } if (! empty($classes)) { $classes[] = 'j-toggleif'; static::attrAdd($attr, 'class', array_unique($classes)); } } /** * Формирование css свойств для классов * @return string */ public function toggleIFcss() { if (! empty($this->toggleIF[static::TOGGLE_MASTER])) { $id = $this->id(); $union = $this->unionID(); if ($union) { $id .= '_' . $union; } return ' .j-hide-' . $id . '{ display: none;} '; } return ''; } /** * Выполнить перезагрузку контента поля $field в зависимости от значения * @param Input $slave * @param callable $condition * @param array $opts */ public function reloadIfMaster($slave, callable $condition, array $opts = []) { $id = $this->id(); $this->classAdd('j-reloadif-master-' . $id); $opts['slave'] = $slave; $opts['condition'] = $condition; if (empty($opts['mode']) || ! in_array($opts['mode'], ['reload', 'present', 'absent'])) { $opts['mode'] = 'reload'; } $this->reloadIF['master'][] = $opts; } /** * @param Input $master */ public function reloadIfSlave($master, callable $condition, array $opts = []) { $opts['master'] = $master; $opts['condition'] = $condition; if (empty($opts['mode']) || ! in_array($opts['mode'], ['reload', 'present', 'absent'])) { $opts['mode'] = 'reload'; } $this->reloadIF['slave'][] = $opts; } # === вспомогательные методы === /** * Преобразования php переменной в json объект. php closure функции выполняются в момент преобразования и в json обект записывается результат * @param mixed $a переменная * @param bool $noNumQuotes * @return string */ public static function php2js($a = false, $noNumQuotes = true, $nativeJSON = false) { if ($nativeJSON) { $options = JSON_UNESCAPED_UNICODE; if ($noNumQuotes) { $options += JSON_NUMERIC_CHECK; } return json_encode($a, $options); } if (is_null($a)) { return 'null'; } if ($a === false) { return 'false'; } if ($a === true) { return 'true'; } if (is_scalar($a)) { if (is_float($a)) { // Always use "." for floats. $a = str_replace(",", ".", strval($a)); } // All scalars are converted to strings to avoid indeterminism. // PHP's "1" and 1 are equal for all PHP operators, but // JS's "1" and 1 are not. So if we pass "1" or 1 from the PHP backend, // we should get the same result in the JS frontend (string). // Character replacements for JSON. static $jsonReplaces = array( array("\\", "/", "\n", "\t", "\r", "\b", "\f", '"', "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", "\x18", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f"), array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017', '\u0018', '\u001a', '\u001b', '\u001c', '\u001d', '\u001e', '\u001f') ); if ($noNumQuotes && is_int($a)) { return $a; } else { return '"' . str_replace($jsonReplaces[0], $jsonReplaces[1], $a) . '"'; } } /* Ключевое отличие от \bff\utils\func::php2js !!! */ if (is_callable($a)) { $result = ''; $handler = ['callable' => $a]; static::obCallable($result, $handler, function ($c) { return call_user_func($c); }, ['js' => true]); return $result; } $isList = true; for ($i = 0, reset($a); $i < count($a); $i++, next($a)) { if (key($a) !== $i) { $isList = false; break; } } $result = []; if ($isList) { foreach ($a as $v) { $result[] = static::php2js($v, $noNumQuotes, $nativeJSON); } return '[' . join(',', $result) . ']'; } else { foreach ($a as $k => $v) { $result[] = static::php2js($k, $noNumQuotes, $nativeJSON) . ': ' . static::php2js($v, $noNumQuotes, $nativeJSON); } return '{' . join(',', $result) . '}'; } } }