yf_html classHTML high-level controls collection.
| File | plugins/html/classes/yf_html.class.php |
|---|---|
| Use | _class('html') |
A site sets them in its config with $PROJECT_CONF['html']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$AUTO_ASSIGN_IDS |
true |
|
$B5_ITEM_OPTIONSbool |
false |
the B5 item keys (list_group() link / active / disabled / variant, navbar() sub, progress_bar() label / striped / animated, a carousel() slide interval) are read from the items; default false renders items as before, so DB rows with such columns change nothing ($PROJECT_CONF['html']['B5_ITEM_OPTIONS'], 4.0 flips). The widget options in $extra need no flag |
$BOXES_USE_STPL |
false |
|
$BS4_AS_BS3bool |
false |
css_framework bs4 and empty get the Bootstrap 3 markup of accordion(), breadcrumbs(), thumbnails() and pagination(); default false keeps their Bootstrap 2 markup (4.0 flips) |
$BS5_BTN_TOKENSarray |
['btn-default' => 'btn-outline-secondary', 'btn-mini' => 'btn-sm', 'btn-xs' => 'btn-sm'] |
Bootstrap 3 button class => Bootstrap 5 class, for the framework HTML _bs5_btn_html() converts (btn-default becomes the neutral colour only without another one) |
$BS5_CLASSESarray |
['CLASS_LABEL_RADIO' => ['radio', 'form-check'], 'CLASS_LABEL_RADIO_INLINE' => ['radio-inline', 'form-check-inline'], 'CLASS_LABEL_CHECKBOX' => ['checkbox', 'form-check'], 'CLASS_LABEL_CHECKBOX_INLINE' => ['checkbox-inline', 'form-check-inline'], 'CLASS_SELECT_BOX' => ['form-control', 'form-select'], 'CLASS_LINK_BTN' => ['btn btn-default btn-mini btn-xs', 'btn btn-outline-secondary btn-sm']] |
property => [bs3 default, Bootstrap 5 value]: _init() switches the ones a site did not change when css_framework is bs5 |
$CLASS_BS5_BTN_NEUTRALstring |
'btn-outline-secondary' |
the neutral button colour the bs5 class maps put where Bootstrap 3 had btn-default; _bs5_btn_class() drops it next to another colour |
$CLASS_BS5_CHECK_INPUTstring |
'form-check-input' |
class of radio / checkbox inputs under Bootstrap 5 |
$CLASS_INPUT |
'form-control' |
|
$CLASS_LABEL_BTN_CHECKBOX |
'btn btn-xs btn-primary' |
|
$CLASS_LABEL_BTN_RADIO |
'btn btn-xs btn-primary' |
|
$CLASS_LABEL_CHECKBOX |
'checkbox' |
|
$CLASS_LABEL_CHECKBOX_INLINE |
'checkbox-inline' |
|
$CLASS_LABEL_CHECKBOX_SELECTED |
'active' |
|
$CLASS_LABEL_RADIO |
'radio' |
|
$CLASS_LABEL_RADIO_INLINE |
'radio-inline' |
|
$CLASS_LABEL_RADIO_SELECTED |
'active' |
|
$CLASS_LINK_BTNstring |
'btn btn-default btn-mini btn-xs' |
a() links without a class |
$CLASS_SELECT_BOX |
'form-control' |
|
$CLASS_SELECT_OPTION_DEFAULT |
'opt-default' |
|
$SELECT_BOX_DEF_OPT_TPL |
'- %select% %name% -' |
|
$_bs5_switchedarray |
[] |
properties _init() switched to their bs5 value (only those go back when css_framework changes) |
$_chained_mode |
null |
|
$_country_names |
null |
|
$_ids |
[] |
|
$_ip_to_country |
[] |
|
$_is_bs3 |
null |
|
$_is_bs5 |
null |
|
$_params |
[] |
public function a()
Hyperlink container.
public function a()
{
$args = func_get_args();
$a = [];
// numerics params
if (isset($args[0]) && is_array($args[0])) {
$a = $args[0];
} elseif (isset($args[0])) {
$a['href'] = $args[0];
$a['title'] = $args[1] ?? null;
$a['icon'] = $args[2] ?? null;
$a['text'] = $args[3] ?? null;
$a['class_add'] = $args[4] ?? null;
$a['target'] = $args[5] ?? null;
$a['no_text'] = $args[6] ?? null;
// named params
} elseif (isset($args['link'])) {
$a = $args;
}
if (isset($args['extra']) && is_array($args['extra'])) {
foreach ($args['extra'] as $k => $v) {
$a[$k] = $v;
}
}
if ( ! isset($a['text'])) {
$a['text'] = ($a['title'] ?? null) ?: ($a['href'] ?? null);
}
if (($a['href'] ?? null) && substr($a['href'], 0, strlen('http')) !== 'http' && substr($a['href'], 0, strlen('//')) !== '//') {
$a['href'] = url($a['href']);
}
// conf can change after _init() (demo and test pages); follow it as form2 / table2 do, so they agree
(conf('css_framework') === 'bs5') !== (bool) $this->_is_bs5 && $this->_init();
if ( ! isset($a['class'])) {
$a['class'] = $this->CLASS_LINK_BTN;
}
if ($a['class_add'] ?? null) {
$a['class'] = $this->_is_bs5 ? $this->_bs5_btn_class($a['class'], $a['class_add']) : $a['class'] . ' ' . $a['class_add'];
}
if ( ! isset($a['target'])) {
$a['target'] = '_blank';
}
$icon = '';
if (isset($a['icon'])) {
$icon = [];
if ( ! is_array($a['icon'])) {
$a['icon'] = [$a['icon']];
}
foreach ((array) $a['icon'] as $i) {
$icon[] = '<i class="' . $i . '"></i>';
}
$icon = implode(' ', $icon);
}
return '<a' . _attrs($a, ['href', 'title', 'class', 'style', 'id', 'rel', 'target', 'disabled']) . '>' . $icon . (($a['no_text'] ?? null) ? '' : (strlen($a['text']) ? ($icon ? ' ' : '') . _prepare_html($a['text']) : '')) . '</a>';
}
public function accordion($data = [], $extra = [])
Accordion; bodies are developer-supplied HTML (as modal() bodies). B5 options: flush (bs5), always_open (no data-parent), theme (bs5).
public function accordion($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->accordion($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$parent_attr = ! empty($extra['always_open']) ? '' : ' data-parent="#' . ($extra['id'] ?? null) . '"';
$items = [];
$i = 0;
foreach ((array) $data as $k => $v) {
if ( ! is_array($v)) {
$content = $v;
$v = [];
} else {
$content = ($v['body'] ?? null);
}
$name = ($v['name'] ?? null) ?: $k;
$desc = ($v['desc'] ?? null) ?: ucfirst(str_replace('_', ' ', $name));
$id = ($v['id'] ?? null) ?: 'accordion_item_' . $k;
if (isset($extra['selected'])) {
$is_selected = (($extra['selected'] ?? null) == $k);
} else {
$is_selected = (++$i == 1);
}
$class_group = ($v['class_group'] ?? null) ?: ($extra['class_group'] ?? null);
$class_head = ($v['class_head'] ?? null) ?: ($extra['class_head'] ?? null);
$class_body = ($v['class_body'] ?? null) ?: ($extra['class_body'] ?? null);
$badge = ($v['badge'] ?? null) ? ' <sup class="badge badge-' . (($v['class_badge'] ?? null) ?: 'info') . '">' . ($v['badge'] ?? null) . '</sup>' : '';
$items[] =
'<div class="accordion-group panel panel-default' . ($class_group ? ' ' . $class_group : '') . '">
<div class="accordion-heading panel-heading' . ($class_head ? ' ' . $class_head : '') . '">
' . ($this->_is_bs3 ? '<h4 class="panel-title">' : '') . '
<a class="accordion-toggle" data-toggle="collapse"' . $parent_attr . ' href="#' . $id . '">' . $desc . $badge . '</a>
' . ($this->_is_bs3 ? '</h4>' : '') . '
</div>
<div id="' . $id . '" class="accordion-body panel-collapse collapse' . ($is_selected ? ' in' : '') . ($class_body ? ' ' . $class_body : '') . '">
<div class="accordion-inner panel-body">' . $content . '</div>
</div>
</div>';
}
return '<div class="panel-group accordion' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . ($extra['id'] ?? null) . '">' . implode(PHP_EOL, (array) $items) . '</div>';
}
public function alert($data = [], $extra = [])
public function alert($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->alert($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$close_btn = ( ! ($extra['no_close'] ?? null) && ! ($data['no_close'] ?? null)) ? '<button type="button" class="close" data-dismiss="alert">×</button>' : '';
$head = is_array($data) ? ($data['head'] ?? null) : '';
$body = is_array($data) ? ($data['body'] ?? null) : $data;
$alert_type = ($extra['alert'] ?? null) ?: (is_array($data) ? ($data['alert'] ?? null) : '');
if ( ! $alert_type) {
$alert_type = 'error';
}
return '
<div class="alert alert-block alert-' . $alert_type . ' fade in' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . ($extra['id'] ?? null) . '">
' . $close_btn . '
' . ($head ? '<h4 class="alert-heading">' . $head . '</h4>' : '') . '
' . $body . '
</div>';
}
public function badge($text = '', $extra = [])
Badge: bs5 `badge text-bg-<type>` (+ `rounded-pill`), bs4 `badge badge-<type>` (+ `badge-pill`), Bootstrap 3 `label label-<type>` (also for a pill: Bootstrap 3 badges have no colours, so it keeps the colour and loses the round corners). Options: type, pill, class_add, id, title, style, no_escape, data-*, attr.
public function badge($text = '', $extra = [])
{
if (is_array($text)) {
$extra = (array) $extra + $text;
$text = $extra['text'] ?? '';
}
$extra = (array) $extra;
$this->_sync_fw();
$type = $this->_color_type($extra['type'] ?? '', 'secondary');
$pill = ! empty($extra['pill']);
if ($this->_is_bs5) {
$class = 'badge text-bg-' . $type . ($pill ? ' rounded-pill' : '');
} elseif ($this->_is_bs4()) {
$class = 'badge badge-' . $type . ($pill ? ' badge-pill' : '');
} else {
$class = 'label label-' . $type;
}
$a = [
'class' => trim($class . ' ' . ($extra['class_add'] ?? '')),
'id' => $extra['id'] ?? '',
'title' => $extra['title'] ?? '',
'style' => $extra['style'] ?? '',
] + $this->_b4_pass($extra);
return '<span' . $this->_b4_attrs($a) . '>' . $this->_b4_text($text, $extra) . '</span>';
}
public function breadcrumbs($data = [], $extra = [])
public function btn_active($url = '', $active = 0, $extra = [])
Active toggler.
public function btn_active($url = '', $active = 0, $extra = [])
{
! $url && $url = url('/@object/active/@id');
$pairs = [
'class' => ['btn-warning', 'btn-success'],
'icon' => ['fa fa-ban', 'fa fa-check'],
'title' => [t('Disabled'), t('Active')],
];
$class = (($extra['class'] ?? null) ?: 'change_active') . (($extra['class_add'] ?? null) ? ' ' . $extra['class_add'] : '');
return ($extra['disabled'] ?? null) ? $pairs['class'][$active] : $this->a(
$url,
$pairs['title'][$active],
$pairs['icon'][$active],
'',
$class . ' active_short ' . $pairs['class'][$active]
);
}
public function button($text = '', $extra = [])
Button, or a link styled as one with `link` (a trusted URL; javascript: / data: / vbscript: become #). Options: type (primary, success... `link`), outline, size (sm / lg), icon (class of an <i>), link, target, rel, button_type (submit / reset), name, value, active, disabled, class (replaces the btn classes), class_add, id, title, style, no_escape, data-*, attr. Bootstrap 4: the Bootstrap 5 classes; Bootstrap 3: btn-default for outline and the neutral button, btn-xs for sm.
public function button_allow_deny_box($name, $values = [], $selected = null)
public function button_box($name, $values = [], $selected = '', $extra = [])
public function button_check_box($name, $values = [], $selected = '')
public function button_radio_box($name, $values = [], $selected = null, $extra = [])
public function button_split_box($name, $values = [], $selected = '', $extra = [])
public function button_yes_no_box($name, $values = [], $selected = null)
public function card($extra = [])
panel() under its Bootstrap 5 name: a card under bs5, a panel under Bootstrap 3 / 4.
public function card($extra = [])
{
return $this->panel($extra);
}
public function carousel($data = [], $extra = [])
Carousel of images (slide img, desc, alt; slide interval with B5_ITEM_OPTIONS). B5 options: fade, interval, pause, ride, touch, wrap, keyboard, theme (bs5: _carousel_attrs(); Bootstrap 3 / 4: _carousel_options_bs3()).
public function carousel($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->carousel($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$items = [];
$headers = [];
$i = 0;
$slide = 0;
foreach ((array) $data as $k => $v) {
if ( ! is_array($v)) {
$img_src = $v;
$v = [];
} else {
$img_src = ($v['img'] ?? null);
}
$desc = ($v['desc'] ?? null);
$alt = ($v['alt'] ?? null) ?: strip_tags((string) $desc);
$id = ($v['id'] ?? null) ?: 'carousel_item_' . $k;
if (isset($extra['selected'])) {
$is_active = (($extra['selected'] ?? null) == $k);
} else {
$is_active = (++$i == 1);
}
$class_head = ($v['class_head'] ?? null) ?: ($extra['class_head'] ?? null);
$class_body = ($v['class_body'] ?? null) ?: ($extra['class_body'] ?? null);
$headers[] = '<li data-target="#' . ($extra['id'] ?? null) . '" data-slide-to="' . $slide++ . '" class="' . ($is_active ? 'active' : '') . ($class_head ? ' ' . $class_head : '') . '"></li>';
$items[] =
'<div class="item' . ($is_active ? ' active' : '') . ($class_body ? ' ' . $class_body : '') . '">
<img src="' . $img_src . '" alt="' . $alt . '">
' . ($desc ? '<div class="carousel-caption">' . $desc . '</div>' : '') . '
</div>';
}
$controls = '
<a class="left carousel-control" href="#' . ($extra['id'] ?? null) . '" data-slide="prev"><span class="icon icon-chevron-left fa fa-chevron-left"></span></a>
<a class="right carousel-control" href="#' . ($extra['id'] ?? null) . '" data-slide="next"><span class="icon icon-chevron-right fa fa-chevron-right"></span></a>
';
[$carousel_add, $carousel_attrs] = $this->_carousel_options_bs3($extra);
return '<div id="' . ($extra['id'] ?? null) . '" class="carousel slide' . $carousel_add . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '"' . $carousel_attrs . '>
<ol class="carousel-indicators">' . implode(PHP_EOL, $headers) . '</ol>
<div class="carousel-inner">' . implode(PHP_EOL, $items) . '</div>
' . ( ! ($extra['no_controls'] ?? null) ? $controls : '') . '
</div>';
}
public function chained_wrapper($params = [])
Wrapper for template engine Example: return html()->dd_table(db()->get_2d('SELECT * FROM '.db('countries')));.
public function chained_wrapper($params = [])
{
$this->_chained_mode = true;
$this->_params = $params;
return $this;
}
public function check_box($name = '', $value = '', $selected = '', $add_str = '', $extra = [])
Simple check box.
public function check_box($name = '', $value = '', $selected = '', $add_str = '', $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
$name = ($extra['name'] ?? null);
}
if ( ! is_array($extra)) {
$extra = [];
}
$extra['name'] = strlen($name ?? '') ? $name : (strlen($extra['name'] ?? '') ? $extra['name'] : 'checkbox');
$extra['value'] = strlen($value ?? '') ? $value : (strlen($extra['value'] ?? '') ? $extra['value'] : '1');
$selected = isset($extra['selected']) ? $extra['selected'] : $selected;
if (isset($extra['checked'])) {
$selected = $extra['checked'];
}
$extra['checked'] = $selected ? 'checked' : null;
$extra['id'] = (($extra['force_id'] ?? null) ?: ($extra['id'] ?? null)) ?: ($this->AUTO_ASSIGN_IDS ? __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1) : null);
$extra['desc'] = isset($extra['desc']) ? $extra['desc'] : ucfirst(str_replace('_', '', $extra['name']));
if ($extra['no_desc'] ?? null) {
$extra['desc'] = '';
}
$extra['class'] = trim(($extra['class'] ?? null) . ' ' . ($extra['class_add'] ?? null));
if ($this->_is_bs5 && ! in_array($this->CLASS_BS5_CHECK_INPUT, preg_split('~\s+~', $extra['class']), true)) {
$extra['class'] = trim($extra['class'] . ' ' . $this->CLASS_BS5_CHECK_INPUT);
}
$add_str = ($extra['add_str'] ?? null) ? $extra['add_str'] : $add_str;
$translate = $extra['translate'] ?? false;
$label_extra = ($extra['label_extra'] ?? null);
$def_label_class = $this->CLASS_LABEL_CHECKBOX . ' ' . $this->CLASS_LABEL_CHECKBOX_INLINE;
$label_extra['class'] = isset($label_extra['class']) ? $label_extra['class'] : (isset($extra['class_label_checkbox']) ? $extra['class_label_checkbox'] : $def_label_class);
if ($extra['class_add_label_checkbox'] ?? null) {
$label_extra['class'] .= ' ' . $extra['class_add_label_checkbox'];
}
if ($selected) {
$label_extra['class'] .= ' ' . $this->CLASS_LABEL_CHECKBOX_SELECTED;
}
$body = [];
if ($extra['outer_label'] ?? null) {
$body[] = '<label class="outer-label">' . $extra['outer_label'] . '</label>';
}
$extra['type'] = 'checkbox';
if (($extra['switch'] ?? null) && $this->_is_bs5) {
$body[] = $this->_bs5()->switch_box($extra, $add_str, $translate);
return implode(PHP_EOL, $body);
}
$body[] = '<label' . _attrs($label_extra, ['id', 'class', 'style']) . '>'
. '<input' . _attrs($extra, ['type', 'name', 'id', 'value', 'checked', 'class', 'style', 'disabled', 'required']) . ($add_str ? ' ' . $add_str : '')
. '> <span>' . ($translate ? t($extra['desc']) : $extra['desc']) . '</span>' // Please do not remove whitespace before :)
. '</label>';
return implode(PHP_EOL, $body);
}
public function chosen_box($name, $values = [], $selected = '', $extra = [])
public function chosen_box($name, $values = [], $selected = '', $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
} else {
$extra['name'] = $name;
}
$extra['force_id'] = $extra['force_id'] ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
asset('chosen');
$js_options = (array) $extra['js_options'] + [
'disable_search_threshold' => 10,
'no_results_text' => t('No results match'),
'placeholder_text_multiple' => t('Select Some Options'),
'placeholder_text_single' => t('Select an Option'),
'search_contains' => true,
// put default js options here
];
jquery('$("#' . addslashes($extra['force_id']) . '").chosen(' . json_encode($js_options, JSON_PRETTY_PRINT) . ');');
$func = $extra['multiple'] ? 'multi_select' : 'select_box';
return $this->$func($extra, $values, $selected);
}
public function collapse($body = '', $extra = [])
A block shown / hidden by a button (Bootstrap collapse). $body is developer-supplied HTML, not escaped (also run request data in it through _escape_tpl_braces(): the page template parses the output again). Options: button (the toggle text, default "More"; false for no button, e.g. a toggle made with button()), button_extra (button() options), show, id, class_add. Bootstrap 3: the same with data-toggle and `in` (Bootstrap 4: `show`).
public function collapse($body = '', $extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
$id = fix_html_attr_id((string) (($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1)));
$show = ! empty($extra['show']);
$button = $extra['button'] ?? t('More');
$out = '';
if ($button !== false && strlen((string) $button)) {
$b = (array) ($extra['button_extra'] ?? []);
$b += $this->_is_bs5 ? ['data-bs-toggle' => 'collapse', 'data-bs-target' => '#' . $id] : ['data-toggle' => 'collapse', 'data-target' => '#' . $id];
$b['attr'] = (array) ($b['attr'] ?? []) + ['aria-expanded' => $show ? 'true' : 'false', 'aria-controls' => $id];
$out = $this->button($button, $b);
}
$class = 'collapse' . ($show ? (($this->_is_bs5 || $this->_is_bs4()) ? ' show' : ' in') : '') . ' ' . ($extra['class_add'] ?? '');
return $out . '<div' . $this->_b4_attrs(['class' => trim($class), 'id' => $id]) . '>' . $body . '</div>';
}
public function country()
Country container.
public function country()
{
asset('bfh-select');
$args = func_get_args();
$a = [];
// numerics params
if (isset($args[0]) && is_array($args[0])) {
$a = $args[0];
} elseif (isset($args[0])) {
$a['code'] = $args[0];
$a['link'] = $args[1];
$a['class'] = $args[2];
}
if (isset($args['extra']) && is_array($args['extra'])) {
foreach ($args['extra'] as $k => $v) {
$a[$k] = $v;
}
}
$code = strtoupper($a['code'] ?? '');
if ( ! $code) {
return false;
}
$name = _prepare_html($this->_get_country_name($code));
if ($a['link']) {
return a(str_replace('%code', $code, $a['link']), $code . ' | ' . $name, 'bfh-flag-' . $code, '', $a['class']);
}
return $this->icon('bfh-flag-' . $code);
}
public function date_box($selected = '', $years = '', $name_postfix = '', $add_str = '', $show_what = 'ymd', $show_text = 1, $translate = 1)
public function date_box($selected = '', $years = '', $name_postfix = '', $add_str = '', $show_what = 'ymd', $show_text = 1, $translate = 1)
{
return _class('html_datetime', 'classes/html/')->date_box($selected, $years, $name_postfix, $add_str, $show_what, $show_text, $translate);
}
public function date_box2($name, $selected = '', $years = '', $add_str = '', $show_what = 'ymd', $show_text = 1, $translate = 1)
public function date_box2($name, $selected = '', $years = '', $add_str = '', $show_what = 'ymd', $show_text = 1, $translate = 1)
{
return _class('html_datetime', 'classes/html/')->date_box2($name, $selected, $years, $add_str, $show_what, $show_text, $translate);
}
public function date_picker($name, $cur_date = '', $extra = [])
public function date_picker($name, $cur_date = '', $extra = [])
{
js('jquery-ui');
css('jquery-ui');
jquery('$(".datepicker").datepicker({ dateFormat: "yy-mm-dd" });');
css('//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.4/css/jquery.ui.datepicker.min.css');
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
// TODO: use input() unified control
return '<input type="text" name="' . $name . '" class="datepicker" value="' . $cur_date . '" style="width:80px" readonly="true" id="' . $extra['id'] . '" />';
}
public function datetime_box2($name, $selected = '', $years = '', $add_str = '', $show_what = 'ymd', $show_text = 1, $translate = 1)
public function datetime_box2($name, $selected = '', $years = '', $add_str = '', $show_what = 'ymd', $show_text = 1, $translate = 1)
{
return _class('html_datetime', 'classes/html/')->datetime_box2($name, $selected, $years, $add_str, $show_what, $show_text, $translate);
}
public function dd_table($replace = [], $field_types = [], $extra = [])
public function dd_table($replace = [], $field_types = [], $extra = [])
{
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$ts = null;
if (DEBUG_MODE) {
$ts = microtime(true);
}
$form = form($replace, [
'legend' => $replace['title'],
'no_form' => 1,
'dd_mode' => 1,
'dd_class' => 'span6 col-md-6',
]);
foreach ((array) $replace as $name => $val) {
$func = 'container';
$_extra = [
'desc' => $name,
'value' => $val,
];
$ft = $field_types[$name];
if (isset($ft)) {
if (is_array($ft)) {
if (isset($ft['func'])) {
$func = $ft['func'];
}
$_extra = (array) $ft + $_extra;
} else {
$func = $ft;
}
}
$_extra += (array) $extra;
// Callback to decide if we need to show this field or not
if (isset($_extra['display_func']) && is_callable($_extra['display_func'])) {
$_display_allowed = $_extra['display_func']($val, $_extra);
if ( ! $_display_allowed) {
continue;
}
}
if ($func) {
$form->$func($val, $_extra);
}
}
$legend = $extra['legend'] ? '<legend>' . _prepare_html(t($extra['legend'])) . '</legend>' : '';
$div_class = $extra['div_class'] ? $extra['div_class'] : 'span6 col-md-6';
if (DEBUG_MODE) {
debug('dd_table[]', [
'fields' => $replace,
'field_types' => $field_types,
'extra' => $extra,
'time' => round(microtime(true) - $ts, 5),
'trace' => main()->trace_string(),
]);
}
return '<div class="row-fluid" id="' . $extra['id'] . '">' . $legend . '<div class="' . $div_class . '">' . $form . '</div></div>';
}
public function div_box($name, $values = [], $selected = '', $extra = [])
public function div_box($name, $values = [], $selected = '', $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->div_box($name, $values, $selected, $extra);
}
if (is_array($name)) {
$extra = (array) $extra + $name;
$name = ($extra['name'] ?? null);
}
$desc = ($extra['desc'] ?? null) ? ($extra['desc'] ?? null) : ucfirst(str_replace('_', '', $name));
$values = isset($extra['values']) ? ($extra['values'] ?? null) : (array) $values; // Required
$translate = isset($extra['translate']) ? ($extra['translate'] ?? null) : 0;
if ($extra['no_translate'] ?? null) {
$translate = 0;
}
$selected = ($extra['selected'] ?? null) ?: $selected;
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ( ! $values) {
return false;
}
$selected = (string) $selected;
$items = [];
$selected_val = '';
foreach ((array) $values as $key => $cur_value) {
$_what_compare = $key;
$is_selected = $_what_compare == $selected;
$val = ($translate ? t($cur_value) : $cur_value);
$items[] = '<li class="dropdown' . ($is_selected ? ' active' : '') . '"><a data-value="' . $key . '"' . ($is_selected ? ' data-selected="selected"' : '') . '>' . $val . '</a></li>';
if ($is_selected) {
$selected_val = $val;
}
}
$body = [];
$body[] = '<li class="dropdown" style="list-style-type:none;" id="' . ($extra['id'] ?? null) . '">';
$body[] = '<a class="dropdown-toggle" data-toggle="dropdown">' . ($selected_val ?: $desc) . ' <span class="caret"></span></a>';
$body[] = '<ul class="dropdown-menu">';
foreach ($items as $v) {
$body[] = $v;
}
$body[] = '</ul>';
$body[] = '</li>';
return implode(PHP_EOL, $body);
}
public function dropdown($items = [], $extra = [])
Dropdown menu on a button. Options: text (toggle, escaped unless no_escape), icon, type / outline / size (button() colours), split (separate toggle; the main part links to `link`, a trusted URL, when given), direction (up, end, start, center, up-center), align (end: menu on the right), dark, auto_close (true, false, inside, outside), class (toggle classes), class_add, id, data-*, attr. Items: see _dropdown_menu_items(). Bootstrap 3 / 4: dropup for up, dropdown-menu-right, no end / start / dark (Bootstrap 4 items are .dropdown-item); a disabled link item has no href there.
public function dropdown($items = [], $extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
if ($this->_is_bs5) {
return $this->_bs5()->dropdown($items, $extra);
}
$bs4 = $this->_is_bs4();
$btn = ($extra['class'] ?? null) ?: $this->_btn_class($extra);
$icon = ($extra['icon'] ?? null) ? '<i class="' . _htmlchars(_escape_tpl_braces((string) $extra['icon'])) . '" aria-hidden="true"></i>' : '';
$text = $this->_b4_text($extra['text'] ?? '', $extra);
$text = $icon . ($icon && strlen($text) ? ' ' : '') . $text;
$toggle = ['type' => 'button', 'class' => $btn . ' dropdown-toggle', 'data-toggle' => 'dropdown', 'aria-haspopup' => 'true', 'aria-expanded' => 'false'];
// Bootstrap 4 draws the caret itself (.dropdown-toggle::after)
$caret = $bs4 ? '' : '<span class="caret"></span>';
if ( ! empty($extra['split'])) {
$link = (string) ($extra['link'] ?? '');
$body = strlen($link) ? '<a' . $this->_b4_attrs(['href' => $this->_safe_href($link), 'class' => $btn, 'role' => 'button']) . '>' . $text . '</a>' : '<button type="button" class="' . $btn . '">' . $text . '</button>';
$bs4 && $toggle['class'] .= ' dropdown-toggle-split';
$body .= '<button' . $this->_b4_attrs($toggle) . '>' . $caret . '<span class="sr-only">' . t('Toggle Dropdown') . '</span></button>';
} else {
$body = '<button' . $this->_b4_attrs($toggle) . '>' . $text . ($caret && strlen($text) ? ' ' : '') . $caret . '</button>';
}
$items_html = $this->_dropdown_items_bs3($items);
$wrap = ['class' => trim('btn-group' . (in_array($extra['direction'] ?? '', ['up', 'up-center'], true) ? ' dropup' : '') . ' ' . ($extra['class_add'] ?? '')), 'id' => $extra['id'] ?? ''] + $this->_b4_pass($extra);
$menu = 'dropdown-menu' . (($extra['align'] ?? '') === 'end' ? ' dropdown-menu-right' : '');
return '<div' . $this->_b4_attrs($wrap) . '>' . $body . '<ul class="' . $menu . '">' . implode('', $items_html) . '</ul></div>';
}
public function grid($data = [], $extra = [])
public function grid($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->grid($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$rows = [];
$ul_opened = false;
foreach ((array) $data as $id => $row) {
$items = [];
$row_col = count((array) $row) ? floor(12 / count((array) $row)) : 1;
if ($row_col < 1) {
$row_col = 1;
}
foreach ((array) $row as $rid => $item) {
// a plain string cell is the body, not an array of characters
$item = is_array($item) ? $item : [$item];
$body = ($item[0] ?? null) ?: ($item['body'] ?? null);
$col = ($item[1] ?? null) ?: ($item['col'] ?? null);
$class = ($item['class'] ?? null);
if ( ! $col) {
$col = $row_col;
}
$items[] = '<div class="span' . $col . ' col-md-' . $col . ($class ? ' ' . $class : '') . '">' . $body . '</div>';
}
$rows[] = '<div class="row-fluid show-grid">' . implode(PHP_EOL, $items) . '</div>';
}
return '<div class="grid' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . ($extra['id'] ?? null) . '">' . implode(PHP_EOL, (array) $rows) . '</div>';
}
public function icon()
Icon container.
public function icon()
{
$args = func_get_args();
$a = [];
// numerics params
if (isset($args[0]) && is_array($args[0])) {
$a = $a[0];
} elseif (isset($args[0])) {
$a['icon'] = $args[0];
$a['title'] = $args[1];
$a['text'] = $args[2];
$a['class'] = $args[3];
// named params
} elseif (isset($args['icon'])) {
$a = $args;
}
if (isset($args['extra']) && is_array($args['extra'])) {
foreach ($args['extra'] as $k => $v) {
$a[$k] = $v;
}
}
if ( ! isset($a['text'])) {
$a['text'] = $a['title'];
}
if ( ! isset($a['style'])) {
$a['style'] = 'padding-right:5px';
}
return '<span style="' . $a['style'] . '" title="' . _prepare_html($a['title']) . '"><i class="' . $a['icon'] . '"></i>' . (strlen($a['text'] ?? '') ? ' ' . _prepare_html($a['text']) : '') . '</span>';
}
public function image_select_box($name, $values = [], $selected = '', $extra = [])
public function image_select_box($name, $values = [], $selected = '', $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
} else {
$extra['name'] = $name;
}
$extra['force_id'] = $extra['force_id'] ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
asset('jquery-image-picker');
asset('jquery-imagesloaded');
asset('masonry');
$extra['class_add'] = trim($extra['class_add'] . ' image-picker masonry');
$js_options = (array) $extra['js_options'] + [
// put default js options here
];
jquery('
var select_box = $("#' . addslashes($extra['force_id']) . '")
select_box.imagepicker(' . json_encode($js_options, JSON_PRETTY_PRINT) . ');
var container = select_box.next("ul.thumbnails");
container.imagesLoaded(function(){
container.masonry({
itemSelector: "li",
});
});
');
$extra['option_callback'] = function ($key, $cur_value, $is_selected, $text, $extra) {
return '<option value="' . $key . '"' . ($is_selected ? ' selected="selected"' : '') . ' data-img-src="' . $cur_value . '">' . $text . '</option>';
};
$func = $extra['multiple'] ? 'multi_select' : 'select_box';
return $this->$func($extra, $values, $selected);
}
public function input($name = '', $value = '', $extra = [])
Simple input form control.
public function input($name = '', $value = '', $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
$name = $extra['name'];
}
if ( ! is_array($extra)) {
$extra = [];
}
$extra['name'] = ($extra['name'] ?? null) ?: ($name ?: 'text');
$extra['value'] = _escape_tpl_braces($extra['value'] ?: $value);
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$extra['desc'] = ($extra['desc'] ?? null) ?: ucfirst(str_replace('_', '', $extra['name']));
$extra['type'] = ($extra['type'] ?? null) ?: 'text';
$extra['placeholder'] = _escape_tpl_braces($extra['placeholder'] ? t($extra['placeholder']) : $extra['desc']);
$extra['title'] = _escape_tpl_braces($extra['title'] ?? null);
$extra['class'] = isset($extra['class']) ? $extra['class'] : $this->CLASS_INPUT;
if ($extra['class_add']) {
$extra['class'] = trim($extra['class'] . ' ' . $extra['class_add']);
}
$attrs_names = ['name', 'type', 'id', 'class', 'style', 'placeholder', 'value', 'data', 'size', 'maxlength', 'pattern', 'disabled', 'required', 'autocomplete', 'accept', 'target', 'autofocus', 'title', 'min', 'max', 'step', 'readonly'];
return '<input' . _attrs($extra, $attrs_names) . '>';
}
public function ip()
IP container.
public function ip()
{
asset('bfh-select');
$args = func_get_args();
$a = [];
// numerics params
if (isset($args[0]) && is_array($args[0])) {
$a = $args[0];
} elseif (isset($args[0])) {
$a['ip'] = $args[0];
}
if (isset($args['extra']) && is_array($args['extra'])) {
foreach ($args['extra'] as $k => $v) {
$a[$k] = $v;
}
}
$ip = $a['ip'];
$code = strtoupper($this->_get_ip_country($ip));
$name = _prepare_html($this->_get_country_name($code));
return a('http://www.infobyip.com/ip-' . urlencode($ip) . '.html', $ip . ' | ' . $code . ' | ' . $name, ($code ? 'bfh-flag-' . $code : ''), $ip, $a['class']);
}
public function jumbotron($data = [], $extra = [])
public function jumbotron($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->jumbotron($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
return '<div class="jumbotron' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . ($extra['id'] ?? null) . '"><h1>' . ($data['head'] ?? null) . '</h1>' . ($data['body'] ?? null) . '</div>';
}
public function li($data = [], $extra = [])
public function li($data = [], $extra = [])
{
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$items = [];
foreach ((array) $data as $v) {
if ( ! is_array($v)) {
$body = $v;
$v = [];
} else {
$body = ($v['body'] ?? null) ?: ($v['name'] ?? null);
}
if ( ! strlen((string) $body)) {
continue;
}
$class_item = ($v['class'] ?? null) ?: ($extra['class_item'] ?? null);
$badge = ($v['badge'] ?? null) ? ' <sup class="badge badge-' . (($v['class_badge'] ?? null) ?: 'info') . '">' . ($v['badge'] ?? null) . '</sup>' : '';
$items[] = '<li class="' . ($class_item ? ' ' . $class_item : '') . '"' . (($v['id'] ?? null) ? ' id="' . ($v['id'] ?? null) . '"' : '') . '>' . $badge . (($v['link'] ?? null) ? '<a href="' . ($v['link'] ?? null) . '">' . $body . '</a>' : $body) . '</li>';
if (is_array(($v['sub'] ?? null))) {
$items[] = $this->li(($v['sub'] ?? null), $extra);
continue;
}
}
return '<ul' . _attrs($extra, ['id', 'class', 'style']) . '>' . implode(PHP_EOL, (array) $items) . '</ul>';
}
public function li_tree($data = [], $extra = [])
public function li_tree($data = [], $extra = [])
{
return _class('html_tree', 'classes/html/')->li_tree($data, $extra);
}
public function list_box($name, $values = [], $selected = '', $extra = [])
public function list_box($name, $values = [], $selected = '', $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
$name = $extra['name'];
}
$desc = isset($extra['desc']) ? $extra['desc'] : ucfirst(str_replace('_', '', $name));
$values = isset($extra['values']) ? $extra['values'] : (array) $values; // Required
$translate = isset($extra['translate']) ? $extra['translate'] : 0;
if ($extra['no_translate'] ?? null) {
$translate = 0;
}
$selected = isset($extra['selected']) ? $extra['selected'] : $selected;
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ( ! $values) {
return false;
}
asset('bfh-select');
$extra['class'] = 'bfh-selectbox ' . ($extra['class_add'] ?? null);
$extra['data-name'] = $name;
$extra['data-value'] = _escape_tpl_braces((string) $selected);
$extra['data-filter'] = isset($extra['data-filter']) ? $extra['data-filter'] : (count((array) $values) > 10 ? 'true' : '');
$body = '<div' . _attrs($extra, ['id', 'class', 'style']) . '>';
foreach ((array) $values as $key => $cur_value) {
$body .= '<div data-value="' . $key . '">' . ($translate ? t($cur_value) : $cur_value) . '</div>' . PHP_EOL;
}
$body .= '</div>';
return $body;
}
public function list_group($data = [], $extra = [])
List group; item bodies are developer-supplied HTML (as modal() bodies). B5 options: numbered, flush (bs4 / bs5), theme (bs5), variant; item link / active / disabled / variant with B5_ITEM_OPTIONS (_list_group_item_state()).
public function list_group($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->list_group($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$has_links = $this->_list_group_has_links($data);
$bs4 = $this->_is_bs4();
$items = [];
foreach ((array) $data as $v) {
if ( ! is_array($v)) {
$body = $v;
$v = [];
} else {
$body = ($v['body'] ?? null);
}
$type = ($v['type'] ?? null) ?: ($extra['type'] ?? null);
$class_item = ($v['class_item'] ?? null) ?: ($extra['class_item'] ?? null);
$state = $this->_list_group_item_state($v, $extra, $bs4 ? 'bs4' : 'bs3');
if ( ! $has_links && $state === null) {
$items[] = '<li class="list-group-item' . ($class_item ? ' ' . $class_item : '') . '"><span class="badge">' . ($v['badge'] ?? null) . '</span> ' . $body . '</li>';
continue;
}
$state ??= ['link' => null, 'class' => '', 'attrs' => []];
$tag = $has_links ? ($state['link'] !== null ? 'a' : 'div') : 'li';
$a = ['href' => $state['link'] ?? '', 'class' => trim('list-group-item' . $state['class'] . ' ' . $class_item)] + $state['attrs'];
$items[] = '<' . $tag . $this->_b4_attrs($a) . '><span class="badge">' . ($v['badge'] ?? null) . '</span> ' . $body . '</' . $tag . '>';
}
$tag = $has_links ? 'div' : ( ! empty($extra['numbered']) ? 'ol' : 'ul');
$class = ($bs4 && ! empty($extra['flush']) ? ' list-group-flush' : '') . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '');
return '<' . $tag . ' class="list-group' . $class . '" id="' . ($extra['id'] ?? null) . '">' . implode(PHP_EOL, (array) $items) . '</' . $tag . '>';
}
public function media_objects($data = [], $extra = [])
public function media_objects($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->media_objects($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ($data) {
$data = $this->_recursive_sort_items($data);
}
$img_class = (($extra['img_class'] ?? null) ?: 'media-object') . (($extra['img_class_add'] ?? null) ? ' ' . ($extra['img_class_add'] ?? null) : '');
$keys = array_keys($data);
$keys_counter = array_flip($keys);
$items = [];
foreach ((array) $data as $id => $item) {
$next_id = ($keys[$keys_counter[$id] + 1] ?? null);
$next_item = $next_id ? ($data[$next_id] ?? null) : [];
$close_num_levels = 1;
if ($next_item) {
$close_num_levels = ($item['level'] ?? null) - ($next_item['level'] ?? null) + 1;
if ($close_num_levels < 0) {
$close_num_levels = 0;
}
} elseif (($item['level'] ?? null) > 0) {
$close_num_levels = ($item['level'] ?? null) + 1;
}
$items[] = '
<div class="media">
<a class="pull-left"' . (($item['link'] ?? null) ? ' href="' . ($item['link'] ?? null) . '"' : '') . '>'
. '<img class="' . $img_class . '" alt="' . ($item['alt'] ?? null) . '" src="' . ($item['img'] ?? null) . '"' . (($item['img_width'] ?? null) ? ' width="' . ($item['img_width'] ?? null) . '"' : '') . (($item['img_height'] ?? null) ? ' height="' . ($item['img_height'] ?? null) . '"' : '') . '></a>
<div class="media-body">
<h4 class="media-heading">'
. (($item['link'] ?? null) ? '<a href="' . ($item['link'] ?? null) . '">' : '') . ($item['head'] ?? null) . (($item['link'] ?? null) ? '</a>' : '')
. (($item['date'] ?? null) ? ' <small class="pull-right">' . _format_date(($item['date'] ?? null), ($extra['date_format'] ?? null) ?: 'full') . '</small>' : '')
. '</h4>'
. ($item['body'] ?? null) . '
';
if ($close_num_levels) {
$items[] = str_repeat(PHP_EOL . '</div></div>' . PHP_EOL, $close_num_levels);
}
}
return '<div class="media-objects' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . ($extra['id'] ?? null) . '">' . implode(PHP_EOL, (array) $items) . '</div>';
}
public function modal($extra = [])
Modal dialog: id, header, body, footer (developer-supplied HTML, not escaped: escape request data in them and run it through _escape_tpl_braces(), the page template parses the output again), show_close, inline, style. B5 options: size (sm, lg, xl), fullscreen (true or a breakpoint), centered, scrollable, static, fade, theme (bs5); open it with modal_button().
public function modal($extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->modal($extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$def_style = ($extra['inline'] ?? null) ? 'position: relative; top: auto; left: auto; right: auto; bottom: auto; margin: 0 auto 20px; z-index: 1; max-width: 100%; display: block; overflow-y: auto;' : '';
$extra['style'] = ($extra['style'] ?? null) ?: $def_style;
[$modal_add, $dialog_add, $modal_attrs] = $this->_modal_options_bs3($extra);
return '
<div class="modal' . $modal_add . '" style="' . ($extra['style'] ?? null) . '" id="' . ($extra['id'] ?? null) . '"' . $modal_attrs . '>
<div class="modal-dialog' . $dialog_add . '">
<div class="modal-content">
<div class="modal-header">'
. (($extra['show_close'] ?? null) ? '<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>' : '')
. (($extra['header'] ?? null) ? '<h3>' . ($extra['header'] ?? null) . '</h3>' : '')
. '</div>
<div class="modal-body">' . ($extra['body'] ?? null) . '</div>'
. (($extra['footer'] ?? null) ? '<div class="modal-footer">' . ($extra['footer'] ?? null) . '</div>' : '') . '
</div>
</div>
</div>';
}
public function modal_button($text = '', $id = '', $extra = [])
Button that opens the modal() $id; $extra are button() options.
public function multi_check_box($name, $values = [], $selected = [], $horizontal = true, $type = 2, $add_str = '', $translate = 0, $name_as_array = false, $extra = [])
Processing many checkboxes at one time.
public function multi_check_box($name, $values = [], $selected = [], $horizontal = true, $type = 2, $add_str = '', $translate = 0, $name_as_array = false, $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
$name = $extra['name'];
}
$values = isset($extra['values']) ? $extra['values'] : (array) $values; // Required
$translate = isset($extra['translate']) ? $extra['translate'] : 0;
if ($extra['no_translate'] ?? null) {
$translate = 0;
}
$selected = ($extra['selected'] ?? null);
$type = isset($extra['type']) ? $extra['type'] : ($type !== null ? $type : 2);
$horizontal = isset($extra['horizontal']) ? $extra['horizontal'] : $horizontal;
$name_as_array = isset($extra['name_as_array']) ? $extra['name_as_array'] : false;
$add_str = isset($extra['add_str']) ? $extra['add_str'] : '';
if ($extra['class'] ?? null) {
$add_str .= ' class="' . trim($extra['class'] . ' ' . $extra['class_add']) . '" ';
}
if ($extra['style'] ?? null) {
$add_str .= ' style="' . $extra['style'] . '" ';
}
if ( ! $values) {
return false;
}
if ( ! is_array($selected)) {
$selected = (string) $selected;
}
$body = [];
if ($extra['outer_label'] ?? null) {
$body[] = '<label class="outer-label">' . $extra['outer_label'] . '</label>';
}
foreach ((array) $values as $key => $value) {
$sel_text = '';
// Selected value could be an array
if (is_array($selected)) {
if ($type == 1) {
$sel_text = in_array($value, $selected) ? 'checked="checked"' : '';
} else {
$sel_text = isset($selected[$key]) ? 'checked="checked"' : '';
}
} elseif (strlen($selected)) {
$_what_compare = (string) ($type == 1 ? $value : $key);
$sel_text = $_what_compare == $selected ? 'checked="checked"' : '';
} else {
$sel_text = '';
}
$is_selected = strlen($sel_text) ? 1 : 0;
if ($name_as_array) {
$val_name = $name . '[' . $key . ']';
} else {
$val_name = $name . '_' . $key;
}
$label_extra = ($extra['label_extra'] ?? null);
$label_extra['class'] = isset($label_extra['class']) ? $label_extra['class'] : (isset($extra['class_label_checkbox']) ? $extra['class_label_checkbox'] : $this->CLASS_LABEL_CHECKBOX);
if ($horizontal) {
$label_extra['class'] .= ' ' . $this->CLASS_LABEL_CHECKBOX_INLINE;
}
if ($extra['class_add_label_checkbox'] ?? null) {
$label_extra['class'] .= ' ' . $extra['class_add_label_checkbox'];
}
if ($is_selected) {
$label_extra['class'] .= ' ' . $this->CLASS_LABEL_CHECKBOX_SELECTED;
}
$id = __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ($extra['no_desc'] ?? null) {
$desc = '';
} else {
$desc = ($translate ? t($value) : $value);
}
if ($this->BOXES_USE_STPL) {
$body[] = tpl()->parse('system/common/check_box_item', [
'name' => $val_name,
'value' => $key,
'desc' => $desc,
'selected' => $sel_text,
'add_str' => $add_str,
'label' => $translate ? t($value) : $value,
'id' => $id,
'extra' => $extra,
]);
} else {
$body[] = '<label' . _attrs($label_extra, ['id', 'class', 'style']) . '>'
. '<input type="checkbox"' . $this->_bs5_check_input_attr($add_str) . ' name="' . $val_name . '" id="' . $id . '" value="' . $key . '"'
. ($is_selected ? ' ' . $sel_text : '') . ($add_str ? ' ' . trim($add_str) : '')
. '> ' . '<span>' . $desc . '</span>' // Please do not remove whitespace :)
. '</label>';
}
}
return implode(PHP_EOL, $body);
}
public function multi_select($name, $values = [], $selected = '', $show_text = false, $type = 2, $add_str = '', $translate = 0, $level = 0, $disabled = false)
public function multi_select($name, $values = [], $selected = '', $show_text = false, $type = 2, $add_str = '', $translate = 0, $level = 0, $disabled = false)
{
$extra = [];
// Passing params as array
if (is_array($name)) {
$extra = $name;
$name = $extra['name'];
}
$values = isset($extra['values']) ? $extra['values'] : (array) $values; // Required
$translate = isset($extra['translate']) ? $extra['translate'] : $translate;
if ($extra['no_translate'] ?? null) {
$translate = 0;
}
$selected = isset($extra['selected']) ? $extra['selected'] : $selected;
$show_text = isset($extra['show_text']) ? $extra['show_text'] : ($show_text !== null ? $show_text : false);
$type = isset($extra['type']) ? $extra['type'] : ($type !== null ? $type : 2);
$level = isset($extra['level']) ? $extra['level'] : $level;
// (example: $add_str = 'size=6') disabled
$add_str = isset($extra['add_str']) ? $extra['add_str'] : $add_str;
$extra['class'] = isset($extra['class']) ? $extra['class'] : $this->CLASS_SELECT_BOX;
($extra['class_add'] ?? null) && $extra['class'] = trim(($extra['class'] ?? null) . ' ' . $extra['class_add']);
if ( ! $values) {
return false;
}
if (($extra['disabled'] ?? null) || $disabled) {
$extra['disabled'] = 'disabled';
}
if ( ! is_array($selected)) {
$selected = (string) $selected;
}
$body = [];
$id = '';
if ($level == 0) {
($extra['force_id'] ?? null) && $id = $extra['force_id'];
$id = $id ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ($this->AUTO_ASSIGN_IDS) {
$extra['id'] = $id;
}
if ($extra['outer_label'] ?? null) {
$body[] = '<label class="outer-label">' . $extra['outer_label'] . '</label>';
}
$extra['multiple'] = 'multiple';
$extra['name'] = $name ? $name . '[]' : '';
$body[] = '<select' . _attrs($extra, ['name', 'id', 'class', 'style', 'multiple', 'disabled', 'required']) . ($add_str ? ' ' . trim($add_str) : '') . '>';
}
if ($show_text && $level == 0) {
$def_opt_text = $show_text;
if ($show_text == 1) {
$def_opt_text = str_replace(['%name%', '%select%'], [t($name), t('Select')], $this->SELECT_BOX_DEF_OPT_TPL);
}
$body[] = '<option value="" class="' . $this->CLASS_SELECT_OPTION_DEFAULT . '">' . $def_opt_text . '</option>';
}
$self_func = __FUNCTION__;
foreach ((array) $values as $key => $value) {
if (is_array($value)) {
$body[] = '<optgroup label="' . $key . '" title="' . ($translate ? t($key) : $key) . '">';
$body[] = $this->$self_func($name, $value, $selected, $show_text, $type, $add_str, $translate, $level + 1);
$body[] = '</optgroup>';
} else {
// Selected value could be an array
if (is_array($selected)) {
if ($type == 1) {
$sel_text = in_array($key, $selected) ? 'selected="selected"' : '';
} else {
$sel_text = isset($selected[$key]) ? 'selected="selected"' : '';
}
} elseif (strlen($selected)) {
$_what_compare = (string) ($type == 1 ? $value : $key);
$sel_text = $_what_compare == $selected ? 'selected="selected"' : '';
} else {
$sel_text = '';
}
$body[] = '<option value="' . $key . '"' . ($sel_text ? ' ' . $sel_text : '') . '>' . ($translate ? t($value) : $value) . '</option>';
}
}
$body[] = $level == 0 ? '</select>' : '';
return implode(PHP_EOL, $body);
}
public function multi_select_box($name, $values = [], $selected = '', $show_text = false, $type = 2, $add_str = '', $translate = 0, $level = 0, $disabled = false)
Alias.
public function multi_select_box($name, $values = [], $selected = '', $show_text = false, $type = 2, $add_str = '', $translate = 0, $level = 0, $disabled = false)
{
return $this->multi_select($name, $values, $selected, $show_text, $type, $add_str, $translate, $level, $disabled);
}
public function offcanvas($extra = [])
Side panel that slides in (bs5 offcanvas); Bootstrap 3 / 4 render a modal() with the same id. $extra['body'] is developer-supplied HTML, not escaped (also run request data in it through _escape_tpl_braces(): the page template parses the output again); the title is escaped. Options: id, title, body, placement (start, end, top, bottom), backdrop (false, `static`), scroll, show, no_close, class_add, no_escape (title), data-*, attr. Open it with offcanvas_button().
public function offcanvas($extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
$extra['id'] = fix_html_attr_id((string) (($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1)));
if ($this->_is_bs5) {
return $this->_bs5()->offcanvas($extra);
}
return $this->modal([
'id' => $extra['id'],
'header' => $this->_b4_text($extra['title'] ?? '', $extra),
'body' => $extra['body'] ?? '',
'show_close' => empty($extra['no_close']),
]);
}
public function offcanvas_button($text = '', $id = '', $extra = [])
Button that opens the offcanvas() $id (Bootstrap 3: the modal); $extra are button() options.
public function pagination($data = [], $extra = [])
public function pagination($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->pagination($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$prev = $next = null;
if (isset($data['prev'])) {
$prev = ($data['prev'] ?? null);
unset($data['prev']);
}
if (isset($data['next'])) {
$next = ($data['next'] ?? null);
unset($data['next']);
}
$items = [];
// TODO: auto-detect current page and need of first. last
if ($prev || $this->_pagination_state('prev', $extra) === 'disabled') {
$items[] = $this->_pagination_item('prev', $prev, t('Prev'), $extra);
}
foreach ((array) $data as $page => $link) {
$items[] = $this->_pagination_item($page, $link, $page, $extra);
}
if ($next || $this->_pagination_state('next', $extra) === 'disabled') {
$items[] = $this->_pagination_item('next', $next, t('Next'), $extra);
}
[$size_class, $wrap_class] = $this->_pagination_classes($extra['size'] ?? '', $extra['align'] ?? '', $this->_is_bs3 ? 'bs3' : 'bs2');
$class = strlen($size_class) ? trim($size_class . ' ' . ($extra['class'] ?? '')) : ($extra['class'] ?? null);
if ($this->_is_bs3) {
return '<div' . (strlen($wrap_class) ? ' class="' . $wrap_class . '"' : '') . '><ul class="pagination' . ($class ? ' ' . $class : '') . '" id="' . ($extra['id'] ?? null) . '">' . implode(PHP_EOL, $items) . '</ul></div>';
}
return '<div class="pagination' . ($class ? ' ' . $class : '') . '" id="' . ($extra['id'] ?? null) . '"><ul>' . implode(PHP_EOL, $items) . '</ul></div>';
}
public function panel($extra = [])
Panel (bs5: card): title, body (developer-supplied HTML, as modal() bodies), class, class_add. B5 options: subtitle (escaped), footer (HTML), img_top / img_alt, border (colour), theme (bs5). Alias card().
public function panel($extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->panel($extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$border = $this->_color_type($extra['border'] ?? '', '');
$extra['class'] = ($extra['class'] ?? null) ?: 'panel panel-' . (in_array($border, ['primary', 'success', 'info', 'warning', 'danger'], true) ? $border : 'default');
$subtitle = strlen((string) ($extra['subtitle'] ?? '')) ? ' <small>' . $this->_b4_text($extra['subtitle'], $extra) . '</small>' : '';
$img = strlen((string) ($extra['img_top'] ?? '')) ? '<img' . $this->_b4_attrs(['src' => $this->_safe_href($extra['img_top']), 'class' => 'img-responsive']) . $this->_img_alt($extra) . '>' : '';
$footer = strlen((string) ($extra['footer'] ?? '')) ? '<div class="panel-footer">' . $extra['footer'] . '</div>' : '';
return
'<div class="' . implode(' ', array_filter([($extra['class'] ?? null), ($extra['class_add'] ?? null)])) . '" id="' . ($extra['id'] ?? null) . '">
<div class="panel-heading">
<h3 class="panel-title">' . ($extra['title'] ?? null) . $subtitle . '</h3>
</div>' . $img . '
<div class="panel-body">' . ($extra['body'] ?? null) . '</div>' . $footer . '
</div>';
}
public function phone_box($name = 'phone', $values = [], $selected = '', $extra = [])
public function phone_box($name = 'phone', $values = [], $selected = '', $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
} else {
$extra['name'] = $name;
}
if ( ! $extra['name']) {
$name = $extra['name'] = 'phone';
}
$extra['force_id'] = $extra['force_id'] ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ( ! $extra['id']) {
$extra['id'] = $extra['force_id'] . '_input';
}
asset('jquery-intl-tel-input');
$countries = [];
foreach ((array) main()->get_data('geo_countries') as $k => $data) {
$countries[] = strtolower($data['code']);
}
$preferred_countries = [
'ua',
'by',
];
$js_options = (array) $extra['js_options'] + [
'autoPlaceholder' => true,
'onlyCountries' => $countries,
'preferredCountries' => $preferred_countries,
// 'excludeCountries' => ["us"],
// 'separateDialCode' => true,
// 'allowDropdown' => false,
// 'autoHideDialCode' => false,
// 'dropdownContainer' => "body",
// 'initialCountry' => "auto",
// 'nationalMode' => false,
// 'numberType' => "MOBILE",
// 'utilsScript' => '//cdnjs.cloudflare.com/ajax/libs/intl-tel-input/8.5.2/js/utils.js',
];
jquery('
var input_selector = "#' . addslashes($extra['id']) . '";
var phone = $(input_selector);
var reset = function() {
phone.removeClass("phone-error");
phone.removeClass("phone-success");
};
phone.intlTelInput($.merge(
' . json_encode($js_options, JSON_PRETTY_PRINT) . '
, {
geoIpLookup : function(callback) {
$.get("//ipinfo.io", function() {}, "jsonp").always(function(resp) {
var countryCode = (resp && resp.country) ? resp.country : "";
callback(countryCode);
});
}
}
));
phone.blur(function() {
reset();
if ($.trim(phone.val())) {
if (phone.intlTelInput("isValidNumber")) {
phone.addClass("phone-success");
} else {
phone.addClass("phone-error");
}
}
});
// on keyup / change flag: reset
phone.on("keyup change", reset);
$(phone).closest("form").submit(function() {
phone.val(phone.intlTelInput("getNumber"));
});
');
return $this->input($extra + [
'maxlength' => 20,
'type' => 'tel',
// 'pattern' => '^[0-9\s\(\)-]{7,20}$',
]);
}
public function placeholder($extra = [])
Skeleton lines while content loads (bs5 placeholders). Options: lines (3), widths (columns of 12 per line, repeated; default [12, 8, 6]), glow (true, `wave`, false), size (xs / sm / lg), color, class_add, id. Bootstrap 3 / 4: an empty div.
public function placeholder($extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
if ($this->_is_bs5) {
return $this->_bs5()->placeholder($extra);
}
return '<div' . $this->_b4_attrs(['class' => trim($extra['class_add'] ?? ''), 'id' => $extra['id'] ?? '', 'aria-hidden' => 'true']) . '></div>';
}
public function progress_bar($data = [], $extra = [])
Progress bar (several values: stacked). B5 options: striped, animated, height, theme (bs5); with B5_ITEM_OPTIONS per bar label (escaped unless no_escape), striped, animated.
public function progress_bar($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->progress_bar($data, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$items = [];
foreach ((array) $data as $v) {
if ( ! is_array($v)) {
$val = $v;
$v = [];
} else {
$val = ($v['val'] ?? null);
}
$type = ($v['type'] ?? null) ?: ($extra['type'] ?? null);
$class_item = ($v['class_item'] ?? null) ?: ($extra['class_item'] ?? null);
[$striped, $animated, $label] = $this->_progress_bar_item($v, $extra);
$bar_add = ($striped || $animated) ? ' progress-bar-striped' : '';
$bar_add .= $animated ? ($this->_is_bs4() ? ' progress-bar-animated' : ' active') : '';
$items[] = '<div class="progress-bar bar bar-' . $type . ' progress-bar-' . $type . $bar_add . ($class_item ? ' ' . $class_item : '')
. '" style="width: ' . $val . '%;' . (($v['style'] ?? null) ? ' ' . ($v['style'] ?? null) : '') . '" role="progressbar">' . $label . '</div>';
}
$height = $this->_css_length($extra['height'] ?? '');
return '<div class="progress' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . ($extra['id'] ?? null) . '"'
. ($height !== '' ? ' style="height: ' . $height . '"' : '') . '>' . implode(PHP_EOL, (array) $items) . '</div>';
}
public function radio_box($name, $values = [], $selected = '', $horizontal = true, $type = 2, $add_str = '', $translate = 0)
public function radio_box($name, $values = [], $selected = '', $horizontal = true, $type = 2, $add_str = '', $translate = 0)
{
$extra = [];
if (is_array($name)) {
$extra = $name;
$name = ($extra['name'] ?? null);
}
$values = isset($extra['values']) ? ($extra['values'] ?? null) : (array) $values; // Required
$translate = isset($extra['translate']) ? ($extra['translate'] ?? null) : $translate;
if ($extra['no_translate'] ?? null) {
$translate = 0;
}
$selected = isset($extra['selected']) ? ($extra['selected'] ?? null) : $selected;
$type = isset($extra['type']) ? ($extra['type'] ?? null) : ($type !== null ? $type : 2);
$horizontal = isset($extra['horizontal']) ? ($extra['horizontal'] ?? null) : $horizontal;
$add_str = isset($extra['add_str']) ? ($extra['add_str'] ?? null) : $add_str;
if ($extra['class_add'] ?? null) {
$add_str .= ' class="' . trim(($extra['class'] ?? null) . ' ' . ($extra['class_add'] ?? null)) . '" ';
}
if ($extra['style'] ?? null) {
$add_str .= ' style="' . ($extra['style'] ?? null) . '" ';
}
if ( ! $values) {
return false;
}
$selected = (string) $selected;
$this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1;
$id_prefix = __FUNCTION__ . '_' . $this->_ids[__FUNCTION__];
($extra['force_id'] ?? null) && $id_prefix = ($extra['force_id'] ?? null);
$counter = 0;
$body = [];
if ($extra['outer_label'] ?? null) {
$body[] = '<label class="outer-label">' . ($extra['outer_label'] ?? null) . '</label>';
}
$orig_extra = $extra;
$use_stpl = isset($extra['use_stpl']) ? ($extra['use_stpl'] ?? null) : $this->BOXES_USE_STPL;
foreach ((array) $values as $value => $val_name) {
$label_extra_class = '';
if (is_array($val_name)) {
$extra = (array) $orig_extra + (array) ($val_name['extra'] ?? null);
$label_extra_class = ($val_name['class'] ?? null);
$val_name = ($val_name['html'] ?? null);
}
$is_selected = ((string) ($type == 1 ? $val_name : $value) == $selected);
$id = $id_prefix . '_' . ++$counter;
if ($use_stpl) {
$body[] = tpl()->parse('system/common/radio_box_item', [
'name' => $name,
'value' => $value,
'selected' => $is_selected ? 'checked="checked"' : '',
'add_str' => $add_str,
'label' => $translate ? t($val_name) : $val_name,
'horizontal' => (int) ((bool) $horizontal),
'id' => $id,
'extra' => $extra,
]);
} else {
$label_extra = $extra['label_extra'] ?? null;
if (isset($label_extra['class'])) {
$label_extra['class'] = ($label_extra_class ?: ($label_extra['class'] ?? null) ?: $this->CLASS_LABEL_RADIO) . ($horizontal ? ' ' . $this->CLASS_LABEL_RADIO_INLINE : '');
}
if ($extra['class_add_label_radio'] ?? null) {
$label_extra['class'] = ($label_extra['class'] ?? '') . ' ' . ($extra['class_add_label_radio'] ?? null);
}
if ($is_selected) {
$label_extra['class'] = ($label_extra['class'] ?? '') . ' ' . $this->CLASS_LABEL_RADIO_SELECTED;
}
$body[] =
'<label' . _attrs($label_extra, ['id', 'class', 'style']) . '>'
. '<input type="radio"' . $this->_bs5_check_input_attr($add_str) . ' name="' . $name . '" id="' . $id . '" value="' . $value . '"' . ($add_str ? ' ' . trim($add_str) : '') . ($is_selected ? ' checked="checked"' : '') . '>'
. '<span>' . t($val_name) . '</span>'
. '</label>' . PHP_EOL;
}
}
return implode(PHP_EOL, $body);
}
public function ratio($html = '', $extra = [])
Responsive embed box: bs5 `ratio ratio-16x9`, Bootstrap 3 `embed-responsive embed-responsive-16by9`. `ratio` is "W x H" (16x9, 4x3, 1x1, 21x9 or any other, e.g. 3:2, which gets an inline style). $html is developer-supplied HTML (an iframe, a video), not escaped (also run request data in it through _escape_tpl_braces(): the page template parses the output again); Bootstrap 3 / 4 fill the box only with an iframe, embed, object, video or `.embed-responsive-item` child. Options: ratio, class_add, id, style, data-*, attr.
public function ratio($html = '', $extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
[$w, $h] = preg_match('~^([1-9]\d{0,3})\s*[x:/]\s*([1-9]\d{0,3})$~D', trim((string) ($extra['ratio'] ?? '')), $m) ? [(int) $m[1], (int) $m[2]] : [16, 9];
$ratio = $w . 'x' . $h;
$percent = rtrim(rtrim(sprintf('%.4F', $h / $w * 100), '0'), '.') . '%';
if ($this->_is_bs5) {
$known = in_array($ratio, ['1x1', '4x3', '16x9', '21x9'], true);
$class = $known ? 'ratio ratio-' . $ratio : 'ratio';
$style = $known ? '' : '--bs-aspect-ratio: ' . $percent . ';';
} else {
$known = ['16x9' => '16by9', '4x3' => '4by3'] + ($this->_is_bs4() ? ['21x9' => '21by9', '1x1' => '1by1'] : []);
$known = $known[$ratio] ?? '';
$class = $known ? 'embed-responsive embed-responsive-' . $known : 'embed-responsive';
$style = $known ? '' : 'padding-bottom: ' . $percent . ';';
}
$a = [
'class' => trim($class . ' ' . ($extra['class_add'] ?? '')),
'id' => $extra['id'] ?? '',
'style' => trim($style . ' ' . ($extra['style'] ?? '')),
] + $this->_b4_pass($extra);
return '<div' . $this->_b4_attrs($a) . '>' . $html . '</div>';
}
public function select2_box($name, $values = [], $selected = '', $extra = [])
public function select2_box($name, $values = [], $selected = '', $extra = [])
{
$css = [];
$style = [];
if (is_array($name)) {
$extra = (array) $extra + $name;
} else {
$extra['name'] = $name;
}
$extra['force_id'] = $extra['force_id'] ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
// put default js options here
$js_options = (array) $extra['js_options'] + [
'width' => 'element',
'placeholder' => ($extra['placeholder'] ?? null) ?: ($extra['desc'] ?? null),
];
// ajax
$js_functions = [];
if ($extra['ajax_func'] ?? null) {
foreach ((array) $extra['ajax_func'] as $key => $value) {
$js_options['ajax'][$key] = '%__' . $key . '__%';
$js_functions['"%__' . $key . '__%"'] = $value;
}
}
if ($extra['ajax'] ?? null) {
$js_options += [
'maximumSelectionSize' => 10,
'minimumInputLength' => 1,
'initSelection' => '%__initSelection__%',
];
$js_options['ajax'] = $extra['ajax'];
$js_options['ajax'] += [
// 'dataType' => 'json',
// 'quietMillis' => 500,
// 'cache' => true,
'data' => '%__data__%',
'results' => '%__results__%',
];
$js_functions += [
'"%__data__%"' => 'function( term, page, context ) { return { q: term, page: page }; }',
'"%__results__%"' => 'function( data, page, query ) { return { results: data.items, more: data.more || false }; }',
'"%__initSelection__%"' => 'function( element, callback ) {
var $this = $(element);
var id = $this.val();
if( id !== "" ) {
$.ajax("' . $js_options['ajax']['url'] . '" + "&q=" + id, {
dataType: "json"
}).done(function(data) { callback(data.items[0] || null); });
}
}',
];
}
asset('jq-select2');
// prepare js options
$_js_options = json_encode($js_options, JSON_PRETTY_PRINT);
$js_functions && $_js_options = str_replace(array_keys($js_functions), array_values($js_functions), $_js_options);
jquery('$("#' . addslashes($extra['force_id']) . '").select2(' . $_js_options . ');');
$func = $extra['multiple'] ? 'multi_select' : 'select_box';
if ($extra['ajax']) {
$func = 'input';
$extra['id'] = $extra['force_id'];
$extra['value'] = ($extra['selected'] ?? null) ?: ($extra['value'] ?? null) ?: '';
$css[] = $this->CLASS_INPUT;
}
$css[] = 'no-chosen';
$extra['class'] && $css[] = $extra['class'];
$extra['style'] && $style[] = $extra['style'];
$extra['class'] = implode(' ', $css);
$extra['style'] = implode(';', $style);
return $this->$func($extra, $values, $selected);
}
public function select_box($name, $values = [], $selected = '', $show_text = false, $type = 2, $add_str = '', $translate = 0, $level = 0, $extra = [])
public function select_box($name, $values = [], $selected = '', $show_text = false, $type = 2, $add_str = '', $translate = 0, $level = 0, $extra = [])
{
// Passing params as array
if (is_array($name)) {
$extra = $name;
$name = $extra['name'];
}
! is_array($extra) && $extra = [];
$values = isset($extra['values']) ? $extra['values'] : (array) $values; // Required
$translate = isset($extra['translate']) ? $extra['translate'] : $translate;
if ($extra['no_translate'] ?? null) {
$translate = 0;
}
$selected = isset($extra['selected']) ? $extra['selected'] : $selected;
$show_text = isset($extra['show_text']) ? $extra['show_text'] : ($show_text !== null ? $show_text : false);
$type = isset($extra['type']) ? $extra['type'] : ($type !== null ? $type : 2);
$level = isset($extra['level']) ? $extra['level'] : $level;
// (example: $add_str = 'size=6')
$add_str = isset($extra['add_str']) ? $extra['add_str'] : $add_str;
$extra['class'] = isset($extra['class']) ? $extra['class'] : $this->CLASS_SELECT_BOX;
($extra['class_add'] ?? null) && $extra['class'] = trim(($extra['class'] ?? null) . ' ' . $extra['class_add']);
if ( ! $values && @ ! $extra['ajax']) {
return false;
}
if ($extra['disabled'] ?? null) {
$extra['disabled'] = 'disabled';
}
$id = null;
$body = [];
if ($level == 0) {
($extra['force_id'] ?? null) && $id = $extra['force_id'];
$id = $id ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ($this->AUTO_ASSIGN_IDS) {
$extra['id'] = $id;
}
if ($extra['outer_label'] ?? null) {
$body[] = '<label class="outer-label">' . $extra['outer_label'] . '</label>';
}
$extra['name'] = $name;
$body[] = '<select' . _attrs($extra, ['name', 'id', 'class', 'style', 'disabled', 'required']) . ($add_str ? ' ' . $add_str : '') . '>';
}
$selected = (string) $selected;
if ($show_text && $level == 0) {
$def_opt_text = $show_text;
if ($show_text == 1) {
$def_opt_text = str_replace(['%name%', '%select%'], [t($name), t('Select')], $this->SELECT_BOX_DEF_OPT_TPL);
}
$body[] = '<option value="" class="' . $this->CLASS_SELECT_OPTION_DEFAULT . '">' . $def_opt_text . '</option>';
}
$self_func = __FUNCTION__;
$option_callback = ($extra['option_callback'] ?? null);
$use_option_callback = is_callable($option_callback);
foreach ((array) $values as $key => $cur_value) {
if (is_array($cur_value)) {
$body[] = '<optgroup label="' . $key . '" title="' . ($translate ? t($key) : $key) . '">';
$body[] = $this->$self_func($name, $cur_value, $selected, $show_text, $type, $add_str, $translate, $level + 1);
$body[] = '</optgroup>';
} else {
$_what_compare = (string) ($type == 1 ? $cur_value : $key);
$is_selected = ($_what_compare == $selected);
$text = $translate ? t($cur_value) : $cur_value;
if ($use_option_callback) {
$body[] = $option_callback($key, $cur_value, $is_selected, $text, $extra);
} else {
$body[] = '<option value="' . $key . '"' . ($is_selected ? ' selected="selected"' : '') . '>' . $text . '</option>';
}
}
}
$body[] = $level == 0 ? '</select>' : '';
return implode(PHP_EOL, $body);
}
public function simple_table($replace = [], $extra = [])
public function simple_table($replace = [], $extra = [])
{
if ( ! $replace) {
return false;
}
$key_name = isset($extra['key']['name']) ? $extra['key']['name'] : 'key';
$val_name = isset($extra['val']['name']) ? $extra['val']['name'] : 'val';
$key_extra = isset($extra['key']['extra']) ? $extra['key']['extra'] : [];
$val_extra = isset($extra['val']['extra']) ? $extra['val']['extra'] : [];
$key_callable = null;
$val_callable = null;
$key_func = isset($extra['key']['func']) ? $extra['key']['func'] : 'text';
if ( ! is_string($key_func) && is_callable($key_func)) {
$key_callable = $key_func;
$key_func = 'func';
}
$val_func = isset($extra['val']['func']) ? $extra['val']['func'] : 'text';
if ( ! is_string($val_func) && is_callable($val_func)) {
$val_callable = $val_func;
$val_func = 'func';
}
$data = [];
foreach ((array) $replace as $k => $v) {
$data[] = [
$key_name => $k,
$val_name => $v,
];
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$extra['no_header'] = isset($extra['no_header']) ? $extra['no_header'] : 1;
$extra['pager_records_on_page'] = isset($extra['pager_records_on_page']) ? $extra['pager_records_on_page'] : 10000;
$extra['no_total'] = isset($extra['no_total']) ? $extra['no_total'] : true;
$extra['no_pages'] = isset($extra['no_pages']) ? $extra['no_pages'] : true;
$extra['hide_empty'] = isset($extra['hide_empty']) ? $extra['hide_empty'] : true;
return table($data, $extra)
->$key_func($key_name, $key_callable, $key_extra)
->$val_func($val_name, $val_callable, $val_extra);
}
public function spinner($extra = [])
Loading indicator: bs5 `spinner-border` / `spinner-grow`, Bootstrap 3 a spinning Font Awesome icon. Options: type (border / grow), size (sm), color, text (for screen readers, default "Loading..."), class_add (`htmx-indicator` makes it an hx-indicator), id, style, no_escape, data-*, attr. Bootstrap 4 gets the Bootstrap 3 markup.
public function spinner($extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
$text = $this->_b4_text($extra['text'] ?? t('Loading...'), $extra);
$color = $this->_color_type($extra['color'] ?? '', '');
$color = ($color === '' || $color === 'default') ? '' : 'text-' . $color;
$sm = ($extra['size'] ?? '') === 'sm';
$a = ['id' => $extra['id'] ?? '', 'style' => $extra['style'] ?? '', 'role' => 'status'] + $this->_b4_pass($extra);
if ($this->_is_bs5) {
$type = ($extra['type'] ?? '') === 'grow' ? 'spinner-grow' : 'spinner-border';
$a = ['class' => trim($type . ($sm ? ' ' . $type . '-sm' : '') . ($color ? ' ' . $color : '') . ' ' . ($extra['class_add'] ?? ''))] + $a;
return '<span' . $this->_b4_attrs($a) . '><span class="visually-hidden">' . $text . '</span></span>';
}
$a = ['class' => trim($color . ' ' . ($extra['class_add'] ?? ''))] + $a;
return '<span' . $this->_b4_attrs($a) . '><i class="fa fa-spinner fa-spin' . ($sm ? '' : ' fa-2x') . '" aria-hidden="true"></i><span class="sr-only">' . $text . '</span></span>';
}
public function tabs($tabs = [], $extra = [])
Tabs; the tab contents are developer-supplied HTML (as modal() bodies). B5 options of the nav: type (tabs, pills, underline), fill, justified, vertical (see _tabs_nav_options()); form2 passes its `tabs` param here.
public function tabs($tabs = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->tabs($tabs, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$extra_by_id = [];
if (isset($extra['by_id'])) {
$extra_by_id = (array) ($extra['by_id'] ?? null);
unset($extra['by_id']);
}
$links_prefix = ($extra['links_prefix'] ?? null) ?: 'tab_';
[$nav_class, $nav_toggle, $nav_vertical] = $this->_tabs_nav_options($extra, 'bs3');
$headers = [];
$items = [];
$i = 0;
foreach ((array) $tabs as $k => $v) {
$desc_raw = null;
$disabled = null;
if ( ! is_array($v)) {
$content = $v;
$v = [];
} else {
$content = ($v['content'] ?? null);
$desc_raw = ($v['desc_raw'] ?? null);
$disabled = ($v['disabled'] ?? null);
}
$content = trim($content ?? '');
$_extra = (array) ($extra_by_id[$k] ?? null) + (array) $extra;
if (($_extra['hide_empty'] ?? null) && ! strlen($content)) {
continue;
}
$name = ($v['name'] ?? null) ?: $k;
$desc = ($v['desc'] ?? null) ?: ( ! ($_extra['no_auto_desc'] ?? null) ? ucfirst(str_replace('_', ' ', $name)) : $name);
$id = preg_replace('~[^a-z0-9_-]+~i', '', ($v['id'] ?? null) ?: $links_prefix . $k);
if (isset($_extra['selected'])) {
$is_active = (($_extra['selected'] ?? null) == $k);
} else {
$is_active = (++$i == 1);
}
if (isset($_extra['totals'][$name])) {
$v['badge'] = (int) (isset($_extra['totals'][$name]['total']) ? ($_extra['totals'][$name]['total'] ?? null) : ($_extra['totals'][$name] ?? null));
}
$badge = isset($v['badge']) ? ' <sup class="badge badge-' . (($v['class_badge'] ?? null) ?: 'info') . '">' . ($v['badge'] ?? null) . '</sup>' : '';
if ( ! ($_extra['no_headers'] ?? null)) {
$class_head = ($v['class_head'] ?? null) ?: ($_extra['class_head'] ?? null);
$class_head .= ($_extra['class_add_head'] ?? null) ? ' ' . ($_extra['class_add_head'] ?? null) : '';
if ($is_active) {
$class_head = trim('active ' . $class_head);
}
$_extra_head = (array) ($_extra['tab_head'] ?? null);
$_extra_head['class'] = ($_extra_head['class'] ?? null) ?: $class_head;
$headers[] =
'<li' . _attrs($_extra_head, ['id', 'class', 'style']) . '>
<a ' . ( ! $disabled ? 'href="#' . fix_html_attr_id($id) . '" ' : '') . 'data-toggle="' . $nav_toggle . '">' . ($desc_raw ?: t($desc)) . $badge . '</a>
</li>';
}
$class_body = (($_extra['class'] ?? null) ?: ($v['class_body'] ?? null)) ?: ($_extra['class_body'] ?? null);
$class_body = $class_body ?: 'tab-pane';
$class_body .= ($_extra['class_add_body'] ?? null) ? ' ' . ($_extra['class_add_body'] ?? null) : '';
if ($is_active || ($_extra['show_all'] ?? null)) {
$class_body = trim('active ' . $class_body);
} else {
$class_body = trim('fade ' . $class_body);
}
$_extra_body = (array) ($_extra['tab_body'] ?? null);
$_extra_body['id'] = ($_extra_body['id'] ?? null) ?: $id;
$_extra_body['class'] = ($_extra_body['class'] ?? null) ?: $class_body;
$items[] = '<div' . _attrs($_extra_body, ['id', 'class', 'style']) . '>' . $content . '</div>';
}
$nav = $headers ? '<ul id="' . ($extra['id'] ?? null) . '" class="' . $nav_class . '">' . implode(PHP_EOL, (array) $headers) . '</ul>' : '';
$content = '<div id="' . ($extra['id'] ?? null) . '_content" class="tab-content">' . implode(PHP_EOL, (array) $items) . '</div>';
if ($nav_vertical && $headers) {
return '<div class="row"><div class="col-sm-3">' . $nav . '</div>' . PHP_EOL . '<div class="col-sm-9">' . $content . '</div></div>';
}
return ($headers ? $nav . PHP_EOL : '') . $content;
}
public function textarea($name = '', $value = '', $extra = [])
Simple textarea form control.
public function textarea($name = '', $value = '', $extra = [])
{
if (is_array($name)) {
$extra = (array) $extra + $name;
$name = $extra['name'];
}
if ( ! is_array($extra)) {
$extra = [];
}
$extra['name'] = ($extra['name'] ?? null) ?: ($name ?: 'text');
$extra['value'] = ($extra['value'] ?? null) ?: $value;
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$extra['desc'] = ($extra['desc'] ?? null) ?: ucfirst(str_replace('_', '', $extra['name']));
$extra['type'] = ($extra['type'] ?? null) ?: 'text';
$extra['placeholder'] = _escape_tpl_braces($extra['placeholder'] ? t($extra['placeholder']) : $extra['desc']);
$extra['title'] = _escape_tpl_braces($extra['title'] ?? null);
$extra['contenteditable'] = ( ! isset($extra['contenteditable']) || $extra['contenteditable']) ? 'true' : false;
$attrs_names = ['id', 'name', 'placeholder', 'contenteditable', 'class', 'style', 'cols', 'rows', 'title', 'required', 'size', 'disabled', 'readonly', 'autocomplete', 'autofocus'];
return '<textarea' . _attrs($extra, $attrs_names) . '>' . _escape_tpl_braces( ! isset($extra['no_escape']) ? _htmlchars($extra['value']) : $extra['value']) . '</textarea>';
}
public function thumbnails($data = [], $extra = [])
public function thumbnails($data = [], $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->thumbnails($data, $extra);
}
$items = [];
$columns = (int) ($extra['columns'] ?? null) ?: 3;
$row_class = 'span' . round(12 / $columns) . ' col-md-' . round(12 / $columns);
foreach ((array) $data as $k => $v) {
if ( ! is_array($v)) {
$img_src = $v;
$v = [];
} else {
$img_src = ($v['img'] ?? null);
}
$class_item = ($v['class_item'] ?? null) ?: ($extra['class_item'] ?? null);
$tag = $this->_is_bs3 ? 'div' : 'li';
$items[] =
'<' . $tag . ' class="' . $row_class . ($class_item ? ' ' . $class_item : '') . '"' . (($v['style'] ?? null) ? ' style="' . ($v['style'] ?? null) . '"' : '') . '>
<div class="thumbnail">
<img alt="' . _prepare_html(($v['alt'] ?? null) ?: ($v['head'] ?? null)) . '" src="' . _prepare_html($img_src) . '" />
' . ((($v['head'] ?? null) || ($v['body'] ?? null)) ? '<div class="caption">' . (($v['head'] ?? null) ? '<h3>' . _prepare_html(($v['head'] ?? null)) . '</h3>' : '') . ' ' . ($v['body'] ?? null) . '</div>' : '') . '
</div>
</' . $tag . '>';
}
$body = [];
$tag = $this->_is_bs3 ? 'div' : 'ul';
foreach (array_chunk($items, $columns) as $_items) {
$id = __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
$body[] = '<' . $tag . ' class="thumbnails' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . $id . '">' . implode(PHP_EOL, (array) $_items) . '</' . $tag . '>';
}
return implode(PHP_EOL, $body);
}
public function time_box($selected = '', $name_postfix = '', $add_str = '', $show_text = 1, $translate = 1)
public function time_box($selected = '', $name_postfix = '', $add_str = '', $show_text = 1, $translate = 1)
{
return _class('html_datetime', 'classes/html/')->time_box($selected, $name_postfix, $add_str, $show_text, $translate);
}
public function time_box2($name, $selected = '', $add_str = '', $show_text = 1, $translate = 1)
public function time_box2($name, $selected = '', $add_str = '', $show_text = 1, $translate = 1)
{
return _class('html_datetime', 'classes/html/')->time_box2($name, $selected, $add_str, $show_text, $translate);
}
public function toast($body = '', $extra = [])
Notice that shows and hides itself: a bs5 toast (asset yf_bs5_toasts shows it on load and after htmx swaps), a Bootstrap 3 / 4 alert(). $body is developer-supplied HTML, not escaped (escape request data in it and run it through _escape_tpl_braces(): the page template parses the output again); the title is escaped. Options: title, type (success, info, warning, danger / error...), autohide (default true), delay (ms, default 5000), show (default true; false renders it hidden for site JS), no_close, id, class_add, no_escape (title), data-*, attr. Put toasts in toast_container(). The alert fallback keeps title, type, no_close, id and class_add; it ignores autohide, delay, show, data-* and attr.
public function toast($body = '', $extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
if ($this->_is_bs5) {
return $this->_bs5()->toast($body, $extra);
}
$type = $this->_color_type($extra['type'] ?? '', 'info');
$alert = ['alert' => in_array($type, ['success', 'info', 'warning', 'danger'], true) ? $type : 'info', 'no_close' => $extra['no_close'] ?? null];
($extra['id'] ?? null) && $alert['id'] = $extra['id'];
($extra['class_add'] ?? null) && $alert['class'] = $extra['class_add'];
return $this->alert(['head' => $this->_b4_text($extra['title'] ?? '', $extra), 'body' => (string) $body], $alert);
}
public function toast_container($extra = [])
Fixed corner box that stacks toasts. Options: toasts (rendered toast() HTML, a string or an array), position (top-end default, top-start, top-center, middle-*, bottom-*), id, class_add, data-*, attr. An empty one also loads the yf_bs5_toasts asset, which shows the toasts of `_class('htmx')->_toast()` in the first container of the page. One container per corner: two overlap (MESSAGES_AS_TOASTS uses top-end). Bootstrap 3: a plain div around the alerts.
public function toast_container($extra = [])
{
$extra = (array) $extra;
$this->_sync_fw();
if ($this->_is_bs5) {
return $this->_bs5()->toast_container($extra);
}
$toasts = $extra['toasts'] ?? '';
$a = ['class' => trim('yf-toast-container ' . ($extra['class_add'] ?? '')), 'id' => $extra['id'] ?? ''] + $this->_b4_pass($extra);
return '<div' . $this->_b4_attrs($a) . '>' . (is_array($toasts) ? implode(PHP_EOL, $toasts) : (string) $toasts) . '</div>';
}
public function tooltip($text = '', $extra = [])
public function tooltip($text = '', $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->tooltip($text, $extra);
}
if (is_array($text)) {
$extra = (array) $extra + $text;
$text = '';
}
if ( ! is_array($extra)) {
$extra = [];
}
$extra['text'] = ($extra['text'] ?? null) ?: $text;
$extra['icon'] = ($extra['icon'] ?? null) ?: 'icon icon-info-sign fa fa-question-circle';
$extra['href'] = ($extra['href'] ?? null) ?: '#';
$extra['class'] = (($extra['class'] ?? null) ?: 'yf_tip') . (($extra['class_add'] ?? null) ? ' ' . ($extra['class_add'] ?? null) : '');
$extra['data-content'] = ($extra['data-content'] ?? null) ?: _prepare_html(($extra['text'] ?? null));
$extra['data-toggle'] = ($extra['data-toggle'] ?? null) ?: 'popover';
$extra['data-container'] = ($extra['data-container'] ?? null) ?: 'body';
$extra['data-html'] = ($extra['data-html'] ?? null) ?: 'true';
return ( ! ($extra['no_nbsp'] ?? null) ? ' ' : '') . '<span' . _attrs($extra, ['id', 'class', 'style']) . '><i class="' . ($extra['icon'] ?? null) . '"></i></span>';
}
public function tree($data = [], $extra = [])
public function tree($data = [], $extra = [])
{
return _class('html_tree', 'classes/html/')->tree($data, $extra);
}
public function well($body = '', $extra = [])
public function well($body = '', $extra = [])
{
if ($this->_is_bs5) {
return $this->_bs5()->well($body, $extra);
}
$extra['id'] = ($extra['id'] ?? null) ?: __FUNCTION__ . '_' . ($this->_ids[__FUNCTION__] = ($this->_ids[__FUNCTION__] ?? 0) + 1);
if ( ! ($extra['class'] ?? null)) {
$extra['class'] = 'well-lg';
}
return '<div class="well well-lg' . (($extra['class'] ?? null) ? ' ' . ($extra['class'] ?? null) : '') . '" id="' . ($extra['id'] ?? null) . '">' . $body . '</div>';
}
_ hooks and helpers; not routable as page actionspublic function __call($name, $args)
Catch missing method call.
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
public function __clone()
We cleanup object properties when cloning.
public function __clone()
{
$keep_prefix = 'CLASS_';
$keep_len = strlen($keep_prefix);
$keep_prefix2 = 'CONF_';
$keep_len2 = strlen($keep_prefix2);
foreach ((array) get_object_vars($this) as $k => $v) {
if (substr($k, 0, $keep_len) === $keep_prefix) {
continue;
}
if (substr($k, 0, $keep_len2) === $keep_prefix2) {
continue;
}
if ($k[0] == '_') {
$this->$k = null;
}
}
}
public function __toString(): string
Need to avoid calling render() without params.
public function __toString()
{
try {
return (string) $this->render();
} catch (Exception $e) {
return '';
}
}
public function _b4_attrs($a)
Attributes string of $a (name => value, plus `attr`), template braces escaped; empty values are left out.
$a array public function _b4_attrs($a)
{
$a = array_filter((array) $a, fn ($v) => $v !== null && $v !== '' && $v !== false);
return _attrs(_escape_tpl_braces($a), array_diff(array_keys($a), ['attr']));
}
public function _b4_pass($extra)
The data-* and `attr` (extra attributes) options of $extra, to add to a component's own attributes.
public function _b4_pass($extra)
{
$a = [];
foreach ((array) $extra as $k => $v) {
is_string($k) && str_starts_with($k, 'data-') && $a[$k] = $v;
}
is_array($extra['attr'] ?? null) && $a['attr'] = $extra['attr'];
return $a;
}
public function _b4_text($text, $extra = [])
Text of a component, escaped; with `no_escape` as it is (developer-supplied HTML, never request data).
public function _b4_text($text, $extra = [])
{
$text = (string) $text;
return ($extra['no_escape'] ?? null) ? $text : _prepare_html($text, 0);
}
public function _bs5()
Bootstrap 5 markup of the widgets, bound to this instance (id counters, class properties).
public function _bs5()
{
$bs5 = _class('html_bs5', 'classes/html/');
$bs5->_parent = $this;
return $bs5;
}
public function _bs5_btn_class($class, $add = '')
Bootstrap 5 button class list $class . ' ' . $add: Bootstrap 3 put a colour over btn-default (btn-default btn-success), Bootstrap 5 has no such base colour, so the neutral colour (CLASS_BS5_BTN_NEUTRAL) leaves $class when $add brings another button colour. Only $class, the base list, loses it: a neutral class in $add stays. The base cannot tell the framework default (CLASS_BTN_MINI) from a site class that happens to be btn-outline-secondary, so that one goes next to a colour from $add too.
$class string$add string public function _bs5_btn_class($class, $add = '')
{
$class = (string) $class;
$add = (string) $add;
$neutral = $this->CLASS_BS5_BTN_NEUTRAL;
$tokens = preg_split('~\s+~', trim($class), -1, PREG_SPLIT_NO_EMPTY);
if (in_array($neutral, $tokens, true) && $this->_bs5_btn_colors($add)) {
$class = implode(' ', array_diff($tokens, [$neutral]));
}
return $add === '' ? $class : $class . ' ' . $add;
}
public function _bs5_btn_colors($class)
Button colour classes (btn-primary, btn-outline-danger, ...) of a class list.
$class string public function _bs5_btn_colors($class)
{
$tokens = preg_split('~\s+~', trim((string) $class), -1, PREG_SPLIT_NO_EMPTY);
return array_values(preg_grep('~^btn-(outline-)?(primary|secondary|success|danger|warning|info|light|dark|link)$~', $tokens));
}
public function _bs5_btn_html($data)
The class attributes of framework HTML (a string or the strings of an array, such as the pair_* data handlers) with Bootstrap 5 button classes: each whole token of BS5_BTN_TOKENS is replaced in place (btn-default only when no other colour is there, a repeated result such as btn-sm once); lists without such a token stay as they are.
public function _bs5_btn_html($data)
{
if (is_array($data)) {
foreach ($data as $k => $v) {
is_string($v) && $data[$k] = $this->_bs5_btn_html($v);
}
return $data;
}
return preg_replace_callback('~(\sclass=)(["\'])(.*?)\2~', function ($m) {
$tokens = preg_split('~\s+~', trim($m[3]), -1, PREG_SPLIT_NO_EMPTY);
if ( ! array_intersect($tokens, array_keys($this->BS5_BTN_TOKENS))) {
return $m[0];
}
$has_color = (bool) $this->_bs5_btn_colors($m[3]);
$out = [];
foreach ($tokens as $t) {
if (isset($this->BS5_BTN_TOKENS[$t])) {
$t = $this->BS5_BTN_TOKENS[$t];
if ($t === '' || ($t === $this->CLASS_BS5_BTN_NEUTRAL && $has_color) || in_array($t, $out, true)) {
continue;
}
}
$out[] = $t;
}
return $m[1] . $m[2] . implode(' ', $out) . $m[2];
}, (string) $data);
}
public function _bs5_check_input_attr(&$add_str = '')
` class="form-check-input"` for a radio / checkbox input under Bootstrap 5, '' otherwise; when $add_str already has a class attribute, '' and the class is merged into $add_str instead (two class attributes: the browser keeps only the first).
$add_str string public function _bs5_check_input_attr(&$add_str = '')
{
if ( ! $this->_is_bs5) {
return '';
}
$add_str = (string) $add_str;
if (preg_match('~(?:^|\s)class=(["\'])(.*?)\1~i', $add_str, $m)) {
if ( ! in_array($this->CLASS_BS5_CHECK_INPUT, preg_split('~\s+~', $m[2]), true)) {
$add_str = str_replace($m[0], substr($m[0], 0, -strlen($m[2]) - 1) . trim($m[2] . ' ' . $this->CLASS_BS5_CHECK_INPUT) . $m[1], $add_str);
}
return '';
}
return ' class="' . $this->CLASS_BS5_CHECK_INPUT . '"';
}
public function _bs5_switch_classes($obj, $is_bs5)
Switches the properties of $obj->BS5_CLASSES (property => [bs3 default, Bootstrap 5 value]) to their Bootstrap 5 value when $is_bs5 and back when not. Used by yf_html, yf_form2 and yf_table2; $obj->_bs5_switched remembers what was switched.
$obj object$is_bs5 bool public function _bs5_switch_classes($obj, $is_bs5)
{
foreach ((array) $obj->BS5_CLASSES as $prop => [$bs3, $bs5]) {
if ($is_bs5 && $obj->$prop === $bs3) {
$obj->$prop = $bs5;
$obj->_bs5_switched[$prop] = true;
} elseif ( ! $is_bs5 && isset($obj->_bs5_switched[$prop])) {
// only a value set here goes back: a site that chose `form-select` under bs3 keeps it
$obj->$prop === $bs5 && $obj->$prop = $bs3;
unset($obj->_bs5_switched[$prop]);
}
}
}
public function _btn_class($extra = [])
Button classes of button() and the components built on it: `type` (colour, `link`), `outline`, `size` (sm / xs / lg). Bootstrap 4 has the Bootstrap 5 ones; Bootstrap 3: an outline button is btn-default, sm is btn-xs (as the framework's mini buttons).
public function _btn_class($extra = [])
{
$extra = (array) $extra;
$type = $this->_color_type($extra['type'] ?? '', '');
$outline = ! empty($extra['outline']);
$bs45 = $this->_is_bs5 || $this->_is_bs4();
if (strtolower((string) ($extra['type'] ?? '')) === 'link') {
$color = 'btn-link';
} elseif ($bs45) {
$color = $type === '' ? $this->CLASS_BS5_BTN_NEUTRAL : 'btn-' . ($outline ? 'outline-' : '') . $type;
} else {
$color = ($type === '' || $outline) ? 'btn-default' : 'btn-' . $type;
}
$size = ['sm' => ['btn-xs', 'btn-sm'], 'xs' => ['btn-xs', 'btn-sm'], 'lg' => ['btn-lg', 'btn-lg']][$extra['size'] ?? ''] ?? null;
return 'btn ' . $color . ($size ? ' ' . $size[(int) $bs45] : '');
}
public function _carousel_options_bs3($extra)
B5 carousel() options under Bootstrap 3 / 4: [classes added to .carousel, attributes from data-ride on]. interval (ms or false), pause (hover or false), wrap / keyboard (false), ride (false: no autoplay); Bootstrap 4 also fade and touch (false). A slide `interval` and `theme` are Bootstrap 5 only.
$extra array public function _carousel_options_bs3($extra)
{
$bs4 = $this->_is_bs4();
$a = ['data-ride' => (isset($extra['ride']) && ! $extra['ride']) ? '' : 'carousel'];
if (array_key_exists('interval', $extra)) {
$a['data-interval'] = $extra['interval'] === false ? 'false' : (string) max(0, (int) $extra['interval']);
}
if (array_key_exists('pause', $extra)) {
$a['data-pause'] = $extra['pause'] === 'hover' ? 'hover' : 'false';
}
foreach (['wrap', 'keyboard'] + ($bs4 ? [2 => 'touch'] : []) as $k) {
(isset($extra[$k]) && ! $extra[$k]) && $a['data-' . $k] = 'false';
}
return [$bs4 && ! empty($extra['fade']) ? ' carousel-fade' : '', $this->_b4_attrs($a)];
}
public function _color_type($type, $default = '')
Colour name of the components (badge, button, toast, spinner...): primary, secondary, success, danger, warning, info, light, dark (error / important = danger, default = secondary), else $default; Bootstrap 3 has no secondary / light / dark (`default`).
$default string public function _color_type($type, $default = '')
{
$type = strtolower(trim((string) $type));
$type = ['error' => 'danger', 'important' => 'danger', 'default' => 'secondary', 'inverse' => 'dark'][$type] ?? $type;
if ( ! in_array($type, ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'], true)) {
$type = $default;
}
return ($this->_is_bs5 || $this->_is_bs4() || ! in_array($type, ['secondary', 'light', 'dark'], true)) ? $type : 'default';
}
public function _count_levels(&$children, $start_id = 0, $level = 0)
public function _count_levels(&$children, $start_id = 0, $level = 0)
{
$ids = [];
foreach ((array) $children[$start_id] as $id => $_tmp) {
$ids[$id] = $level;
if (isset($children[$id])) {
foreach ((array) $this->_count_levels($children, $id, $level + 1) as $_id => $_level) {
$ids[$_id] = $_level;
}
}
}
return $ids;
}
public function _css_length($v)
CSS length of a `height` option: a positive int (or digits) is px, a number with px / rem / em / % stays; anything else is ''.
public function _css_length($v)
{
if (is_int($v) || (is_string($v) && ctype_digit($v))) {
return (int) $v > 0 && (int) $v < 10000 ? (int) $v . 'px' : '';
}
$v = trim((string) $v);
return preg_match('~^\d{1,4}(?:\.\d{1,3})?(?:px|rem|em|%)$~D', $v) ? $v : '';
}
public function _dropdown_items_bs3($items)
The Bootstrap 3 / 4 <li> items of dropdown() (and of the navbar() dropdowns); see _dropdown_menu_items() for the formats.
public function _dropdown_items_bs3($items)
{
$bs4 = $this->_is_bs4();
$items_html = [];
foreach ($this->_dropdown_menu_items($items) as $v) {
if (isset($v['divider'])) {
$items_html[] = $bs4 ? '<li class="dropdown-divider"></li>' : '<li role="separator" class="divider"></li>';
} elseif (isset($v['header'])) {
$items_html[] = '<li class="dropdown-header">' . $v['header'] . '</li>';
} elseif ( ! empty($v['text_only'])) {
$items_html[] = $bs4 ? '<li><span class="dropdown-item-text">' . $v['html'] . '</span></li>' : '<li><a>' . $v['html'] . '</a></li>';
} else {
$is_link = strlen((string) ($v['link'] ?? '')) > 0;
$active = ! empty($v['active']);
$disabled = ! empty($v['disabled']);
$a = [
// Bootstrap 3 has no pointer-events: none on a disabled item, so it gets no href; Bootstrap 4 has it
'href' => $is_link && ( ! $disabled || $bs4) ? $this->_safe_href($v['link']) : '',
'class' => trim(($bs4 ? 'dropdown-item' . ($active ? ' active' : '') . ($disabled ? ' disabled' : '') : '') . ' ' . ($v['class_add'] ?? '')),
'id' => $v['id'] ?? '',
'title' => $v['title'] ?? '',
'target' => $v['target'] ?? '',
'rel' => $v['rel'] ?? '',
'aria-current' => $bs4 && $active ? 'true' : '',
'aria-disabled' => $disabled ? 'true' : '',
'tabindex' => $disabled ? '-1' : '',
] + $this->_b4_pass($v);
if ( ! $is_link && $bs4) {
unset($a['href'], $a['target'], $a['rel'], $a['tabindex']);
$a = ['type' => 'button'] + $a + ['disabled' => $disabled ? 'disabled' : ''];
$items_html[] = '<li><button' . $this->_b4_attrs($a) . '>' . $v['html'] . '</button></li>';
continue;
}
// a Bootstrap 3 item without a link: no href (no jump to the top), still focusable
if ( ! $is_link && ! $disabled) {
$a['tabindex'] = '0';
$a['role'] = 'button';
}
$li = $bs4 ? '' : trim(($active ? 'active ' : '') . ($disabled ? 'disabled' : ''));
$items_html[] = '<li' . ($li ? ' class="' . $li . '"' : '') . '><a' . $this->_b4_attrs($a) . '>' . $v['html'] . '</a></li>';
}
}
return $items_html;
}
public function _get_country_name($code)
public function _get_country_name($code)
{
if ( ! isset($this->_country_names)) {
$this->_country_names = db()->select('code', 'name')->from('geo_countries')->get_2d();
}
return $this->_country_names[$code];
}
public function _get_ip_country($ip)
public function _get_ip_country($ip)
{
if ( ! isset($this->_ip_to_country[$ip])) {
$code = _class('api')->_geoip_country_code_by_name($ip);
$this->_ip_to_country[$ip] = $code;
}
return $this->_ip_to_country[$ip];
}
public function _img_alt($extra)
` alt="..."` of the card image (`img_alt`, escaped; empty for a decorative image).
$extra array public function _img_alt($extra)
{
return ' alt="' . _htmlchars(_escape_tpl_braces((string) ($extra['img_alt'] ?? ''))) . '"';
}
public function _init()
public function _init()
{
$css_framework = (string) conf('css_framework');
$this->_is_bs3 = ($css_framework == 'bs3') || ($this->BS4_AS_BS3 && in_array($css_framework, ['bs4', 'empty', ''], true));
$this->_is_bs5 = ($css_framework == 'bs5');
$this->_bs5_switch_classes($this, $this->_is_bs5);
}
public function _is_bs4()
css_framework bs4: the components use Bootstrap 4 classes where they differ from Bootstrap 3 (badge, button, collapse, dropdown, ratio), as table2 does for its row variants; the others get the Bootstrap 3 markup.
public function _is_bs4()
{
return ! $this->_is_bs5 && conf('css_framework') === 'bs4';
}
public function _list_group_has_links($data)
Whether a list_group() has an item with a `link` (then it is a div of links); never without B5_ITEM_OPTIONS.
public function _list_group_has_links($data)
{
if ( ! $this->B5_ITEM_OPTIONS) {
return false;
}
foreach ((array) $data as $v) {
if (is_array($v) && strlen((string) ($v['link'] ?? ''))) {
return true;
}
}
return false;
}
public function _list_group_item_state($v, $extra, $fw)
B5 options of one list_group() item: null without them (the old markup), else ['link' => safe URL or null, 'class' => classes to add, 'attrs' => aria attributes]. `link` (trusted URL; javascript: / data: / vbscript: become #), `active`, `disabled` (a disabled link has no href), `variant` (item colour: list-group-item-<colour>; Bootstrap 3 has success, info, warning, danger). `type` stays ignored, as it always was. The item keys count only with B5_ITEM_OPTIONS; a `variant` of $extra always. $fw: bs5, bs4 (list-group-item-action on links) or bs3.
$v array$extra array$fw string public function _list_group_item_state($v, $extra, $fw)
{
$v = $this->B5_ITEM_OPTIONS ? (array) $v : [];
$link = (string) ($v['link'] ?? '');
$active = ! empty($v['active']);
$disabled = ! empty($v['disabled']);
$variant = $this->_color_type(($v['variant'] ?? null) ?: ($extra['variant'] ?? ''), '');
if ($fw === 'bs3' && ! in_array($variant, ['success', 'info', 'warning', 'danger'], true)) {
$variant = '';
}
if ( ! strlen($link) && ! $active && ! $disabled && $variant === '') {
return null;
}
$is_link = strlen($link) > 0;
$class = ($is_link && $fw !== 'bs3' ? ' list-group-item-action' : '') . ($variant !== '' ? ' list-group-item-' . $variant : '') . ($active ? ' active' : '') . ($disabled ? ' disabled' : '');
return [
'link' => $is_link ? ($disabled ? '' : $this->_safe_href($link)) : null,
'class' => $class,
'attrs' => ['aria-current' => $active ? 'true' : '', 'aria-disabled' => $disabled ? 'true' : ''],
];
}
public function _modal_options_bs3($extra)
B5 modal() options under Bootstrap 3 / 4: [classes added to .modal, classes added to .modal-dialog, attributes]. size sm / lg (xl is lg under Bootstrap 3), fade, static (backdrop and Escape do not close it); Bootstrap 4 also xl, centered, scrollable. fullscreen and theme are Bootstrap 5 only.
$extra array public function _modal_options_bs3($extra)
{
$bs4 = $this->_is_bs4();
$sizes = ['sm' => 'modal-sm', 'lg' => 'modal-lg', 'xl' => $bs4 ? 'modal-xl' : 'modal-lg'];
$dialog = [$sizes[$extra['size'] ?? ''] ?? ''];
if ($bs4) {
! empty($extra['centered']) && $dialog[] = 'modal-dialog-centered';
! empty($extra['scrollable']) && $dialog[] = 'modal-dialog-scrollable';
}
$dialog = trim(implode(' ', $dialog));
return [
// an inline modal is shown without the JS that would add `in`
! empty($extra['fade']) && empty($extra['inline']) ? ' fade' : '',
strlen($dialog) ? ' ' . $dialog : '',
! empty($extra['static']) ? ' data-backdrop="static" data-keyboard="false"' : '',
];
}
public function _pagination_classes($size, $align, $fw)
Classes of the pagination `size` (sm, lg) and `align` (center, end) options: [classes of the pagination list, classes of its wrapper]. $fw: bs5 (justify-content-* on the list), bs3 (text-center / text-right on the wrapper, the list is inline-block) or bs2 (pagination-small / -large / -centered / -right on div.pagination). Other values give no class.
$size string$align string$fw string public function _pagination_classes($size, $align, $fw)
{
$size = in_array($size, ['sm', 'lg'], true) ? $size : '';
$align = in_array($align, ['center', 'end'], true) ? $align : '';
$list = [];
$wrap = '';
if ($fw === 'bs2') {
$size && $list[] = $size === 'sm' ? 'pagination-small' : 'pagination-large';
$align && $list[] = $align === 'center' ? 'pagination-centered' : 'pagination-right';
} else {
$size && $list[] = 'pagination-' . $size;
if ($fw === 'bs5') {
$align && $list[] = 'justify-content-' . $align;
} elseif ($align) {
$wrap = $align === 'center' ? 'text-center' : 'text-right';
}
}
return [implode(' ', $list), $wrap];
}
public function _pagination_item($key, $link, $text, $extra)
One pagination() item (Bootstrap 2 / 3 markup; yf_html_bs5 has its own): a link, the current page (active) or a disabled entry.
$key int|string$link string$text string$extra array public function _pagination_item($key, $link, $text, $extra)
{
$state = $this->_pagination_state($key, $extra);
if ($state === 'disabled') {
return '<li class="disabled"><span>' . $text . '</span></li>';
}
if ($state === 'active') {
return '<li class="active"><a href="' . $link . '" aria-current="page">' . $text . '</a></li>';
}
return '<li><a href="' . $link . '">' . $text . '</a></li>';
}
public function _pagination_state($key, $extra)
State of a pagination() item: 'disabled' for a key listed in $extra['disabled'] (page keys, 'prev', 'next'), 'active' for the key in $extra['current'], '' otherwise.
$key int|string$extra array public function _pagination_state($key, $extra)
{
if (in_array((string) $key, array_map('strval', (array) ($extra['disabled'] ?? [])), true)) {
return 'disabled';
}
return (isset($extra['current']) && (string) $key === (string) $extra['current']) ? 'active' : '';
}
public function _progress_bar_item($v, $extra)
[striped, animated, label (escaped unless no_escape)] of one progress_bar() bar: the bar keys only with B5_ITEM_OPTIONS, the striped / animated options of $extra always.
$v array$extra array public function _progress_bar_item($v, $extra)
{
$v = $this->B5_ITEM_OPTIONS ? (array) $v : [];
return [$v['striped'] ?? ($extra['striped'] ?? null), $v['animated'] ?? ($extra['animated'] ?? null), isset($v['label']) ? $this->_b4_text($v['label'], $v) : ''];
}
public function _recursive_sort_items($items = [], $skip_item_id = 0, $parent_id = 0)
Get and sort items ordered array (recursively).
public function _recursive_sort_items($items = [], $skip_item_id = 0, $parent_id = 0)
{
$children = [];
foreach ((array) $items as $id => $info) {
$parent_id = (int) $info['parent_id'];
if ($skip_item_id == $id) {
continue;
}
$children[$parent_id][$id] = $id;
}
$ids = $this->_count_levels($children, 0);
$new_items = [];
foreach ((array) $ids as $id => $level) {
$new_items[$id] = $items[$id] + ['level' => $level];
}
return $new_items;
}
public function _safe_href($url)
$url, or '#' when it is a javascript:, data: or vbscript: URL (entities decoded, whitespace and control characters dropped, as browsers read the scheme). The link options take trusted URLs; this only keeps a script URL out of them.
public function _safe_href($url)
{
$url = (string) $url;
$scheme = preg_replace('~[\x00-\x20\x7f]+~', '', html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
return preg_match('~^(?:javascript|data|vbscript):~i', $scheme) ? '#' : $url;
}
public function _sync_fw()
conf can change after _init() (demo and test pages); the components follow it as a() does.
public function _sync_fw()
{
(conf('css_framework') === 'bs5') !== (bool) $this->_is_bs5 && $this->_init();
}
public function _theme_attr($theme)
` data-bs-theme="light|dark"` of the B5 `theme` option (colour mode of one widget) under bs5; '' otherwise and for other values.
public function _theme_attr($theme)
{
return ($this->_is_bs5 && in_array($theme, ['light', 'dark'], true)) ? ' data-bs-theme="' . $theme . '"' : '';
}