new: [app] Lots of new helpers for views, js and genericElements
parent
d62639667b
commit
79e34052c8
|
@ -198,7 +198,8 @@ class CRUDComponent extends Component
|
|||
$patchEntityParams['fields'] = $params['fields'];
|
||||
}
|
||||
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
|
||||
if ($this->Table->save($data)) {
|
||||
$savedData = $this->Table->save($data);
|
||||
if ($savedData !== false) {
|
||||
$message = __('{0} updated.', $this->ObjectAlias);
|
||||
if (!empty($input['metaFields'])) {
|
||||
$this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $data->id]);
|
||||
|
@ -206,6 +207,8 @@ class CRUDComponent extends Component
|
|||
}
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'edit', $savedData, $message);
|
||||
} else {
|
||||
$this->Controller->Flash->success($message);
|
||||
if (empty($params['redirect'])) {
|
||||
|
@ -222,7 +225,8 @@ class CRUDComponent extends Component
|
|||
empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage)
|
||||
);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $data, $message, $validationMessage);
|
||||
} else {
|
||||
$this->Controller->Flash->error($message);
|
||||
}
|
||||
|
@ -410,18 +414,18 @@ class CRUDComponent extends Component
|
|||
$data = $this->Table->get($id, $params);
|
||||
if ($this->request->is(['post', 'put'])) {
|
||||
$data->{$fieldName} = !$data->{$fieldName};
|
||||
$data = $this->Table->save($data);
|
||||
if ($data !== false) {
|
||||
$message = __('{0}\'s `{1}` field: {2}. (ID: {3})',
|
||||
$this->ObjectAlias,
|
||||
$savedData = $this->Table->save($data);
|
||||
if ($savedData !== false) {
|
||||
$message = __('{0} field {1}. (ID: {2} {3})',
|
||||
$fieldName,
|
||||
$data->{$fieldName} ? __('enabled') : __('disabled'),
|
||||
$data->id,
|
||||
Inflector::humanize($this->ObjectAlias),
|
||||
$data->id
|
||||
);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'toggle', $data, $message);
|
||||
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'toggle', $savedData, $message);
|
||||
} else {
|
||||
$this->Controller->Flash->success($message);
|
||||
if (empty($params['redirect'])) {
|
||||
|
@ -439,7 +443,7 @@ class CRUDComponent extends Component
|
|||
);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $data, $message);
|
||||
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'toggle', $message, $validationMessage);
|
||||
} else {
|
||||
$this->Controller->Flash->error($message);
|
||||
if (empty($params['redirect'])) {
|
||||
|
|
|
@ -426,17 +426,19 @@ class RestResponseComponent extends Component
|
|||
$response = [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $entity->toArray(),
|
||||
'url' => $this->__generateURL($action, $ObjectAlias, $entity->id)
|
||||
];
|
||||
return $this->viewData($response);
|
||||
}
|
||||
|
||||
public function ajaxFailResponse($ObjectAlias, $action, $entity, $message)
|
||||
public function ajaxFailResponse($ObjectAlias, $action, $entity, $message, $errors = [])
|
||||
{
|
||||
$action = $this->__dissectAdminRouting($action);
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
'errors' => $errors,
|
||||
'url' => $this->__generateURL($action, $ObjectAlias, $entity->id)
|
||||
];
|
||||
return $this->viewData($response);
|
||||
|
|
|
@ -49,6 +49,8 @@ class IndividualsController extends AppController
|
|||
$this->CRUD->edit($id);
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->restResponsePayload;
|
||||
} else if($this->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) {
|
||||
return $this->ajaxResponsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
$this->render('add');
|
||||
|
|
|
@ -70,9 +70,9 @@ class MetaTemplatesController extends AppController
|
|||
$this->set('metaGroup', 'Administration');
|
||||
}
|
||||
|
||||
public function toggle($id)
|
||||
public function toggle($id, $fieldName = 'enabled')
|
||||
{
|
||||
$this->CRUD->toggle($id);
|
||||
$this->CRUD->toggle($id, $fieldName);
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->restResponsePayload;
|
||||
} else if($this->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) {
|
||||
|
|
|
@ -11,4 +11,9 @@ class HashHelper extends Helper
|
|||
{
|
||||
return Hash::extract($target, $extraction_string);
|
||||
}
|
||||
|
||||
public function get($target, $extraction_string)
|
||||
{
|
||||
return Hash::get($target, $extraction_string);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper;
|
||||
|
||||
use Cake\View\Helper;
|
||||
use Cake\Utility\Hash;
|
||||
|
||||
class StringFromPathHelper extends Helper
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'sanitize' => true,
|
||||
'highlight' => false,
|
||||
];
|
||||
|
||||
public function buildStringFromDataPath(String $str, $data=[], array $dataPaths=[], array $options=[])
|
||||
{
|
||||
$options = array_merge($this->defaultOptions, $options);
|
||||
if (!empty($dataPaths)) {
|
||||
$extractedVars = [];
|
||||
foreach ($dataPaths as $i => $dataPath) {
|
||||
if (is_array($dataPath)) {
|
||||
$varValue = '';
|
||||
if (!empty($dataPath['datapath'])) {
|
||||
$varValue = Hash::get($data, $dataPath['datapath']);
|
||||
} else if (!empty($dataPath['raw'])) {
|
||||
$varValue = $dataPath['raw'];
|
||||
}
|
||||
$extractedVars[] = $varValue;
|
||||
} else {
|
||||
$extractedVars[] = Hash::get($data, $dataPath);
|
||||
}
|
||||
}
|
||||
foreach ($extractedVars as $i => $value) {
|
||||
$value = $options['sanitize'] ? h($value) : $value;
|
||||
$value = $options['highlight'] ? "<span class=\"font-weight-light\">${value}</span>" : $value;
|
||||
$str = str_replace(
|
||||
"{{{$i}}}",
|
||||
$value,
|
||||
$str
|
||||
);
|
||||
}
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
}
|
|
@ -28,13 +28,53 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'sort' => 'enabled',
|
||||
'data_path' => 'enabled',
|
||||
'element' => 'toggle',
|
||||
'url' => '/metaTemplates/toggle',
|
||||
'url_params_data_paths' => ['id'],
|
||||
'toggle_requirement' => [
|
||||
'url' => '/metaTemplates/toggle/{{0}}',
|
||||
'url_params_vars' => ['id'],
|
||||
'toggle_data' => [
|
||||
'requirement' => [
|
||||
'function' => function($row, $options) {
|
||||
return true;
|
||||
}
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => 'Default',
|
||||
'sort' => 'is_default',
|
||||
'data_path' => 'is_default',
|
||||
'element' => 'toggle',
|
||||
'url' => '/metaTemplates/toggle/{{0}}/{{1}}',
|
||||
'url_params_vars' => [['datapath' => 'id'], ['raw' => 'is_default']],
|
||||
'toggle_data' => [
|
||||
'requirement' => [
|
||||
'function' => function($row, $options) {
|
||||
return true;
|
||||
}
|
||||
],
|
||||
'confirm' => [
|
||||
'enable' => [
|
||||
'titleHtml' => __('Make {{0}} the default template?'),
|
||||
'titleHtml_vars' => ['name'],
|
||||
'bodyHtml' => $this->Html->nestedList([
|
||||
__('Only one template per scope can be set as the default template'),
|
||||
__('Current scope: {{0}}'),
|
||||
]),
|
||||
'bodyHtml_vars' => ['scope'],
|
||||
'type' => 'confirm-warning',
|
||||
'confirmText' => __('Yes, set as default')
|
||||
],
|
||||
'disable' => [
|
||||
'titleHtml' => __('Remove {{0}} as the default template?'),
|
||||
'titleHtml_vars' => ['name'],
|
||||
'bodyHtml' => $this->Html->nestedList([
|
||||
__('Current scope: {{0}}'),
|
||||
]),
|
||||
'bodyHtml_vars' => ['scope'],
|
||||
'type' => 'confirm-warning',
|
||||
'confirmText' => __('Yes, do not set as default')
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => __('Scope'),
|
||||
|
|
|
@ -20,6 +20,16 @@ echo $this->element(
|
|||
'key' => __('Description'),
|
||||
'path' => 'description'
|
||||
],
|
||||
[
|
||||
'key' => __('Enabled'),
|
||||
'path' => 'enabled',
|
||||
'type' => 'boolean'
|
||||
],
|
||||
[
|
||||
'key' => __('is_default'),
|
||||
'path' => 'is_default',
|
||||
'type' => 'boolean'
|
||||
],
|
||||
[
|
||||
'key' => __('Version'),
|
||||
'path' => 'version'
|
||||
|
|
|
@ -122,10 +122,11 @@
|
|||
$data['description']
|
||||
),
|
||||
$fieldsString,
|
||||
empty($metaFieldString) ? '' : $this->element(
|
||||
empty($metaTemplateString) ? '' : $this->element(
|
||||
'genericElements/accordion_scaffold', [
|
||||
'body' => $metaFieldString,
|
||||
'title' => 'Meta fields'
|
||||
'body' => $metaTemplateString,
|
||||
'title' => 'Meta fields',
|
||||
'class' => 'mb-2'
|
||||
]
|
||||
),
|
||||
$this->element('genericElements/Form/submitButton', $submitButtonData),
|
||||
|
|
|
@ -6,82 +6,85 @@
|
|||
* to fetch it.
|
||||
*
|
||||
*/
|
||||
$data = $this->Hash->extract($row, $field['data_path']);
|
||||
$data = $this->Hash->get($row, $field['data_path']);
|
||||
$seed = rand();
|
||||
$checkboxId = 'GenericToggle-' . $seed;
|
||||
$tempboxId = 'TempBox-' . $seed;
|
||||
|
||||
$requirementMet = true;
|
||||
if (isset($field['toggle_requirement'])) {
|
||||
if (isset($field['toggle_requirement']['options']['datapath'])) {
|
||||
foreach ($field['toggle_requirement']['options']['datapath'] as $name => $path) {
|
||||
$field['toggle_requirement']['options']['datapath'][$name] = empty($this->Hash->extract($row, $path)[0]) ? null : $this->Hash->extract($row, $path)[0];
|
||||
if (isset($field['toggle_data']['requirement'])) {
|
||||
if (isset($field['toggle_data']['requirement']['options']['datapath'])) {
|
||||
foreach ($field['toggle_data']['requirement']['options']['datapath'] as $name => $path) {
|
||||
$field['toggle_data']['requirement']['options']['datapath'][$name] = empty($this->Hash->extract($row, $path)[0]) ? null : $this->Hash->extract($row, $path)[0];
|
||||
}
|
||||
}
|
||||
$options = isset($field['toggle_requirement']['options']) ? $field['toggle_requirement']['options'] : array();
|
||||
$requirementMet = $field['toggle_requirement']['function']($row, $options);
|
||||
$options = isset($field['toggle_data']['requirement']['options']) ? $field['toggle_data']['requirement']['options'] : array();
|
||||
$requirementMet = $field['toggle_data']['requirement']['function']($row, $options);
|
||||
}
|
||||
|
||||
echo sprintf(
|
||||
'<input type="checkbox" id="%s" %s %s><span id="%s" class="d-none">',
|
||||
'<input type="checkbox" id="%s" %s %s><span id="%s" class="d-none"></span>',
|
||||
$checkboxId,
|
||||
empty($data[0]) ? '' : 'checked',
|
||||
empty($data) ? '' : 'checked',
|
||||
$requirementMet ? '' : 'disabled="disabled"',
|
||||
$tempboxId
|
||||
);
|
||||
|
||||
// inject title and body vars into their placeholder
|
||||
if (!empty($field['toggle_data']['confirm'])) {
|
||||
$availableConfirmOptions = ['enable', 'disable'];
|
||||
$confirmOptions = $field['toggle_data']['confirm'];
|
||||
foreach ($availableConfirmOptions as $optionType) {
|
||||
$availableType = ['title', 'titleHtml', 'body', 'bodyHtml'];
|
||||
foreach ($availableType as $varType) {
|
||||
if (!isset($confirmOptions[$optionType][$varType])) {
|
||||
continue;
|
||||
}
|
||||
$confirmOptions[$optionType][$varType] = $this->StringFromPath->buildStringFromDataPath(
|
||||
$confirmOptions[$optionType][$varType],
|
||||
$row,
|
||||
$confirmOptions[$optionType][$varType . '_vars'],
|
||||
['highlight' => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$url = $this->StringFromPath->buildStringFromDataPath($field['url'], $row, $field['url_params_vars']);
|
||||
?>
|
||||
|
||||
<?php if ($requirementMet): ?>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var url = "<?= h($field['url']) ?>";
|
||||
<?php
|
||||
if (!empty($field['url_params_data_paths'][0])) {
|
||||
$id = $this->Hash->extract($row, $field['url_params_data_paths'][0]);
|
||||
echo 'url = url + "/' . h($id[0]) . '";';
|
||||
}
|
||||
?>
|
||||
(function() {
|
||||
const url = "<?= h($url) ?>"
|
||||
const confirmationOptions = <?= isset($confirmOptions) ? json_encode($confirmOptions) : 'false' ?>;
|
||||
$('#<?= $checkboxId ?>').click(function(evt) {
|
||||
evt.preventDefault();
|
||||
$.ajax({
|
||||
type:"get",
|
||||
url: url,
|
||||
error:function() {
|
||||
showToast({
|
||||
variant: 'danger',
|
||||
title: '<?= __('Could not retrieve current state.') ?>.'
|
||||
evt.preventDefault()
|
||||
if(confirmationOptions !== false) {
|
||||
const correctOptions = $('#<?= $checkboxId ?>').prop('checked') ? confirmationOptions['enable'] : confirmationOptions['disable'] // Adjust modal option based on checkbox state
|
||||
const modalOptions = {
|
||||
...correctOptions,
|
||||
APIConfirm: (tmpApi) => {
|
||||
return submitForm(tmpApi, url)
|
||||
.catch(e => {
|
||||
// Provide feedback inside modal?
|
||||
})
|
||||
},
|
||||
success: function (data, textStatus) {
|
||||
$('#<?= $tempboxId ?>').html(data);
|
||||
var $form = $('#<?= $tempboxId ?>').find('form');
|
||||
$.ajax({
|
||||
data: $form.serialize(),
|
||||
cache: false,
|
||||
type:"post",
|
||||
url: $form.attr('action'),
|
||||
success:function(data, textStatus) {
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: data.message
|
||||
}
|
||||
UI.modal(modalOptions)
|
||||
} else {
|
||||
const tmpApi = new AJAXApi({
|
||||
statusNode: $('#<?= $checkboxId ?>')[0]
|
||||
})
|
||||
if (data.success) {
|
||||
$('#<?= $checkboxId ?>').prop('checked', !$('#<?= $checkboxId ?>').prop('checked'));
|
||||
submitForm(tmpApi, url)
|
||||
}
|
||||
},
|
||||
error:function() {
|
||||
showToast({
|
||||
variant: 'danger',
|
||||
title: data.message
|
||||
})
|
||||
},
|
||||
complete:function() {
|
||||
$('#<?= $tempboxId ?>').empty();
|
||||
|
||||
function submitForm(api, url) {
|
||||
return api.fetchAndPostForm(url, {})
|
||||
.then(() => {
|
||||
reloadElement('/meta-templates', $('#table-container-<?= $tableRandomValue ?>'), $('#table-container-<?= $tableRandomValue ?> table.table'))
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}())
|
||||
</script>
|
||||
<?php endif; ?>
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
$randomId = Cake\Utility\Security::randomString(8);
|
||||
?>
|
||||
<div id="accordion">
|
||||
<div id="accordion" class="<?= !empty($class) ? $class : '' ?>">
|
||||
<div class="card">
|
||||
<div class="card-header" id="heading-<?= $randomId ?>">
|
||||
<h5 class="mb0"><a href="#" class="btn btn-link" data-toggle="collapse" data-target="#view-child-<?= $randomId ?>" aria-expanded="true" aria-controls="collapseOne"><?= h($title) ?></a></h5>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<?= $this->Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id], ['confirm' => __('Are you sure you want to toggle {0} of {1}?', $fieldName. $entity->id)]) ?>
|
||||
<?= $this->Form->postLink(__('Toggle'), ['action' => 'toggle', $entity->id, $fieldName], ['confirm' => __('Are you sure you want to toggle {0} of {1}?', $fieldName. $entity->id)]) ?>
|
|
@ -40,6 +40,7 @@ $cakeDescription = 'Cerebrate';
|
|||
<?= $this->Html->script('bootstrap.bundle.js') ?>
|
||||
<?= $this->Html->script('main.js') ?>
|
||||
<?= $this->Html->script('bootstrap-helper.js') ?>
|
||||
<?= $this->Html->script('api-helper.js') ?>
|
||||
<?= $this->fetch('meta') ?>
|
||||
<?= $this->fetch('css') ?>
|
||||
<?= $this->fetch('script') ?>
|
||||
|
@ -66,5 +67,6 @@ $cakeDescription = 'Cerebrate';
|
|||
</main>
|
||||
<div id="mainModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mediumModalLabel" aria-hidden="true"></div>
|
||||
<div id="mainToastContainer" style="position: absolute; top: 15px; right: 15px;"></div>
|
||||
<div id="mainModalContainer"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
class AJAXApi {
|
||||
static genericRequestHeaders = new Headers({
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
});
|
||||
static genericRequestConfigGET = {
|
||||
headers: AJAXApi.genericRequestHeaders
|
||||
}
|
||||
static genericRequestConfigPOST = {
|
||||
headers: AJAXApi.genericRequestHeaders,
|
||||
redirect: 'manual',
|
||||
method: 'POST',
|
||||
}
|
||||
|
||||
static defaultOptions = {
|
||||
showToast: true,
|
||||
statusNode: false
|
||||
}
|
||||
options = {}
|
||||
loadingOverlay = false
|
||||
|
||||
constructor(options) {
|
||||
this.mergeOptions(AJAXApi.defaultOptions)
|
||||
this.mergeOptions(options)
|
||||
}
|
||||
|
||||
provideFeedback(options) {
|
||||
if (this.options.showToast) {
|
||||
UI.toast(options)
|
||||
} else {
|
||||
console.error(options.body)
|
||||
}
|
||||
}
|
||||
|
||||
mergeOptions(newOptions) {
|
||||
this.options = Object.assign({}, this.options, newOptions)
|
||||
}
|
||||
|
||||
static mergeFormData(formData, dataToMerge) {
|
||||
for (const [fieldName, value] of Object.entries(dataToMerge)) {
|
||||
formData.set(fieldName, value)
|
||||
}
|
||||
return formData
|
||||
}
|
||||
|
||||
async fetchURL(url, skipRequestHooks=false) {
|
||||
if (!skipRequestHooks) {
|
||||
this.beforeRequest()
|
||||
}
|
||||
let toReturn
|
||||
try {
|
||||
const response = await fetch(url, AJAXApi.genericRequestConfigGET);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.text();
|
||||
toReturn = data;
|
||||
} catch (error) {
|
||||
this.provideFeedback({
|
||||
variant: 'danger',
|
||||
title: 'There has been a problem with the operation',
|
||||
body: error
|
||||
});
|
||||
toReturn = Promise.reject(error);
|
||||
} finally {
|
||||
if (!skipRequestHooks) {
|
||||
this.afterRequest()
|
||||
}
|
||||
}
|
||||
return toReturn
|
||||
}
|
||||
|
||||
async fetchForm(url, skipRequestHooks=false) {
|
||||
if (!skipRequestHooks) {
|
||||
this.beforeRequest()
|
||||
}
|
||||
let toReturn
|
||||
try {
|
||||
const response = await fetch(url, AJAXApi.genericRequestConfigGET);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const formHtml = await response.text();
|
||||
let tmpNode = document.createElement("div");
|
||||
tmpNode.innerHTML = formHtml;
|
||||
let form = tmpNode.getElementsByTagName('form');
|
||||
if (form.length == 0) {
|
||||
throw new Error('The server did not return a form element')
|
||||
}
|
||||
toReturn = form[0];
|
||||
} catch (error) {
|
||||
this.provideFeedback({
|
||||
variant: 'danger',
|
||||
title: 'There has been a problem with the operation',
|
||||
body: error
|
||||
});
|
||||
toReturn = Promise.reject(error);
|
||||
} finally {
|
||||
if (!skipRequestHooks) {
|
||||
this.afterRequest()
|
||||
}
|
||||
}
|
||||
return toReturn
|
||||
}
|
||||
|
||||
async fetchAndPostForm(url, dataToMerge={}, skipRequestHooks=false) {
|
||||
if (!skipRequestHooks) {
|
||||
this.beforeRequest()
|
||||
}
|
||||
let toReturn
|
||||
try {
|
||||
const form = await this.fetchForm(url, true);
|
||||
try {
|
||||
let formData = new FormData(form)
|
||||
formData = AJAXApi.mergeFormData(formData, dataToMerge)
|
||||
let options = {
|
||||
...AJAXApi.genericRequestConfigPOST,
|
||||
body: formData,
|
||||
};
|
||||
const response = await fetch(form.action, options);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.provideFeedback({
|
||||
variant: 'success',
|
||||
body: data.message
|
||||
});
|
||||
toReturn = data;
|
||||
} else {
|
||||
this.provideFeedback({
|
||||
variant: 'danger',
|
||||
title: 'There has been a problem with the operation',
|
||||
body: data.errors
|
||||
});
|
||||
toReturn = Promise.reject(error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.provideFeedback({
|
||||
variant: 'danger',
|
||||
title: 'There has been a problem with the operation',
|
||||
body: error
|
||||
});
|
||||
toReturn = Promise.reject(error);
|
||||
}
|
||||
} catch (error) {
|
||||
toReturn = Promise.reject(error);
|
||||
} finally {
|
||||
if (!skipRequestHooks) {
|
||||
this.afterRequest()
|
||||
}
|
||||
}
|
||||
return toReturn
|
||||
}
|
||||
|
||||
beforeRequest() {
|
||||
if (this.options.statusNode !== false) {
|
||||
this.toggleLoading(true)
|
||||
}
|
||||
}
|
||||
|
||||
afterRequest() {
|
||||
if (this.options.statusNode !== false) {
|
||||
this.toggleLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
toggleLoading(loading) {
|
||||
if (this.loadingOverlay === false) {
|
||||
this.loadingOverlay = new OverlayFactory({node: this.options.statusNode});
|
||||
}
|
||||
if (loading) {
|
||||
this.loadingOverlay.show()
|
||||
} else {
|
||||
this.loadingOverlay.hide()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,18 @@
|
|||
function showToast(options) {
|
||||
var theToast = new Toaster(options)
|
||||
|
||||
class UIFactory {
|
||||
toast(options) {
|
||||
const theToast = new Toaster(options);
|
||||
theToast.makeToast()
|
||||
theToast.show()
|
||||
return theToast.$toast
|
||||
return theToast
|
||||
}
|
||||
|
||||
modal (options) {
|
||||
const theModal = new ModalFactory(options);
|
||||
theModal.makeModal()
|
||||
theModal.show()
|
||||
return theModal
|
||||
}
|
||||
}
|
||||
|
||||
class Toaster {
|
||||
|
@ -15,6 +25,7 @@ class Toaster {
|
|||
}
|
||||
|
||||
static defaultOptions = {
|
||||
id: false,
|
||||
title: false,
|
||||
muted: false,
|
||||
body: false,
|
||||
|
@ -50,28 +61,31 @@ class Toaster {
|
|||
}
|
||||
|
||||
isValid() {
|
||||
return this.options.title !== false || this.options.muted !== false || this.options.body !== false
|
||||
return this.options.title !== false || this.options.muted !== false || this.options.body !== false || this.options.titleHtml !== false || this.options.mutedHtml !== false || this.options.bodyHtml !== false
|
||||
}
|
||||
|
||||
static buildToast(options) {
|
||||
var $toast = $('<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"/>')
|
||||
if (options.id !== false) {
|
||||
$toast.attr('id', options.id)
|
||||
}
|
||||
$toast.addClass('toast-' + options.variant)
|
||||
if (options.title !== false || options.muted !== false) {
|
||||
if (options.title !== false || options.titleHtml !== false || options.muted !== false || options.mutedHtml !== false) {
|
||||
var $toastHeader = $('<div class="toast-header"/>')
|
||||
$toastHeader.addClass('toast-' + options.variant)
|
||||
if (options.title !== false) {
|
||||
if (options.title !== false || options.titleHtml !== false) {
|
||||
var $toastHeaderText
|
||||
if (options.titleHtml) {
|
||||
$toastHeaderText = $('<div class="mr-auto"/>').html(options.title);
|
||||
if (options.titleHtml !== false) {
|
||||
$toastHeaderText = $('<div class="mr-auto"/>').html(options.titleHtml);
|
||||
} else {
|
||||
$toastHeaderText = $('<strong class="mr-auto"/>').text(options.title)
|
||||
}
|
||||
$toastHeader.append($toastHeaderText)
|
||||
}
|
||||
if (options.muted !== false) {
|
||||
if (options.muted !== false || options.mutedHtml !== false) {
|
||||
var $toastHeaderMuted
|
||||
if (options.mutedHtml) {
|
||||
$toastHeaderMuted = $('<div/>').html(options.muted)
|
||||
if (options.mutedHtml !== false) {
|
||||
$toastHeaderMuted = $('<div/>').html(options.mutedHtml)
|
||||
} else {
|
||||
$toastHeaderMuted = $('<small class="text-muted"/>').text(options.muted)
|
||||
}
|
||||
|
@ -83,10 +97,10 @@ class Toaster {
|
|||
}
|
||||
$toast.append($toastHeader)
|
||||
}
|
||||
if (options.body !== false) {
|
||||
if (options.body !== false || options.bodyHtml !== false) {
|
||||
var $toastBody
|
||||
if (options.bodyHtml) {
|
||||
$toastBody = $('<div class="toast-body"/>').html(options.body)
|
||||
if (options.bodyHtml !== false) {
|
||||
$toastBody = $('<div class="toast-body"/>').html(options.mutedHtml)
|
||||
} else {
|
||||
$toastBody = $('<div class="toast-body"/>').text(options.body)
|
||||
}
|
||||
|
@ -95,3 +109,305 @@ class Toaster {
|
|||
return $toast
|
||||
}
|
||||
}
|
||||
|
||||
class ModalFactory {
|
||||
constructor(options) {
|
||||
this.options = Object.assign({}, ModalFactory.defaultOptions, options)
|
||||
this.bsModalOptions = {
|
||||
show: true
|
||||
}
|
||||
}
|
||||
|
||||
static defaultOptions = {
|
||||
id: false,
|
||||
size: 'md',
|
||||
centered: false,
|
||||
scrollable: false,
|
||||
title: '',
|
||||
titleHtml: false,
|
||||
body: '',
|
||||
bodyHtml: false,
|
||||
variant: '',
|
||||
modalClass: [],
|
||||
headerClass: [],
|
||||
bodyClass: [],
|
||||
footerClass: [],
|
||||
buttons: [],
|
||||
type: 'ok-only',
|
||||
confirmText: 'Confirm',
|
||||
cancelText: 'Cancel',
|
||||
closeManually: false,
|
||||
closeOnSuccess: true,
|
||||
confirm: function() {},
|
||||
APIConfirm: null,
|
||||
cancel: function() {},
|
||||
error: function() {},
|
||||
shownCallback: function() {},
|
||||
hiddenCallback: function() {},
|
||||
}
|
||||
|
||||
static availableType = [
|
||||
'ok-only',
|
||||
'confirm',
|
||||
'confirm-success',
|
||||
'confirm-warning',
|
||||
'confirm-danger',
|
||||
]
|
||||
|
||||
static closeButtonHtml = '<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>'
|
||||
static spinnerHtml = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Loading...'
|
||||
|
||||
makeModal() {
|
||||
if (this.isValid()) {
|
||||
this.$modal = this.buildModal()
|
||||
$('#mainModalContainer').append(this.$modal)
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this.isValid()) {
|
||||
var that = this
|
||||
this.$modal.modal(this.bsModalOptions)
|
||||
.on('hidden.bs.modal', function () {
|
||||
that.removeModal()
|
||||
that.options.hiddenCallback()
|
||||
})
|
||||
.on('shown.bs.modal', function () {
|
||||
that.options.shownCallback()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$modal.modal('hide')
|
||||
}
|
||||
|
||||
removeModal() {
|
||||
this.$modal.remove();
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.options.title !== false || this.options.body !== false || this.options.titleHtml !== false || this.options.bodyHtml !== false
|
||||
}
|
||||
|
||||
buildModal() {
|
||||
var $modal = $('<div class="modal fade" tabindex="-1" aria-hidden="true"/>')
|
||||
if (this.options.id !== false) {
|
||||
$modal.attr('id', this.options.id)
|
||||
$modal.attr('aria-labelledby', this.options.id)
|
||||
}
|
||||
if (this.options.modalClass !== false) {
|
||||
$modal.addClass(this.options.modalClass)
|
||||
}
|
||||
var $modalDialog = $('<div class="modal-dialog"/>')
|
||||
var $modalContent = $('<div class="modal-content"/>')
|
||||
if (this.options.title !== false || this.options.titleHtml !== false) {
|
||||
var $modalHeader = $('<div class="modal-header"/>')
|
||||
var $modalHeaderText
|
||||
if (this.options.titleHtml !== false) {
|
||||
$modalHeaderText = $('<div/>').html(this.options.titleHtml);
|
||||
} else {
|
||||
$modalHeaderText = $('<h5 class="modal-title"/>').text(this.options.title)
|
||||
}
|
||||
$modalHeader.append($modalHeaderText, ModalFactory.getCloseButton())
|
||||
$modalContent.append($modalHeader)
|
||||
}
|
||||
|
||||
if (this.options.body !== false || this.options.bodyHtml !== false) {
|
||||
var $modalBody = $('<div class="modal-body"/>')
|
||||
var $modalBodyText
|
||||
if (this.options.bodyHtml !== false) {
|
||||
$modalBodyText = $('<div/>').html(this.options.bodyHtml);
|
||||
} else {
|
||||
$modalBodyText = $('<div/>').text(this.options.body)
|
||||
}
|
||||
$modalBody.append($modalBodyText)
|
||||
$modalContent.append($modalBody)
|
||||
}
|
||||
|
||||
var $modalFooter = $('<div class="modal-footer"/>')
|
||||
$modalFooter.append(this.getFooterBasedOnType())
|
||||
$modalContent.append($modalFooter)
|
||||
|
||||
$modalDialog.append($modalContent)
|
||||
$modal.append($modalDialog)
|
||||
return $modal
|
||||
}
|
||||
|
||||
getFooterBasedOnType() {
|
||||
if (this.options.type == 'ok-only') {
|
||||
return this.getFooterOkOnly()
|
||||
} else if (this.options.type.includes('confirm')) {
|
||||
return this.getFooterConfirm()
|
||||
} else {
|
||||
return this.getFooterOkOnly()
|
||||
}
|
||||
}
|
||||
|
||||
getFooterOkOnly() {
|
||||
return [
|
||||
$('<button type="button" class="btn btn-primary">OK</button>')
|
||||
.attr('data-dismiss', 'modal'),
|
||||
]
|
||||
}
|
||||
|
||||
getFooterConfirm() {
|
||||
let variant = this.options.type.split('-')[1]
|
||||
variant = variant !== undefined ? variant : 'primary'
|
||||
return [
|
||||
$('<button type="button" class="btn btn-secondary" data-dismiss="modal"></button>')
|
||||
.text(this.options.cancelText)
|
||||
.click(
|
||||
(evt) => {
|
||||
this.options.cancel(() => { this.hide() }, this, evt)
|
||||
}
|
||||
)
|
||||
.attr('data-dismiss', (this.options.closeManually || !this.options.closeOnSuccess) ? '' : 'modal'),
|
||||
$('<button type="button" class="btn"></button>')
|
||||
.addClass('btn-' + variant)
|
||||
.text(this.options.confirmText)
|
||||
.click(
|
||||
(evt) => {
|
||||
let confirmFunction = this.options.confirm
|
||||
if (this.options.APIConfirm) {
|
||||
const tmpApi = new AJAXApi({
|
||||
statusNode: evt.target
|
||||
})
|
||||
confirmFunction = () => { return this.options.APIConfirm(tmpApi) }
|
||||
}
|
||||
let confirmResult = confirmFunction(() => { this.hide() }, this, evt)
|
||||
if (confirmResult === undefined) {
|
||||
this.hide()
|
||||
} else {
|
||||
confirmResult.then(() => {
|
||||
if (this.options.closeOnSuccess) {
|
||||
this.hide()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.options.error(() => { this.hide() }, this, evt)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
.attr('data-dismiss', (this.options.closeManually || this.options.closeOnSuccess) ? '' : 'modal')
|
||||
]
|
||||
}
|
||||
|
||||
static getCloseButton() {
|
||||
return $(ModalFactory.closeButtonHtml)
|
||||
}
|
||||
}
|
||||
|
||||
class OverlayFactory {
|
||||
constructor(options) {
|
||||
this.options = Object.assign({}, OverlayFactory.defaultOptions, options)
|
||||
if (this.options.spinnerAuto) {
|
||||
this.adjustSpinnerOptionsBasedOnNode()
|
||||
}
|
||||
}
|
||||
|
||||
static defaultOptions = {
|
||||
node: false,
|
||||
variant: 'light',
|
||||
opacity: 0.85,
|
||||
blur: '2px',
|
||||
rounded: false,
|
||||
spinnerVariant: '',
|
||||
spinnerSmall: false,
|
||||
spinnerAuto: true
|
||||
}
|
||||
|
||||
static overlayWrapper = '<div aria-busy="true" class="position-relative"/>'
|
||||
static overlayContainer = '<div class="position-absolute" style="inset: 0px; z-index: 10;"/>'
|
||||
static overlayBg = '<div class="position-absolute" style="inset: 0px;"/>'
|
||||
static overlaySpinner = '<div class="position-absolute" style="top: 50%; left: 50%; transform: translateX(-50%) translateY(-50%);"><span aria-hidden="true" class="spinner-border"><!----></span></div></div>'
|
||||
|
||||
shown = false
|
||||
originalNodeIndex = 0
|
||||
|
||||
isValid() {
|
||||
return this.options.node !== false
|
||||
}
|
||||
|
||||
buildOverlay() {
|
||||
this.$node = $(this.options.node)
|
||||
this.$overlayWrapper = $(OverlayFactory.overlayWrapper)
|
||||
this.$overlayContainer = $(OverlayFactory.overlayContainer)
|
||||
this.$overlayBg = $(OverlayFactory.overlayBg)
|
||||
.addClass([`bg-${this.options.variant}`, (this.options.rounded ? 'rounded' : '')])
|
||||
.css('opacity', this.options.opacity)
|
||||
this.$overlaySpinner = $(OverlayFactory.overlaySpinner)
|
||||
if (this.options.spinnerSmall) {
|
||||
this.$overlaySpinner.children().addClass('spinner-border-sm')
|
||||
}
|
||||
if (this.options.spinnerVariant.length > 0) {
|
||||
this.$overlaySpinner.children().addClass(`text-${this.options.spinnerVariant}`)
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this.isValid()) {
|
||||
this.buildOverlay()
|
||||
this.mountOverlay()
|
||||
this.shown = true
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this.isValid() && this.shown) {
|
||||
this.unmountOverlay()
|
||||
}
|
||||
this.shown = false
|
||||
}
|
||||
|
||||
mountOverlay() {
|
||||
this.originalNodeIndex = this.$node.index()
|
||||
this.$overlayBg.appendTo(this.$overlayContainer)
|
||||
this.$overlaySpinner.appendTo(this.$overlayContainer)
|
||||
this.appendToIndex(this.$overlayWrapper, this.$node.parent(), this.originalNodeIndex)
|
||||
this.$overlayContainer.appendTo(this.$overlayWrapper)
|
||||
this.$node.prependTo(this.$overlayWrapper)
|
||||
}
|
||||
|
||||
unmountOverlay() {
|
||||
this.appendToIndex(this.$node, this.$overlayWrapper.parent(), this.originalNodeIndex)
|
||||
this.$overlayWrapper.remove()
|
||||
this.originalNodeIndex = 0
|
||||
}
|
||||
|
||||
appendToIndex($node, $targetContainer, index) {
|
||||
const $target = $targetContainer.children().eq(index);
|
||||
$node.insertBefore($target);
|
||||
}
|
||||
|
||||
adjustSpinnerOptionsBasedOnNode() {
|
||||
let $node = $(this.options.node)
|
||||
if ($node.width() < 50 || $node.height() < 50) {
|
||||
this.options.spinnerSmall = true
|
||||
}
|
||||
if ($node.is('input[type="checkbox"]')) {
|
||||
this.options.rounded = true
|
||||
} else {
|
||||
let classes = $node.attr('class')
|
||||
if (classes !== undefined) {
|
||||
classes = classes.split(' ')
|
||||
this.options.spinnerVariant = OverlayFactory.detectedBootstrapVariant(classes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static detectedBootstrapVariant(classes) {
|
||||
const re = /^[a-zA-Z]+-(?<variant>primary|success)$/;
|
||||
let result
|
||||
for (let i=0; i<classes.length; i++) {
|
||||
let theClass = classes[i]
|
||||
if ((result = re.exec(theClass)) !== null) {
|
||||
if (result.groups !== undefined && result.groups.variant !== undefined) {
|
||||
return result.groups.variant
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -37,3 +37,21 @@ function executeStateDependencyChecks(dependenceSourceSelector) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
var AjaxApi, UI
|
||||
$(document).ready(() => {
|
||||
AjaxApi = new AJAXApi()
|
||||
UI = new UIFactory()
|
||||
})
|
||||
|
||||
function reloadElement(url, $container, $statusNode=null) {
|
||||
if (!$statusNode) {
|
||||
$statusNode = $container
|
||||
}
|
||||
tmpApi = new AJAXApi({
|
||||
statusNode: $statusNode[0],
|
||||
})
|
||||
tmpApi.fetchURL(url).then((data) => {
|
||||
$container.html(data)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue