2020-12-15 15:00:21 +01:00
/** Class containing common UI functionalities */
2020-12-10 15:18:02 +01:00
class UIFactory {
2020-12-15 13:40:26 +01:00
/ * *
* Create and display a toast
* @ param { Object } options - The options to be passed to the Toaster class
* @ return { Object } The Toaster object
* /
2020-12-10 15:18:02 +01:00
toast ( options ) {
const theToast = new Toaster ( options ) ;
theToast . makeToast ( )
theToast . show ( )
return theToast
}
2020-12-15 13:40:26 +01:00
/ * *
* Create and display a modal
* @ param { Object } options - The options to be passed to the ModalFactory class
* @ return { Object } The ModalFactory object
* /
2021-01-13 14:20:04 +01:00
modal ( options ) {
2020-12-10 15:18:02 +01:00
const theModal = new ModalFactory ( options ) ;
theModal . makeModal ( )
theModal . show ( )
return theModal
}
2020-12-10 15:37:39 +01:00
2020-12-15 13:40:26 +01:00
/ * *
2021-03-10 14:54:52 +01:00
* Create and display a modal where the modal ' s content is fetched from the provided URL . Link an AJAXApi to the submission button
2020-12-15 13:40:26 +01:00
* @ param { string } url - The URL from which the modal ' s content should be fetched
* @ param { ModalFactory ~ POSTSuccessCallback } POSTSuccessCallback - The callback that handles successful form submission
* @ param { ModalFactory ~ POSTFailCallback } POSTFailCallback - The callback that handles form submissions errors and validation errors .
2021-07-07 15:03:54 +02:00
* @ param { Object = [ ] } modalOptions - Additional options to be passed to the modal constructor
2020-12-15 13:40:26 +01:00
* @ return { Promise < Object > } Promise object resolving to the ModalFactory object
* /
2021-07-07 15:03:54 +02:00
async submissionModal ( url , POSTSuccessCallback , POSTFailCallback , modalOptions = { } ) {
2020-12-15 10:40:49 +01:00
return AJAXApi . quickFetchURL ( url ) . then ( ( modalHTML ) => {
2021-07-07 15:03:54 +02:00
const defaultOptions = {
2021-01-14 11:33:51 +01:00
rawHtml : modalHTML ,
2020-12-15 13:40:26 +01:00
POSTSuccessCallback : POSTSuccessCallback !== undefined ? POSTSuccessCallback : ( ) => { } ,
POSTFailCallback : POSTFailCallback !== undefined ? POSTFailCallback : ( errorMessage ) => { } ,
2021-07-07 15:03:54 +02:00
}
const options = Object . assign ( { } , defaultOptions , modalOptions )
const theModal = new ModalFactory ( options ) ;
2021-03-10 15:37:19 +01:00
theModal . makeModal ( )
2020-12-15 10:40:49 +01:00
theModal . show ( )
theModal . $modal . data ( 'modalObject' , theModal )
2021-06-23 11:17:39 +02:00
return [ theModal , theModal . ajaxApi ]
2021-06-12 11:57:49 +02:00
} ) . catch ( ( error ) => {
UI . toast ( {
variant : 'danger' ,
title : 'Error while loading the processor' ,
body : error . message
} )
2020-12-15 10:40:49 +01:00
} )
}
2021-01-11 16:28:07 +01:00
/ * *
2021-03-10 14:54:52 +01:00
* Creates and displays a modal where the modal ' s content is fetched from the provided URL . Reloads the single page view after a successful operation .
* Supports ` displayOnSuccess ` option to show another modal after the submission
2021-01-11 16:28:07 +01:00
* @ param { string } url - The URL from which the modal ' s content should be fetched
2021-03-10 14:54:52 +01:00
* @ param { ( boolean | string ) } [ reloadUrl = false ] - The URL from which the data should be fetched after confirming
* @ param { ( jQuery | string ) } [ $table = false ] - The table ID which should be reloaded on success
2021-01-11 16:28:07 +01:00
* @ return { Promise < Object > } Promise object resolving to the ModalFactory object
* /
2021-03-10 14:54:52 +01:00
submissionModalForSinglePage ( url , reloadUrl = false , $table = false ) {
let $statusNode , $reloadedElement
if ( reloadUrl === false ) {
reloadUrl = location . pathname
}
if ( $table === false ) { // Try to get information from the DOM
const $elligibleTable = $ ( 'table[id^="single-view-table-"]' )
const $container = $elligibleTable . closest ( 'div[id^="single-view-table-container-"]' )
$reloadedElement = $container
$statusNode = $elligibleTable
} else {
if ( $table instanceof jQuery ) {
$reloadedElement = $table
$statusNode = $table . find ( 'table[id^="single-view-table-"]' )
} else {
$reloadedElement = $ ( ` single-view-table-container- ${ $table } ` )
$statusNode = $ ( ` single-view-table- ${ $table } ` )
}
}
if ( $reloadedElement . length == 0 ) {
2021-03-10 15:37:19 +01:00
UI . toast ( {
2021-03-10 14:54:52 +01:00
variant : 'danger' ,
title : 'Could not find element to be reloaded' ,
body : 'The content of this page may have changed and has not been reflected. Reloading the page is advised.'
} )
return
}
return UI . submissionReloaderModal ( url , reloadUrl , $reloadedElement , $statusNode ) ;
}
2021-06-23 11:17:39 +02:00
getContainerForTable ( $table ) {
const tableRandomID = $table . data ( 'table-random-value' )
return $table . closest ( ` #table-container- ${ tableRandomID } ` )
}
2021-03-10 14:54:52 +01:00
/ * *
* Creates and displays a modal where the modal ' s content is fetched from the provided URL . Reloads the index table after a successful operation .
* Supports ` displayOnSuccess ` option to show another modal after the submission
* @ param { string } url - The URL from which the modal ' s content should be fetched
* @ param { ( boolean | string ) } [ reloadUrl = false ] - The URL from which the data should be fetched after confirming
* @ param { ( jQuery | string ) } [ $table = false ] - The table ID which should be reloaded on success
* @ return { Promise < Object > } Promise object resolving to the ModalFactory object
* /
submissionModalForIndex ( url , reloadUrl = false , $table = false ) {
let $statusNode , $reloadedElement
if ( reloadUrl === false ) {
const currentModel = location . pathname . split ( '/' ) [ 1 ]
if ( currentModel . length > 0 ) {
reloadUrl = ` / ${ currentModel } /index `
} else {
2021-03-10 15:37:19 +01:00
UI . toast ( {
2021-03-10 14:54:52 +01:00
variant : 'danger' ,
title : 'Could not find URL for the reload' ,
body : 'The content of this page may have changed and has not been reflected. Reloading the page is advised.'
} )
return
}
}
if ( $table === false ) { // Try to get information from the DOM
const $elligibleTable = $ ( 'table.table' )
const $container = $elligibleTable . closest ( 'div[id^="table-container-"]' )
$reloadedElement = $container
$statusNode = $elligibleTable
} else {
if ( $table instanceof jQuery ) {
2021-06-23 11:33:36 +02:00
$reloadedElement = this . getContainerForTable ( $table )
2021-03-10 14:54:52 +01:00
$statusNode = $table . find ( 'table.table' )
2021-01-11 16:28:07 +01:00
} else {
2021-03-10 14:54:52 +01:00
$reloadedElement = $ ( ` #table-container- ${ $table } ` )
$statusNode = $ ( ` #table-container- ${ $table } table.table ` )
2021-01-14 11:33:51 +01:00
}
2021-03-10 14:54:52 +01:00
}
if ( $reloadedElement . length == 0 ) {
2021-03-10 15:37:19 +01:00
UI . toast ( {
2021-03-10 14:54:52 +01:00
variant : 'danger' ,
title : 'Could not find element to be reloaded' ,
body : 'The content of this page may have changed and has not been reflected. Reloading the page is advised.'
} )
return
}
return UI . submissionReloaderModal ( url , reloadUrl , $reloadedElement , $statusNode ) ;
}
2021-03-15 22:47:13 +01:00
/ * *
* Creates and displays a modal where the modal ' s content is fetched from the provided URL . Reloads the index table after a successful operation .
* Supports ` displayOnSuccess ` option to show another modal after the submission
* @ param { string } url - The URL from which the modal ' s content should be fetched
* @ param { ( boolean | string ) } [ reloadUrl = false ] - The URL from which the data should be fetched after confirming
* @ param { ( jQuery | string ) } [ $table = false ] - The table ID which should be reloaded on success
* @ return { Promise < Object > } Promise object resolving to the ModalFactory object
* /
submissionModalAutoGuess ( url , reloadUrl = false , $table = false ) {
let currentAction = location . pathname . split ( '/' ) [ 2 ]
if ( currentAction !== undefined ) {
if ( currentAction === 'index' ) {
return UI . submissionModalForIndex ( url , reloadUrl , $table )
} else if ( currentAction === 'view' ) {
return UI . submissionModalForSinglePage ( url , reloadUrl , $table )
}
}
const successCallback = ( ) => {
UI . toast ( {
variant : 'danger' ,
title : 'Could not reload the page' ,
body : 'Reloading the page manually is advised.'
} )
}
return UI . submissionModal ( url , successCallback )
}
2021-03-10 14:54:52 +01:00
/ * *
* Creates and displays a modal where the modal ' s content is fetched from the provided URL . Reloads the provided element after a successful operation .
* Supports ` displayOnSuccess ` option to show another modal after the submission
* @ param { string } url - The URL from which the modal ' s content should be fetched
* @ param { string } reloadUrl - The URL from which the data should be fetched after confirming
* @ param { ( jQuery | string ) } $reloadedElement - The element which should be reloaded on success
* @ param { ( jQuery | string ) } [ $statusNode = null ] - A reference to a HTML node on which the loading animation should be displayed . If not provided , $container will be used
* @ return { Promise < Object > } Promise object resolving to the ModalFactory object
* /
submissionReloaderModal ( url , reloadUrl , $reloadedElement , $statusNode = null ) {
2021-06-29 07:31:36 +02:00
const successCallback = function ( [ data , modalObject ] ) {
2021-03-10 14:54:52 +01:00
UI . reload ( reloadUrl , $reloadedElement , $statusNode )
2021-11-08 15:54:37 +01:00
if ( data . additionalData !== undefined ) {
if ( data . additionalData . displayOnSuccess !== undefined ) {
UI . modal ( {
rawHtml : data . additionalData . displayOnSuccess
} )
} else if ( data . additionalData . redirect !== undefined ) {
window . location = data . additionalData . redirect
}
2021-01-11 16:28:07 +01:00
}
2021-03-10 14:54:52 +01:00
}
return UI . submissionModal ( url , successCallback )
2021-01-11 16:28:07 +01:00
}
2020-12-15 13:40:26 +01:00
/ * *
* Fetch HTML from the provided URL and override the $container ' s content . $statusNode allows to specify another HTML node to display the loading
* @ param { string } url - The URL from which the $container ' s content should be fetched
* @ param { ( jQuery | string ) } $container - The container that should hold the data fetched
* @ param { ( jQuery | string ) } [ $statusNode = null ] - A reference to a HTML node on which the loading animation should be displayed . If not provided , $container will be used
2021-01-15 12:12:21 +01:00
* @ param { array } [ additionalStatusNodes = [ ] ] - A list of other node on which to apply overlay . Must contain the node and possibly the overlay configuration
2020-12-15 15:49:07 +01:00
* @ return { Promise < jQuery > } Promise object resolving to the $container object after its content has been replaced
2020-12-15 13:40:26 +01:00
* /
2021-01-15 12:12:21 +01:00
reload ( url , $container , $statusNode = null , additionalStatusNodes = [ ] ) {
2020-12-10 15:37:39 +01:00
$container = $ ( $container )
$statusNode = $ ( $statusNode )
if ( ! $statusNode ) {
$statusNode = $container
}
2021-01-15 12:12:21 +01:00
const otherStatusNodes = [ ]
additionalStatusNodes . forEach ( otherStatusNode => {
const loadingOverlay = new OverlayFactory ( otherStatusNode . node , otherStatusNode . config )
loadingOverlay . show ( )
otherStatusNodes . push ( loadingOverlay )
} )
2020-12-15 13:40:26 +01:00
return AJAXApi . quickFetchURL ( url , {
2021-01-18 09:49:20 +01:00
statusNode : $statusNode [ 0 ] ,
2020-12-15 13:40:26 +01:00
} ) . then ( ( theHTML ) => {
2021-08-26 12:06:12 +02:00
var $tmp = $ ( theHTML ) ;
$container . replaceWith ( $tmp )
return $tmp ;
2021-01-15 12:12:21 +01:00
} ) . finally ( ( ) => {
otherStatusNodes . forEach ( overlay => {
overlay . hide ( )
} )
2020-12-10 15:37:39 +01:00
} )
}
2021-01-13 14:20:04 +01:00
/ * *
* Place an overlay onto a node and remove it whenever the promise resolves
* @ param { ( jQuery | string ) } node - The node on which the overlay should be placed
* @ param { Promise } promise - A promise to be fulfilled
* @ param { Object } [ overlayOptions = { } - The options to be passed to the overlay class
2021-01-15 09:17:44 +01:00
* @ return { Promise } Result of the passed promised
2021-01-13 14:20:04 +01:00
* /
overlayUntilResolve ( node , promise , overlayOptions = { } ) {
const $node = $ ( node )
const loadingOverlay = new OverlayFactory ( $node [ 0 ] , overlayOptions ) ;
loadingOverlay . show ( )
promise . finally ( ( ) => {
loadingOverlay . hide ( )
} )
2021-01-14 12:42:05 +01:00
return promise
2021-01-13 14:20:04 +01:00
}
2020-12-07 14:15:59 +01:00
}
2020-12-15 13:40:26 +01:00
/** Class representing a Toast */
2020-12-07 14:15:59 +01:00
class Toaster {
2020-12-15 13:40:26 +01:00
/ * *
* Create a Toast .
* @ param { Object } options - The options supported by Toaster # defaultOptions
* /
2020-12-07 14:15:59 +01:00
constructor ( options ) {
this . options = Object . assign ( { } , Toaster . defaultOptions , options )
this . bsToastOptions = {
autohide : this . options . autohide ,
delay : this . options . delay ,
}
}
2020-12-15 13:40:26 +01:00
/ * *
* @ namespace
* @ property { number } id - The ID to be used for the toast ' s container
* @ property { string } title - The title ' s content of the toast
* @ property { string } muted - The muted ' s content of the toast
* @ property { string } body - The body ' s content of the toast
* @ property { string = ( 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark' | 'white' | 'transparent' ) } variant - The variant of the toast
* @ property { 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
2021-08-26 12:06:12 +02:00
* @ 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
2020-12-15 13:40:26 +01:00
* @ property { boolean } closeButton - If the toast ' s title should include a close button
* /
2020-12-07 14:15:59 +01:00
static defaultOptions = {
2020-12-10 15:18:02 +01:00
id : false ,
2020-12-07 14:15:59 +01:00
title : false ,
muted : false ,
body : false ,
variant : 'default' ,
autohide : true ,
delay : 5000 ,
titleHtml : false ,
mutedHtml : false ,
bodyHtml : false ,
closeButton : true ,
}
2020-12-15 13:40:26 +01:00
/** Create the HTML of the toast and inject it into the DOM */
2020-12-07 14:15:59 +01:00
makeToast ( ) {
if ( this . isValid ( ) ) {
this . $toast = Toaster . buildToast ( this . options )
2021-09-02 18:00:36 +02:00
this . $toast . data ( 'toastObject' , this )
2020-12-07 14:15:59 +01:00
$ ( '#mainToastContainer' ) . append ( this . $toast )
}
}
2020-12-15 13:40:26 +01:00
/** Display the toast to the user and remove it from the DOM once it get hidden */
2020-12-07 14:15:59 +01:00
show ( ) {
if ( this . isValid ( ) ) {
var that = this
this . $toast . toast ( this . bsToastOptions )
. toast ( 'show' )
2021-09-02 18:00:36 +02:00
. on ( 'hide.bs.toast' , function ( evt ) {
const $toast = $ ( this )
const hoveredElements = $ ( ':hover' ) . filter ( function ( ) {
return $ ( this ) . is ( $toast )
} ) ;
if ( hoveredElements . length > 0 ) {
evt . preventDefault ( )
setTimeout ( ( ) => {
$toast . toast ( 'hide' )
} , that . options . delay ) ;
}
} )
2020-12-07 14:15:59 +01:00
. on ( 'hidden.bs.toast' , function ( ) {
that . removeToast ( )
} )
}
}
2020-12-15 13:40:26 +01:00
/** Remove the toast from the DOM */
2020-12-07 14:15:59 +01:00
removeToast ( ) {
this . $toast . remove ( ) ;
}
2020-12-15 13:40:26 +01:00
/ * *
* Check wheter a toast is valid
* @ return { boolean } Return true if the toast contains at least data to be rendered
* /
2020-12-07 14:15:59 +01:00
isValid ( ) {
2020-12-15 13:40:26 +01:00
return this . options . title !== false || this . options . titleHtml !== false ||
this . options . muted !== false || this . options . mutedHtml !== false ||
this . options . body !== false || this . options . bodyHtml !== false
2020-12-07 14:15:59 +01:00
}
2020-12-15 13:40:26 +01:00
/ * *
* Build the toast HTML
* @ param { Object } options - The options supported by Toaster # defaultOptions to build the toast
* @ return { jQuery } The toast jQuery object
* /
2020-12-07 14:15:59 +01:00
static buildToast ( options ) {
var $toast = $ ( '<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"/>' )
2020-12-10 15:18:02 +01:00
if ( options . id !== false ) {
$toast . attr ( 'id' , options . id )
}
2020-12-07 14:15:59 +01:00
$toast . addClass ( 'toast-' + options . variant )
2022-10-25 14:31:10 +02:00
if ( options . title !== false || options . titleHtml !== false || options . muted !== false || options . mutedHtml !== false || options . closeButton ) {
2020-12-07 14:15:59 +01:00
var $toastHeader = $ ( '<div class="toast-header"/>' )
$toastHeader . addClass ( 'toast-' + options . variant )
2022-10-25 14:31:10 +02:00
let $toastHeaderText = $ ( '<span class="me-auto"/>' )
if ( options . titleHtml !== false ) {
$toastHeaderText = $ ( '<div class="me-auto"/>' ) . html ( options . titleHtml ) ;
} else if ( options . title !== false ) {
$toastHeaderText = $ ( '<strong class="me-auto"/>' ) . text ( options . title )
2020-12-07 14:15:59 +01:00
}
2022-10-25 14:31:10 +02:00
$toastHeader . append ( $toastHeaderText )
2020-12-10 15:18:02 +01:00
if ( options . muted !== false || options . mutedHtml !== false ) {
2020-12-07 14:15:59 +01:00
var $toastHeaderMuted
2020-12-10 15:18:02 +01:00
if ( options . mutedHtml !== false ) {
$toastHeaderMuted = $ ( '<div/>' ) . html ( options . mutedHtml )
2020-12-07 14:15:59 +01:00
} else {
$toastHeaderMuted = $ ( '<small class="text-muted"/>' ) . text ( options . muted )
}
$toastHeader . append ( $toastHeaderMuted )
}
if ( options . closeButton ) {
2021-09-28 09:23:02 +02:00
var $closeButton = $ ( '<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>' )
2021-09-02 18:00:36 +02:00
. click ( function ( ) {
$ ( this ) . closest ( '.toast' ) . data ( 'toastObject' ) . removeToast ( )
} )
2020-12-07 14:15:59 +01:00
$toastHeader . append ( $closeButton )
}
$toast . append ( $toastHeader )
}
2020-12-10 15:18:02 +01:00
if ( options . body !== false || options . bodyHtml !== false ) {
2020-12-07 14:15:59 +01:00
var $toastBody
2020-12-10 15:18:02 +01:00
if ( options . bodyHtml !== false ) {
2021-03-10 15:37:19 +01:00
$toastBody = $ ( '<div class="toast-body"/>' ) . html ( options . bodyHtml )
2020-12-07 14:15:59 +01:00
} else {
2021-03-15 22:47:13 +01:00
$toastBody = $ ( '<div class="toast-body"/>' ) . append ( $ ( '<div style="white-space: break-spaces;"/>' ) . text ( options . body ) )
2020-12-07 14:15:59 +01:00
}
$toast . append ( $toastBody )
}
return $toast
}
}
2020-12-10 15:18:02 +01:00
2020-12-15 13:40:26 +01:00
/** Class representing a Modal */
2020-12-10 15:18:02 +01:00
class ModalFactory {
2020-12-15 13:40:26 +01:00
/ * *
* Create a Modal .
* @ param { Object } options - The options supported by ModalFactory # defaultOptions
* /
2020-12-10 15:18:02 +01:00
constructor ( options ) {
this . options = Object . assign ( { } , ModalFactory . defaultOptions , options )
2021-03-10 15:37:19 +01:00
if ( options . POSTSuccessCallback !== undefined ) {
if ( this . options . rawHtml ) {
this . attachSubmitButtonListener = true
} else {
UI . toast ( {
variant : 'danger' ,
bodyHtml : '<b>POSTSuccessCallback</b> can only be used in conjuction with the <i>rawHtml</i> option. Instead, use the promise instead returned by the API call in <b>APIConfirm</b>.'
} )
}
2020-12-15 13:40:26 +01:00
}
2021-01-13 14:20:04 +01:00
if ( options . type === undefined && options . cancel !== undefined ) {
this . options . type = 'confirm'
}
2020-12-10 15:18:02 +01:00
this . bsModalOptions = {
show : true
}
2021-01-14 11:33:51 +01:00
if ( this . options . backdropStatic ) {
this . bsModalOptions [ 'backdrop' ] = 'static'
}
2021-06-23 11:17:39 +02:00
this . ajaxApi = new AJAXApi ( )
2020-12-10 15:18:02 +01:00
}
2020-12-15 13:40:26 +01:00
/ * *
* @ callback ModalFactory ~ closeModalFunction
* /
/ * *
* @ callback ModalFactory ~ confirm
* @ param { ModalFactory ~ closeModalFunction } closeFunction - A function that will close the modal if called
* @ param { Object } modalFactory - The instance of the ModalFactory
* @ param { Object } evt - The event that triggered the confirm operation
* /
/ * *
* @ callback ModalFactory ~ cancel
* @ param { ModalFactory ~ closeModalFunction } closeFunction - A function that will close the modal if called
* @ param { Object } modalFactory - The instance of the ModalFactory
* @ param { Object } evt - The event that triggered the confirm operation
* /
/ * *
* @ callback ModalFactory ~ APIConfirm
* @ param { AJAXApi } ajaxApi - An instance of the AJAXApi with the AJAXApi . statusNode linked to the modal confirm button
* /
/ * *
* @ callback ModalFactory ~ APIError
* @ param { ModalFactory ~ closeModalFunction } closeFunction - A function that will close the modal if called
* @ param { Object } modalFactory - The instance of the ModalFactory
* @ param { Object } evt - The event that triggered the confirm operation
* /
/ * *
* @ callback ModalFactory ~ shownCallback
* @ param { Object } modalFactory - The instance of the ModalFactory
* /
/ * *
* @ callback ModalFactory ~ hiddenCallback
* @ param { Object } modalFactory - The instance of the ModalFactory
* /
/ * *
* @ callback ModalFactory ~ POSTSuccessCallback
* @ param { Object } data - The data received from the successful POST operation
* /
/ * *
* @ callback ModalFactory ~ POSTFailCallback
* @ param { string } errorMessage
* /
/ * *
* @ namespace
* @ property { number } id - The ID to be used for the modal ' s container
* @ property { string = ( 'sm' | 'lg' | 'xl' | '' ) } size - The size of the modal
* @ property { boolean } centered - Should the modal be vertically centered
* @ property { boolean } scrollable - Should the modal be scrollable
2021-01-14 11:33:51 +01:00
* @ property { boolean } backdropStatic - When set , the modal will not close when clicking outside it .
2020-12-15 13:40:26 +01:00
* @ property { string } title - The title ' s content of the modal
* @ property { string } titleHtml - The raw HTML title ' s content of the modal
* @ property { string } body - The body ' s content of the modal
* @ property { string } bodyHtml - The raw HTML body ' s content of the modal
2021-01-14 11:33:51 +01:00
* @ property { string } rawHtml - The raw HTML of the whole modal . If provided , will override any other content
2020-12-15 13:40:26 +01:00
* @ property { string = ( 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark' | 'white' | 'transparent' ) } variant - The variant of the modal
* @ property { string } modalClass - Classes to be added to the modal ' s container
* @ property { string } headerClass - Classes to be added to the modal ' s header
* @ property { string } bodyClass - Classes to be added to the modal ' s body
* @ property { string } footerClass - Classes to be added to the modal ' s footer
2021-01-13 14:20:04 +01:00
* @ property { string = ( 'ok-only' , 'confirm' , 'confirm-success' , 'confirm-warning' , 'confirm-danger' ) } type - Pre - configured template covering most use cases
2020-12-15 13:40:26 +01:00
* @ property { string } confirmText - The text to be placed in the confirm button
* @ property { string } cancelText - The text to be placed in the cancel button
* @ property { boolean } closeManually - If true , the modal will be closed automatically whenever a footer ' s button is pressed
2021-03-10 15:37:19 +01:00
* @ property { boolean } closeOnSuccess - If true , the modal will be closed if the operation is successful
2020-12-15 13:40:26 +01:00
* @ property { ModalFactory ~ confirm } confirm - The callback that should be called if the user confirm the modal
* @ property { ModalFactory ~ cancel } cancel - The callback that should be called if the user cancel the modal
2021-01-13 14:20:04 +01:00
* @ property { ModalFactory ~ APIConfirm } APIConfirm - The callback that should be called if the user confirm the modal . Behaves like the confirm option but provides an AJAXApi object that can be used to issue requests
2020-12-15 13:40:26 +01:00
* @ property { ModalFactory ~ APIError } APIError - The callback called if the APIConfirm callback fails .
* @ property { ModalFactory ~ shownCallback } shownCallback - The callback that should be called whenever the modal is shown
* @ property { ModalFactory ~ hiddenCallback } hiddenCallback - The callback that should be called whenever the modal is hiddenAPIConfirm
2021-03-10 15:37:19 +01:00
* @ property { ModalFactory ~ POSTSuccessCallback } POSTSuccessCallback - The callback that should be called if the POST operation has been a success . Works in confunction with the ` rawHtml `
2020-12-15 13:40:26 +01:00
* @ property { ModalFactory ~ POSTFailCallback } POSTFailCallback - The callback that should be called if the POST operation has been a failure ( Either the request failed or the form validation did not pass )
* /
2020-12-10 15:18:02 +01:00
static defaultOptions = {
id : false ,
size : 'md' ,
centered : false ,
scrollable : false ,
2021-01-14 11:33:51 +01:00
backdropStatic : false ,
2020-12-10 15:18:02 +01:00
title : '' ,
titleHtml : false ,
2020-12-10 16:50:46 +01:00
body : false ,
2020-12-10 15:18:02 +01:00
bodyHtml : false ,
2021-01-14 11:33:51 +01:00
rawHtml : false ,
2020-12-10 15:18:02 +01:00
variant : '' ,
2020-12-15 13:40:26 +01:00
modalClass : '' ,
headerClass : '' ,
bodyClass : '' ,
footerClass : '' ,
2020-12-10 15:18:02 +01:00
type : 'ok-only' ,
confirmText : 'Confirm' ,
cancelText : 'Cancel' ,
closeManually : false ,
closeOnSuccess : true ,
confirm : function ( ) { } ,
cancel : function ( ) { } ,
2020-12-15 13:40:26 +01:00
APIConfirm : null ,
APIError : function ( ) { } ,
2020-12-10 15:18:02 +01:00
shownCallback : function ( ) { } ,
hiddenCallback : function ( ) { } ,
2020-12-15 13:40:26 +01:00
POSTSuccessCallback : function ( ) { } ,
POSTFailCallback : function ( ) { } ,
2020-12-10 15:18:02 +01:00
}
static availableType = [
'ok-only' ,
'confirm' ,
'confirm-success' ,
'confirm-warning' ,
'confirm-danger' ,
]
2021-09-28 09:23:02 +02:00
static closeButtonHtml = '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>'
2020-12-10 15:18:02 +01:00
2020-12-15 13:40:26 +01:00
/** Create the HTML of the modal and inject it into the DOM */
2020-12-10 15:18:02 +01:00
makeModal ( ) {
if ( this . isValid ( ) ) {
this . $modal = this . buildModal ( )
$ ( '#mainModalContainer' ) . append ( this . $modal )
2021-09-17 13:04:37 +02:00
this . modalInstance = new bootstrap . Modal ( this . $modal [ 0 ] , this . bsModalOptions )
2021-01-14 11:33:51 +01:00
} else {
console . log ( 'Modal not valid' )
2020-12-10 15:18:02 +01:00
}
}
2020-12-15 13:40:26 +01:00
/** Display the modal and remove it form the DOM once it gets hidden */
2020-12-10 15:18:02 +01:00
show ( ) {
if ( this . isValid ( ) ) {
var that = this
2021-09-17 13:04:37 +02:00
this . modalInstance . show ( )
this . $modal
2020-12-10 15:18:02 +01:00
. on ( 'hidden.bs.modal' , function ( ) {
that . removeModal ( )
2020-12-15 13:40:26 +01:00
that . options . hiddenCallback ( that )
2020-12-10 15:18:02 +01:00
} )
. on ( 'shown.bs.modal' , function ( ) {
2020-12-15 13:40:26 +01:00
that . options . shownCallback ( that )
if ( that . attachSubmitButtonListener ) {
that . findSubmitButtonAndAddListener ( )
2020-12-15 10:40:49 +01:00
}
2020-12-10 15:18:02 +01:00
} )
2021-09-17 13:04:37 +02:00
// this.$modal.modal(this.bsModalOptions)
// .on('hidden.bs.modal', function () {
// that.removeModal()
// that.options.hiddenCallback(that)
// })
// .on('shown.bs.modal', function () {
// that.options.shownCallback(that)
// if (that.attachSubmitButtonListener) {
// that.findSubmitButtonAndAddListener()
// }
// })
2021-01-14 11:33:51 +01:00
} else {
console . log ( 'Modal not valid' )
2020-12-10 15:18:02 +01:00
}
}
2020-12-15 13:40:26 +01:00
/** Hide the modal using the bootstrap modal's hide command */
2020-12-10 15:18:02 +01:00
hide ( ) {
2021-09-17 13:04:37 +02:00
// this.$modal.modal('hide')
this . modalInstance . hide ( )
2020-12-10 15:18:02 +01:00
}
2020-12-15 13:40:26 +01:00
/** Remove the modal from the DOM */
2020-12-10 15:18:02 +01:00
removeModal ( ) {
this . $modal . remove ( ) ;
}
2020-12-15 13:40:26 +01:00
/ * *
* Check wheter a modal is valid
* @ return { boolean } Return true if the modal contains at least data to be rendered
* /
2020-12-10 15:18:02 +01:00
isValid ( ) {
2020-12-15 13:40:26 +01:00
return this . options . title !== false || this . options . titleHtml !== false ||
this . options . body !== false || this . options . bodyHtml !== false ||
2021-01-14 11:33:51 +01:00
this . options . rawHtml !== false
2020-12-10 15:18:02 +01:00
}
2020-12-15 13:40:26 +01:00
/ * *
* Build the modal HTML
* @ return { jQuery } The modal jQuery object
* /
2020-12-10 15:18:02 +01:00
buildModal ( ) {
2021-09-17 13:04:37 +02:00
const $modal = $ ( '<div class="modal fade" tabindex="-1"/>' )
2020-12-10 15:18:02 +01:00
if ( this . options . id !== false ) {
$modal . attr ( 'id' , this . options . id )
$modal . attr ( 'aria-labelledby' , this . options . id )
}
2020-12-15 13:40:26 +01:00
if ( this . options . modalClass ) {
2020-12-10 15:18:02 +01:00
$modal . addClass ( this . options . modalClass )
}
2020-12-15 10:40:49 +01:00
let $modalDialog
2021-01-14 11:33:51 +01:00
if ( this . options . rawHtml ) {
$modalDialog = $ ( this . options . rawHtml )
if ( $modalDialog . data ( 'backdrop' ) == 'static' ) {
this . bsModalOptions [ 'backdrop' ] = 'static'
}
2020-12-15 10:40:49 +01:00
} else {
$modalDialog = $ ( '<div class="modal-dialog"/>' )
const $modalContent = $ ( '<div class="modal-content"/>' )
if ( this . options . title !== false || this . options . titleHtml !== false ) {
const $modalHeader = $ ( '<div class="modal-header"/>' )
2020-12-15 13:40:26 +01:00
if ( this . options . headerClass ) {
$modalHeader . addClass ( this . options . headerClass )
}
2020-12-15 10:40:49 +01:00
let $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 )
2020-12-10 15:18:02 +01:00
}
2020-12-15 10:40:49 +01:00
if ( this . options . body !== false || this . options . bodyHtml !== false ) {
const $modalBody = $ ( '<div class="modal-body"/>' )
2020-12-15 13:40:26 +01:00
if ( this . options . bodyClass ) {
$modalBody . addClass ( this . options . bodyClass )
}
2020-12-15 10:40:49 +01:00
let $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 )
2020-12-10 15:18:02 +01:00
}
2020-12-15 10:40:49 +01:00
const $modalFooter = $ ( '<div class="modal-footer"/>' )
2020-12-15 13:40:26 +01:00
if ( this . options . footerClass ) {
$modalFooter . addClass ( this . options . footerClass )
}
2020-12-15 10:40:49 +01:00
$modalFooter . append ( this . getFooterBasedOnType ( ) )
$modalContent . append ( $modalFooter )
$modalDialog . append ( $modalContent )
2020-12-10 15:18:02 +01:00
}
$modal . append ( $modalDialog )
return $modal
}
2020-12-15 13:40:26 +01:00
/** Returns the correct footer data based on the provided type */
2020-12-10 15:18:02 +01:00
getFooterBasedOnType ( ) {
if ( this . options . type == 'ok-only' ) {
return this . getFooterOkOnly ( )
} else if ( this . options . type . includes ( 'confirm' ) ) {
return this . getFooterConfirm ( )
} else {
return this . getFooterOkOnly ( )
}
}
2020-12-15 13:40:26 +01:00
/** Generate the ok-only footer type */
2020-12-10 15:18:02 +01:00
getFooterOkOnly ( ) {
return [
$ ( '<button type="button" class="btn btn-primary">OK</button>' )
2021-09-17 13:04:37 +02:00
. attr ( 'data-bs-dismiss' , 'modal' ) ,
2020-12-10 15:18:02 +01:00
]
}
2020-12-15 13:40:26 +01:00
/** Generate the confirm-* footer type */
2020-12-10 15:18:02 +01:00
getFooterConfirm ( ) {
let variant = this . options . type . split ( '-' ) [ 1 ]
variant = variant !== undefined ? variant : 'primary'
2021-09-17 13:04:37 +02:00
const $buttonCancel = $ ( '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"></button>' )
2020-12-10 15:18:02 +01:00
. text ( this . options . cancelText )
. click (
( evt ) => {
this . options . cancel ( ( ) => { this . hide ( ) } , this , evt )
}
)
2021-09-17 13:04:37 +02:00
. attr ( 'data-bs-dismiss' , ( this . options . closeManually || ! this . options . closeOnSuccess ) ? '' : 'modal' )
2020-12-15 10:40:49 +01:00
const $buttonConfirm = $ ( '<button type="button" class="btn"></button>' )
2020-12-10 15:18:02 +01:00
. addClass ( 'btn-' + variant )
. text ( this . options . confirmText )
2021-09-17 13:04:37 +02:00
. attr ( 'data-bs-dismiss' , ( this . options . closeManually || this . options . closeOnSuccess ) ? '' : 'modal' )
2021-06-23 11:17:39 +02:00
$buttonConfirm . click ( this . getConfirmationHandlerFunction ( $buttonConfirm ) )
2020-12-15 10:40:49 +01:00
return [ $buttonCancel , $buttonConfirm ]
2020-12-10 15:18:02 +01:00
}
2020-12-15 13:40:26 +01:00
/** Return a close button */
2020-12-10 15:18:02 +01:00
static getCloseButton ( ) {
return $ ( ModalFactory . closeButtonHtml )
}
2020-12-15 10:40:49 +01:00
2020-12-15 13:40:26 +01:00
/** Generate the function that will be called when the user confirm the modal */
2021-06-23 11:17:39 +02:00
getConfirmationHandlerFunction ( $buttonConfirm , buttonIndex ) {
if ( this . options . APIConfirms ) {
if ( Array . isArray ( this . ajaxApi ) ) {
const tmpApi = new AJAXApi ( {
statusNode : $buttonConfirm [ 0 ]
} )
this . ajaxApi . push ( tmpApi )
} else {
2021-10-21 11:11:33 +02:00
this . ajaxApi . options . statusNode = $buttonConfirm [ 0 ]
2021-06-23 11:17:39 +02:00
this . ajaxApi = [ this . ajaxApi ] ;
}
} else {
2021-10-21 11:11:33 +02:00
this . ajaxApi . options . statusNode = $buttonConfirm [ 0 ]
2021-06-23 11:17:39 +02:00
}
2020-12-15 10:40:49 +01:00
return ( evt ) => {
let confirmFunction = this . options . confirm
2021-06-14 13:03:58 +02:00
if ( this . options . APIConfirms ) {
2021-06-23 11:17:39 +02:00
if ( buttonIndex !== undefined && this . options . APIConfirms [ buttonIndex ] !== undefined ) {
confirmFunction = ( ) => { return this . options . APIConfirms [ buttonIndex ] ( this . ajaxApi [ buttonIndex ] ) }
2021-06-14 13:03:58 +02:00
}
} else if ( this . options . APIConfirm ) {
2021-06-23 11:17:39 +02:00
confirmFunction = ( ) => { return this . options . APIConfirm ( this . ajaxApi ) }
2020-12-15 10:40:49 +01:00
}
let confirmResult = confirmFunction ( ( ) => { this . hide ( ) } , this , evt )
if ( confirmResult === undefined ) {
this . hide ( )
} else {
confirmResult . then ( ( data ) => {
if ( this . options . closeOnSuccess ) {
this . hide ( )
}
} )
2021-01-11 16:28:07 +01:00
. catch ( ( err ) => {
2020-12-15 13:40:26 +01:00
this . options . APIError ( ( ) => { this . hide ( ) } , this , evt )
2020-12-15 10:40:49 +01:00
} )
}
}
}
2020-12-15 13:40:26 +01:00
/** Attach the submission click listener for modals that have been generated by raw HTML */
2021-03-18 14:01:14 +01:00
findSubmitButtonAndAddListener ( ) {
2021-06-14 13:03:58 +02:00
let $modalFooter = this . $modal . find ( '.modal-footer' )
if ( $modalFooter . data ( 'custom-footer' ) ) { // setup basic listener as callback are defined in the template
let $submitButtons = this . $modal . find ( '.modal-footer .modal-confirm-button' )
var selfModal = this ;
selfModal . options . APIConfirms = [ ] ;
$submitButtons . each ( function ( i ) {
const $submitButton = $ ( this )
if ( $submitButton . data ( 'clickfunction' ) !== undefined && $submitButton . data ( 'clickfunction' ) !== '' ) {
const clickHandler = window [ $submitButton . data ( 'clickfunction' ) ]
selfModal . options . APIConfirms [ i ] = ( tmpApi ) => {
let clickResult = clickHandler ( selfModal , tmpApi )
if ( clickResult !== undefined ) {
return clickResult
. then ( ( data ) => {
if ( data . success ) {
2021-10-21 11:11:33 +02:00
selfModal . options . POSTSuccessCallback ( [ data , this ] )
2021-06-14 13:03:58 +02:00
} else { // Validation error
selfModal . injectFormValidationFeedback ( form , data . errors )
return Promise . reject ( 'Validation error' ) ;
}
} )
. catch ( ( errorMessage ) => {
selfModal . options . POSTFailCallback ( errorMessage )
return Promise . reject ( errorMessage ) ;
} )
}
}
}
2021-06-23 11:17:39 +02:00
$submitButton . click ( selfModal . getConfirmationHandlerFunction ( $submitButton , i ) )
2021-06-14 13:03:58 +02:00
} )
} else {
let $submitButton = this . $modal . find ( '.modal-footer #submitButton' )
if ( ! $submitButton [ 0 ] ) {
$submitButton = this . $modal . find ( '.modal-footer .modal-confirm-button' )
2021-03-10 09:24:39 +01:00
}
2021-06-14 13:03:58 +02:00
if ( $submitButton [ 0 ] ) {
const formID = $submitButton . data ( 'form-id' )
let $form
if ( formID ) {
$form = $ ( formID )
} else {
$form = this . $modal . find ( 'form' )
}
if ( $submitButton . data ( 'confirmfunction' ) !== undefined && $submitButton . data ( 'confirmfunction' ) !== '' ) {
const clickHandler = window [ $submitButton . data ( 'confirmfunction' ) ]
this . options . APIConfirm = ( tmpApi ) => {
let clickResult = clickHandler ( this , tmpApi )
if ( clickResult !== undefined ) {
return clickResult
. then ( ( data ) => {
if ( data . success ) {
2021-10-21 11:11:33 +02:00
this . options . POSTSuccessCallback ( [ data , this ] )
2021-06-14 13:03:58 +02:00
} else { // Validation error
this . injectFormValidationFeedback ( form , data . errors )
return Promise . reject ( 'Validation error' ) ;
}
} )
. catch ( ( errorMessage ) => {
this . options . POSTFailCallback ( errorMessage )
return Promise . reject ( errorMessage ) ;
} )
}
}
} else {
$submitButton [ 0 ] . removeAttribute ( 'onclick' )
this . options . APIConfirm = ( tmpApi ) => {
return tmpApi . postForm ( $form [ 0 ] )
2021-03-23 10:04:06 +01:00
. then ( ( data ) => {
if ( data . success ) {
2021-06-23 11:17:39 +02:00
// this.options.POSTSuccessCallback(data)
this . options . POSTSuccessCallback ( [ data , this ] )
2021-03-23 10:04:06 +01:00
} else { // Validation error
this . injectFormValidationFeedback ( form , data . errors )
return Promise . reject ( 'Validation error' ) ;
}
} )
. catch ( ( errorMessage ) => {
2021-06-23 11:17:39 +02:00
this . options . POSTFailCallback ( [ errorMessage , this ] )
// this.options.POSTFailCallback(errorMessage)
2021-03-23 10:04:06 +01:00
return Promise . reject ( errorMessage ) ;
} )
}
2021-03-18 14:01:14 +01:00
}
2021-06-23 11:17:39 +02:00
$submitButton . click ( this . getConfirmationHandlerFunction ( $submitButton ) )
2021-03-10 09:24:39 +01:00
}
2020-12-15 10:40:49 +01:00
}
}
2020-12-10 15:18:02 +01:00
}
2020-12-15 13:40:26 +01:00
/** Class representing an loading overlay */
2020-12-10 15:18:02 +01:00
class OverlayFactory {
2020-12-15 13:40:26 +01:00
/ * *
* Create a loading overlay
2021-01-18 09:49:20 +01:00
* @ param { ( jQuery | string | HTMLButtonElement ) } node - The node on which the overlay should be placed
* @ param { Object } options - The options supported by OverlayFactory # defaultOptions
2020-12-15 13:40:26 +01:00
* /
2021-01-12 11:30:15 +01:00
constructor ( node , options = { } ) {
2020-12-15 13:40:26 +01:00
this . node = node
this . $node = $ ( this . node )
2021-09-16 15:53:59 +02:00
this . options = Object . assign ( { } , OverlayFactory . defaultOptions , options )
2021-01-12 11:30:15 +01:00
this . options . auto = options . auto ? this . options . auto : ! ( options . variant || options . spinnerVariant )
2020-12-15 13:40:26 +01:00
if ( this . options . auto ) {
this . adjustOptionsBasedOnNode ( )
2020-12-10 15:18:02 +01:00
}
}
2020-12-15 13:40:26 +01:00
/ * *
* @ namespace
2021-01-14 12:04:24 +01:00
* @ property { string } text - A small text indicating the reason of the overlay
2020-12-15 13:40:26 +01:00
* @ property { string = ( 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark' | 'white' | 'transparent' ) } variant - The variant of the overlay
2021-10-04 16:03:15 +02:00
* @ property { number | string } opacity - The opacity of the overlay
2020-12-15 13:40:26 +01:00
* @ property { boolean } rounded - If the overlay should be rounded
2021-07-22 15:51:06 +02:00
* @ property { boolean } auto - Whether overlay and spinner options should be adapted automatically based on the node
2020-12-15 13:40:26 +01:00
* @ property { string = ( 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark' | 'white' | 'transparent' ) } spinnerVariant - The variant of the spinner
* @ property { boolean } spinnerSmall - If the spinner inside the overlay should be small
2021-01-12 11:30:15 +01:00
* @ property { string = ( 'border' | 'grow' ) } spinnerSmall - If the spinner inside the overlay should be small
2020-12-15 13:40:26 +01:00
* /
2020-12-10 15:18:02 +01:00
static defaultOptions = {
2021-01-14 12:04:24 +01:00
text : '' ,
2021-10-04 16:03:15 +02:00
variant : '' ,
opacity : '' ,
2020-12-10 15:18:02 +01:00
blur : '2px' ,
rounded : false ,
2020-12-15 13:40:26 +01:00
auto : true ,
2020-12-10 15:18:02 +01:00
spinnerVariant : '' ,
spinnerSmall : false ,
2021-01-18 09:49:20 +01:00
spinnerType : 'border' ,
2021-08-26 12:06:12 +02:00
fallbackBoostrapVariant : '' ,
wrapperCSSDisplay : '' ,
2020-12-10 15:18:02 +01:00
}
static overlayWrapper = '<div aria-busy="true" class="position-relative"/>'
2021-10-04 16:03:15 +02:00
static overlayContainer = '<div class="position-absolute text-nowrap loading-overlay-container" style="inset: 0px; z-index: 1100;"/>'
2021-09-16 15:53:59 +02:00
static overlayBg = '<div class="position-absolute loading-overlay" style="inset: 0px;"/>'
2021-01-12 11:30:15 +01:00
static overlaySpinner = '<div class="position-absolute" style="top: 50%; left: 50%; transform: translateX(-50%) translateY(-50%);"><span aria-hidden="true" class=""><!----></span></div></div>'
2021-01-14 12:04:24 +01:00
static overlayText = '<span class="ml-1 align-text-top"></span>'
2020-12-10 15:18:02 +01:00
shown = false
originalNodeIndex = 0
2020-12-15 13:40:26 +01:00
/** Create the HTML of the overlay */
2020-12-10 15:18:02 +01:00
buildOverlay ( ) {
this . $overlayWrapper = $ ( OverlayFactory . overlayWrapper )
2021-08-26 12:06:12 +02:00
if ( this . options . wrapperCSSDisplay ) {
this . $overlayWrapper . css ( 'display' , this . options . wrapperCSSDisplay )
}
if ( this . $node [ 0 ] ) {
const boundingRect = this . $node [ 0 ] . getBoundingClientRect ( )
2021-09-17 19:06:56 +02:00
this . $overlayWrapper . css ( 'min-height' , Math . max ( boundingRect . height , 20 ) )
this . $overlayWrapper . css ( 'min-width' , Math . max ( boundingRect . width , 20 ) )
2021-10-18 13:07:18 +02:00
if ( this . $node . hasClass ( 'row' ) ) {
this . $overlayWrapper . addClass ( 'row' )
}
2021-08-26 12:06:12 +02:00
}
2020-12-10 15:18:02 +01:00
this . $overlayContainer = $ ( OverlayFactory . overlayContainer )
this . $overlayBg = $ ( OverlayFactory . overlayBg )
. addClass ( [ ` bg- ${ this . options . variant } ` , ( this . options . rounded ? 'rounded' : '' ) ] )
2021-10-04 16:03:15 +02:00
if ( this . options . opacity !== '' ) {
this . $overlayBg . css ( 'opacity' , this . options . opacity )
}
2020-12-10 15:18:02 +01:00
this . $overlaySpinner = $ ( OverlayFactory . overlaySpinner )
2021-01-12 11:30:15 +01:00
this . $overlaySpinner . children ( ) . addClass ( ` spinner- ${ this . options . spinnerType } ` )
2020-12-10 15:18:02 +01:00
if ( this . options . spinnerSmall ) {
2021-01-12 11:30:15 +01:00
this . $overlaySpinner . children ( ) . addClass ( ` spinner- ${ this . options . spinnerType } -sm ` )
2020-12-10 15:18:02 +01:00
}
if ( this . options . spinnerVariant . length > 0 ) {
this . $overlaySpinner . children ( ) . addClass ( ` text- ${ this . options . spinnerVariant } ` )
}
2021-01-14 12:04:24 +01:00
if ( this . options . text . length > 0 ) {
this . $overlayText = $ ( OverlayFactory . overlayText ) ;
this . $overlayText . addClass ( ` text- ${ this . options . spinnerVariant } ` )
. text ( this . options . text )
this . $overlaySpinner . append ( this . $overlayText )
}
2020-12-10 15:18:02 +01:00
}
2020-12-15 13:40:26 +01:00
/** Create the overlay, attach it to the DOM and display it */
2020-12-10 15:18:02 +01:00
show ( ) {
2020-12-15 13:40:26 +01:00
this . buildOverlay ( )
this . mountOverlay ( )
this . shown = true
2020-12-10 15:18:02 +01:00
}
2020-12-15 13:40:26 +01:00
/** Hide the overlay and remove it from the DOM */
2020-12-10 15:18:02 +01:00
hide ( ) {
2020-12-15 13:40:26 +01:00
if ( this . shown ) {
2020-12-10 15:18:02 +01:00
this . unmountOverlay ( )
}
this . shown = false
}
2020-12-15 13:40:26 +01:00
/** Attach the overlay to the DOM */
2020-12-10 15:18:02 +01:00
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 )
}
2020-12-15 13:40:26 +01:00
/** Remove the overlay from the DOM */
2020-12-10 15:18:02 +01:00
unmountOverlay ( ) {
this . appendToIndex ( this . $node , this . $overlayWrapper . parent ( ) , this . originalNodeIndex )
this . $overlayWrapper . remove ( )
this . originalNodeIndex = 0
}
2020-12-15 13:40:26 +01:00
/** Append a node to the provided DOM index */
2020-12-10 15:18:02 +01:00
appendToIndex ( $node , $targetContainer , index ) {
const $target = $targetContainer . children ( ) . eq ( index ) ;
$node . insertBefore ( $target ) ;
}
2020-12-15 13:40:26 +01:00
/** Adjust instance's options based on the provided node */
adjustOptionsBasedOnNode ( ) {
if ( this . $node . width ( ) < 50 || this . $node . height ( ) < 50 ) {
2020-12-10 15:18:02 +01:00
this . options . spinnerSmall = true
}
2021-01-18 09:49:20 +01:00
if ( this . $node . is ( 'input[type="checkbox"]' ) || this . $node . css ( 'border-radius' ) !== '0px' ) {
2020-12-10 15:18:02 +01:00
this . options . rounded = true
2021-08-26 12:06:12 +02:00
}
this . options . wrapperCSSDisplay = this . $node . css ( 'display' )
2021-01-18 09:49:20 +01:00
let classes = this . $node . attr ( 'class' )
if ( classes !== undefined ) {
classes = classes . split ( ' ' )
const detectedVariant = OverlayFactory . detectedBootstrapVariant ( classes , this . options . fallbackBoostrapVariant )
this . options . spinnerVariant = detectedVariant
2020-12-10 15:18:02 +01:00
}
}
2020-12-15 13:40:26 +01:00
/ * *
* Detect the bootstrap variant from a list of classes
* @ param { Array } classes - A list of classes containg a bootstrap variant
* /
2021-01-18 09:49:20 +01:00
static detectedBootstrapVariant ( classes , fallback = OverlayFactory . defaultOptions . fallbackBoostrapVariant ) {
2020-12-15 13:40:26 +01:00
const re = /^[a-zA-Z]+-(?<variant>primary|success|danger|warning|info|light|dark|white|transparent)$/ ;
2020-12-10 15:18:02 +01:00
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
}
}
}
2021-01-18 09:49:20 +01:00
return fallback
2020-12-10 15:18:02 +01:00
}
2021-01-12 10:16:58 +01:00
}
2021-01-13 14:20:04 +01:00
/** Class representing a FormValidationHelper */
class FormValidationHelper {
2021-01-12 10:16:58 +01:00
/ * *
2021-01-13 14:20:04 +01:00
* Create a FormValidationHelper .
* @ param { Object } options - The options supported by FormValidationHelper # defaultOptions
2021-01-12 10:16:58 +01:00
* /
constructor ( form , options = { } ) {
this . form = form
this . options = Object . assign ( { } , Toaster . defaultOptions , options )
}
/ * *
* @ namespace
* /
static defaultOptions = {
}
/ * *
* Create node containing validation information from validationError . If no field can be associated to the error , it will be placed on top
2021-01-13 14:20:04 +01:00
* @ param { Object } validationErrors - The validation errors to be displayed . Keys are the fieldName that had errors , values are the error text
2021-01-12 10:16:58 +01:00
* /
injectValidationErrors ( validationErrors ) {
this . cleanValidationErrors ( )
for ( const [ fieldName , errors ] of Object . entries ( validationErrors ) ) {
this . injectValidationErrorInForm ( fieldName , errors )
}
}
injectValidationErrorInForm ( fieldName , errors ) {
const inputField = Array . from ( this . form ) . find ( node => { return node . name == fieldName } )
if ( inputField !== undefined ) {
2021-06-30 12:18:58 +02:00
const $messageNode = this . buildValidationMessageNode ( fieldName , errors )
2021-01-12 10:16:58 +01:00
const $inputField = $ ( inputField )
$inputField . addClass ( 'is-invalid' )
$messageNode . insertAfter ( $inputField )
} else {
2021-06-30 12:18:58 +02:00
const $messageNode = this . buildValidationMessageNode ( fieldName , errors , true )
2021-01-12 10:16:58 +01:00
const $flashContainer = $ ( this . form ) . parent ( ) . find ( '#flashContainer' )
$messageNode . insertAfter ( $flashContainer )
}
}
2021-06-30 12:18:58 +02:00
buildValidationMessageNode ( fieldName , errors , isAlert = false ) {
2021-01-12 10:16:58 +01:00
const $messageNode = $ ( '<div></div>' )
if ( isAlert ) {
$messageNode . addClass ( 'alert alert-danger' ) . attr ( 'role' , 'alert' )
2021-06-30 12:18:58 +02:00
$messageNode . append ( $ ( '<strong></strong>' ) . text ( ` ${ fieldName } : ` ) )
2021-01-12 10:16:58 +01:00
} else {
$messageNode . addClass ( 'invalid-feedback' )
}
2021-06-17 14:13:10 +02:00
if ( typeof errors === 'object' ) {
const hasMultipleErrors = Object . keys ( errors ) . length > 1
for ( const [ ruleName , error ] of Object . entries ( errors ) ) {
if ( hasMultipleErrors ) {
$messageNode . append ( $ ( '<li></li>' ) . text ( error ) )
} else {
2021-06-30 12:18:58 +02:00
$messageNode . append ( $ ( '<span></span>' ) . text ( error ) )
2021-06-17 14:13:10 +02:00
}
2021-01-12 10:16:58 +01:00
}
2021-06-17 14:13:10 +02:00
} else {
$messageNode . text ( errors )
2021-01-12 10:16:58 +01:00
}
return $messageNode
}
cleanValidationErrors ( ) {
$ ( this . form ) . find ( 'textarea, input, select' ) . removeClass ( 'is-invalid' )
$ ( this . form ) . find ( '.invalid-feedback' ) . remove ( )
$ ( this . form ) . parent ( ) . find ( '.alert' ) . remove ( )
}
2021-01-15 16:58:46 +01:00
}
class HtmlHelper {
static table ( head = [ ] , body = [ ] , options = { } ) {
const $table = $ ( '<table/>' )
const $thead = $ ( '<thead/>' )
const $tbody = $ ( '<tbody/>' )
$table . addClass ( 'table' )
if ( options . striped ) {
$table . addClass ( 'table-striped' )
}
if ( options . bordered ) {
$table . addClass ( 'table-bordered' )
}
if ( options . borderless ) {
$table . addClass ( 'table-borderless' )
}
if ( options . hoverable ) {
$table . addClass ( 'table-hover' )
}
if ( options . small ) {
$table . addClass ( 'table-sm' )
}
if ( options . variant ) {
$table . addClass ( ` table- ${ options . variant } ` )
}
if ( options . tableClass ) {
$table . addClass ( options . tableClass )
}
const $caption = $ ( '<caption/>' )
if ( options . caption ) {
if ( options . caption instanceof jQuery ) {
$caption = options . caption
} else {
$caption . text ( options . caption )
}
}
const $theadRow = $ ( '<tr/>' )
head . forEach ( head => {
if ( head instanceof jQuery ) {
$theadRow . append ( $ ( '<td/>' ) . append ( head ) )
} else {
$theadRow . append ( $ ( '<th/>' ) . text ( head ) )
}
} )
$thead . append ( $theadRow )
body . forEach ( row => {
const $bodyRow = $ ( '<tr/>' )
row . forEach ( item => {
if ( item instanceof jQuery ) {
$bodyRow . append ( $ ( '<td/>' ) . append ( item ) )
} else {
$bodyRow . append ( $ ( '<td/>' ) . text ( item ) )
}
} )
$tbody . append ( $bodyRow )
} )
$table . append ( $caption , $thead , $tbody )
if ( options . responsive ) {
options . responsiveBreakpoint = options . responsiveBreakpoint !== undefined ? options . responsiveBreakpoint : ''
$table = $ ( '<div/>' ) . addClass ( options . responsiveBreakpoint !== undefined ? ` table-responsive- ${ options . responsiveBreakpoint } ` : 'table-responsive' ) . append ( $table )
}
return $table
}
2021-08-24 10:48:53 +02:00
}