diff --git a/INSTALL/cerebrate_nginx.conf b/INSTALL/cerebrate_nginx.conf
index 59ad861..b8936f5 100644
--- a/INSTALL/cerebrate_nginx.conf
+++ b/INSTALL/cerebrate_nginx.conf
@@ -28,7 +28,7 @@ server {
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
- fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
+ fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
diff --git a/config/Migrations/20221130000000_MoreDataOnMetaFields.php b/config/Migrations/20221130000000_MoreDataOnMetaFields.php
new file mode 100644
index 0000000..69598ad
--- /dev/null
+++ b/config/Migrations/20221130000000_MoreDataOnMetaFields.php
@@ -0,0 +1,160 @@
+table('meta_fields');
+ if (!$metaFieldTable->hasColumn('meta_template_directory_id')) {
+ $metaFieldTable
+ ->addColumn('meta_template_directory_id', 'integer', [
+ 'default' => null,
+ 'null' => false,
+ 'signed' => false,
+ 'length' => 10
+ ])
+ ->addIndex('meta_template_directory_id')
+ ->update();
+ }
+
+ $exists = $this->hasTable('meta_template_name_directory');
+ if (!$exists) {
+ $templateNameDirectoryTable = $this->table('meta_template_name_directory', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci'
+ ]);
+ $templateNameDirectoryTable
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ ])
+ ->addColumn('namespace', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ ])
+ ->addColumn('version', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ ])
+ ->addColumn('uuid', 'uuid', [
+ 'null' => false,
+ 'default' => null,
+ ]);
+
+ $templateNameDirectoryTable
+ ->addIndex(['uuid', 'version'], ['unique' => true])
+ ->addIndex('name')
+ ->addIndex('namespace');
+
+ $templateNameDirectoryTable->create();
+
+ $allTemplates = $this->getAllTemplates();
+ $this->populateTemplateDirectoryTable($allTemplates);
+
+ $metaTemplateTable = $this->table('meta_templates');
+ $metaTemplateTable
+ ->addColumn('meta_template_directory_id', 'integer', [
+ 'default' => null,
+ 'null' => false,
+ 'signed' => false,
+ 'length' => 10
+ ])
+ ->update();
+ $this->assignTemplateDirectory($allTemplates);
+ $metaTemplateTable
+ ->addForeignKey('meta_template_directory_id', 'meta_template_name_directory', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
+ ->save();
+
+ $metaFieldTable
+ ->dropForeignKey('meta_template_id')
+ ->dropForeignKey('meta_template_field_id')
+ ->addForeignKey('meta_template_directory_id', 'meta_template_name_directory', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
+ ->save();
+ }
+ }
+
+ private function populateTemplateDirectoryTable(array $allTemplates): void
+ {
+ $builder = $this->getQueryBuilder()
+ ->insert(['uuid', 'name', 'namespace', 'version'])
+ ->into('meta_template_name_directory');
+
+ if (!empty($allTemplates)) {
+ foreach ($allTemplates as $template) {
+ $builder->values([
+ 'uuid' => $template['uuid'],
+ 'name' => $template['name'],
+ 'namespace' => $template['namespace'],
+ 'version' => $template['version'],
+ ]);
+ }
+ $builder->execute();
+ }
+ }
+
+ private function assignTemplateDirectory(array $allTemplates): void
+ {
+ foreach ($allTemplates as $template) {
+ $directory_template = $this->getDirectoryTemplate($template['uuid'], $template['version'])[0];
+ $this->getQueryBuilder()
+ ->update('meta_templates')
+ ->set('meta_template_directory_id', $directory_template['id'])
+ ->where(['meta_template_id' => $template['id']])
+ ->execute();
+ $this->getQueryBuilder()
+ ->update('meta_fields')
+ ->set('meta_template_directory_id', $directory_template['id'])
+ ->where(['id' => $template['id']])
+ ->execute();
+ }
+ }
+
+ private function getAllTemplates(): array
+ {
+ return $this->getQueryBuilder()
+ ->select(['id', 'uuid', 'name', 'namespace', 'version'])
+ ->from('meta_templates')
+ ->execute()->fetchAll('assoc');
+ }
+
+ private function getDirectoryTemplate(string $uuid, string $version): array
+ {
+ return $this->getQueryBuilder()
+ ->select(['id', 'uuid', 'version'])
+ ->from('meta_template_name_directory')
+ ->where([
+ 'uuid' => $uuid,
+ 'version' => $version,
+ ])
+ ->execute()->fetchAll('assoc');
+ }
+}
diff --git a/libraries/default/InboxProcessors/templates/LocalTool/GenericRequest.php b/libraries/default/InboxProcessors/templates/LocalTool/GenericRequest.php
index 15ea91b..6709c7e 100644
--- a/libraries/default/InboxProcessors/templates/LocalTool/GenericRequest.php
+++ b/libraries/default/InboxProcessors/templates/LocalTool/GenericRequest.php
@@ -50,28 +50,28 @@ $footerButtons[] = [
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
'fields' => [
- ['key' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
+ ['path' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
}],
- ['key' => 'connector', 'label' => __('Tool Name'), 'formatter' => function($connector, $row) {
+ ['path' => 'connector', 'label' => __('Tool Name'), 'formatter' => function($connector, $row) {
return sprintf('%s',
$this->Url->build(['controller' => 'localTools', 'action' => 'viewConnector', $connector['name']]),
sprintf('%s (v%s)', h($connector['name']), h($connector['connector_version']))
);
}],
- ['key' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
+ ['path' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
return sprintf('%s',
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
h($brood['name'])
);
}],
- ['key' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
+ ['path' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
return sprintf('%s',
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
h($individual['email'])
);
}],
- ['key' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
+ ['path' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
$html = '';
foreach ($alignments as $alignment) {
$html .= sprintf('
',
@@ -101,7 +101,7 @@ $localToolHTML = $this->fetch('content', sprintf('%s
Bootstrap->collapse(
[
- 'title' => __('Inter-connection data'),
+ 'text' => __('Inter-connection data'),
'open' => true,
],
sprintf('%s
', json_encode($request['data'], JSON_PRETTY_PRINT))
diff --git a/libraries/default/InboxProcessors/templates/Notification/DataChange.php b/libraries/default/InboxProcessors/templates/Notification/DataChange.php
index 952fae1..eb5679a 100644
--- a/libraries/default/InboxProcessors/templates/Notification/DataChange.php
+++ b/libraries/default/InboxProcessors/templates/Notification/DataChange.php
@@ -52,8 +52,10 @@ echo $this->Bootstrap->modal([
'bodyHTML' => $this->element('genericElements/SingleViews/Fields/jsonField', ['field' => ['raw' => $data['changed']]])
])
),
- 'confirmText' => __('Acknowledge & Discard'),
- 'confirmIcon' => 'check',
+ 'confirmButton' => [
+ 'text' => __('Acknowledge & Discard'),
+ 'icon' => 'check',
+ ]
]);
?>
diff --git a/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php b/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php
index b9339fb..2d84f1e 100644
--- a/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php
+++ b/libraries/default/OutboxProcessors/templates/Broods/ResendFailedMessage.php
@@ -40,22 +40,22 @@ $tools = sprintf(
$table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped' => false, 'hover' => false], [
'fields' => [
- ['key' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
+ ['path' => 'created', 'label' => __('Date'), 'formatter' => function($value, $row) {
return $value->i18nFormat('yyyy-MM-dd HH:mm:ss');
}],
- ['key' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
+ ['path' => 'brood', 'label' => __('Brood'), 'formatter' => function($brood, $row) {
return sprintf('%s',
$this->Url->build(['controller' => 'broods', 'action' => 'view', $brood['id']]),
h($brood['name'])
);
}],
- ['key' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
+ ['path' => 'individual', 'label' => __('Individual'), 'formatter' => function($individual, $row) {
return sprintf('%s',
$this->Url->build(['controller' => 'users', 'action' => 'view', $individual['id']]),
h($individual['email'])
);
}],
- ['key' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
+ ['path' => 'individual.alignments', 'label' => __('Alignment'), 'formatter' => function($alignments, $row) {
$html = '';
foreach ($alignments as $alignment) {
$html .= sprintf('',
@@ -71,7 +71,7 @@ $table = $this->Bootstrap->table(['small' => true, 'bordered' => false, 'striped
]);
$requestData = $this->Bootstrap->collapse([
- 'title' => __('Message data'),
+ 'text' => __('Message data'),
'open' => true,
],
sprintf('%s
', json_encode($request['data']['sent'], JSON_PRETTY_PRINT))
diff --git a/plugins/Tags/src/View/Helper/TagHelper.php b/plugins/Tags/src/View/Helper/TagHelper.php
index 71b48d7..405c513 100644
--- a/plugins/Tags/src/View/Helper/TagHelper.php
+++ b/plugins/Tags/src/View/Helper/TagHelper.php
@@ -60,9 +60,7 @@ class TagHelper extends Helper
'icon' => 'plus',
'variant' => 'secondary',
'class' => ['badge'],
- 'params' => [
- 'onclick' => 'createTagPicker(this)',
- ]
+ 'onclick' => 'createTagPicker(this)',
]);
} else {
$html .= '';
@@ -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',
diff --git a/src/Application.php b/src/Application.php
index 0bf89c9..aad0322 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -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;
}
diff --git a/src/Controller/BroodsController.php b/src/Controller/BroodsController.php
index 8a054a5..407a9d3 100644
--- a/src/Controller/BroodsController.php
+++ b/src/Controller/BroodsController.php
@@ -96,18 +96,26 @@ class BroodsController extends AppController
public function previewIndex($id, $scope)
{
- if (!in_array($scope, ['organisations', 'individuals', 'sharingGroups'])) {
- throw new MethodNotAllowedException(__('Invalid scope. Valid options are: organisations, individuals, sharing_groups'));
+ $validScopes = array_keys($this->Broods->previewScopes);
+ if (!in_array($scope, $validScopes)) {
+ throw new MethodNotAllowedException(__('Invalid scope. Valid options are: {0}', implode(', ', $validScopes)));
}
$filter = $this->request->getQuery('quickFilter');
- $data = $this->Broods->queryIndex($id, $scope, $filter);
+ $data = $this->Broods->queryIndex($id, $scope, $filter, true);
if (!is_array($data)) {
$data = [];
}
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($data, 'json');
} else {
+ $data = $this->Broods->attachAllSyncStatus($data, $scope);
$data = $this->CustomPagination->paginate($data);
+ $optionFilters = ['quickFilter'];
+ $CRUDParams = $this->ParamHandler->harvestParams($optionFilters);
+ $CRUDOptions = [
+ 'quickFilters' => $this->Broods->previewScopes[$scope]['quickFilterFields'],
+ ];
+ $this->CRUD->setQuickFilterForView($CRUDParams, $CRUDOptions);
$this->set('data', $data);
$this->set('brood_id', $id);
if ($this->request->is('ajax')) {
@@ -120,23 +128,43 @@ class BroodsController extends AppController
public function downloadOrg($brood_id, $org_id)
{
- $result = $this->Broods->downloadOrg($brood_id, $org_id);
- $success = __('Organisation fetched from remote.');
- $fail = __('Could not save the remote organisation');
- if ($this->ParamHandler->isRest()) {
- if ($result) {
- return $this->RestResponse->saveSuccessResponse('Brood', 'downloadOrg', $brood_id, 'json', $success);
+ if ($this->request->is('post')) {
+ $result = $this->Broods->downloadOrg($brood_id, $org_id);
+ $success = __('Organisation fetched from remote.');
+ $fail = __('Could not save the remote organisation');
+ if ($this->ParamHandler->isRest()) {
+ if ($result) {
+ return $this->RestResponse->saveSuccessResponse('Brood', 'downloadOrg', $brood_id, 'json', $success);
+ } else {
+ return $this->RestResponse->saveFailResponse('Brood', 'downloadOrg', $brood_id, $fail, 'json');
+ }
} else {
- return $this->RestResponse->saveFailResponse('Brood', 'downloadOrg', $brood_id, $fail, 'json');
+ if ($result) {
+ $this->Flash->success($success);
+ } else {
+ $this->Flash->error($fail);
+ }
+ $this->redirect($this->referer());
}
- } else {
- if ($result) {
- $this->Flash->success($success);
- } else {
- $this->Flash->error($fail);
- }
- $this->redirect($this->referer());
}
+ if ($org_id === 'all') {
+ $question = __('All organisations from brood `{0}` will be downloaded. Continue?', h($brood_id));
+ $title = __('Download all organisations from this brood');
+ $actionName = __('Download all');
+ } else {
+ $question = __('The organisations `{0}` from brood `{1}` will be downloaded. Continue?', h($org_id), h($brood_id));
+ $title = __('Download organisation from this brood');
+ $actionName = __('Download organisation');
+ }
+ $this->set('title', $title);
+ $this->set('question', $question);
+ $this->set('modalOptions', [
+ 'confirmButton' => [
+ 'variant' => $org_id === 'all' ? 'warning' : 'primary',
+ 'text' => $actionName,
+ ],
+ ]);
+ $this->render('/genericTemplates/confirm');
}
public function downloadIndividual($brood_id, $individual_id)
diff --git a/src/Controller/Component/APIRearrangeComponent.php b/src/Controller/Component/APIRearrangeComponent.php
index 561b271..0de9a3e 100644
--- a/src/Controller/Component/APIRearrangeComponent.php
+++ b/src/Controller/Component/APIRearrangeComponent.php
@@ -16,17 +16,17 @@ use Cake\Collection\Collection;
class APIRearrangeComponent extends Component
{
- public function rearrangeForAPI(object $data)
+ public static function rearrangeForAPI(object $data, array $options = [])
{
if (is_subclass_of($data, 'Iterator')) {
$newData = [];
- $data->each(function ($value, $key) use (&$newData) {
- $value->rearrangeForAPI();
+ $data->each(function ($value, $key) use (&$newData, $options) {
+ $value->rearrangeForAPI($options);
$newData[] = $value;
});
return new Collection($newData);
} else {
- $data->rearrangeForAPI();
+ $data->rearrangeForAPI($options);
}
return $data;
}
diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php
index fa98a42..1b9fe38 100644
--- a/src/Controller/Component/CRUDComponent.php
+++ b/src/Controller/Component/CRUDComponent.php
@@ -435,6 +435,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(),
]);
@@ -465,6 +466,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(),
]);
@@ -1122,17 +1124,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'])) {
@@ -1142,10 +1136,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
diff --git a/src/Controller/Component/RestResponseComponent.php b/src/Controller/Component/RestResponseComponent.php
index db5072b..8da9128 100644
--- a/src/Controller/Component/RestResponseComponent.php
+++ b/src/Controller/Component/RestResponseComponent.php
@@ -283,6 +283,7 @@ class RestResponseComponent extends Component
private $__scopedFieldsConstraint = array();
public function initialize(array $config): void {
+ parent::initialize($config);
$this->__configureFieldConstraints();
$this->Controller = $this->getController();
}
@@ -559,7 +560,14 @@ class RestResponseComponent extends Component
$data['errors'] = $errors;
}
if (!$raw && is_object($data)) {
- $data = $this->APIRearrange->rearrangeForAPI($data);
+ $rearrangeOptions = [];
+ if (!empty($this->Controller->getRequest()->getQuery('includeMetatemplate', false))) {
+ $rearrangeOptions['includeMetatemplate'] = true;
+ }
+ if (!empty($this->Controller->getRequest()->getQuery('includeFullMetaFields', false))) {
+ $rearrangeOptions['includeFullMetaFields'] = true;
+ }
+ $data = $this->APIRearrange->rearrangeForAPI($data, $rearrangeOptions);
}
return $this->__sendResponse($data, 200, $format, $raw, $download, $headers);
}
diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php
index 93ddfb9..6d2720f 100644
--- a/src/Controller/OrganisationsController.php
+++ b/src/Controller/OrganisationsController.php
@@ -65,6 +65,11 @@ class OrganisationsController extends AppController
]
];
}
+ $additionalContainFields = [];
+ if ($this->ParamHandler->isRest() && !empty($this->request->getQuery('full'))) {
+ $additionalContainFields[] = 'MetaFields';
+ }
+ $containFields = array_merge($this->containFields, $additionalContainFields);
$this->CRUD->index([
'filters' => $this->filterFields,
@@ -73,7 +78,7 @@ class OrganisationsController extends AppController
'contextFilters' => [
'custom' => $customContextFilters,
],
- 'contain' => $this->containFields,
+ 'contain' => $containFields,
'statisticsFields' => $this->statisticsFields,
]);
$responsePayload = $this->CRUD->getResponsePayload();
diff --git a/src/Model/Entity/AppModel.php b/src/Model/Entity/AppModel.php
index 3e70518..2d4a553 100644
--- a/src/Model/Entity/AppModel.php
+++ b/src/Model/Entity/AppModel.php
@@ -40,28 +40,47 @@ class AppModel extends Entity
return TableRegistry::get($this->getSource());
}
- public function rearrangeForAPI(): void
+ public function rearrangeForAPI(array $options = []): void
{
}
- public function rearrangeMetaFields(): void
+ public function rearrangeMetaFields(array $options = []): void
{
- $this->meta_fields = [];
- foreach ($this->MetaTemplates as $template) {
- foreach ($template['meta_template_fields'] as $field) {
- if ($field['counter'] > 0) {
- foreach ($field['metaFields'] as $metaField) {
- if (!empty($this->meta_fields[$template['name']][$field['field']])) {
- if (!is_array($this->meta_fields[$template['name']][$field['field']])) {
- $this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]];
+ if (!empty($options['includeFullMetaFields'])) {
+ $this->meta_fields = [];
+ foreach ($this->MetaTemplates as $template) {
+ foreach ($template['meta_template_fields'] as $field) {
+ if ($field['counter'] > 0) {
+ foreach ($field['metaFields'] as $metaField) {
+ if (!empty($this->meta_fields[$template['name']][$field['field']])) {
+ if (!is_array($this->meta_fields[$template['name']][$field['field']])) {
+ $this->meta_fields[$template['name']][$field['field']] = [$this->meta_fields[$template['name']][$field['field']]];
+ }
+ $this->meta_fields[$template['name']][$field['field']][] = $metaField['value'];
+ } else {
+ $this->meta_fields[$template['name']][$field['field']] = $metaField['value'];
}
- $this->meta_fields[$template['name']][$field['field']][] = $metaField['value'];
- } else {
- $this->meta_fields[$template['name']][$field['field']] = $metaField['value'];
}
}
}
}
+ } elseif (!empty($this->meta_fields)) {
+ $templateDirectoryTable = TableRegistry::get('MetaTemplateNameDirectory');
+ $templates = [];
+ foreach ($this->meta_fields as $i => $metafield) {
+ $templateDirectoryId = $metafield['meta_template_directory_id'];
+ if (empty($templates[$templateDirectoryId])) {
+ $templates[$templateDirectoryId] = $templateDirectoryTable->find()->where(['id' => $templateDirectoryId])->first();
+ }
+ $this->meta_fields[$i]['template_uuid'] = $templates[$templateDirectoryId]['uuid'];
+ $this->meta_fields[$i]['template_version'] = $templates[$templateDirectoryId]['version'];
+ $this->meta_fields[$i]['template_name'] = $templates[$templateDirectoryId]['name'];
+ $this->meta_fields[$i]['template_namespace'] = $templates[$templateDirectoryId]['namespace'];
+ }
+ }
+ // if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate'])) && !empty($this->MetaTemplates)) {
+ if ((!isset($options['includeMetatemplate']) || empty($options['includeMetatemplate']))) {
+ unset($this->MetaTemplates);
}
}
diff --git a/src/Model/Entity/AuditLog.php b/src/Model/Entity/AuditLog.php
index 4f1a2fe..eaaac47 100644
--- a/src/Model/Entity/AuditLog.php
+++ b/src/Model/Entity/AuditLog.php
@@ -66,7 +66,7 @@ class AuditLog extends AppModel
return $title;
}
- public function rearrangeForAPI(): void
+ public function rearrangeForAPI(array $options = []): void
{
if (!empty($this->user)) {
$this->user = $this->user->toArray();
diff --git a/src/Model/Entity/EncryptionKey.php b/src/Model/Entity/EncryptionKey.php
index a9b2f67..e735e15 100644
--- a/src/Model/Entity/EncryptionKey.php
+++ b/src/Model/Entity/EncryptionKey.php
@@ -8,7 +8,7 @@ use Cake\ORM\Entity;
class EncryptionKey extends AppModel
{
- public function rearrangeForAPI(): void
+ public function rearrangeForAPI(array $options = []): void
{
$this->rearrangeSimplify(['organisation', 'individual']);
}
diff --git a/src/Model/Entity/Individual.php b/src/Model/Entity/Individual.php
index 1afd30c..14863b5 100644
--- a/src/Model/Entity/Individual.php
+++ b/src/Model/Entity/Individual.php
@@ -42,7 +42,7 @@ class Individual extends AppModel
return $emails;
}
- public function rearrangeForAPI(): void
+ public function rearrangeForAPI(array $options = []): void
{
if (!empty($this->tags)) {
$this->tags = $this->rearrangeTags($this->tags);
@@ -51,10 +51,7 @@ class Individual extends AppModel
$this->alignments = $this->rearrangeAlignments($this->alignments);
}
if (!empty($this->meta_fields)) {
- $this->rearrangeMetaFields();
- }
- if (!empty($this->MetaTemplates)) {
- unset($this->MetaTemplates);
+ $this->rearrangeMetaFields($options);
}
}
}
diff --git a/src/Model/Entity/MetaTemplateNameDirectory.php b/src/Model/Entity/MetaTemplateNameDirectory.php
new file mode 100644
index 0000000..84c9696
--- /dev/null
+++ b/src/Model/Entity/MetaTemplateNameDirectory.php
@@ -0,0 +1,11 @@
+ true
];
- public function rearrangeForAPI(): void
+ public function rearrangeForAPI(array $options = []): void
{
if (!empty($this->tags)) {
$this->tags = $this->rearrangeTags($this->tags);
@@ -25,11 +25,9 @@ class Organisation extends AppModel
if (!empty($this->alignments)) {
$this->alignments = $this->rearrangeAlignments($this->alignments);
}
- if (!empty($this->meta_fields)) {
- $this->rearrangeMetaFields();
- }
- if (!empty($this->MetaTemplates)) {
- unset($this->MetaTemplates);
+ if (!empty($this->meta_fields) || !empty($this->MetaTemplates)) {
+ $this->rearrangeMetaFields($options);
}
}
+ // MetaTemplate object property is not unset!!
}
diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php
index 5f39720..31437d8 100644
--- a/src/Model/Entity/User.php
+++ b/src/Model/Entity/User.php
@@ -49,16 +49,13 @@ class User extends AppModel
}
}
- public function rearrangeForAPI(): void
+ public function rearrangeForAPI(array $options = []): void
{
if (!empty($this->tags)) {
$this->tags = $this->rearrangeTags($this->tags);
}
if (!empty($this->meta_fields)) {
- $this->rearrangeMetaFields();
- }
- if (!empty($this->MetaTemplates)) {
- unset($this->MetaTemplates);
+ $this->rearrangeMetaFields($options);
}
if (!empty($this->user_settings_by_name)) {
$this->rearrangeUserSettings();
diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php
index 0708c18..ad7c703 100644
--- a/src/Model/Table/BroodsTable.php
+++ b/src/Model/Table/BroodsTable.php
@@ -2,10 +2,15 @@
namespace App\Model\Table;
+require_once APP . DS . 'Utility/Utils.php';
use App\Model\Table\AppTable;
+use function App\Utility\Utils\array_diff_recursive;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Core\Configure;
+use Cake\Utility\Inflector;
+use Cake\Utility\Hash;
+use Cake\I18n\FrozenTime;
use Cake\Http\Client;
use Cake\Http\Client\Response;
use Cake\Http\Exception\NotFoundException;
@@ -15,6 +20,27 @@ use Cake\Error\Debugger;
class BroodsTable extends AppTable
{
+
+ public $previewScopes = [
+ 'organisations' => [
+ 'quickFilterFields' => ['uuid', ['name' => true],],
+ 'contain' => ['MetaFields' => ['MetaTemplateNameDirectory'], 'Tags'],
+ 'compareFields' => ['name', 'url', 'nationality', 'sector', 'type', 'contacts', 'modified', 'tags', 'meta_fields',],
+ ],
+ 'individuals' => [
+ 'quickFilterFields' => ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true],],
+ 'contain' => ['MetaFields'],
+ 'compareFields' => ['email', 'first_name', 'last_name', 'position', 'modified', 'meta_fields', 'tags',],
+ ],
+ 'sharingGroups' => [
+ 'quickFilterFields' => ['uuid', ['name' => true],],
+ 'contain' => ['SharingGroupOrgs', 'Organisations'],
+ 'compareFields' => ['name', 'releasability', 'description', 'organisation_id', 'user_id', 'active', 'local', 'modified', 'organisation', 'sharing_group_orgs',],
+ ],
+ ];
+
+ private $metaFieldCompareFields = ['modified', 'value'];
+
public function initialize(array $config): void
{
parent::initialize($config);
@@ -119,13 +145,16 @@ class BroodsTable extends AppTable
return $result;
}
- public function queryIndex($id, $scope, $filter)
+ public function queryIndex($id, $scope, $filter, $full = false)
{
$brood = $this->find()->where(['id' => $id])->first();
if (empty($brood)) {
throw new NotFoundException(__('Brood not found'));
}
$filterQuery = empty($filter) ? '' : '?quickFilter=' . urlencode($filter);
+ if (!empty($full)) {
+ $filterQuery .= (empty($filterQuery) ? '?' : '&') . 'full=1';
+ }
$response = $this->HTTPClientGET(sprintf('/%s/index.json%s', $scope, $filterQuery), $brood);
if ($response->isOk()) {
return $response->getJson();
@@ -371,4 +400,124 @@ class BroodsTable extends AppTable
$connector = $params['connector'][$params['remote_tool']['connector']];
$connector->remoteToolConnectionStatus($params, constant(get_class($connector) . '::' . $status));
}
+
+ public function attachAllSyncStatus(array $data, string $scope): array
+ {
+ $options = $this->previewScopes[$scope];
+ foreach ($data as $i => $entry) {
+ $data[$i] = $this->__attachSyncStatus($scope, $entry, $options);
+ }
+ return $data;
+ }
+
+ private function __attachSyncStatus(string $scope, array $entry, array $options = []): array
+ {
+ $table = TableRegistry::getTableLocator()->get(Inflector::camelize($scope));
+ $localEntry = $table
+ ->find()
+ ->where(['uuid' => $entry['uuid']])
+ ->first();
+ if (is_null($localEntry)) {
+ $entry['status'] = $this->__statusNotLocal();
+ } else {
+ if (!empty($options['contain'])) {
+ $localEntry = $table->loadInto($localEntry, $options['contain']);
+ }
+ $localEntry = json_decode(json_encode($localEntry), true);
+ $entry['status'] = $this->__statusLocal($entry, $localEntry, $options);
+ }
+
+ return $entry;
+ }
+
+ private function __statusNotLocal(): array
+ {
+ return self::__getStatus(false);
+ }
+
+ private function __statusLocal(array $remoteEntry, $localEntry, array $options = []): array
+ {
+ $isLocalNewer = (new FrozenTime($localEntry['modified']))->toUnixString() >= (new FrozenTime($remoteEntry['modified']))->toUnixString();
+ $compareFields = $options['compareFields'];
+ $fieldDifference = [];
+ $fieldDifference = array_diff_recursive($remoteEntry, $localEntry);
+ // if (in_array('meta_fields', $options['compareFields']) && !empty($fieldDifference['meta_fields'])) {
+ // $fieldDifference['meta_fields'] = $this->_compareMetaFields($remoteEntry, $localEntry, $options);
+ // }
+ $fieldDifference = array_filter($fieldDifference, function($value, $field) use ($compareFields) {
+ return in_array($field, $compareFields);
+ }, ARRAY_FILTER_USE_BOTH);
+ foreach ($fieldDifference as $fieldName => $value) {
+ $fieldDifference[$fieldName] = [
+ 'local' => $localEntry[$fieldName],
+ 'remote' => $value,
+ ];
+ }
+ if (in_array('meta_fields', $options['compareFields']) && !empty($fieldDifference['meta_fields'])) {
+ $fieldDifference['meta_fields'] = $this->_compareMetaFields($remoteEntry, $localEntry, $options);
+ }
+
+ return self::__getStatus(true, $isLocalNewer, $fieldDifference);
+ }
+
+ private static function __getStatus($local=true, $updateToDate=false, array $data = []): array
+ {
+ $status = [
+ 'local' => $local,
+ 'up_to_date' => $updateToDate,
+ 'data' => $data,
+ ];
+ if ($status['local'] && $status['up_to_date']) {
+ $status['title'] = __('This entity is up-to-date');
+ } else if ($status['local'] && !$status['up_to_date']) {
+ $status['title'] = __('This entity is known but differs with the remote');
+ } else {
+ $status['title'] = __('This entity is not known locally');
+ }
+ return $status;
+ }
+
+ private function _compareMetaFields($remoteEntry, $localEntry): array
+ {
+ $compareFields = $this->metaFieldCompareFields;
+ $indexedRemoteMF = [];
+ $indexedLocalMF = [];
+ foreach ($remoteEntry['meta_fields'] as $metafields) {
+ $indexedRemoteMF[$metafields['uuid']] = array_intersect_key($metafields, array_flip($compareFields));
+ }
+ foreach ($localEntry['meta_fields'] as $metafields) {
+ $indexedLocalMF[$metafields['uuid']] = array_intersect_key($metafields, array_flip($compareFields));
+ }
+ $fieldDifference = [];
+ foreach ($remoteEntry['meta_fields'] as $remoteMetafield) {
+ $uuid = $remoteMetafield['uuid'];
+ $metafieldName = $remoteMetafield['field'];
+ // $metafieldName = sprintf('%s(v%s) :: %s', $remoteMetafield['template_name'], $remoteMetafield['template_version'], $remoteMetafield['field']);
+ if (empty($fieldDifference[$metafieldName])) {
+ $fieldDifference[$metafieldName] = [
+ 'meta_template' => [
+ 'name' => $remoteMetafield['template_name'],
+ 'version' => $remoteMetafield['template_version'],
+ 'uuid' => $remoteMetafield['template_uuid']
+ ],
+ 'delta' => [],
+ ];
+ }
+ if (empty($indexedLocalMF[$uuid])) {
+ $fieldDifference[$metafieldName]['delta'][] = [
+ 'local' => null,
+ 'remote' => $indexedRemoteMF[$uuid],
+ ];
+ } else {
+ $fieldDifferenceTmp = array_diff_recursive($indexedRemoteMF[$uuid], $indexedLocalMF[$uuid]);
+ if (!empty($fieldDifferenceTmp)) {
+ $fieldDifference[$metafieldName]['delta'][] = [
+ 'local' => $indexedLocalMF[$uuid],
+ 'remote' => $indexedRemoteMF[$uuid],
+ ];
+ }
+ }
+ }
+ return $fieldDifference;
+ }
}
diff --git a/src/Model/Table/MetaFieldsTable.php b/src/Model/Table/MetaFieldsTable.php
index 8c5a6d0..1e55501 100644
--- a/src/Model/Table/MetaFieldsTable.php
+++ b/src/Model/Table/MetaFieldsTable.php
@@ -5,7 +5,9 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
+use Cake\Event\EventInterface;
use Cake\ORM\RulesChecker;
+use ArrayObject;
class MetaFieldsTable extends AppTable
{
@@ -22,6 +24,8 @@ class MetaFieldsTable extends AppTable
$this->addBehavior('Timestamp');
$this->belongsTo('MetaTemplates');
$this->belongsTo('MetaTemplateFields');
+ $this->belongsTo('MetaTemplateNameDirectory')
+ ->setForeignKey('meta_template_directory_id');
$this->setDisplayField('field');
}
@@ -35,7 +39,10 @@ class MetaFieldsTable extends AppTable
->notEmptyString('value')
->notEmptyString('meta_template_id')
->notEmptyString('meta_template_field_id')
- ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create');
+ // ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create');
+ // ->requirePresence(['scope', 'field', 'value', 'uuid',], 'create');
+ ->notEmptyString('meta_template_directory_id')
+ ->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_directory_id', ], 'create');
$validator->add('value', 'validMetaField', [
'rule' => 'isValidMetaField',
@@ -46,10 +53,28 @@ class MetaFieldsTable extends AppTable
return $validator;
}
+ public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
+ {
+ if (!isset($data['meta_template_directory_id'])) {
+ $data['meta_template_directory_id'] = $this->getTemplateDirectoryIdFromMetaTemplate($data['meta_template_id']);
+ }
+ }
+
+ public function getTemplateDirectoryIdFromMetaTemplate($metaTemplateId): int
+ {
+ return $this->MetaTemplates->find()
+ ->select('meta_template_directory_id')
+ ->where(['id' => $metaTemplateId])
+ ->first();
+ }
+
public function isValidMetaField($value, array $context)
{
$metaFieldsTable = $context['providers']['table'];
$entityData = $context['data'];
+ if (empty($entityData['meta_template_field_id'])) {
+ return true;
+ }
$metaTemplateField = $metaFieldsTable->MetaTemplateFields->get($entityData['meta_template_field_id']);
return $this->isValidMetaFieldForMetaTemplateField($value, $metaTemplateField);
}
diff --git a/src/Model/Table/MetaTemplateNameDirectoryTable.php b/src/Model/Table/MetaTemplateNameDirectoryTable.php
new file mode 100644
index 0000000..f1a3dfe
--- /dev/null
+++ b/src/Model/Table/MetaTemplateNameDirectoryTable.php
@@ -0,0 +1,47 @@
+hasMany(
+ 'MetaFields',
+ [
+ 'foreignKey' => 'meta_template_directory_id',
+ ]
+ );
+ $this->setDisplayField('name');
+ }
+
+ public function validationDefault(Validator $validator): Validator
+ {
+ $validator
+ ->notEmptyString('name')
+ ->notEmptyString('namespace')
+ ->notEmptyString('uuid')
+ ->notEmptyString('version')
+ ->requirePresence(['version', 'uuid', 'name', 'namespace'], 'create');
+ return $validator;
+ }
+
+ public function createFromMetaTemplate(MetaTemplate $metaTemplate): MetaTemplateNameDirectory
+ {
+ $metaTemplateDirectory = $this->newEntity([
+ 'name' => $metaTemplate['name'],
+ 'namespace' => $metaTemplate['namespace'],
+ 'uuid' => $metaTemplate['uuid'],
+ 'version' => $metaTemplate['version'],
+ ]);
+ $this->save($metaTemplateDirectory);
+ return $metaTemplateDirectory;
+ }
+}
diff --git a/src/Model/Table/MetaTemplatesTable.php b/src/Model/Table/MetaTemplatesTable.php
index 77df33d..76d82e9 100644
--- a/src/Model/Table/MetaTemplatesTable.php
+++ b/src/Model/Table/MetaTemplatesTable.php
@@ -42,6 +42,9 @@ class MetaTemplatesTable extends AppTable
'cascadeCallbacks' => true,
]
);
+ $this->hasOne('MetaTemplateNameDirectory')
+ ->setForeignKey('meta_template_directory_id');
+
$this->setDisplayField('name');
}
@@ -54,7 +57,7 @@ class MetaTemplatesTable extends AppTable
->notEmptyString('uuid')
->notEmptyString('version')
->notEmptyString('source')
- ->requirePresence(['scope', 'source', 'version', 'uuid', 'name', 'namespace'], 'create');
+ ->requirePresence(['scope', 'source', 'version', 'uuid', 'name', 'namespace', 'meta_template_directory_id'], 'create');
return $validator;
}
@@ -731,6 +734,8 @@ class MetaTemplatesTable extends AppTable
$metaTemplate = $this->newEntity($template, [
'associated' => ['MetaTemplateFields']
]);
+ $metaTemplateDirectory = $this->MetaTemplateNameDirectory->createFromMetaTemplate($metaTemplate);
+ $metaTemplate->meta_template_directory_id = $metaTemplateDirectory->id;
$tmp = $this->save($metaTemplate, [
'associated' => ['MetaTemplateFields']
]);
diff --git a/src/Model/Table/PermissionLimitationsTable.php b/src/Model/Table/PermissionLimitationsTable.php
index 6300823..e40cf96 100644
--- a/src/Model/Table/PermissionLimitationsTable.php
+++ b/src/Model/Table/PermissionLimitationsTable.php
@@ -91,10 +91,6 @@ class PermissionLimitationsTable extends AppTable
foreach ($metaTemplate['meta_template_fields'] as &$meta_template_field) {
$boolean = $meta_template_field['type'] === 'boolean';
foreach ($meta_template_field['metaFields'] as &$metaField) {
- if ($boolean) {
- $metaField['value'] = '';
- $metaField['no_escaping'] = true;
- }
if (isset($permissionLimitations[$metaField['field']])) {
foreach ($permissionLimitations[$metaField['field']] as $scope => $value) {
$messageType = 'warning';
diff --git a/src/Utility/Utils.php b/src/Utility/Utils.php
new file mode 100644
index 0000000..9f63c24
--- /dev/null
+++ b/src/Utility/Utils.php
@@ -0,0 +1,32 @@
+ $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;
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapAccordion.php b/src/View/Helper/BootstrapElements/BootstrapAccordion.php
new file mode 100644
index 0000000..6f0688c
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapAccordion.php
@@ -0,0 +1,155 @@
+Bootstrap->accordion(
+ * [
+ * 'stayOpen' => true,
+ * ],
+ * [
+ * [
+ * 'open' => true,
+ * 'header' => [
+ * 'variant' => 'danger',
+ * 'text' => 'nav 1',
+ * ],
+ * 'body' => 'body',
+ * ],
+ * [
+ * 'class' => ['opacity-50'],
+ * 'variant' => 'success',
+ * 'header' => [
+ * 'html' => 'nav 1',
+ * ],
+ * 'body' => 'body',
+ * ],
+ * ]
+ * );
+ */
+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;
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapAlert.php b/src/View/Helper/BootstrapElements/BootstrapAlert.php
new file mode 100644
index 0000000..8bff3c1
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapAlert.php
@@ -0,0 +1,83 @@
+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;
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapBadge.php b/src/View/Helper/BootstrapElements/BootstrapBadge.php
new file mode 100644
index 0000000..3fe745b
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapBadge.php
@@ -0,0 +1,88 @@
+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 '';
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapButton.php b/src/View/Helper/BootstrapElements/BootstrapButton.php
new file mode 100644
index 0000000..458680c
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapButton.php
@@ -0,0 +1,143 @@
+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 '';
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapCard.php b/src/View/Helper/BootstrapElements/BootstrapCard.php
new file mode 100644
index 0000000..47ecd68
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapCard.php
@@ -0,0 +1,135 @@
+Bootstrap->card([
+ * 'headerText' => 'header',
+ * 'bodyHTML' => 'body',
+ * '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;
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapCollapse.php b/src/View/Helper/BootstrapElements/BootstrapCollapse.php
new file mode 100644
index 0000000..8d90f79
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapCollapse.php
@@ -0,0 +1,121 @@
+Bootstrap->collapse([
+ * 'button' => [
+ * 'text' => 'Open sesame',
+ * 'variant' => 'success',
+ * ],
+ * 'card' => [
+ * 'bodyClass' => 'p-2 rounded-3',
+ * 'bodyVariant' => 'secondary',
+ * ]
+ * ], 'content');
+ */
+
+class BootstrapCollapse extends BootstrapGeneric
+{
+ private $defaultOptions = [
+ 'text' => '',
+ 'html' => null,
+ 'open' => false,
+ 'horizontal' => false,
+ 'class' => [],
+ 'button' => [],
+ 'card' => [],
+ '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 (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
+ {
+ $cardConfig = $this->options['card'];
+ $cardConfig['bodyHTML'] = $this->content;
+ $content = $this->btHelper->card($cardConfig);
+ $container = $this->node('div', [
+ 'class' => $this->options['class'],
+ 'id' => $this->options['id'],
+ ], $content);
+ return $container;
+ }
+
+ private function genCollapse(): string
+ {
+ return $this->genControl() . $this->genContent();
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapDropdownMenu.php b/src/View/Helper/BootstrapElements/BootstrapDropdownMenu.php
new file mode 100644
index 0000000..e3a64c6
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapDropdownMenu.php
@@ -0,0 +1,206 @@
+ element
+ *
+ * # Usage:
+ * $this->Bootstrap->dropdownMenu([
+ * 'dropdown-class' => 'ms-1',
+ * 'alignment' => 'end',
+ * 'direction' => 'down',
+ * 'button' => [
+ * 'icon' => 'sliders-h',
+ * 'variant' => 'primary',
+ * ],
+ * 'submenu_alignment' => 'end',
+ * 'submenu_direction' => 'end',
+ * 'attrs' => [],
+ * 'menu' => [
+ * [
+ * 'text' => __('Eye'),
+ * 'icon' => 'eye-slash',
+ * 'keepOpen' => true,
+ * 'menu' => [
+ * ['header' => true, 'text' => 'nested menu'],
+ * ['text' => 'item 1'],
+ * ['text' => 'item 2', 'sup' => 'v1'],
+ * ],
+ * ],
+ * [
+ * 'html' => 'html item',
+ * ],
+ * ]
+ * ]);
+ */
+
+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'];
+ $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);
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapIcon.php b/src/View/Helper/BootstrapElements/BootstrapIcon.php
new file mode 100644
index 0000000..71c0c8a
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapIcon.php
@@ -0,0 +1,61 @@
+Bootstrap->icon('eye-slash', [
+ * 'class' => 'm-3',
+ * ]);
+ */
+class BootstrapIcon extends BootstrapGeneric
+{
+ private $icon = '';
+ private $defaultOptions = [
+ '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(
+ [
+ 'class' => array_merge(
+ $this->options['class'],
+ ["fa fa-{$this->icon}"]
+ ),
+ 'title' => h($this->options['title'])
+ ],
+ $this->options['attrs']
+ ));
+ return $html;
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapListGroup.php b/src/View/Helper/BootstrapElements/BootstrapListGroup.php
new file mode 100644
index 0000000..5804a3f
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapListGroup.php
@@ -0,0 +1,119 @@
+Bootstrap->listGroup(
+ * [
+ * [
+ * 'text' => 'test',
+ * 'badge' => [
+ * 'text' => 'test',
+ * 'variant' => 'warning'
+ * ],
+ * 'attrs' => [
+ * 'data-test' => 'tes'
+ * ]
+ * ],
+ * [
+ * 'html' => 'test2',
+ * ],
+ * ],
+ * [
+ * '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);
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapListTable.php b/src/View/Helper/BootstrapElements/BootstrapListTable.php
new file mode 100644
index 0000000..a2ba806
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapListTable.php
@@ -0,0 +1,223 @@
+ or array
+ *
+ * # 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' => 'raw_value',
+ * 'rawNoEscaping' => true,
+ * ],
+ * [
+ * 'key' => 'Label 4',
+ * 'path' => 'key3',
+ * 'formatter' => function ($value) {
+ * return '' . $value . '';
+ * },
+ * ],
+ * ],
+ * '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(' %s', $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)) : '';
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapModal.php b/src/View/Helper/BootstrapElements/BootstrapModal.php
new file mode 100644
index 0000000..a7b60fb
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapModal.php
@@ -0,0 +1,343 @@
+Bootstrap->modal([
+ * 'title' => 'Modal title',
+ * 'size' => 'lg',
+ * 'type' => 'ok-only',
+ * 'body' => 'Body content',
+ * 'header-variant' => 'dark',
+ * 'body-variant' => 'light',
+ * 'footer-variant' => 'warning',
+ * 'show' => true,
+ * ]);
+
+ * ## Modal with custom onclick handler
+ * $this->Bootstrap->modal([
+ * 'type' => 'confirm',
+ * 'bodyHtml' => 'Body content',
+ * 'confirmButton' => [
+ * 'text' => 'Show modal',
+ * 'icon' => 'eye',
+ * 'onclick' => 'UI.toast({"title": "confirmed!"})',
+ * ],
+ * 'cancelOnclick' => 'UI.toast({"title": "cancelled"})',
+ * 'show' => true,
+ * ]);
+ *
+ * ## Modal with a onclick handler with prepared arguments bound to the confirm button
+ * $this->Bootstrap->modal([
+ * 'type' => 'confirm',
+ * 'confirmButton' => [
+ * 'text' => 'Confirm',
+ * 'icon' => 'check',
+ * ],
+ * 'confirmFunction' => 'myConfirmFunction', // myConfirmFunction is called with the $modalObject and $tmpApi intialized
+ * 'show' => true,
+ * ]);
+ *
+ * /*
+ * Example of confirm function
+ * - case 1: If void is returned the modal close automatically regardless of the result
+ * - case 2: If a promise is returned, the modal close automatically if the promise is a success
+ * A success is defined as follow:
+ * - No exceptions
+ * - No data returned
+ * - Object returned with key `success` evaluting to true
+ * - case 3: The modal can be closed manually with: `modalObject.hide()`
+ *
+ * function myConfirmFunction(modalObject, tmpApi) {
+ * const $form = modalObject.$modal.find('form')
+ * const postPromise = $form.length == 1 ?
+ * tmpApi.postForm($form[0]) :
+ * tmpApi.fetchJSON('/users/view/', false, true)
+ * .then((result) => {
+ * console.log(result)
+ * constToReturn = {
+ * success: true, // will close the modal automatically
+ * }
+ * return constToReturn
+ * })
+ * .catch((errors) => {
+ * console.log(errors)
+ * })
+ *
+ * return postPromise
+ * }
+
+ * ## Modal with custom footer made of buttons
+ * $this->Bootstrap->modal([
+ * 'type' => 'custom',
+ * 'footerButtons' => [
+ * [
+ * 'text' => 'Confirm',
+ * 'icon' => 'check',
+ * 'variant' => 'danger',
+ * 'clickFunction' => 'testapi',
+ * ],
+ * [
+ * 'text' => 'Cancel',
+ * 'onclick' => 'UI.toast({"title": "confirmed!"})',
+ * ],
+ * ],
+ * 'show' => true,
+ * ]);
+ */
+class BootstrapModal extends BootstrapGeneric
+{
+ private $defaultOptions = [
+ 'size' => '',
+ 'centered' => true,
+ 'scrollable' => true,
+ 'backdropStatic' => false,
+ 'show' => false,
+ 'header-variant' => '',
+ 'body-variant' => '',
+ 'footer-variant' => '',
+ 'title' => '',
+ 'titleHtml' => null,
+ 'body' => '',
+ 'bodyHtml' => null,
+ 'footerHtml' => null,
+ 'modalClass' => [''],
+ 'headerClass' => [''],
+ 'bodyClass' => [''],
+ 'footerClass' => [''],
+ 'confirmButton' => [
+ 'text' => 'Confirm',
+ ],
+ 'cancelButton' => [
+ 'text' => 'Cancel',
+ ],
+ 'type' => 'ok-only',
+ 'footerButtons' => [],
+ 'confirmFunction' => '', // Will be called with the following arguments confirmFunction(modalObject, tmpApi)
+ 'cancelOnclick' => ''
+ ];
+
+ function __construct(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']);
+
+ $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);
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapNotificationBubble.php b/src/View/Helper/BootstrapElements/BootstrapNotificationBubble.php
new file mode 100644
index 0000000..e0788ab
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapNotificationBubble.php
@@ -0,0 +1,106 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapProgress.php b/src/View/Helper/BootstrapElements/BootstrapProgress.php
new file mode 100644
index 0000000..27117b5
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapProgress.php
@@ -0,0 +1,88 @@
+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;
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapProgressTimeline.php b/src/View/Helper/BootstrapElements/BootstrapProgressTimeline.php
new file mode 100644
index 0000000..89c20d0
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapProgressTimeline.php
@@ -0,0 +1,153 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapSwitch.php b/src/View/Helper/BootstrapElements/BootstrapSwitch.php
new file mode 100644
index 0000000..b70ab6f
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapSwitch.php
@@ -0,0 +1,81 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/src/View/Helper/BootstrapElements/BootstrapTable.php b/src/View/Helper/BootstrapElements/BootstrapTable.php
new file mode 100644
index 0000000..2b9a8f3
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapTable.php
@@ -0,0 +1,243 @@
+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' => 'column 3',
+ * ],
+ * [
+ * 'path' => 'key1',
+ * 'label' => __('Field'),
+ * 'formatter' => function ($field, $row) {
+ * return sprintf('%s', 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, [], $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)) : '';
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapTabs.php b/src/View/Helper/BootstrapElements/BootstrapTabs.php
new file mode 100644
index 0000000..e0ac5af
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapTabs.php
@@ -0,0 +1,307 @@
+Bootstrap->tabs([
+ * 'horizontal-position' => 'top',
+ * 'header-variant' => 'danger',
+ * 'card' => true,
+ * 'data' => [
+ * 'navs' => [
+ * ['text' => 'nav 1'],
+ * ['html' => 'nav 2', 'active' => true],
+ * ],
+ * 'content' => [
+ * 'content 1',
+ * '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' => 'nav 2', 'disabled' => true],
+ * ],
+ * 'content' => [
+ * 'content 1',
+ * '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']}",
+ "text-{$this->options['header-text-variant']}",
+ "border-{$this->options['header-border-variant']}"
+ ]
+ )], $this->genNav());
+ $content = $this->node('div', ['class' => array_merge(
+ [
+ ($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''),
+ ($this->options['card'] ? 'card-body2' : '')
+ ],
+ [
+ "bg-{$this->options['body-variant']}",
+ "text-{$this->options['body-text-variant']}"
+ ]
+ )], $this->genContent());
+
+ $containerContent = $this->options['vertical-position'] === 'start' ? [$header, $content] : [$content, $header];
+ $container = $this->node('div', ['class' => array_merge(
+ [
+ 'row',
+ ($this->options['card'] ? 'card flex-row' : ''),
+ ($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '')
+ ],
+ [
+ "border-{$this->options['header-border-variant']}"
+ ]
+ )], $containerContent);
+ return $container;
+ }
+
+ private function genNav(): 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);
+ }
+}
diff --git a/src/View/Helper/BootstrapElements/BootstrapToast.php b/src/View/Helper/BootstrapElements/BootstrapToast.php
new file mode 100644
index 0000000..9df3b8d
--- /dev/null
+++ b/src/View/Helper/BootstrapElements/BootstrapToast.php
@@ -0,0 +1,74 @@
+Bootstrap->toast([
+ * 'title' => 'Title',
+ * 'bodyHtml' => 'Body',
+ * '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)
+ ));
+ }
+}
diff --git a/src/View/Helper/BootstrapHelper.php b/src/View/Helper/BootstrapHelper.php
index e452bb0..f814116 100644
--- a/src/View/Helper/BootstrapHelper.php
+++ b/src/View/Helper/BootstrapHelper.php
@@ -1,165 +1,307 @@
Bootstrap->Tabs([
- * 'pills' => true,
- * 'card' => true,
- * 'data' => [
- * 'navs' => [
- * 'tab1',
- * ['text' => 'tab2', 'active' => true],
- * ['html' => 'tab3', 'disabled' => true],
- * ],
- * 'content' => [
- * 'body1',
- * 'body2',
- * '~body3~'
- * ]
- * ]
- * ]);
+ * $this->Bootstrap->{$componentName}($options);
*/
namespace App\View\Helper;
use Cake\View\Helper;
-use Cake\Utility\Hash;
-use Cake\Utility\Inflector;
-use Cake\Utility\Security;
+use Cake\Utility\Text;
use InvalidArgumentException;
+use \App\View\Helper\BootstrapElements\BootstrapSwitch;
+use \App\View\Helper\BootstrapElements\BootstrapTabs;
+use \App\View\Helper\BootstrapElements\BootstrapAlert;
+use \App\View\Helper\BootstrapElements\BootstrapAccordion;
+use \App\View\Helper\BootstrapElements\BootstrapBadge;
+use \App\View\Helper\BootstrapElements\BootstrapButton;
+use \App\View\Helper\BootstrapElements\BootstrapCard;
+use \App\View\Helper\BootstrapElements\BootstrapCollapse;
+use \App\View\Helper\BootstrapElements\BootstrapDropdownMenu;
+use \App\View\Helper\BootstrapElements\BootstrapIcon;
+use \App\View\Helper\BootstrapElements\BootstrapListGroup;
+use \App\View\Helper\BootstrapElements\BootstrapListTable;
+use \App\View\Helper\BootstrapElements\BootstrapModal;
+use \App\View\Helper\BootstrapElements\BootstrapNotificationBubble;
+use \App\View\Helper\BootstrapElements\BootstrapProgress;
+use \App\View\Helper\BootstrapElements\BootstrapProgressTimeline;
+use \App\View\Helper\BootstrapElements\BootstrapTable;
+use \App\View\Helper\BootstrapElements\BootstrapToast;
+
+
+const COMPACT_ATTRIBUTES = [
+ 'checked' => true,
+ 'default' => true,
+ 'disabled' => true,
+ 'enabled' => true,
+ 'hidden' => true,
+ 'multiple' => true,
+ 'novalidate' => true,
+ 'readonly' => true,
+ 'required' => true,
+ 'selected' => true,
+ 'visible' => true,
+];
+
class BootstrapHelper extends Helper
{
public $helpers = ['FontAwesome'];
- public function tabs($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function tabs(array $options): string
{
$bsTabs = new BootstrapTabs($options);
return $bsTabs->tabs();
}
- public function alert($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function alert(array $options): string
{
- $bsAlert = new BoostrapAlert($options);
+ $bsAlert = new BootstrapAlert($options);
return $bsAlert->alert();
}
- public function table($options, $data)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @param array $data See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function table(array $options, array $data = []): string
{
- $bsTable = new BoostrapTable($options, $data, $this);
+ $bsTable = new BootstrapTable($options, $data, $this);
return $bsTable->table();
}
- public function listTable($options, $data)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @param array $data See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function listTable(array $options, array $data = []): string
{
- $bsListTable = new BoostrapListTable($options, $data, $this);
+ $bsListTable = new BootstrapListTable($options, $data, $this);
return $bsListTable->table();
}
- public function button($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function button(array $options): string
{
- $bsButton = new BoostrapButton($options);
+ $bsButton = new BootstrapButton($options);
return $bsButton->button();
}
- public function icon($icon, $options = [])
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param string $icon See BootstrapElements\BootstrapTabs
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function icon(string $icon, array $options = []): string
{
- $bsIcon = new BoostrapIcon($icon, $options);
+ $bsIcon = new BootstrapIcon($icon, $options);
return $bsIcon->icon();
}
- public function badge($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function badge(array $options): string
{
- $bsBadge = new BoostrapBadge($options);
+ $bsBadge = new BootstrapBadge($options);
return $bsBadge->badge();
}
- public function modal($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function modal(array $options): string
{
- $bsModal = new BoostrapModal($options);
+ $bsModal = new BootstrapModal($options);
return $bsModal->modal();
}
- public function card($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function card(array $options): string
{
- $bsCard = new BoostrapCard($options);
+ $bsCard = new BootstrapCard($options);
return $bsCard->card();
}
- public function progress($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function progress(array $options): string
{
- $bsProgress = new BoostrapProgress($options);
+ $bsProgress = new BootstrapProgress($options);
return $bsProgress->progress();
}
- public function collapse($options, $content)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @param string $content See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function collapse(array $options, string $content): string
{
- $bsCollapse = new BoostrapCollapse($options, $content, $this);
+ $bsCollapse = new BootstrapCollapse($options, $content, $this);
return $bsCollapse->collapse();
}
- public function accordion($options, $content)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @param array $content See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function accordion(array $options, array $content): string
{
- $bsAccordion = new BoostrapAccordion($options, $content, $this);
+ $bsAccordion = new BootstrapAccordion($options, $content, $this);
return $bsAccordion->accordion();
}
- public function progressTimeline($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function progressTimeline(array $options): string
{
- $bsProgressTimeline = new BoostrapProgressTimeline($options, $this);
+ $bsProgressTimeline = new BootstrapProgressTimeline($options, $this);
return $bsProgressTimeline->progressTimeline();
}
- public function listGroup($options, $data)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $data See BootstrapElements\BootstrapTabs
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function listGroup(array $data, array $options = []): string
{
- $bsListGroup = new BootstrapListGroup($options, $data, $this);
+ $bsListGroup = new BootstrapListGroup($data, $options, $this);
return $bsListGroup->listGroup();
}
- public function genNode($node, $params = [], $content = '')
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function switch(array $options): string
{
- return BootstrapGeneric::genNode($node, $params, $content);
- }
-
- public function switch($options)
- {
- $bsSwitch = new BoostrapSwitch($options, $this);
+ $bsSwitch = new BootstrapSwitch($options, $this);
return $bsSwitch->switch();
}
- public function notificationBubble($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function notificationBubble(array $options): string
{
- $bsNotificationBubble = new BoostrapNotificationBuble($options, $this);
+ $bsNotificationBubble = new BootstrapNotificationBubble($options, $this);
return $bsNotificationBubble->notificationBubble();
}
- public function dropdownMenu($options)
+ /**
+ * Creates a Bootstrap tabs from the given options
+ *
+ * @param array $options See BootstrapElements\BootstrapTabs
+ * @return string
+ */
+ public function dropdownMenu(array $options): string
{
- $bsDropdownMenu = new BoostrapDropdownMenu($options, $this);
+ $bsDropdownMenu = new BootstrapDropdownMenu($options, $this);
return $bsDropdownMenu->dropdownMenu();
}
+
+ /**
+ * Creates a Bootstrap toast from the given options
+ *
+ * @param array $options
+ * @return string
+ */
+ public function toast(array $options): string
+ {
+ $bsToast = new BootstrapToast($options, $this);
+ return $bsToast->toast();
+ }
+
+ /**
+ * Creates a HTML node
+ *
+ * @param string $tag The tag of the node. Example: `div`, `span`, ...
+ * @param array $attrs Optional HTML attributes to be added on the node
+ * @param string $content Optional innerHTML of the node
+ * @param array $options Optional options to build the node. See BootstrapGeneric\node
+ * @return string
+ */
+ public function node(string $tag, array $attrs = [], string $content = '', array $options = []): string
+ {
+ return BootstrapGeneric::node($tag, $attrs, $content, $options);
+ }
+
+ /**
+ * Render the provided template with the given data
+ *
+ * @param string $template The template to render. See BootstrapGeneric\render
+ * @param array $data The data to be used during the template building
+ * @param array $options Optional options to build the template
+ * @return string
+ */
+ public function render(string $template, array $data = [], array $options = []): string
+ {
+ return BootstrapGeneric::render($template, $data, $options);
+ }
}
+
class BootstrapGeneric
{
public static $variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'white', 'transparent'];
@@ -190,19 +332,128 @@ class BootstrapGeneric
}
}
- public static function genNode($node, $params = [], $content = "")
+ /**
+ * Replaces {{placeholders}} inside a $template with the given $data
+ *
+ * Example:
+ * ```
+ * render('{{name}} is {{age}} years old.', ['name' => 'Bob', 'age' => '65']);
+ * ```
+ * Returns: Bob is 65 years old.
+ *
+ * @param string $template The template containing the placeholders
+ * @param array $data A K-V array where keys are placeholder name to be replaced by their value
+ * @param array $options Array of options passed to the Text::insert function
+ * @return string
+ */
+ public static function render(string $template, array $data, array $options=[]): string
{
- return sprintf('<%s %s>%s%s>', $node, BootstrapGeneric::genHTMLParams($params), $content, $node);
+ $defaults = [
+ 'before' => '{{', 'after' => '}}', 'escape' => '\\', 'format' => null, 'clean' => false,
+ ];
+ $options += $defaults;
+ return Text::insert(
+ $template,
+ $data,
+ $options
+ );
}
- protected static function openNode($node, $params = [])
+ /**
+ * Creates an HTML node
+ *
+ * # Options
+ *
+ * - `escape` Set to false to disable escaping of attribute value.
+ *
+ * @param string $tag The tag of the node. Example: 'div', 'span'
+ * @param array $attrs Attributes to be added to the node
+ * @param string|array $content Optional content to be added as innerHTML. If an array is given, it gets converted into string
+ * @param array $options Array of options
+ * @return string
+ */
+ public static function node(string $tag, array $attrs = [], $content = '', array $options = []): string
{
- return sprintf('<%s %s>', $node, BootstrapGeneric::genHTMLParams($params));
+ return self::render(
+ '<{{tag}} {{attrs}}>{{content}}{{tag}}>',
+ [
+ 'tag' => $tag,
+ 'attrs' => self::buildAttrs($attrs, $options),
+ 'content' => is_array($content) ? implode('', $content) : $content,
+ ]
+ );
}
- protected static function closeNode($node)
+ public static function nodeOpen(string $tag, array $attrs = [], array $options = []): string
{
- return sprintf('%s>', $node);
+ return self::render(
+ '<{{tag}} {{attrs}}>',
+ [
+ 'tag' => $tag,
+ 'attrs' => self::buildAttrs($attrs, $options),
+ ]
+ );
+ }
+
+ public static function nodeClose(string $tag): string
+ {
+ return self::render(
+ '{{tag}}>',
+ [
+ 'tag' => $tag,
+ ]
+ );
+ }
+
+ /**
+ * Build a space-delimited string with each HTML attribute generated.
+ *
+ * @param array $attrs
+ * @param array $options Array of options
+ * @return string
+ */
+ public static function buildAttrs(array $attrs, array $options): string
+ {
+ $defaults = [
+ 'escape' => true,
+ ];
+ $options = $options + $defaults;
+
+ $attributes = [];
+ foreach ($attrs as $key => $value) {
+ if (!empty($key) && $value !== null) {
+ $attributes[] = self::__formatAttribute((string) $key, $value, $options['escape']);
+ }
+ }
+ $html = trim(implode(' ', $attributes));
+ return $html;
+ }
+
+ /**
+ * Format an individual HTML attribute
+ * Support minimized attributes such as `selected` and `disabled`
+ *
+ * @param string $key The name of the attribute
+ * @param array|string $value The value of the attribute
+ * @param bool $escape Should the attribute value be escaped
+ * @return string
+ */
+ public static function __formatAttribute(string $key, $value, bool $escape = true): string
+ {
+ $value = is_array($value) ? implode(' ', $value): $value;
+ if (is_numeric($key)) {
+ return sprintf('%s="%s"', h($value), (!empty($escape) ? h($value) : $value));
+ }
+ $isMinimized = isset(COMPACT_ATTRIBUTES[$key]);
+ if ($isMinimized) {
+ if (!empty($value)) {
+ return sprintf('%s="%s"', h($key), (!empty($escape) ? h($value) : $value));
+ }
+ return '';
+ } else if (empty($value)) {
+ return '';
+ }
+ return sprintf('%s="%s"', h($key), (!empty($escape) ? h($value) : $value));
}
protected static function genHTMLParams($params)
@@ -224,9 +475,14 @@ class BootstrapGeneric
return sprintf('%s="%s"', $paramName, implode(' ', $values));
}
+ protected static function convertToArrayIfNeeded($data): array
+ {
+ return is_array($data) ? $data : [$data];
+ }
+
protected static function genericCloseButton($dismissTarget)
{
- return BootstrapGeneric::genNode('button', [
+ return self::node('button', [
'type' => 'button',
'class' => 'btn-close',
'data-bs-dismiss' => $dismissTarget,
@@ -234,1809 +490,13 @@ class BootstrapGeneric
]);
}
- protected static function getTextClassForVariant($variant)
+ protected static function getTextClassForVariant(string $variant): string
{
return !empty(self::$textClassByVariants[$variant]) ? self::$textClassByVariants[$variant] : 'text-black';
}
-}
-class BootstrapTabs extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'fill' => false,
- 'justify' => false,
- 'pills' => false,
- 'vertical' => false,
- 'vertical-size' => 3,
- 'card' => false,
- 'header-variant' => '',
- 'body-variant' => '',
- 'body-class' => [],
- 'nav-class' => [],
- 'nav-item-class' => [],
- 'content-class' => [],
- 'data' => [
- 'navs' => [],
- 'content' => [],
- ],
- ];
- private $bsClasses = null;
-
- function __construct($options)
+ protected static function getBGAndTextClassForVariant(string $variant): string
{
- $this->allowedOptionValues = [
- 'justify' => [false, 'center', 'end'],
- 'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
- 'header-variant' => array_merge(BootstrapGeneric::$variants, ['']),
- ];
- $this->processOptions($options);
- }
-
- public function tabs()
- {
- return $this->genTabs();
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->data = $this->options['data'];
- $this->checkOptionValidity();
- if (empty($this->data['navs'])) {
- throw new InvalidArgumentException(__('No navigation data provided'));
- }
- $this->bsClasses = [
- 'nav' => [],
- 'nav-item' => $this->options['nav-item-class'],
- ];
-
- if (!empty($this->options['justify'])) {
- $this->bsClasses['nav'][] = 'justify-content-' . $this->options['justify'];
- }
-
- 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']) {
- $this->bsClasses['nav'][] = 'nav-fill';
- }
- if ($this->options['justify']) {
- $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'];
- }
-
- $this->options['header-text-variant'] = $this->options['header-variant'] == 'light' ? 'body' : 'white';
- $this->options['header-border-variant'] = $this->options['header-variant'] == 'light' ? '' : $this->options['header-variant'];
- $this->options['body-text-variant'] = $this->options['body-variant'] == '' ? 'body' : 'white';
-
- 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()
- {
- $html = '';
- if ($this->options['vertical']) {
- $html .= $this->genVerticalTabs();
- } else {
- $html .= $this->genHorizontalTabs();
- }
- return $html;
- }
-
- private function genHorizontalTabs()
- {
- $html = '';
- if ($this->options['card']) {
- $html .= $this->openNode('div', ['class' => array_merge(['card'], ["border-{$this->options['header-border-variant']}"])]);
- $html .= $this->openNode('div', ['class' => array_merge(['card-header'], ["bg-{$this->options['header-variant']}", "text-{$this->options['header-text-variant']}"])]);
- }
- $html .= $this->genNav();
- if ($this->options['card']) {
- $html .= $this->closeNode('div');
- $html .= $this->openNode('div', [
- 'class' => array_merge(
- ['card-body'],
- $this->options['body-class'] ?? [],
- ["bg-{$this->options['body-variant']}", "text-{$this->options['body-text-variant']}"]
- )
- ]);
- }
- $html .= $this->genContent();
- if ($this->options['card']) {
- $html .= $this->closeNode('div');
- $html .= $this->closeNode('div');
- }
- return $html;
- }
-
- private function genVerticalTabs()
- {
- $html = $this->openNode('div', ['class' => array_merge(
- [
- 'row',
- ($this->options['card'] ? 'card flex-row' : ''),
- ($this->options['vertical-size'] == 'auto' ? 'flex-nowrap' : '')
- ],
- [
- "border-{$this->options['header-border-variant']}"
- ]
- )]);
- $html .= $this->openNode('div', ['class' => array_merge(
- [
- ($this->options['vertical-size'] != 'auto' ? 'col-' . $this->options['vertical-size'] : ''),
- ($this->options['card'] ? 'card-header border-end' : '')
- ],
- [
- "bg-{$this->options['header-variant']}",
- "text-{$this->options['header-text-variant']}",
- "border-{$this->options['header-border-variant']}"
- ]
- )]);
- $html .= $this->genNav();
- $html .= $this->closeNode('div');
- $html .= $this->openNode('div', ['class' => array_merge(
- [
- ($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''),
- ($this->options['card'] ? 'card-body2' : '')
- ],
- [
- "bg-{$this->options['body-variant']}",
- "text-{$this->options['body-text-variant']}"
- ]
- )]);
- $html .= $this->genContent();
- $html .= $this->closeNode('div');
- $html .= $this->closeNode('div');
- return $html;
- }
-
- private function genNav()
- {
- $html = $this->openNode('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->closeNode('ul');
- return $html;
- }
-
- private function genNavItem($navItem)
- {
- $html = $this->openNode('li', [
- 'class' => array_merge(['nav-item'], $this->bsClasses['nav-item'], $this->options['nav-item-class']),
- 'role' => 'presentation',
- ]);
- $html .= $this->openNode('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',
- ]);
- if (!empty($navItem['html'])) {
- $html .= $navItem['html'];
- } else {
- $html .= h($navItem['text']);
- }
- $html .= $this->closeNode('a');
- $html .= $this->closeNode('li');
- return $html;
- }
-
- private function genContent()
- {
- $html = $this->openNode('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->closeNode('div');
- return $html;
- }
-
- private function genContentItem($navItem, $content)
- {
- return $this->genNode('div', [
- 'class' => array_merge(['tab-pane', 'fade'], [!empty($navItem['active']) ? 'show active' : '']),
- 'role' => 'tabpanel',
- 'id' => $navItem['id'],
- 'aria-labelledby' => $navItem['id'] . '-tab'
- ], $content);
- }
-}
-
-class BoostrapAlert extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'text' => '',
- 'html' => null,
- 'dismissible' => true,
- 'variant' => 'primary',
- 'fade' => true
- ];
-
- private $bsClasses = null;
-
- function __construct($options)
- {
- $this->allowedOptionValues = [
- 'variant' => BootstrapGeneric::$variants,
- ];
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function alert()
- {
- return $this->genAlert();
- }
-
- private function genAlert()
- {
- $html = $this->openNode('div', [
- 'class' => [
- 'alert',
- "alert-{$this->options['variant']}",
- $this->options['dismissible'] ? 'alert-dismissible' : '',
- $this->options['fade'] ? 'fade show' : '',
- ],
- 'role' => "alert"
- ]);
-
- $html .= $this->genContent();
- $html .= $this->genCloseButton();
- $html .= $this->closeNode('div');
- return $html;
- }
-
- private function genCloseButton()
- {
- $html = '';
- if ($this->options['dismissible']) {
- $html .= $this->genericCloseButton('alert');
- }
- return $html;
- }
-
- private function genContent()
- {
- return !is_null($this->options['html']) ? $this->options['html'] : h($this->options['text']);
- }
-}
-
-class BoostrapTable extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'striped' => true,
- 'bordered' => true,
- 'borderless' => false,
- 'hover' => true,
- 'small' => false,
- 'variant' => '',
- 'tableClass' => [],
- 'headerClass' => [],
- 'bodyClass' => [],
- ];
-
- private $bsClasses = null;
-
- function __construct($options, $data, $btHelper)
- {
- $this->allowedOptionValues = [
- 'variant' => array_merge(BootstrapGeneric::$variants, [''])
- ];
- $this->processOptions($options);
- $this->fields = $data['fields'];
- $this->items = $data['items'];
- $this->caption = !empty($data['caption']) ? $data['caption'] : '';
- $this->btHelper = $btHelper;
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function table()
- {
- return $this->genTable();
- }
-
- private function genTable()
- {
- $html = $this->openNode('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' : '',
- !empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
- !empty($this->options['tableClass']) ? (is_array($this->options['tableClass']) ? implode(' ', $this->options['tableClass']) : $this->options['tableClass']) : ''
- ],
- ]);
-
- $html .= $this->genCaption();
- $html .= $this->genHeader();
- $html .= $this->genBody();
-
- $html .= $this->closeNode('table');
- return $html;
- }
-
- private function genHeader()
- {
- $head = $this->openNode('thead', [
- 'class' => [
- !empty($this->options['headerClass']) ? $this->options['headerClass'] : ''
- ],
- ]);
- $head .= $this->openNode('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['key']);
- $label = h($label);
- }
- } else {
- $label = Inflector::humanize($field);
- $label = h($label);
- }
- $head .= $this->genNode('th', [], $label);
- }
- $head .= $this->closeNode('tr');
- $head .= $this->closeNode('thead');
- return $head;
- }
-
- private function genBody()
- {
- $body = $this->openNode('tbody', [
- 'class' => [
- !empty($this->options['bodyClass']) ? (is_array($this->options['bodyClass']) ? implode(' ', $this->options['bodyClass']) : $this->options['bodyClass']) : ''
- ],
- ]);
- foreach ($this->items as $i => $row) {
- $body .= $this->genRow($row, $i);
- }
- $body .= $this->closeNode('tbody');
- return $body;
- }
-
- private function genRow($row, $rowIndex)
- {
- $html = $this->openNode('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) {
- if (is_array($field)) {
- $key = $field['key'];
- } else {
- $key = $field;
- }
- $cellValue = Hash::get($row, $key);
- $html .= $this->genCell($cellValue, $field, $row, $rowIndex);
- }
- } else { // indexed array
- foreach ($row as $i => $cellValue) {
- $html .= $this->genCell($cellValue, 'index', $row, $rowIndex);
- }
- }
- $html .= $this->closeNode('tr');
- return $html;
- }
-
- private function genCell($value, $field=[], $row=[], $rowIndex=0)
- {
- if (isset($field['formatter'])) {
- $cellContent = $field['formatter']($value, $row, $rowIndex);
- } else if (isset($field['element'])) {
- $cellContent = $this->btHelper->getView()->element($field['element'], [
- 'data' => [$value],
- 'field' => ['path' => '0']
- ]);
- } else {
- $cellContent = h($value);
- }
- return $this->genNode('td', [
- 'class' => [
- !empty($row['_cellVariant']) ? "bg-{$row['_cellVariant']}" : ''
- ]
- ], $cellContent);
- }
-
- private function genCaption()
- {
- return !empty($this->caption) ? $this->genNode('caption', [], h($this->caption)) : '';
- }
-}
-
-class BoostrapListTable extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'striped' => true,
- 'bordered' => false,
- 'borderless' => false,
- 'hover' => true,
- 'small' => false,
- 'variant' => '',
- 'tableClass' => [],
- 'bodyClass' => [],
- ];
-
- private $bsClasses = null;
-
- function __construct($options, $data, $btHelper)
- {
- $this->allowedOptionValues = [
- 'variant' => array_merge(BootstrapGeneric::$variants, [''])
- ];
- $this->processOptions($options);
- $this->fields = $data['fields'];
- $this->item = $data['item'];
- $this->caption = !empty($data['caption']) ? $data['caption'] : '';
- $this->btHelper = $btHelper;
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function table()
- {
- return $this->genTable();
- }
-
- private function genTable()
- {
- $html = $this->openNode('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' : '',
- !empty($this->options['variant']) ? "table-{$this->options['variant']}" : '',
- !empty($this->options['tableClass']) ? (is_array($this->options['tableClass']) ? implode(' ', $this->options['tableClass']) : $this->options['tableClass']) : ''
- ],
- 'id' => $this->options['id'] ?? ''
- ]);
-
- $html .= $this->genCaption();
- $html .= $this->genBody();
-
- $html .= $this->closeNode('table');
- return $html;
- }
-
- private function genBody()
- {
- $body = $this->openNode('tbody', [
- 'class' => [
- !empty($this->options['bodyClass']) ? (is_array($this->options['bodyClass']) ? implode(' ', $this->options['bodyClass']) : $this->options['bodyClass']) : ''
- ],
- ]);
- foreach ($this->fields as $i => $field) {
- $body .= $this->genRow($field);
- }
- $body .= $this->closeNode('tbody');
- return $body;
- }
-
- private function genRow($field)
- {
- $rowValue = $this->genCell($field);
- $rowKey = $this->genNode('th', [
- 'class' => [
- 'col-4 col-sm-2'
- ],
- 'scope' => 'row'
- ], h($field['key']));
- $row = $this->genNode('tr', [
- 'class' => [
- 'd-flex',
- !empty($field['_rowVariant']) ? "table-{$field['_rowVariant']}" : ''
- ]
- ], implode('', [$rowKey, $rowValue]));
- return $row;
- }
-
- private function genCell($field = [])
- {
- if (isset($field['raw'])) {
- $cellContent = $field['raw'];
- if (empty($field['no_escaping'])) {
- $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 (['info', 'warning', 'danger'] as $message_type) {
- if (!empty($field[$message_type])) {
- $cellContent .= sprintf(' %s', $message_type, $field[$message_type]);
- }
- }
- return $this->genNode('td', [
- 'class' => [
- 'col-8 col-sm-10',
- !empty($field['_cellVariant']) ? "bg-{$field['_cellVariant']}" : ''
- ]
- ], $cellContent);
- }
-
- private function getValueFromObject($field)
- {
- if (is_array($field)) {
- $key = $field['path'];
- } else {
- $key = $field;
- }
- $cellValue = Hash::get($this->item, $key);
- return $cellValue;
- }
-
- private function getElementPath($type)
- {
- return sprintf(
- '%s%sField',
- $this->options['elementsRootPath'] ?? '',
- $type
- );
- }
-
- private function genCaption()
- {
- return !empty($this->caption) ? $this->genNode('caption', [], h($this->caption)) : '';
- }
-}
-
-class BoostrapButton extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'id' => '',
- 'text' => '',
- 'html' => null,
- 'variant' => 'primary',
- 'outline' => false,
- 'size' => '',
- 'icon' => null,
- 'image' => null,
- 'class' => [],
- 'type' => 'button',
- 'nodeType' => 'button',
- 'title' => '',
- 'params' => [],
- 'badge' => false
- ];
-
- private $bsClasses = [];
-
- function __construct($options)
- {
- $this->allowedOptionValues = [
- 'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
- 'size' => ['', 'xs', 'sm', 'lg'],
- 'type' => ['button', 'submit', 'reset']
- ];
- if (empty($options['class'])) {
- $options['class'] = '';
- }
- $options['class'] = !is_array($options['class']) ? [$options['class']] : $options['class'];
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
-
- if (!empty($this->options['id'])) {
- $this->options['params']['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';
- }
- }
-
- public function button()
- {
- return $this->genButton();
- }
-
- private function genButton()
- {
- $html = $this->openNode($this->options['nodeType'], array_merge($this->options['params'], [
- '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->genContent();
- if (!empty($this->options['badge'])) {
- $bsBadge = new BoostrapBadge($this->options['badge']);
- $html .= $bsBadge->badge();
- }
- $html .= $this->closeNode($this->options['nodeType']);
- return $html;
- }
-
- private function genIcon()
- {
- if (!empty($this->options['icon'])) {
- $bsIcon = new BoostrapIcon($this->options['icon'], [
- 'class' => [(!empty($this->options['text']) ? 'me-1' : '')]
- ]);
- return $bsIcon->icon();
- }
- return '';
- }
-
- private function genImage()
- {
- if (!empty($this->options['image'])) {
- return $this->genNode('img', [
- 'src' => $this->options['image']['path'] ?? '',
- 'class' => ['img-fluid', 'me-1'],
- 'width' => '26',
- 'height' => '26',
- 'alt' => $this->options['image']['alt'] ?? ''
- ]);
- }
- return '';
- }
-
- private function genContent()
- {
- return !is_null($this->options['html']) ? $this->options['html'] : $this->options['text'];
- }
-}
-
-class BoostrapBadge extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'text' => '',
- 'variant' => 'primary',
- 'pill' => false,
- 'title' => '',
- 'class' => [],
- ];
-
- function __construct($options)
- {
- $this->allowedOptionValues = [
- 'variant' => BootstrapGeneric::$variants,
- ];
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->options['class'] = is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']];
- $this->checkOptionValidity();
- }
-
- public function badge()
- {
- return $this->genBadge();
- }
-
- private function genBadge()
- {
- $html = $this->genNode('span', [
- 'class' => array_merge($this->options['class'], [
- 'ms-1',
- 'badge',
- "bg-{$this->options['variant']}",
- $this->getTextClassForVariant($this->options['variant']),
- $this->options['pill'] ? 'rounded-pill' : '',
- ]),
- 'title' => $this->options['title']
- ], h($this->options['text']));
- return $html;
- }
-}
-
-class BoostrapIcon extends BootstrapGeneric
-{
- private $icon = '';
- private $defaultOptions = [
- 'class' => [],
- 'title' => '',
- 'params' => [],
- ];
-
- function __construct($icon, $options = [])
- {
- $this->icon = $icon;
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function icon()
- {
- return $this->genIcon();
- }
-
- private function genIcon()
- {
- $html = $this->genNode('span', array_merge([
- 'class' => array_merge(
- is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']],
- ["fa fa-{$this->icon}"]
- ),
- 'title' => h($this->options['title'])
- ], $this->options['params']));
- return $html;
- }
-}
-
-class BoostrapModal extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'size' => '',
- 'centered' => true,
- 'scrollable' => true,
- 'backdropStatic' => false,
- 'title' => '',
- 'titleHtml' => false,
- 'body' => '',
- 'bodyHtml' => false,
- 'footerHtml' => false,
- 'confirmText' => 'Confirm',
- 'confirmIcon' => false,
- 'cancelText' => 'Cancel',
- 'modalClass' => [''],
- 'headerClass' => [''],
- 'bodyClass' => [''],
- 'footerClass' => [''],
- 'type' => 'ok-only',
- 'variant' => '',
- 'confirmFunction' => '', // Will be called with the following arguments confirmFunction(modalObject, tmpApi)
- 'cancelFunction' => ''
- ];
-
- private $bsClasses = null;
-
- function __construct($options)
- {
- $this->allowedOptionValues = [
- 'size' => ['sm', 'lg', 'xl', ''],
- 'type' => ['ok-only', 'confirm', 'confirm-success', 'confirm-warning', 'confirm-danger', 'custom'],
- 'variant' => array_merge(BootstrapGeneric::$variants, ['']),
- ];
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function modal()
- {
- return $this->genModal();
- }
-
- private function genModal()
- {
- $this->options['modalClass'] = !empty($this->options['modalClass']) && !is_array($this->options['modalClass']) ? [$this->options['modalClass']] : $this->options['modalClass'];
- $dialog = $this->openNode('div', [
- 'class' => array_merge(
- ['modal-dialog', (!empty($this->options['size'])) ? "modal-{$this->options['size']}" : ''],
- $this->options['modalClass']
- ),
- ]);
- $content = $this->openNode('div', [
- 'class' => ['modal-content'],
- ]);
- $header = $this->genHeader();
- $body = $this->genBody();
- $footer = $this->genFooter();
- $closedDiv = $this->closeNode('div');
-
- $html = "{$dialog}{$content}{$header}{$body}{$footer}{$closedDiv}{$closedDiv}";
- return $html;
- }
-
- private function genHeader()
- {
- $header = $this->openNode('div', ['class' => array_merge(['modal-header'], $this->options['headerClass'])]);
- if (!empty($this->options['titleHtml'])) {
- $header .= $this->options['titleHtml'];
- } else {
- $header .= $this->genNode('h5', ['class' => ['modal-title']], h($this->options['title']));
- }
- if (empty($this->options['backdropStatic'])) {
- $header .= $this->genericCloseButton('modal');
- }
- $header .= $this->closeNode('div');
- return $header;
- }
-
- private function genBody()
- {
- $body = $this->openNode('div', ['class' => array_merge(['modal-body'], $this->options['bodyClass'])]);
- if (!empty($this->options['bodyHtml'])) {
- $body .= $this->options['bodyHtml'];
- } else {
- $body .= h($this->options['body']);
- }
- $body .= $this->closeNode('div');
- return $body;
- }
-
- private function genFooter()
- {
- $footer = $this->openNode('div', [
- 'class' => array_merge(['modal-footer'], $this->options['footerClass']),
- 'data-custom-footer' => $this->options['type'] == 'custom'
- ]);
- if (!empty($this->options['footerHtml'])) {
- $footer .= $this->options['footerHtml'];
- } else {
- $footer .= $this->getFooterBasedOnType();
- }
- $footer .= $this->closeNode('div');
- return $footer;
- }
-
- private function getFooterBasedOnType()
- {
- if ($this->options['type'] == 'ok-only') {
- return $this->getFooterOkOnly();
- } else if (str_contains($this->options['type'], 'confirm')) {
- return $this->getFooterConfirm();
- } else if ($this->options['type'] == 'custom') {
- return $this->getFooterCustom();
- } else {
- return $this->getFooterOkOnly();
- }
- }
-
- private function getFooterOkOnly()
- {
- return (new BoostrapButton([
- 'variant' => 'primary',
- 'text' => __('Ok'),
- 'params' => [
- 'data-bs-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
- 'onclick' => $this->options['confirmFunction']
- ]
- ]))->button();
- }
-
- private function getFooterConfirm()
- {
- if ($this->options['type'] == 'confirm') {
- $variant = 'primary';
- } else {
- $variant = explode('-', $this->options['type'])[1];
- }
- $buttonCancel = (new BoostrapButton([
- 'variant' => 'secondary',
- 'text' => h($this->options['cancelText']),
- 'params' => [
- 'data-bs-dismiss' => 'modal',
- 'onclick' => $this->options['cancelFunction']
- ]
- ]))->button();
-
- $buttonConfirm = (new BoostrapButton([
- 'variant' => $variant,
- 'text' => h($this->options['confirmText']),
- 'icon' => h($this->options['confirmIcon']),
- 'class' => 'modal-confirm-button',
- 'params' => [
- // 'data-bs-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
- 'data-confirmFunction' => sprintf('%s', $this->options['confirmFunction'])
- ]
- ]))->button();
- return $buttonCancel . $buttonConfirm;
- }
-
- private function getFooterCustom()
- {
- $buttons = [];
- foreach ($this->options['footerButtons'] as $buttonConfig) {
- $buttons[] = (new BoostrapButton([
- 'variant' => h($buttonConfig['variant'] ?? 'primary'),
- 'text' => h($buttonConfig['text']),
- 'class' => 'modal-confirm-button',
- 'params' => [
- 'data-bs-dismiss' => !empty($buttonConfig['clickFunction']) ? '' : 'modal',
- 'data-clickFunction' => sprintf('%s', $buttonConfig['clickFunction'])
- ]
- ]))->button();
- }
- return implode('', $buttons);
- }
-}
-
-class BoostrapCard extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'variant' => '',
- 'headerText' => '',
- 'footerText' => '',
- 'bodyText' => '',
- 'headerHTML' => '',
- 'footerHTML' => '',
- 'bodyHTML' => '',
- 'class' => '',
- 'headerClass' => '',
- 'bodyClass' => '',
- 'footerClass' => '',
- ];
-
- public function __construct($options)
- {
- $this->allowedOptionValues = [
- 'variant' => array_merge(BootstrapGeneric::$variants, ['']),
- ];
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function card()
- {
- return $this->genCard();
- }
-
- private function genCard()
- {
- $card = $this->genNode('div', [
- 'class' => [
- 'card',
- !empty($this->options['variant']) ? "bg-{$this->options['variant']}" : '',
- !empty($this->options['variant']) ? $this->getTextClassForVariant($this->options['variant']) : '',
- h(is_array($this->options['class']) ? implode(' ', $this->options['class']) : $this->options['class']),
- ],
- ], implode('', [$this->genHeader(), $this->genBody(), $this->genFooter()]));
- return $card;
- }
-
- private function genHeader()
- {
- if (empty($this->options['headerHTML']) && empty($this->options['headerText'])) {
- return '';
- }
- $content = !empty($this->options['headerHTML']) ? $this->options['headerHTML'] : h($this->options['headerText']);
- $header = $this->genNode('div', [
- 'class' => [
- 'card-header',
- h($this->options['headerClass']),
- ],
- ], $content);
- return $header;
- }
-
- private function genBody()
- {
- if (empty($this->options['bodyHTML']) && empty($this->options['bodyText'])) {
- return '';
- }
- $content = !empty($this->options['bodyHTML']) ? $this->options['bodyHTML'] : h($this->options['bodyText']);
- $body = $this->genNode('div', [
- 'class' => [
- 'card-body',
- h($this->options['bodyClass']),
- ],
- ], $content);
- return $body;
- }
-
- private function genFooter()
- {
- if (empty($this->options['footerHTML']) && empty($this->options['footerText'])) {
- return '';
- }
- $content = !empty($this->options['footerHTML']) ? $this->options['footerHTML'] : h($this->options['footerText']);
- $footer = $this->genNode('div', [
- 'class' => [
- 'card-footer',
- h($this->options['footerClass']),
- ],
- ], $content);
- return $footer;
- }
-}
-
-class BoostrapSwitch extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'label' => '',
- 'variant' => 'primary',
- 'disabled' => false,
- 'checked' => false,
- 'title' => '',
- 'class' => [],
- 'attrs' => [],
- ];
-
- function __construct($options)
- {
- $this->allowedOptionValues = [
- 'variant' => BootstrapGeneric::$variants,
- ];
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function switch()
- {
- return $this->genSwitch();
- }
-
- private function genSwitch()
- {
- $tmpId = 'tmp-' . mt_rand();
- $html = $this->genNode('div', [
- 'class' => [
- 'form-check form-switch',
- ],
- 'title' => $this->options['title']
- ], implode('', [
- $this->genNode('input', array_merge([
- 'type' => "checkbox",
- 'class' => 'form-check-input',
- 'id' => $tmpId,
- ($this->options['disabled'] ? 'disabled' : '') => '',
- ($this->options['checked'] ? 'checked' : '') => $this->options['checked'] ? 'checked' : '',
- ], $this->options['attrs'])),
- $this->genNode('label', [
- 'class' => 'form-check-label',
- 'for' => $tmpId,
- ], h($this->options['label']))
- ]));
- return $html;
- }
-}
-
-class BoostrapNotificationBuble extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'label' => '',
- 'variant' => 'warning',
- 'borderVariant' => 'ligth',
- 'title' => 'Notification',
- 'class' => [],
- 'attrs' => [],
- ];
-
- function __construct($options)
- {
- $this->allowedOptionValues = [
- 'variant' => BootstrapGeneric::$variants,
- ];
- $this->defaultOptions['label'] = __('New notifications');
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- if (!empty($this->options['attrs']['style'])) {
- $this->options['attrs']['style'] += 'box-shadow: 0 0.125rem 0.25rem #00000050;';
- } else {
- $this->options['attrs']['style'] = 'box-shadow: 0 0.125rem 0.25rem #00000050;';
- }
- }
-
- public function notificationBubble()
- {
- return $this->genNotificationBubble();
- }
-
- private function genNotificationBubble()
- {
- $tmpId = 'tmp-' . mt_rand();
- $html = $this->genNode('span', [
- 'id' => $tmpId,
- 'class' => [
- 'position-absolute',
- 'top-0',
- 'start-100',
- 'translate-middle',
- 'p-1',
- 'border border-2 rounded-circle',
- "border-{$this->options['borderVariant']}",
- "bg-{$this->options['variant']}",
- ],
- 'title' => $this->options['title']
- ], $this->genNode('span', [
- 'class' => [
- ],
- $this->options['label']
- ]));
- return $html;
- }
-}
-
-class BoostrapProgress extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'value' => 0,
- 'total' => 100,
- 'text' => '',
- 'title' => '',
- 'variant' => 'primary',
- 'height' => '',
- 'striped' => false,
- 'animated' => false,
- 'label' => true
- ];
-
- function __construct($options)
- {
- $this->allowedOptionValues = [
- 'variant' => BootstrapGeneric::$variants,
- ];
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function progress()
- {
- return $this->genProgress();
- }
-
- private function genProgress()
- {
- $percentage = round(100 * $this->options['value'] / $this->options['total']);
- $heightStyle = !empty($this->options['height']) ? sprintf('height: %s;', h($this->options['height'])) : '';
- $widthStyle = sprintf('width: %s%%;', $percentage);
- $label = $this->options['label'] ? "{$percentage}%" : '';
- $pb = $this->genNode('div', [
- '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' => $this->options['title']
- ], $label);
- $container = $this->genNode('div', [
- 'class' => [
- 'progress',
- ],
- 'style' => "${heightStyle}",
- 'title' => h($this->options['title']),
- ], $pb);
- return $container;
- }
-}
-
-class BoostrapCollapse extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'title' => '',
- 'open' => false,
- ];
-
- function __construct($options, $content, $btHelper)
- {
- $this->allowedOptionValues = [];
- $this->processOptions($options);
- $this->content = $content;
- $this->btHelper = $btHelper;
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function collapse()
- {
- return $this->genCollapse();
- }
-
- private function genControl()
- {
- $html = $this->genNode('a', [
- 'class' => ['text-decoration-none'],
- 'data-bs-toggle' => 'collapse',
- 'href' => '#collapseExample',
- 'role' => 'button',
- 'aria-expanded' => 'false',
- 'aria-controls' => 'collapseExample',
- ], h($this->options['title']));
- return $html;
- }
-
- private function genContent()
- {
- $content = $this->genNode('div', [
- 'class' => 'card',
- ], $this->content);
- $container = $this->genNode('div', [
- 'class' => ['collapse', $this->options['open'] ? 'show' : ''],
- 'id' => 'collapseExample',
- ], $content);
- return $container;
- }
-
- private function genCollapse()
- {
- $html = $this->genControl();
- $html .= $this->genContent();
- return $html;
- }
-}
-
-class BoostrapAccordion extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'stayOpen' => true,
- 'class' => [],
- ];
-
- function __construct($options, $content, $btHelper)
- {
- $this->allowedOptionValues = [];
- $this->content = $content;
- $this->btHelper = $btHelper;
- $this->processOptions($options);
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- if (!is_array($this->options['class']) && !empty($this->options['class'])) {
- $this->options['class'] = [$this->options['class']];
- }
- $this->seed = 'acc-' . mt_rand();
- $this->contentSeeds = [];
- foreach ($this->content as $accordionItem) {
- $this->contentSeeds[] = mt_rand();
- }
- }
-
- public function accordion()
- {
- return $this->genAccordion();
- }
-
- private function genHeader($accordionItem, $i)
- {
- $html = $this->openNode('h2', [
- 'class' => ['accordion-header'],
- 'id' => 'head-' . $this->contentSeeds[$i]
- ]);
- $content = !empty($accordionItem['header']['html']) ? $accordionItem['header']['html'] : h($accordionItem['header']['title'] ?? '- no title -');
- $buttonOptions = [
- 'class' => array_merge(['accordion-button', empty($accordionItem['_open']) ? 'collapsed' : ''], $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->genNode('button', $buttonOptions, $content);
- $html .= $this->closeNode(('h2'));
- return $html;
- }
-
- private function genBody($accordionItem, $i)
- {
- $content = $this->genNode('div', [
- 'class' => ['accordion-body']
- ], $accordionItem['body']);
- $divOptions = [
- 'class' => array_merge(['accordion-collapse collapse', empty($accordionItem['_open']) ? '' : 'show'], $accordionItem['body']['__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->genNode('div', $divOptions, $content);
- return $html;
- }
-
- private function genAccordion()
- {
- $html = $this->openNode('div', [
- 'class' => array_merge(['accordion'], $this->options['class']),
- 'id' => $this->seed
- ]);
- foreach ($this->content as $i => $accordionItem) {
- $html .= $this->openNode('div', [
- 'class' => array_merge(['accordion-item'], $accordionItem['__class'] ?? [])
- ]);
- $html .= $this->genHeader($accordionItem, $i);
- $html .= $this->genBody($accordionItem, $i);
- $html .= $this->closeNode('div');
- }
- $html .= $this->closeNode('div');
- return $html;
- }
-}
-
-class BoostrapProgressTimeline extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'steps' => [],
- 'selected' => 0,
- 'variant' => 'info',
- 'variantInactive' => 'secondary',
- ];
-
- function __construct($options, $btHelper)
- {
- $this->allowedOptionValues = [
- 'variant' => BootstrapGeneric::$variants,
- 'variantInactive' => BootstrapGeneric::$variants,
- ];
- $this->processOptions($options);
- $this->btHelper = $btHelper;
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- $this->checkOptionValidity();
- }
-
- public function progressTimeline()
- {
- return $this->genProgressTimeline();
- }
-
- private function getStepIcon($step, $i, $nodeActive, $lineActive)
- {
- $icon = $this->genNode('b', [
- 'class' => [
- !empty($step['icon']) ? h($this->btHelper->FontAwesome->getClass($step['icon'])) : '',
- $this->getTextClassForVariant($this->options['variant'])
- ],
- ], empty($step['icon']) ? h($i + 1) : '');
- $iconContainer = $this->genNode('span', [
- 'class' => [
- 'd-flex', 'align-items-center', 'justify-content-center',
- 'rounded-circle',
- $nodeActive ? "bg-{$this->options['variant']}" : "bg-{$this->options['variantInactive']}"
- ],
- 'style' => 'width:50px; height:50px'
- ], $icon);
- $li = $this->genNode('li', [
- 'class' => [
- 'd-flex', 'flex-column',
- $nodeActive ? 'progress-active' : 'progress-inactive',
- ],
- ], $iconContainer);
- $html = $li . $this->getHorizontalLine($i, $nodeActive, $lineActive);
- return $html;
- }
-
- private function getHorizontalLine($i, $nodeActive, $lineActive)
- {
- $stepCount = count($this->options['steps']);
- if ($i == $stepCount - 1) {
- return '';
- }
- $progressBar = (new BoostrapProgress([
- 'label' => false,
- 'value' => $nodeActive ? ($lineActive ? 100 : 50) : 0,
- 'height' => '2px',
- 'variant' => $this->options['variant']
- ]))->progress();
- $line = $this->genNode('span', [
- 'class' => [
- 'progress-line',
- 'flex-grow-1', 'align-self-center',
- $lineActive ? "bg-{$this->options['variant']}" : ''
- ],
- ], $progressBar);
- return $line;
- }
-
- private function getStepText($step, $isActive)
- {
- return $this->genNode('li', [
- 'class' => [
- 'text-center',
- 'fw-bold',
- $isActive ? 'progress-active' : 'progress-inactive',
- ],
- ], h($step['text'] ?? ''));
- }
-
- private function genProgressTimeline()
- {
- $iconLis = '';
- $textLis = '';
- foreach ($this->options['steps'] as $i => $step) {
- $nodeActive = $i <= $this->options['selected'];
- $lineActive = $i < $this->options['selected'];
- $iconLis .= $this->getStepIcon($step, $i, $nodeActive, $lineActive);
- $textLis .= $this->getStepText($step, $nodeActive);
- }
- $ulIcons = $this->genNode('ul', [
- 'class' => [
- 'd-flex', 'justify-content-around',
- ],
- ], $iconLis);
- $ulText = $this->genNode('ul', [
- 'class' => [
- 'd-flex', 'justify-content-between',
- ],
- ], $textLis);
- $html = $this->genNode('div', [
- 'class' => ['progress-timeline', 'mw-75', 'mx-auto']
- ], $ulIcons . $ulText);
- return $html;
- }
-}
-
-class BootstrapListGroup extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'hover' => false,
- ];
-
- private $bsClasses = null;
-
- function __construct($options, $data, $btHelper)
- {
- $this->data = $data;
- $this->processOptions($options);
- $this->btHelper = $btHelper;
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- }
-
- public function listGroup()
- {
- return $this->genListGroup();
- }
-
- private function genListGroup()
- {
- $html = $this->openNode('div', [
- 'class' => ['list-group',],
- ]);
- foreach ($this->data as $item) {
- $html .= $this->genItem($item);
- }
- $html .= $this->closeNode('div');
- return $html;
- }
-
- private function genItem($item)
- {
- if (!empty($item['heading'])) { // complex layout with heading, badge and body
- $html = $this->genNode('a', [
- 'class' => ['list-group-item', (!empty($this->options['hover']) ? 'list-group-item-action' : ''),],
- ], implode('', [
- $this->genHeadingGroup($item),
- $this->genBody($item),
- ]));
- } else { // simple layout with just -like elements
- $html = $this->genNode('a', [
- 'class' => ['list-group-item', 'd-flex', 'align-items-center', 'justify-content-between'],
- ], implode('', [
- h($item['text']),
- $this->genBadge($item)
- ]));
- }
- return $html;
- }
-
- private function genHeadingGroup($item)
- {
- $html = $this->genNode('div', [
- 'class' => ['d-flex', 'w-100', 'justify-content-between',],
- ], implode('', [
- $this->genHeading($item),
- $this->genBadge($item)
- ]));
- return $html;
- }
-
- private function genHeading($item)
- {
- if (empty($item['heading'])) {
- return '';
- }
- return $this->genNode('h5', [
- 'class' => ['mb-1'],
- ], h($item['heading']));
- }
-
- private function genBadge($item)
- {
- if (empty($item['badge'])) {
- return '';
- }
- return $this->genNode('span', [
- 'class' => ['badge rounded-pill', (!empty($item['badge-variant']) ? "bg-{$item['badge-variant']}" : 'bg-primary')],
- ], h($item['badge']));
- }
-
- private function genBody($item)
- {
- if (!empty($item['bodyHTML'])) {
- return $item['bodyHTML'];
- }
- return !empty($item['body']) ? h($item['body']) : '';
- }
-}
-
-
-class BoostrapDropdownMenu extends BootstrapGeneric
-{
- private $defaultOptions = [
- 'dropdown-class' => [],
- 'toggle-button' => [],
- 'menu' => [],
- 'direction' => 'end',
- 'alignment' => 'start',
- 'submenu_alignment' => 'start',
- 'submenu_direction' => 'end',
- 'submenu_classes' => [],
- ];
-
- function __construct($options, $btHelper)
- {
- $this->allowedOptionValues = [
- 'direction' => ['start', 'end', 'up', 'down'],
- 'alignment' => ['start', 'end'],
- ];
- $this->processOptions($options);
- $this->menu = $this->options['menu'];
- $this->btHelper = $btHelper;
- }
-
- private function processOptions($options)
- {
- $this->options = array_merge($this->defaultOptions, $options);
- if (!empty($this->options['dropdown-class']) && !is_array($this->options['dropdown-class'])) {
- $this->options['dropdown-class'] = [$this->options['dropdown-class']];
- }
- $this->checkOptionValidity();
- }
-
- public function dropdownMenu()
- {
- return $this->fullDropdown();
- }
-
- public function fullDropdown()
- {
- return $this->genDropdownWrapper($this->genDropdownToggleButton(), $this->genDropdownMenu($this->menu));
- }
-
- public function genDropdownWrapper($toggle = '', $menu = '', $direction = null, $classes = null)
- {
- $classes = !is_null($classes) ? $classes : $this->options['dropdown-class'];
- $direction = !is_null($direction) ? $direction : $this->options['direction'];
- $content = $toggle . $menu;
- $html = $this->genNode('div', array_merge(
- $this->options['params'],
- [
- 'class' => array_merge(
- $classes,
- [
- 'dropdown',
- "drop{$direction}"
- ]
- )
- ]
- ), $content);
- return $html;
- }
-
- public function genDropdownToggleButton()
- {
- $defaultOptions = [
- 'class' => ['dropdown-toggle'],
- 'params' => [
- 'data-bs-toggle' => 'dropdown',
- 'aria-expanded' => 'false',
- ]
- ];
- $options = array_merge_recursive($this->options['toggle-button'], $defaultOptions);
- return $this->btHelper->button($options);
- }
-
- private function genDropdownMenu($entries, $alignment = null)
- {
- $alignment = !is_null($alignment) ? $alignment : $this->options['alignment'];
- $html = $this->genNode('div', [
- 'class' => ['dropdown-menu', "dropdown-menu-{$alignment}"],
- ], $this->genEntries($entries));
- return $html;
- }
-
- private function genEntries($entries)
- {
- $html = '';
- foreach ($entries as $entry) {
- $link = $this->genEntry($entry);
- if (!empty($entry['menu'])) {
- $html .= $this->genDropdownWrapper($link, $this->genDropdownMenu($entry['menu']), $this->options['submenu_direction'], $this->options['submenu_classes']);
- } else {
- $html .= $link;
- }
- }
- return $html;
- }
-
- private function genEntry($entry)
- {
- if (!empty($entry['html'])) {
- return $entry['html'];
- }
- $classes = [];
- $icon = '';
- if (!empty($entry['icon'])) {
- $icon = $this->btHelper->icon($entry['icon'], ['class' => 'me-2']);
- }
- $badge = '';
- if (!empty($entry['badge'])) {
- $bsBadge = new BoostrapBadge(array_merge(
- ['class' => ['ms-auto']],
- $entry['badge']
- ));
- $badge = $bsBadge->badge();
- }
-
- if (!empty($entry['header'])) {
- return $this->genNode('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 = ['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->genNode('sup', ['class' => 'ms-1 text-muted'], $entry['sup']) : ''
- );
- $label = $this->genNode('span', ['class' => 'mx-1'], $labelContent);
- $content = $icon . $label . $badge;
-
- return $this->genNode('a', array_merge([
- 'class' => $classes,
- ], $params), $content);
+ return sprintf('bg-%s %s', $variant, self::getTextClassForVariant($variant));
}
}
diff --git a/src/View/Helper/SocialProviderHelper.php b/src/View/Helper/SocialProviderHelper.php
index effb8c5..adfd6a8 100644
--- a/src/View/Helper/SocialProviderHelper.php
+++ b/src/View/Helper/SocialProviderHelper.php
@@ -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,
diff --git a/templates/Broods/preview_individuals.php b/templates/Broods/preview_individuals.php
index 73e2199..d0ae0e4 100644
--- a/templates/Broods/preview_individuals.php
+++ b/templates/Broods/preview_individuals.php
@@ -5,6 +5,16 @@ echo $this->element('genericElements/IndexTable/index_table', [
'top_bar' => [
'pull' => 'right',
'children' => [
+ [
+ 'type' => 'simple',
+ 'children' => [
+ 'data' => [
+ 'type' => 'simple',
+ 'text' => __('Download All'),
+ 'popover_url' => sprintf('/broods/downloadIndividual/%s/all', h($brood_id)),
+ ]
+ ]
+ ],
[
'type' => 'search',
'button' => __('Search'),
@@ -21,6 +31,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
'sort' => 'id',
'data_path' => 'id',
],
+ [
+ 'name' => __('Status'),
+ 'class' => 'short',
+ 'data_path' => 'status',
+ 'sort' => 'status',
+ 'element' => 'brood_sync_status',
+ ],
[
'name' => __('Email'),
'sort' => 'email',
@@ -53,8 +70,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
'pull' => 'right',
'actions' => [
[
- 'url' => '/broods/downloadIndividual/' . $brood_id,
- 'url_params_data_paths' => ['id'],
+ 'open_modal' => '/broods/downloadIndividual/' . $brood_id . '/[onclick_params_data_path]',
+ 'modal_params_data_path' => 'id',
'title' => __('Download'),
'icon' => 'download'
]
diff --git a/templates/Broods/preview_organisations.php b/templates/Broods/preview_organisations.php
index 40b09c3..2f082e4 100644
--- a/templates/Broods/preview_organisations.php
+++ b/templates/Broods/preview_organisations.php
@@ -3,8 +3,17 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
- 'pull' => 'right',
'children' => [
+ [
+ 'type' => 'simple',
+ 'children' => [
+ 'data' => [
+ 'type' => 'simple',
+ 'text' => __('Download All'),
+ 'popover_url' => sprintf('/broods/downloadOrg/%s/all', h($brood_id)),
+ ]
+ ]
+ ],
[
'type' => 'search',
'button' => __('Search'),
@@ -22,6 +31,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
'class' => 'short',
'data_path' => 'id',
],
+ [
+ 'name' => __('Status'),
+ 'class' => 'short',
+ 'data_path' => 'status',
+ 'display_field_data_path' => 'name',
+ 'sort' => 'status',
+ 'element' => 'brood_sync_status',
+ ],
[
'name' => __('Name'),
'class' => 'short',
@@ -58,8 +75,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
'pull' => 'right',
'actions' => [
[
- 'url' => '/broods/downloadOrg/' . $brood_id,
- 'url_params_data_paths' => ['id'],
+ 'open_modal' => '/broods/downloadOrg/' . $brood_id . '/[onclick_params_data_path]',
+ 'modal_params_data_path' => 'id',
'title' => __('Download'),
'icon' => 'download'
]
diff --git a/templates/Broods/preview_sharing_groups.php b/templates/Broods/preview_sharing_groups.php
index 5a4cc58..1dac07a 100644
--- a/templates/Broods/preview_sharing_groups.php
+++ b/templates/Broods/preview_sharing_groups.php
@@ -5,6 +5,16 @@ echo $this->element('genericElements/IndexTable/index_table', [
'top_bar' => [
'pull' => 'right',
'children' => [
+ [
+ 'type' => 'simple',
+ 'children' => [
+ 'data' => [
+ 'type' => 'simple',
+ 'text' => __('Download All'),
+ 'popover_url' => sprintf('/broods/downloadSharingGroup/%s/all', h($brood_id)),
+ ]
+ ]
+ ],
[
'type' => 'search',
'button' => __('Search'),
@@ -22,6 +32,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
'class' => 'short',
'data_path' => 'id',
],
+ [
+ 'name' => __('Status'),
+ 'class' => 'short',
+ 'data_path' => 'status',
+ 'sort' => 'status',
+ 'element' => 'brood_sync_status',
+ ],
[
'name' => __('Name'),
'class' => 'short',
@@ -38,8 +55,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
'pull' => 'right',
'actions' => [
[
- 'url' => '/broods/downloadSharingGroup/' . $brood_id,
- 'url_params_data_paths' => ['id'],
+ 'open_modal' => '/broods/downloadSharingGroup/' . $brood_id . '/[onclick_params_data_path]',
+ 'modal_params_data_path' => 'id',
'title' => __('Download'),
'icon' => 'download'
]
diff --git a/templates/Instance/migration_index.php b/templates/Instance/migration_index.php
index 022cc28..68eb7b0 100644
--- a/templates/Instance/migration_index.php
+++ b/templates/Instance/migration_index.php
@@ -9,9 +9,7 @@ if (!empty($updateAvailables)) {
'icon' => 'arrow-alt-circle-up',
'class' => 'mt-1',
'text' => __n('Run update', 'Run all updates', count($updateAvailables)),
- 'params' => [
- 'onclick' => 'runAllUpdate()'
- ]
+ 'onclick' => 'runAllUpdate()',
])
);
echo $this->Bootstrap->alert([
@@ -35,11 +33,11 @@ foreach ($status as $i => &$update) {
echo $this->Bootstrap->table([], [
'fields' => [
- ['key' => 'id', 'label' => __('ID')],
- ['key' => 'name', 'label' => __('Name')],
- ['key' => 'end_time', 'label' => __('End Time')],
- ['key' => 'time_taken_formated', 'label' => __('Time Taken')],
- ['key' => 'status', 'label' => __('Status')]
+ ['path' => 'id', 'label' => __('ID')],
+ ['path' => 'name', 'label' => __('Name')],
+ ['path' => 'end_time', 'label' => __('End Time')],
+ ['path' => 'time_taken_formated', 'label' => __('Time Taken')],
+ ['path' => 'status', 'label' => __('Status')]
],
'items' => $status,
]);
diff --git a/templates/MetaTemplates/index.php b/templates/MetaTemplates/index.php
index 799eedc..d67d859 100644
--- a/templates/MetaTemplates/index.php
+++ b/templates/MetaTemplates/index.php
@@ -18,9 +18,7 @@ if (!empty($updateableTemplates['new'])) {
'size' => 'sm',
'icon' => 'download',
'title' => __('Create this template'),
- 'params' => [
- 'onclick' => "UI.submissionModalForIndex('/metaTemplates/createNewTemplate/{$entry['uuid']}', '/meta-templates')"
- ]
+ 'onclick' => "UI.submissionModalForIndex('/metaTemplates/createNewTemplate/{$entry['uuid']}', '/meta-templates')",
])
);
}, $alertList);
@@ -225,4 +223,3 @@ function getConflictingTemplate($row, $data) {
}
return [];
}
-?>
diff --git a/templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php b/templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php
index 8a7ee11..32252c7 100644
--- a/templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php
+++ b/templates/MetaTemplates/migrate_old_meta_template_to_newest_version_for_entity.php
@@ -71,9 +71,7 @@ use Cake\Routing\Router;
$this->Bootstrap->button([
'text' => __('Update to version {0}', h($newMetaTemplate->version)),
'variant' => 'success',
- 'params' => [
- 'onclick' => 'submitMigration()'
- ]
+ 'onclick' => 'submitMigration()',
])
?>
diff --git a/templates/MetaTemplates/update.php b/templates/MetaTemplates/update.php
index 411e477..473f29a 100644
--- a/templates/MetaTemplates/update.php
+++ b/templates/MetaTemplates/update.php
@@ -51,7 +51,7 @@ if ($updateStatus['up-to-date']) {
'templateOnDisk' => $templateOnDisk,
]);
$bodyHtml .= $this->Bootstrap->collapse([
- 'title' => __('View conflicts'),
+ 'text' => __('View conflicts'),
'open' => false
], $conflictTable);
$bodyHtml .= $this->element('MetaTemplates/conflictResolution', [
diff --git a/templates/Users/login.php b/templates/Users/login.php
index 7317a43..f9de87f 100644
--- a/templates/Users/login.php
+++ b/templates/Users/login.php
@@ -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('%s
', __('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('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]);
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();
}
diff --git a/templates/element/Settings/field.php b/templates/element/Settings/field.php
index d52bc14..ed297f0 100644
--- a/templates/element/Settings/field.php
+++ b/templates/element/Settings/field.php
@@ -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',
diff --git a/templates/element/Settings/fieldGroup.php b/templates/element/Settings/fieldGroup.php
index cd60977..462ae22 100644
--- a/templates/element/Settings/fieldGroup.php
+++ b/templates/element/Settings/fieldGroup.php
@@ -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('%s', 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]));
diff --git a/templates/element/Settings/notice.php b/templates/element/Settings/notice.php
index 9e79877..38c1f64 100644
--- a/templates/element/Settings/notice.php
+++ b/templates/element/Settings/notice.php
@@ -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('%s', $settingID, $settingID, h($name));
+ return sprintf('%s', $settingID, $settingID, h($name));
}],
- ['key' => 'setting-path', 'label' => __('Category'), 'formatter' => function($path, $row) {
+ ['path' => 'setting-path', 'label' => __('Category'), 'formatter' => function($path, $row) {
return '' . h(str_replace('.', ' ▸ ', $path)) . '';
}],
- ['key' => 'value', 'label' => __('Value'), 'formatter' => function($value, $row) {
+ ['path' => 'value', 'label' => __('Value'), 'formatter' => function($value, $row) {
$formatedValue = '';
if (is_null($value)) {
$formatedValue .= '' . __('No value') . '';
@@ -71,7 +71,7 @@ foreach (array_keys($mainNoticeHeading) as $level) {
$formatedValue .= '';
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,
]);
diff --git a/templates/element/Settings/panel.php b/templates/element/Settings/panel.php
index 6fdb0b0..bc37919 100644
--- a/templates/element/Settings/panel.php
+++ b/templates/element/Settings/panel.php
@@ -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',
diff --git a/templates/element/Settings/search.php b/templates/element/Settings/search.php
index ceca462..4fc97da 100644
--- a/templates/element/Settings/search.php
+++ b/templates/element/Settings/search.php
@@ -17,7 +17,7 @@
$(document).ready(function() {
$("#search-settings").select2({
data: selectData,
- placeholder: '= __('Search setting by typing here...') ?>',
+ placeholder: '= __('Search a setting by typing here...') ?>',
templateResult: formatSettingSearchResult,
templateSelection: formatSettingSearchSelection,
matcher: settingMatcher,
diff --git a/templates/element/UserSettings/saved-bookmarks.php b/templates/element/UserSettings/saved-bookmarks.php
index c3b5a5f..8671c82 100644
--- a/templates/element/UserSettings/saved-bookmarks.php
+++ b/templates/element/UserSettings/saved-bookmarks.php
@@ -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('%s', 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),
]);
}],
],
diff --git a/templates/element/genericElements/Form/formLayouts/formDefault.php b/templates/element/genericElements/Form/formLayouts/formDefault.php
index 1658889..5561edd 100644
--- a/templates/element/genericElements/Form/formLayouts/formDefault.php
+++ b/templates/element/genericElements/Form/formLayouts/formDefault.php
@@ -22,9 +22,9 @@
],
[
[
- '_open' => true,
+ 'open' => true,
'header' => [
- 'title' => __('Meta fields')
+ 'text' => __('Meta fields')
],
'body' => $metaTemplateString,
],
diff --git a/templates/element/genericElements/Form/formLayouts/formRaw.php b/templates/element/genericElements/Form/formLayouts/formRaw.php
index 8b66bae..05be8b8 100644
--- a/templates/element/genericElements/Form/formLayouts/formRaw.php
+++ b/templates/element/genericElements/Form/formLayouts/formRaw.php
@@ -15,9 +15,9 @@
],
[
[
- '_open' => true,
+ 'open' => true,
'header' => [
- 'title' => __('Meta fields')
+ 'text' => __('Meta fields')
],
'body' => $metaTemplateString,
],
diff --git a/templates/element/genericElements/Form/metaTemplateForm.php b/templates/element/genericElements/Form/metaTemplateForm.php
index f52cbb2..5331cf9 100644
--- a/templates/element/genericElements/Form/metaTemplateForm.php
+++ b/templates/element/genericElements/Form/metaTemplateForm.php
@@ -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;
\ No newline at end of file
diff --git a/templates/element/genericElements/IndexTable/Fields/brood_sync_status.php b/templates/element/genericElements/IndexTable/Fields/brood_sync_status.php
new file mode 100644
index 0000000..018c585
--- /dev/null
+++ b/templates/element/genericElements/IndexTable/Fields/brood_sync_status.php
@@ -0,0 +1,171 @@
+Hash->extract($row, $field['data_path']);
+$displayField = $this->Hash->get($row, $field['display_field_data_path']);
+
+if ($status['local'] && $status['up_to_date']) {
+ $variant = 'success';
+ $text = __('Ok');
+} else if ($status['local'] && !$status['up_to_date']) {
+ $variant = 'warning';
+ $text = __('Outdated');
+} else {
+ $variant = 'danger';
+ $text = __('N/A');
+}
+
+echo $this->Bootstrap->badge([
+ 'id' => $seed,
+ 'variant' => $variant,
+ 'text' => $text,
+ 'icon' => ($status['local'] && !$status['up_to_date']) ? 'question-circle' : false,
+ 'title' => $status['title'],
+ 'class' => [
+ (($status['local'] && !$status['up_to_date']) ? 'cursor-pointer' : ''),
+ ],
+]);
+?>
+
+
+
+
\ No newline at end of file
diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php
index 123da84..dcaff63 100644
--- a/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php
+++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php
@@ -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']
diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php
index bd0d43b..11e2a2b 100644
--- a/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php
+++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_timestamp.php
@@ -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(
'%s %s
',
__('{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(
'%s %s
',
__('{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('%s%s
', $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']
diff --git a/templates/element/genericElements/IndexTable/index_table.php b/templates/element/genericElements/IndexTable/index_table.php
index 956fcf8..b722aa0 100644
--- a/templates/element/genericElements/IndexTable/index_table.php
+++ b/templates/element/genericElements/IndexTable/index_table.php
@@ -14,6 +14,7 @@ use Cake\Utility\Text;
* ),
* '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)
* ));
@@ -48,7 +49,7 @@ if (!empty($data['title'])) {
'help' => $this->Bootstrap->icon('info', [
'class' => ['fs-6', 'align-text-top',],
'title' => empty($data['description']) ? '' : h($data['description']),
- 'params' => [
+ 'attrs' => [
'data-bs-toggle' => 'tooltip',
]
]),
diff --git a/templates/element/genericElements/ListTopBar/group_multi_select_actions.php b/templates/element/genericElements/ListTopBar/group_multi_select_actions.php
index 382781d..75f77ea 100644
--- a/templates/element/genericElements/ListTopBar/group_multi_select_actions.php
+++ b/templates/element/genericElements/ListTopBar/group_multi_select_actions.php
@@ -7,7 +7,7 @@
'text' => $child['text'],
'outline' => !empty($child['outline']),
'icon' => $child['icon'] ?? null,
- 'params' => array_merge([
+ 'attrs' => array_merge([
'data-onclick-function' => $child['onclick'] ?? '',
'data-table-random-value' => $tableRandomValue,
'onclick' => 'multiActionClickHandler(this)'
diff --git a/templates/element/genericElements/ListTopBar/group_search.php b/templates/element/genericElements/ListTopBar/group_search.php
index c2743f1..0cfb2f9 100644
--- a/templates/element/genericElements/ListTopBar/group_search.php
+++ b/templates/element/genericElements/ListTopBar/group_search.php
@@ -31,10 +31,8 @@
$buttonConfig = [
'icon' => 'filter',
'variant' => $numberActiveFilters > 0 ? 'warning' : 'primary',
- 'params' => [
- 'title' => __('Filter index'),
- 'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
- ]
+ 'title' => __('Filter index'),
+ 'id' => sprintf('toggleFilterButton-%s', h($tableRandomValue))
];
if (count($activeFilters) > 0) {
$buttonConfig['badge'] = [
diff --git a/templates/element/genericElements/ListTopBar/group_table_action.php b/templates/element/genericElements/ListTopBar/group_table_action.php
index d239d82..25e130c 100644
--- a/templates/element/genericElements/ListTopBar/group_table_action.php
+++ b/templates/element/genericElements/ListTopBar/group_table_action.php
@@ -71,14 +71,14 @@ $numberOfElementHtml = $this->element('/genericElements/ListTopBar/group_table_a
'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'],
],
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php b/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
index 8e14c6d..f826422 100644
--- a/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
+++ b/templates/element/genericElements/ListTopBar/group_table_action/hiddenColumns.php
@@ -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;
diff --git a/templates/element/genericElements/ListTopBar/group_table_action/hiddenMetaColumns.php b/templates/element/genericElements/ListTopBar/group_table_action/hiddenMetaColumns.php
index 8415dce..fa3e98d 100644
--- a/templates/element/genericElements/ListTopBar/group_table_action/hiddenMetaColumns.php
+++ b/templates/element/genericElements/ListTopBar/group_table_action/hiddenMetaColumns.php
@@ -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;
diff --git a/templates/element/genericElements/SingleViews/metafields_panel.php b/templates/element/genericElements/SingleViews/metafields_panel.php
index d7f2e19..82b24be 100644
--- a/templates/element/genericElements/SingleViews/metafields_panel.php
+++ b/templates/element/genericElements/SingleViews/metafields_panel.php
@@ -1,10 +1,12 @@
[],
'content' => []
];
+$viewElementCandidatePath = '/genericElements/SingleViews/Fields/';
foreach($data['MetaTemplates'] as $metaTemplate) {
if (!empty($metaTemplate->meta_template_fields)) {
$tabData['navs'][] = [
@@ -15,9 +17,17 @@ 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,
+ // 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,
'warning' => $metaField->warning ?? null,
'info' => $metaField->info ?? null,
'danger' => $metaField->danger ?? null
@@ -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',
diff --git a/templates/element/layouts/header/header-notifications.php b/templates/element/layouts/header/header-notifications.php
index e18b619..fd8f942 100644
--- a/templates/element/layouts/header/header-notifications.php
+++ b/templates/element/layouts/header/header-notifications.php
@@ -25,6 +25,7 @@ $variant = array_flip($severity)[$maxSeverity];
if ($hasNotification) {
echo $this->Bootstrap->notificationBubble([
'variant' => $variant,
+ 'borderVariant' => 'light',
]);
}
?>
diff --git a/templates/element/layouts/header/header-profile.php b/templates/element/layouts/header/header-profile.php
index 197eb8e..d0d3bb7 100644
--- a/templates/element/layouts/header/header-profile.php
+++ b/templates/element/layouts/header/header-profile.php
@@ -20,14 +20,14 @@ use Cake\Routing\Router;
-
+
= __('My Account') ?>
-
+
= __('Account Settings') ?>
"
>
SocialProvider->getIcon($this->request->getAttribute('identity')))): ?>
- = $this->SocialProvider->getIcon($this->request->getAttribute('identity')) ?>
+ = $this->SocialProvider->getIcon($this->request->getAttribute('identity'), ['me-1']) ?>
-
+
= __('SSO Account') ?>
-
+
= __('Logout') ?>
diff --git a/templates/element/layouts/sidebar/bookmark-add.php b/templates/element/layouts/sidebar/bookmark-add.php
index 69dd353..d68d449 100644
--- a/templates/element/layouts/sidebar/bookmark-add.php
+++ b/templates/element/layouts/sidebar/bookmark-add.php
@@ -6,7 +6,5 @@ echo $this->Bootstrap->button([
'variant' => 'primary',
'size' => 'sm',
'class' => 'mb-1',
- 'params' => [
- 'id' => 'btn-add-bookmark',
- ]
+ 'id' => 'btn-add-bookmark',
]);
diff --git a/templates/element/layouts/sidebar/bookmark-entry.php b/templates/element/layouts/sidebar/bookmark-entry.php
index a33ffe4..68b8d4f 100644
--- a/templates/element/layouts/sidebar/bookmark-entry.php
+++ b/templates/element/layouts/sidebar/bookmark-entry.php
@@ -28,7 +28,7 @@
'size' => 'sm',
'icon' => h($icon),
'class' => ['mb-1', !$validURI ? 'disabled' : ''],
- 'params' => [
+ 'attrs' => [
'href' => $validURI ? h($url) : '#',
]
]);
diff --git a/templates/element/layouts/sidebar/entry.php b/templates/element/layouts/sidebar/entry.php
index 81949a1..9c0bdab 100644
--- a/templates/element/layouts/sidebar/entry.php
+++ b/templates/element/layouts/sidebar/entry.php
@@ -71,6 +71,7 @@
if ($childHasNotification || ($hasNotification && !empty($children))) {
echo $this->Bootstrap->notificationBubble([
'variant' => $childHasNotification ? $childNotificationVariant : $notificationVariant,
+ 'borderVariant' => 'light',
]);
}
?>
diff --git a/templates/element/widgets/highlight-panel.php b/templates/element/widgets/highlight-panel.php
index bbb1645..ef442ba 100644
--- a/templates/element/widgets/highlight-panel.php
+++ b/templates/element/widgets/highlight-panel.php
@@ -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' : '')]
diff --git a/templates/genericTemplates/confirm.php b/templates/genericTemplates/confirm.php
index 6428f6c..9910adf 100644
--- a/templates/genericTemplates/confirm.php
+++ b/templates/genericTemplates/confirm.php
@@ -1,28 +1,39 @@
-
-
+element('genericElements/Form/genericForm', [
+ 'entity' => null,
+ 'ajax' => false,
+ 'raw' => true,
+ 'data' => [
+ 'fields' => [
+ ],
+ 'submit' => [
+ 'action' => $this->request->getParam('action')
+ ]
+ ]
+]);
+$formHTML = sprintf('%s
', $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);
+?>
diff --git a/templates/genericTemplates/delete.php b/templates/genericTemplates/delete.php
index de45ab6..763750a 100644
--- a/templates/genericTemplates/delete.php
+++ b/templates/genericTemplates/delete.php
@@ -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,
]);
?>
diff --git a/templates/genericTemplates/filters.php b/templates/genericTemplates/filters.php
index a193721..858d5c3 100644
--- a/templates/genericTemplates/filters.php
+++ b/templates/genericTemplates/filters.php
@@ -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('%s', 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,7 +41,7 @@ $filteringForm = $this->Bootstrap->table(
}
],
[
- 'key' => 'value',
+ 'path' => 'value',
'labelHtml' => sprintf(
'%s %s',
__('Value'),
@@ -71,23 +71,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).
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 +104,9 @@ echo $this->Bootstrap->modal([
'size' => !empty($metaFieldsEnabled) ? 'xl' : 'lg',
'type' => 'confirm',
'bodyHtml' => $modalBody,
- 'confirmText' => __('Filter'),
+ 'confirmButton' => [
+ 'text' => __('Filter'),
+ ],
'confirmFunction' => 'filterIndex'
]);
?>
diff --git a/webroot/css/layout.css b/webroot/css/layout.css
index 1afecec..f7f57c2 100644
--- a/webroot/css/layout.css
+++ b/webroot/css/layout.css
@@ -203,7 +203,8 @@ main.content {
margin: auto 0;
}
-.right-navbar .header-menu .dropdown-menu a.dropdown-item > i {
+.right-navbar .header-menu .dropdown-menu a.dropdown-item > i,
+.right-navbar .header-menu .dropdown-menu a.dropdown-item > img {
min-width: 25px;
}
diff --git a/webroot/css/themes/additional/bootstrap-additional.css b/webroot/css/themes/additional/bootstrap-additional.css
index 2654c1e..af9a36c 100644
--- a/webroot/css/themes/additional/bootstrap-additional.css
+++ b/webroot/css/themes/additional/bootstrap-additional.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/css/themes/theme-darkly.css b/webroot/css/themes/theme-darkly.css
index f0ab02c..34e902b 100644
--- a/webroot/css/themes/theme-darkly.css
+++ b/webroot/css/themes/theme-darkly.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/css/themes/theme-default.css b/webroot/css/themes/theme-default.css
index 632df9b..56e4cd5 100644
--- a/webroot/css/themes/theme-default.css
+++ b/webroot/css/themes/theme-default.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/css/themes/theme-flatly.css b/webroot/css/themes/theme-flatly.css
index a136080..b0dbfc1 100644
--- a/webroot/css/themes/theme-flatly.css
+++ b/webroot/css/themes/theme-flatly.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/css/themes/theme-minty.css b/webroot/css/themes/theme-minty.css
index f118a7d..421f575 100644
--- a/webroot/css/themes/theme-minty.css
+++ b/webroot/css/themes/theme-minty.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(243, 156, 18, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/css/themes/theme-quartz.css b/webroot/css/themes/theme-quartz.css
index 8853db3..45e7268 100644
--- a/webroot/css/themes/theme-quartz.css
+++ b/webroot/css/themes/theme-quartz.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/css/themes/theme-slate.css b/webroot/css/themes/theme-slate.css
index be86d33..cb59b58 100644
--- a/webroot/css/themes/theme-slate.css
+++ b/webroot/css/themes/theme-slate.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(248, 148, 6, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/css/themes/theme-vapor.css b/webroot/css/themes/theme-vapor.css
index be21737..d44b6ca 100644
--- a/webroot/css/themes/theme-vapor.css
+++ b/webroot/css/themes/theme-vapor.css
@@ -344,6 +344,10 @@
box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25);
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
.mw-75 {
max-width: 75% !important;
diff --git a/webroot/js/api-helper.js b/webroot/js/api-helper.js
index 24d34cc..d01a941 100644
--- a/webroot/js/api-helper.js
+++ b/webroot/js/api-helper.js
@@ -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 {
diff --git a/webroot/js/bootstrap-helper.js b/webroot/js/bootstrap-helper.js
index 173fdca..94aa6f2 100644
--- a/webroot/js/bootstrap-helper.js
+++ b/webroot/js/bootstrap-helper.js
@@ -257,6 +257,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 +274,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 +287,7 @@ class Toaster {
body: false,
variant: 'default',
autohide: true,
- delay: 5000,
+ delay: 'auto',
titleHtml: false,
mutedHtml: false,
bodyHtml: false,
@@ -389,6 +392,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 */
@@ -400,15 +409,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: 'POSTSuccessCallback can only be used in conjuction with the rawHtml option. Instead, use the promise instead returned by the API call in APIConfirm.'
})
}
}
+ if (this.options.rawHtml) {
+ this.attachSubmitButtonListener = true
+ }
if (options.type === undefined && options.cancel !== undefined) {
this.options.type = 'confirm'
}
@@ -619,6 +629,9 @@ class ModalFactory {
}
} else {
$modalDialog = $('')
+ if (this.options.size !== false) {
+ $modalDialog.addClass(`modal-${this.options.size}`)
+ }
const $modalContent = $('')
if (this.options.title !== false || this.options.titleHtml !== false) {
const $modalHeader = $('')
@@ -794,17 +807,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) => {
@@ -814,23 +835,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))
@@ -877,7 +903,7 @@ class OverlayFactory {
spinnerVariant: '',
spinnerSmall: false,
spinnerType: 'border',
- fallbackBoostrapVariant: '',
+ fallbackBootstrapVariant: '',
wrapperCSSDisplay: '',
}
@@ -976,7 +1002,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
}
}
@@ -985,7 +1011,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]+-(?primary|success|danger|warning|info|light|dark|white|transparent)$/;
let result
for (let i=0; i')
+ let $caption = null
if (options.caption) {
+ $caption = $('')
if (options.caption instanceof jQuery) {
$caption = options.caption
} else {
@@ -1111,21 +1141,28 @@ class HtmlHelper {
}
}
- const $theadRow = $('
')
- head.forEach(head => {
- if (head instanceof jQuery) {
- $theadRow.append($(' | ').append(head))
- } else {
- $theadRow.append($(' | ').text(head))
- }
- })
- $thead.append($theadRow)
+ let $theadRow = null
+ if (head) {
+ $theadRow = $('
')
+ head.forEach(head => {
+ if (head instanceof jQuery) {
+ $theadRow.append($(' | ').append(head))
+ } else {
+ $theadRow.append($(' | ').text(head))
+ }
+ })
+ $thead.append($theadRow)
+ }
body.forEach(row => {
const $bodyRow = $('
')
row.forEach(item => {
if (item instanceof jQuery) {
- $bodyRow.append($(' | ').append(item))
+ if (item.is('td')) {
+ $bodyRow.append(item)
+ } else {
+ $bodyRow.append($(' | ').append(item))
+ }
} else {
$bodyRow.append($(' | ').text(item))
}
diff --git a/webroot/theme/scss/additional/bootstrap-additional.scss b/webroot/theme/scss/additional/bootstrap-additional.scss
index 6c0675d..339b9fa 100644
--- a/webroot/theme/scss/additional/bootstrap-additional.scss
+++ b/webroot/theme/scss/additional/bootstrap-additional.scss
@@ -122,6 +122,10 @@ $toast-color-level: 70% !default;
}
}
+.table-xs > :not(caption) > * > * {
+ padding: 0.05rem 0.05rem;
+}
+
/* Utilities */
// Use bootstrap's utilities API with something like this
// $utilities: map-merge(