66 *
77 */
88var csrf_field = $ ( 'meta[name=csrf_field_name]' ) . attr ( 'content' ) ;
9- var csrf_token = $ ( 'meta[name=' + csrf_field + ']' ) . attr ( 'content' ) ;
9+ var csrf_token = $ ( 'meta[name=' + csrf_field + ']' ) . attr ( 'content' ) ;
1010
1111htmx . on ( 'htmx:configRequest' , ( event ) => {
1212 if ( csrf_token ) {
@@ -17,12 +17,12 @@ htmx.on('htmx:configRequest', (event) => {
1717function htmx_cleanup_before_swap ( event ) {
1818 // Dispose any active tooltips before HTMX swaps the DOM
1919 event . detail . target . querySelectorAll ( '[data-bs-toggle="tooltip"]' ) . forEach (
20- el => {
21- const tooltip = bootstrap . Tooltip . getInstance ( el )
22- if ( tooltip ) {
23- tooltip . dispose ( )
24- }
25- } )
20+ el => {
21+ const tooltip = bootstrap . Tooltip . getInstance ( el )
22+ if ( tooltip ) {
23+ tooltip . dispose ( )
24+ }
25+ } )
2626}
2727document . body . addEventListener ( "htmx:beforeSwap" , htmx_cleanup_before_swap ) ;
2828document . body . addEventListener ( "htmx:oobBeforeSwap" , htmx_cleanup_before_swap ) ;
@@ -40,41 +40,126 @@ function htmx_initialize_ckan_modules(event) {
4040 }
4141
4242 event . detail . target . querySelectorAll ( '[data-bs-toggle="tooltip"]'
43- ) . forEach ( node => {
43+ ) . forEach ( node => {
4444 bootstrap . Tooltip . getOrCreateInstance ( node )
4545 } )
46+
4647 event . detail . target . querySelectorAll ( '.show-filters' ) . forEach ( node => {
47- node . onclick = function ( ) {
48+ node . onclick = function ( ) {
4849 $ ( "body" ) . addClass ( "filters-modal" )
4950 }
5051 } )
52+
5153 event . detail . target . querySelectorAll ( '.hide-filters' ) . forEach ( node => {
52- node . onclick = function ( ) {
54+ node . onclick = function ( ) {
5355 $ ( "body" ) . removeClass ( "filters-modal" )
5456 }
5557 } )
5658}
57- document . body . addEventListener ( "htmx:afterSwap" , htmx_initialize_ckan_modules ) ;
59+
60+ document . body . addEventListener ( "htmx:afterSwap" , function ( event ) {
61+ htmx_initialize_ckan_modules ( event ) ;
62+
63+ const element = event . detail . requestConfig ?. elt ;
64+ if ( ! element ) return ;
65+
66+ const toastHandler = new ToastHandler ( element ) ;
67+
68+ if ( event . detail . successful ) {
69+ toastHandler . showToast ( ) ;
70+ }
71+ } ) ;
72+
73+ /**
74+ * ToastHandler parses a single JSON-like attribute from an HTML element
75+ * and triggers a CKAN toast notification.
76+ *
77+ * It expects a `data-hx-toast` attribute to be present on the element,
78+ * which should contain a JSON string with the toast configuration:
79+ *
80+ * <div hx-target='..' hx-get='...' data-hx-toast='{"message": "Something happened", "type": "info"}'></div>
81+ *
82+ * Use it together with HTMX to show notifications after actions.
83+ *
84+ * @class
85+ * @param {HTMLElement } element - The element containing the toast config.
86+ */
87+ class ToastHandler {
88+ constructor ( element ) {
89+ this . attrKey = "data-hx-toast" ;
90+ this . defaultToastOptions = {
91+ type : "success" ,
92+ title : ckan . i18n . _ ( "Notification" ) ,
93+ } ;
94+ this . options = this . buildToastOptions ( element ) ;
95+ }
96+
97+ /**
98+ * Parses the JSON string from the toast attribute and merges with defaults.
99+ *
100+ * @param {HTMLElement } element
101+ *
102+ * @returns {Object }
103+ */
104+ buildToastOptions ( element ) {
105+ const attrValue = element . getAttribute ( this . attrKey ) ;
106+ if ( ! attrValue ) return this . defaultToastOptions ;
107+
108+ try {
109+ const parsed = JSON . parse ( attrValue ) ;
110+ return { ...this . defaultToastOptions , ...parsed } ;
111+ } catch ( e ) {
112+ console . error ( `Invalid JSON in ${ this . attrKey } :` , attrValue ) ;
113+ return {
114+ ...this . defaultToastOptions ,
115+ message : `Invalid toast config: ${ e . message } ` ,
116+ type : "danger"
117+ } ;
118+ }
119+ }
120+
121+ showToast ( ) {
122+ if ( ! this . options . message ) return ;
123+ ckan . toast ( this . options ) ;
124+ }
125+ }
126+
58127document . body . addEventListener ( "htmx:oobAfterSwap" , htmx_initialize_ckan_modules ) ;
59128
60- document . body . addEventListener ( "htmx:responseError" , function ( event ) {
61- const xhr = event . detail . xhr
62- const error = $ ( xhr . response ) . find ( '#error-content' )
63- const headerHTML = error . find ( 'h1' ) . remove ( ) . html ( ) || `${ xhr . status } ${ xhr . statusText } `
64- const messageHTML = error . html ( ) || event . detail . error
65- $ ( '#responseErrorToast' ) . remove ( )
66- $ ( `
67- <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
68- <div id="responseErrorToast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
69- <div class="toast-header">
70- <strong class="me-auto text-danger">${ headerHTML } </strong>
71- <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="${ ckan . i18n . _ ( "Close" ) } "></button>
72- </div>
73- <div class="toast-body">
74- ${ messageHTML }
75- </div>
76- </div>
77- </div>
78- ` ) . appendTo ( 'body' )
79- $ ( '#responseErrorToast' ) . toast ( 'show' )
129+ document . body . addEventListener ( "htmx:responseError" , function ( event ) {
130+ const xhr = event . detail . xhr ;
131+
132+ if ( xhr . response . startsWith ( "<!doctype html>" ) ) {
133+ const error = $ ( xhr . response ) . find ( '#error-content' ) ;
134+ var message = error . html ( ) || event . detail . error ;
135+ } else {
136+ var message = xhr . responseText ;
137+ }
138+
139+ ckan . toast ( {
140+ message : message . trim ( ) . replace ( / ^ " ( .* ) " $ / , '$1' ) ,
141+ type : "danger" ,
142+ title : `${ xhr . status } ${ xhr . statusText } `
143+ } ) ;
144+ } )
145+
146+ document . addEventListener ( "htmx:confirm" , function ( e ) {
147+ // The event is triggered on every trigger for a request, so we need to check if the element
148+ // that triggered the request has a confirm question set via the hx-confirm attribute,
149+ // if not we can return early and let the default behavior happen
150+ if ( ! e . detail . question ) return
151+
152+ // This will prevent the request from being issued to later manually issue it
153+ e . preventDefault ( )
154+
155+ ckan . confirm ( {
156+ message : e . detail . question ,
157+ type : "primary" ,
158+ centered : true ,
159+ onConfirm : ( ) => {
160+ // If the user confirms, we manually issue the request
161+ // true to skip the built-in window.confirm()
162+ e . detail . issueRequest ( true ) ;
163+ }
164+ } ) ;
80165} )
0 commit comments