chg: [generic] Added Modal from URL support

- Support Form submission
- Success / Fail callbacks
- Modal reloading in case of validation errors
pull/37/head
mokaddem 2020-12-15 10:40:49 +01:00
parent ae0272a62c
commit f9bf1c6f55
10 changed files with 254 additions and 119 deletions

View File

@ -110,6 +110,9 @@ class AppController extends Controller
$this->ACL->checkAccess(); $this->ACL->checkAccess();
$this->set('menu', $this->ACL->getMenu()); $this->set('menu', $this->ACL->getMenu());
$this->set('ajax', $this->request->is('ajax')); $this->set('ajax', $this->request->is('ajax'));
if (!empty($this->request->getHeader('X-Request-Html-On-Failure'))) {
$this->ajax_with_html_on_failure = true;
}
$this->request->getParam('prefix'); $this->request->getParam('prefix');
$this->set('darkMode', !empty(Configure::read('Cerebrate.dark'))); $this->set('darkMode', !empty(Configure::read('Cerebrate.dark')));
} }

View File

@ -81,13 +81,16 @@ class CRUDComponent extends Component
$patchEntityParams['fields'] = $params['fields']; $patchEntityParams['fields'] = $params['fields'];
} }
$data = $this->Table->patchEntity($data, $input, $patchEntityParams); $data = $this->Table->patchEntity($data, $input, $patchEntityParams);
if ($this->Table->save($data)) { $savedData = $this->Table->save($data);
if ($savedData !== false) {
$message = __('{0} added.', $this->ObjectAlias); $message = __('{0} added.', $this->ObjectAlias);
if (!empty($input['metaFields'])) { if (!empty($input['metaFields'])) {
$this->saveMetaFields($data->id, $input); $this->saveMetaFields($data->id, $input);
} }
if ($this->Controller->ParamHandler->isRest()) { if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message);
} else { } else {
$this->Controller->Flash->success($message); $this->Controller->Flash->success($message);
if (!empty($params['displayOnSuccess'])) { if (!empty($params['displayOnSuccess'])) {
@ -103,6 +106,7 @@ class CRUDComponent extends Component
} }
} }
} else { } else {
$this->Controller->isFailResponse = true;
$validationMessage = $this->prepareValidationError($data); $validationMessage = $this->prepareValidationError($data);
$message = __( $message = __(
'{0} could not be added.{1}', '{0} could not be added.{1}',
@ -110,7 +114,8 @@ class CRUDComponent extends Component
empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage) empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage)
); );
if ($this->Controller->ParamHandler->isRest()) { if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->Controller->RestResponse->ajaxFailResponse($this->ObjectAlias, 'add', $data, $message, $validationMessage);
} else { } else {
$this->Controller->Flash->error($message); $this->Controller->Flash->error($message);
} }

View File

