chg: [ui] Migrated UI from Cerebrate and adapted to MISP logo usage

pull/9075/head
Sami Mokaddem 2023-03-24 09:13:03 +01:00
parent b1b8024ad0
commit 2bbbe69d6d
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
73 changed files with 4898 additions and 7483 deletions

View File

@ -74,8 +74,10 @@ echo $this->Bootstrap->modal([
'<div class="form-container">%s</div>',
$combinedForm
),
'confirmText' => __('Create user'),
'confirmFunction' => 'submitRegistration'
'confirmButton' => [
'text' => __('Create user'),
'onclick' => 'submitRegistration',
],
]);
?>
</div>

View File

@ -60,9 +60,7 @@ class TagHelper extends Helper
'icon' => 'plus',
'variant' => 'secondary',
'class' => ['badge'],
'params' => [
'onclick' => 'createTagPicker(this)',
]
'onclick' => 'createTagPicker(this)',
]);
} else {
$html .= '<script>$(document).ready(function() { initSelect2Pickers() })</script>';
@ -111,22 +109,20 @@ class TagHelper extends Helper
'class' => ['ms-1', 'border-0', "text-${textColour}"],
'variant' => 'text',
'title' => __('Delete tag'),
'params' => [
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
$this->Url->build([
'controller' => $this->getView()->getName(),
'action' => 'untag',
$this->getView()->get('entity')['id']
]),
h($tag['name'])
),
],
'onclick' => sprintf('deleteTag(\'%s\', \'%s\', this)',
$this->Url->build([
'controller' => $this->getView()->getName(),
'action' => 'untag',
$this->getView()->get('entity')['id']
]),
h($tag['name'])
),
]);
} else {
$deleteButton = '';
}
$html = $this->Bootstrap->genNode('span', [
$html = $this->Bootstrap->node('span', [
'class' => [
'tag',
'badge',

View File

@ -120,8 +120,9 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
]));
\SocialConnect\JWX\JWT::$screw = Configure::check('keycloak.screw') ? Configure::read('keycloak.screw') : 0;
}
$middlewareQueue->add(new AuthenticationMiddleware($this))
->add(new BodyParserMiddleware());
$middlewareQueue
->add(new BodyParserMiddleware())
->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}

View File

@ -9,6 +9,8 @@ use Cake\Utility\Inflector;
use Cake\Utility\Text;
use Cake\View\ViewBuilder;
use Cake\ORM\TableRegistry;
use Cake\ORM\Query;
use Cake\Database\Expression\QueryExpression;
use Cake\Routing\Router;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\NotFoundException;
@ -32,6 +34,10 @@ class CRUDComponent extends Component
public function index(array $options): void
{
$embedInModal = !empty($this->request->getQuery('embedInModal', false));
$excludeStats = !empty($this->request->getQuery('excludeStats', false));
$skipTableToolbar = !empty($this->request->getQuery('skipTableToolbar', false));
if (!empty($options['quickFilters'])) {
if (empty($options['filters'])) {
$options['filters'] = [];
@ -45,9 +51,12 @@ class CRUDComponent extends Component
$options['filters'][] = 'filteringTags';
}
$optionFilters = empty($options['filters']) ? [] : $options['filters'];
$optionFilters = [];
$optionFilters += empty($options['filters']) ? [] : $options['filters'];
foreach ($optionFilters as $i => $filter) {
$optionFilters[] = "{$filter} !=";
$optionFilters[] = "{$filter} >=";
$optionFilters[] = "{$filter} <=";
}
$params = $this->Controller->ParamHandler->harvestParams($optionFilters);
$params = $this->fakeContextFilter($options, $params);
@ -70,13 +79,22 @@ class CRUDComponent extends Component
$query->select($options['fields']);
}
if (!empty($options['order'])) {
$query->order($options['order']);
}
if ($this->Controller->ParamHandler->isRest()) {
if ($this->metaFieldsSupported()) {
$query = $this->includeRequestedMetaFields($query);
$orderFields = array_keys($options['order']);
if ($this->_validOrderFields($orderFields)) {
$query->order($options['order']);
$this->Controller->paginate['order'] = $options['order'];
}
$data = $query->all();
}
if ($this->metaFieldsSupported() && !$this->Controller->ParamHandler->isRest()) {
$query = $this->includeRequestedMetaFields($query);
}
if (!$this->Controller->ParamHandler->isRest()) {
$this->setRequestedEntryAmount();
}
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
$totalCount = $this->Controller->getRequest()->getAttribute('paging')[$this->TableAlias]['count'];
if ($this->Controller->ParamHandler->isRest()) {
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
if (isset($options['hidden'])) {
$data->each(function($value, $key) use ($options) {
$hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']];
@ -107,12 +125,11 @@ class CRUDComponent extends Component
return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates);
});
}
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json', false, false, false, [
'X-Total-Count' => $totalCount,
]);
} else {
if ($this->metaFieldsSupported()) {
$query = $this->includeRequestedMetaFields($query);
}
$data = $this->Controller->paginate($query, $this->Controller->paginate ?? []);
$this->Controller->setResponse($this->Controller->getResponse()->withHeader('X-Total-Count', $totalCount));
if (isset($options['afterFind'])) {
$function = $options['afterFind'];
if (is_callable($function)) {
@ -138,8 +155,11 @@ class CRUDComponent extends Component
$data[$i] = $this->attachMetaTemplatesIfNeeded($row, $metaTemplates);
}
$this->Controller->set('meta_templates', $metaTemplates);
$this->Controller->set('meta_templates_enabled', array_filter($metaTemplates, function($template) {
return $template['enabled'];
}));
}
if (true) { // check if stats are requested
if (empty($excludeStats)) { // check if stats are requested
$modelStatistics = [];
if ($this->Table->hasBehavior('Timestamp')) {
$modelStatistics = $this->Table->getActivityStatisticsForModel(
@ -180,6 +200,8 @@ class CRUDComponent extends Component
}
$this->Controller->set('model', $this->Table);
$this->Controller->set('data', $data);
$this->Controller->set('embedInModal', $embedInModal);
$this->Controller->set('skipTableToolbar', $skipTableToolbar);
}
}
@ -195,21 +217,50 @@ class CRUDComponent extends Component
$metaTemplates = $this->getMetaTemplates()->toArray();
$this->Controller->set('metaFieldsEnabled', true);
$this->Controller->set('metaTemplates', $metaTemplates);
$typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
$typeHandlersOperators = [];
foreach ($typeHandlers as $type => $handler) {
$typeHandlersOperators[$type] = $handler::OPERATORS;
}
$this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
} else {
$this->Controller->set('metaFieldsEnabled', false);
}
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
$typeHandlersOperators = [];
foreach ($typeHandlers as $type => $handler) {
$typeHandlersOperators[$type] = $handler::OPERATORS;
$filtersConfigRaw= !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$filtersConfig = [];
foreach ($filtersConfigRaw as $fieldConfig) {
if (is_array($fieldConfig)) {
$filtersConfig[$fieldConfig['name']] = $fieldConfig;
} else {
$filtersConfig[$fieldConfig] = ['name' => $fieldConfig];
}
}
$this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
$this->Controller->set('filters', $filters);
$filtersName = $this->getFilterFieldsName();
$typeMap = $this->Table->getSchema()->typeMap();
$associatedtypeMap = !empty($filtersName) ? $this->_getAssociatedTypeMap() : [];
$typeMap = array_merge(
$this->Table->getSchema()->typeMap(),
$associatedtypeMap
);
$typeMap = array_filter($typeMap, function ($field) use ($filtersName) {
return in_array($field, $filtersName);
}, ARRAY_FILTER_USE_KEY);
$this->Controller->set('typeMap', $typeMap);
$this->Controller->set('filters', $filtersName);
$this->Controller->set('filtersConfig', $filtersConfig);
$this->Controller->viewBuilder()->setLayout('ajax');
$this->Controller->render('/genericTemplates/filters');
}
public function getFilterFieldsName(): array
{
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
$filters = array_map(function($item) {
return is_array($item) ? $item['name'] : $item;
}, $filters);
return $filters;
}
/**
* getResponsePayload Returns the adaquate response payload based on the request context
*
@ -258,6 +309,9 @@ class CRUDComponent extends Component
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
}
if ($this->request->is('post')) {
$patchEntityParams = [
@ -418,6 +472,7 @@ class CRUDComponent extends Component
'field' => $rawMetaTemplateField->field,
'meta_template_id' => $rawMetaTemplateField->meta_template_id,
'meta_template_field_id' => $rawMetaTemplateField->id,
'meta_template_directory_id' => $allMetaTemplates[$template_id]->meta_template_directory_id,
'parent_id' => $entity->id,
'uuid' => Text::uuid(),
]);
@ -448,6 +503,7 @@ class CRUDComponent extends Component
'field' => $rawMetaTemplateField->field,
'meta_template_id' => $rawMetaTemplateField->meta_template_id,
'meta_template_field_id' => $rawMetaTemplateField->id,
'meta_template_directory_id' => $template->meta_template_directory_id,
'parent_id' => $entity->id,
'uuid' => Text::uuid(),
]);
@ -466,6 +522,7 @@ class CRUDComponent extends Component
}
$entity->setDirty('meta_fields', true);
$entity->_metafields_to_delete = $metaFieldsToDelete;
return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
}
@ -506,7 +563,7 @@ class CRUDComponent extends Component
$params['contain'] = [$params['contain'], 'MetaFields'];
}
}
$query = $this->Table->find()->where(['id' => $id]);
$query = $this->Table->find()->where(["{$this->TableAlias}.id" => $id]);
if (!empty($params['contain'])) {
$query->contain($params['contain']);
}
@ -514,16 +571,16 @@ class CRUDComponent extends Component
$query->where($params['conditions']);
}
$data = $query->first();
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
}
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
}
if ($this->request->is(['post', 'put'])) {
$patchEntityParams = [
'associated' => []
@ -713,6 +770,15 @@ class CRUDComponent extends Component
]);
}
protected function setRequestedEntryAmount()
{
$user = $this->Controller->ACL->getUser();
$tableSettings = IndexSetting::getTableSetting($user, $this->Table);
if (!empty($tableSettings['number_of_element'])) {
$this->Controller->paginate['limit'] = intval($tableSettings['number_of_element']);
}
}
public function view(int $id, array $params = []): void
{
if (empty($id)) {
@ -785,6 +851,9 @@ class CRUDComponent extends Component
$query->contain($params['contain']);
}
$data = $query->first();
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
@ -807,6 +876,9 @@ class CRUDComponent extends Component
$query->contain($params['contain']);
}
$data = $query->first();
if (isset($params['afterFind'])) {
$data = $params['afterFind']($data, $params);
}
if (isset($params['beforeSave'])) {
try {
$data = $params['beforeSave']($data);
@ -1095,17 +1167,9 @@ class CRUDComponent extends Component
public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
{
$this->setQuickFilterForView($params, $options);
$quickFilterFields = $options['quickFilters'];
$queryConditions = [];
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
$this->Controller->set('quickFilterForMetaField', [
'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false,
'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false,
]);
}
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
$this->Controller->set('quickFilterValue', $params['quickFilter']);
$queryConditions = $this->genQuickFilterConditions($params, $quickFilterFields);
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
@ -1115,10 +1179,25 @@ class CRUDComponent extends Component
}
$query->where(['OR' => $queryConditions]);
}
return $query;
}
public function setQuickFilterForView(array $params, array $options): void
{
$quickFilterFields = $options['quickFilters'];
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
$this->Controller->set('quickFilterForMetaField', [
'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false,
'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false,
]);
}
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
$this->Controller->set('quickFilterValue', $params['quickFilter']);
} else {
$this->Controller->set('quickFilterValue', '');
}
return $query;
}
public function genQuickFilterConditions(array $params, array $quickFilterFields): array
@ -1175,7 +1254,7 @@ class CRUDComponent extends Component
}
$activeFilters[$filter] = $filterValue;
if (is_array($filterValue)) {
$query->where([($filter . ' IN') => $filterValue]);
$query = $this->setInCondition($query, $filter, $filterValue);
} else {
$query = $this->setValueCondition($query, $filter, $filterValue);
}
@ -1261,6 +1340,27 @@ class CRUDComponent extends Component
}
}
protected function setInCondition($query, $fieldName, $values)
{
$split = explode(' ', $fieldName);
if (count($split) == 1) {
$field = $fieldName;
$operator = '=';
} else {
$field = $split[0];
$operator = $split[1];
}
if ($operator == '=') {
return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) {
return $exp->in($field, $values);
});
} else if ($operator == '!=') {
return $query->where(function (QueryExpression $exp, Query $q) use ($field, $values) {
return $exp->notIn($field, $values);
});
}
}
protected function setFilteringContext($contextFilters, $params)
{
$filteringContexts = [];
@ -1309,7 +1409,7 @@ class CRUDComponent extends Component
{
if (empty($params['filteringLabel']) && !empty($options['contextFilters']['custom'])) {
foreach ($options['contextFilters']['custom'] as $contextFilter) {
if (!empty($contextFilter['default'])) {
if (!empty($contextFilter['default']) && empty($params)) {
$params['filteringLabel'] = $contextFilter['label'];
$this->Controller->set('fakeFilteringLabel', $contextFilter['label']);
break;
@ -1434,7 +1534,7 @@ class CRUDComponent extends Component
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationErrors);
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', [], $message, $validationErrors);
} else {
$this->Controller->Flash->error($message);
if (empty($params['redirect'])) {
@ -1530,8 +1630,61 @@ class CRUDComponent extends Component
private function renderViewInVariable($templateRelativeName, $data)
{
$builder = new ViewBuilder();
$builder->disableAutoLayout()->setTemplate("{$this->TableAlias}/{$templateRelativeName}");
$view = $builder->build($data);
$builder->disableAutoLayout()
->setClassName('Monad')
->setTemplate("{$this->TableAlias}/{$templateRelativeName}")
->setVars($data);
$view = $builder->build();
return $view->render();
}
protected function _getAssociatedTypeMap(): array
{
$typeMap = [];
foreach ($this->getFilterFieldsName() as $filter) {
$exploded = explode('.', $filter);
if (count($exploded) > 1) {
$model = $exploded[0];
$subField = $exploded[1];
if ($model == $this->Table->getAlias()) {
$typeMap[$filter] = $this->Table->getSchema()->typeMap()[$subField] ?? 'text';
} else {
$association = $this->Table->associations()->get($model);
$associatedTable = $association->getTarget();
$typeMap[$filter] = $associatedTable->getSchema()->typeMap()[$subField] ?? 'text';
}
}
}
return $typeMap;
}
protected function _validOrderFields($fields): bool
{
if (!is_array($fields)) {
$fields = [$fields];
}
foreach ($fields as $field) {
$exploded = explode('.', $field);
if (count($exploded) > 1) {
$model = $exploded[0];
$subField = $exploded[1];
if ($model == $this->Table->getAlias()) {
if (empty($this->Table->getSchema()->typeMap()[$subField])) {
return false;
}
} else {
$association = $this->Table->associations()->get($model);
$associatedTable = $association->getTarget();
if (empty($associatedTable->getSchema()->typeMap()[$subField])) {
return false;
}
}
} else {
if (empty($this->Table->getSchema()->typeMap()[$field])) {
return false;
}
}
}
return true;
}
}

32
src/Utility/Utils.php Normal file
View File

@ -0,0 +1,32 @@
<?php
namespace App\Utility\Utils;
// src: https://www.php.net/manual/en/function.array-diff.php#91756
function array_diff_recursive($arr1, $arr2)
{
$outputDiff = [];
foreach ($arr1 as $key => $value) {
//if the key exists in the second array, recursively call this function
//if it is an array, otherwise check if the value is in arr2
if (array_key_exists($key, $arr2)) {
if (is_array($value)) {
$recursiveDiff = array_diff_recursive($value, $arr2[$key]);
if (count($recursiveDiff)) {
$outputDiff[$key] = $recursiveDiff;
}
} else if (!in_array($value, $arr2)) {
$outputDiff[$key] = $value;
}
}
//if the key is not in the second array, check if the value is in
//the second array (this is a quirk of how array_diff works)
else if (!in_array($value, $arr2)) {
$outputDiff[$key] = $value;
}
}
return $outputDiff;
}

View File

@ -0,0 +1,155 @@
<?php
namespace App\View\Helper\BootstrapElements;
use App\View\Helper\BootstrapGeneric;
use App\View\Helper\BootstrapHelper;
/**
* 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(array $options, array $content, BootstrapHelper $btHelper)
{
$this->allowedOptionValues = [];
$this->content = $content;
$this->btHelper = $btHelper;
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$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(): string
{
return $this->genAccordion();
}
private function genHeader(array $accordionItem, int $i): string
{
$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(array $accordionItem, int $i): string
{
$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(): string
{
$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;
}
}

View File

@ -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(array $options)
{
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$this->options = array_merge($this->defaultOptions, $options);
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
$this->checkOptionValidity();
}
public function alert(): string
{
return $this->genAlert();
}
private function genAlert(): string
{
$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(): string
{
$html = '';
if ($this->options['dismissible']) {
$html .= $this->genericCloseButton('alert');
}
return $html;
}
}

View File

@ -0,0 +1,88 @@
<?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
* - icon: Should the button have an icon right before the text
* - 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 = [
'id' => '',
'text' => '',
'html' => null,
'variant' => 'primary',
'pill' => false,
'icon' => false,
'title' => '',
'class' => [],
];
function __construct(array $options)
{
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$this->options = array_merge($this->defaultOptions, $options);
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
$this->checkOptionValidity();
}
public function badge(): string
{
return $this->genBadge();
}
private function genBadge(): string
{
$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'],
'id' => $this->options['id'] ?? '',
], [
$this->genIcon(),
$this->options['html'] ?? h($this->options['text'])
]);
return $html;
}
private function genIcon(): string
{
if (!empty($this->options['icon'])) {
$bsIcon = new BootstrapIcon($this->options['icon'], [
'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
]);
return $bsIcon->icon();
}
return '';
}
}

View File

@ -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(array $options)
{
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
'size' => ['', 'xs', 'sm', 'lg'],
'type' => ['button', 'submit', 'reset']
];
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$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(): string
{
return $this->genButton();
}
private function genButton(): string
{
$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(): string
{
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(): string
{
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 '';
}
}

View File

@ -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(array $options)
{
$this->allowedOptionValues = [
'headerVariant' => array_merge(BootstrapGeneric::$variants, ['']),
'bodyVariant' => array_merge(BootstrapGeneric::$variants, ['']),
'footerVariant' => array_merge(BootstrapGeneric::$variants, ['']),
];
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$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(): string
{
return $this->genCard();
}
private function genCard(): string
{
$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(): string
{
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(): string
{
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(): string
{
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;
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace App\View\Helper\BootstrapElements;
use Cake\Utility\Security;
use App\View\Helper\BootstrapGeneric;
use App\View\Helper\BootstrapHelper;
/**
* 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' => false,
'attrs' => [],
];
function __construct(array $options, string $content, BootstrapHelper $btHelper)
{
$this->allowedOptionValues = [];
$this->processOptions($options);
$this->content = $content;
$this->btHelper = $btHelper;
}
private function processOptions(array $options): void
{
$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 ($this->options['card'] !== false && 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(): string
{
return $this->genCollapse();
}
private function genControl(): string
{
$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(): string
{
if (!empty($this->options['card'])) {
$cardConfig = $this->options['card'];
$cardConfig['bodyHTML'] = $this->content;
$content = $this->btHelper->card($cardConfig);
} else {
$content = $this->content;
}
$container = $this->node('div', [
'class' => $this->options['class'],
'id' => $this->options['id'],
], $content);
return $container;
}
private function genCollapse(): string
{
return $this->genControl() . $this->genContent();
}
}

View File

@ -0,0 +1,214 @@
<?php
namespace App\View\Helper\BootstrapElements;
use App\View\Helper\BootstrapGeneric;
use App\View\Helper\BootstrapHelper;
/**
* # 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 to 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
* - attrs: Additional HTML attributes to be applied on the entry
*
* # 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(array $options, BootstrapHelper $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(array $options): void
{
$this->options = array_merge($this->defaultOptions, $options);
$this->options['dropdown-class'] = $this->convertToArrayIfNeeded($this->options['dropdown-class']);
$this->checkOptionValidity();
}
public function dropdownMenu(): string
{
return $this->fullDropdown();
}
public function fullDropdown(): string
{
return $this->genDropdownWrapper($this->genDropdownToggleButton(), $this->genDropdownMenu($this->menu));
}
public function genDropdownWrapper(string $toggle = '', string $menu = '', $direction = null, $classes = null): string
{
$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(): string
{
$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(array $entries, $alignment = null): string
{
$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(array $entries): string
{
$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(array $entry): string
{
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'];
if (!empty($entry['class'])) {
if (!is_array($entry['class'])) {
$entry['class'] = [$entry['class']];
}
$classes = array_merge($classes, $entry['class']);
}
$params = $entry['attrs'] ?? [];
$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);
}
}

View File

@ -0,0 +1,63 @@
<?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 = [
'id' => '',
'class' => [],
'title' => '',
'attrs' => [],
];
function __construct(string $icon, array $options = [])
{
$this->icon = $icon;
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
$this->options['class'] = $this->convertToArrayIfNeeded($this->options['class']);
}
public function icon(): string
{
return $this->genIcon();
}
private function genIcon(): string
{
$html = $this->node('span', array_merge(
[
'id' => $this->options['id'] ?? '',
'class' => array_merge(
$this->options['class'],
["fa fa-{$this->icon}"]
),
'title' => h($this->options['title'])
],
$this->options['attrs']
));
return $html;
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\View\Helper\BootstrapElements;
use App\View\Helper\BootstrapGeneric;
/**
* Creates a Bootstrap list group where items can be links or buttons
*
* # 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(array $items, array $options, \App\View\BootstrapHelper $btHelper)
{
$this->items = $items;
$this->processOptions($options);
$this->btHelper = $btHelper;
}
private function processOptions(array $options): void
{
$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(array $item): string
{
$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(array $badge): string
{
if (empty($badge)) {
return '';
}
return $this->btHelper->badge($badge);
}
}

View File

@ -0,0 +1,223 @@
<?php
namespace App\View\Helper\BootstrapElements;
use Cake\Utility\Hash;
use App\View\Helper\BootstrapGeneric;
use App\View\Helper\BootstrapHelper;
/**
* 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(array $options, array $data, BootstrapHelper $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(array $options): void
{
$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(): string
{
return $this->genTable();
}
private function genTable(): string
{
$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(): string
{
$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(array $field): string
{
$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(array $field = []): string
{
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(array $field): string
{
$key = is_array($field) ? $field['path'] : $field;
$cellValue = Hash::get($this->item, $key);
return !is_null($cellValue) ? $cellValue : '';
}
private function getElementPath($type): string
{
return sprintf(
'%s%sField',
$this->options['elementsRootPath'] ?? '',
$type
);
}
private function genCaption(): string
{
return !empty($this->caption) ? $this->node('caption', [], h($this->caption)) : '';
}
}

View File

@ -0,0 +1,349 @@
<?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
* - dialogScrollable: Allows to scroll the modal body
* - 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,
'dialogScrollable' => true,
'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(array $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(array $options): void
{
$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']);
if (!empty($this->options['dialogScrollable'])) {
$this->options['modalClass'][] = 'modal-dialog-scrollable';
}
$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(): string
{
$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(): string
{
$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(): string
{
$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(): string
{
$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(): string
{
$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(): string
{
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(): string
{
return (new BootstrapButton([
'variant' => 'primary',
'text' => __('Ok'),
'onclick' => $this->options['confirmOnclick'],
'attrs' => [
'data-bs-dismiss' => $this->options['confirmOnclick'] ?? 'modal',
],
]))->button();
}
private function getFooterConfirm(): string
{
$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(): string
{
$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);
}
}

View File

@ -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(array $options)
{
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
'borderVariant' => array_merge(BootstrapGeneric::$variants, ['']),
];
$this->defaultOptions['title'] = __('New notifications');
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$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(): string
{
return $this->genNotificationBubble();
}
private function genNotificationBubble(): string
{
$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;
}
}

View File

@ -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): void
{
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
}
public function progress(): string
{
return $this->genProgress();
}
private function genProgress(): string
{
$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;
}
}

View File

@ -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): void
{
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
}
public function progressTimeline(): string
{
return $this->genProgressTimeline();
}
private function getStepIcon(array $step, int $i, bool $nodeActive, bool $lineActive): string
{
$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(int $i, bool $nodeActive, bool $lineActive): string
{
$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(array $step, bool $isActive): string
{
return $this->node('li', [
'class' => [
'text-center',
'fw-bold',
$isActive ? 'progress-active' : 'progress-inactive',
],
], h($step['text'] ?? ''));
}
private function genProgressTimeline(): string
{
$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;
}
}

View File

@ -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(array $options)
{
$this->allowedOptionValues = [
'variant' => BootstrapGeneric::$variants,
];
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$this->options = array_merge($this->defaultOptions, $options);
$this->checkOptionValidity();
}
public function switch(): string
{
return $this->genSwitch();
}
public function genSwitch(): string
{
$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;
}
}

View File

@ -0,0 +1,243 @@
<?php
namespace App\View\Helper\BootstrapElements;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
use App\View\Helper\BootstrapGeneric;
use App\View\Helper\BootstrapHelper;
/**
* 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
* - class: Additional classes to add for that row
* - 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(array $options, array $data, BootstrapHelper $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(array $options): void
{
$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(): string
{
return $this->genTable();
}
private function genTable(): string
{
$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(): string
{
$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(): string
{
$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(array $row, int $rowIndex): string
{
$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, $this->fields[$i], $row, $rowIndex);
}
}
$html .= $this->nodeClose('tr');
return $html;
}
private function genCell($value, array $field = [], array $row = [], int $rowIndex = 0): string
{
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' => array_merge(
[
!empty($field['columnVariant']) ? "table-{$field['columnVariant']}" : ''
],
$field['class'] ?? []
),
], $cellContent);
}
private function getValueFromObject(array $row, $field)
{
$path = is_array($field) ? $field['path'] : $field;
$cellValue = Hash::get($row, $path);
return !is_null($cellValue) ? $cellValue : '';
}
private function getElementPath(string $type): string
{
return sprintf(
'%s%sField',
$this->options['elementsRootPath'] ?? '',
$type
);
}
private function genCaption(): string
{
return !empty($this->caption) ? $this->node('caption', [], h($this->caption)) : '';
}
}

View File

@ -0,0 +1,303 @@
<?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(array $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(): string
{
return $this->genTabs();
}
private function processOptions(array $options): void
{
$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(): string
{
return $this->options['vertical'] ? $this->genVerticalTabs() : $this->genHorizontalTabs();
}
private function genHorizontalTabs(): string
{
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(): string
{
$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']}",
]
)], $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']}",
]
)], $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' : '')
],
[
]
)], $containerContent);
return $container;
}
private function genNav(): string
{
$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(array $navItem): string
{
$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(): string
{
$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(array $navItem, string $content): string
{
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);
}
}

View File

@ -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(array $options)
{
$this->allowedOptionValues = [
'variant' => array_merge(BootstrapGeneric::$variants, ['default']),
];
$this->processOptions($options);
}
private function processOptions(array $options): void
{
$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(): string
{
return $this->genToast();
}
private function genToast(): string
{
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

View File

@ -8,6 +8,15 @@ class FormFieldMassageHelper extends Helper
{
public function prepareFormElement(\Cake\View\Helper\FormHelper $form, array $controlParams, array $fieldData): string
{
if (!empty($fieldData['tooltip'])) {
$form->setTemplates([
'label' => '{{text}}{{tooltip}}',
]);
$controlParams['templateVars'] = array_merge(
$controlParams['templateVars'] ?? [],
['tooltip' => $fieldData['tooltip'],]
);
}
if (!empty($fieldData['stateDependence'])) {
$controlParams['data-dependence-source'] = h($fieldData['stateDependence']['source']);
$controlParams['data-dependence-option'] = h($fieldData['stateDependence']['option']);

View File

@ -18,22 +18,22 @@ class SocialProviderHelper extends Helper
return !empty($identity['social_profile']);
}
public function getIcon($identity)
public function getIcon($identity, array $classes=[])
{
if (!empty($identity['social_profile'])) {
$provider = $identity['social_profile']['provider'];
if (!empty($this->providerImageMapping[$provider])) {
return $this->genImage($this->providerImageMapping[$provider], h($provider));
return $this->genImage($this->providerImageMapping[$provider], h($provider), $classes);
}
}
return '';
}
private function genImage($url, $alt)
private function genImage($url, $alt, array $classes=[])
{
return $this->Bootstrap->genNode('img', [
return $this->Bootstrap->node('img', [
'src' => $url,
'class' => ['img-fluid'],
'class' => array_merge(['img-fluid'], $classes),
'width' => '16',
'height' => '16',
'alt' => $alt,

View File

@ -9,7 +9,7 @@ use Cake\Core\Configure;
$this->Html->image('misp-logo.png', [
'alt' => __('MISP logo'),
'height' => 100,
'style' => ['filter: drop-shadow(4px 4px 4px #924da666);']
'style' => ['filter: drop-shadow(4px 4px 4px #22222233);']
])
);
$template = [
@ -20,7 +20,7 @@ use Cake\Core\Configure;
$this->Form->setTemplates($template);
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
echo sprintf('<h4 class="text-uppercase fw-light mb-3">%s</h4>', __('Sign In'));
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]);
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login', '?' => ['redirect' => $this->request->getQuery('redirect')],]]);
echo $this->Form->control('email', ['label' => 'Email', 'class' => 'form-control mb-2', 'placeholder' => __('Email')]);
echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]);
echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']);
@ -52,8 +52,8 @@ use Cake\Core\Configure;
'class' => ['d-block', 'w-100'],
'image' => [
'path' => '/img/keycloak_logo.png',
'alt' => 'Keycloak'
]
'alt' => 'Keycloak',
],
]);
echo $this->Form->end();
}

View File

@ -2,7 +2,7 @@
if ($setting['type'] == 'string' || $setting['type'] == 'textarea' || empty($setting['type'])) {
$input = (function ($settingName, $setting, $appView) {
$settingId = str_replace('.', '_', $settingName);
return $appView->Bootstrap->genNode(
return $appView->Bootstrap->node(
$setting['type'] == 'textarea' ? 'textarea' : 'input',
[
'class' => [
@ -43,7 +43,7 @@
} elseif ($setting['type'] == 'integer') {
$input = (function ($settingName, $setting, $appView) {
$settingId = str_replace('.', '_', $settingName);
return $appView->Bootstrap->genNode('input', [
return $appView->Bootstrap->node('input', [
'class' => [
'form-control',
(!empty($setting['error']) ? 'is-invalid' : ''),
@ -73,7 +73,7 @@
}
}
$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) {
$optionParam = [
'class' => [],
@ -88,10 +88,10 @@
$optionParam['selected'] = 'selected';
}
}
$options[] = $appView->Bootstrap->genNode('option', $optionParam, h($value));
$options[] = $appView->Bootstrap->node('option', $optionParam, h($value));
}
$options = implode('', $options);
return $appView->Bootstrap->genNode('select', [
return $appView->Bootstrap->node('select', [
'class' => [
'form-select',
'pe-4',

View File

@ -3,7 +3,7 @@
$dependsOnHtml = '';
if (!empty($setting['dependsOn'])) {
$dependsOnHtml = $this->Bootstrap->genNode('span', [
$dependsOnHtml = $this->Bootstrap->node('span', [
'class' => [
'ms-1',
'd-inline-block',
@ -11,18 +11,18 @@
],
'style' => 'min-width: 0.75em;',
'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'),
]));
}
$label = $this->Bootstrap->genNode('label', [
$label = $this->Bootstrap->node('label', [
'class' => ['form-label', 'fw-bolder', 'mb-0'],
'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);
$description = '';
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'],
'id' => "{$settingId}Help"
], h($setting['description']));
@ -31,7 +31,7 @@
if (!empty($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],
], (!empty($setting['error']) ? h($setting['errorMessage']) : ''));
@ -50,11 +50,11 @@
'variant' => 'success',
'class' => ['btn-setting-action', 'btn-save-setting', 'd-none'],
]);
$inputGroup = $this->Bootstrap->genNode('div', [
$inputGroup = $this->Bootstrap->node('div', [
'class' => ['input-group'],
], implode('', [$input, $inputGroupSave]));
$container = $this->Bootstrap->genNode('div', [
$container = $this->Bootstrap->node('div', [
'class' => ['setting-group', 'row', 'mb-2']
], implode('', [$label, $inputGroup, $description, $validationError]));

View File

@ -50,14 +50,14 @@ foreach (array_keys($mainNoticeHeading) as $level) {
'bordered' => false,
], [
'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']));
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>';
}],
['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;">';
if (is_null($value)) {
$formatedValue .= '<i class="text-nowrap">' . __('No value') . '</i>';
@ -71,7 +71,7 @@ foreach (array_keys($mainNoticeHeading) as $level) {
$formatedValue .= '</span>';
return $formatedValue;
}],
['key' => 'description', 'label' => __('Description')]
['path' => 'description', 'label' => __('Description')]
],
'items' => $notices[$level],
]);
@ -87,14 +87,14 @@ $alertBody = $this->Bootstrap->table([
'tableClass' => 'mb-0'
], [
'fields' => [
['key' => 'severity', 'label' => __('Severity')],
['key' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) {
['path' => 'severity', 'label' => __('Severity')],
['path' => 'issues', 'label' => __('Issues'), 'formatter' => function($count, $row) {
return $this->Bootstrap->badge([
'variant' => $row['badge-variant'],
'text' => $count
]);
}],
['key' => 'description', 'label' => __('Description')]
['path' => 'description', 'label' => __('Description')]
],
'items' => $tableItems,
]);

View File

@ -34,7 +34,7 @@ if (isLeaf($panelSettings)) {
h($panelName)
);
if (!empty($panelSettings['_description'])) {
$panelHTML .= $this->Bootstrap->genNode('div', [
$panelHTML .= $this->Bootstrap->node('div', [
'class' => ['mb-1',],
], h($panelSettings['_description']));
}
@ -59,7 +59,7 @@ if (isLeaf($panelSettings)) {
}
}
}
$panelHTML = $this->Bootstrap->genNode('div', [
$panelHTML = $this->Bootstrap->node('div', [
'class' => [
'shadow',
'p-2',

View File

@ -5,19 +5,17 @@ $table = $this->Bootstrap->table([
'hover' => false,
], [
'fields' => [
['key' => 'label', 'label' => __('Label')],
['key' => 'name', 'label' => __('Name')],
['key' => 'url', 'label' => __('URL'), 'formatter' => function ($value, $row) {
['path' => 'label', 'label' => __('Label')],
['path' => 'name', 'label' => __('Name')],
['path' => 'url', 'label' => __('URL'), 'formatter' => function ($value, $row) {
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([
'icon' => 'trash',
'variant' => 'danger',
'size' => 'sm',
'params' => [
'onclick' => sprintf('deleteBookmark(window.bookmarks[%s])', $index),
]
'onclick' => sprintf('deleteBookmark(window.bookmarks[%s])', $index),
]);
}],
],

View File

@ -2,7 +2,7 @@
$seed = 's-' . mt_rand();
$controlParams = [
'type' => 'select',
'options' => $fieldData['options'],
'options' => $fieldData['options'] ?? [],
'empty' => $fieldData['empty'] ?? false,
'value' => $fieldData['value'] ?? null,
'multiple' => $fieldData['multiple'] ?? false,
@ -19,6 +19,13 @@ if (!empty($fieldData['label'])) {
if ($controlParams['options'] instanceof \Cake\ORM\Query) {
$controlParams['options'] = $controlParams['options']->all()->toList();
}
$initSelect2 = false;
if (isset($fieldData['select2']) && $fieldData['select2'] == true) {
$initSelect2 = true;
$fieldData['select2'] = $fieldData['select2'] === true ? [] : $fieldData['select2'];
$controlParams['class'] .= ' select2-input';
}
$controlParams['class'] .= ' dropdown-custom-value' . "-$seed";
if (in_array('_custom', array_keys($controlParams['options']))) {
$customInputValue = $this->Form->getSourceValue($fieldData['field']);
if (!in_array($customInputValue, $controlParams['options'])) {
@ -31,7 +38,6 @@ if (in_array('_custom', array_keys($controlParams['options']))) {
} else {
$customInputValue = '';
}
$controlParams['class'] .= ' dropdown-custom-value' . "-$seed";
$adaptedField = $fieldData['field'] . '_custom';
$controlParams['templates']['formGroup'] = sprintf(
'<label class="col-sm-2 col-form-label form-label" {{attrs}}>{{label}}</label><div class="col-sm-10 multi-metafield-input-container"><div class="d-flex form-dropdown-with-freetext input-group">{{input}}{{error}}%s</div></div>',
@ -49,6 +55,19 @@ echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $f
$select.attr('onclick', 'toggleFreetextSelectField(this)')
$select.parent().find('input.custom-value').attr('oninput', 'updateAssociatedSelect(this)')
updateAssociatedSelect($select.parent().find('input.custom-value')[0])
<?php if ($initSelect2) : ?>
// let $container = $select.closest('.modal-dialog .modal-body')
let $container = []
if ($container.length == 0) {
$container = $(document.body)
}
const defaultSelect2Options = {
dropdownParent: $container,
}
const passedSelect2Options = <?= json_encode($fieldData['select2']) ?>;
const select2Options = Object.assign({}, passedSelect2Options, defaultSelect2Options)
$select.select2(select2Options)
<?php endif; ?>
})
})()

View File

@ -1,43 +1,43 @@
<?php
$random = Cake\Utility\Security::randomString(8);
$params['div'] = false;
$this->Form->setTemplates([
'inputContainer' => '{{content}}',
'inputContainerError' => '{{content}}',
'formGroup' => '{{input}}',
]);
$label = $fieldData['label'];
$formElement = $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
$temp = sprintf(
'<div class="row mb-3">
<div class="col-sm-2 form-label">%s</div>
<div class="col-sm-10">
<div class="input-group">
%s<span>%s</span>
</div>
</div>
</div>',
h($label),
$formElement,
sprintf(
'<span id="uuid-gen-%s" class="btn btn-secondary">%s</span>',
$random,
__('Generate')
)
);
echo $temp;
$random = Cake\Utility\Security::randomString(8);
$params['div'] = false;
$genUUIDButton = $this->Bootstrap->button([
'id' => "uuid-gen-{$random}",
'variant' => 'secondary',
'text' => __('Generate'),
]);
$this->Form->setTemplates([
'input' => sprintf('<div class="input-group">%s{{genUUIDButton}}</div>', $this->Form->getTemplates('input')),
]);
$params['templateVars'] = [
'genUUIDButton' => $genUUIDButton,
];
$formElement = $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
echo $formElement;
?>
<script type="text/javascript">
$(document).ready(function() {
$('#uuid-gen-<?= h($random) ?>').on('click', function() {
$.ajax({
success:function (data, textStatus) {
$('#uuid-field').val(data["uuid"]);
},
type: "get",
cache: false,
url: "/organisations/generateUUID",
});
});
const $node = $('#uuid-gen-<?= h($random) ?>')
$node.click(fetchUUID)
function fetchUUID() {
const urlGet = '/organisations/generateUUID'
const options = {
statusNode: $node,
}
return AJAXApi.quickFetchJSON(urlGet, options)
.then(function(data) {
$('#uuid-field').val(data["uuid"])
})
.catch((e) => {
UI.toast({
variant: 'danger',
text: '<?= __('Could not generate UUID') ?>'
})
})
}
});
</script>
</script>

View File

@ -1,52 +1,54 @@
<?php
if (is_array($fieldData)) {
$fieldTemplate = 'genericField';
if (!empty($fieldData['type'])) {
if (file_exists(ROOT . '/templates/element/genericElements/Form/Fields/' . $fieldData['type'] . 'Field.php')) {
$fieldTemplate = $fieldData['type'] . 'Field';
}
if (is_array($fieldData)) {
$fieldTemplate = 'genericField';
if (!empty($fieldData['type'])) {
if (file_exists(ROOT . '/templates/element/genericElements/Form/Fields/' . $fieldData['type'] . 'Field.php')) {
$fieldTemplate = $fieldData['type'] . 'Field';
}
if (empty($fieldData['label'])) {
if (!isset($fieldData['label']) || $fieldData['label'] !== false) {
$fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']);
}
}
if (!empty($fieldDesc[$fieldData['field']])) {
$fieldData['label'] .= $this->element(
'genericElements/Form/formInfo', array(
'field' => $fieldData,
'fieldDesc' => $fieldDesc[$fieldData['field']],
'modelForForm' => $modelForForm
)
);
}
$params = array();
if (!empty($fieldData['class'])) {
if (is_array($fieldData['class'])) {
$class = implode(' ', $fieldData['class']);
} else {
$class = $fieldData['class'];
}
$params['class'] = $class;
} else {
$params['class'] = '';
}
if (empty($fieldData['type']) || ($fieldData['type'] !== 'checkbox' && $fieldData['type'] !== 'radio')) {
$params['class'] .= ' form-control';
}
foreach ($fieldData as $k => $fd) {
if (!isset($simpleFieldWhitelist) || in_array($k, $simpleFieldWhitelist) || strpos($k, 'data-') === 0) {
$params[$k] = $fd;
}
}
$temp = $this->element('genericElements/Form/Fields/' . $fieldTemplate, array(
'fieldData' => $fieldData,
'params' => $params
));
if (!empty($fieldData['hidden'])) {
$temp = '<span class="hidden">' . $temp . '</span>';
}
echo $temp;
} else {
echo $fieldData;
}
if (empty($fieldData['label'])) {
if (!isset($fieldData['label']) || $fieldData['label'] !== false) {
$fieldData['label'] = \Cake\Utility\Inflector::humanize($fieldData['field']);
}
}
$fieldDescription = $fieldData['tooltip'] ?? ($fieldDesc[$fieldData['field']] ?? false);
if (!empty($fieldDescription)) {
$fieldData['tooltip'] = $this->element(
'genericElements/Form/formInfo',
array(
'field' => $fieldData,
'fieldDesc' => $fieldDescription,
'modelForForm' => $modelForForm
)
);
}
$params = array();
if (!empty($fieldData['class'])) {
if (is_array($fieldData['class'])) {
$class = implode(' ', $fieldData['class']);
} else {
$class = $fieldData['class'];
}
$params['class'] = $class;
} else {
$params['class'] = '';
}
if (empty($fieldData['type']) || ($fieldData['type'] !== 'checkbox' && $fieldData['type'] !== 'radio')) {
$params['class'] .= ' form-control';
}
foreach ($fieldData as $k => $fd) {
if (!isset($simpleFieldWhitelist) || in_array($k, $simpleFieldWhitelist) || strpos($k, 'data-') === 0) {
$params[$k] = $fd;
}
}
$temp = $this->element('genericElements/Form/Fields/' . $fieldTemplate, array(
'fieldData' => $fieldData,
'params' => $params
));
if (!empty($fieldData['hidden'])) {
$temp = '<span class="hidden">' . $temp . '</span>';
}
echo $temp;
} else {
echo $fieldData;
}

View File

@ -1,4 +1,5 @@
<?php
$seed = mt_rand();
if (!is_array($fieldDesc)) {
$fieldDesc = array('info' => $fieldDesc);
$default = 'info';
@ -16,32 +17,46 @@
$default = 'info';
}
}
echo sprintf(
'<span id = "%sInfoPopover" class="icon-info-sign" data-bs-toggle="popover" data-bs-trigger="hover"></span>',
h($field['field'])
);
$popoverID = sprintf("%sInfoPopover%s", h($field['field']), $seed);
echo $this->Bootstrap->icon('info-circle', [
'id' => $popoverID,
'class' => ['ms-1'],
'attrs' => [
'data-bs-toggle' => 'popover',
'data-bs-trigger' => 'hover',
]
]);
?>
<script type="text/javascript">
$(document).ready(function() {
new bootstrap.Popover('#<?php echo h($field['field']); ?>InfoPopover', {
new bootstrap.Popover('#<?= $popoverID ?>', {
html: true,
content: function() {
var tempSelector = '#<?php echo h($modelForForm . \Cake\Utility\Inflector::camelize($field['field'])); ?>';
if ($(tempSelector)[0].nodeName === "SELECT" && Object.keys(fieldDesc).length > 1) {
return $('<div>').append(
$('<span>').attr('class', 'blue bold').text($(tempSelector +" option:selected").text())
).append(
$('<span>').text(': ' + fieldDesc[$(tempSelector).val()])
return $('<div>')
.append(
$('<span>')
.attr('class', 'text-primary fw-bold')
.text('<?php echo h(\Cake\Utility\Inflector::humanize($field['field'])); ?>')
)
.append(
$('<span>').text(": <?= h($fieldDesc["info"]) ?>")
);
} else {
return $('<div>').append(
$('<span>').attr('class', 'blue bold').text('<?php echo h(\Cake\Utility\Inflector::humanize($field['field'])); ?>')
).append(
$('<span>').text(": " + fieldDesc["info"])
);
}
// var tempSelector = '#<?php echo h($modelForForm . \Cake\Utility\Inflector::camelize($field['field'])); ?>';
// if ($(tempSelector)[0].nodeName === "SELECT" && Object.keys(fieldDesc).length > 1) {
// return $('<div>').append(
// $('<span>').attr('class', 'blue bold').text($(tempSelector +" option:selected").text())
// ).append(
// $('<span>').text(': ' + fieldDesc[$(tempSelector).val()])
// );
// } else {
// return $('<div>').append(
// $('<span>').attr('class', 'blue bold').text('<?php echo h(\Cake\Utility\Inflector::humanize($field['field'])); ?>')
// ).append(
// $('<span>').text(": " + fieldDesc["info"])
// );
// }
}
});
var fieldDesc = <?php echo json_encode($fieldDesc); ?>;
// var fieldDesc = <?php echo json_encode($fieldDesc); ?>;
});
</script>

View File

@ -22,9 +22,9 @@
],
[
[
'_open' => true,
'open' => true,
'header' => [
'title' => __('Meta fields')
'text' => __('Meta fields')
],
'body' => $metaTemplateString,
],

View File

@ -15,9 +15,9 @@
],
[
[
'_open' => true,
'open' => true,
'header' => [
'title' => __('Meta fields')
'text' => __('Meta fields')
],
'body' => $metaTemplateString,
],

View File

@ -19,7 +19,7 @@
$entity = isset($entity) ? $entity : null;
$fieldsString = '';
$simpleFieldWhitelist = [
'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required', 'templates', 'options', 'value', 'checked'
'default', 'type', 'placeholder', 'label', 'empty', 'rows', 'div', 'required', 'templates', 'options', 'value', 'checked',
];
if (empty($data['url'])) {
$data['url'] = ["controller" => $this->request->getParam('controller'), "action" => $this->request->getParam('url')];
@ -155,14 +155,5 @@
$('.formDropdown').on('change', function() {
executeStateDependencyChecks('#' + this.id);
})
<?php if (!empty($initSelect2)): ?>
<?php
$dropdownParent = !empty($seedModal) ? sprintf("$('.modal-dialog.%s .modal-body')", $seedModal) : "$(document.body)";
?>
$('select.select2-input').select2({
dropdownParent: <?= $dropdownParent ?>,
width: '100%',
})
<?php endif; ?>
});
</script>

View File

@ -4,7 +4,7 @@ use Cake\Utility\Inflector;
$default_template = [
'inputContainer' => '<div class="row mb-3 metafield-container">{{content}}</div>',
'inputContainerError' => '<div class="row mb-3 metafield-container has-error">{{content}}</div>',
'inputContainerError' => '<div class="row mb-3 metafield-container has-error">{{content}}{{error}}</div>',
'formGroup' => '<label class="col-sm-2 col-form-label form-label" {{attrs}}>{{label}}</label><div class="col-sm-10">{{input}}{{error}}</div>',
'error' => '<div class="error-message invalid-feedback d-block">{{content}}</div>',
'errorList' => '<ul>{{content}}</ul>',
@ -82,7 +82,7 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
}
}
}
$fieldContainer = $this->Bootstrap->genNode('div', [
$fieldContainer = $this->Bootstrap->node('div', [
'class' => [],
], $fieldsHtml);
echo $fieldContainer;
echo $fieldContainer;

View File

@ -41,7 +41,7 @@ foreach ($statistics['usage'] as $scope => $graphData) {
'nodeType' => 'a',
'onclick' => '',
'class' => ['btn-statistics-pie-configurator-' . $seedPiechart],
'params' => [
'attrs' => [
'data-bs-toggle' => 'popover',
]
])
@ -52,7 +52,7 @@ foreach ($statistics['usage'] as $scope => $graphData) {
$pieChart
);
$statPie = $this->Bootstrap->card([
'variant' => 'secondary',
'bodyVariant' => 'secondary',
'bodyHTML' => $panelHtml,
'bodyClass' => 'py-1 px-2',
'class' => ['shadow-sm', 'h-100']

View File

@ -38,7 +38,7 @@ $panelControlHtml = sprintf(
'nodeType' => 'a',
'onclick' => '',
'class' => ['btn-statistics-days-configurator-' . $seed,],
'params' => [
'attrs' => [
'data-bs-toggle' => 'popover',
]
])
@ -46,13 +46,13 @@ $panelControlHtml = 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>',
__('{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']
);
$modifiedNumber = empty($timeline['modified']) ? '' : sprintf(
'<div class="lh-1 d-flex align-items-center" title="%s">%s<span class="ms-1"> %s</span></div>',
__('{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']
);
$activityNumbers = sprintf('<div class="my-1 fs-5">%s%s</div>', $createdNumber, $modifiedNumber);
@ -87,7 +87,7 @@ $cardContent = sprintf(
);
$card = $this->Bootstrap->card([
'variant' => 'secondary',
'bodyVariant' => 'secondary',
'bodyHTML' => $cardContent,
'bodyClass' => 'py-1 px-2',
'class' => ['shadow-sm', 'h-100']

View File

@ -2,23 +2,24 @@
use Cake\Utility\Text;
/*
* echo $this->element('/genericElements/IndexTable/index_table', [
* 'top_bar' => (
* // search/filter bar information compliant with ListTopBar
* ),
* 'data' => [
// the actual data to be used
* ),
* 'fields' => [
* // field list with information for the paginator, the elements used for the individual cells, etc
* ),
* 'title' => optional title,
* 'description' => optional description,
* '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)
* ));
*
*/
* echo $this->element('/genericElements/IndexTable/index_table', [
* 'top_bar' => (
* // search/filter bar information compliant with ListTopBar
* ),
* 'data' => [
// the actual data to be used
* ),
* 'fields' => [
* // field list with information for the paginator, the elements used for the individual cells, etc
* ),
* 'title' => optional title,
* 'description' => optional description,
* 'notice' => optional alert to be placed at the top,
* '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)
* ));
*
*/
$newMetaFields = [];
if (!empty($requestedMetaFields)) { // Create mapping for new index table fields on the fly
@ -39,49 +40,56 @@ if (!empty($requestedMetaFields)) { // Create mapping for new index table fields
$data['fields'] = array_merge($data['fields'], $newMetaFields);
$tableRandomValue = Cake\Utility\Security::randomString(8);
echo '<div id="table-container-' . h($tableRandomValue) . '">';
$html = '<div id="table-container-' . h($tableRandomValue) . '">';
if (!empty($data['title'])) {
echo Text::insert(
'<h2 class="fw-light">:title :help</h2>',
[
'title' => $this->ValueGetter->get($data['title']),
'help' => $this->Bootstrap->icon('info', [
'class' => ['fs-6', 'align-text-top',],
'title' => empty($data['description']) ? '' : h($data['description']),
'params' => [
'data-bs-toggle' => 'tooltip',
]
]),
]
);
if (empty($embedInModal)) {
$html .= Text::insert(
'<h2 class="fw-light">:title :help</h2>',
[
'title' => h($this->ValueGetter->get($data['title'])),
'help' => $this->Bootstrap->icon('info', [
'class' => ['fs-6', 'align-text-top',],
'title' => empty($data['description']) ? '' : h($data['description']),
'attrs' => [
'data-bs-toggle' => 'tooltip',
]
]),
]
);
} else {
$pageTitle = $this->Bootstrap->node('h5', [], h($this->ValueGetter->get($data['title'])));
}
}
if(!empty($notice)) {
echo $this->Bootstrap->alert($notice);
$html .= $this->Bootstrap->alert($notice);
}
if (!empty($modelStatistics)) {
echo $this->element('genericElements/IndexTable/Statistics/index_statistic_scaffold', [
$html .= $this->element('genericElements/IndexTable/Statistics/index_statistic_scaffold', [
'statistics' => $modelStatistics,
]);
}
echo '<div class="panel">';
$html .= '<div class="panel">';
if (!empty($data['html'])) {
echo sprintf('<div>%s</div>', $data['html']);
$html .= sprintf('<div>%s</div>', $data['html']);
}
$skipPagination = isset($data['skip_pagination']) ? $data['skip_pagination'] : 0;
if (!$skipPagination) {
$paginationData = !empty($data['paginatorOptions']) ? $data['paginatorOptions'] : [];
echo $this->element(
if (!empty($embedInModal)) {
$paginationData['update'] = ".modal-main-{$tableRandomValue}";
}
$html .= $this->element(
'/genericElements/IndexTable/pagination',
[
'paginationOptions' => $paginationData,
'tableRandomValue' => $tableRandomValue
]
);
echo $this->element(
$html .= $this->element(
'/genericElements/IndexTable/pagination_links'
);
}
@ -94,8 +102,8 @@ if (!empty($multiSelectData)) {
];
array_unshift($data['fields'], $multiSelectField);
}
if (!empty($data['top_bar'])) {
echo $this->element(
if (!empty($data['top_bar']) && empty($skipTableToolbar)) {
$html .= $this->element(
'/genericElements/ListTopBar/scaffold',
[
'data' => $data['top_bar'],
@ -143,7 +151,7 @@ foreach ($data['data'] as $k => $data_row) {
);
}
$tbody = '<tbody>' . $rows . '</tbody>';
echo sprintf(
$html .= sprintf(
'<table class="table table-hover" id="index-table-%s" data-table-random-value="%s" data-reload-url="%s">%s%s</table>',
$tableRandomValue,
$tableRandomValue,
@ -160,11 +168,23 @@ echo sprintf(
$tbody
);
if (!$skipPagination) {
echo $this->element('/genericElements/IndexTable/pagination_counter', $paginationData);
echo $this->element('/genericElements/IndexTable/pagination_links');
$html .= $this->element('/genericElements/IndexTable/pagination_counter', $paginationData);
$html .= $this->element('/genericElements/IndexTable/pagination_links');
}
$html .= '</div>';
$html .= '</div>';
if (!empty($embedInModal)) {
echo $this->Bootstrap->modal([
'titleHtml' => $pageTitle ?? '',
'bodyHtml' => $html,
'size' => 'xl',
'type' => 'ok-only',
'modalClass' => "modal-main-{$tableRandomValue}"
]);
} else {
echo $html;
}
echo '</div>';
echo '</div>';
?>
<script type="text/javascript">
$(document).ready(function() {

View File

@ -8,7 +8,7 @@
}
$onClick = sprintf(
'onClick="executePagination(%s, %s);"',
"'" . h($tableRandomValue) . "'",
"'" . h($options['update']) . "'",
"'{{url}}'"
);

View File

@ -7,10 +7,10 @@
'text' => $child['text'],
'outline' => !empty($child['outline']),
'icon' => $child['icon'] ?? null,
'params' => array_merge([
'onclick' => 'multiActionClickHandler(this)',
'attrs' => array_merge([
'data-onclick-function' => $child['onclick'] ?? '',
'data-table-random-value' => $tableRandomValue,
'onclick' => 'multiActionClickHandler(this)'
], $child['params'] ?? [])
]);
}

View File

@ -29,12 +29,10 @@
$numberActiveFilters += count($activeFilters['filteringMetaFields']) - 1;
}
$buttonConfig = [
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue)),
'icon' => 'filter',
'variant' => $numberActiveFilters > 0 ? 'warning' : 'primary',
'params' => [
'title' => __('Filter index'),
'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
]
'title' => __('Filter index'),
];
if (count($activeFilters) > 0) {
$buttonConfig['badge'] = [

View File

@ -19,27 +19,31 @@ $availableColumnsHtml = $this->element('/genericElements/ListTopBar/group_table_
$metaTemplateColumnMenu = [];
if (!empty($meta_templates)) {
$metaTemplateColumnMenu[] = ['header' => true, 'text' => __('Meta Templates'), 'icon' => 'object-group',];
foreach ($meta_templates as $meta_template) {
$numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0;
$metaTemplateColumnMenu[] = [
'text' => $meta_template->name,
'sup' => $meta_template->version,
'badge' => [
'text' => $numberActiveMetaField,
'variant' => 'secondary',
'title' => __n('{0} meta-field active for this meta-template', '{0} meta-fields active for this meta-template', $numberActiveMetaField, $numberActiveMetaField),
],
'keepOpen' => true,
'menu' => [
[
'html' => $this->element('/genericElements/ListTopBar/group_table_action/hiddenMetaColumns', [
'tableSettings' => $tableSettings,
'table_setting_id' => $data['table_setting_id'],
'meta_template' => $meta_template,
])
]
],
];
if (empty($meta_templates_enabled)) {
$metaTemplateColumnMenu[] = ['header' => false, 'text' => __('- No enabled Meta Templates found -'), 'class' => ['disabled', 'muted']];
} else {
foreach ($meta_templates_enabled as $meta_template) {
$numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0;
$metaTemplateColumnMenu[] = [
'text' => $meta_template->name,
'sup' => $meta_template->version,
'badge' => [
'text' => $numberActiveMetaField,
'variant' => 'secondary',
'title' => __n('{0} meta-field active for this meta-template', '{0} meta-fields active for this meta-template', $numberActiveMetaField, $numberActiveMetaField),
],
'keepOpen' => true,
'menu' => [
[
'html' => $this->element('/genericElements/ListTopBar/group_table_action/hiddenMetaColumns', [
'tableSettings' => $tableSettings,
'table_setting_id' => $data['table_setting_id'],
'meta_template' => $meta_template,
])
]
],
];
}
}
}
$indexColumnMenu = array_merge(
@ -63,18 +67,20 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
?>
<?php if (!isset($data['requirement']) || $data['requirement']) : ?>
<?php
$now = date("Y-m-d_H-i-s");
$downloadFilename = sprintf('%s_%s.json', $data['table_setting_id'] ?? h($model), $now);
echo $this->Bootstrap->dropdownMenu([
'dropdown-class' => 'ms-1',
'alignment' => 'end',
'direction' => 'down',
'toggle-button' => [
'button' => [
'icon' => 'sliders-h',
'variant' => 'primary',
'class' => ['table_setting_dropdown_button'],
],
'submenu_alignment' => 'end',
'submenu_direction' => 'start',
'params' => [
'attrs' => [
'data-table-random-value' => $tableRandomValue,
'data-table_setting_id' => $data['table_setting_id'],
],
@ -85,6 +91,13 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
'keepOpen' => true,
'menu' => $indexColumnMenu,
],
[
'text' => __('Download'),
'icon' => 'download',
'attrs' => [
'onclick' => sprintf('downloadIndexTable(this, "%s")', $downloadFilename),
],
],
[
'html' => $compactDisplayHtml,
],

View File

@ -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'],
], $availableColumnsHtml);
echo $availableColumnsHtml;

View File

@ -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'],
], $availableMetaColumnsHtml);
echo $availableMetaColumnsHtml;

View File

@ -3,6 +3,6 @@
$tags = Cake\Utility\Hash::get($data, 'tags');
echo $this->Tag->tags($tags, [
'allTags' => $allTags,
'picker' => true,
'editable' => true,
'picker' => !empty($field['editable']),
'editable' => !empty($field['editable']),
]);

View File

@ -1,10 +1,12 @@
<?php
use \Cake\Routing\Router;
use \Cake\Utility\Hash;
$tabData = [
'navs' => [],
'content' => []
];
$viewElementCandidatePath = '/genericElements/SingleViews/Fields/';
foreach($data['MetaTemplates'] as $metaTemplate) {
if (!empty($metaTemplate->meta_template_fields)) {
$tabData['navs'][] = [
@ -15,12 +17,20 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
$labelPrintedOnce = false;
if (!empty($metaTemplateField->metaFields)) {
foreach ($metaTemplateField->metaFields as $metaField) {
$viewElementCandidate = $metaTemplateField->index_type == 'text' ? 'generic' : $metaTemplateField->index_type; // Currently, single-view generic fields are not using index-view fields
$fields[] = [
'key' => !$labelPrintedOnce ? $metaField->field : '',
'raw' => $metaField->value,
'warning' => $metaField->warning ?? null,
'info' => $metaField->info ?? null,
'danger' => $metaField->danger ?? null
// Not relying on the `type` option as this table is a special case where not all values have a label
'raw' => $this->element(sprintf('%s%sField', $viewElementCandidatePath, $viewElementCandidate), [
'data' => $metaField,
'field' => [
'path' => 'value',
]
]),
'rawNoEscaping' => true,
'notice_warning' => $metaTemplateField->warning ?? null,
'notice_info' => $metaTemplateField->info ?? null,
'notice_danger' => $metaTemplateField->danger ?? null
];
$labelPrintedOnce = true;
}
@ -48,7 +58,7 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
'text' => __('Migrate to version {0}', $metaTemplate['hasNewerVersion']->version),
'variant' => 'success',
'nodeType' => 'a',
'params' => [
'attrs' => [
'href' => Router::url([
'controller' => 'metaTemplates',
'action' => 'migrateOldMetaTemplateToNewestVersionForEntity',

View File

@ -45,6 +45,13 @@ use Cake\Routing\Router;
if (!empty($actionEntry['badge'])) {
$badgeNumber += 1;
}
if (!empty($actionEntry['isPOST'])) {
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url'])));
} else if (!empty($actionEntry['isRedirect'])) {
$onclickFunction = sprintf('window.location.replace(\'%s\');', h(Router::url($actionEntry['url'])));
} else {
$onclickFunction = sprintf('UI.overlayUntilResolve(this, UI.modalFromUrl(\'%s\'))', h(Router::url($actionEntry['url'])));
}
$buttonBadge = !empty($actionEntry['badge']) ? $this->Bootstrap->badge($actionEntry['badge']) : '';
echo $this->Bootstrap->button([
'text' => h($actionEntry['label']),
@ -52,33 +59,10 @@ use Cake\Routing\Router;
'variant' => $actionEntry['variant'] ?? 'primary',
'size' => 'sm',
'class' => ['text-nowrap'],
'onclick' => sprintf('UI.overlayUntilResolve(this, UI.submissionModalAutoGuess(\'%s\'))', h(Router::url($actionEntry['url']))),
'onclick' => $onclickFunction,
], $buttonBadge);
}
echo '</div>';
}
?>
</div>
<!--
<div class="breadcrumb-link-container position-absolute end-0 d-flex">
<div class="header-breadcrumb-children d-none d-md-flex btn-group">
<?= $breadcrumbLinks ?>
<?php if (!empty($breadcrumbAction)) : ?>
<a class="btn btn-primary btn-sm dropdown-toggle" href="#" role="button" id="dropdownMenuBreadcrumbAction" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<?= __('Actions') ?>
<?=
$badgeNumber == 0 ? '' : $this->Bootstrap->badge([
'text' => h($badgeNumber),
'variant' => 'warning',
'pill' => false,
'title' => __n('There is {0} action available', 'There are {0} actions available', $badgeNumber, h($badgeNumber)),
])
?>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuBreadcrumbAction">
<?= $breadcrumbAction ?>
</div>
<?php endif; ?>
</div>
</div> -->

View File

@ -2,7 +2,7 @@
<div class="left-navbar">
<a class="navbar-brand d-sm-block d-none" href="<?= $baseurl ?>">
<div class="composed-app-icon-container">
<span class="app-icon w-100 h-100" title="<?= __('Cerebrate') ?>"></span>
<span class="app-icon w-100 h-100" title="<?= __('MISP') ?>"></span>
</div>
</a>
<button class="navbar-toggler d-sm-none" type="button" data-bs-toggle="collapse" data-bs-target="#app-sidebar" aria-controls="app-sidebar" aria-expanded="false" aria-label="Toggle navigation">
@ -13,6 +13,6 @@
<?= $this->element('layouts/header/header-breadcrumb'); ?>
</div>
<div class="right-navbar">
<?= $this->element('layouts/header/header-right'); ?>
<?= $this->element('layouts/header/header-right'); ?>
</div>
</div>
</div>

View File

@ -25,6 +25,7 @@ $variant = array_flip($severity)[$maxSeverity];
if ($hasNotification) {
echo $this->Bootstrap->notificationBubble([
'variant' => $variant,
'borderVariant' => 'light',
]);
}
?>

View File

@ -6,7 +6,5 @@ echo $this->Bootstrap->button([
'variant' => 'primary',
'size' => 'sm',
'class' => 'mb-1',
'params' => [
'id' => 'btn-add-bookmark',
]
'id' => 'btn-add-bookmark',
]);

View File

@ -28,7 +28,7 @@
'size' => 'sm',
'icon' => h($icon),
'class' => ['mb-1', !$validURI ? 'disabled' : ''],
'params' => [
'attrs' => [
'href' => $validURI ? h($url) : '#',
]
]);

View File

@ -77,7 +77,7 @@ $cardContent = sprintf(
);
echo $this->Bootstrap->card([
'variant' => 'secondary',
'bodyVariant' => 'secondary',
'bodyHTML' => $cardContent,
'bodyClass' => 'p-3',
'class' => ['shadow-sm', (empty($panelNoGrow) ? 'grow-on-hover' : '')]

View File

@ -1,28 +1,38 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?= h($title) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div class="modal-body">
<p><?= h($question) ?></p>
</div>
<div class="modal-footer">
<?= $this->Form->postLink(
h($actionName),
$path,
['class' => 'btn btn-primary button-execute', 'id' => 'submitButton']
)
?>
<button type="button" class="btn btn-secondary cancel-button" data-bs-dismiss="modal"><?= __('Cancel') ?></button>
</div>
</div>
</div>
<script type="text/javascript">
$(document).keydown(function(e) {
if(e.which === 13 && e.ctrlKey) {
$('.button-execute').click();
}
});
</script>
<?php
/**
* Supported parameters:
* - title: The title of the modal
* - question: The content of the modal's body.
* - actionName: The text of the confirm button. Basically what the confirmation will do
* - modalOptions: Additional options to be passed to the modal
*/
$form = $this->element('genericElements/Form/genericForm', [
'entity' => null,
'ajax' => false,
'raw' => true,
'data' => [
'fields' => [
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
$formHTML = sprintf('<div class="d-none">%s</div>', $form);
$bodyMessage = h($question ?? '');
$bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage);
$defaultOptions = [
'size' => 'lg',
'title' => isset($title) ? h($title) : __('Confirm'),
'type' => 'confirm',
'confirmButton' => [
'text' => !empty($actionName) ? h($actionName) : __('Confirm'),
'variant' => 'primary',
],
];
$modalOptions = array_merge($defaultOptions, $modalOptions ?? []);
$modalOptions['bodyHtml'] = $bodyHTML;
echo $this->Bootstrap->modal($modalOptions);

View File

@ -28,8 +28,11 @@ $bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage);
echo $this->Bootstrap->modal([
'size' => 'lg',
'title' => !empty($deletionTitle) ? $deletionTitle : __('Delete {0}', h(Cake\Utility\Inflector::singularize(Cake\Utility\Inflector::humanize($this->request->getParam('controller'))))),
'type' => 'confirm-danger',
'confirmText' => !empty($deletionConfirm) ? $deletionConfirm : __('Delete'),
'type' => 'confirm',
'confirmButton' => [
'text' => !empty($deletionConfirm) ? $deletionConfirm : __('Delete'),
'variant' => 'danger',
],
'bodyHtml' => $bodyHTML,
]);
?>

View File

@ -19,12 +19,12 @@ $filteringForm = $this->Bootstrap->table(
[
'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));
}
],
[
'key' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
'path' => 'operator', 'label' => __('Operator'), 'formatter' => function ($field, $row) use ($typeMap) {
$fieldName = $row['fieldname'];
$type = $typeMap[$fieldName] ?? 'text';
$options = [
@ -41,25 +41,34 @@ $filteringForm = $this->Bootstrap->table(
}
],
[
'key' => 'value',
'path' => 'value',
'labelHtml' => sprintf(
'%s %s',
__('Value'),
sprintf('<sup class="fa fa-info" title="%s"><sup>', __('Supports strict matches and LIKE matches with the `%` character.&#10;Example: `%.com`'))
),
'formatter' => function ($field, $row) use ($typeMap, $formTypeMap) {
'formatter' => function ($field, $row) use ($typeMap, $formTypeMap, $filtersConfig) {
$fieldName = $row['fieldname'];
$formType = $formTypeMap[$typeMap[$fieldName]] ?? 'text';
$fieldData = [
'field' => $fieldName,
'type' => $formType,
'label' => '',
'class' => 'fieldValue form-control-sm'
];
if (!empty($filtersConfig[$fieldName]['multiple'])) {
$fieldData['type'] = 'dropdown';
$fieldData['multiple'] = true;
$fieldData['select2'] = [
'tags' => true,
'tokenSeparators' => [',', ' '],
];
}
$this->Form->setTemplates([
'formGroup' => '<div class="col-sm-10">{{input}}</div>',
]);
return $this->element('genericElements/Form/fieldScaffold', [
'fieldData' => [
'field' => $fieldName,
'type' => $formType,
'label' => '',
'class' => 'fieldValue form-control-sm'
],
'fieldData' => $fieldData,
'params' => []
]);
}
@ -71,23 +80,23 @@ $filteringForm = $this->Bootstrap->table(
$filteringMetafields = '';
if ($metaFieldsEnabled) {
$helpText = $this->Bootstrap->genNode('sup', [
$helpText = $this->Bootstrap->node('sup', [
'class' => ['ms-1 fa fa-info'],
'title' => __('Include help'),
'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);
}
$filteringTags = '';
if ($taggingEnabled) {
$helpText = $this->Bootstrap->genNode('sup', [
$helpText = $this->Bootstrap->node('sup', [
'class' => ['ms-1 fa fa-info'],
'title' => __('Supports negation matches (with the `!` character) and LIKE matches (with the `%` character).&#10;Example: `!exportable`, `%able`'),
'data-bs-toggle' => 'tooltip',
]);
$filteringTags = $this->Bootstrap->genNode('h5', [
$filteringTags = $this->Bootstrap->node('h5', [
'class' => 'mt-2'
], __('Tags') . $helpText);
$filteringTags .= $this->Tag->tags([], [
@ -104,7 +113,9 @@ echo $this->Bootstrap->modal([
'size' => !empty($metaFieldsEnabled) ? 'xl' : 'lg',
'type' => 'confirm',
'bodyHtml' => $modalBody,
'confirmText' => __('Filter'),
'confirmButton' => [
'text' => __('Filter'),
],
'confirmFunction' => 'filterIndex'
]);
?>
@ -167,6 +178,36 @@ echo $this->Bootstrap->modal([
}
setFilteringValues($filteringTable, field, value, operator)
}
if (tags.length > 0) {
setFilteringTags($filteringTable, tags)
}
}
function setFilteringValues($filteringTable, field, value, operator) {
$row = $filteringTable.find('td > span.fieldName').filter(function() {
return $(this).data('fieldname') == field
}).closest('tr')
$row.find('.fieldOperator').val(operator)
const $formElement = $row.find('.fieldValue');
if ($formElement.attr('type') === 'datetime-local') {
$formElement.val(moment(value).format('yyyy-MM-DDThh:mm:ss'))
} else if ($formElement.is('select') && Array.isArray(value)) {
let newOptions = [];
value.forEach(aValue => {
const existingOption = $formElement.find('option').filter(function() {
return $(this).val() === aValue
})
if (existingOption.length == 0) {
newOptions.push(new Option(aValue, aValue, true, true))
}
})
$formElement.append(newOptions).trigger('change');
} else {
$formElement.val(value)
}
}
function setFilteringTags($filteringTable, tags) {
$select = $filteringTable.closest('.modal-body').find('select.select2-input')
let passedTags = []
tags.forEach(tagname => {
@ -183,19 +224,6 @@ echo $this->Bootstrap->modal([
.trigger('change')
}
function setFilteringValues($filteringTable, field, value, operator) {
$row = $filteringTable.find('td > span.fieldName').filter(function() {
return $(this).data('fieldname') == field
}).closest('tr')
$row.find('.fieldOperator').val(operator)
const $formElement = $row.find('.fieldValue');
if ($formElement.attr('type') === 'datetime-local') {
$formElement.val(moment(value).format('yyyy-MM-DDThh:mm:ss'))
} else {
$formElement.val(value)
}
}
function getDataFromRow($row) {
const rowData = {};
rowData['name'] = $row.find('td > span.fieldName').data('fieldname')

View File

@ -1 +1,13 @@
<?= $this->Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id, $fieldName], ['confirm' => __('Are you sure you want to toggle {0} of {1}?', $fieldName. $entity->id)]) ?>
<?php
$form = $this->Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id, $fieldName], ['confirm' => __('Are you sure you want to toggle `{0}` of {1}?', h($fieldName), h($entity->id))]);
$formHTML = sprintf('<div class="d-none">%s</div>', $form);
$bodyHTML = $formHTML;
$modalOptions = [
'title' => __('Are you sure you want to toggle `{0}` of {1}?', h($fieldName), h($entity->id)),
'type' => 'confirm',
'bodyHtml' => $bodyHTML,
];
echo $this->Bootstrap->modal($modalOptions);

View File

@ -58,7 +58,7 @@ $sidebarOpen = $loggedUser->user_settings_by_name_with_fallback['ui.sidebar.expa
<?= $this->Html->css('CodeMirror/addon/hint/show-hint') ?>
<?= $this->Html->css('CodeMirror/addon/lint/lint') ?>
<?= $this->Html->css('select2.min') ?>
<?= $this->Html->css('select2-bootstrap5') ?>
<?= $this->Html->css('select2-bootstrap5-vars') ?>
<?= $this->Html->script('apexcharts.min') ?>
<?= $this->Html->script('moment-with-locales.min') ?>
<?= $this->Html->css('apexcharts') ?>

View File

@ -27,7 +27,7 @@
</head>
<body>
<div class="position-absolute cerebrate-background-logo"></div>
<div class="position-absolute"></div>
<?= $this->Flash->render() ?>
<?= $this->fetch('content') ?>
<div id="mainModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mediumModalLabel" aria-hidden="true"></div>

View File

@ -19,6 +19,10 @@ body {
margin-bottom: 1rem;
}
.top-navbar .left-navbar .navbar-brand {
margin-right: 0;
}
/* navbar-toggler breakpoint */
@media (max-width: 576px) {
body {
@ -576,31 +580,65 @@ ul.sidebar-elements > li.category > span.category-divider > hr {
.composed-app-icon-container {
height: calc(var(--navbar-height) - 10px);
width: calc(var(--navbar-height) - 10px);
width: calc(var(--navbar-height) - 0px);
position: relative;
display: block;
}
.composed-app-icon-container > .app-icon {
display: inline-block;
mask-image: url(/img/icon-composition/sheet-all.svg), url(/img/icon-composition/z.svg);
mask-position: 0 0, 2.4px calc((var(--navbar-height) - 10px) / 2);
mask-image: url(/img/icon-composition/sheet-all.svg),
url(/img/icon-composition/bubble.svg),
url(/img/icon-composition/bubble.svg),
url(/img/icon-composition/bubble.svg);
mask-position: 0 0, 0 0, 6px 0, 12px 0;
mask-repeat: no-repeat;
mask-size: contain, cover;
mask-composite: source-out;
mask-composite: subtract;
-webkit-mask-image: url(/img/icon-composition/sheet-all.svg), url(/img/icon-composition/z.svg);
-webkit-mask-position: 0 0, 2.4px calc((var(--navbar-height) - 10px) / 2);
mask-size: contain, 100%, 100%, 100%;
mask-composite: xor;
-webkit-mask-image: url(/img/icon-composition/sheet-all.svg),
url(/img/icon-composition/bubble.svg),
url(/img/icon-composition/bubble.svg),
url(/img/icon-composition/bubble.svg);
-webkit-mask-position: 0 0, 0 0, 6px 0, 12px 0;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain, cover;
-webkit-mask-composite: source-out;
-webkit-mask-composite: subtract;
transition-timing-function: ease-out;
transition-duration: 0.2s;
transition-property: -webkit-mask-position;
-webkit-mask-size: contain, 100%, 100%, 100%;
-webkit-mask-composite: xor;
}
.composed-app-icon-container > .app-icon:hover {
mask-position: 0 0, 2.4px calc(0.75 * (var(--navbar-height) - 10px) / 2);
-webkit-mask-position: 0 0, 2.4px calc(0.75 * (var(--navbar-height) - 10px) / 2);
}
animation: MoveUpDown 1.5s linear infinite;
background: linear-gradient(0deg, #fff 0%, #fff 54%, #2fa1db 54%);
}
@keyframes MoveUpDown {
0%, 100% {
mask-size: contain, 100%, 100%, 100%;
-webkit-mask-size: contain, 100%, 100%, 100%;
mask-position: 0 0, 0 0, 6px 0, 12px 0;
-webkit-mask-position: 0 0, 0 0, 6px 0, 12px 0;
}
5% {
mask-size: contain, 200%, 100%, 100%;
-webkit-mask-size: contain, 200%, 100%, 100%;
mask-position: 0 0, -30px -8px, 6px 0, 12px 0;
-webkit-mask-position: 0 0, -30px -8px, 6px 0, 12px 0;
}
15% {
mask-size: contain, 100%, 200%, 100%;
-webkit-mask-size: contain, 100%, 200%, 100%;
mask-position: 0 0, 0 0, -24px -8px, 12px 0;
-webkit-mask-position: 0 0, 0 0, -24px -8px, 12px 0;
}
25% {
mask-size: contain, 100%, 100%, 200%;
-webkit-mask-size: contain, 100%, 100%, 200%;
mask-position: 0 0, 0 0, 6px 0, -18.5px -8px;
-webkit-mask-position: 0 0, 0 0, 6px 0, -18.5px -8px;
}
35% {
mask-size: contain, 100%, 100%, 100%;
-webkit-mask-size: contain, 100%, 100%, 100%;
mask-position: 0 0, 0 0, 6px 0, 12px 0;
-webkit-mask-position: 0 0, 0 0, 6px 0, 12px 0;
}
}

View File

@ -17,30 +17,6 @@ body {
/* background by SVGBackgrounds.com */
}
.cerebrate-background-logo {
background-color: var(--application-color);
-webkit-mask: url(/img/icon-composition/z.svg) no-repeat;
mask: url(/img/icon-composition/z.svg) no-repeat;
-webkit-mask-size: 400px 400px;
mask-size: 400px 400px;
width: 400px;
height: 400px;
opacity: 0.15;
-webkit-transform: rotate(100deg);
transform: rotate(100deg);
-webkit-transform-origin: center;
transform-origin: center;
left: calc(50% - 120px);
top: 30%;
}
.cerebrate-background-logo:hover {
-webkit-animation:spin 6s linear infinite;
animation:spin 6s linear infinite;
-webkit-animation-delay: 2s;
animation-delay: 2s;
}
@-webkit-keyframes spin {
100% { -webkit-transform: rotate(460deg); }
}

View File

@ -0,0 +1,516 @@
/*!
* Select2 v4 Bootstrap 5 theme v1.1.1
*/
.select2-container--bootstrap-5 {
display: block;
}
.select2-container--bootstrap-5 *:focus {
outline: 0;
}
.select2-container--bootstrap-5 .select2-selection {
width: 100%;
min-height: calc(1.5em + (0.75rem + 2px));
padding: 0.375rem 0.75rem;
font-family: inherit;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border: 1px solid var(--bs-gray-400);
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
@media (prefers-reduced-motion: reduce) {
.select2-container--bootstrap-5 .select2-selection {
transition: none;
}
}
.select2-container--bootstrap-5.select2-container--focus .select2-selection, .select2-container--bootstrap-5.select2-container--open .select2-selection {
border-color: rgba(var(--bs-primary-rgb), 0.5);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}
.select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection {
border-bottom: 1px solid transparent;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection {
border-top: 1px solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--bootstrap-5 .select2-search {
width: 100%;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear,
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
position: absolute;
top: 50%;
right: 2.25rem;
width: 0.75rem;
height: 0.75rem;
padding: 0.25em 0.25em;
overflow: hidden;
text-indent: 100%;
white-space: nowrap;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
transform: translateY(-50%);
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover,
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear > span,
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear > span {
display: none;
}
.select2-container--bootstrap-5 .select2-dropdown {
border-color: var(--bs-gray-400);
border-radius: 0.25rem;
}
.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--below {
border-top: 0 solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--bootstrap-5 .select2-dropdown.select2-dropdown--above {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-search {
padding: 0.375rem 0.75rem;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-family: inherit;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
background-clip: padding-box;
border: 1px solid var(--bs-gray-400);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
transition: none;
}
}
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field:focus {
border-color: rgba(var(--bs-primary-rgb), 0.5);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options:not(.select2-results__options--nested) {
max-height: 15rem;
overflow-y: auto;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__message {
color: var(--bs-secondary);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--highlighted {
color: #000;
background-color: var(--bs-light);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--selected, .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-selected=true] {
color: var(--bs-body-color);
background-color: var(--bs-primary);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--disabled, .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-disabled=true] {
color: var(--bs-gray);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] {
padding: 0;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.375rem 0.375rem;
font-weight: 500;
line-height: 1.5;
color: var(--bs-gray);
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.375rem 0.75rem;
}
.select2-container--bootstrap-5 .select2-selection--single {
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered {
padding: 0;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder {
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
}
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__arrow {
display: none;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding-left: 0;
margin: 0;
list-style: none;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.35em 0.65em;
margin-right: 0.375rem;
margin-bottom: 0.375rem;
font-size: 1rem;
color: var(--bs-body-color);
cursor: auto;
border: 1px solid var(--bs-gray-400);
border-radius: 0.25rem;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 0.75rem;
height: 0.75rem;
padding: 0.25em 0.25em;
margin-right: 0.25rem;
overflow: hidden;
text-indent: 100%;
white-space: nowrap;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
border: 0;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.75rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove > span {
display: none;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-search {
display: block;
width: 100%;
height: 1.5rem;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-search .select2-search__field {
width: 100%;
height: 1.5rem;
margin-top: 0;
margin-left: 0;
font-family: inherit;
line-height: 1.5;
background-color: transparent;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
right: 0.75rem;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection {
color: var(--bs-secondary);
cursor: not-allowed;
background-color: var(--bs-gray-200);
border-color: var(--bs-gray-400);
box-shadow: none;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__clear, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__clear {
display: none;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice {
cursor: not-allowed;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__choice .select2-selection__choice__remove {
display: none;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty), .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0;
}
.select2-container--bootstrap-5.select2-container--disabled .select2-selection--multiple .select2-selection__rendered:not(:empty) + .select2-search, .select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection--multiple .select2-selection__rendered:not(:empty) + .select2-search {
display: none;
}
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu).select2-container--bootstrap-5 .select2-selection {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > .input-group-text ~ .select2-container--bootstrap-5 .select2-selection,
.input-group > .btn ~ .select2-container--bootstrap-5 .select2-selection,
.input-group > .dropdown-menu ~ .select2-container--bootstrap-5 .select2-selection {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group .select2-container--bootstrap-5 {
flex-grow: 1;
}
.input-group .select2-container--bootstrap-5 .select2-selection {
height: 100%;
}
.is-valid + .select2-container--bootstrap-5 .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5 .select2-selection {
border-color: var(--bs-success);
}
.is-valid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, .is-valid + .select2-container--bootstrap-5.select2-container--open .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open .select2-selection {
border-color: var(--bs-success);
box-shadow: 0 0 0 0.25rem rgba( var(--bs-success-rgb), 0.25);
}
.is-valid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection {
border-bottom: 1px solid transparent;
}
.is-valid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection, form.was-validated select:valid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection {
border-top: 1px solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.is-invalid + .select2-container--bootstrap-5 .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5 .select2-selection {
border-color: var(--bs-danger);
}
.is-invalid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, .is-invalid + .select2-container--bootstrap-5.select2-container--open .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--focus .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open .select2-selection {
border-color: var(--bs-danger);
box-shadow: 0 0 0 0.25rem rgba( var(--bs-danger-rgb), 0.25);
}
.is-invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--below .select2-selection {
border-bottom: 1px solid transparent;
}
.is-invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection, form.was-validated select:invalid + .select2-container--bootstrap-5.select2-container--open.select2-container--above .select2-selection {
border-top: 1px solid transparent;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection {
min-height: calc(1.5em + (0.5rem + 2px));
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--single .select2-selection__clear,
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--single .select2-selection__clear:hover,
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-search .select2-search__field {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.25rem 0.25rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.25rem 0.5rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--single {
padding: 0.25rem 2.25rem 0.25rem 0.5rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.25rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 0.875rem;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--small ~ .select2-selection--multiple .select2-selection__clear {
right: 0.5rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection {
min-height: calc(1.5em + (1rem + 2px));
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--single .select2-selection__clear,
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--single .select2-selection__clear:hover,
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-search .select2-search__field {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.5rem 0.5rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.5rem 1rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--single {
padding: 0.5rem 2.25rem 0.5rem 1rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.5rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 1.25rem;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.select2-container--bootstrap-5 .select2--large ~ .select2-selection--multiple .select2-selection__clear {
right: 1rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection {
min-height: calc(1.5em + (0.5rem + 2px));
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear,
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover,
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.25rem 0.25rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.25rem 0.5rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--single {
padding: 0.25rem 2.25rem 0.25rem 0.5rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.25rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 0.875rem;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 0.5rem;
height: 0.5rem;
padding: 0.125rem 0.125rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/0.5rem auto no-repeat;
}
.form-select-sm ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
right: 0.5rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection {
min-height: calc(1.5em + (1rem + 2px));
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear,
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single .select2-selection__clear:hover,
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option {
padding: 0.5rem 1rem;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
padding: 0.5rem 0.5rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__options--nested .select2-results__option {
padding: 0.5rem 1rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--single {
padding: 0.5rem 2.25rem 0.5rem 1rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered:not(:empty) {
padding-bottom: 0.5rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.35em 0.65em;
font-size: 1.25rem;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
width: 1rem;
height: 1rem;
padding: 0.5rem 0.5rem;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23676a6d'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove:hover {
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1rem auto no-repeat;
}
.form-select-lg ~ .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__clear {
right: 1rem;
}

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="67.622406mm"
height="49.558979mm"
viewBox="0 0 67.622406 49.558979"
version="1.1"
id="svg108"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="bubbles1.svg">
<defs
id="defs102">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath312-7">
<path
inkscape:connector-curvature="0"
d="M 0,0 H 595.276 V 841.89 H 0 Z"
id="path314-0" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1086">
<path
inkscape:connector-curvature="0"
d="M 0,0 H 595.276 V 841.89 H 0 Z"
id="path1088" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.8"
inkscape:cx="121.95418"
inkscape:cy="111.35466"
inkscape:document-units="mm"
inkscape:current-layer="g310-5"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="2289"
inkscape:window-height="1403"
inkscape:window-x="2560"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata105">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-6.2542725,-124.80979)">
<g
id="g308"
transform="matrix(0.17650827,0,0,-0.17650827,-4.3435815,225.27546)">
<g
id="g310-5"
clip-path="url(#clipPath312-7)">
<path
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.80924821;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 126.62695,26.017578 c -3.92265,0 -7.10156,3.17891 -7.10156,7.101563 0,3.922653 3.17891,7.103515 7.10156,7.103515 3.91865,0 7.10352,-3.180862 7.10352,-7.103515 0,-3.922653 -3.18487,-7.101563 -7.10352,-7.101563 z"
transform="matrix(1.4989855,0,0,-1.4989855,60.041685,569.18393)"
id="path1681" />
<g
id="g320-4"
transform="translate(349.7253,569.1839)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 145 KiB

View File

@ -359,6 +359,9 @@ class AJAXApi {
if (!skipRequestHooks) {
this.beforeRequest()
}
if (form === undefined || form.nodeName !== 'FORM') {
throw new Error(`Form argument must be a valid HTMLFormELement.`)
}
let toReturn
let feedbackShown = false
try {
@ -381,7 +384,7 @@ class AJAXApi {
this.provideSuccessFeedback(data, {}, skipFeedback)
toReturn = data;
} else {
this.provideFailureFeedback(data, {}, skipFeedback)
this.provideFailureFeedback(data, {}, false)
feedbackShown = true
this.injectFormValidationFeedback(form, data.errors)
toReturn = Promise.reject(data.errors);

View File

@ -24,6 +24,18 @@ class UIFactory {
return theModal
}
/**
* Create a popover
* @param {Object} element - The target element on which to attach the popover
* @param {Object} options - The options to be passed to the PopoverFactory class
* @return {Object} The PopoverFactory object
*/
popover(element, options) {
const thePopover = new PopoverFactory(element, options);
thePopover.makePopover()
return thePopover
}
/**
* Create and display a modal where the modal's content is fetched from the provided URL. Link an AJAXApi to the submission button
* @param {string} url - The URL from which the modal's content should be fetched
@ -48,7 +60,33 @@ class UIFactory {
}).catch((error) => {
UI.toast({
variant: 'danger',
title: 'Error while loading the processor',
title: 'Error while loading the modal',
body: error.message
})
})
}
/**
* Create and display a modal where the modal's content is fetched from the provided URL
* @param {string} url - The URL from which the modal's content should be fetched
* @param {Object=[]} modalOptions - Additional options to be passed to the modal constructor
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
*/
async modalFromUrl(url, modalOptions={}) {
return AJAXApi.quickFetchURL(url).then((modalHTML) => {
const defaultOptions = {
rawHtml: modalHTML,
}
const options = Object.assign({}, defaultOptions, modalOptions)
const theModal = new ModalFactory(options);
theModal.makeModal()
theModal.show()
theModal.$modal.data('modalObject', theModal)
return [theModal, theModal.ajaxApi]
}).catch((error) => {
UI.toast({
variant: 'danger',
title: 'Error while loading the modal',
body: error.message
})
})
@ -171,7 +209,6 @@ class UIFactory {
}
return UI.submissionModal(url, successCallback)
}
/**
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the provided element after a successful operation.
@ -247,6 +284,67 @@ class UIFactory {
})
return promise
}
/**
* Place an overlay onto a node and remove it whenever the promise resolves
* @param {(jQuery|string)} node - The node on which the confirm popover should be palced
* @param {Object} options - The options to be passed to the overlay class
* @return {Promise} Result of the passed promised
*/
quickConfirm(node, options={}) {
const $node = $(node)
const defaultOptions = {
title: 'Confirm action',
description: '',
descriptionHtml: false,
container: 'body',
variant: 'success',
confirmText: 'Confirm',
confirm: function() {}
}
options = Object.assign({}, defaultOptions, options)
options.description = options.descriptionHtml ? options.descriptionHtml : sanitize(options.description)
const popoverOptions = {
title: options.title,
titleHtml: options.titleHtml,
container: options.container,
html: true,
}
var promiseResolve, promiseReject;
const confirmPromise = new Promise(function (resolve, reject) {
promiseResolve = resolve;
promiseReject = reject;
})
popoverOptions.content = function() {
const $node = $(this)
const $container = $('<div>')
const $buttonCancel = $('<a class="btn btn-secondary btn-sm me-2">Cancel</a>')
.click(function() {
const popover = bootstrap.Popover.getInstance($node[0])
popover.dispose()
})
const $buttonSubmit = $(`<a class="submit-button btn btn-${options.variant} btn-sm">${options.confirmText}</a>`)
.click(function() {
options.confirm()
.then(function(result) {
promiseResolve(result)
})
.catch(function(error) {
promiseReject(error)
})
const popover = bootstrap.Popover.getInstance($node[0])
popover.dispose()
})
$container.append(`<p>${options.description}</p>`)
$container.append($(`<div>`).append($buttonCancel, $buttonSubmit))
return $container
}
const thePopover = this.popover($node, popoverOptions)
thePopover.show()
return confirmPromise // have to return a promise to avoid closing the modal
}
}
/** Class representing a Toast */
@ -257,6 +355,9 @@ class Toaster {
*/
constructor(options) {
this.options = Object.assign({}, Toaster.defaultOptions, options)
if (this.options.delay == 'auto') {
this.options.delay = this.computeDelay()
}
this.bsToastOptions = {
autohide: this.options.autohide,
delay: this.options.delay,
@ -271,7 +372,7 @@ class Toaster {
* @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 {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)} mutedHtml - The raw HTML muted's content of the toast
* @property {(jQuery|string)} bodyHtml - The raw HTML body's content of the toast
@ -284,7 +385,7 @@ class Toaster {
body: false,
variant: 'default',
autohide: true,
delay: 5000,
delay: 'auto',
titleHtml: false,
mutedHtml: false,
bodyHtml: false,
@ -350,18 +451,16 @@ class Toaster {
$toast.attr('id', options.id)
}
$toast.addClass('toast-' + options.variant)
if (options.title !== false || options.titleHtml !== false || options.muted !== false || options.mutedHtml !== false) {
if (options.title !== false || options.titleHtml !== false || options.muted !== false || options.mutedHtml !== false || options.closeButton) {
var $toastHeader = $('<div class="toast-header"/>')
$toastHeader.addClass('toast-' + options.variant)
if (options.title !== false || options.titleHtml !== false) {
var $toastHeaderText
if (options.titleHtml !== false) {
$toastHeaderText = $('<div class="me-auto"/>').html(options.titleHtml);
} else {
$toastHeaderText = $('<strong class="me-auto"/>').text(options.title)
}
$toastHeader.append($toastHeaderText)
let $toastHeaderText = $('<span class="me-auto"/>')
if (options.titleHtml !== false) {
$toastHeaderText = $('<div class="me-auto"/>').html(options.titleHtml);
} else if (options.title !== false) {
$toastHeaderText = $('<strong class="me-auto"/>').text(options.title)
}
$toastHeader.append($toastHeaderText)
if (options.muted !== false || options.mutedHtml !== false) {
var $toastHeaderMuted
if (options.mutedHtml !== false) {
@ -391,6 +490,12 @@ class Toaster {
}
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 */
@ -402,15 +507,16 @@ class ModalFactory {
constructor(options) {
this.options = Object.assign({}, ModalFactory.defaultOptions, options)
if (options.POSTSuccessCallback !== undefined) {
if (this.options.rawHtml) {
this.attachSubmitButtonListener = true
} else {
if (!this.options.rawHtml) {
UI.toast({
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>.'
})
}
}
if (this.options.rawHtml) {
this.attachSubmitButtonListener = true
}
if (options.type === undefined && options.cancel !== undefined) {
this.options.type = 'confirm'
}
@ -428,13 +534,13 @@ class ModalFactory {
*/
/**
* @callback ModalFactory~confirm
* @param {ModalFactory~closeModalFunction} closeFunction - A function that will close the modal if called
* @param {ModalFactory~confirm} closeFunction - A function that will close the modal if called
* @param {Object} modalFactory - The instance of the ModalFactory
* @param {Object} evt - The event that triggered the confirm operation
*/
/**
* @callback ModalFactory~cancel
* @param {ModalFactory~closeModalFunction} closeFunction - A function that will close the modal if called
* @param {ModalFactory~cancel} closeFunction - A function that will close the modal if called
* @param {Object} modalFactory - The instance of the ModalFactory
* @param {Object} evt - The event that triggered the confirm operation
*/
@ -621,6 +727,9 @@ class ModalFactory {
}
} else {
$modalDialog = $('<div class="modal-dialog"/>')
if (this.options.size !== false) {
$modalDialog.addClass(`modal-${this.options.size}`)
}
const $modalContent = $('<div class="modal-content"/>')
if (this.options.title !== false || this.options.titleHtml !== false) {
const $modalHeader = $('<div class="modal-header"/>')
@ -796,17 +905,25 @@ class ModalFactory {
$form = this.$modal.find('form')
}
if ($submitButton.data('confirmfunction') !== undefined && $submitButton.data('confirmfunction') !== '') {
$submitButton[0].removeAttribute('onclick')
const clickHandler = window[$submitButton.data('confirmfunction')]
if (clickHandler === undefined) {
console.error(`Function \`${$submitButton.data('confirmfunction')}\` is not defined`)
}
this.options.APIConfirm = (tmpApi) => {
let clickResult = clickHandler(this, tmpApi)
if (clickResult !== undefined) {
return clickResult
.then((data) => {
if (data.success) {
if (!data) {
this.options.POSTSuccessCallback([data, this])
} else { // Validation error
this.injectFormValidationFeedback(form, data.errors)
return Promise.reject('Validation error');
} else {
if (data.success == undefined || data.success) {
this.options.POSTSuccessCallback([data, this])
} else { // Validation error
this.injectFormValidationFeedback(form, data.errors)
return Promise.reject('Validation error');
}
}
})
.catch((errorMessage) => {
@ -816,23 +933,28 @@ class ModalFactory {
}
}
} else {
$submitButton[0].removeAttribute('onclick')
this.options.APIConfirm = (tmpApi) => {
return tmpApi.postForm($form[0])
.then((data) => {
if (data.success) {
// this.options.POSTSuccessCallback(data)
this.options.POSTSuccessCallback([data, this])
} else { // Validation error
this.injectFormValidationFeedback(form, data.errors)
return Promise.reject('Validation error');
}
})
.catch((errorMessage) => {
this.options.POSTFailCallback([errorMessage, this])
// this.options.POSTFailCallback(errorMessage)
return Promise.reject(errorMessage);
})
if ($form[0]) {
// Submit the form via the AJAXApi
$submitButton[0].removeAttribute('onclick')
this.options.APIConfirm = (tmpApi) => {
return tmpApi.postForm($form[0])
.then((data) => {
if (!data) {
this.options.POSTSuccessCallback([data, this])
} else {
if (data.success == undefined || data.success) {
this.options.POSTSuccessCallback([data, this])
} else { // Validation error
this.injectFormValidationFeedback(form, data.errors)
return Promise.reject('Validation error');
}
}
})
.catch((errorMessage) => {
this.options.POSTFailCallback([errorMessage, this])
return Promise.reject(errorMessage);
})
}
}
}
$submitButton.click(this.getConfirmationHandlerFunction($submitButton))
@ -879,7 +1001,7 @@ class OverlayFactory {
spinnerVariant: '',
spinnerSmall: false,
spinnerType: 'border',
fallbackBoostrapVariant: '',
fallbackBootstrapVariant: '',
wrapperCSSDisplay: '',
}
@ -978,7 +1100,7 @@ class OverlayFactory {
let classes = this.$node.attr('class')
if (classes !== undefined) {
classes = classes.split(' ')
const detectedVariant = OverlayFactory.detectedBootstrapVariant(classes, this.options.fallbackBoostrapVariant)
const detectedVariant = OverlayFactory.detectedBootstrapVariant(classes, this.options.fallbackBootstrapVariant)
this.options.spinnerVariant = detectedVariant
}
}
@ -987,7 +1109,7 @@ class OverlayFactory {
* Detect the bootstrap variant from a list of classes
* @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)$/;
let result
for (let i=0; i<classes.length; i++) {
@ -1075,6 +1197,90 @@ class FormValidationHelper {
}
/** Class representing a Popover */
class PopoverFactory {
/**
* Create a Popover.
* @param {Object} element - The target element on which to attach the popover
* @param {Object} options - The options supported by PopoverFactory#defaultOptions
*/
constructor(element, options) {
this.element = $(element)[0]
this.options = Object.assign({}, PopoverFactory.defaultOptions, options)
}
/**
* @namespace
* @property {string} title - The title's content of the popover
* @property {string} titleHtml - The raw HTML title's content of the popover
* @property {string} body - The body's content of the popover
* @property {string} bodyHtml - The raw HTML body's content of the popover
* @property {string} content - Forward the popover's content to the bootstrap popover constructor
* @property {string} html - Manually allow HTML in both the title and body
* @property {string=('primary'|'secondary'|'success'|'danger'|'warning'|'info'|'light'|'dark'|'white'|'transparent')} variant - The variant of the popover
* @property {string} popoverClass - Classes to be added to the popover's container
* @property {string} container - Appends the popover to a specific element
* @property {string=('auto'|'top'|'bottom'|'left'|'right')} placement - How to position the popover
*/
static defaultOptions = {
title: '',
titleHtml: false,
body: false,
bodyHtml: false,
content: null,
html: false,
popoverClass: '',
container: false,
placement: 'right',
}
/** Create the HTML of the modal and inject it into the DOM */
makePopover() {
if (this.isValid()) {
if (this.options.titleHtml || this.options.bodyHtml) {
this.options.html = true
}
this.options.title = this.options.titleHtml ? this.options.titleHtml : sanitize(this.options.title)
if (this.options.content === null) {
this.options.content = this.options.bodyHtml ? this.options.bodyHtml : sanitize(this.options.body)
}
this.popoverInstance = new bootstrap.Popover(this.element, this.options)
} else {
console.error('Popover not valid')
}
}
/** Display the popover */
show() {
this.popoverInstance.show()
}
/** Hide the popover */
hide() {
this.popoverInstance.hide()
}
/** Updates the position of an elements popover. */
updatePosition() {
this.popoverInstance.update()
}
/** Hides and destroys an elements popover */
dispose() {
this.popoverInstance.dispose();
}
/**
* Check wheter a popover is valid
* @return {boolean} Return true if the popover contains at least data to be rendered
*/
isValid() {
return this.options.title !== false || this.options.titleHtml !== false ||
this.options.body !== false || this.options.bodyHtml !== false ||
this.options.rawHtml !== false
}
}
class HtmlHelper {
static table(head=[], body=[], options={}) {
const $table = $('<table/>')
@ -1100,12 +1306,16 @@ class HtmlHelper {
if (options.variant) {
$table.addClass(`table-${options.variant}`)
}
if (options.fixed_layout) {
$table.css('table-layout', 'fixed')
}
if (options.tableClass) {
$table.addClass(options.tableClass)
}
const $caption = $('<caption/>')
let $caption = null
if (options.caption) {
$caption = $('<caption/>')
if (options.caption instanceof jQuery) {
$caption = options.caption
} else {
@ -1113,21 +1323,28 @@ class HtmlHelper {
}
}
const $theadRow = $('<tr/>')
head.forEach(head => {
if (head instanceof jQuery) {
$theadRow.append($('<td/>').append(head))
} else {
$theadRow.append($('<th/>').text(head))
}
})
$thead.append($theadRow)
let $theadRow = null
if (head) {
$theadRow = $('<tr/>')
head.forEach(head => {
if (head instanceof jQuery) {
$theadRow.append($('<td/>').append(head))
} else {
$theadRow.append($('<th/>').text(head))
}
})
$thead.append($theadRow)
}
body.forEach(row => {
const $bodyRow = $('<tr/>')
row.forEach(item => {
if (item instanceof jQuery) {
$bodyRow.append($('<td/>').append(item))
if (item.is('td')) {
$bodyRow.append(item)
} else {
$bodyRow.append($('<td/>').append(item))
}
} else {
$bodyRow.append($('<td/>').text(item))
}

View File

@ -1,5 +1,5 @@
function executePagination(randomValue, url) {
UI.reload(url, $(`#table-container-${randomValue}`), $(`#table-container-${randomValue} table.table`))
function executePagination(selector, url) {
UI.reload(url, $(selector), $(selector).find('table.table'))
}
function executeStateDependencyChecks(dependenceSourceSelector) {
@ -55,11 +55,8 @@ function attachTestConnectionResultHtml(result, $container) {
$testResultDiv.append(getKVHtml('Internal error', result, ['text-danger fw-bold']))
} else {
if (result['error']) {
if (result['ping']) {
$testResultDiv.append('Status', 'OK', ['text-danger'], `${result['ping']} ms`);
}
$testResultDiv.append(
getKVHtml('Status', `Error: ${result['error']}`, ['text-danger']),
getKVHtml('Status', `Error: ${result['error']}`, ['text-danger'], (result['ping'] ? `${result['ping']} ms` : '')),
getKVHtml('Reason', result['reason'], ['text-danger'])
)
} else {
@ -209,6 +206,24 @@ function deleteBookmark(bookmark, forSidebar=false) {
}).catch((e) => { })
}
function downloadIndexTable(downloadButton, filename) {
const $dropdownMenu = $(downloadButton).closest('.dropdown')
const tableRandomValue = $dropdownMenu.attr('data-table-random-value')
const $container = $dropdownMenu.closest('div[id^="table-container-"]')
const $table = $container.find(`table[data-table-random-value="${tableRandomValue}"]`)
const $filterButton = $(`#toggleFilterButton-${tableRandomValue}`)
const activeFilters = $filterButton.data('activeFilters')
const additionalUrlParams = $filterButton.data('additionalUrlParams') ? $filterButton.data('additionalUrlParams') : ''
const searchParam = jQuery.param(activeFilters);
const url = $table.data('reload-url') + additionalUrlParams + '?' + searchParam
let options = {}
const downloadPromise = AJAXApi.quickFetchJSON(url, options)
UI.overlayUntilResolve($dropdownMenu, downloadPromise)
downloadPromise.then((data) => {
download(filename, JSON.stringify(data, undefined, 4))
})
}
function overloadBSDropdown() {
// Inspired from https://jsfiddle.net/dallaslu/mvk4uhzL/
(function ($bs) {

View File

@ -189,3 +189,21 @@ function mergeDeep(target, ...sources) {
return mergeDeep(target, ...sources);
}
function download(filename, data, type='application/json') {
const blob = new Blob([data], {type: type})
const a = window.document.createElement('a')
const objectURL = URL.createObjectURL(blob)
a.href = objectURL
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(objectURL)
}
function sanitize(unsafeText) {
const decoder = $('<div>')
decoder.text(unsafeText)
return decoder.html()
}