1 2/** 3 * Save the sidebar menu collapse state. 4 */ 5const sidebar_collapse_toggler = document.getElementById( 'sidebar-collapse-toggler' ); 6if ( sidebar_collapse_toggler ) { 7 8 sidebar_collapse_toggler.addEventListener( 'change', ( event ) => { 9 if ( sidebar_collapse_toggler.checked === true ) { 10 localStorage.setItem( 'menu-sidebar-state', 'closed' ); 11 } else { 12 localStorage.setItem( 'menu-sidebar-state', 'opened' ); 13 } 14 } ); 15 16 document.addEventListener( 'DOMContentLoaded', function() { 17 const menu_sidebar_state = localStorage.getItem( 'menu-sidebar-state' ); 18 if ( menu_sidebar_state === 'opened' ) { 19 sidebar_collapse_toggler.checked = false; 20 } else if ( menu_sidebar_state === 'closed' ) { 21 sidebar_collapse_toggler.checked = true; 22 } 23 } ); 24 25} 26 27/** 28 * Confimation message trigger. 29 * 30 * Usage: <a href="#" data-confirmation="Are you sure?"> 31 * Usage: <button type="submit" data-confirmation="Are you sure?"> (untested) 32 */ 33document.addEventListener( 'click', function( e ) { 34 if ( e.target.hasAttribute( 'data-confirmation' ) && e.target.dataset.confirmation.length ) { 35 if ( ! window.confirm( e.target.dataset.confirmation ) ) { 36 e.preventDefault(); 37 } 38 } 39} ); 40 41/** 42 * Enable tooltips 43 */ 44 45function saas_enable_tooltips () { 46 const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); 47 if ( tooltipTriggerList ) { 48 [ ...tooltipTriggerList ].map( tooltipTriggerEl => new Tooltip( tooltipTriggerEl, { 49 html: true, 50 container: tooltipTriggerEl 51 } ) ); 52 } 53} 54 55document.addEventListener( 'DOMContentLoaded', function() { 56 57 // Page load 58 saas_enable_tooltips(); 59 60 // Gravity forms ajax reload 61 if ( typeof jQuery !== 'undefined' ) { 62 jQuery( document ).on( 'gform_post_render', saas_enable_tooltips ); 63 } 64 65 // On get-template-part response 66 document.body.addEventListener( 'ajax-template-loaded', saas_enable_tooltips ); 67 68 // On tab change (if they were invisible before) 69 document.addEventListener( 'shown.bs.tab', saas_enable_tooltips ); 70 71} ); 72 73// Loop through all dropdowns inside datatables and set their boundary to the wrapper div instead of the first parent with relative positioning 74document.addEventListener( 'DOMContentLoaded', ( e ) => { 75 if ( typeof jQuery !== 'undefined' ) { 76 77 jQuery( document ).on( 'draw.dt', 'table', function( e ) { 78 const dropdownElementList = document.querySelectorAll( '.dataTables_wrapper .dropdown-toggle' ); 79 const dropdownList = [...dropdownElementList].map( ( dropdownToggleEl ) => { 80 dropdownToggleEl.classList.add( '-fixed' ); 81 new Dropdown( dropdownToggleEl, { 82 popperConfig: function( obj ) { 83 obj.strategy = 'fixed'; 84 return obj; 85 } 86 } ); 87 } ); 88 } ); 89 90 } 91} ); 92 93// Confirmation submit modal logic 94//todo Only load where needed? See class-gf-field-saas-tab.php -> $saas_form_tab_is_inited 95document.addEventListener( 'DOMContentLoaded', function() { 96 97 var saas_confirmation_submit_modal = document.querySelector( '.saas-confirmation-submit-modal' ); 98 if ( saas_confirmation_submit_modal ) { 99 100 var confirmationModalInstance = new Modal( saas_confirmation_submit_modal, {}); 101 var form = saas_confirmation_submit_modal.closest( 'form' ); 102 var modal_submit_btn = saas_confirmation_submit_modal.querySelector( '.submit-btn' ); 103 var form_submit_btn = form.querySelector( '#gform_submit_button_' + modal_submit_btn.getAttribute( 'data-form_id' ) ); 104 var required_modal_checkboxes = saas_confirmation_submit_modal.querySelectorAll( 'input[required]' ); 105 var form_has_tabs = form.querySelector( '.tab-pane' ); 106 var modal_tab_id = form_has_tabs ? saas_confirmation_submit_modal.closest( '.tab-pane' ).getAttribute( 'aria-labelledby' ) : null; 107 var save_draft_buttons = form.querySelector( '[name="is-modds-quick-draft"], [name="save-draft"], .btn.no-submit' ); 108 109 // Initial state of submit button 110 var checked_required_checkboxes = saas_confirmation_submit_modal.querySelectorAll( 'input[required]:checked' ); 111 if ( checked_required_checkboxes.length === required_modal_checkboxes.length ) { 112 modal_submit_btn.removeAttribute( 'disabled' ); 113 } 114 115 var submit_check = function( e ) { 116 e.preventDefault(); 117 e.stopPropagation(); 118 if ( form_has_tabs ) {
119 jQuery( '#' + modal_tab_id ).tab( 'show' ); 120 } 121 confirmationModalInstance.show(); 122 }; 123 124 // Clicking submit outside of modal = open modal 125 form.addEventListener( 'submit', submit_check ); 126 127 // Exclude the draft button 128 if ( save_draft_buttons ) { 129 save_draft_buttons.addEventListener( 'click', function() { 130 form.removeEventListener( 'submit', submit_check ); 131 } ); 132 } 133 134 // Clicking from modal = submit 135 modal_submit_btn.addEventListener( 'click', ( e ) => { 136 form.removeEventListener( 'submit', submit_check ); 137 confirmationModalInstance.hide(); // Otherwise layout will break on failed validation 138 form_submit_btn.click(); 139 } ); 140 141 // Update submit button disabled state according to required checkboxes 142 required_modal_checkboxes.forEach( ( checkbox ) => { 143 checkbox.addEventListener('change', ( e ) => { 144 checked_required_checkboxes = saas_confirmation_submit_modal.querySelectorAll( 'input[required]:checked' ); 145 if ( checked_required_checkboxes.length === required_modal_checkboxes.length ) { 146 modal_submit_btn.removeAttribute( 'disabled' ); 147 } else { 148 modal_submit_btn.setAttribute( 'disabled', '' ); 149 } 150 }); 151 }); 152 153 } 154 155} ); 156 157// Reload GF form after submit (only applicable to modal forms) 158//todo Combine with toast trigger script 159document.addEventListener( 'DOMContentLoaded', function() { 160 if ( typeof jQuery !== 'undefined' ) { 161 162 var $forms = jQuery( '.saas-form-modal .gform_wrapper' ); 163 if ( $forms.length ) { 164 $forms.each( function() { 165 166 var $form = jQuery( this ); 167 var $parent = $form.parent(); 168 var form_id = $form.attr( 'id' ).replace( 'gform_wrapper_', '' ); 169 var form_html = jQuery( '<div />' ).append( $form.clone() ).html(); 170 171 jQuery( document ).on( 'gform_confirmation_loaded', function( event, formId ) { 172 if ( formId == form_id ) { 173 174 $parent.find( '#gform_confirmation_wrapper_' + form_id ).replaceWith( form_html ); 175 $parent.find( '.gform_anchor + .gform_anchor' ).remove(); 176 jQuery( document ).trigger( 'gform_post_render', [ form_id, 0 ] ); 177 178 } 179 } ); 180 181 } ); 182 } 183 184 } 185} ); 186 187// Hide arrows on non-looped carousels 188document.addEventListener( 'DOMContentLoaded', ( e ) => { 189 190 // Toggle (show/hide) next/prev buttons when relevant. 191 const toggle_navigation = function( carousel ) { 192 193 const prev_buttons = document.querySelectorAll( '[data-bs-target="#' + carousel.id + '"][data-bs-slide="prev"]' ); 194 const next_buttons = document.querySelectorAll( '[data-bs-target="#' + carousel.id + '"][data-bs-slide="next"]' ); 195 196 // Hide prev button if the first slide is active 197 if ( prev_buttons ) { 198 prev_buttons.forEach( prev_button => { 199 prev_button.classList.toggle( '-hidden', carousel.querySelector( '.carousel-item:first-child.active' ) ); 200 } ); 201 } 202 203 // Hide next button if the first slide is active 204 if ( next_buttons ) { 205 next_buttons.forEach( next_button => { 206 next_button.classList.toggle( '-hidden', carousel.querySelector( '.carousel-item:last-child.active' ) ); 207 } ); 208 } 209 210 } 211 212 // Set initial navigation state 213 const init = function() { 214 const carousels_with_hard_stops = document.querySelectorAll( '.carousel[data-bs-wrap="false"]' ); 215 if ( carousels_with_hard_stops ) { 216 carousels_with_hard_stops.forEach( carousel => { 217 toggle_navigation( carousel ); 218 } ); 219 } 220 } 221 222 // On pageload 223 init(); 224 225 // On ajax load event 226 document.body.addEventListener( 'ajax-template-loaded', function() { 227 init(); 228 } ); 229 230 // On slide event (of any carousel) 231 document.addEventListener( 'slid.bs.carousel', function( e ) { 232 233 // Toggle navigation arrows 234 const carousel = e.target; 235 const wrap = carousel.getAttribute( 'data-bs-wrap' ); 236 if ( ! wrap || wrap === '0' || wrap === 'false' ) { 237 toggle_navigation( carousel ); 238 } 239 240 // Carousel page info component 241 const carousel_page_infos = document.querySelectorAll( '.carousel-page-info[data-bs-target="#' + e.target.id + '"]' ); 242 if ( carousel_page_infos ) { 243 carousel_page_infos.forEach( el => { 244 el.querySelector( '.-num' ).textContent = e.to + 1; 245 } ); 246 } 247 248 } ); 249 250} ); 251 252// Tab anchor functionality 253function saas_activate_matching_tab( tab_trigger ) { 254 255 // Exceptions 256 if ( ! tab_trigger ) { 257 return true; 258 } 259 if ( window.getComputedStyle( tab_trigger, null ).display === 'none' ) { 260 return true; 261 } 262 if ( ! document.getElementById( tab_trigger.getAttribute( 'aria-controls' ) ) ) { 263 return true; 264 } 265 266 // Activate the matching tab 267 const tab = new Tab( tab_trigger ); 268 tab.show(); 269 270 // Scroll to the tab content on page load 271 // Hacky timeout: doesn't scroll on form page unless delayed for 250ms or more 272 setTimeout( () => { 273 tab_trigger.parentElement.scrollIntoView( { behavior: 'smooth', block: 'start' } ); 274 }, 250 ); 275 276} 277 278function saas_tab_anchor_init() { 279 const tabs_container = document.querySelector( '.nav-tabs' ); 280 if ( tabs_container ) { 281 282 // Open the tab based on the ID from the URL and scroll to the tab content 283 let tab_id_from_url = window.location.hash ? window.location.hash.slice(1) : ''; 284 if ( tab_id_from_url ) { 285 tab_id_from_url = tab_id_from_url.replace( 'tab_anchor_', ''); 286 saas_activate_matching_tab( document.querySelector( `[data-bs-target="#${tab_id_from_url}"]` ) ); 287 } 288 289 // Update the URL with the active tab's ID on tab activation 290 document.addEventListener( 'show.bs.tab', function( e ) { 291 const tab_id = 'tab_anchor_' + e.target.getAttribute( 'data-bs-target' ).slice(1); 292 history.replaceState( null, null, `#${tab_id}` ); 293 } ); 294 295 // Add click event listener to document to check for matching anchors 296 document.addEventListener('click', e => { 297 const clickedElement = e.target; 298 let anchor = clickedElement.getAttribute('href'); 299 if (anchor && anchor.includes( 'tab_anchor_' ) ) { 300 let tab_id = anchor.split( 'tab_anchor_' )[1];
301 saas_activate_matching_tab( document.querySelector( `[data-bs-target="#${tab_id}"]` ) ); 302 } 303 }); 304 305 } 306} 307window.addEventListener( 'load', saas_tab_anchor_init ); 308 309document.addEventListener( 'click', function( e ) { 310 if ( e.target.closest( '.saas-expand-text' ) ) { 311 e.stopPropagation(); 312 e.preventDefault(); 313 e.target.closest( '.saas-expand-text' ).previousSibling.classList.toggle( 'text-truncate-2' ); 314 } 315} ); 316 317// Gravity Forms JS filters 318if ( typeof gform !== 'undefined' ) { 319 gform.addFilter('gform_file_upload_markup', function (html, file, up, strings, imagesUrl) { 320 var formId = up.settings.multipart_params.form_id, 321 fieldId = up.settings.multipart_params.field_id; 322 html = `<button class='btn gform_delete_file p-0 pb-1' onclick='gformDeleteUploadedFile(${formId},${fieldId}, this);'> 323 <i class='fa-solid fa-trash text-danger'></i> 324 <span class='screen-reader-text'>${strings.delete_file}</span> 325 <span class="text-body fw-normal text-lowercase ps-1">${file.name}</span> 326 </button>`; 327 328 return html; 329 }); 330} 331 332// Gravity Forms repeater2 customization 333// window.addEventListener( 'load', () => { 334document.addEventListener( 'gf-repeater2-ready', ( event ) => { 335 336 let repeater_index = 1; 337 let message; 338 while ( document.querySelector( '[data-repeater2-parentid="' + repeater_index + '"]' ) ) { 339 340 let repeater_child_fields = document.querySelectorAll( '[data-repeater2-parentid="' + repeater_index + '"][data-repeater2-repeatid="1"]' ); 341 let have_more = document.querySelectorAll( '[data-repeater2-parentid="' + repeater_index + '"][data-repeater2-repeatid="2"]' ).length; 342 if ( repeater_child_fields.length ) { 343 344 let last_field = repeater_child_fields[ repeater_child_fields.length - 1 ]; 345 let gf_repeater2_remove = last_field.nextSibling.querySelector( '.gf_repeater2_remove' ); 346 347 // If only 1 item is available on page load, make the remove button disabled 348 if ( ! have_more ) { 349 last_field.querySelector( 'label' ).innerHTML += `<a href="#" class="saas_repeater2_remove float-end text-danger pe-none opacity-50">${gf_repeater2_remove.innerHTML}</a>`; 350 } 351 else { 352 last_field.querySelector( 'label' ).innerHTML += `<a href="#" class="saas_repeater2_remove float-end text-danger">${gf_repeater2_remove.innerHTML}</a>`; 353 } 354 355 // Add repeater limit message to be displayed after max item count is met or exceeded 356 message = document.createElement( 'div' ); 357 message.innerHTML = theme_ajax_object.translations.gf_repeater2_max_items_message; 358 message.classList.add( 'saas_repeater_limit_message', 'd-none', 'text-danger' ); 359 gf_repeater2_remove.closest( '.ginput_container' ).appendChild( message ); 360 361 } 362 363 repeater_index += 1; 364 365 } 366 367}, { once : true } ); 368 369// GF repeater2 add-row event 370document.addEventListener( 'DOMContentLoaded', function() { 371 if ( typeof jQuery !== 'undefined' ) { 372 jQuery( 'form' ).on( 'gform_repeater2_after_repeat', function( e, repeater_index, repeatId ) { 373 374 const form_id = this.id.match(/\d+/)[0]; 375 const form_body = this.querySelector( '.gform_body' ); 376 const repeater = gfRepeater_repeater2s[ form_id ][ repeater_index ]; 377 const subfields = form_body.querySelectorAll( '[data-repeater2-parentid="' + repeater_index + '"]' ); 378 const last_subfield = subfields[ subfields.length - 1 ]; 379 const add_button = last_subfield.nextSibling.querySelector( '.gf_repeater2_add' ); 380 const remove_buttons = form_body.querySelectorAll( '[data-repeater2-parentid="' + repeater_index + '"] .saas_repeater2_remove' ); 381 const limit_message = last_subfield.nextSibling.querySelector( '.saas_repeater_limit_message' ); 382 383 // Loop through all the repeater fields and toggle the remove buttons depending on the total item count 384 remove_buttons.forEach( field => { 385 if ( remove_buttons.length === 1 ) { 386 field.classList.add( 'pe-none', 'opacity-50' ); 387 } else { 388 field.classList.remove( 'pe-none', 'opacity-50' ); 389 } 390 } ); 391 392 // Toggle the add button and limit message visibility 393 if ( repeater['settings']['max'] && ( repeater['data']['repeatCount'] >= repeater['settings']['max'] ) ) { 394 add_button.classList.remove( 'd-block' ); 395 add_button.classList.add( 'd-none' ); 396 limit_message.classList.remove( 'd-none' ); 397 } 398 else { 399 limit_message.classList.add( 'd-none' ); 400 } 401 402 // Add separators 403 saas_repeater2_redraw_separators( form_id, form_body, repeater_index ); 404 405 // Enable tooltips/icons for cloned item 406 saas_enable_tooltips(); 407 if ( typeof window.FontAwesome !== 'undefined' ) { 408 //todo Specify element to check 409 window.FontAwesome.dom.i2svg(); 410 } 411 412 } ); 413 } 414} ); 415 416// GF repeater2 remove-row event 417document.addEventListener( 'click', event => { 418 419 const target = event.target; 420 let form_body, form_id, add_button, remove_buttons, limit_message, repeater_index, last_subfield; 421 422 // Remove 423 if ( target.classList.contains( 'saas_repeater2_remove' ) || target.closest( '.saas_repeater2_remove' ) ) { 424 425 event.preventDefault(); 426 427 form_body = target.closest( '.gform_body' ); 428 form_id = target.closest( 'form' ).id.match(/\d+/)[0]; 429 repeater_index = target.closest( '.gfield' ).getAttribute( 'data-repeater2-parentid' ); 430 last_subfield = form_body.querySelectorAll( '[data-repeater2-parentid="' + repeater_index + '"]' ); 431 last_subfield = last_subfield[ last_subfield.length - 1 ]; 432 add_button = last_subfield.nextSibling.querySelector( '.gf_repeater2_add' ); 433 limit_message = last_subfield.nextSibling.querySelector( '.saas_repeater_limit_message' ); 434 remove_buttons = form_body.querySelectorAll( '[data-repeater2-parentid="' + repeater_index + '"] .saas_repeater2_remove' ); 435 436 // Loop through all the repeater fields and toggle the remove buttons depending on the total item count 437 remove_buttons.forEach( field => { 438 if ( remove_buttons.length === 2 ) { 439 field.classList.add( 'pe-none', 'opacity-50' ); 440 } 441 else { 442 field.classList.remove( 'pe-none', 'opacity-50' ); 443 } 444 } ); 445 446 // Remove current item 447 gfRepeater_unrepeatRepeater( form_id, repeater_index, target.closest( '.gfield' ).getAttribute( 'data-repeater2-repeatid' ) ); 448 449 // Toggle the add button and limit message visibility 450 add_button.classList.add( 'd-block' ); 451 add_button.classList.remove( 'd-none' ); 452 limit_message.classList.add( 'd-none' ); 453 454 // Enable tooltips/icons for cloned item 455 saas_enable_tooltips(); 456 if ( typeof window.FontAwesome !== 'undefined' ) { 457 //todo Specify element to check 458 window.FontAwesome.dom.i2svg(); 459 } 460 461 // Redraw separators 462 saas_repeater2_redraw_separators( form_id, form_body, repeater_index ); 463 464 } 465 466} ); 467 468/** 469 * Remove+add line-separators between repeater rows. 470 * 471 * @param {int} form_id 472 * @param {Node} form_body 473 * @param {int} repeater_index 474 */ 475function saas_repeater2_redraw_separators( form_id, form_body, repeater_index ) { 476 477 // If separators are enabled 478 const repeater = gfRepeater_repeater2s[ form_id ][ repeater_index ]; 479 if ( repeater.controllers.start[0].classList.contains( '-separator' ) ) { 480 481 const subfields = form_body.querySelectorAll( '[data-repeater2-parentid="' + repeater_index + '"]' ); 482 const last_subfields_except_last = Array.from( subfields ).filter( field => { 483 return field.getAttribute( 'data-repeater2-childid' ) == repeater.data.childrenCount && field !== subfields[ subfields.length - 1 ]; 484 } ); 485 486 // Remove all existing separators 487 const separators = form_body.querySelectorAll( '.saas_repeater2_separator--' + repeater_index );
488 separators.forEach( separator => { 489 separator.remove(); 490 } ); 491 492 // Add new ones 493 const separator = document.createElement( 'div' ); 494 separator.classList.add( 'col-12', 'saas_repeater2_separator--' + repeater_index ); 495 separator.innerHTML = '<hr class="-saas_repeater2-separator">'; 496 last_subfields_except_last.forEach( field => { 497 field.after( separator.cloneNode( true ) ); 498 } ); 499 500 } 501 502} 503 504// Disable the Gravity Forms button on click 505document.addEventListener('DOMContentLoaded', function () { 506 const submitButtons = document.querySelectorAll('.saas-form-modal [id^="gform_submit_button_"]'); 507 508 if (submitButtons.length > 0) { 509 // Disable the submit button on click 510 submitButtons.forEach(function (submitButton) { 511 submitButton.addEventListener('click', function () { 512 // Wait for the Gravity Forms validation to start 513 setTimeout( () => { 514 submitButton.setAttribute('disabled', 'disabled'); 515 }, 100 ); 516 }); 517 }); 518 } 519}); 520 521// Max-1 checkbox group 522// You are allowed to check another box in the same group but it will uncheck all others. You can still uncheck all, which differentiates it from a radio group. 523document.addEventListener( 'click', function( e ) { 524 if ( e.target.getAttribute( 'data-max1-group' ) ) { 525 526 const input = e.target; 527 const group = input.getAttribute( 'data-max1-group' ); 528 const siblings = document.querySelectorAll( '[data-max1-group="' + group + '"]' ); 529 siblings.forEach( ( sibling ) => { 530 if ( ! sibling.isSameNode( input ) ) { 531 sibling.checked = false; 532 } 533 } ); 534 535 } 536} ); 537 538// Un-hide element from first scroll event 539window.addEventListener( 'scroll', ( event ) => { 540 const els = document.querySelectorAll( '.-show-me-on-scroll' ); 541 els.forEach( ( el ) => { 542 el.classList.add( '-showing' ); 543 } ); 544}, { once: true } ); 545 546// Maintain megamenu visibility while touching inside it on mobile 547const SAAS_MEGAMENU_TOUCH_CLASS = '-touch-active'; 548 549const saasHandleMegaTap = ( event ) => { 550 const activeItems = document.querySelectorAll( '.owmenu-li-has-mega.' + SAAS_MEGAMENU_TOUCH_CLASS ); 551 const target = event.target instanceof Element ? event.target : event.target?.parentElement; 552 const innerLink = target ? target.closest( '.owmenu-megamenu a[href]' ) : null; 553 const inMegaMenu = target ? target.closest( '.owmenu-megamenu' ) : null; 554 const menuItem = target ? target.closest( '.owmenu-li-has-mega' ) : null; 555 556 // Allow real links inside megamenu to navigate normally. 557 if ( inMegaMenu && innerLink && innerLink.getAttribute( 'href' ) && innerLink.getAttribute( 'href' ) !== '#' ) { 558 return; 559 } 560 // Keep megamenu open when tapping non-link content inside it. 561 if ( inMegaMenu && ! innerLink ) { 562 event.preventDefault(); 563 return; 564 } 565 566 activeItems.forEach( ( item ) => { 567 if ( item !== menuItem && ! item.contains( target ) ) { 568 item.classList.remove( SAAS_MEGAMENU_TOUCH_CLASS ); 569 } 570 } ); 571 572 if ( menuItem ) { 573 574 event.preventDefault(); 575 576 577 if ( menuItem.classList.contains( SAAS_MEGAMENU_TOUCH_CLASS ) ) { 578 menuItem.classList.remove( SAAS_MEGAMENU_TOUCH_CLASS ); 579 return; 580 } 581 582 menuItem.classList.add( SAAS_MEGAMENU_TOUCH_CLASS ); 583 } 584}; 585 586let saasLastTouchTs = 0; 587 588document.addEventListener( 'click', ( event ) => { 589 // Ignore synthetic clicks that follow a touch. 590 if ( Date.now() - saasLastTouchTs < 800 ) { 591 return; 592 } 593 594 saasHandleMegaTap( event ); 595} ); 596 597document.addEventListener( 'keyup', ( event ) => { 598 if ( event.key !== 'Escape' ) { 599 return; 600 } 601 602 document.querySelectorAll( '.owmenu-li-has-mega.' + SAAS_MEGAMENU_TOUCH_CLASS ).forEach( ( item ) => { 603 item.classList.remove( SAAS_MEGAMENU_TOUCH_CLASS ); 604 } ); 605} ); 606 607// Trigger event for scripts that are waiting for these methods to be available. 608document.dispatchEvent( new CustomEvent( 'saas-themejs-ready' ) ); 609 610// Download file with a token 611// 612// Delegated handler for `.download-pending` anchors: the API download routes 613// sit behind auth:api, but a plain link navigation cannot carry the bearer 614// token â so the click is intercepted here and the href is fetched with the 615// Authorization header, then served to the browser as a blob. 616// 617// Anchors that also carry `.download-once` are throwaways (created dynamically, 618// e.g. from an API `urls` response). For those we skip the restore/re-add 619// cleanup and remove the element after the download fires. 620document.addEventListener( 'click', ( event ) => { 621 if ( event.target.classList.contains( 'download-pending' ) ) { 622 623 let anchor = event.target; 624 let is_once = anchor.classList.contains( 'download-once' ); 625 let file_path = anchor.href; 626 627 // Only send the bearer token to our own endpoints (allowlist). Exter
627nal 628 // URLs â e.g. presigned object-storage links â are self-authenticating 629 // via their query string and force the download via their 630 // Content-Disposition header. Sending our token to them would both 631 // trigger a CORS preflight the storage host rejects and leak the token to 632 // a third-party origin, so let the browser download them natively. The 633 // list of bases is provided by theme_ajax_object.bearer_token_base_urls 634 // (filterable in PHP via the `saas_bearer_token_base_urls` filter). 635 let bearer_token_base_urls = ( window.theme_ajax_object && theme_ajax_object.bearer_token_base_urls ) || []; 636 if ( ! Array.isArray( bearer_token_base_urls ) ) { 637 bearer_token_base_urls = [ bearer_token_base_urls ]; 638 } 639 let attach_token = bearer_token_base_urls.some( function ( base ) { 640 return base && file_path.indexOf( base ) === 0; 641 } ); 642 643 if ( ! attach_token ) { 644 // Do not preventDefault: allow the native download to proceed. 645 if ( is_once ) { 646 setTimeout( function () { anchor.remove(); }, 0 ); 647 } 648 return; 649 } 650 651 event.preventDefault(); 652 653 let filename = anchor.innerText; 654 let headers = new Headers(); 655 headers.append( 'Authorization', 'Bearer ' + User_Token.get_token() ); 656 anchor.disabled = true; 657 658 fetch( file_path, { headers } ) 659 .then( response => { 660 661 // Prefer the server-provided filename (Content-Disposition); 662 // fall back to the link text for static, pre-rendered links. 663 let disposition = response.headers.get( 'Content-Disposition' ); 664 if ( disposition && disposition.indexOf( 'filename' ) !== -1 ) { 665 let matches = disposition.match( /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i ); 666 if ( matches && matches[ 1 ] ) { 667 filename = matches[ 1 ].trim(); 668 } 669 } 670 671 return response.blob(); 672 } ) 673 .then( blobby => { 674 675 let objectUrl = window.URL.createObjectURL( blobby ); 676 677 // Drop the trigger class before the synthetic click so this 678 // delegated handler does not re-enter on the blob navigation. 679 anchor.classList.remove( 'download-pending' ); 680 anchor.href = objectUrl; 681 anchor.download = filename; 682 anchor.click(); 683 684 window.URL.revokeObjectURL( objectUrl ); 685 686 if ( is_once ) { 687 anchor.remove(); 688 } else { 689 anchor.href = file_path; 690 anchor.disabled = false; 691 anchor.classList.add( 'download-pending' ); 692 } 693 694 } ) 695 .catch( () => { 696 // Never leave a throwaway anchor stuck in the DOM on failure. 697 if ( is_once ) { 698 anchor.remove(); 699 } else { 700 anchor.disabled = false; 701 } 702 } ); 703 704 } 705} ); 706 707// Reload fontawesome on gravity forms ajax response. 708document.addEventListener( 'DOMContentLoaded', function() { 709 if ( typeof jQuery !== 'undefined' ) { 710 jQuery( document ).on( 'gform_post_render', function() { 711 if ( typeof window.FontAwesome !== 'undefined' ) { 712 //todo Specify element to check 713 window.FontAwesome.dom.i2svg(); 714 } 715 } ); 716 } 717} ); 718 719/* Safari: Prevent megamenu from closing when clicking links inside it on mobile */ 720// On mobile Safari, clicking a link inside a megamenu can cause :focus-within to become false before navigation, causing the menu to close. This handler ensures the click proceeds. 721/* 722function handleMegamenuClick( e ) { 723console.log( 'event start', screen.width, theme_ajax_object.owmenu.breakpoint ); 724 // if ( screen.width < theme_ajax_object.owmenu.breakpoint ) { 725// console.log( 'Screen width ok' ); 726 727 let megamenu = null; 728 let link = null; 729 var path = e.composedPath ? e.composedPath() : []; 730 for ( var i = 0; i < path.length; i++ ) { 731 if ( path[i] && path[i].classList && path[i].classList.contains( 'owmenu-megamenu' ) ) { 732 megamenu = path[i]; 733 break; 734 } 735 } 736console.log( 'Megamenu', megamenu, path ); 737 738 if ( megamenu ) { 739 740 for ( var i = 0; i < path.length; i++ ) { 741 if ( path[i] && path[i].tagName === 'A' ) { 742 // Check if this link is inside the megamenu we found 743 if ( megamenu.contains( path[i] ) || path[i].closest( '.owmenu-megamenu' ) === megamenu ) { 744 link = path[i]; 745 break; 746 } 747 } 748 } 749 if ( ! link ) { 750 var target = e.target; 751 if ( target.tagName === 'A' ) { 752 link = target; 753 } else { 754 // Try to find a link that contains the target or is near it 755 var potentialLink = target.closest( 'a' ); 756 if ( potentialLink && megamenu.contains( potentialLink ) ) { 757 link = potentialLink; 758 } 759 } 760 } 761console.log( 'Link', link, path ); 762 763 // if ( link && link.href && link.href !== '#' && link.getAttribute( 'href' ) !== '' ) { 764 if ( link && link.href && link.href !== '#' && link.getAttribute( 'href' ) !== '' && link.href !== window.location.href + '#' ) { 765 const megamenuParent = link.closest( '.owmenu-li-has-mega' ); 766console.log( 'Megamenu parent', megamenuParent ); 767 if ( megamenuParent ) { 768 megamenuParent.classList.add( '-focus-within' ); 769 console.log( 'Added the class!' ); 770 setTimeout( function() { 771 megamenuParent.classList.remove( '-focus-within' ); 772 }, 500 ); 773 } 774 } 775 776 } 777 // } 778} 779 780document.addEventListener( 'touchstart', handleMegamenuClick, true ); 781document.addEventListener( 'click', handleMegamenuClick, true ); 782console.log( 'vcheck' ); 783*/
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.