chg: [herlper:bootstrap] Major refactor of the BootstrapHelper to make it more modular + added documentation
parent
d15f74698f
commit
2c840c7d4f
|
@ -50,28 +50,28 @@ $footerButtons[] = [
|
||||||
|
|
||||||
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
|
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
|
||||||
'fields' => [
|
'fields' => [
|
||||||
['key' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
['path' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
||||||
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
|
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
}],
|
}],
|
||||||
['key' => 'connector', 'label' => __('Tool Name'), 'formatter' => function($connector, $row) {
|
['path' => 'connector', 'label' => __('Tool Name'), 'formatter' => function($connector, $row) {
|
||||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||||
$this->Url->build(['controller' => 'localTools', 'action' => 'viewConnector', $connector['name']]),
|
$this->Url->build(['controller' => 'localTools', 'action' => 'viewConnector', $connector['name']]),
|
||||||
sprintf('%s (v%s)', h($connector['name']), h($connector['connector_version']))
|
sprintf('%s (v%s)', h($connector['name']), h($connector['connector_version']))
|
||||||
);
|
);
|
||||||
}],
|
}],
|
||||||
['key' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
['path' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
||||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||||
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
|
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
|
||||||
h($brood['name'])
|
h($brood['name'])
|
||||||
);
|
);
|
||||||
}],
|
}],
|
||||||
['key' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
['path' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
||||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||||
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
|
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
|
||||||
h($individual['email'])
|
h($individual['email'])
|
||||||
);
|
);
|
||||||
}],
|
}],
|
||||||
['key' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
['path' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
||||||
$html = '';
|
$html = '';
|
||||||
foreach ($alignments as $alignment) {
|
foreach ($alignments as $alignment) {
|
||||||
$html .= sprintf('<div class="text-nowrap"><b>%s</b> @ <a href="%s" target="_blank">%s</a></div>',
|
$html .= sprintf('<div class="text-nowrap"><b>%s</b> @ <a href="%s" target="_blank">%s</a></div>',
|
||||||
|
@ -101,7 +101,7 @@ $localToolHTML = $this->fetch('content', sprintf('<div class="d-none">%s</div><d
|
||||||
|
|
||||||
$requestData = $this->Bootstrap->collapse(
|
$requestData = $this->Bootstrap->collapse(
|
||||||
[
|
[
|
||||||
'title' => __('Inter-connection data'),
|
'text' => __('Inter-connection data'),
|
||||||
'open' => true,
|
'open' => true,
|
||||||
],
|
],
|
||||||
sprintf('<pre class="p-2 rounded mb-0" style="background: #eeeeee55;"><code>%s</code></pre>', json_encode($request['data'], JSON_PRETTY_PRINT))
|
sprintf('<pre class="p-2 rounded mb-0" style="background: #eeeeee55;"><code>%s</code></pre>', json_encode($request['data'], JSON_PRETTY_PRINT))
|
||||||
|
|
|
@ -52,8 +52,10 @@ echo $this->Bootstrap->modal([
|
||||||
'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['changed']]])
|
'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['changed']]])
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
'confirmText' => __('Acknowledge & Discard'),
|
'confirmButton' => [
|
||||||
'confirmIcon' => 'check',
|
'text' => __('Acknowledge & Discard'),
|
||||||
|
'icon' => 'check',
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,22 +40,22 @@ $tools = sprintf(
|
||||||
|
|
||||||
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
|
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
|
||||||
'fields' => [
|
'fields' => [
|
||||||
['key' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
['path' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
|
||||||
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
|
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
}],
|
}],
|
||||||
['key' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
['path' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
|
||||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||||
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
|
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
|
||||||
h($brood['name'])
|
h($brood['name'])
|
||||||
);
|
);
|
||||||
}],
|
}],
|
||||||
['key' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
['path' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
|
||||||
return sprintf('<a href="%s" target="_blank">%s</a>',
|
return sprintf('<a href="%s" target="_blank">%s</a>',
|
||||||
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
|
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
|
||||||
h($individual['email'])
|
h($individual['email'])
|
||||||
);
|
);
|
||||||
}],
|
}],
|
||||||
['key' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
['path' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
|
||||||
$html = '';
|
$html = '';
|
||||||
foreach ($alignments as $alignment) {
|
foreach ($alignments as $alignment) {
|
||||||
$html .= sprintf('<div class="text-nowrap"><b>%s</b> @ <a href="%s" target="_blank">%s</a></div>',
|
$html .= sprintf('<div class="text-nowrap"><b>%s</b> @ <a href="%s" target="_blank">%s</a></div>',
|
||||||
|
@ -71,7 +71,7 @@ $table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$requestData = $this->Bootstrap->collapse([
|
$requestData = $this->Bootstrap->collapse([
|
||||||
'title' => __('Message data'),
|
'text' => __('Message data'),
|
||||||
'open' => true,
|
'open' => true,
|
||||||
],
|
],
|
||||||
sprintf('<pre class="p-2 rounded mb-0" style="background: #eeeeee55;"><code>%s</code></pre>', json_encode($request['data']['sent'], JSON_PRETTY_PRINT))
|
sprintf('<pre class="p-2 rounded mb-0" style="background: #eeeeee55;"><code>%s</code></pre>', json_encode($request['data']['sent'], JSON_PRETTY_PRINT))
|
||||||
|
|
|
@ -60,9 +60,7 @@ class TagHelper extends Helper
|
||||||
'icon' => 'plus',
|
'icon' => 'plus',
|
||||||
'variant' => 'secondary',
|
'variant' => 'secondary',
|
||||||
'class' => ['badge'],
|
'class' => ['badge'],
|
||||||
'params' => [
|
'onclick' => 'createTagPicker(this)',
|
||||||
'onclick' => 'createTagPicker(this)',
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$html .= '<script>$(document).ready(function() { initSelect2Pickers() })</script>';
|
$html .= '<script>$(document).ready(function() { initSelect2Pickers() })</script>';
|
||||||
|
@ -111,22 +109,20 @@ class TagHelper extends Helper
|
||||||
'class' => ['ms-1', 'border-0', "text-${textColour}"],
|
'class' => ['ms-1', 'border-0', "text-${textColour}"],
|
||||||
'variant' => 'text',
|
'variant' => 'text',
|
||||||
'title' => __('Delete tag'),
|
'title' => __('Delete tag'),
|
||||||
'params' => [
|
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
|
||||||
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
|
$this->Url->build([
|
||||||
$this->Url->build([
|
'controller' => $this->getView()->getName(),
|
||||||
'controller' => $this->getView()->getName(),
|
'action' => 'untag',
|
||||||
'action' => 'untag',
|
$this->getView()->get('entity')['id']
|
||||||
$this->getView()->get('entity')['id']
|
]),
|
||||||
]),
|
h($tag['name'])
|
||||||
h($tag['name'])
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$deleteButton = '';
|
$deleteButton = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = $this->Bootstrap->genNode('span', [
|
$html = $this->Bootstrap->node('span', [
|
||||||
'class' => [
|
'class' => [
|
||||||
'tag',
|
'tag',
|
||||||
'badge',
|
'badge',
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an collapsible accordion component
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - stayOpen: Should collapsible components stay open when another one is opened
|
||||||
|
* - class: Additional classes to add to the main accordion container
|
||||||
|
* - content: Definition of the collapsible components. Must have at least the $body key set. See the "# Content" section for the options
|
||||||
|
*
|
||||||
|
* # Content:
|
||||||
|
* - class: Additional class to add to the body container
|
||||||
|
* - open: Should that collapsible element be opened by default
|
||||||
|
* - variant: The background variant to be applied to the body element
|
||||||
|
* - header: The definition of the interactive header. Accepts the following options:
|
||||||
|
* - variant: The bootstrap variant to apply on the header element
|
||||||
|
* - text: The text content of the header
|
||||||
|
* - html: The HTML content of the header
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->accordion(
|
||||||
|
* [
|
||||||
|
* 'stayOpen' => true,
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* [
|
||||||
|
* 'open' => true,
|
||||||
|
* 'header' => [
|
||||||
|
* 'variant' => 'danger',
|
||||||
|
* 'text' => 'nav 1',
|
||||||
|
* ],
|
||||||
|
* 'body' => '<b>body</b>',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'class' => ['opacity-50'],
|
||||||
|
* 'variant' => 'success',
|
||||||
|
* 'header' => [
|
||||||
|
* 'html' => '<i>nav 1</i>',
|
||||||
|
* ],
|
||||||
|
* 'body' => '<b>body</b>',
|
||||||
|
* ],
|
||||||
|
* ]
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
class BootstrapAccordion extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'stayOpen' => false,
|
||||||
|
'class' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options, $content, $btHelper)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [];
|
||||||
|
$this->content = $content;
|
||||||
|
$this->btHelper = $btHelper;
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
$this->seed = 'acc-' . mt_rand();
|
||||||
|
$this->contentSeeds = [];
|
||||||
|
foreach ($this->content as $accordionItem) {
|
||||||
|
$this->contentSeeds[] = mt_rand();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->content as $i => $item) {
|
||||||
|
$this->content[$i]['class'] = $this->convertToArrayIfNeeded($item['class'] ?? []);
|
||||||
|
$this->content[$i]['header']['class'] = $this->convertToArrayIfNeeded($item['header']['class'] ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function accordion()
|
||||||
|
{
|
||||||
|
return $this->genAccordion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genHeader($accordionItem, $i)
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('h2', [
|
||||||
|
'class' => ['accordion-header'],
|
||||||
|
'id' => 'head-' . $this->contentSeeds[$i]
|
||||||
|
]);
|
||||||
|
$content = $accordionItem['header']['html'] ?? h($accordionItem['header']['text']);
|
||||||
|
$buttonOptions = [
|
||||||
|
'class' => array_merge(
|
||||||
|
[
|
||||||
|
'accordion-button',
|
||||||
|
empty($accordionItem['open']) ? 'collapsed' : '',
|
||||||
|
self::getBGAndTextClassForVariant($accordionItem['header']['variant'] ?? ''),
|
||||||
|
],
|
||||||
|
$accordionItem['header']['class'],
|
||||||
|
),
|
||||||
|
'type' => 'button',
|
||||||
|
'data-bs-toggle' => 'collapse',
|
||||||
|
'data-bs-target' => '#body-' . $this->contentSeeds[$i],
|
||||||
|
'aria-expanded' => 'false',
|
||||||
|
'aria-controls' => 'body-' . $this->contentSeeds[$i],
|
||||||
|
];
|
||||||
|
$html .= $this->node('button', $buttonOptions, $content);
|
||||||
|
$html .= $this->nodeClose(('h2'));
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genBody($accordionItem, $i)
|
||||||
|
{
|
||||||
|
$content = $this->node('div', [
|
||||||
|
'class' => ['accordion-body']
|
||||||
|
], $accordionItem['body']);
|
||||||
|
$divOptions = [
|
||||||
|
'class' => array_merge(
|
||||||
|
[
|
||||||
|
'accordion-collapse collapse',
|
||||||
|
empty($accordionItem['open']) ? '' : 'show',
|
||||||
|
self::getBGAndTextClassForVariant($accordionItem['variant'] ?? ''),
|
||||||
|
],
|
||||||
|
$accordionItem['class'],
|
||||||
|
),
|
||||||
|
'id' => 'body-' . $this->contentSeeds[$i],
|
||||||
|
'aria-labelledby' => 'head-' . $this->contentSeeds[$i],
|
||||||
|
];
|
||||||
|
if (empty($this->options['stayOpen'])) {
|
||||||
|
$divOptions['data-bs-parent'] = '#' . $this->seed;
|
||||||
|
}
|
||||||
|
$html = $this->node('div', $divOptions, $content);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genAccordion()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('div', [
|
||||||
|
'class' => array_merge(['accordion'], $this->options['class']),
|
||||||
|
'id' => $this->seed
|
||||||
|
]);
|
||||||
|
foreach ($this->content as $i => $accordionItem) {
|
||||||
|
$html .= $this->nodeOpen('div', [
|
||||||
|
'class' => array_merge(['accordion-item'])
|
||||||
|
]);
|
||||||
|
$html .= $this->genHeader($accordionItem, $i);
|
||||||
|
$html .= $this->genBody($accordionItem, $i);
|
||||||
|
$html .= $this->nodeClose('div');
|
||||||
|
}
|
||||||
|
$html .= $this->nodeClose('div');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bootstrap alert
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - text: The text content of the alert
|
||||||
|
* - html: The HTML content of the alert
|
||||||
|
* - dismissible: Can the alert be dissmissed
|
||||||
|
* - variant: The Bootstrap variant of the alert
|
||||||
|
* - fade: Should the alert fade when dismissed
|
||||||
|
* - class: Additional classes to add to the alert container
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->alert([
|
||||||
|
* 'text' => 'This is an alert',
|
||||||
|
* 'dismissible' => false,
|
||||||
|
* 'variant' => 'warning',
|
||||||
|
* 'fade' => false,
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapAlert extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'text' => '',
|
||||||
|
'html' => null,
|
||||||
|
'dismissible' => true,
|
||||||
|
'variant' => 'primary',
|
||||||
|
'fade' => true,
|
||||||
|
'class' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => BootstrapGeneric::$variants,
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alert()
|
||||||
|
{
|
||||||
|
return $this->genAlert();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genAlert()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('div', [
|
||||||
|
'class' => array_merge([
|
||||||
|
'alert',
|
||||||
|
"alert-{$this->options['variant']}",
|
||||||
|
$this->options['dismissible'] ? 'alert-dismissible' : '',
|
||||||
|
$this->options['fade'] ? 'fade show' : '',
|
||||||
|
], $this->options['class']),
|
||||||
|
'role' => "alert"
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html .= $this->options['html'] ?? h($this->options['text']);
|
||||||
|
$html .= $this->genCloseButton();
|
||||||
|
$html .= $this->nodeClose('div');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genCloseButton()
|
||||||
|
{
|
||||||
|
$html = '';
|
||||||
|
if ($this->options['dismissible']) {
|
||||||
|
$html .= $this->genericCloseButton('alert');
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Bootstrap badge
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - text: The text content of the badge
|
||||||
|
* - html: The HTML content of the badge
|
||||||
|
* - variant: The Bootstrap variant of the badge
|
||||||
|
* - pill: Should the badge have a Bootstrap pill style
|
||||||
|
* - title: The title of the badge
|
||||||
|
* - class: Additional class to add to the button
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* echo $this->Bootstrap->badge([
|
||||||
|
* 'text' => 'text',
|
||||||
|
* 'variant' => 'success',
|
||||||
|
* 'pill' => false,
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapBadge extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'text' => '',
|
||||||
|
'html' => null,
|
||||||
|
'variant' => 'primary',
|
||||||
|
'pill' => false,
|
||||||
|
'title' => '',
|
||||||
|
'class' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => BootstrapGeneric::$variants,
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function badge()
|
||||||
|
{
|
||||||
|
return $this->genBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genBadge()
|
||||||
|
{
|
||||||
|
$html = $this->node('span', [
|
||||||
|
'class' => array_merge($this->options['class'], [
|
||||||
|
'ms-1',
|
||||||
|
'badge',
|
||||||
|
self::getBGAndTextClassForVariant($this->options['variant']),
|
||||||
|
$this->options['pill'] ? 'rounded-pill' : '',
|
||||||
|
]),
|
||||||
|
'title' => $this->options['title']
|
||||||
|
], $this->options['html'] ?? h($this->options['text']));
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bootstrap button
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - text: The text content of the button
|
||||||
|
* - html: The HTML content of the button
|
||||||
|
* - variant: The Bootstrap variant of the button
|
||||||
|
* - outline: Should the button be outlined
|
||||||
|
* - size: The size of the button. Accepts 'xs', 'sm', 'lg'. Leave empty for normal size
|
||||||
|
* - icon: Should the button have an icon right before the text
|
||||||
|
* - image: Should the button have an image in place of an icon right before the text
|
||||||
|
* - class: Additional class to add to the button
|
||||||
|
* - type: The HTML type of the button for forms. Accepts: 'button' (default), 'submit', and 'reset'
|
||||||
|
* - nodeType: Allow to use a different HTML tag than 'button'
|
||||||
|
* - title: The button title
|
||||||
|
* - Badge: Should the button have a badge. Accepts a \BootstrapElement\BootstrapBadge configuration object
|
||||||
|
* - onclick: Shorthand to add a onclick listener function
|
||||||
|
* - attrs: Additional HTML attributes
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->button([
|
||||||
|
* 'text' => 'Press me!',
|
||||||
|
* 'variant' => 'warning',
|
||||||
|
* 'icon' => 'exclamation-triangle',
|
||||||
|
* 'onclick' => 'alert(1)',
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapButton extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'id' => '',
|
||||||
|
'text' => '',
|
||||||
|
'html' => null,
|
||||||
|
'variant' => 'primary',
|
||||||
|
'outline' => false,
|
||||||
|
'size' => '',
|
||||||
|
'icon' => null,
|
||||||
|
'image' => null,
|
||||||
|
'class' => [],
|
||||||
|
'type' => 'button',
|
||||||
|
'nodeType' => 'button',
|
||||||
|
'title' => '',
|
||||||
|
'badge' => false,
|
||||||
|
'onclick' => false,
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
private $bsClasses = [];
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
|
||||||
|
'size' => ['', 'xs', 'sm', 'lg'],
|
||||||
|
'type' => ['button', 'submit', 'reset']
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
|
||||||
|
if (!empty($this->options['id'])) {
|
||||||
|
$this->options['attrs']['id'] = $this->options['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bsClasses[] = 'btn';
|
||||||
|
if ($this->options['outline']) {
|
||||||
|
$this->bsClasses[] = "btn-outline-{$this->options['variant']}";
|
||||||
|
} else {
|
||||||
|
$this->bsClasses[] = "btn-{$this->options['variant']}";
|
||||||
|
}
|
||||||
|
if (!empty($this->options['size'])) {
|
||||||
|
$this->bsClasses[] = "btn-{$this->options['size']}";
|
||||||
|
}
|
||||||
|
if ($this->options['variant'] == 'text') {
|
||||||
|
$this->bsClasses[] = 'p-0';
|
||||||
|
$this->bsClasses[] = 'lh-1';
|
||||||
|
}
|
||||||
|
if (!empty($this->options['onclick'])) {
|
||||||
|
$this->options['attrs']['onclick'] = $this->options['onclick'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function button()
|
||||||
|
{
|
||||||
|
return $this->genButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genButton()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen($this->options['nodeType'], array_merge($this->options['attrs'], [
|
||||||
|
'class' => array_merge($this->options['class'], $this->bsClasses),
|
||||||
|
'role' => "alert",
|
||||||
|
'type' => $this->options['type'],
|
||||||
|
'title' => h($this->options['title']),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$html .= $this->genIcon();
|
||||||
|
$html .= $this->genImage();
|
||||||
|
$html .= $this->options['html'] ?? h($this->options['text']);
|
||||||
|
if (!empty($this->options['badge'])) {
|
||||||
|
$bsBadge = new BootstrapBadge($this->options['badge']);
|
||||||
|
$html .= $bsBadge->badge();
|
||||||
|
}
|
||||||
|
$html .= $this->nodeClose($this->options['nodeType']);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genIcon()
|
||||||
|
{
|
||||||
|
if (!empty($this->options['icon'])) {
|
||||||
|
$bsIcon = new BootstrapIcon($this->options['icon'], [
|
||||||
|
'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
|
||||||
|
]);
|
||||||
|
return $bsIcon->icon();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genImage()
|
||||||
|
{
|
||||||
|
if (!empty($this->options['image'])) {
|
||||||
|
return $this->node('img', [
|
||||||
|
'src' => $this->options['image']['path'] ?? '',
|
||||||
|
'class' => ['img-fluid', 'me-1'],
|
||||||
|
'width' => '26',
|
||||||
|
'height' => '26',
|
||||||
|
'alt' => $this->options['image']['alt'] ?? ''
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Bootstrap card with the given options
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - headerText, bodyText, footerText: The text for the mentioned card component
|
||||||
|
* - headerHTML, bodyHTML, footerHtml: The HTML for the mentioned card component
|
||||||
|
* - class: A list of additional class to be added to the main container
|
||||||
|
* - headerVariant, bodyVariant, footerVariant: The variant for the mentioned card component
|
||||||
|
* - headerClass, bodyClass, footerClass: A list of additional class to be added to the main container
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->card([
|
||||||
|
* 'headerText' => 'header',
|
||||||
|
* 'bodyHTML' => '<i>body</i>',
|
||||||
|
* 'footerText' => 'footer',
|
||||||
|
* 'headerVariant' => 'warning',
|
||||||
|
* 'footerVariant' => 'dark',
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
class BootstrapCard extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'headerText' => '',
|
||||||
|
'bodyText' => '',
|
||||||
|
'footerText' => '',
|
||||||
|
'headerHTML' => null,
|
||||||
|
'bodyHTML' => null,
|
||||||
|
'footerHTML' => null,
|
||||||
|
'class' => [],
|
||||||
|
'headerVariant' => '',
|
||||||
|
'bodyVariant' => '',
|
||||||
|
'footerVariant' => '',
|
||||||
|
'headerClass' => '',
|
||||||
|
'bodyClass' => '',
|
||||||
|
'footerClass' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'headerVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
'bodyVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
'footerVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['headerClass'] = $this->convertToArrayIfNeeded($this->options['headerClass']);
|
||||||
|
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||||
|
$this->options['footerClass'] = $this->convertToArrayIfNeeded($this->options['footerClass']);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
$this->options['borderVariant'] = !empty($this->options['headerVariant']) ? "border-{$this->options['headerVariant']}" : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function card()
|
||||||
|
{
|
||||||
|
return $this->genCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genCard()
|
||||||
|
{
|
||||||
|
$card = $this->node('div', [
|
||||||
|
'class' => array_merge(
|
||||||
|
[
|
||||||
|
'card',
|
||||||
|
$this->options['borderVariant'],
|
||||||
|
],
|
||||||
|
$this->options['class']
|
||||||
|
),
|
||||||
|
], implode('', [$this->genHeader(), $this->genBody(), $this->genFooter()]));
|
||||||
|
return $card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genHeader()
|
||||||
|
{
|
||||||
|
if (empty($this->options['headerHTML']) && empty($this->options['headerText'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$content = $this->options['headerHTML'] ?? h($this->options['headerText']);
|
||||||
|
$header = $this->node('div', [
|
||||||
|
'class' => array_merge(
|
||||||
|
[
|
||||||
|
'card-header',
|
||||||
|
self::getBGAndTextClassForVariant($this->options['headerVariant']),
|
||||||
|
],
|
||||||
|
$this->options['headerClass']
|
||||||
|
),
|
||||||
|
], $content);
|
||||||
|
return $header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genBody()
|
||||||
|
{
|
||||||
|
if (empty($this->options['bodyHTML']) && empty($this->options['bodyText'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$content = $this->options['bodyHTML'] ?? h($this->options['bodyText']);
|
||||||
|
$body = $this->node('div', [
|
||||||
|
'class' => array_merge(
|
||||||
|
[
|
||||||
|
'card-body',
|
||||||
|
self::getBGAndTextClassForVariant($this->options['bodyVariant']),
|
||||||
|
],
|
||||||
|
$this->options['bodyClass']
|
||||||
|
)
|
||||||
|
], $content);
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genFooter()
|
||||||
|
{
|
||||||
|
if (empty($this->options['footerHTML']) && empty($this->options['footerText'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$content = $this->options['footerHTML'] ?? h($this->options['footerText']);
|
||||||
|
$footer = $this->node('div', [
|
||||||
|
'class' => array_merge([
|
||||||
|
'card-footer',
|
||||||
|
self::getBGAndTextClassForVariant($this->options['footerVariant']),
|
||||||
|
],
|
||||||
|
$this->options['footerClass']
|
||||||
|
)
|
||||||
|
], $content);
|
||||||
|
return $footer;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use Cake\Utility\Security;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Bootstrap collapsible component
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - text: The text of the control element
|
||||||
|
* - html: The HTML content of the control element
|
||||||
|
* - open: Should the collapsible element be opened by default
|
||||||
|
* - horizontal: Should the collapsible be revealed from the side
|
||||||
|
* - class: List of additional classes to be added to the main container
|
||||||
|
* - id: Optional ID to link the collapsible element with its control button
|
||||||
|
* - button: Configuration object to make the control element into a button. Accepts BootstrapElements\BootstrapButton parameters
|
||||||
|
* - card: Configuration object to adjust the content container based on configuration. Accepts BootstrapElements\BootstrapCard parameters
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->collapse([
|
||||||
|
* 'button' => [
|
||||||
|
* 'text' => 'Open sesame',
|
||||||
|
* 'variant' => 'success',
|
||||||
|
* ],
|
||||||
|
* 'card' => [
|
||||||
|
* 'bodyClass' => 'p-2 rounded-3',
|
||||||
|
* 'bodyVariant' => 'secondary',
|
||||||
|
* ]
|
||||||
|
* ], '<i>content</i>');
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BootstrapCollapse extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'text' => '',
|
||||||
|
'html' => null,
|
||||||
|
'open' => false,
|
||||||
|
'horizontal' => false,
|
||||||
|
'class' => [],
|
||||||
|
'button' => [],
|
||||||
|
'card' => [],
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options, $content, $btHelper)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [];
|
||||||
|
$this->processOptions($options);
|
||||||
|
$this->content = $content;
|
||||||
|
$this->btHelper = $btHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
$this->options['class'][] = 'collapse';
|
||||||
|
if (!empty($this->options['horizontal'])) {
|
||||||
|
$this->options['class'][] = 'collapse-horizontal';
|
||||||
|
}
|
||||||
|
if ($this->options['open']) {
|
||||||
|
$this->options['class'][] = 'show';
|
||||||
|
}
|
||||||
|
if (empty($this->options['card']['bodyClass'])) {
|
||||||
|
$this->options['card']['bodyClass'] = ['p-0'];
|
||||||
|
}
|
||||||
|
if (empty($this->options['id'])) {
|
||||||
|
$this->options['id'] = 'c-' . Security::randomString(8);
|
||||||
|
}
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collapse()
|
||||||
|
{
|
||||||
|
return $this->genCollapse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genControl()
|
||||||
|
{
|
||||||
|
$attrsConfig = [
|
||||||
|
'data-bs-toggle' => 'collapse',
|
||||||
|
'role' => 'button',
|
||||||
|
'aria-expanded' => 'false',
|
||||||
|
'aria-controls' => $this->options['id'],
|
||||||
|
'href' => '#' . $this->options['id'],
|
||||||
|
];
|
||||||
|
$html = '';
|
||||||
|
if (!empty($this->options['button'])) {
|
||||||
|
$btnConfig = array_merge($this->options['button'], ['attrs' => $attrsConfig]);
|
||||||
|
$html = $this->btHelper->button($btnConfig);
|
||||||
|
} else {
|
||||||
|
$nodeConfig = [
|
||||||
|
'class' => ['text-decoration-none'],
|
||||||
|
];
|
||||||
|
$nodeConfig = array_merge($nodeConfig, $attrsConfig);
|
||||||
|
$html = $this->node('a', $nodeConfig, $this->options['html'] ?? h($this->options['text']));
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genContent()
|
||||||
|
{
|
||||||
|
$cardConfig = $this->options['card'];
|
||||||
|
$cardConfig['bodyHTML'] = $this->content;
|
||||||
|
$content = $this->btHelper->card($cardConfig);
|
||||||
|
$container = $this->node('div', [
|
||||||
|
'class' => $this->options['class'],
|
||||||
|
'id' => $this->options['id'],
|
||||||
|
], $content);
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genCollapse()
|
||||||
|
{
|
||||||
|
return $this->genControl() . $this->genContent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Options
|
||||||
|
* - dropdown-class: Class for the dropdown
|
||||||
|
* - alignment: How should the dropdown be aligned. Valid: "start", "end"
|
||||||
|
* - direction: Position where the dropdown will be displayed Valid: "start", "end", "up", "down"
|
||||||
|
* - button: Configuration for the dropdown button to be passed to BootstrapElements\BootstrapButton
|
||||||
|
* - submenu_alignment: Alignment of the child dropdown will be displayed Valid: "start", "end", "up", "down"
|
||||||
|
* - submenu_direction: Position where the child dropdown will be displayed Valid: "start", "end", "up", "down"
|
||||||
|
* - attrs: Additional HTML attributes tro be applied on the dropdown container
|
||||||
|
* - menu: Entries making the dropdown menu. Accept the following options:
|
||||||
|
* - text: Text of the entry
|
||||||
|
* - html: HTML of the entry
|
||||||
|
* - icon: Icon displayed before the text
|
||||||
|
* - badge: Badge displayed after the text. Accepts BootstrapElements\BootstrapBadge
|
||||||
|
* - header: Is this item a list header
|
||||||
|
* - keepOpen: Keep the dropdown open if this entry is clicked
|
||||||
|
* - sup: Additional text to be added as a <sup> element
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->dropdownMenu([
|
||||||
|
* 'dropdown-class' => 'ms-1',
|
||||||
|
* 'alignment' => 'end',
|
||||||
|
* 'direction' => 'down',
|
||||||
|
* 'button' => [
|
||||||
|
* 'icon' => 'sliders-h',
|
||||||
|
* 'variant' => 'primary',
|
||||||
|
* ],
|
||||||
|
* 'submenu_alignment' => 'end',
|
||||||
|
* 'submenu_direction' => 'end',
|
||||||
|
* 'attrs' => [],
|
||||||
|
* 'menu' => [
|
||||||
|
* [
|
||||||
|
* 'text' => __('Eye'),
|
||||||
|
* 'icon' => 'eye-slash',
|
||||||
|
* 'keepOpen' => true,
|
||||||
|
* 'menu' => [
|
||||||
|
* ['header' => true, 'text' => 'nested menu'],
|
||||||
|
* ['text' => 'item 1'],
|
||||||
|
* ['text' => 'item 2', 'sup' => 'v1'],
|
||||||
|
* ],
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'html' => '<i class="p-3">html item</i>',
|
||||||
|
* ],
|
||||||
|
* ]
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BootstrapDropdownMenu extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'dropdown-class' => [],
|
||||||
|
'alignment' => 'start',
|
||||||
|
'direction' => 'end',
|
||||||
|
'button' => [],
|
||||||
|
'menu' => [],
|
||||||
|
'submenu_direction' => 'end',
|
||||||
|
'submenu_classes' => [],
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options, $btHelper)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'direction' => ['start', 'end', 'up', 'down'],
|
||||||
|
'alignment' => ['start', 'end'],
|
||||||
|
'submenu_direction' => ['start', 'end', 'up', 'down'],
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
$this->menu = $this->options['menu'];
|
||||||
|
$this->btHelper = $btHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['dropdown-class'] = $this->convertToArrayIfNeeded($this->options['dropdown-class']);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropdownMenu()
|
||||||
|
{
|
||||||
|
return $this->fullDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fullDropdown()
|
||||||
|
{
|
||||||
|
return $this->genDropdownWrapper($this->genDropdownToggleButton(), $this->genDropdownMenu($this->menu));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function genDropdownWrapper($toggle = '', $menu = '', $direction = null, $classes = null)
|
||||||
|
{
|
||||||
|
$classes = !is_null($classes) ? $classes : $this->options['dropdown-class'];
|
||||||
|
$direction = !is_null($direction) ? $direction : $this->options['direction'];
|
||||||
|
$content = $toggle . $menu;
|
||||||
|
$html = $this->node('div', array_merge(
|
||||||
|
$this->options['attrs'],
|
||||||
|
[
|
||||||
|
'class' => array_merge(
|
||||||
|
$classes,
|
||||||
|
[
|
||||||
|
'dropdown',
|
||||||
|
"drop{$direction}"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
), $content);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function genDropdownToggleButton()
|
||||||
|
{
|
||||||
|
$defaultOptions = [
|
||||||
|
'class' => ['dropdown-toggle'],
|
||||||
|
'attrs' => [
|
||||||
|
'data-bs-toggle' => 'dropdown',
|
||||||
|
'aria-expanded' => 'false',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$options = array_merge_recursive($this->options['button'], $defaultOptions);
|
||||||
|
return $this->btHelper->button($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genDropdownMenu($entries, $alignment = null)
|
||||||
|
{
|
||||||
|
$alignment = !is_null($alignment) ? $alignment : $this->options['alignment'];
|
||||||
|
$html = $this->node('div', [
|
||||||
|
'class' => ['dropdown-menu', "dropdown-menu-{$alignment}"],
|
||||||
|
], $this->genEntries($entries));
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genEntries($entries)
|
||||||
|
{
|
||||||
|
$html = '';
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
$link = $this->genEntry($entry);
|
||||||
|
if (!empty($entry['menu'])) {
|
||||||
|
$html .= $this->genDropdownWrapper($link, $this->genDropdownMenu($entry['menu']), $this->options['submenu_direction'], $this->options['submenu_classes']);
|
||||||
|
} else {
|
||||||
|
$html .= $link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genEntry($entry)
|
||||||
|
{
|
||||||
|
if (!empty($entry['html'])) {
|
||||||
|
return $entry['html'];
|
||||||
|
}
|
||||||
|
$classes = [];
|
||||||
|
$icon = '';
|
||||||
|
if (!empty($entry['icon'])) {
|
||||||
|
$icon = $this->btHelper->icon($entry['icon'], ['class' => 'me-2']);
|
||||||
|
}
|
||||||
|
$badge = '';
|
||||||
|
if (!empty($entry['badge'])) {
|
||||||
|
$bsBadge = new BootstrapBadge(array_merge(
|
||||||
|
['class' => ['ms-auto']],
|
||||||
|
$entry['badge']
|
||||||
|
));
|
||||||
|
$badge = $bsBadge->badge();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($entry['header'])) {
|
||||||
|
return $this->node('h6', [
|
||||||
|
'class' => ['dropdown-header',],
|
||||||
|
], $icon . h($entry['text']) . $badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
$classes = ['dropdown-item'];
|
||||||
|
$params = ['href' => '#'];
|
||||||
|
|
||||||
|
if (!empty($entry['menu'])) {
|
||||||
|
$classes[] = 'dropdown-toggle';
|
||||||
|
$classes[] = 'd-flex align-items-center';
|
||||||
|
$params['data-bs-toggle'] = 'dropdown';
|
||||||
|
$params['aria-haspopup'] = 'true';
|
||||||
|
$params['aria-expanded'] = 'false';
|
||||||
|
if (!empty($entry['keepOpen'])) {
|
||||||
|
$classes[] = 'open-form';
|
||||||
|
}
|
||||||
|
$params['data-open-form-id'] = mt_rand();
|
||||||
|
}
|
||||||
|
|
||||||
|
$labelContent = sprintf(
|
||||||
|
'%s%s',
|
||||||
|
h($entry['text']),
|
||||||
|
!empty($entry['sup']) ? $this->node('sup', ['class' => 'ms-1 text-muted'], $entry['sup']) : ''
|
||||||
|
);
|
||||||
|
$label = $this->node('span', ['class' => 'mx-1'], $labelContent);
|
||||||
|
$content = $icon . $label . $badge;
|
||||||
|
|
||||||
|
return $this->node('a', array_merge([
|
||||||
|
'class' => $classes,
|
||||||
|
], $params), $content);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an icon relying on the FontAwesome library.
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - class: Additional classes to add
|
||||||
|
* - title: A title to add to the icon
|
||||||
|
* - attrs: Additional HTML parameters to add
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->icon('eye-slash', [
|
||||||
|
* 'class' => 'm-3',
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapIcon extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $icon = '';
|
||||||
|
private $defaultOptions = [
|
||||||
|
'class' => [],
|
||||||
|
'title' => '',
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($icon, $options = [])
|
||||||
|
{
|
||||||
|
$this->icon = $icon;
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function icon()
|
||||||
|
{
|
||||||
|
return $this->genIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genIcon()
|
||||||
|
{
|
||||||
|
$html = $this->node('span', array_merge(
|
||||||
|
[
|
||||||
|
'class' => array_merge(
|
||||||
|
$this->options['class'],
|
||||||
|
["fa fa-{$this->icon}"]
|
||||||
|
),
|
||||||
|
'title' => h($this->options['title'])
|
||||||
|
],
|
||||||
|
$this->options['attrs']
|
||||||
|
));
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Options for list container
|
||||||
|
* - class: A list of class
|
||||||
|
* - attrs: A list of additional HTML attributes
|
||||||
|
*
|
||||||
|
* # Options for list items
|
||||||
|
* - href: Link location
|
||||||
|
* - text: Text content of the item
|
||||||
|
* - html: Html content of the item
|
||||||
|
* - class: A list of class
|
||||||
|
* - attrs: A list of additional HTML attributes
|
||||||
|
* - badge: Options to be passed to BootstrapElements\BootstrapBadge
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* $this->Bootstrap->listGroup(
|
||||||
|
* [
|
||||||
|
* [
|
||||||
|
* 'text' => 'test',
|
||||||
|
* 'badge' => [
|
||||||
|
* 'text' => 'test',
|
||||||
|
* 'variant' => 'warning'
|
||||||
|
* ],
|
||||||
|
* 'attrs' => [
|
||||||
|
* 'data-test' => 'tes'
|
||||||
|
* ]
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'html' => '<i>test2</i>',
|
||||||
|
* ],
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'class' => 'container-class'
|
||||||
|
* ]
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
class BootstrapListGroup extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'class' => [],
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
private $defaultItemOptions = [
|
||||||
|
'href' => '#',
|
||||||
|
'text' => '',
|
||||||
|
'html' => null,
|
||||||
|
'badge' => '',
|
||||||
|
'class' => [],
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $defaultClasses = ['list-group',];
|
||||||
|
private static $defaultItemClasses = ['list-group-item', 'list-group-item-action', 'd-flex', 'align-items-start', 'justify-content-between'];
|
||||||
|
|
||||||
|
function __construct($items, $options, $btHelper)
|
||||||
|
{
|
||||||
|
$this->items = $items;
|
||||||
|
$this->processOptions($options);
|
||||||
|
$this->btHelper = $btHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listGroup()
|
||||||
|
{
|
||||||
|
return $this->genListGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genListGroup()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('div', array_merge([
|
||||||
|
'class' => array_merge(self::$defaultClasses, $this->options['class']),
|
||||||
|
], $this->options['attrs']));
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
$html .= $this->genItem($item);
|
||||||
|
}
|
||||||
|
$html .= $this->nodeClose('div');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genItem($item)
|
||||||
|
{
|
||||||
|
$item['class'] = !is_array($item['class']) ? [$item['class']] : $item['class'];
|
||||||
|
$itemOptions = array_merge($this->defaultItemOptions, $item);
|
||||||
|
$itemOptions['class'] = array_merge(self::$defaultItemClasses, $itemOptions['class']);
|
||||||
|
|
||||||
|
$html = $this->node('a',
|
||||||
|
array_merge([
|
||||||
|
'class' => array_merge(self::$defaultItemClasses, $itemOptions['class']),
|
||||||
|
'href' => '#',
|
||||||
|
], $itemOptions['attrs']),
|
||||||
|
[
|
||||||
|
!is_null($itemOptions['html']) ? $this->node('div', ['class' => 'w-100'], $itemOptions['html']) : h($itemOptions['text']),
|
||||||
|
$this->genBadge($itemOptions['badge'])
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genBadge($badge)
|
||||||
|
{
|
||||||
|
if (empty($badge)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $this->btHelper->badge($badge);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use Cake\Utility\Hash;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a list looking like a table from 1-dimensional data $item.
|
||||||
|
* Perfect to display the Key-Values of an object.
|
||||||
|
*
|
||||||
|
* # Options for table
|
||||||
|
* - striped, bordered, borderless, hover, small: Default bootstrap behavior
|
||||||
|
* - variant: Variant to apply on the entire table
|
||||||
|
* - tableClass: A list of class to add on the table container
|
||||||
|
* - bodyClass: A list of class to add on the tbody container
|
||||||
|
* - id: The ID to use for the table
|
||||||
|
* - caption: Optional table caption
|
||||||
|
* - elementsRootPath: Root path to use when item are relying on cakephp's element. See options for fields
|
||||||
|
*
|
||||||
|
* # Items
|
||||||
|
* - They have the content that's used to generate the table. Typically and array<array> or array<entity>
|
||||||
|
*
|
||||||
|
* # Options for fields
|
||||||
|
* - key: The name of the field to be displayed as a label
|
||||||
|
* - keyHtml: The HTML of the field to be displayed as a label
|
||||||
|
* - path: The path to be fed to Hash::get() in order to get the value from the $item
|
||||||
|
* - raw: The raw value to be displayed. Disable the `path` option
|
||||||
|
* - rawNoEscaping: If the raw value should not be escaped. False by default
|
||||||
|
* - type: The type of element to use combined with $elementsRootPath from the table's option
|
||||||
|
* - formatter: A callback function to format the value
|
||||||
|
* - cellVariant: The bootstrap variant to be applied on the cell
|
||||||
|
* - rowVariant: The bootstrap variant to be applied on the row
|
||||||
|
* - notice_$variant: A text with the passed variant to be append at the end
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->listTable(
|
||||||
|
* [
|
||||||
|
* 'hover' => false,
|
||||||
|
* 'variant' => 'success',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'item' => [
|
||||||
|
* 'key1' => 'value1',
|
||||||
|
* 'key2' => true,
|
||||||
|
* 'key3' => 'value3',
|
||||||
|
* ],
|
||||||
|
* 'fields' => [
|
||||||
|
* [
|
||||||
|
* 'key' => 'Label 1',
|
||||||
|
* 'path' => 'key1',
|
||||||
|
* 'notice_warning' => '::warning::',
|
||||||
|
* 'notice_danger' => '::danger::',
|
||||||
|
* 'rowVariant' => 'danger',
|
||||||
|
* 'cellVariant' => 'success',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'key' => 'Label 2',
|
||||||
|
* 'path' => 'key2',
|
||||||
|
* 'type' => 'boolean',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'key' => 'Label 3',
|
||||||
|
* 'raw' => '<b>raw_value</b>',
|
||||||
|
* 'rawNoEscaping' => true,
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'key' => 'Label 4',
|
||||||
|
* 'path' => 'key3',
|
||||||
|
* 'formatter' => function ($value) {
|
||||||
|
* return '<i>' . $value . '</i>';
|
||||||
|
* },
|
||||||
|
* ],
|
||||||
|
* ],
|
||||||
|
* 'caption' => 'This is a caption'
|
||||||
|
* ]
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
class BootstrapListTable extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'striped' => true,
|
||||||
|
'bordered' => false,
|
||||||
|
'borderless' => false,
|
||||||
|
'hover' => true,
|
||||||
|
'small' => false,
|
||||||
|
'variant' => '',
|
||||||
|
'tableClass' => [],
|
||||||
|
'bodyClass' => [],
|
||||||
|
'id' => '',
|
||||||
|
'caption' => '',
|
||||||
|
'elementsRootPath' => '/genericElements/SingleViews/Fields/',
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options, $data, $btHelper)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => array_merge(BootstrapGeneric::$variants, [''])
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
$this->fields = $data['fields'];
|
||||||
|
$this->item = $data['item'];
|
||||||
|
$this->caption = !empty($data['caption']) ? $data['caption'] : '';
|
||||||
|
$this->btHelper = $btHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->options['tableClass'] = $this->convertToArrayIfNeeded($this->options['tableClass']);
|
||||||
|
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table()
|
||||||
|
{
|
||||||
|
return $this->genTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genTable()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('table', [
|
||||||
|
'class' => [
|
||||||
|
'table',
|
||||||
|
"table-{$this->options['variant']}",
|
||||||
|
$this->options['striped'] ? 'table-striped' : '',
|
||||||
|
$this->options['bordered'] ? 'table-bordered' : '',
|
||||||
|
$this->options['borderless'] ? 'table-borderless' : '',
|
||||||
|
$this->options['hover'] ? 'table-hover' : '',
|
||||||
|
$this->options['small'] ? 'table-sm' : '',
|
||||||
|
implode(' ', $this->options['tableClass']),
|
||||||
|
!empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
|
||||||
|
],
|
||||||
|
'id' => $this->options['id'] ?? ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html .= $this->genCaption();
|
||||||
|
$html .= $this->genBody();
|
||||||
|
|
||||||
|
$html .= $this->nodeClose('table');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genBody()
|
||||||
|
{
|
||||||
|
$body = $this->nodeOpen('tbody', [
|
||||||
|
'class' => $this->options['bodyClass'],
|
||||||
|
]);
|
||||||
|
foreach ($this->fields as $i => $field) {
|
||||||
|
$body .= $this->genRow($field);
|
||||||
|
}
|
||||||
|
$body .= $this->nodeClose('tbody');
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genRow($field)
|
||||||
|
{
|
||||||
|
$rowValue = $this->genCell($field);
|
||||||
|
$rowKey = $this->node('th', [
|
||||||
|
'class' => [
|
||||||
|
'col-4 col-sm-2'
|
||||||
|
],
|
||||||
|
'scope' => 'row'
|
||||||
|
], $field['keyHtml'] ?? h($field['key']));
|
||||||
|
$row = $this->node('tr', [
|
||||||
|
'class' => [
|
||||||
|
'd-flex',
|
||||||
|
!empty($field['rowVariant']) ? "table-{$field['rowVariant']}" : ''
|
||||||
|
]
|
||||||
|
], [$rowKey, $rowValue]);
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genCell($field = [])
|
||||||
|
{
|
||||||
|
if (isset($field['raw'])) {
|
||||||
|
$cellContent = !empty($field['rawNoEscaping']) ? $field['raw'] : h($field['raw']);
|
||||||
|
} else if (isset($field['formatter'])) {
|
||||||
|
$cellContent = $field['formatter']($this->getValueFromObject($field), $this->item);
|
||||||
|
} else if (isset($field['type'])) {
|
||||||
|
$cellContent = $this->btHelper->getView()->element($this->getElementPath($field['type']), [
|
||||||
|
'data' => $this->item,
|
||||||
|
'field' => $field
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$cellContent = h($this->getValueFromObject($field));
|
||||||
|
}
|
||||||
|
foreach (BootstrapGeneric::$variants as $variant) {
|
||||||
|
if (!empty($field["notice_$variant"])) {
|
||||||
|
$cellContent .= sprintf(' <span class="text-%s">%s</span>', $variant, $field["notice_$variant"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this->node('td', [
|
||||||
|
'class' => [
|
||||||
|
'col-8 col-sm-10',
|
||||||
|
!empty($field['cellVariant']) ? "bg-{$field['cellVariant']}" : ''
|
||||||
|
]
|
||||||
|
], $cellContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getValueFromObject($field)
|
||||||
|
{
|
||||||
|
$key = is_array($field) ? $field['path'] : $field;
|
||||||
|
$cellValue = Hash::get($this->item, $key);
|
||||||
|
return $cellValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getElementPath($type)
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'%s%sField',
|
||||||
|
$this->options['elementsRootPath'] ?? '',
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genCaption()
|
||||||
|
{
|
||||||
|
return !empty($this->caption) ? $this->node('caption', [], h($this->caption)) : '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,343 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bootstrap modal based on the given options
|
||||||
|
*
|
||||||
|
* # Options
|
||||||
|
* - size: Control the horizontal size of the modal. Valid values: 'sm', 'lg', 'xl'
|
||||||
|
* - centered, scrollable, backdropStatic: Default bootstrap behavior
|
||||||
|
* - show: Immediately instantiate the modal and show it
|
||||||
|
* - header-variant, body-variant, footer-variant: Default bootstrap variant to be applied to these modal sections
|
||||||
|
* - title: The title of the modal
|
||||||
|
* - titleHtml: The HTML title of the modal
|
||||||
|
* - body: The body of the modal
|
||||||
|
* - bodyHtml: The HTML body of the modal
|
||||||
|
* - footerHtml: The HTML footer of the modal. Override the $type option
|
||||||
|
* - modalClass, headerClass, footerClass: Classes to be applied to these modal sections
|
||||||
|
* - type: Control the type of actions available.
|
||||||
|
* Valid values: 'ok-only', 'confirm', 'custom'
|
||||||
|
* - The `ok-only` Displays a single 'Ok' button
|
||||||
|
* - The `confirm` Displays a 'Confirm' and 'Cancel' buttons
|
||||||
|
* - `confirmButton` and `cancelButton`: Can be used to pass a BootstrapElements/BootstrapButton configuration
|
||||||
|
* - The `custom` Display a list of button defined in the $footerButtons parameter
|
||||||
|
* - confirmFunction: The function to be called when clicking the "confirm" button
|
||||||
|
* - This options only works if the option $show is enabled or if the modal is loaded with the UI ModalFactory function (e.g. `UI.submissionModal()` or `UI.modal()`)
|
||||||
|
* - cancelOnclick: The function to be called once the "cancel" button trigger the `onclick` event
|
||||||
|
* - footerButtons: A list of configuration to be passed to BootstrapElements/BootstrapButton
|
||||||
|
* - The option `clickFunction` can be used to set the function to be called when clicking the button. Behavior similar to "confirmFunction"
|
||||||
|
*
|
||||||
|
* # Click functions behaviors:
|
||||||
|
* - *-Onclick functions have the same behavior as the 'onclick' HTML parameter
|
||||||
|
* - `confirmFunction` and `clickFunction` are called with additional 2 additional arguments:
|
||||||
|
* - modalObject: The instantiated ModalFactory object
|
||||||
|
* - tmpApi: An instantiated AJAXApi object linked with the modal button
|
||||||
|
* - If no functions are provided, Submit the form in place or close the modal
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
*
|
||||||
|
* ## Simple styled modal that is displayed automatically when the HTML is attached to the page
|
||||||
|
* $this->Bootstrap->modal([
|
||||||
|
* 'title' => 'Modal title',
|
||||||
|
* 'size' => 'lg',
|
||||||
|
* 'type' => 'ok-only',
|
||||||
|
* 'body' => '<b>Body content</b>',
|
||||||
|
* 'header-variant' => 'dark',
|
||||||
|
* 'body-variant' => 'light',
|
||||||
|
* 'footer-variant' => 'warning',
|
||||||
|
* 'show' => true,
|
||||||
|
* ]);
|
||||||
|
|
||||||
|
* ## Modal with custom onclick handler
|
||||||
|
* $this->Bootstrap->modal([
|
||||||
|
* 'type' => 'confirm',
|
||||||
|
* 'bodyHtml' => '<b>Body content</b>',
|
||||||
|
* 'confirmButton' => [
|
||||||
|
* 'text' => 'Show modal',
|
||||||
|
* 'icon' => 'eye',
|
||||||
|
* 'onclick' => 'UI.toast({"title": "confirmed!"})',
|
||||||
|
* ],
|
||||||
|
* 'cancelOnclick' => 'UI.toast({"title": "cancelled"})',
|
||||||
|
* 'show' => true,
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
* ## Modal with a onclick handler with prepared arguments bound to the confirm button
|
||||||
|
* $this->Bootstrap->modal([
|
||||||
|
* 'type' => 'confirm',
|
||||||
|
* 'confirmButton' => [
|
||||||
|
* 'text' => 'Confirm',
|
||||||
|
* 'icon' => 'check',
|
||||||
|
* ],
|
||||||
|
* 'confirmFunction' => 'myConfirmFunction', // myConfirmFunction is called with the $modalObject and $tmpApi intialized
|
||||||
|
* 'show' => true,
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
* /*
|
||||||
|
* Example of confirm function
|
||||||
|
* - case 1: If void is returned the modal close automatically regardless of the result
|
||||||
|
* - case 2: If a promise is returned, the modal close automatically if the promise is a success
|
||||||
|
* A success is defined as follow:
|
||||||
|
* - No exceptions
|
||||||
|
* - No data returned
|
||||||
|
* - Object returned with key `success` evaluting to true
|
||||||
|
* - case 3: The modal can be closed manually with: `modalObject.hide()`
|
||||||
|
*
|
||||||
|
* function myConfirmFunction(modalObject, tmpApi) {
|
||||||
|
* const $form = modalObject.$modal.find('form')
|
||||||
|
* const postPromise = $form.length == 1 ?
|
||||||
|
* tmpApi.postForm($form[0]) :
|
||||||
|
* tmpApi.fetchJSON('/users/view/', false, true)
|
||||||
|
* .then((result) => {
|
||||||
|
* console.log(result)
|
||||||
|
* constToReturn = {
|
||||||
|
* success: true, // will close the modal automatically
|
||||||
|
* }
|
||||||
|
* return constToReturn
|
||||||
|
* })
|
||||||
|
* .catch((errors) => {
|
||||||
|
* console.log(errors)
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* return postPromise
|
||||||
|
* }
|
||||||
|
|
||||||
|
* ## Modal with custom footer made of buttons
|
||||||
|
* $this->Bootstrap->modal([
|
||||||
|
* 'type' => 'custom',
|
||||||
|
* 'footerButtons' => [
|
||||||
|
* [
|
||||||
|
* 'text' => 'Confirm',
|
||||||
|
* 'icon' => 'check',
|
||||||
|
* 'variant' => 'danger',
|
||||||
|
* 'clickFunction' => 'testapi',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'text' => 'Cancel',
|
||||||
|
* 'onclick' => 'UI.toast({"title": "confirmed!"})',
|
||||||
|
* ],
|
||||||
|
* ],
|
||||||
|
* 'show' => true,
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapModal extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'size' => '',
|
||||||
|
'centered' => true,
|
||||||
|
'scrollable' => true,
|
||||||
|
'backdropStatic' => false,
|
||||||
|
'show' => false,
|
||||||
|
'header-variant' => '',
|
||||||
|
'body-variant' => '',
|
||||||
|
'footer-variant' => '',
|
||||||
|
'title' => '',
|
||||||
|
'titleHtml' => null,
|
||||||
|
'body' => '',
|
||||||
|
'bodyHtml' => null,
|
||||||
|
'footerHtml' => null,
|
||||||
|
'modalClass' => [''],
|
||||||
|
'headerClass' => [''],
|
||||||
|
'bodyClass' => [''],
|
||||||
|
'footerClass' => [''],
|
||||||
|
'confirmButton' => [
|
||||||
|
'text' => 'Confirm',
|
||||||
|
],
|
||||||
|
'cancelButton' => [
|
||||||
|
'text' => 'Cancel',
|
||||||
|
],
|
||||||
|
'type' => 'ok-only',
|
||||||
|
'footerButtons' => [],
|
||||||
|
'confirmFunction' => '', // Will be called with the following arguments confirmFunction(modalObject, tmpApi)
|
||||||
|
'cancelOnclick' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'size' => ['sm', 'lg', 'xl', ''],
|
||||||
|
'type' => ['ok-only', 'confirm', 'custom'],
|
||||||
|
'header-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
'footer-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
$this->options['modalClass'] = $this->convertToArrayIfNeeded($this->options['modalClass']);
|
||||||
|
$this->options['headerClass'] = $this->convertToArrayIfNeeded($this->options['headerClass']);
|
||||||
|
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||||
|
$this->options['footerClass'] = $this->convertToArrayIfNeeded($this->options['footerClass']);
|
||||||
|
|
||||||
|
$possiblVariants = ['header-variant', 'body-variant', 'footer-variant'];
|
||||||
|
foreach ($possiblVariants as $possiblVariant) {
|
||||||
|
if (!empty($this->options[$possiblVariant])) {
|
||||||
|
$this->options[sprintf('%sClass', substr($possiblVariant, 0, -8))][] = self::getBGAndTextClassForVariant($this->options[$possiblVariant]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($options['confirmFunction']) && !empty($options['confirmButton']['onclick'])) {
|
||||||
|
throw new \InvalidArgumentException(__('Option `{0}` can not be used in conjuction with `{1}` for the confirm button', 'confirmFunction', 'onclick'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function modal()
|
||||||
|
{
|
||||||
|
$modal = $this->genModal();
|
||||||
|
if ($this->options['show']) {
|
||||||
|
return $this->encapsulateWithUIHelper($modal);
|
||||||
|
}
|
||||||
|
return $modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function encapsulateWithUIHelper(string $modal): string
|
||||||
|
{
|
||||||
|
return $this->node('script', [], sprintf(
|
||||||
|
"$(document).ready(function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
UI.modal({
|
||||||
|
rawHtml: \"%s\"
|
||||||
|
})
|
||||||
|
}, 1);
|
||||||
|
})",
|
||||||
|
str_replace('"', '\"', $modal)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genModal()
|
||||||
|
{
|
||||||
|
$dialog = $this->nodeOpen('div', [
|
||||||
|
'class' => array_merge(
|
||||||
|
['modal-dialog', (!empty($this->options['size'])) ? "modal-{$this->options['size']}" : ''],
|
||||||
|
$this->options['modalClass']
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
$content = $this->nodeOpen('div', [
|
||||||
|
'class' => ['modal-content'],
|
||||||
|
]);
|
||||||
|
$header = $this->genHeader();
|
||||||
|
$body = $this->genBody();
|
||||||
|
$footer = $this->genFooter();
|
||||||
|
$closedDiv = $this->nodeClose('div');
|
||||||
|
|
||||||
|
$html = "{$dialog}{$content}{$header}{$body}{$footer}{$closedDiv}{$closedDiv}";
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genHeader()
|
||||||
|
{
|
||||||
|
$header = $this->nodeOpen('div', ['class' => array_merge(['modal-header'], $this->options['headerClass'])]);
|
||||||
|
$header .= $this->options['titleHtml'] ?? $this->node('h5', ['class' => ['modal-title']], h($this->options['title']));
|
||||||
|
if (empty($this->options['backdropStatic'])) {
|
||||||
|
$header .= $this->genericCloseButton('modal');
|
||||||
|
}
|
||||||
|
$header .= $this->nodeClose('div');
|
||||||
|
return $header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genBody()
|
||||||
|
{
|
||||||
|
$body = $this->nodeOpen('div', ['class' => array_merge(['modal-body'], $this->options['bodyClass'])]);
|
||||||
|
$body .= $this->options['bodyHtml'] ?? h($this->options['body']);
|
||||||
|
$body .= $this->nodeClose('div');
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genFooter()
|
||||||
|
{
|
||||||
|
$footer = $this->nodeOpen('div', [
|
||||||
|
'class' => array_merge(['modal-footer'], $this->options['footerClass']),
|
||||||
|
'data-custom-footer' => $this->options['type'] == 'custom'
|
||||||
|
]);
|
||||||
|
$footer .= $this->options['footerHtml'] ?? $this->getFooterBasedOnType();
|
||||||
|
$footer .= $this->nodeClose('div');
|
||||||
|
return $footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFooterBasedOnType()
|
||||||
|
{
|
||||||
|
if ($this->options['type'] == 'ok-only') {
|
||||||
|
return $this->getFooterOkOnly();
|
||||||
|
} else if (str_contains($this->options['type'], 'confirm')) {
|
||||||
|
return $this->getFooterConfirm();
|
||||||
|
} else if ($this->options['type'] == 'custom') {
|
||||||
|
return $this->getFooterCustom();
|
||||||
|
} else {
|
||||||
|
return $this->getFooterOkOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFooterOkOnly()
|
||||||
|
{
|
||||||
|
return (new BootstrapButton([
|
||||||
|
'variant' => 'primary',
|
||||||
|
'text' => __('Ok'),
|
||||||
|
'onclick' => $this->options['confirmOnclick'],
|
||||||
|
'attrs' => [
|
||||||
|
'data-bs-dismiss' => $this->options['confirmOnclick'] ?? 'modal',
|
||||||
|
],
|
||||||
|
]))->button();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFooterConfirm()
|
||||||
|
{
|
||||||
|
$buttonCancelConfig = array_merge(
|
||||||
|
[
|
||||||
|
'variant' => 'secondary',
|
||||||
|
'attrs' => [
|
||||||
|
'data-bs-dismiss' => 'modal',
|
||||||
|
'onclick' => $this->options['cancelOnclick']
|
||||||
|
]
|
||||||
|
],
|
||||||
|
$this->options['cancelButton'],
|
||||||
|
);
|
||||||
|
$buttonCancel = (new BootstrapButton($buttonCancelConfig))->button();
|
||||||
|
|
||||||
|
$defaultConfig = [
|
||||||
|
'variant' => 'primary',
|
||||||
|
'class' => 'modal-confirm-button',
|
||||||
|
];
|
||||||
|
if (!empty($this->options['confirmOnclick'])) {
|
||||||
|
$defaultConfig['onclick'] = $this->options['confirmOnclick'];
|
||||||
|
}
|
||||||
|
if (!empty($this->options['confirmFunction'])) {
|
||||||
|
$defaultConfig['attrs']['data-confirmFunction'] = $this->options['confirmFunction'];
|
||||||
|
}
|
||||||
|
$buttonConfirmConfig = array_merge(
|
||||||
|
$defaultConfig,
|
||||||
|
$this->options['confirmButton'],
|
||||||
|
);
|
||||||
|
$buttonConfirm = (new BootstrapButton($buttonConfirmConfig))->button();
|
||||||
|
return $buttonCancel . $buttonConfirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFooterCustom()
|
||||||
|
{
|
||||||
|
$buttons = [];
|
||||||
|
foreach ($this->options['footerButtons'] as $buttonConfig) {
|
||||||
|
$defaultConfig = [
|
||||||
|
'variant' => 'primary',
|
||||||
|
'class' => 'modal-confirm-button',
|
||||||
|
'attrs' => [
|
||||||
|
'data-bs-dismiss' => !empty($buttonConfig['clickFunction']) ? '' : 'modal',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
if (!empty($buttonConfig['clickFunction'])) {
|
||||||
|
$defaultConfig['attrs']['data-clickFunction'] = $buttonConfig['clickFunction'];
|
||||||
|
}
|
||||||
|
$buttonConfig = array_merge(
|
||||||
|
$defaultConfig,
|
||||||
|
$buttonConfig,
|
||||||
|
);
|
||||||
|
$buttons[] = (new BootstrapButton($buttonConfig))->button();
|
||||||
|
}
|
||||||
|
return implode('', $buttons);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a small colored circle meant to show notifications
|
||||||
|
*
|
||||||
|
* # Options
|
||||||
|
* - text: Optinal text to be displayed inside the circle
|
||||||
|
* - variant: The Bootstrap variant of the notification circle
|
||||||
|
* - borderVariant: If set, creates a border around the circle. Typically will hold the value `light` or `dark`
|
||||||
|
* - title: The HTML title of the notification
|
||||||
|
* - class: Additional classes to be added
|
||||||
|
* - attrs: Additional attributes to be added
|
||||||
|
*
|
||||||
|
* # Usage
|
||||||
|
* $this->Bootstrap->notificationBubble([
|
||||||
|
* 'text' => '3',
|
||||||
|
* 'variant' => 'warning',
|
||||||
|
* 'title' => '3 unread messages',
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapNotificationBubble extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'text' => '',
|
||||||
|
'variant' => 'warning',
|
||||||
|
'borderVariant' => '',
|
||||||
|
'title' => '',
|
||||||
|
'class' => [],
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => BootstrapGeneric::$variants,
|
||||||
|
'borderVariant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
];
|
||||||
|
$this->defaultOptions['title'] = __('New notifications');
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
|
||||||
|
if (!empty($this->options['borderVariant'])) {
|
||||||
|
if (!empty($this->options['attrs']['style'])) {
|
||||||
|
$this->options['attrs']['style'] .= 'box-shadow: 0 0.125rem 0.25rem #00000050;';
|
||||||
|
} else {
|
||||||
|
$this->options['attrs']['style'] = 'box-shadow: 0 0.125rem 0.25rem #00000050;';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notificationBubble()
|
||||||
|
{
|
||||||
|
return $this->genNotificationBubble();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genNotificationBubble()
|
||||||
|
{
|
||||||
|
$tmpId = 'tmp-' . mt_rand();
|
||||||
|
$defaultClasses = [
|
||||||
|
'position-absolute',
|
||||||
|
'top-0',
|
||||||
|
'start-100',
|
||||||
|
'translate-middle',
|
||||||
|
'p-1',
|
||||||
|
'rounded-circle',
|
||||||
|
];
|
||||||
|
if (!empty($this->options['borderVariant'])) {
|
||||||
|
$defaultClasses[] = "border border-2 border-{$this->options['borderVariant']}";
|
||||||
|
}
|
||||||
|
if (!empty($this->options['variant'])) {
|
||||||
|
$defaultClasses[] = "bg-{$this->options['variant']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->options['text'])) {
|
||||||
|
$this->options['attrs']['style'] .= ' min-width: 0.7rem; line-height: 1; box-sizing: content-box;';
|
||||||
|
$defaultClasses[] = 'text-center';
|
||||||
|
$defaultClasses[] = 'fs-8';
|
||||||
|
$defaultClasses[] = 'fw-bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = $this->node('span',
|
||||||
|
array_merge(
|
||||||
|
[
|
||||||
|
'id' => $tmpId,
|
||||||
|
'class' => array_merge(
|
||||||
|
$defaultClasses,
|
||||||
|
$this->options['class']
|
||||||
|
),
|
||||||
|
'title' => h($this->options['title'])
|
||||||
|
],
|
||||||
|
$this->options['attrs']
|
||||||
|
),
|
||||||
|
!empty($this->options['text']) ? $this->node('span', [], h($this->options['text'])) : ''
|
||||||
|
);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bootstrap progress bar
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - label: A text to be centered in the active part of the progress bar. If set to `true`, will display the percentage of the progress bar
|
||||||
|
* - title: The title HTML attribute to set
|
||||||
|
* - total: The total amount of the progress
|
||||||
|
* - value: The active part of the progress
|
||||||
|
* - variant: The bootstrap variant of the active part of the progress bar
|
||||||
|
* - height: The height of the bar
|
||||||
|
* - striped, animated: If the bar should have the striped and animated bootstrap properties
|
||||||
|
* - attrs: Additional HTML attributes to add
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->progress([
|
||||||
|
* 'value' => 45,
|
||||||
|
* 'total' => 100,
|
||||||
|
* 'label' => true,
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class BootstrapProgress extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'value' => 0,
|
||||||
|
'total' => 100,
|
||||||
|
'label' => true,
|
||||||
|
'title' => '',
|
||||||
|
'variant' => 'primary',
|
||||||
|
'height' => '',
|
||||||
|
'striped' => false,
|
||||||
|
'animated' => false,
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => BootstrapGeneric::$variants,
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function progress()
|
||||||
|
{
|
||||||
|
return $this->genProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genProgress()
|
||||||
|
{
|
||||||
|
$percentage = round(100 * $this->options['value'] / $this->options['total']);
|
||||||
|
$heightStyle = !empty($this->options['height']) ? sprintf('height: %s;', h($this->options['height'])) : '';
|
||||||
|
$widthStyle = sprintf('width: %s%%;', $percentage);
|
||||||
|
$label = !empty($this->options['label']) ? ($this->options['label'] === true ? "{$percentage}%" : h($this->options['label'])) : '';
|
||||||
|
$pb = $this->node('div', array_merge([
|
||||||
|
'class' => [
|
||||||
|
'progress-bar',
|
||||||
|
"bg-{$this->options['variant']}",
|
||||||
|
$this->options['striped'] ? 'progress-bar-striped' : '',
|
||||||
|
$this->options['animated'] ? 'progress-bar-animated' : '',
|
||||||
|
],
|
||||||
|
'role' => "progressbar",
|
||||||
|
'aria-valuemin' => "0", 'aria-valuemax' => "100", 'aria-valuenow' => $percentage,
|
||||||
|
'style' => $widthStyle,
|
||||||
|
'title' => h($this->options['title']),
|
||||||
|
], $this->options['attrs']), $label);
|
||||||
|
$container = $this->node('div', [
|
||||||
|
'class' => [
|
||||||
|
'progress',
|
||||||
|
],
|
||||||
|
'style' => $heightStyle,
|
||||||
|
'title' => h($this->options['title']),
|
||||||
|
], $pb);
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a progress timeline similar to a form wizard
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - variant: The variant of the active part of the timeline
|
||||||
|
* - variantInactive: The variant of the inactive part of the timeline
|
||||||
|
* - selected: 0-indexed step number to be selected. Will make all steps before the selected step active
|
||||||
|
* - steps: The definition of the step. Options are:
|
||||||
|
* - text: The text of the step
|
||||||
|
* - icon: The icon of the step. Default to the text number if empty
|
||||||
|
* - title: A title to be set for the step
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->progressTimeline([
|
||||||
|
* 'selected' => 1,
|
||||||
|
* 'steps' => [
|
||||||
|
* [
|
||||||
|
* 'text' => __('Step 1'),
|
||||||
|
* 'icon' => 'star',
|
||||||
|
* 'title' => __('Title'),
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'text' => __('Step 3'),
|
||||||
|
* 'icon' => 'exchange-alt',
|
||||||
|
* ]
|
||||||
|
* ],
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapProgressTimeline extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'steps' => [],
|
||||||
|
'selected' => 0,
|
||||||
|
'variant' => 'primary',
|
||||||
|
'variantInactive' => 'secondary',
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options, $btHelper)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => BootstrapGeneric::$variants,
|
||||||
|
'variantInactive' => BootstrapGeneric::$variants,
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
$this->btHelper = $btHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function progressTimeline()
|
||||||
|
{
|
||||||
|
return $this->genProgressTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStepIcon($step, $i, $nodeActive, $lineActive)
|
||||||
|
{
|
||||||
|
$icon = $this->node('b', [
|
||||||
|
'class' => [
|
||||||
|
!empty($step['icon']) ? h($this->btHelper->FontAwesome->getClass($step['icon'])) : '',
|
||||||
|
$this->getTextClassForVariant($this->options['variant'])
|
||||||
|
],
|
||||||
|
], empty($step['icon']) ? h($i + 1) : '');
|
||||||
|
|
||||||
|
$containerDefaultClass = [
|
||||||
|
'd-flex',
|
||||||
|
'align-items-center',
|
||||||
|
'justify-content-center',
|
||||||
|
'rounded-circle',
|
||||||
|
];
|
||||||
|
$containerDefaultClass[] = $nodeActive ? "bg-{$this->options['variant']}" : "bg-{$this->options['variantInactive']}";
|
||||||
|
$iconContainer = $this->node('span', [
|
||||||
|
'class' => $containerDefaultClass,
|
||||||
|
'style' => 'width:50px; height:50px'
|
||||||
|
], $icon);
|
||||||
|
$li = $this->node('li', [
|
||||||
|
'class' => [
|
||||||
|
'd-flex', 'flex-column',
|
||||||
|
$nodeActive ? 'progress-active' : 'progress-inactive',
|
||||||
|
],
|
||||||
|
], $iconContainer);
|
||||||
|
$html = $li . $this->getHorizontalLine($i, $nodeActive, $lineActive);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHorizontalLine($i, $nodeActive, $lineActive)
|
||||||
|
{
|
||||||
|
$stepCount = count($this->options['steps']);
|
||||||
|
if ($i == $stepCount - 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$progressBar = (new BootstrapProgress([
|
||||||
|
'label' => false,
|
||||||
|
'value' => $nodeActive ? ($lineActive ? 100 : 50) : 0,
|
||||||
|
'height' => '2px',
|
||||||
|
'variant' => $this->options['variant']
|
||||||
|
]))->progress();
|
||||||
|
$line = $this->node('span', [
|
||||||
|
'class' => [
|
||||||
|
'progress-line',
|
||||||
|
'flex-grow-1', 'align-self-center',
|
||||||
|
$lineActive ? "bg-{$this->options['variant']}" : ''
|
||||||
|
],
|
||||||
|
], $progressBar);
|
||||||
|
return $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStepText($step, $isActive)
|
||||||
|
{
|
||||||
|
return $this->node('li', [
|
||||||
|
'class' => [
|
||||||
|
'text-center',
|
||||||
|
'fw-bold',
|
||||||
|
$isActive ? 'progress-active' : 'progress-inactive',
|
||||||
|
],
|
||||||
|
], h($step['text'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genProgressTimeline()
|
||||||
|
{
|
||||||
|
$iconLis = '';
|
||||||
|
$textLis = '';
|
||||||
|
foreach ($this->options['steps'] as $i => $step) {
|
||||||
|
$nodeActive = $i <= $this->options['selected'];
|
||||||
|
$lineActive = $i < $this->options['selected'];
|
||||||
|
$iconLis .= $this->getStepIcon($step, $i, $nodeActive, $lineActive);
|
||||||
|
$textLis .= $this->getStepText($step, $nodeActive);
|
||||||
|
}
|
||||||
|
$ulIcons = $this->node('ul', [
|
||||||
|
'class' => [
|
||||||
|
'd-flex', 'justify-content-around',
|
||||||
|
],
|
||||||
|
], $iconLis);
|
||||||
|
$ulText = $this->node('ul', [
|
||||||
|
'class' => [
|
||||||
|
'd-flex', 'justify-content-between',
|
||||||
|
],
|
||||||
|
], $textLis);
|
||||||
|
$html = $this->node('div', [
|
||||||
|
'class' => ['progress-timeline', 'mw-75', 'mx-auto']
|
||||||
|
], $ulIcons . $ulText);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a switch acting as a checkbox
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - label: The label associated with the switch
|
||||||
|
* - disabled: Should the switch be disabled
|
||||||
|
* - checked: Should the switch be checked by default
|
||||||
|
* - title: Optional title to add to the switch
|
||||||
|
* - variant: The variant to use to show if the switch is active
|
||||||
|
* - class: Additional class to add to the switch
|
||||||
|
* - attrs: Additional HTML attributes to add to the switch
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->switch([
|
||||||
|
* 'label' => 'my label',
|
||||||
|
* 'checked' => true,
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapSwitch extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'label' => '',
|
||||||
|
'variant' => 'primary',
|
||||||
|
'disabled' => false,
|
||||||
|
'checked' => false,
|
||||||
|
'title' => '',
|
||||||
|
'class' => [],
|
||||||
|
'attrs' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => BootstrapGeneric::$variants,
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function switch()
|
||||||
|
{
|
||||||
|
return $this->genSwitch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function genSwitch()
|
||||||
|
{
|
||||||
|
$tmpId = 'tmp-' . mt_rand();
|
||||||
|
$input = self::node('input', array_merge(
|
||||||
|
[
|
||||||
|
'type' => "checkbox",
|
||||||
|
'class' => 'form-check-input',
|
||||||
|
'id' => $tmpId,
|
||||||
|
'disabled' => !empty($this->options['disabled']),
|
||||||
|
'checked' => !empty($this->options['checked']),
|
||||||
|
],
|
||||||
|
$this->options['attrs']
|
||||||
|
));
|
||||||
|
$label = self::node('label', [
|
||||||
|
'class' => 'form-check-label',
|
||||||
|
'for' => $tmpId,
|
||||||
|
], h($this->options['label']));
|
||||||
|
$html = self::node('div', [
|
||||||
|
'class' => [
|
||||||
|
'form-check form-switch',
|
||||||
|
],
|
||||||
|
'title' => h($this->options['title']),
|
||||||
|
], [$input, $label]);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use Cake\Utility\Hash;
|
||||||
|
use Cake\Utility\Inflector;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a table from 2-dimensional data $items.
|
||||||
|
* Perfect to display a list of objects.
|
||||||
|
*
|
||||||
|
* # Options for table
|
||||||
|
* - striped, bordered, borderless, hover, small: Default bootstrap behavior
|
||||||
|
* - variant: Variant to apply on the entire table
|
||||||
|
* - tableClass: A list of class to add on the table container
|
||||||
|
* - bodyClass: A list of class to add on the tbody container
|
||||||
|
* - id: The ID to use for the table
|
||||||
|
* - caption: Optional table caption
|
||||||
|
* - elementsRootPath: Root path to use when item are relying on cakephp's element. See options for fields
|
||||||
|
*
|
||||||
|
* # Options for fields
|
||||||
|
* - label: The name of the field to be displayed as a label
|
||||||
|
* - labelHtml: The HTML of the field to be displayed as a label
|
||||||
|
* - path: The path to be fed to Hash::get() in order to get the value from the $item
|
||||||
|
* - element: The type of element to use combined with $elementsRootPath from the table's option
|
||||||
|
* - formatter: A callback function to format the value
|
||||||
|
* - columnVariant: The bootstrap variant to be applied on the cell
|
||||||
|
* - notice_$variant: A text with the passed variant to be append at the end. $variant can be any valid bootstrap variant. Example: `notice_warning` or `notice_info`.
|
||||||
|
*
|
||||||
|
* # Special fields for $items
|
||||||
|
* - _rowVariant: The bootstrap variant to be applied on the row
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->table(
|
||||||
|
* [
|
||||||
|
* 'hover' => false,
|
||||||
|
* 'striped' => 'false',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'items' => [
|
||||||
|
* ['column 1' => 'col1', 'column 2' => 'col2', 'key1' => 'val1', 'key2' => true],
|
||||||
|
* ['column 1' => 'col1', 'column 2' => 'col2', 'key1' => 'val2', 'key2' => false,'_rowVariant' => 'success'],
|
||||||
|
* ['column 1' => 'col1', 'column 2' => 'col2', 'key1' => 'val3', 'key2' => true],
|
||||||
|
* ],
|
||||||
|
* 'fields' => [
|
||||||
|
* 'column 1',
|
||||||
|
* [
|
||||||
|
* 'path' => 'column 2',
|
||||||
|
* 'label' => 'COLUMN 2',
|
||||||
|
* 'columnVariant' => 'danger',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'labelHtml' => '<i>column 3</i>',
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'path' => 'key1',
|
||||||
|
* 'label' => __('Field'),
|
||||||
|
* 'formatter' => function ($field, $row) {
|
||||||
|
* return sprintf('<i>%s</i>', h($field));
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'path' => 'key2',
|
||||||
|
* 'element' => 'boolean',
|
||||||
|
* ],
|
||||||
|
* ],
|
||||||
|
* 'caption' => 'This is a caption'
|
||||||
|
* ]
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
class BootstrapTable extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'striped' => true,
|
||||||
|
'bordered' => true,
|
||||||
|
'borderless' => false,
|
||||||
|
'hover' => true,
|
||||||
|
'small' => false,
|
||||||
|
'variant' => '',
|
||||||
|
'tableClass' => [],
|
||||||
|
'headerClass' => [],
|
||||||
|
'bodyClass' => [],
|
||||||
|
'id' => '',
|
||||||
|
'caption' => '',
|
||||||
|
'elementsRootPath' => '/genericElements/SingleViews/Fields/',
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options, $data, $btHelper)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => array_merge(BootstrapGeneric::$variants, [''])
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
$this->fields = $data['fields'];
|
||||||
|
$this->items = $data['items'];
|
||||||
|
$this->caption = !empty($data['caption']) ? $data['caption'] : '';
|
||||||
|
$this->btHelper = $btHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
$this->options['tableClass'] = $this->convertToArrayIfNeeded($this->options['tableClass']);
|
||||||
|
$this->options['bodyClass'] = $this->convertToArrayIfNeeded($this->options['bodyClass']);
|
||||||
|
$this->options['headerClass'] = $this->convertToArrayIfNeeded($this->options['headerClass']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table()
|
||||||
|
{
|
||||||
|
return $this->genTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genTable()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('table', [
|
||||||
|
'class' => [
|
||||||
|
'table',
|
||||||
|
"table-{$this->options['variant']}",
|
||||||
|
$this->options['striped'] ? 'table-striped' : '',
|
||||||
|
$this->options['bordered'] ? 'table-bordered' : '',
|
||||||
|
$this->options['borderless'] ? 'table-borderless' : '',
|
||||||
|
$this->options['hover'] ? 'table-hover' : '',
|
||||||
|
$this->options['small'] ? 'table-sm' : '',
|
||||||
|
implode(' ', $this->options['tableClass']),
|
||||||
|
!empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
|
||||||
|
],
|
||||||
|
'id' => $this->options['id'] ?? ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html .= $this->genCaption();
|
||||||
|
$html .= $this->genHeader();
|
||||||
|
$html .= $this->genBody();
|
||||||
|
|
||||||
|
$html .= $this->nodeClose('table');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genHeader()
|
||||||
|
{
|
||||||
|
$head = $this->nodeOpen('thead', [
|
||||||
|
'class' => $this->options['headerClass'],
|
||||||
|
]);
|
||||||
|
$head .= $this->nodeOpen('tr');
|
||||||
|
foreach ($this->fields as $i => $field) {
|
||||||
|
if (is_array($field)) {
|
||||||
|
if (!empty($field['labelHtml'])) {
|
||||||
|
$label = $field['labelHtml'];
|
||||||
|
} else {
|
||||||
|
$label = !empty($field['label']) ? $field['label'] : Inflector::humanize($field['path']);
|
||||||
|
$label = h($label);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$label = Inflector::humanize($field);
|
||||||
|
$label = h($label);
|
||||||
|
}
|
||||||
|
$head .= $this->node('th', [], $label);
|
||||||
|
}
|
||||||
|
$head .= $this->nodeClose('tr');
|
||||||
|
$head .= $this->nodeClose('thead');
|
||||||
|
return $head;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genBody()
|
||||||
|
{
|
||||||
|
$body = $this->nodeOpen('tbody', [
|
||||||
|
'class' => $this->options['bodyClass'],
|
||||||
|
]);
|
||||||
|
foreach ($this->items as $i => $row) {
|
||||||
|
$body .= $this->genRow($row, $i);
|
||||||
|
}
|
||||||
|
$body .= $this->nodeClose('tbody');
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genRow($row, $rowIndex)
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('tr', [
|
||||||
|
'class' => [
|
||||||
|
!empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : ''
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
if (array_keys($row) !== range(0, count($row) - 1)) { // associative array
|
||||||
|
foreach ($this->fields as $i => $field) {
|
||||||
|
$cellValue = $this->getValueFromObject($row, $field);
|
||||||
|
$html .= $this->genCell($cellValue, $field, $row, $rowIndex);
|
||||||
|
}
|
||||||
|
} else { // indexed array
|
||||||
|
foreach ($row as $i => $cellValue) {
|
||||||
|
$html .= $this->genCell($cellValue, 'index', $row, $rowIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$html .= $this->nodeClose('tr');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genCell($value, $field = [], $row = [], $rowIndex = 0)
|
||||||
|
{
|
||||||
|
if (isset($field['formatter'])) {
|
||||||
|
$cellContent = $field['formatter']($value, $row, $rowIndex);
|
||||||
|
} else if (isset($field['element'])) {
|
||||||
|
$cellContent = $this->btHelper->getView()->element($this->getElementPath($field['element']), [
|
||||||
|
'data' => [$value],
|
||||||
|
'field' => ['path' => '0']
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$cellContent = h($value);
|
||||||
|
}
|
||||||
|
return $this->node('td', [
|
||||||
|
'class' => [
|
||||||
|
!empty($field['columnVariant']) ? "table-{$field['columnVariant']}" : ''
|
||||||
|
]
|
||||||
|
], $cellContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getValueFromObject($row, $field)
|
||||||
|
{
|
||||||
|
$path = is_array($field) ? $field['path'] : $field;
|
||||||
|
$cellValue = Hash::get($row, $path);
|
||||||
|
return $cellValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getElementPath($type)
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'%s%sField',
|
||||||
|
$this->options['elementsRootPath'] ?? '',
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genCaption()
|
||||||
|
{
|
||||||
|
return !empty($this->caption) ? $this->node('caption', [], h($this->caption)) : '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,307 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use Cake\Utility\Security;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bootstrap panel with navigation component.
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - fill-header: Should the navigation header takes up all the space available
|
||||||
|
* - justify-header: Allow to specify how the naviation component should be justified. Accepts: false (no justify), 'start', 'end', 'center';
|
||||||
|
* - pills: Should the navigation element be pills
|
||||||
|
* - card: Should the content and navigation elements be wrapped in a Bootstrap card component
|
||||||
|
* - header-variant, body-variant: The variant that the card's header and body should have. Ignore if $card is not set
|
||||||
|
* - body-class, nav-class, nav-class-item, content-class: Additional classes to be added to the nav, body, navigation items or content
|
||||||
|
* - vertical: Should the navigation component be placed vertically next to the content. Best used with the `pills` option enabled.
|
||||||
|
* - vertical-size: Controls the horizontal size of the vertical header. Must be between [1, 11]
|
||||||
|
* - vertical-position: Controls the position of the header. Accepts 'start and 'end'
|
||||||
|
* - horizontal-position: Controls the position of the header. Accepts 'top and 'bottom'
|
||||||
|
* - data: The data used to generate the tabs. Must have a `navs` and `content` key. See the "# Data" section
|
||||||
|
*
|
||||||
|
* # Data
|
||||||
|
* - navs: The data for the navigation items. Supported options:
|
||||||
|
* - id: The ID of the nav. Auto-generated if left empty
|
||||||
|
* - active: Should the tab be active
|
||||||
|
* - disabled: Should the tab be disabled
|
||||||
|
* - text: The text content of the tab
|
||||||
|
* - html: The HTML content of the tab
|
||||||
|
*
|
||||||
|
* - content: The HTML content for each tabs
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* ## Simple formatted tabs using the card option
|
||||||
|
* echo $this->Bootstrap->tabs([
|
||||||
|
* 'horizontal-position' => 'top',
|
||||||
|
* 'header-variant' => 'danger',
|
||||||
|
* 'card' => true,
|
||||||
|
* 'data' => [
|
||||||
|
* 'navs' => [
|
||||||
|
* ['text' => 'nav 1'],
|
||||||
|
* ['html' => '<b>nav 2</b>', 'active' => true],
|
||||||
|
* ],
|
||||||
|
* 'content' => [
|
||||||
|
* '<i>content 1</i>',
|
||||||
|
* 'content 2',
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
* ## Simple formatted tabs using the card option and vertical options
|
||||||
|
* echo $this->Bootstrap->tabs([
|
||||||
|
* 'pills' => true,
|
||||||
|
* 'vertical' => true,
|
||||||
|
* 'vertical-position' => 'start',
|
||||||
|
* 'card' => true,
|
||||||
|
* 'data' => [
|
||||||
|
* 'navs' => [
|
||||||
|
* ['text' => 'nav 1'],
|
||||||
|
* ['html' => '<b>nav 2</b>', 'disabled' => true],
|
||||||
|
* ],
|
||||||
|
* 'content' => [
|
||||||
|
* '<i>content 1</i>',
|
||||||
|
* 'content 2',
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapTabs extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'fill-header' => false,
|
||||||
|
'justify-header' => false,
|
||||||
|
'pills' => false,
|
||||||
|
'vertical' => false,
|
||||||
|
'vertical-size' => 3,
|
||||||
|
'vertical-position' => 'start',
|
||||||
|
'horizontal-position' => 'top',
|
||||||
|
'card' => false,
|
||||||
|
'header-variant' => '',
|
||||||
|
'body-variant' => '',
|
||||||
|
'body-class' => [],
|
||||||
|
'nav-class' => [],
|
||||||
|
'nav-item-class' => [],
|
||||||
|
'content-class' => [],
|
||||||
|
'data' => [
|
||||||
|
'navs' => [],
|
||||||
|
'content' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
private $bsClasses = null;
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'justify-header' => [false, 'center', 'end', 'start'],
|
||||||
|
'vertical-position' => ['start', 'end'],
|
||||||
|
'horizontal-position' => ['top', 'bottom'],
|
||||||
|
'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
'header-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tabs()
|
||||||
|
{
|
||||||
|
return $this->genTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$this->options = array_merge($this->defaultOptions, $options);
|
||||||
|
$this->data = $this->options['data'];
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
if (empty($this->data['navs'])) {
|
||||||
|
throw new InvalidArgumentException(__('No navigation data provided'));
|
||||||
|
}
|
||||||
|
$this->bsClasses = [
|
||||||
|
'nav' => [],
|
||||||
|
'nav-item' => $this->options['nav-item-class'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($this->options['justify-header'])) {
|
||||||
|
$this->bsClasses['nav'][] = 'justify-content-' . $this->options['justify-header'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->options['vertical'] && !isset($options['pills']) && !isset($options['card'])) {
|
||||||
|
$this->options['pills'] = true;
|
||||||
|
$this->options['card'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->options['pills']) {
|
||||||
|
$this->bsClasses['nav'][] = 'nav-pills';
|
||||||
|
if ($this->options['vertical']) {
|
||||||
|
$this->bsClasses['nav'][] = 'flex-column';
|
||||||
|
}
|
||||||
|
if ($this->options['card']) {
|
||||||
|
$this->bsClasses['nav'][] = 'card-header-pills';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->bsClasses['nav'][] = 'nav-tabs';
|
||||||
|
if ($this->options['card']) {
|
||||||
|
$this->bsClasses['nav'][] = 'card-header-tabs';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->options['fill-header']) {
|
||||||
|
$this->bsClasses['nav'][] = 'nav-fill';
|
||||||
|
}
|
||||||
|
if ($this->options['justify-header']) {
|
||||||
|
$this->bsClasses['nav'][] = 'nav-justify';
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeTab = array_key_first($this->data['navs']);
|
||||||
|
foreach ($this->data['navs'] as $i => $nav) {
|
||||||
|
if (!is_array($nav)) {
|
||||||
|
$this->data['navs'][$i] = ['text' => $nav];
|
||||||
|
}
|
||||||
|
if (!isset($this->data['navs'][$i]['id'])) {
|
||||||
|
$this->data['navs'][$i]['id'] = 't-' . Security::randomString(8);
|
||||||
|
}
|
||||||
|
if (!empty($nav['active'])) {
|
||||||
|
$activeTab = $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->data['navs'][$activeTab]['active'] = true;
|
||||||
|
|
||||||
|
if (!empty($this->options['vertical-size']) && $this->options['vertical-size'] != 'auto') {
|
||||||
|
$this->options['vertical-size'] = ($this->options['vertical-size'] < 0 || $this->options['vertical-size'] > 11) ? 3 : $this->options['vertical-size'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($this->options['nav-class'])) {
|
||||||
|
$this->options['nav-class'] = [$this->options['nav-class']];
|
||||||
|
}
|
||||||
|
if (!is_array($this->options['content-class'])) {
|
||||||
|
$this->options['content-class'] = [$this->options['content-class']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genTabs()
|
||||||
|
{
|
||||||
|
return $this->options['vertical'] ? $this->genVerticalTabs() : $this->genHorizontalTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genHorizontalTabs()
|
||||||
|
{
|
||||||
|
if ($this->options['card']) {
|
||||||
|
$cardOptions = [
|
||||||
|
'bodyHTML' => $this->genContent(),
|
||||||
|
'bodyVariant' => $this->options['body-variant'],
|
||||||
|
];
|
||||||
|
if ($this->options['horizontal-position'] === 'bottom') {
|
||||||
|
$cardOptions['footerHTML'] = $this->genNav();
|
||||||
|
$cardOptions['footerVariant'] = $this->options['header-variant'];
|
||||||
|
$cardOptions['headerVariant'] = $this->options['header-variant'];
|
||||||
|
} else {
|
||||||
|
$cardOptions['headerHTML'] = $this->genNav();
|
||||||
|
$cardOptions['headerVariant'] = $this->options['header-variant'];
|
||||||
|
}
|
||||||
|
$bsCard = new BootstrapCard($cardOptions);
|
||||||
|
return $bsCard->card();
|
||||||
|
} else {
|
||||||
|
return $this->genNav() . $this->genContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genVerticalTabs()
|
||||||
|
{
|
||||||
|
$header = $this->node('div', ['class' => array_merge(
|
||||||
|
[
|
||||||
|
($this->options['vertical-size'] != 'auto' ? 'col-' . $this->options['vertical-size'] : ''),
|
||||||
|
($this->options['card'] ? 'card-header border-end' : '')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"bg-{$this->options['header-variant']}",
|
||||||
|
"text-{$this->options['header-text-variant']}",
|
||||||
|
"border-{$this->options['header-border-variant']}"
|
||||||
|
]
|
||||||
|
)], $this->genNav());
|
||||||
|
$content = $this->node('div', ['class' => array_merge(
|
||||||
|
[
|
||||||
|
($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''),
|
||||||
|
($this->options['card'] ? 'card-body2' : '')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"bg-{$this->options['body-variant']}",
|
||||||
|
"text-{$this->options['body-text-variant']}"
|
||||||
|
]
|
||||||
|
)], $this->genContent());
|
||||||
|
|
||||||
|
$containerContent = $this->options['vertical-position'] === 'start' ? [$header, $content] : [$content, $header];
|
||||||
|
$container = $this->node('div', ['class' => array_merge(
|
||||||
|
[
|
||||||
|
'row',
|
||||||
|
($this->options['card'] ? 'card flex-row' : ''),
|
||||||
|
($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"border-{$this->options['header-border-variant']}"
|
||||||
|
]
|
||||||
|
)], $containerContent);
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genNav()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('ul', [
|
||||||
|
'class' => array_merge(['nav'], $this->bsClasses['nav'], $this->options['nav-class']),
|
||||||
|
'role' => 'tablist',
|
||||||
|
]);
|
||||||
|
foreach ($this->data['navs'] as $navItem) {
|
||||||
|
$html .= $this->genNavItem($navItem);
|
||||||
|
}
|
||||||
|
$html .= $this->nodeClose('ul');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genNavItem($navItem)
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('li', [
|
||||||
|
'class' => array_merge(['nav-item'], $this->bsClasses['nav-item'], $this->options['nav-item-class']),
|
||||||
|
'role' => 'presentation',
|
||||||
|
]);
|
||||||
|
$html .= $this->nodeOpen('a', [
|
||||||
|
'class' => array_merge(
|
||||||
|
['nav-link'],
|
||||||
|
[!empty($navItem['active']) ? 'active' : ''],
|
||||||
|
[!empty($navItem['disabled']) ? 'disabled' : '']
|
||||||
|
),
|
||||||
|
'data-bs-toggle' => $this->options['pills'] ? 'pill' : 'tab',
|
||||||
|
'id' => $navItem['id'] . '-tab',
|
||||||
|
'href' => '#' . $navItem['id'],
|
||||||
|
'aria-controls' => $navItem['id'],
|
||||||
|
'aria-selected' => !empty($navItem['active']),
|
||||||
|
'role' => 'tab',
|
||||||
|
]);
|
||||||
|
$html .= $navItem['html'] ?? h($navItem['text']);
|
||||||
|
$html .= $this->nodeClose('a');
|
||||||
|
$html .= $this->nodeClose('li');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genContent()
|
||||||
|
{
|
||||||
|
$html = $this->nodeOpen('div', [
|
||||||
|
'class' => array_merge(['tab-content'], $this->options['content-class']),
|
||||||
|
]);
|
||||||
|
foreach ($this->data['content'] as $i => $content) {
|
||||||
|
$navItem = $this->data['navs'][$i];
|
||||||
|
$html .= $this->genContentItem($navItem, $content);
|
||||||
|
}
|
||||||
|
$html .= $this->nodeClose('div');
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genContentItem($navItem, $content)
|
||||||
|
{
|
||||||
|
return $this->node('div', [
|
||||||
|
'class' => array_merge(['tab-pane', 'fade'], [!empty($navItem['active']) ? 'show active' : '']),
|
||||||
|
'role' => 'tabpanel',
|
||||||
|
'id' => $navItem['id'],
|
||||||
|
'aria-labelledby' => $navItem['id'] . '-tab'
|
||||||
|
], $content);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Helper\BootstrapElements;
|
||||||
|
|
||||||
|
use App\View\Helper\BootstrapGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bootstrap toast by calling creating a Toaster object and passing the provided options
|
||||||
|
*
|
||||||
|
* # Options:
|
||||||
|
* - text: The text content of the alert
|
||||||
|
* - html: The HTML content of the alert
|
||||||
|
* - dismissible: Can the alert be dissmissed
|
||||||
|
* - variant: The Bootstrap variant of the alert
|
||||||
|
* - fade: Should the alert fade when dismissed
|
||||||
|
* - class: Additional classes to add to the alert container
|
||||||
|
*
|
||||||
|
* # Usage:
|
||||||
|
* $this->Bootstrap->toast([
|
||||||
|
* 'title' => 'Title',
|
||||||
|
* 'bodyHtml' => '<i>Body</i>',
|
||||||
|
* 'muted' => 'Muted text',
|
||||||
|
* 'variant' => 'warning',
|
||||||
|
* 'closeButton' => true,
|
||||||
|
* ]);
|
||||||
|
*/
|
||||||
|
class BootstrapToast extends BootstrapGeneric
|
||||||
|
{
|
||||||
|
private $defaultOptions = [
|
||||||
|
'id' => false,
|
||||||
|
'title' => false,
|
||||||
|
'muted' => false,
|
||||||
|
'body' => false,
|
||||||
|
'variant' => 'default',
|
||||||
|
'autohide' => true,
|
||||||
|
'delay' => 'auto',
|
||||||
|
'titleHtml' => false,
|
||||||
|
'mutedHtml' => false,
|
||||||
|
'bodyHtml' => false,
|
||||||
|
'closeButton' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
function __construct($options)
|
||||||
|
{
|
||||||
|
$this->allowedOptionValues = [
|
||||||
|
'variant' => array_merge(BootstrapGeneric::$variants, ['default']),
|
||||||
|
];
|
||||||
|
$this->processOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processOptions($options)
|
||||||
|
{
|
||||||
|
$validOptions = array_filter($options, function($optionName) {
|
||||||
|
return isset($this->defaultOptions[$optionName]);
|
||||||
|
}, ARRAY_FILTER_USE_KEY);
|
||||||
|
$this->options = array_merge($this->defaultOptions, $validOptions);
|
||||||
|
$this->checkOptionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toast()
|
||||||
|
{
|
||||||
|
return $this->genToast();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function genToast()
|
||||||
|
{
|
||||||
|
return $this->node('script', [], sprintf(
|
||||||
|
"$(document).ready(function() {
|
||||||
|
UI.toast(%s);
|
||||||
|
})",
|
||||||
|
json_encode($this->options, JSON_FORCE_OBJECT)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -31,7 +31,7 @@ class SocialProviderHelper extends Helper
|
||||||
|
|
||||||
private function genImage($url, $alt)
|
private function genImage($url, $alt)
|
||||||
{
|
{
|
||||||
return $this->Bootstrap->genNode('img', [
|
return $this->Bootstrap->node('img', [
|
||||||
'src' => $url,
|
'src' => $url,
|
||||||
'class' => ['img-fluid'],
|
'class' => ['img-fluid'],
|
||||||
'width' => '16',
|
'width' => '16',
|
||||||
|
|
|
@ -9,9 +9,7 @@ if (!empty($updateAvailables)) {
|
||||||
'icon' => 'arrow-alt-circle-up',
|
'icon' => 'arrow-alt-circle-up',
|
||||||
'class' => 'mt-1',
|
'class' => 'mt-1',
|
||||||
'text' => __n('Run update', 'Run all updates', count($updateAvailables)),
|
'text' => __n('Run update', 'Run all updates', count($updateAvailables)),
|
||||||
'params' => [
|
'onclick' => 'runAllUpdate()',
|
||||||
'onclick' => 'runAllUpdate()'
|
|
||||||
]
|
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
echo $this->Bootstrap->alert([
|
echo $this->Bootstrap->alert([
|
||||||
|
@ -35,11 +33,11 @@ foreach ($status as $i => &$update) {
|
||||||
|
|
||||||
echo $this->Bootstrap->table([], [
|
echo $this->Bootstrap->table([], [
|
||||||
'fields' => [
|
'fields' => [
|
||||||
['key' => 'id', 'label' => __('ID')],
|
['path' => 'id', 'label' => __('ID')],
|
||||||
['key' => 'name', 'label' => __('Name')],
|
['path' => 'name', 'label' => __('Name')],
|
||||||
['key' => 'end_time', 'label' => __('End Time')],
|
['path' => 'end_time', 'label' => __('End Time')],
|
||||||
['key' => 'time_taken_formated', 'label' => __('Time Taken')],
|
['path' => 'time_taken_formated', 'label' => __('Time Taken')],
|
||||||
['key' => 'status', 'label' => __('Status')]
|
['path' => 'status', 'label' => __('Status')]
|
||||||
],
|
],
|
||||||
'items' => $status,
|
'items' => $status,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -18,9 +18,7 @@ if (!empty($updateableTemplates['new'])) {
|
||||||
'size' => 'sm',
|
'size' => 'sm',
|
||||||
'icon' => 'download',
|
'icon' => 'download',
|
||||||
'title' => __('Create this template'),
|
'title' => __('Create this template'),
|
||||||
'params' => [
|
'onclick' => "UI.submissionModalForIndex('/metaTemplates/createNewTemplate/{$entry['uuid']}', '/meta-templates')",
|
||||||
'onclick' => "UI.submissionModalForIndex('/metaTemplates/createNewTemplate/{$entry['uuid']}', '/meta-templates')"
|
|
||||||
]
|
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}, $alertList);
|
}, $alertList);
|
||||||
|
@ -225,4 +223,3 @@ function getConflictingTemplate($row, $data) {
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
|
|
|
@ -71,9 +71,7 @@ use Cake\Routing\Router;
|
||||||
$this->Bootstrap->button([
|
$this->Bootstrap->button([
|
||||||
'text' => __('Update to version {0}', h($newMetaTemplate->version)),
|
'text' => __('Update to version {0}', h($newMetaTemplate->version)),
|
||||||
'variant' => 'success',
|
'variant' => 'success',
|
||||||
'params' => [
|
'onclick' => 'submitMigration()',
|
||||||
'onclick' => 'submitMigration()'
|
|
||||||
]
|
|
||||||
])
|
])
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -51,7 +51,7 @@ if ($updateStatus['up-to-date']) {
|
||||||
'templateOnDisk' => $templateOnDisk,
|
'templateOnDisk' => $templateOnDisk,
|
||||||
]);
|
]);
|
||||||
$bodyHtml .= $this->Bootstrap->collapse([
|
$bodyHtml .= $this->Bootstrap->collapse([
|
||||||
'title' => __('View conflicts'),
|
'text' => __('View conflicts'),
|
||||||
'open' => false
|
'open' => false
|
||||||
], $conflictTable);
|
], $conflictTable);
|
||||||
$bodyHtml .= $this->element('MetaTemplates/conflictResolution', [
|
$bodyHtml .= $this->element('MetaTemplates/conflictResolution', [
|
||||||
|
|
|
@ -52,8 +52,8 @@ use Cake\Core\Configure;
|
||||||
'class' => ['d-block', 'w-100'],
|
'class' => ['d-block', 'w-100'],
|
||||||
'image' => [
|
'image' => [
|
||||||
'path' => '/img/keycloak_logo.png',
|
'path' => '/img/keycloak_logo.png',
|
||||||
'alt' => 'Keycloak'
|
'alt' => 'Keycloak',
|
||||||
]
|
],
|
||||||
]);
|
]);
|
||||||
echo $this->Form->end();
|
echo $this->Form->end();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
if ($setting['type'] == 'string' || $setting['type'] == 'textarea' || empty($setting['type'])) {
|
if ($setting['type'] == 'string' || $setting['type'] == 'textarea' || empty($setting['type'])) {
|
||||||
$input = (function ($settingName, $setting, $appView) {
|
$input = (function ($settingName, $setting, $appView) {
|
||||||
$settingId = str_replace('.', '_', $settingName);
|
$settingId = str_replace('.', '_', $settingName);
|
||||||
return $appView->Bootstrap->genNode(
|
return $appView->Bootstrap->node(
|
||||||
$setting['type'] == 'textarea' ? 'textarea' : 'input',
|
$setting['type'] == 'textarea' ? 'textarea' : 'input',
|
||||||
[
|
[
|
||||||
'class' => [
|
'class' => [
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
} elseif ($setting['type'] == 'integer') {
|
} elseif ($setting['type'] == 'integer') {
|
||||||
$input = (function ($settingName, $setting, $appView) {
|
$input = (function ($settingName, $setting, $appView) {
|
||||||
$settingId = str_replace('.', '_', $settingName);
|
$settingId = str_replace('.', '_', $settingName);
|
||||||
return $appView->Bootstrap->genNode('input', [
|
return $appView->Bootstrap->node('input', [
|
||||||
'class' => [
|
'class' => [
|
||||||
'form-control',
|
'form-control',
|
||||||
(!empty($setting['error']) ? 'is-invalid' : ''),
|
(!empty($setting['error']) ? 'is-invalid' : ''),
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$options = [];
|
$options = [];
|
||||||
$options[] = $appView->Bootstrap->genNode('option', ['value' => '-1', 'data-is-empty-option' => '1'], __('Select an option'));
|
$options[] = $appView->Bootstrap->node('option', ['value' => '-1', 'data-is-empty-option' => '1'], __('Select an option'));
|
||||||
foreach ($setting['options'] as $key => $value) {
|
foreach ($setting['options'] as $key => $value) {
|
||||||
$optionParam = [
|
$optionParam = [
|
||||||
'class' => [],
|
'class' => [],
|
||||||
|
@ -88,10 +88,10 @@
|
||||||
$optionParam['selected'] = 'selected';
|
$optionParam['selected'] = 'selected';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$options[] = $appView->Bootstrap->genNode('option', $optionParam, h($value));
|
$options[] = $appView->Bootstrap->node('option', $optionParam, h($value));
|
||||||
}
|
}
|
||||||
$options = implode('', $options);
|
$options = implode('', $options);
|
||||||
return $appView->Bootstrap->genNode('select', [
|
return $appView->Bootstrap->node('select', [
|
||||||
'class' => [
|
'class' => [
|
||||||
'form-select',
|
'form-select',
|
||||||
'pe-4',
|
'pe-4',
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
$dependsOnHtml = '';
|
$dependsOnHtml = '';
|
||||||
if (!empty($setting['dependsOn'])) {
|
if (!empty($setting['dependsOn'])) {
|
||||||
$dependsOnHtml = $this->Bootstrap->genNode('span', [
|
$dependsOnHtml = $this->Bootstrap->node('span', [
|
||||||
'class' => [
|
'class' => [
|
||||||
'ms-1',
|
'ms-1',
|
||||||
'd-inline-block',
|
'd-inline-block',
|
||||||
|
@ -11,18 +11,18 @@
|
||||||
],
|
],
|
||||||
'style' => 'min-width: 0.75em;',
|
'style' => 'min-width: 0.75em;',
|
||||||
'title' => __('This setting depends on the validity of: {0}', h($setting['dependsOn'])),
|
'title' => __('This setting depends on the validity of: {0}', h($setting['dependsOn'])),
|
||||||
], $this->Bootstrap->genNode('sup', [
|
], $this->Bootstrap->node('sup', [
|
||||||
'class' => $this->FontAwesome->getClass('info'),
|
'class' => $this->FontAwesome->getClass('info'),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
$label = $this->Bootstrap->genNode('label', [
|
$label = $this->Bootstrap->node('label', [
|
||||||
'class' => ['form-label', 'fw-bolder', 'mb-0'],
|
'class' => ['form-label', 'fw-bolder', 'mb-0'],
|
||||||
'for' => $settingId
|
'for' => $settingId
|
||||||
], sprintf('<a id="lb-%s" href="#lb-%s" class="text-reset text-decoration-none">%s</a>', h($settingId), h($settingId), h($setting['name'])) . $dependsOnHtml);
|
], sprintf('<a id="lb-%s" href="#lb-%s" class="text-reset text-decoration-none">%s</a>', h($settingId), h($settingId), h($setting['name'])) . $dependsOnHtml);
|
||||||
|
|
||||||
$description = '';
|
$description = '';
|
||||||
if (!empty($setting['description']) && (empty($setting['type']) || $setting['type'] != 'boolean')) {
|
if (!empty($setting['description']) && (empty($setting['type']) || $setting['type'] != 'boolean')) {
|
||||||
$description = $this->Bootstrap->genNode('small', [
|
$description = $this->Bootstrap->node('small', [
|
||||||
'class' => ['form-text', 'text-muted', 'mt-0'],
|
'class' => ['form-text', 'text-muted', 'mt-0'],
|
||||||
'id' => "{$settingId}Help"
|
'id' => "{$settingId}Help"
|
||||||
], h($setting['description']));
|
], h($setting['description']));
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
if (!empty($setting['severity'])) {
|
if (!empty($setting['severity'])) {
|
||||||
$textColor = "text-{$this->get('variantFromSeverity')[$setting['severity']]}";
|
$textColor = "text-{$this->get('variantFromSeverity')[$setting['severity']]}";
|
||||||
}
|
}
|
||||||
$validationError = $this->Bootstrap->genNode('div', [
|
$validationError = $this->Bootstrap->node('div', [
|
||||||
'class' => ['d-block', 'invalid-feedback', $textColor],
|
'class' => ['d-block', 'invalid-feedback', $textColor],
|
||||||
], (!empty($setting['error']) ? h($setting['errorMessage']) : ''));
|
], (!empty($setting['error']) ? h($setting['errorMessage']) : ''));
|
||||||
|
|
||||||
|
@ -50,11 +50,11 @@
|
||||||
'variant' => 'success',
|
'variant' => 'success',
|
||||||
'class' => ['btn-setting-action', 'btn-save-setting', 'd-none'],
|
'class' => ['btn-setting-action', 'btn-save-setting', 'd-none'],
|
||||||
]);
|
]);
|
||||||
$inputGroup = $this->Bootstrap->genNode('div', [
|
$inputGroup = $this->Bootstrap->node('div', [
|
||||||
'class' => ['input-group'],
|
'class' => ['input-group'],
|
||||||
], implode('', [$input, $inputGroupSave]));
|
], implode('', [$input, $inputGroupSave]));
|
||||||
|
|
||||||
$container = $this->Bootstrap->genNode('div', [
|
$container = $this->Bootstrap->node('div', [
|
||||||
'class' => ['setting-group', 'row', 'mb-2']
|
'class' => ['setting-group', 'row', 'mb-2']
|
||||||
], implode('', [$label, $inputGroup, $description, $validationError]));
|
], implode('', [$label, $inputGroup, $description, $validationError]));
|
||||||
|
|
||||||
|
|
|
@ -50,14 +50,14 @@ foreach (array_keys($mainNoticeHeading) as $level) {
|
||||||
'bordered' => false,
|
'bordered' => false,
|
||||||
], [
|
], [
|
||||||
'fields' => [
|
'fields' => [
|
||||||
['key' => 'name', 'label' => __('Name'), 'formatter' => function($name, $row) {
|
['path' => 'name', 'label' => __('Name'), 'formatter' => function($name, $row) {
|
||||||
$settingID = preg_replace('/(\.|\W)/', '_', h($row['true-name']));
|
$settingID = preg_replace('/(\.|\W)/', '_', h($row['true-name']));
|
||||||
return sprintf('<a style="max-width: 200px; white-space: pre-wrap;" href="#lb-%s" onclick="redirectToSetting(\'#lb-%s\')">%s</a>', $settingID, $settingID, h($name));
|
return sprintf('<a style="max-width: 200px; white-space: pre-wrap;" href="#lb-%s" onclick="redirectToSetting(\'#lb-%s\')">%s</a>', $settingID, $settingID, h($name));
|
||||||
}],
|
}],
|
||||||
['key' => 'setting-path', 'label' => __('Category'), 'formatter' => function($path, $row) {
|
['path' => 'setting-path', 'label' => __('Category'), 'formatter' => function($path, $row) {
|
||||||
return '<span class="text-nowrap">' . h(str_replace('.', ' ▸ ', $path)) . '</span>';
|
return '<span class="text-nowrap">' . h(str_replace('.', ' ▸ ', $path)) . '</span>';
|
||||||
}],
|
}],
|
||||||
['key' => 'value', 'label' => __('Value'), 'formatter' => function($value, $row) {
|
['path' => 'value', 'label' => __('Value'), 'formatter' => function($value, $row) {
|
||||||
$formatedValue = '<span class="p-1 rounded mb-0" style="background: #eeeeee55; font-family: monospace;">';
|
$formatedValue = '<span class="p-1 rounded mb-0" style="background: #eeeeee55; font-family: monospace;">';
|
||||||
if (is_null($value)) {
|
if (is_null($value)) {
|
||||||
$formatedValue .= '<i class="text-nowrap">' . __('No value') . '</i>';
|
$formatedValue .= '<i class="text-nowrap">' . __('No value') . '</i>';
|
||||||
|
@ -71,7 +71,7 @@ foreach (array_keys($mainNoticeHeading) as $level) {
|
||||||
$formatedValue .= '</span>';
|
$formatedValue .= '</span>';
|
||||||
return $formatedValue;
|
return $formatedValue;
|
||||||
}],
|
}],
|
||||||
['key' => 'description', 'label' => __('Description')]
|
['path' => 'description', 'label' => __('Description')]
|
||||||
],
|
],
|
||||||
'items' => $notices[$level],
|
'items' => $notices[$level],
|
||||||
]);
|
]);
|
||||||
|
@ -87,14 +87,14 @@ $alertBody = $this->Bootstrap->table([
|
||||||
'tableClass' => 'mb-0'
|
'tableClass' => 'mb-0'
|
||||||
], [
|
], [
|
||||||
'fields' => [
|
'fields' => [
|
||||||
['key' => 'severity', 'label' => __('Severity')],
|
['path' => 'severity', 'label' => __('Severity')],
|
||||||
['key' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) {
|
['path' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) {
|
||||||
return $this->Bootstrap->badge([
|
return $this->Bootstrap->badge([
|
||||||
'variant' => $row['badge-variant'],
|
'variant' => $row['badge-variant'],
|
||||||
'text' => $count
|
'text' => $count
|
||||||
]);
|
]);
|
||||||
}],
|
}],
|
||||||
['key' => 'description', 'label' => __('Description')]
|
['path' => 'description', 'label' => __('Description')]
|
||||||
],
|
],
|
||||||
'items' => $tableItems,
|
'items' => $tableItems,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -34,7 +34,7 @@ if (isLeaf($panelSettings)) {
|
||||||
h($panelName)
|
h($panelName)
|
||||||
);
|
);
|
||||||
if (!empty($panelSettings['_description'])) {
|
if (!empty($panelSettings['_description'])) {
|
||||||
$panelHTML .= $this->Bootstrap->genNode('div', [
|
$panelHTML .= $this->Bootstrap->node('div', [
|
||||||
'class' => ['mb-1',],
|
'class' => ['mb-1',],
|
||||||
], h($panelSettings['_description']));
|
], h($panelSettings['_description']));
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ if (isLeaf($panelSettings)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$panelHTML = $this->Bootstrap->genNode('div', [
|
$panelHTML = $this->Bootstrap->node('div', [
|
||||||
'class' => [
|
'class' => [
|
||||||
'shadow',
|
'shadow',
|
||||||
'p-2',
|
'p-2',
|
||||||
|
|
|
@ -5,19 +5,17 @@ $table = $this->Bootstrap->table([
|
||||||
'hover' => false,
|
'hover' => false,
|
||||||
], [
|
], [
|
||||||
'fields' => [
|
'fields' => [
|
||||||
['key' => 'label', 'label' => __('Label')],
|
['path' => 'label', 'label' => __('Label')],
|
||||||
['key' => 'name', 'label' => __('Name')],
|
['path' => 'name', 'label' => __('Name')],
|
||||||
['key' => 'url', 'label' => __('URL'), 'formatter' => function ($value, $row) {
|
['path' => 'url', 'label' => __('URL'), 'formatter' => function ($value, $row) {
|
||||||
return sprintf('<span class="font-monospace">%s</span>', h($value));
|
return sprintf('<span class="font-monospace">%s</span>', h($value));
|
||||||
}],
|
}],
|
||||||
['key' => 'action', 'label' => __('Action'), 'formatter' => function ($value, $row, $index) {
|
['path' => 'action', 'label' => __('Action'), 'formatter' => function ($value, $row, $index) {
|
||||||
return $this->Bootstrap->button([
|
return $this->Bootstrap->button([
|
||||||
'icon' => 'trash',
|
'icon' => 'trash',
|
||||||
'variant' => 'danger',
|
'variant' => 'danger',
|
||||||
'size' => 'sm',
|
'size' => 'sm',
|
||||||
'params' => [
|
'onclick' => sprintf('deleteBookmark(window.bookmarks[%s])', $index),
|
||||||
'onclick' => sprintf('deleteBookmark(window.bookmarks[%s])', $index),
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
}],
|
}],
|
||||||
],
|
],
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
'_open' => true,
|
'open' => true,
|
||||||
'header' => [
|
'header' => [
|
||||||
'title' => __('Meta fields')
|
'title' => __('Meta fields')
|
||||||
],
|
],
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
'_open' => true,
|
'open' => true,
|
||||||
'header' => [
|
'header' => [
|
||||||
'title' => __('Meta fields')
|
'title' => __('Meta fields')
|
||||||
],
|
],
|
||||||
|
|
|
@ -82,7 +82,7 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$fieldContainer = $this->Bootstrap->genNode('div', [
|
$fieldContainer = $this->Bootstrap->node('div', [
|
||||||
'class' => [],
|
'class' => [],
|
||||||
], $fieldsHtml);
|
], $fieldsHtml);
|
||||||
echo $fieldContainer;
|
echo $fieldContainer;
|
|
@ -41,7 +41,7 @@ foreach ($statistics['usage'] as $scope => $graphData) {
|
||||||
'nodeType' => 'a',
|
'nodeType' => 'a',
|
||||||
'onclick' => '',
|
'onclick' => '',
|
||||||
'class' => ['btn-statistics-pie-configurator-' . $seedPiechart],
|
'class' => ['btn-statistics-pie-configurator-' . $seedPiechart],
|
||||||
'params' => [
|
'attrs' => [
|
||||||
'data-bs-toggle' => 'popover',
|
'data-bs-toggle' => 'popover',
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
@ -52,7 +52,7 @@ foreach ($statistics['usage'] as $scope => $graphData) {
|
||||||
$pieChart
|
$pieChart
|
||||||
);
|
);
|
||||||
$statPie = $this->Bootstrap->card([
|
$statPie = $this->Bootstrap->card([
|
||||||
'variant' => 'secondary',
|
'bodyVariant' => 'secondary',
|
||||||
'bodyHTML' => $panelHtml,
|
'bodyHTML' => $panelHtml,
|
||||||
'bodyClass' => 'py-1 px-2',
|
'bodyClass' => 'py-1 px-2',
|
||||||
'class' => ['shadow-sm', 'h-100']
|
'class' => ['shadow-sm', 'h-100']
|
||||||
|
|
|
@ -38,7 +38,7 @@ $panelControlHtml = sprintf(
|
||||||
'nodeType' => 'a',
|
'nodeType' => 'a',
|
||||||
'onclick' => '',
|
'onclick' => '',
|
||||||
'class' => ['btn-statistics-days-configurator-' . $seed,],
|
'class' => ['btn-statistics-days-configurator-' . $seed,],
|
||||||
'params' => [
|
'attrs' => [
|
||||||
'data-bs-toggle' => 'popover',
|
'data-bs-toggle' => 'popover',
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
@ -46,13 +46,13 @@ $panelControlHtml = sprintf(
|
||||||
$createdNumber = empty($timeline['created']) ? '' : sprintf(
|
$createdNumber = empty($timeline['created']) ? '' : sprintf(
|
||||||
'<div class="lh-1 d-flex align-items-center" title="%s">%s<span class="ms-1"> %s</span></div>',
|
'<div class="lh-1 d-flex align-items-center" title="%s">%s<span class="ms-1"> %s</span></div>',
|
||||||
__('{0} Created', $timeline['created']['variation']),
|
__('{0} Created', $timeline['created']['variation']),
|
||||||
$this->Bootstrap->icon('plus', ['class' => ['fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
|
$this->Bootstrap->icon('plus', ['class' => ['fa-fw'], 'attrs' => ['style' => 'font-size: 60%;']]),
|
||||||
$timeline['created']['variation']
|
$timeline['created']['variation']
|
||||||
);
|
);
|
||||||
$modifiedNumber = empty($timeline['modified']) ? '' : sprintf(
|
$modifiedNumber = empty($timeline['modified']) ? '' : sprintf(
|
||||||
'<div class="lh-1 d-flex align-items-center" title="%s">%s<span class="ms-1"> %s</span></div>',
|
'<div class="lh-1 d-flex align-items-center" title="%s">%s<span class="ms-1"> %s</span></div>',
|
||||||
__('{0} Modified', $timeline['modified']['variation']),
|
__('{0} Modified', $timeline['modified']['variation']),
|
||||||
$this->Bootstrap->icon('edit', ['class' => ['fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
|
$this->Bootstrap->icon('edit', ['class' => ['fa-fw'], 'attrs' => ['style' => 'font-size: 60%;']]),
|
||||||
$timeline['modified']['variation']
|
$timeline['modified']['variation']
|
||||||
);
|
);
|
||||||
$activityNumbers = sprintf('<div class="my-1 fs-5">%s%s</div>', $createdNumber, $modifiedNumber);
|
$activityNumbers = sprintf('<div class="my-1 fs-5">%s%s</div>', $createdNumber, $modifiedNumber);
|
||||||
|
@ -87,7 +87,7 @@ $cardContent = sprintf(
|
||||||
);
|
);
|
||||||
|
|
||||||
$card = $this->Bootstrap->card([
|
$card = $this->Bootstrap->card([
|
||||||
'variant' => 'secondary',
|
'bodyVariant' => 'secondary',
|
||||||
'bodyHTML' => $cardContent,
|
'bodyHTML' => $cardContent,
|
||||||
'bodyClass' => 'py-1 px-2',
|
'bodyClass' => 'py-1 px-2',
|
||||||
'class' => ['shadow-sm', 'h-100']
|
'class' => ['shadow-sm', 'h-100']
|
||||||
|
|
|
@ -14,6 +14,7 @@ use Cake\Utility\Text;
|
||||||
* ),
|
* ),
|
||||||
* 'title' => optional title,
|
* 'title' => optional title,
|
||||||
* 'description' => optional description,
|
* 'description' => optional description,
|
||||||
|
* 'notice' => optional alert to be placed at the top,
|
||||||
* 'index_statistics' => optional statistics to be displayed for the index,
|
* 'index_statistics' => optional statistics to be displayed for the index,
|
||||||
* 'primary_id_path' => path to each primary ID (extracted and passed as $primary to fields)
|
* 'primary_id_path' => path to each primary ID (extracted and passed as $primary to fields)
|
||||||
* ));
|
* ));
|
||||||
|
@ -48,7 +49,7 @@ if (!empty($data['title'])) {
|
||||||
'help' => $this->Bootstrap->icon('info', [
|
'help' => $this->Bootstrap->icon('info', [
|
||||||
'class' => ['fs-6', 'align-text-top',],
|
'class' => ['fs-6', 'align-text-top',],
|
||||||
'title' => empty($data['description']) ? '' : h($data['description']),
|
'title' => empty($data['description']) ? '' : h($data['description']),
|
||||||
'params' => [
|
'attrs' => [
|
||||||
'data-bs-toggle' => 'tooltip',
|
'data-bs-toggle' => 'tooltip',
|
||||||
]
|
]
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
'text' => $child['text'],
|
'text' => $child['text'],
|
||||||
'outline' => !empty($child['outline']),
|
'outline' => !empty($child['outline']),
|
||||||
'icon' => $child['icon'] ?? null,
|
'icon' => $child['icon'] ?? null,
|
||||||
'params' => array_merge([
|
'attrs' => array_merge([
|
||||||
'data-onclick-function' => $child['onclick'] ?? '',
|
'data-onclick-function' => $child['onclick'] ?? '',
|
||||||
'data-table-random-value' => $tableRandomValue,
|
'data-table-random-value' => $tableRandomValue,
|
||||||
'onclick' => 'multiActionClickHandler(this)'
|
'onclick' => 'multiActionClickHandler(this)'
|
||||||
|
|
|
@ -31,10 +31,8 @@
|
||||||
$buttonConfig = [
|
$buttonConfig = [
|
||||||
'icon' => 'filter',
|
'icon' => 'filter',
|
||||||
'variant' => $numberActiveFilters > 0 ? 'warning' : 'primary',
|
'variant' => $numberActiveFilters > 0 ? 'warning' : 'primary',
|
||||||
'params' => [
|
'title' => __('Filter index'),
|
||||||
'title' => __('Filter index'),
|
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
||||||
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
|
|
||||||
]
|
|
||||||
];
|
];
|
||||||
if (count($activeFilters) > 0) {
|
if (count($activeFilters) > 0) {
|
||||||
$buttonConfig['badge'] = [
|
$buttonConfig['badge'] = [
|
||||||
|
|
|
@ -67,14 +67,14 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
|
||||||
'dropdown-class' => 'ms-1',
|
'dropdown-class' => 'ms-1',
|
||||||
'alignment' => 'end',
|
'alignment' => 'end',
|
||||||
'direction' => 'down',
|
'direction' => 'down',
|
||||||
'toggle-button' => [
|
'button' => [
|
||||||
'icon' => 'sliders-h',
|
'icon' => 'sliders-h',
|
||||||
'variant' => 'primary',
|
'variant' => 'primary',
|
||||||
'class' => ['table_setting_dropdown_button'],
|
'class' => ['table_setting_dropdown_button'],
|
||||||
],
|
],
|
||||||
'submenu_alignment' => 'end',
|
'submenu_alignment' => 'end',
|
||||||
'submenu_direction' => 'start',
|
'submenu_direction' => 'start',
|
||||||
'params' => [
|
'attrs' => [
|
||||||
'data-table-random-value' => $tableRandomValue,
|
'data-table-random-value' => $tableRandomValue,
|
||||||
'data-table_setting_id' => $data['table_setting_id'],
|
'data-table_setting_id' => $data['table_setting_id'],
|
||||||
],
|
],
|
||||||
|
|
|
@ -28,7 +28,7 @@ foreach ($table_data['fields'] as $field) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$availableColumnsHtml = $this->Bootstrap->genNode('form', [
|
$availableColumnsHtml = $this->Bootstrap->node('form', [
|
||||||
'class' => ['visible-column-form', 'px-2 py-1'],
|
'class' => ['visible-column-form', 'px-2 py-1'],
|
||||||
], $availableColumnsHtml);
|
], $availableColumnsHtml);
|
||||||
echo $availableColumnsHtml;
|
echo $availableColumnsHtml;
|
||||||
|
|
|
@ -26,7 +26,7 @@ if (!empty($meta_template)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$availableMetaColumnsHtml = $this->Bootstrap->genNode('form', [
|
$availableMetaColumnsHtml = $this->Bootstrap->node('form', [
|
||||||
'class' => ['visible-meta-column-form', 'px-2 py-1'],
|
'class' => ['visible-meta-column-form', 'px-2 py-1'],
|
||||||
], $availableMetaColumnsHtml);
|
], $availableMetaColumnsHtml);
|
||||||
echo $availableMetaColumnsHtml;
|
echo $availableMetaColumnsHtml;
|
||||||
|
|
|
@ -48,7 +48,7 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
|
||||||
'text' => __('Migrate to version {0}', $metaTemplate['hasNewerVersion']->version),
|
'text' => __('Migrate to version {0}', $metaTemplate['hasNewerVersion']->version),
|
||||||
'variant' => 'success',
|
'variant' => 'success',
|
||||||
'nodeType' => 'a',
|
'nodeType' => 'a',
|
||||||
'params' => [
|
'attrs' => [
|
||||||
'href' => Router::url([
|
'href' => Router::url([
|
||||||
'controller' => 'metaTemplates',
|
'controller' => 'metaTemplates',
|
||||||
'action' => 'migrateOldMetaTemplateToNewestVersionForEntity',
|
'action' => 'migrateOldMetaTemplateToNewestVersionForEntity',
|
||||||
|
|
|
@ -25,6 +25,7 @@ $variant = array_flip($severity)[$maxSeverity];
|
||||||
if ($hasNotification) {
|
if ($hasNotification) {
|
||||||
echo $this->Bootstrap->notificationBubble([
|
echo $this->Bootstrap->notificationBubble([
|
||||||
'variant' => $variant,
|
'variant' => $variant,
|
||||||
|
'borderVariant' => 'light',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -6,7 +6,5 @@ echo $this->Bootstrap->button([
|
||||||
'variant' => 'primary',
|
'variant' => 'primary',
|
||||||
'size' => 'sm',
|
'size' => 'sm',
|
||||||
'class' => 'mb-1',
|
'class' => 'mb-1',
|
||||||
'params' => [
|
'id' => 'btn-add-bookmark',
|
||||||
'id' => 'btn-add-bookmark',
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
'size' => 'sm',
|
'size' => 'sm',
|
||||||
'icon' => h($icon),
|
'icon' => h($icon),
|
||||||
'class' => ['mb-1', !$validURI ? 'disabled' : ''],
|
'class' => ['mb-1', !$validURI ? 'disabled' : ''],
|
||||||
'params' => [
|
'attrs' => [
|
||||||
'href' => $validURI ? h($url) : '#',
|
'href' => $validURI ? h($url) : '#',
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
if ($childHasNotification || ($hasNotification && !empty($children))) {
|
if ($childHasNotification || ($hasNotification && !empty($children))) {
|
||||||
echo $this->Bootstrap->notificationBubble([
|
echo $this->Bootstrap->notificationBubble([
|
||||||
'variant' => $childHasNotification ? $childNotificationVariant : $notificationVariant,
|
'variant' => $childHasNotification ? $childNotificationVariant : $notificationVariant,
|
||||||
|
'borderVariant' => 'light',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -77,7 +77,7 @@ $cardContent = sprintf(
|
||||||
);
|
);
|
||||||
|
|
||||||
echo $this->Bootstrap->card([
|
echo $this->Bootstrap->card([
|
||||||
'variant' => 'secondary',
|
'bodyVariant' => 'secondary',
|
||||||
'bodyHTML' => $cardContent,
|
'bodyHTML' => $cardContent,
|
||||||
'bodyClass' => 'p-3',
|
'bodyClass' => 'p-3',
|
||||||
'class' => ['shadow-sm', (empty($panelNoGrow) ? 'grow-on-hover' : '')]
|
'class' => ['shadow-sm', (empty($panelNoGrow) ? 'grow-on-hover' : '')]
|
||||||
|
|
|
@ -19,12 +19,12 @@ $filteringForm = $this->Bootstrap->table(
|
||||||
[
|
[
|
||||||
'fields' => [
|
'fields' => [
|
||||||
[
|
[
|
||||||
'key' => 'fieldname', 'label' => __('Field'), 'formatter' => function ($field, $row) {
|
'path' => 'fieldname', 'label' => __('Field'), 'formatter' => function ($field, $row) {
|
||||||
return sprintf('<span class="fieldName" data-fieldname="%s">%s</span>', h($field), h($field));
|
return sprintf('<span class="fieldName" data-fieldname="%s">%s</span>', h($field), h($field));
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
|
'path' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
|
||||||
$fieldName = $row['fieldname'];
|
$fieldName = $row['fieldname'];
|
||||||
$type = $typeMap[$fieldName] ?? 'text';
|
$type = $typeMap[$fieldName] ?? 'text';
|
||||||
$options = [
|
$options = [
|
||||||
|
@ -41,7 +41,7 @@ $filteringForm = $this->Bootstrap->table(
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'value',
|
'path' => 'value',
|
||||||
'labelHtml' => sprintf(
|
'labelHtml' => sprintf(
|
||||||
'%s %s',
|
'%s %s',
|
||||||
__('Value'),
|
__('Value'),
|
||||||
|
@ -71,23 +71,23 @@ $filteringForm = $this->Bootstrap->table(
|
||||||
|
|
||||||
$filteringMetafields = '';
|
$filteringMetafields = '';
|
||||||
if ($metaFieldsEnabled) {
|
if ($metaFieldsEnabled) {
|
||||||
$helpText = $this->Bootstrap->genNode('sup', [
|
$helpText = $this->Bootstrap->node('sup', [
|
||||||
'class' => ['ms-1 fa fa-info'],
|
'class' => ['ms-1 fa fa-info'],
|
||||||
'title' => __('Include help'),
|
'title' => __('Include help'),
|
||||||
'data-bs-toggle' => 'tooltip',
|
'data-bs-toggle' => 'tooltip',
|
||||||
]);
|
]);
|
||||||
$filteringMetafields = $this->Bootstrap->genNode('h5', [], __('Meta Fields') . $helpText);
|
$filteringMetafields = $this->Bootstrap->node('h5', [], __('Meta Fields') . $helpText);
|
||||||
$filteringMetafields .= $this->element('genericElements/IndexTable/metafield_filtering', $metaTemplates);
|
$filteringMetafields .= $this->element('genericElements/IndexTable/metafield_filtering', $metaTemplates);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filteringTags = '';
|
$filteringTags = '';
|
||||||
if ($taggingEnabled) {
|
if ($taggingEnabled) {
|
||||||
$helpText = $this->Bootstrap->genNode('sup', [
|
$helpText = $this->Bootstrap->node('sup', [
|
||||||
'class' => ['ms-1 fa fa-info'],
|
'class' => ['ms-1 fa fa-info'],
|
||||||
'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character). Example: `!exportable`, `%able`'),
|
'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character). Example: `!exportable`, `%able`'),
|
||||||
'data-bs-toggle' => 'tooltip',
|
'data-bs-toggle' => 'tooltip',
|
||||||
]);
|
]);
|
||||||
$filteringTags = $this->Bootstrap->genNode('h5', [
|
$filteringTags = $this->Bootstrap->node('h5', [
|
||||||
'class' => 'mt-2'
|
'class' => 'mt-2'
|
||||||
], __('Tags') . $helpText);
|
], __('Tags') . $helpText);
|
||||||
$filteringTags .= $this->Tag->tags([], [
|
$filteringTags .= $this->Tag->tags([], [
|
||||||
|
@ -104,7 +104,9 @@ echo $this->Bootstrap->modal([
|
||||||
'size' => !empty($metaFieldsEnabled) ? 'xl' : 'lg',
|
'size' => !empty($metaFieldsEnabled) ? 'xl' : 'lg',
|
||||||
'type' => 'confirm',
|
'type' => 'confirm',
|
||||||
'bodyHtml' => $modalBody,
|
'bodyHtml' => $modalBody,
|
||||||
'confirmText' => __('Filter'),
|
'confirmButton' => [
|
||||||
|
'text' => __('Filter'),
|
||||||
|
],
|
||||||
'confirmFunction' => 'filterIndex'
|
'confirmFunction' => 'filterIndex'
|
||||||
]);
|
]);
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -359,6 +359,9 @@ class AJAXApi {
|
||||||
if (!skipRequestHooks) {
|
if (!skipRequestHooks) {
|
||||||
this.beforeRequest()
|
this.beforeRequest()
|
||||||
}
|
}
|
||||||
|
if (form === undefined || form.nodeName !== 'FORM') {
|
||||||
|
throw new Error(`Form argument must be a valid HTMLFormELement.`)
|
||||||
|
}
|
||||||
let toReturn
|
let toReturn
|
||||||
let feedbackShown = false
|
let feedbackShown = false
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -257,6 +257,9 @@ class Toaster {
|
||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.options = Object.assign({}, Toaster.defaultOptions, options)
|
this.options = Object.assign({}, Toaster.defaultOptions, options)
|
||||||
|
if (this.options.delay == 'auto') {
|
||||||
|
this.options.delay = this.computeDelay()
|
||||||
|
}
|
||||||
this.bsToastOptions = {
|
this.bsToastOptions = {
|
||||||
autohide: this.options.autohide,
|
autohide: this.options.autohide,
|
||||||
delay: this.options.delay,
|
delay: this.options.delay,
|
||||||
|
@ -271,7 +274,7 @@ class Toaster {
|
||||||
* @property {string} body - The body's content of the toast
|
* @property {string} body - The body's content of the toast
|
||||||
* @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the toast
|
* @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the toast
|
||||||
* @property {boolean} autohide - If the toast show be hidden after some time defined by the delay
|
* @property {boolean} autohide - If the toast show be hidden after some time defined by the delay
|
||||||
* @property {number} delay - The number of milliseconds the toast should stay visible before being hidden
|
* @property {(number|string)} delay - The number of milliseconds the toast should stay visible before being hidden or 'auto' to deduce the delay based on the content
|
||||||
* @property {(jQuery|string)} titleHtml - The raw HTML title's content of the toast
|
* @property {(jQuery|string)} titleHtml - The raw HTML title's content of the toast
|
||||||
* @property {(jQuery|string)} mutedHtml - The raw HTML muted's content of the toast
|
* @property {(jQuery|string)} mutedHtml - The raw HTML muted's content of the toast
|
||||||
* @property {(jQuery|string)} bodyHtml - The raw HTML body's content of the toast
|
* @property {(jQuery|string)} bodyHtml - The raw HTML body's content of the toast
|
||||||
|
@ -284,7 +287,7 @@ class Toaster {
|
||||||
body: false,
|
body: false,
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
autohide: true,
|
autohide: true,
|
||||||
delay: 5000,
|
delay: 'auto',
|
||||||
titleHtml: false,
|
titleHtml: false,
|
||||||
mutedHtml: false,
|
mutedHtml: false,
|
||||||
bodyHtml: false,
|
bodyHtml: false,
|
||||||
|
@ -389,6 +392,12 @@ class Toaster {
|
||||||
}
|
}
|
||||||
return $toast
|
return $toast
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeDelay() {
|
||||||
|
return 3000
|
||||||
|
+ 40*((this.options.title?.length ?? 0) + (this.options.body?.length ?? 0))
|
||||||
|
+ (['danger', 'warning'].includes(this.options.variant) ? 5000 : 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Class representing a Modal */
|
/** Class representing a Modal */
|
||||||
|
@ -400,15 +409,16 @@ class ModalFactory {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.options = Object.assign({}, ModalFactory.defaultOptions, options)
|
this.options = Object.assign({}, ModalFactory.defaultOptions, options)
|
||||||
if (options.POSTSuccessCallback !== undefined) {
|
if (options.POSTSuccessCallback !== undefined) {
|
||||||
if (this.options.rawHtml) {
|
if (!this.options.rawHtml) {
|
||||||
this.attachSubmitButtonListener = true
|
|
||||||
} else {
|
|
||||||
UI.toast({
|
UI.toast({
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
bodyHtml: '<b>POSTSuccessCallback</b> can only be used in conjuction with the <i>rawHtml</i> option. Instead, use the promise instead returned by the API call in <b>APIConfirm</b>.'
|
bodyHtml: '<b>POSTSuccessCallback</b> can only be used in conjuction with the <i>rawHtml</i> option. Instead, use the promise instead returned by the API call in <b>APIConfirm</b>.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.options.rawHtml) {
|
||||||
|
this.attachSubmitButtonListener = true
|
||||||
|
}
|
||||||
if (options.type === undefined && options.cancel !== undefined) {
|
if (options.type === undefined && options.cancel !== undefined) {
|
||||||
this.options.type = 'confirm'
|
this.options.type = 'confirm'
|
||||||
}
|
}
|
||||||
|
@ -794,17 +804,25 @@ class ModalFactory {
|
||||||
$form = this.$modal.find('form')
|
$form = this.$modal.find('form')
|
||||||
}
|
}
|
||||||
if ($submitButton.data('confirmfunction') !== undefined && $submitButton.data('confirmfunction') !== '') {
|
if ($submitButton.data('confirmfunction') !== undefined && $submitButton.data('confirmfunction') !== '') {
|
||||||
|
$submitButton[0].removeAttribute('onclick')
|
||||||
const clickHandler = window[$submitButton.data('confirmfunction')]
|
const clickHandler = window[$submitButton.data('confirmfunction')]
|
||||||
|
if (clickHandler === undefined) {
|
||||||
|
console.error(`Function \`${$submitButton.data('confirmfunction')}\` is not defined`)
|
||||||
|
}
|
||||||
this.options.APIConfirm = (tmpApi) => {
|
this.options.APIConfirm = (tmpApi) => {
|
||||||
let clickResult = clickHandler(this, tmpApi)
|
let clickResult = clickHandler(this, tmpApi)
|
||||||
if (clickResult !== undefined) {
|
if (clickResult !== undefined) {
|
||||||
return clickResult
|
return clickResult
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.success) {
|
if (!data) {
|
||||||
this.options.POSTSuccessCallback([data, this])
|
this.options.POSTSuccessCallback([data, this])
|
||||||
} else { // Validation error
|
} else {
|
||||||
this.injectFormValidationFeedback(form, data.errors)
|
if (data.success == undefined || data.success) {
|
||||||
return Promise.reject('Validation error');
|
this.options.POSTSuccessCallback([data, this])
|
||||||
|
} else { // Validation error
|
||||||
|
this.injectFormValidationFeedback(form, data.errors)
|
||||||
|
return Promise.reject('Validation error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((errorMessage) => {
|
.catch((errorMessage) => {
|
||||||
|
@ -814,23 +832,28 @@ class ModalFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$submitButton[0].removeAttribute('onclick')
|
if ($form[0]) {
|
||||||
this.options.APIConfirm = (tmpApi) => {
|
// Submit the form via the AJAXApi
|
||||||
return tmpApi.postForm($form[0])
|
$submitButton[0].removeAttribute('onclick')
|
||||||
.then((data) => {
|
this.options.APIConfirm = (tmpApi) => {
|
||||||
if (data.success) {
|
return tmpApi.postForm($form[0])
|
||||||
// this.options.POSTSuccessCallback(data)
|
.then((data) => {
|
||||||
this.options.POSTSuccessCallback([data, this])
|
if (!data) {
|
||||||
} else { // Validation error
|
this.options.POSTSuccessCallback([data, this])
|
||||||
this.injectFormValidationFeedback(form, data.errors)
|
} else {
|
||||||
return Promise.reject('Validation error');
|
if (data.success == undefined || data.success) {
|
||||||
}
|
this.options.POSTSuccessCallback([data, this])
|
||||||
})
|
} else { // Validation error
|
||||||
.catch((errorMessage) => {
|
this.injectFormValidationFeedback(form, data.errors)
|
||||||
this.options.POSTFailCallback([errorMessage, this])
|
return Promise.reject('Validation error');
|
||||||
// this.options.POSTFailCallback(errorMessage)
|
}
|
||||||
return Promise.reject(errorMessage);
|
}
|
||||||
})
|
})
|
||||||
|
.catch((errorMessage) => {
|
||||||
|
this.options.POSTFailCallback([errorMessage, this])
|
||||||
|
return Promise.reject(errorMessage);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$submitButton.click(this.getConfirmationHandlerFunction($submitButton))
|
$submitButton.click(this.getConfirmationHandlerFunction($submitButton))
|
||||||
|
@ -877,7 +900,7 @@ class OverlayFactory {
|
||||||
spinnerVariant: '',
|
spinnerVariant: '',
|
||||||
spinnerSmall: false,
|
spinnerSmall: false,
|
||||||
spinnerType: 'border',
|
spinnerType: 'border',
|
||||||
fallbackBoostrapVariant: '',
|
fallbackBootstrapVariant: '',
|
||||||
wrapperCSSDisplay: '',
|
wrapperCSSDisplay: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -976,7 +999,7 @@ class OverlayFactory {
|
||||||
let classes = this.$node.attr('class')
|
let classes = this.$node.attr('class')
|
||||||
if (classes !== undefined) {
|
if (classes !== undefined) {
|
||||||
classes = classes.split(' ')
|
classes = classes.split(' ')
|
||||||
const detectedVariant = OverlayFactory.detectedBootstrapVariant(classes, this.options.fallbackBoostrapVariant)
|
const detectedVariant = OverlayFactory.detectedBootstrapVariant(classes, this.options.fallbackBootstrapVariant)
|
||||||
this.options.spinnerVariant = detectedVariant
|
this.options.spinnerVariant = detectedVariant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -985,7 +1008,7 @@ class OverlayFactory {
|
||||||
* Detect the bootstrap variant from a list of classes
|
* Detect the bootstrap variant from a list of classes
|
||||||
* @param {Array} classes - A list of classes containg a bootstrap variant
|
* @param {Array} classes - A list of classes containg a bootstrap variant
|
||||||
*/
|
*/
|
||||||
static detectedBootstrapVariant(classes, fallback=OverlayFactory.defaultOptions.fallbackBoostrapVariant) {
|
static detectedBootstrapVariant(classes, fallback = OverlayFactory.defaultOptions.fallbackBootstrapVariant) {
|
||||||
const re = /^[a-zA-Z]+-(?<variant>primary|success|danger|warning|info|light|dark|white|transparent)$/;
|
const re = /^[a-zA-Z]+-(?<variant>primary|success|danger|warning|info|light|dark|white|transparent)$/;
|
||||||
let result
|
let result
|
||||||
for (let i=0; i<classes.length; i++) {
|
for (let i=0; i<classes.length; i++) {
|
||||||
|
|
Loading…
Reference in New Issue