@ -25,6 +25,10 @@ class UsersController extends AppController
$this->CRUD->add(); $this->CRUD->add();
if ($this->ParamHandler->isRest()) { if ($this->ParamHandler->isRest()) {
return $this->restResponsePayload; return $this->restResponsePayload;
} else if ($this->ParamHandler->isAjax() && $this->request->is(['post', 'put'])) {
if (empty($this->isFailResponse) || empty($this->ajax_with_html_on_failure)) {
return $this->ajaxResponsePayload;
}
} }
$dropdownData = [ $dropdownData = [
'role' => $this->Users->Roles->find('list', [ 'role' => $this->Users->Roles->find('list', [

View File

@ -12,6 +12,7 @@
- use these to define dynamic form fields, or anything that will feed into the regular fields via JS population - use these to define dynamic form fields, or anything that will feed into the regular fields via JS population
* - submit: The submit button itself. By default it will simply submit to the form as defined via the 'model' field * - submit: The submit button itself. By default it will simply submit to the form as defined via the 'model' field
*/ */
$this->Form->setConfig('errorClass', 'is-invalid');
$modelForForm = empty($data['model']) ? $modelForForm = empty($data['model']) ?
h(\Cake\Utility\Inflector::singularize(\Cake\Utility\Inflector::classify($this->request->getParam('controller')))) : h(\Cake\Utility\Inflector::singularize(\Cake\Utility\Inflector::classify($this->request->getParam('controller')))) :
h($data['model']); h($data['model']);
@ -35,11 +36,14 @@
'select' => '<select name="{{name}}" {{attrs}}>{{content}}</select>', 'select' => '<select name="{{name}}" {{attrs}}>{{content}}</select>',
'checkbox' => '<input type="checkbox" name="{{name}}" value="{{value}}"{{attrs}}>', 'checkbox' => '<input type="checkbox" name="{{name}}" value="{{value}}"{{attrs}}>',
'checkboxFormGroup' => '{{label}}', 'checkboxFormGroup' => '{{label}}',
'formGroup' => '<div class="col-sm-2 col-form-label" {{attrs}}>{{label}}</div><div class="col-sm-10">{{input}}</div>', 'formGroup' => '<div class="col-sm-2 col-form-label" {{attrs}}>{{label}}</div><div class="col-sm-10">{{input}}{{error}}</div>',
'nestingLabel' => '{{hidden}}<div class="col-sm-2 col-form-label">{{text}}</div><div class="col-sm-10">{{input}}</div>', 'nestingLabel' => '{{hidden}}<div class="col-sm-2 col-form-label">{{text}}</div><div class="col-sm-10">{{input}}</div>',
'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>', 'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>',
'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>', 'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>',
'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>' 'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>',
'error' => '<div class="error-message invalid-feedback d-block">{{content}}</div>',
'errorList' => '<ul>{{content}}</ul>',
'errorItem' => '<li>{{text}}</li>',
]; ];
if (!empty($data['fields'])) { if (!empty($data['fields'])) {
foreach ($data['fields'] as $fieldData) { foreach ($data['fields'] as $fieldData) {
@ -49,6 +53,7 @@
} }
} }
// we reset the template each iteration as individual fields might override the defaults. // we reset the template each iteration as individual fields might override the defaults.
$this->Form->setConfig($default_template);
$this->Form->setTemplates($default_template); $this->Form->setTemplates($default_template);
if (isset($fieldData['requirements']) && !$fieldData['requirements']) { if (isset($fieldData['requirements']) && !$fieldData['requirements']) {
continue; continue;

View File

@ -3,8 +3,8 @@
echo sprintf( echo sprintf(
'%s', '%s',
sprintf( sprintf(
'<button id="submitButton" class="btn btn-primary" onClick="%s" autofocus>%s</button>', '<button id="submitButton" class="btn btn-primary" data-form-id="%s" autofocus>%s</button>',
"$('#form-" . h($formRandomValue) . "').submit()", '#form-' . h($formRandomValue),
__('Submit') __('Submit')
) )
); );

View File

@ -2,7 +2,7 @@
if (!isset($data['requirement']) || $data['requirement']) { if (!isset($data['requirement']) || $data['requirement']) {
if (!empty($data['popover_url'])) { if (!empty($data['popover_url'])) {
$onClick = sprintf( $onClick = sprintf(
'onClick="populateAndLoadModal(%s)"', 'onClick="openModalFromURL(%s)"',
sprintf("'%s'", h($data['popover_url'])) sprintf("'%s'", h($data['popover_url']))
); );
} }
@ -67,3 +67,11 @@
); );
} }
?> ?>
<script>
function openModalFromURL(url) {
UI.modalFromURL(url, (data) => {
UI.reload('<?= $this->Url->build(['action' => 'index']); ?>', $('#table-container-<?= $tableRandomValue ?>'), $('#table-container-<?= $tableRandomValue ?> table.table'))
})
}
</script>

View File

@ -2,7 +2,7 @@
if (!isset($data['requirement']) || $data['requirement']) { if (!isset($data['requirement']) || $data['requirement']) {
$elements = ''; $elements = '';
foreach ($data['children'] as $element) { foreach ($data['children'] as $element) {
$elements .= $this->element('/genericElements/ListTopBar/element_' . (empty($element['type']) ? 'simple' : h($element['type'])), array('data' => $element)); $elements .= $this->element('/genericElements/ListTopBar/element_' . (empty($element['type']) ? 'simple' : h($element['type'])), array('data' => $element, 'tableRandomValue' => $tableRandomValue));
} }
echo sprintf( echo sprintf(
'<div %s class="btn-group mr-2" role="group" aria-label="button-group">%s</div>', '<div %s class="btn-group mr-2" role="group" aria-label="button-group">%s</div>',

View File

@ -66,7 +66,7 @@ $cakeDescription = 'Cerebrate';
</div> </div>
</main> </main>
<div id="mainModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mediumModalLabel" aria-hidden="true"></div> <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="mainToastContainer" style="position: absolute; top: 15px; right: 15px; z-index: 1080"></div>
<div id="mainModalContainer"></div> <div id="mainModalContainer"></div>
</body> </body>
</html> </html>

View File

@ -1,19 +1,27 @@
class AJAXApi { class AJAXApi {
static genericRequestHeaders = new Headers({ static genericRequestHeaders = {
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
}); };
static genericRequestConfigGET = { static genericRequestConfigGET = {
headers: AJAXApi.genericRequestHeaders headers: new Headers(Object.assign({}, AJAXApi.genericRequestHeaders))
} }
static genericRequestConfigPOST = { static genericRequestConfigPOST = {
headers: AJAXApi.genericRequestHeaders, headers: new Headers(Object.assign({}, AJAXApi.genericRequestHeaders)),
redirect: 'manual', redirect: 'manual',
method: 'POST', method: 'POST',
} }
static renderHTMLOnFailureHeader = {
name: 'X-Request-HTML-On-Failure',
value: '1'
}
static defaultOptions = { static defaultOptions = {
provideFeedback: true, provideFeedback: true,
statusNode: false, statusNode: false,
renderedHTMLOnFailureRequested: false,
errorToast: {
delay: 10000
}
} }
options = {} options = {}
loadingOverlay = false loadingOverlay = false
@ -23,12 +31,15 @@ class AJAXApi {
this.mergeOptions(options) this.mergeOptions(options)
} }
provideFeedback(options, isError=false) { provideFeedback(toastOptions, isError=false, skip=false) {
const alteredToastOptions = isError ? Object.assign({}, AJAXApi.defaultOptions.errorToast, toastOptions) : toastOptions
if (!skip) {
if (this.options.provideFeedback) { if (this.options.provideFeedback) {
UI.toast(options) UI.toast(alteredToastOptions)
} else { } else {
if (isError) { if (isError) {
console.error(options.body) console.error(alteredToastOptions.body)
}
} }
} }
} }
@ -56,13 +67,19 @@ class AJAXApi {
return tmpApi.fetchForm(url, constAlteredOptions.skipRequestHooks) return tmpApi.fetchForm(url, constAlteredOptions.skipRequestHooks)
} }
static async quickPostForm(form, dataToMerge={}, options={}) {
const constAlteredOptions = Object.assign({}, {}, options)
const tmpApi = new AJAXApi(constAlteredOptions)
return tmpApi.postForm(form, dataToMerge, constAlteredOptions.skipRequestHooks)
}
static async quickFetchAndPostForm(url, dataToMerge={}, options={}) { static async quickFetchAndPostForm(url, dataToMerge={}, options={}) {
const constAlteredOptions = Object.assign({}, {}, options) const constAlteredOptions = Object.assign({}, {}, options)
const tmpApi = new AJAXApi(constAlteredOptions) const tmpApi = new AJAXApi(constAlteredOptions)
return tmpApi.fetchAndPostForm(url, dataToMerge, constAlteredOptions.skipRequestHooks) return tmpApi.fetchAndPostForm(url, dataToMerge, constAlteredOptions.skipRequestHooks)
} }
async fetchURL(url, skipRequestHooks=false) { async fetchURL(url, skipRequestHooks=false, skipFeedback=false) {
if (!skipRequestHooks) { if (!skipRequestHooks) {
this.beforeRequest() this.beforeRequest()
} }
@ -76,14 +93,14 @@ class AJAXApi {
this.provideFeedback({ this.provideFeedback({
variant: 'success', variant: 'success',
title: 'URL fetched', title: 'URL fetched',
}); }, false, skipFeedback);
toReturn = data; toReturn = data;
} catch (error) { } catch (error) {
this.provideFeedback({ this.provideFeedback({
variant: 'danger', variant: 'danger',
title: 'There has been a problem with the operation', title: 'There has been a problem with the operation',
body: error body: error
}, true); }, true, skipFeedback);
toReturn = Promise.reject(error); toReturn = Promise.reject(error);
} finally { } finally {
if (!skipRequestHooks) { if (!skipRequestHooks) {
@ -93,7 +110,7 @@ class AJAXApi {
return toReturn return toReturn
} }
async fetchForm(url, skipRequestHooks=false) { async fetchForm(url, skipRequestHooks=false, skipFeedback=false) {
if (!skipRequestHooks) { if (!skipRequestHooks) {
this.beforeRequest() this.beforeRequest()
} }
@ -116,7 +133,74 @@ class AJAXApi {
variant: 'danger', variant: 'danger',
title: 'There has been a problem with the operation', title: 'There has been a problem with the operation',
body: error body: error
}, true); }, true, skipFeedback);
toReturn = Promise.reject(error);
} finally {
if (!skipRequestHooks) {
this.afterRequest()
}
}
return toReturn
}
async postForm(form, dataToMerge={}, skipRequestHooks=false, skipFeedback=false) {
if (!skipRequestHooks) {
this.beforeRequest()
}
let toReturn
let feedbackShown = false
try {
try {
let formData = new FormData(form)
formData = AJAXApi.mergeFormData(formData, dataToMerge)
let requestConfig = AJAXApi.genericRequestConfigPOST
if (this.options.renderedHTMLOnFailureRequested) {
requestConfig.headers.append(AJAXApi.renderHTMLOnFailureHeader.name, AJAXApi.renderHTMLOnFailureHeader.value)
}
let options = {
...requestConfig,
body: formData,
};
const response = await fetch(form.action, options);
if (!response.ok) {
throw new Error('Network response was not ok')
}
const clonedResponse = response.clone()
try {
const data = await response.json()
if (data.success) {
this.provideFeedback({
variant: 'success',
body: data.message
}, false, skipFeedback);
toReturn = data;
} else {
this.provideFeedback({
variant: 'danger',
title: 'There has been a problem with the operation',
body: data.errors
}, true, skipFeedback);
feedbackShown = true
toReturn = Promise.reject(data.errors);
}
} catch (error) { // could not parse JSON
if (this.options.renderedHTMLOnFailureRequested) {
const data = await clonedResponse.text();
toReturn = {
success: 0,
html: data,
}
}
}
} catch (error) {
this.provideFeedback({
variant: 'danger',
title: 'There has been a problem with the operation',
body: error
}, true, feedbackShown);
toReturn = Promise.reject(error);
}
} catch (error) {
toReturn = Promise.reject(error); toReturn = Promise.reject(error);
} finally { } finally {
if (!skipRequestHooks) { if (!skipRequestHooks) {
@ -132,41 +216,8 @@ class AJAXApi {
} }
let toReturn let toReturn
try { try {
const form = await this.fetchForm(url, true); const form = await this.fetchForm(url, true, true);
try { toReturn = await this.postForm(form, dataToMerge, true, true)
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
}, true);
toReturn = Promise.reject(error);
}
} catch (error) {
this.provideFeedback({
variant: 'danger',
title: 'There has been a problem with the operation',
body: error
}, true);
toReturn = Promise.reject(error);
}
} catch (error) { } catch (error) {
toReturn = Promise.reject(error); toReturn = Promise.reject(error);
} finally { } finally {

View File

@ -16,6 +16,22 @@ class UIFactory {
return theModal return theModal
} }
/* Display a modal based on provided options */
modalFromURL (url, successCallback, failCallback) {
return AJAXApi.quickFetchURL(url).then((modalHTML) => {
const theModal = new ModalFactory({
rawHTML: modalHTML,
replaceFormSubmissionByAjax: true,
successCallback: successCallback !== undefined ? successCallback : () => {},
failCallback: failCallback !== undefined ? failCallback : (errorMessage) => {},
});
theModal.makeModal(modalHTML)
theModal.show()
theModal.$modal.data('modalObject', theModal)
return theModal
})
}
/* Fetch HTML from the provided URL and override content of $container. $statusNode allows to specify another HTML node to display the loading */ /* Fetch HTML from the provided URL and override content of $container. $statusNode allows to specify another HTML node to display the loading */
reload (url, $container, $statusNode=null) { reload (url, $container, $statusNode=null) {
$container = $($container) $container = $($container)
@ -26,7 +42,7 @@ class UIFactory {
AJAXApi.quickFetchURL(url, { AJAXApi.quickFetchURL(url, {
statusNode: $statusNode[0] statusNode: $statusNode[0]
}).then((data) => { }).then((data) => {
$container[0].outerHTML = data $container.replaceWith(data)
}) })
} }
} }
@ -143,6 +159,7 @@ class ModalFactory {
titleHtml: false, titleHtml: false,
body: false, body: false,
bodyHtml: false, bodyHtml: false,
rawHTML: false,
variant: '', variant: '',
modalClass: [], modalClass: [],
headerClass: [], headerClass: [],
@ -160,6 +177,8 @@ class ModalFactory {
error: function() {}, error: function() {},
shownCallback: function() {}, shownCallback: function() {},
hiddenCallback: function() {}, hiddenCallback: function() {},
successCallback: function() {},
replaceFormSubmissionByAjax: false
} }
static availableType = [ static availableType = [
@ -190,6 +209,9 @@ class ModalFactory {
}) })
.on('shown.bs.modal', function () { .on('shown.bs.modal', function () {
that.options.shownCallback() that.options.shownCallback()
if (that.options.replaceFormSubmissionByAjax) {
that.replaceFormSubmissionByAjax()
}
}) })
} }
} }
@ -203,11 +225,11 @@ class ModalFactory {
} }
isValid() { isValid() {
return this.options.title !== false || this.options.body !== false || this.options.titleHtml !== false || this.options.bodyHtml !== false return this.options.title !== false || this.options.body !== false || this.options.titleHtml !== false || this.options.bodyHtml !== false || this.options.rawHTML !== false
} }
buildModal() { buildModal() {
var $modal = $('<div class="modal fade" tabindex="-1" aria-hidden="true"/>') const $modal = $('<div class="modal fade" tabindex="-1" aria-hidden="true"/>')
if (this.options.id !== false) { if (this.options.id !== false) {
$modal.attr('id', this.options.id) $modal.attr('id', this.options.id)
$modal.attr('aria-labelledby', this.options.id) $modal.attr('aria-labelledby', this.options.id)
@ -215,11 +237,15 @@ class ModalFactory {
if (this.options.modalClass !== false) { if (this.options.modalClass !== false) {
$modal.addClass(this.options.modalClass) $modal.addClass(this.options.modalClass)
} }
var $modalDialog = $('<div class="modal-dialog"/>') let $modalDialog
var $modalContent = $('<div class="modal-content"/>') if (this.options.rawHTML) {
$modalDialog = $(this.options.rawHTML)
} else {
$modalDialog = $('<div class="modal-dialog"/>')
const $modalContent = $('<div class="modal-content"/>')
if (this.options.title !== false || this.options.titleHtml !== false) { if (this.options.title !== false || this.options.titleHtml !== false) {
var $modalHeader = $('<div class="modal-header"/>') const $modalHeader = $('<div class="modal-header"/>')
var $modalHeaderText let $modalHeaderText
if (this.options.titleHtml !== false) { if (this.options.titleHtml !== false) {
$modalHeaderText = $('<div/>').html(this.options.titleHtml); $modalHeaderText = $('<div/>').html(this.options.titleHtml);
} else { } else {
@ -230,8 +256,8 @@ class ModalFactory {
} }
if (this.options.body !== false || this.options.bodyHtml !== false) { if (this.options.body !== false || this.options.bodyHtml !== false) {
var $modalBody = $('<div class="modal-body"/>') const $modalBody = $('<div class="modal-body"/>')
var $modalBodyText let $modalBodyText
if (this.options.bodyHtml !== false) { if (this.options.bodyHtml !== false) {
$modalBodyText = $('<div/>').html(this.options.bodyHtml); $modalBodyText = $('<div/>').html(this.options.bodyHtml);
} else { } else {
@ -241,11 +267,12 @@ class ModalFactory {
$modalContent.append($modalBody) $modalContent.append($modalBody)
} }
var $modalFooter = $('<div class="modal-footer"/>') const $modalFooter = $('<div class="modal-footer"/>')
$modalFooter.append(this.getFooterBasedOnType()) $modalFooter.append(this.getFooterBasedOnType())
$modalContent.append($modalFooter) $modalContent.append($modalFooter)
$modalDialog.append($modalContent) $modalDialog.append($modalContent)
}
$modal.append($modalDialog) $modal.append($modalDialog)
return $modal return $modal
} }
@ -270,20 +297,29 @@ class ModalFactory {
getFooterConfirm() { getFooterConfirm() {
let variant = this.options.type.split('-')[1] let variant = this.options.type.split('-')[1]
variant = variant !== undefined ? variant : 'primary' variant = variant !== undefined ? variant : 'primary'
return [ const $buttonCancel = $('<button type="button" class="btn btn-secondary" data-dismiss="modal"></button>')
$('<button type="button" class="btn btn-secondary" data-dismiss="modal"></button>')
.text(this.options.cancelText) .text(this.options.cancelText)
.click( .click(
(evt) => { (evt) => {
this.options.cancel(() => { this.hide() }, this, evt) this.options.cancel(() => { this.hide() }, this, evt)
} }
) )
.attr('data-dismiss', (this.options.closeManually || !this.options.closeOnSuccess) ? '' : 'modal'), .attr('data-dismiss', (this.options.closeManually || !this.options.closeOnSuccess) ? '' : 'modal')
$('<button type="button" class="btn"></button>')
const $buttonConfirm = $('<button type="button" class="btn"></button>')
.addClass('btn-' + variant) .addClass('btn-' + variant)
.text(this.options.confirmText) .text(this.options.confirmText)
.click( .click(this.getConfirmationHandlerFunction())
(evt) => { .attr('data-dismiss', (this.options.closeManually || this.options.closeOnSuccess) ? '' : 'modal')
return [$buttonCancel, $buttonConfirm]
}
static getCloseButton() {
return $(ModalFactory.closeButtonHtml)
}
getConfirmationHandlerFunction() {
return (evt) => {
let confirmFunction = this.options.confirm let confirmFunction = this.options.confirm
if (this.options.APIConfirm) { if (this.options.APIConfirm) {
const tmpApi = new AJAXApi({ const tmpApi = new AJAXApi({
@ -295,7 +331,7 @@ class ModalFactory {
if (confirmResult === undefined) { if (confirmResult === undefined) {
this.hide() this.hide()
} else { } else {
confirmResult.then(() => { confirmResult.then((data) => {
if (this.options.closeOnSuccess) { if (this.options.closeOnSuccess) {
this.hide() this.hide()
} }
@ -305,13 +341,36 @@ class ModalFactory {
}) })
} }
} }
)
.attr('data-dismiss', (this.options.closeManually || this.options.closeOnSuccess) ? '' : 'modal')
]
} }
static getCloseButton() { replaceFormSubmissionByAjax() {
return $(ModalFactory.closeButtonHtml) const $submitButton = this.$modal.find('.modal-footer #submitButton')
const formID = $submitButton.data('form-id')
let $form
if (formID) {
$form = $(formID)
} else {
$form = this.$modal.find('form')
}
this.options.APIConfirm = (tmpApi) => {
tmpApi.mergeOptions({renderedHTMLOnFailureRequested: true})
return tmpApi.postForm($form[0])
.then((data) => {
if (data.success) {
this.options.successCallback(data)
} else { // Validation error, replace modal content with new html
this.$modal.html(data.html)
this.replaceFormSubmissionByAjax()
return Promise.reject('Validation error');
}
})
.catch((errorMessage, response) => {
this.options.failCallback(errorMessage)
return Promise.reject(errorMessage);
})
}
$submitButton.click(this.getConfirmationHandlerFunction())
} }
} }