1<!DOCTYPE html> 2<html lang="en-GB"> 3<head> 4<!-- start of head--> 5 <title>Contact - St Stephen's Guernsey</title> 6 <link rel="shortcut icon" href="/_data/site/117/templates/current/favicon.ico" type="image/x-icon" /> 7 <meta charset="utf-8"> 8 <meta name="dcterms.rightsHolder" content="Khoo Systems Limited"> 9 <meta name="dcterms.dateCopyrighted" content="2014"> 10 <meta name="robots" content="all"> 11 <meta name="description" content="Contact St Stephen's Guernsey."> 12 <meta name="csrf_token" content="ststephens:0xB242C4CC847B11F19A5EF23C9435931F:::1790331874:be69cd8faad048b357aa141a8b5aa89f"> 13 <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> 14 15<meta name="viewport" content="width=device-width, initial-scale=1.0"> 16<meta property="og:type" content="website"> 17<meta property="og:site_name" content="St Stephens Guernsey"> 18<meta property="og:image" content="https://st-stephens-guernsey.org/cache/img/117/pg|418&sz500x500&cp&tn&ql&fm&bo&bc&sg515d8c2607&ft1707408783&contactBanner.jpg"> 19<meta property="og:title" content="Contact - St Stephen's Guernsey"> 20<meta property="og:description" content="Contact St Stephen's Guernsey."> 21<meta property="og:url" content="https://st-stephens-guernsey.org/418/Contact-Us"> 22<meta name="twitter:card" content="summary_large_image"> 23<meta name="twitter:title" content="Contact - St Stephen's Guernsey"> 24<meta name="twitter:description" content="Contact St Stephen's Guernsey."> 25<meta name="twitter:image" content="https://st-stephens-guernsey.org/cache/img/117/pg|418&sz500x500&cp&tn&ql&fm&bo&bc&sg515d8c2607&ft1707408783&contactBanner.jpg">
26<script type='application/ld+json'> 27{"@type":"Organization","@context":"https://schema.org","@id":"https://st-stephens-guernsey.org/#organization","sameAs":["https://www.facebook.com/StStephensGuernsey/"],"name":"St Stephens Guernsey","url":"https://st-stephens-guernsey.org/","contactPoint":[{"@type":"ContactPoint","telephone":"+44-1481 720268","contactType":"customer support"}],"logo":"https://st-stephens-guernsey.org/_data/site/117/templates/current/mobile_logo.svg"} 28</script>
28 29
30<script type='application/ld+json'> 31{"@type":"WebSite","@context":"https://schema.org","@id":"https://st-stephens-guernsey.org/#website","url":"https://st-stephens-guernsey.org/","name":"St Stephens Guernsey","publisher":{"@id":"https://st-stephens-guernsey.org/#organization"}} 32</script>
32 33<!-- Start_Google_structured_data_breadcrumb -->
34<script type="application/ld+json"> 35{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"https://st-stephens-guernsey.org/","name":"Home"}},{"@type":"ListItem","position":2,"item":{"@id":"https://st-stephens-guernsey.org/311/Contact-Us","name":"Contact"}},{"@type":"ListItem","position":3,"item":{"@id":"https://st-stephens-guernsey.org/418/Contact-Us","name":"Contact Us","image":"https://st-stephens-guernsey.org/cache/img/117/pg|418&sz200x200&cp&tn&ql&fm&bo&bc&sg133d703b53&ft1707408783&contactBanner.jpg"}}]} 36</script>
36 37<!-- End_Google_structured_data_breadcrumb --> 38<link rel='canonical' href='https://st-stephens-guernsey.org/418/Contact-Us'> 39 40 41 42<link rel='stylesheet' type='text/css' href='/js/plugins/cookieconsent/dist/cookieconsent.css?v=5'> 43<link rel='stylesheet' type='text/css' href='/js/plugins/swiper/swiper-bundle.min.css?v=5'> 44<link rel='stylesheet' type='text/css' href='/js/jquery-plugins/jquery-ui/jquery-ui.custom.css?v=5'> 45<link rel='stylesheet' type='text/css' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.theme.css'> 46<link rel='stylesheet' type='text/css' href='https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css'> 47<link rel='stylesheet' type='text/css' href='https://cdn.jsdelivr.net/npm/shortcut-buttons-flatpickr/dist/themes/light.min.css'> 48<link rel='stylesheet' type='text/css' href='/css/default.css?v=5'> 49<link rel='stylesheet' type='text/css' href='/modules/folder/css/folder.css?v=5'> 50<link rel='stylesheet' type='text/css' href='/modules/contact/css/contact.css?v=5'> 51<link rel='stylesheet' type='text/css' href='/modules/news/css/news.css?v=5'> 52<link rel='stylesheet' type='text/css' href='/modules/pg/css/pg.css?v=5'> 53<link rel='stylesheet' type='text/css' href='/modules/search/css/search.css?v=5'> 54<link rel='stylesheet' type='text/css' href='/modules/user/css/user.css?v=5'> 55<link rel='stylesheet' type='text/css' href='/modules/attribute/css/attribute.css?v=5'> 56<link rel='stylesheet' type='text/css' href='/modules/calendar/css/calendar.css?v=5'> 57<link rel='stylesheet' type='text/css' href='/modules/directory/css/directory.css?v=5'> 58<link rel='stylesheet' type='text/css' href='/modules/crm/css/crm.css?v=5'> 59<link rel='stylesheet' type='text/css' href='/modules/sermon/css/sermon.css?v=5'> 60<link rel='stylesheet' type='text/css' href='/css/search-autocomplete.css?v=5'> 61 62<link rel='stylesheet' type='text/css' href='/cache/css/117/modern-form.css?v=5.1781178638'> 63<link rel='stylesheet' type='text/css' href='/cache/css/117/responsive.css?v=5.1781178638'> 64<link rel='stylesheet' type='text/css' href='/cache/css/117/menu.css?v=5.1781178638'> 65<link rel='stylesheet' type='text/css' href='/cache/css/117/ecampaign.css?v=5.1781178638'> 66<link rel='stylesheet' type='text/css' href='/cache/css/117/calevent.css?v=5.1781178638'> 67<link rel='stylesheet' type='text/css' href='/cache/css/117/template_fonts.css?v=5.1781178638'> 68<link rel='stylesheet' type='text/css' href='/cache/css/117/template.css?v=5.1781178638'> 69<link rel='stylesheet' type='text/css' href='/cache/css/117/template_custom.css?v=5.1781178638'> 70 71
72<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js'></script>
vendor: 1 bytes, line 72
72
73<script defer src='https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js'></script>
vendor: 1 bytes, line 73
73
74<script defer src='https://unpkg.com/[email protected]/dist/htmx.min.js'></script>
vendor: 1 bytes, line 74
74
75<script defer src='https://cdn.jsdelivr.net/npm/@alpinejs/[email protected]/dist/cdn.min.js'></script>
vendor: 1 bytes, line 75
75
76<script defer src='https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js'></script>
vendor: 1 bytes, line 76
76
77<script defer src='https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js'></script>
vendor: 1 bytes, line 77
77
78<script defer src='https://cdn.jsdelivr.net/npm/tippy.js@6/dist/tippy-bundle.umd.js'></script>
vendor: 1 bytes, line 78
78
79<script defer src='/js/plugins/swiper/swiper-bundle.min.js?v=5'></script>
vendor: 1 bytes, line 79
79
80<script defer src='/js/component/swiperLegacyAlias.js?v=5'></script>
80 81<!-- Viewport width for Google --> 82<meta name="viewport" content="width=device-width, initial-scale=1">
83<script src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'></script>
vendor: 1 bytes, line 83
83
84<script defer src='/js/jquery-plugins/jquery.validate/jquery.validate.min.js?v=5'></script>
vendor: 1 bytes, line 84
84
85<script defer src='/js/jquery-plugins/jquery.validate/additional/creditcard.js?v=5'></script>
vendor: 1 bytes, line 85
85
86<script defer src='/js/component/matchHeight.js?v=5'></script>
vendor: 1 bytes, line 86
86
87<script defer src='/js/plugins/headroom/headroom.min.js?v=5'></script>
vendor: 1 bytes, line 87
87
88<script defer src='/js/plugins/lazyload/lazyload.js?v=5'></script>
vendor: 1 bytes, line 88
88
89<script> 90document.addEventListener("DOMContentLoaded",function(){lazyload();}); 91</script>
vendor: 1 bytes, line 91
91
92<script defer src='https://cdn.jsdelivr.net/npm/flatpickr'></script>
vendor: 1 bytes, line 92
92
93<script defer src='https://cdn.jsdelivr.net/npm/shortcut-buttons-flatpickr/dist/shortcut-buttons-flatpickr.min.js'></script>
vendor: 1 bytes, line 93
93
94<script defer src='/js/plugins/base64.js?v=5'></script>
vendor: 1 bytes, line 94
94
95<script src='/js/default.js?v=5'></script>
vendor: 1 bytes, line 95
95
96<script defer src='/modules/menu/js/menu_h.js?v=5'></script>
vendor: 1 bytes, line 96
96
97<script defer src='/modules/menu/js/menu_hamb.js?v=5'></script>
vendor: 1 bytes, line 97
97
98<script defer src='/modules/menu/js/menu_mega.js?v=5'></script>
vendor: 1 bytes, line 98
98
99<script defer src='/js/component/searchAutocomplete.js?v=5'></script>
99 100 101 102 103 104 105<!--end of head --> 106</head> 107<body class="body-template body-311 body-418 _body-418 body-pg body-layout-preset-985 browser-chrome engine-webkit majorver-128 ver-128.0.0.0 is-desktop geo-CA body-menu-hamb body-menu-hamb-left body-menu-hamb-slide "> 108 109 <div id="q-app"> 110 <div id="shim"></div> 111 <div id='menu-hamb' class='left slide'><a id='menu-icon-close'> <svg style="fill:#fff" version="1.1" id="hamb-icon" class="hamb-icon-svg" 112 xmlns="http://www.w3.org/2000/svg" 113 xmlns:xlink="http://www.w3.org/1999/xlink" 114 x="0px" y="0px" width="45px" height="45px" 115 viewBox="0 0 128 128" 116 enable-background="new 0 0 128 128" xml:space="preserve"> 117 <g> 118 <rect style="fill:#26B5E3;" id="hamb-icon-close-box" x="0" y="0" width="128" height="128"/> 119 <g transform="matrix(0.5, 0, 0, 0.5, 32,32)"> 120 <g id="close" transform="translate(18.000000, 18.000000)"> 121 <path d="M92,9.19938255 L82.8006174,0 L46,36.8006174 L9.19938255,0 L0,9.19938255 L36.8006174,46 L0,82.8006174 L9.19938255,92 L46,55.1993826 L82.8006174,92 L92,82.8006174 L55.1993826,46 L92,9.19938255 Z" id="Shape"></path> 122 </g> 123 </g> 124 </g> 125 </svg></a><ul class=' n0'><li class='n159 show-items folder' data-nav-id='159'><div><a class=' ajax-branch' href='/159/About' >About</a><a class='menu-branch ajax' href='/159/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n405 show-items folder' data-nav-id='405'><div><a class=' ajax-branch' href='/405/Services' >Services</a><a class='menu-branch ajax' href='/405/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n451 show-items folder' data-nav-id='451'><div><a class=' ajax-branch' href='/451/Whats-On' >What's On</a><a class='menu-branch ajax' href='/451/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n217 hide-items folder' data-nav-id='217'><div><a class='' href='/217/News' >News</a></div></li><li class='n350 show-items folder' data-nav-id='350'><div><a class=' ajax-branch' href='/350/Community' >Community</a><a class='menu-branch ajax' href='/350/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n9 show-items folder' data-nav-id='9'><div><a class=' ajax-branch' href='/9/Safeguarding' >Safeguarding</a><a class='menu-branch ajax' href='/9/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n311 hide-items folder' data-nav-id='311'><div><a class='' href='/311/Contact-Us' >Contact</a></div></li></ul></div> 126 <div id="header-fixed" class="header--fixed"> 127
127<script> 128 $(function(){ 129 var header = document.querySelector("#header-fixed"); 130 if (window.location.hash) { 131 header.classList.add("slide--up"); 132 } 133 var headroom = new Headroom(header, { 134 tolerance: { 135 down : 10, 136 up : 20 137 }, 138 offset: 100 139 }).init(); 140 }) 141 </script>
141 <header id="header-mob"> 142 <div id="header-mob-box"> 143 <a id="header-mob-logo" href="/"></a> 144 <div id="mobile-actions"> 145 146 147 <svg id='mobile-search-icon' viewBox='0 0 100 100' x-data @click="$dispatch('toggle-mobile-search')"><path d='M94.5,88.6L76.8,70.9c6.6-7.4,10.7-17.2,10.7-27.9c0-23.1-18.8-41.9-41.9-41.9S3.7,19.9,3.7,43s18.8,41.9,41.9,41.9c8.2,0,15.8-2.4,22.2-6.4l18.4,18.4c1.1,1.1,2.6,1.7,4.1,1.7c1.5,0,3-0.6,4.1-1.7C96.8,94.6,96.8,90.9,94.5,88.6z M15.4,43c0-16.6,13.5-30.2,30.2-30.2S75.7,26.4,75.7,43S62.2,73.2,45.6,73.2S15.4,59.7,15.4,43z'/></svg> 148</div> 149<div id="mobile-search" 150 x-data="{ open: false }" 151 :class="{ 'is-open': open }" 152 @toggle-mobile-search.window="open = !open"> 153 <form action="/search/index.htm" method="post"> 154 <img id="icon-search" src="/media/icons/search.svg" alt="" width="18" height="18"> 155 <input name="p_search" 156 value="" 157 type="text" 158 placeholder="Search" 159 autocomplete="off" 160 data-autocomplete-url="/search/ajax/search_select.php" 161 data-autocomplete-min="3" 162 data-autocomplete-delay="100" /> 163 <img id="close-search" src="/media/icons/close.svg" alt="Close" width="18" height="18" @click="open = false"> 164 </form> 165</div> 166 167<div id='menu-icon-wrapper'><div id='menu-icon'> <svg style="fill:#fff" version="1.1" id="hamb-icon" class="hamb-icon-svg" 168 xmlns="http://www.w3.org/2000/svg" 169 xmlns:xlink="http://www.w3.org/1999/xlink" 170 x="0px" y="0px" width="45px" height="45px" 171 viewBox="0 0 128 128" 172 enable-background="new 0 0 128 128" xml:space="preserve"> 173 <g> 174 <rect style="fill:#26B5E3;" id="hamb-icon-box" x="0" y="0" width="128" height="128"/> 175 <g transform="matrix(0.6, 0, 0, 0.6, 25.6,25.6)"> 176 <rect x="24" y="60" width="80" height="8"/> 177 <rect x="24" y="36" width="80" height="8"/> 178 <rect x="24" y="84" width="80" height="8"/> 179 </g> 180 </g> 181 </svg></div></div> 182 </div> 183 <div id='mobile-trolley-notification'></div> 184 </header> 185 </div> 186 187 <div id="template-content" class="template template-box"> 188 <a id="staging-box" href="/_default/staging.htm">You are viewing this site in staging mode. Click in this bar to return to normal site.</a> 189 <div id="svg-icons"><!-- --></div> 190 <div id="notify-container"><div id="basic-template"><a class="ui-notify-close ui-notify-cross" href="#">x</a>#{notify_img}<div class="notify-title">#{notify_title}</div><div class="notify-text">#{notify_text}</div></div></div> 191 <div class="modal-dlg" id="modal-dlg"><!-- --></div> 192 <div class="modal-dlg" id="modal-dlg1"><!-- --></div> 193 <div class="modal-dlg" id="modal-dlg2"><!-- --></div> 194 195 <div id="browser-box"> 196 <div id="page-box"> 197 <div id="header-nav"> 198 <header id="header"> 199 <div id="header-inner" class="section-inner"><!-- --> 200 <div id="header-box"> 201 <div class="header-logo"><a href="/"><img src="/_data/site/117/_default/stStephensLogo2.svg" alt="St Stephen's Guernsey Logo" /></a></div> 202<div class="header-search"><form id="search" class="search" action="/search/index.htm" method="post"> 203 <input id="p-search" 204 name="p_search" 205 value="" 206 type="text" 207 placeholder="Search" 208 autocomplete="off" 209 data-autocomplete-url="/search/ajax/search_select.php" 210 data-autocomplete-min="3" 211 data-autocomplete-delay="100" /> 212</form> 213</div> 214<div class="header-give"><a class="button-cta" href="/359/Give">Give</a></div> 215 </div> 216 </div> 217 </header> 218 <nav id="h-nav"> 219 <div id="h-nav-inner" class="section-inner"> 220 <div id="h-nav-box"> 221 <div id='menu-h'><ul class=' n0'><li class='n159 show-items folder' data-nav-id='159'><div><a class=' ajax-branch' href='/159/About' >About</a><a class='menu-branch ajax' href='/159/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n405 show-items folder' data-nav-id='405'><div><a class=' ajax-branch' href='/405/Services' >Services</a><a class='menu-branch ajax' href='/405/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n451 show-items folder' data-nav-id='451'><div><a class=' ajax-branch' href='/451/Whats-On' >What's On</a><a class='menu-branch ajax' href='/451/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n217 hide-items folder' data-nav-id='217'><div><a class='' href='/217/News' >News</a></div></li><li class='n350 show-items folder' data-nav-id='350'><div><a class=' ajax-branch' href='/350/Community' >Community</a><a class='menu-branch ajax' href='/350/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n9 show-items folder' data-nav-id='9'><div><a class=' ajax-branch' href='/9/Safeguarding' >Safeguarding</a><a class='menu-branch ajax' href='/9/ajax/menu_branch.php' rel='nofollow'> </a></div></li><li class='n311 hide-items folder' data-nav-id='311'><div><a class='' href='/311/Contact-Us' >Contact</a></div></li></ul></div> 222 </div> 223 </div> 224 </nav> 225 226 227 </div> 228 <section id="body-header"><!-- --> 229 <div id="body-header-inner" class="section-inner" ><!-- --> 230 <div id="body-header-box"> 231 <div><div class='pg-gallery-fw'><div id="gallery-fw-wrapper-418" class='gallery-fw-wrapper-preset-585 breakout'> 232 233 <div id="gallery-fw-418" class='swiper slick-slider cycle-slideshow gallery-fw-preset-585'> 234 <div class='swiper-wrapper slick-list slick-track'>
235 <div class='swiper-slide slick-slide cycle-slide im-wrapper ' style='background-image:url(/cache/img/117/pg|418&sz3000x3000&cp&tn&ql&fm&bo&bc&sgfbcdd90aa5&ft1707408783&contactBanner.jpg)' data-title='' data-description='<h1 class='pg-title'>Contact Us</h1>' data-link=""></div> 236 </div> 237 238 239 </div> 240 241</div> 242 243<style> 244 .gallery-fw-wrapper-preset-585 {min-height:500px} 245.gallery-fw-wrapper-preset-585 .im-wrapper {background-size:cover;background-position:center center;width:100%;height:500px} 246#gallery-fw-418 {width:100%;min-width:0px;max-width:100%;overflow:hidden} 247.gallery-fw-wrapper-preset-585 .swiper-button-prev {background:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='15 6 9 12 15 18'/></svg>") no-repeat scroll 20% 50% rgba(0, 0, 0, 0);background-size:30px 30px;left:0px;margin-top:0px} 248.gallery-fw-wrapper-preset-585 .swiper-button-next {background:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='9 6 15 12 9 18'/></svg>") no-repeat scroll 80% 50% rgba(0, 0, 0, 0);background-size:30px 30px;right:0px;margin-top:0px} 249.gallery-fw-wrapper-preset-585 .swiper-button-prev:hover, #gallery-fw-418 .swiper-button-next:hover {opacity:1} 250.gallery-fw-wrapper-preset-585 .swiper-button-prev, #gallery-fw-418 .swiper-button-next {cursor:pointer;position:absolute;height:100%;opacity:0;top:0px;width:20%;z-index:800} 251.gallery-fw-wrapper-preset-585 .swiper-pagination {text-align:center;position:absolute;overflow:hidden;z-index:500} 252.gallery-fw-wrapper-preset-585 .swiper-pagination-bullet {font-family:arial;font-size:50px;width:16px;height:16px;display:inline-block;color:#ddd;cursor:pointer} 253.gallery-fw-wrapper-preset-585 .swiper-pagination-bullet-active {color:#D69746} 254.gallery-fw-wrapper-preset-585 .gallery-fw-caption {pointer-events:none} 255.gallery-fw-wrapper-preset-585 .gallery-fw-caption .caption-description {pointer-events:none;background-color:rgba(78,81,102,0.75);color:#ffffff;height:100%} 256.gallery-fw-wrapper-preset-585 .im-wrapper.video {overflow:hidden} 257.gallery-fw-wrapper-preset-585 .im-wrapper.video video {width:100%;object-fit:cover;min-height:500px;max-height:500px} 258@media only screen and (max-width: 767px ) { 259#gallery-fw-wrapper-418 {min-height:500px} 260.gallery-fw-wrapper-preset-585 .im-wrapper {height:500px} 261.gallery-fw-wrapper-preset-585 .im-wrapper.video video {object-fit:cover;min-height:500px;max-height:500px} 262} 263 264</style>
265<script> 266(function(){ 267 var navId = 418; 268 function init() { 269 var root = document.getElementById('gallery-fw-' + navId); 270 if (!root || typeof Swiper === 'undefined') return; 271 272 var swiper = new Swiper(root, {"speed":1000,"effect":"fade","direction":"horizontal","fadeEffect":{"crossFade":true},"autoplay":false}); 273 if (typeof window.swiperLegacyAlias === 'function') window.swiperLegacyAlias(swiper); 274 275 /* Swiper auto-attaches click listeners to navigation buttons, 276 * but on opacity:0 buttons inside our breakout/table-cell 277 * layout those clicks were observed not firing. Bind 278 * explicit handlers as a guarantee. Calling slideNext/Prev 279 * while a transition is in flight is safe (Swiper's own 280 * isLocked/animating guards handle the no-op). */ 281 var prevBtn = root.querySelector('.swiper-button-prev'); 282 var nextBtn = root.querySelector('.swiper-button-next'); 283 if (prevBtn) prevBtn.addEventListener('click', function(e){ e.stopPropagation(); swiper.slidePrev(); }); 284 if (nextBtn) nextBtn.addEventListener('click', function(e){ e.stopPropagation(); swiper.slideNext(); }); 285 286 var captionEnabled = (0 === 1); 287 if (captionEnabled) { 288 var caption = root.parentNode.querySelector('.gallery-fw-caption .caption-description'); 289 if (caption) { 290 /* The first slide's caption is rendered server-side (FwImage.php), 291 * so interactive content in it (e.g. a lightbox CTA) is present at 292 * parse time and bound by page-level init like any static markup â 293 * no initial JS injection here. On slide change we swap in the new 294 * slide's caption from data-description and fire a bubbling 295 * 'gallery:caption-changed' event so any plugin that binds to 296 * injected markup can rescan. The widget stays framework-agnostic 297 * and never hard-codes a plugin selector. */ 298 var setCaption = function(s) { 299 var slides = root.querySelectorAll('.swiper-slide:not(.swiper-slide-duplicate)'); 300 var active = slides[s.realIndex]; 301 caption.innerHTML = (active && active.dataset.description) ? active.dataset.description : ''; 302 caption.dispatchEvent(new CustomEvent('gallery:caption-changed', { bubbles: true, detail: { slide: s.realIndex } })); 303 }; 304 swiper.on('slideChangeTransitionStart', setCaption); 305 } 306 } 307 308 root.querySelectorAll('video.bgvid').forEach(function(v){ 309 var p = v.play(); 310 if (p && p.catch) p.catch(function(){}); 311 }); 312 313 root.addEventListener('click', function(e){ 314 if (e.target.closest('.swiper-button-prev, .swiper-button-next, .swiper-pagination')) return; 315 var link = e.target.closest('.linkable'); 316 if (!link) return; 317 var href = link.dataset.link; 318 if (href) window.location = href; 319 }); 320 } 321 322 if (document.readyState === 'loading') { 323 document.addEventListener('DOMContentLoaded', init); 324 } else { 325 init(); 326 } 327})(); 328</script>
328 329</div></div> 330 </div> 331 </div> 332 </section> 333 334 <main id="main-content"> 335 <div id="main-content-inner" class="section-inner"> 336 <div id="main-content-box"> 337 338 <div id="c-col" class='no-lc no-rc '> 339 <div id="c-col-inner"> 340 <div id="c-col-box"> 341 <div id="content-header"><!-- --> 342 343 </div> 344 345 <div id="content-box" class="layout-0xDCE416AE847B11F19A5EF23C9435931F"> 346 <!-- content --> 347<div class="item-contact "> 348 <div class="fg-row"> 349 <div class="small-12 medium-6 large-6 columns"> 350 <div class='pg-body'><h5>Contact information:</h5> 351<div class="fa"> 352<ul> 353<li class="fa-map">St Stephen's Church, Les Gravées, St Peter Port, Guernsey, GY1 1RN.</li> 354<li class="fa-phone"><a href="tel:01481720268">01481 720268</a></li> 355<li class="fa-mail"><a href='#' class='email-b64' data-e='am9lLnRob21wc29uQGRlYW5lcnkuZ2c='>email us for Church enquiries</a></li> 356</ul> 357</div> 358<p>Or send us a message using the form provided.</p> 359<p>You can also <a href="https://www.facebook.com/StStephensGuernsey/" target="_blank" rel="noopener noreferrer">follow us on facebook here</a></p></div> 360 </div> 361 <div class="small-12 medium-6 large-6 columns"> 362 <div class='fetch-load ajax-contact-form' data-url='/contact-115::::1790331874:bdb66d7c4676a410ec4bc66c510396f7/ajax/contact_form.htm?' data-fade=0></div> 363 </div> 364 </div> 365 <div class='pg-gallery-av'></div> 366</div> 367 368<!-- end of content --> 369 370 </div> 371 372 <div id="content-footer"><!-- --> 373 374 </div> 375 </div> 376 </div> 377 </div> 378 379 </div> 380 </div> 381 </main> 382 <section id="body-footer"> 383 <div id="body-footer-inner" class="section-inner" ><!-- --> 384 <div id="body-footer-box"> 385 386 </div> 387 </div> 388 </section> 389 <footer id="footer"> 390 <div id="footer-inner" class="section-inner"> 391 <div id="footer-box"> 392 <div class="block-fw footer-bg"> 393<div class="section-inner"> 394<div class="section-upper"> 395<div class="footer-logo"><a href="/"><img src="/_data/site/117/_default/stStephensLogoWhite.svg" alt="St Stephen's Guernsey Logo" /></a></div> 396</div> 397<div class="connect-wrapper"> 398 <ul> 399 <a title="St Stephen's Guernsey on Facebook" class="facebook" href="https://www.facebook.com/StStephensGuernsey/" target="_blank" rel="noopener noreferrer"> 400 <li>Facebook</li> 401 </a> 402 <a title="Call St Stephen's Guernsey" class="phone" href="tel:01481720268" target="_blank" rel="noopener noreferrer"> 403 <li>Phone</li> 404 </a> 405 </ul> 406</div> 407<hr /> 408<div class="section-lower"><h6>Copyright ©
408<script type="text/javascript">// <![CDATA[ 409document.write(new Date().getFullYear()); 410// ]]></script>
410 St Stephen's Guernsey | <a href="/9/Safeguarding">Safeguarding</a> | <a href="/8/Sitemap">Sitemap</a> | <a title="Privacy Policy" href="/434/"> Privacy</a> | Designed & Powered by the <a href="https://www.churchpages.co.uk" target="_blank" rel="noopener noreferrer">Church Pages Initiative</a></h6></div> 411</div> 412</div> 413 <span class="powered">Powered by 414 <a href='https://www.khooseller.co.uk' target='_blank'>Khooweb e-commerce website specialists</a> 415 </span> 416 </div> 417 </div> 418 </footer> 419 </div> 420 </div> 421 </div> 422 423 </div> 424 425 426
426<script src='/js/plugins/cookieconsent/dist/cookieconsent.umd.js?v=5'></script>
vendor: 1 bytes, line 426
426
427<script src='/js/alpine-init.js?v=5'></script>
vendor: 1 bytes, line 427
427
428<script src='/js/component/fieldValidator.js?v=5'></script>
vendor: 1 bytes, line 428
428
429<script> 430$(function() { 431$("#h-nav").sticky({ topSpacing: 0 }); 432}); 433 434</script>
vendor: 1 bytes, line 434
434
435<script defer src='/js/plugins/popupwindow.js?v=5'></script>
vendor: 1 bytes, line 435
435
436<script src='/js/stores/modals.js?v=5'></script>
vendor: 1 bytes, line 436
436
437<script src='/js/stores/slideout.js?v=5'></script>
vendor: 1 bytes, line 437
437
438<script src='/js/component/slideoutForm.js?v=5'></script>
vendor: 1 bytes, line 438
438
439<script src='/js/component/suggestionBox.js?v=5'></script>
vendor: 1 bytes, line 439
439
440<script src='/js/component/progressBar.js?v=5'></script>
vendor: 1 bytes, line 440
440
441<script src='/js/component/successRedirect.js?v=5'></script>
vendor: 1 bytes, line 441
441
442<script src='/js/component/tinyMceModalForm.js?v=5'></script>
vendor: 1 bytes, line 442
442
443<script defer src='/js/default_body.js?v=5'></script>
443 444 445
445<script src="/_default/js/cookie_consent.htm"></script>
445 446 <!-- ========================================================================= --> 447 <!-- Modal HTML. We wrap it in a simple x-data --> 448 <!-- to ensure it's part of Alpine's initialization process. --> 449 <!-- ========================================================================= --> 450 <div id="alpine-modal-root" class="modern-form" 451 x-data 452 @htmx:before-request="$store.modals.clearTopModalErrors()" 453 @htmx:after-request="$store.modals.handleHtmxResponse(event)" 454 @htmx:after-swap="$store.modals.bindFormValidation(event)" 455 @close-top-modal="$store.modals.close()" 456 > 457 <!-- The x-data makes this an official Alpine component --> 458 459 <!-- BEGIN: Global Alpine/HTMX Modal Stack --> 460 <template x-if="$store.modals.stack.length > 0"> <!-- THE CRITICAL FIX IS HERE --> 461 <div class="modal-stack-container" 462 @keydown.escape="$store.modals.close()" 463 x-cloak> 464 465 <!-- The blackout background. Closes the topmost modal when clicked. 466 Tracks the topmost modal's z-base (minus 1) so that a modal 467 elevated above an "-above-modal" slideout (z-base 2200) dims 468 the slideout too; the default case stays at 1999 via the CSS. --> 469 <div class="blackout-bg" @click="$store.modals.close()" 470 :style="$store.modals.top && $store.modals.top.zBase ? { zIndex: $store.modals.top.zBase - 1 } : {}"></div> 471 472 <!-- This loops through each open modal and "stacks" them visually. 473 Each modal carries its own z-base (2000 by default, 2200 when 474 opened from inside an elevated slideout â see modals.js open()), 475 keeping the dialog above the modal blackout, the slideout panel 476 (1100 / 2100 elevated), and the legacy \Core\Dialog stack (1001). --> 477 <template x-for="(modal, index) in $store.modals.stack" :key="index"> 478 <div class="dialog-wrapper" :style="{ zIndex: (modal.zBase || 2000) + index }"> 479 <div class="dialog" :style="{ maxWidth: modal.width || '600px' }"> 480 481 <!-- ======================================================= --> 482 <!-- BEGIN: "HTML" Dynamic Content from HTMX PRESET --> 483 <!-- ======================================================= --> 484 <template x-if="modal.type === 'html'"> 485 <div x-html="modal.content"></div> 486 </template> 487 488 <!-- ======================================================= --> 489 <!-- BEGIN: "FORM" PRESET --> 490 <!-- ======================================================= --> 491 <template x-if="modal.type === 'form'"> 492 <div class="dialog-content-wrapper" x-data="{ 493 title: modal.title || 'Dialog', 494 confirmText: modal.confirmText || 'Save Changes', 495 loadUrl: modal.loadUrl || '', 496 postUrl: modal.postUrl || '', 497 context: modal.context || '', 498 clearError(event) { 499 const fieldName = event.target.name; 500 // If there's a name and a corresponding error exists, delete it. 501 if (fieldName && $store.modals.top?.errors?.[fieldName]) { 502 delete $store.modals.top.errors[fieldName]; 503 // If it was a field-specific error, the form is no longer fatal. 504 // A general fatal error will remain. 505 if ($store.modals.top.isFatal && !$store.modals.top.errors.general) { 506 $store.modals.top.isFatal = false;
507 } 508 $store.modals._renderErrors(); 509 } 510 } 511 }" 512 x-init="Alpine.nextTick(() => htmx.process($el))"> 513 514 <!-- 1. The Header (Now part of the shell) --> 515 <div class="dialog-header"> 516 <div class="dialog-title" x-text="title"></div> 517 <button class="dialog-close" @click="$store.modals.close()">×</button> 518 </div> 519 520 <!-- 2. The Form --> 521 <!-- Bind fieldValidator once the body's htmx content lands so 522 .required / .digits / min / max are enforced client-side, 523 mirroring the slideout and legacy Core\Dialog. Without this 524 a `max` on a text/digits input is never checked (native 525 HTML5 max only applies to type=number). bind() self-guards 526 against the repeat afterSwaps of nested panel loads. 527 528 `context` (which can be a JSON array of every ticked row id) 529 travels as a hidden input, NOT hx-vals: hx-vals is inherited 530 by descendants, so the .dialog-body loader GET below would 531 carry the whole id list in its query string and 414 on a 532 large selection. htmx collects enclosing-form inputs on the 533 form's own POST only. --> 534 <form 535 :hx-post="postUrl" 536 hx-swap="none" 537 hx-disabled-elt="find .button-submit" 538 x-init="$el.addEventListener('htmx:afterSwap', () => window.fieldValidator && window.fieldValidator.bind($el))" 539 @input.debounce.250ms="clearError($event)" 540 > 541 <input type="hidden" name="context" :value="context" /> 542 <div class="validation-error -general" 543 data-error-general 544 style="display:none"> 545 </div> 546 <div class="dialog-body modern-form" 547 :hx-get="loadUrl" 548 hx-trigger="load" 549 hx-swap="innerHTML" 550 hx-indicator="#loading-spinner" 551 > 552 <div id="loading-spinner" class="htmx-indicator"> 553 <div class="loader"></div> 554 </div> 555 </div> 556 557 <div class="dialog-buttons"> 558 <button type="button" class="button-cancel" @click="$store.modals.close()">Cancel</button> 559 <button type="submit" class="button-submit" :disabled="$store.modals.top?.isFatal" x-text="confirmText"></button> 560 </div> 561 </form> 562 </div> 563 </template> 564 565 <!-- ======================================================= --> 566 <!-- BEGIN: "CONFIRM" PRESET --> 567 <!-- ======================================================= --> 568 <template x-if="modal.type === 'confirm'"> 569 <div class="dialog-content-wrapper"> 570 <div class="dialog-header"> 571 <div class="dialog-title" x-text="modal.title || 'Confirm'"></div> 572 <button class="dialog-close" @click="$store.modals.close()">×</button> 573 </div> 574 <div class="dialog-body"> 575 <template x-if="modal.bodyHtml"><div x-html="modal.bodyHtml"></div></template> 576 <template x-if="!modal.bodyHtml"><div class="info-box" x-text="modal.message || modal.description || 'Are you sure?'"></div></template> 577 </div> 578 <div class="dialog-buttons"> 579 <button class="button-cancel" @click="$store.modals.close()">
580 <span x-text="modal.cancelText || 'Cancel'"></span> 581 </button> 582 <button :class="modal.destructive ? 'button-destructive' : 'button-submit'" 583 @click=" 584 if (modal.okUrl) { window.location.assign(modal.okUrl); return; } 585 // Function-valued onOk (inline callers) or string method on targetId (Node::modalConfirm) 586 if (typeof modal.onOk === 'function') { 587 modal.onOk(...(modal.okArgs || [])); 588 } else if (modal.targetId && modal.onOk) { 589 const targetEl = document.getElementById(modal.targetId); 590 if (targetEl && Alpine.$data(targetEl) && typeof Alpine.$data(targetEl)[modal.onOk] === 'function') { 591 Alpine.$data(targetEl)[modal.onOk](...(modal.okArgs || [])); 592 } 593 } 594 $store.modals.close(); 595 "> 596 <span x-text="modal.okText || modal.confirmText || 'OK'"></span> 597 </button> 598 </div> 599 </div> 600 </template> 601 602 <!-- ======================================================= --> 603 <!-- BEGIN: "INPUT CONFIRM" PRESET --> 604 <!-- ======================================================= --> 605 <template x-if="modal.type === 'inputConfirm'"> 606 <!-- This component has its own internal state for the user's input --> 607 <div class="dialog-content-wrapper" x-data="{ userInput: '' }"> 608 <div class="dialog-header"> 609 <div class="dialog-title" x-text="modal.title || 'Confirm Action'"></div> 610 <button class="dialog-close" @click="$store.modals.close()">×</button> 611 </div> 612 <div class="dialog-body modern-form"> 613 <!-- The main instructional message --> 614 <div class="info-box" x-text="modal.message || 'Please confirm your action.'"></div> 615 616 <!-- The input field for validation --> 617 <div class="form-section"> 618 <div class="form-group"> 619 <input type="text" 620 x-model="userInput" 621 :placeholder="modal.requiredText" 622 autocomplete="off" 623 @keydown.enter.prevent="if (userInput === modal.requiredText) $refs.submitButton.click()" 624 /> 625 <label x-text="modal.label || 'Please type the confirmation text'"></label> 626 </div> 627 </div> 628 </div> 629 <div class="dialog-buttons"> 630 <button class="button-cancel" @click="$store.modals.close()">
631 <span x-text="modal.cancelText || 'Cancel'"></span> 632 </button> 633 <button class="button-submit" 634 x-ref="submitButton" 635 :disabled="userInput !== modal.requiredText" 636 @click=" 637 if (modal.targetId && modal.onOk) { 638 const targetEl = document.getElementById(modal.targetId); 639 if (targetEl) Alpine.$data(targetEl)[modal.onOk](...(modal.okArgs || [])); 640 } 641 $store.modals.close(); 642 "> 643 <span x-text="modal.okText || 'OK'"></span> 644 </button> 645 </div> 646 </div> 647 </template> 648 649 <!-- ======================================================= --> 650 <!-- BEGIN: "PROGRESS" PRESET (NEW!) --> 651 <!-- ======================================================= --> 652 <template x-if="modal.type === 'progress'"> 653 <!-- 654 This block is now pure structure. The logic comes from the progressBar component. 655 It's initialized with the taskId passed in the modal data object. 656 --> 657 <div class="dialog-content-wrapper" 658 x-data="progressBar(modal.taskId, modal.stoppable)" 659 x-cloak 660 > 661 <!-- The rest of your template remains exactly the same! --> 662 <div class="dialog-header"><div class="dialog-title">Processing...</div></div> 663 <div class="dialog-body"> 664 <p class="progress-status" x-text="statusText" 665 :class="hasFailed ? 'validation-error' : ''"></p> 666 <div style="background: #eee; border-radius: 5px; overflow: hidden;"> 667 <div style="width: 0%; background: #3498db; height: 20px; text-align: center; color: white; transition: width 0.3s ease;" 668 :style="{ width: percent + '%' }" 669 x-text="percent > 0 ? percent + '%' : ''" 670 :class="hasFailed ? 'bg-fail' : ''" 671 > 672 </div> 673 </div> 674 </div> 675 <div class="dialog-buttons"> 676 <!-- The new, optional Stop button --> 677 <button type="button" class="button-destructive" 678 x-show="isStoppable && !isComplete && !hasFailed && !isStopping" 679 @click="stopTask()"> 680 Stop 681 </button> 682 683 <!-- A disabled version for better UX after clicking stop --> 684 <button type="button" class="button-destructive" disabled 685 x-show="isStopping"> 686 Stopping... 687 </button> 688 689 <!-- Your existing close button for failures --> 690 <button type="button" class="button-cancel" 691 x-show="hasFailed" 692 @click="closeAndCleanup()"> 693 Close 694 </button> 695 </div> 696 </div> 697 </template> 698 699 <!-- ======================================================= --> 700 <!-- BEGIN: "SUCCESS REDIRECT" PRESET (NEW!) --> 701 <!-- ======================================================= --> 702 <template x-if="modal.type === 'success'"> 703 <!-- 704 The component is initialized with the message and URL from the modal data. 705 The @destroy directive ensures we clean up timers if the user hits Escape. 706 --> 707 <div class="dialog-content-wrapper" 708 x-data="successRedirect(modal.message, modal.redirectUrl)" 709 @destroy="destroy()" 710 x-cloak> 711 712 <div class="dialog-header"><div class="dialog-title">Success</div></div> 713 <div class="dialog-body" style="text-align: center;"> 714 <!-- You can add a success icon here if you like --> 715 <p style="margin: 1rem 0;" x-text="message"></p> 716 </div> 717 <div class="dialog-buttons"> 718 <!-- The OK button calls redirectNow() and shows the countdown --> 719 <button type="button" class="button-submit" @click="redirectNow()"> 720 OK <span x-text="'(' + countdown + ')'"></span> 721 </button> 722 </div> 723 </div> 724 </template> 725 726 </div> 727 </div> 728 </template> 729 </div> 730 </template> 731 <!-- END: Global Alpine/HTMX Modal Stack --> 732 733 </div> <!-- End of #alpine-modal-root --> 734 735 <!-- ======================================================= --> 736 <!-- SLIDE-OUT PANEL COMPONENT (Styled for .modern-form) --> 737 <!-- ======================================================= --> 738 <div id="slideout-panel-root" class="modern-form" 739 x-data="slideoutForm()" 740 > 741 <!-- Overlay --> 742 <div 743 class="slideout-overlay" 744 :class="{ '-below-topbar': $store.slideout.panelClass && $store.slideout.panelClass.indexOf('-below-topbar') !== -1, '-above-modal': $store.slideout.panelClass && $store.slideout.panelClass.indexOf('-above-modal') !== -1 }" 745 @click="handleOverlayClick()" 746 x-show="$store.slideout.isOpen && (!$store.slideout.panelClass || $store.slideout.panelClass.indexOf('-object-map-list') === -1)" 747 x-cloak 748 x-transition:enter="transition-opacity ease-in-out duration-300" 749 x-transition:enter-start="opacity-0" 750 x-transition:enter-end="opacity-100" 751 x-transition:leave="transition-opacity ease-in-out duration-300" 752 x-transition:leave-start="opacity-100" 753 x-transition:leave-end="opacity-0" 754 ></div> 755 756 <!-- Panel --> 757 <div 758 class="slideout-panel" 759 :class="$store.slideout.panelClass.split(' ').filter(function (c) { return c; })" 760 x-show="$store.slideout.isOpen" 761 x-cloak 762 x-transition:enter="transition ease-in-out duration-300 transform" 763 x-transition:enter-start="translate-x-full" 764 x-transition:enter-end="translate-x-0" 765 x-transition:leave="transition ease-in-out duration-300 transform" 766 x-transition:leave-start="translate-x-0" 767 x-transition:leave-end="translate-x-full" 768 :style="$store.slideout.currentWidth ? { width: $store.slideout.currentWidth } : {}" 769 > 770 <!-- Header --> 771 <div class="slideout-header"> 772 <div class="slideout-header-text"> 773 <h2 class="slideout-title" x-text="$store.slideout.title"></h2> 774 <p class="slideout-subtitle" x-show="$store.slideout.subt
774itle" x-text="$store.slideout.subtitle" x-cloak></p> 775 </div> 776 <div id="slideout-head-slot" class="slideout-head-slot"></div> 777 <button @click="handleCloseClick()" class="slideout-close" aria-label="Close panel">×</button> 778 </div> 779 780 <!-- Content shell: teleports (e.g. object-map list) mount as direct children; 781 htmx fragments load into #slideout-htmx-body so innerHTML swaps never 782 wipe Alpine teleport anchors on this container. --> 783 <div id="slideout-content" class="slideout-body"> 784 <div id="slideout-htmx-body" 785 hx-trigger="load-slideout-content" 786 hx-swap="innerHTML"></div> 787 </div> 788 789 <!-- Footer (form type only) --> 790 <div class="slideout-footer" 791 x-show="$store.slideout.type === 'form' && !$store.slideout.hideFooter" 792 x-cloak> 793 <div id="slideout-foot-left" class="slideout-foot-left"></div> 794 <div class="slideout-foot-actions"> 795 <button type="button" class="btn button-cancel" @click="cancelExplicit()">Cancel</button> 796 <div id="slideout-foot-actions"></div> 797 </div> 798 </div> 799 </div> 800 801 <iframe id="slideout-save-frame" name="slideout-save-frame" class="slideout-save-frame" 802 title="slideout save target" tabindex="-1" aria-hidden="true"></iframe> 803 </div> 804 805 <!-- ======================================================= --> 806 <!-- GUIDED TUTORIAL OVERLAY (engine mount point) --> 807 <!-- The player builds its dim-backdrop + popover in <body>; --> 808 <!-- this stable root just hosts the Alpine component. --> 809 <!-- Only mounted where the tutorial JS bundle is registered --> 810 <!-- (admin) â the slot is left empty otherwise; see _foot.php --> 811 <!-- ======================================================= --> 812 813 <!-- Hidden form â submits same data as original --> 814 <form id="advanced-search-hidden-form" x-data 815 hx-post="/index.php?url=/418/index.htm" 816 hx-target="#search-results-content" 817 hx-select="#search-results-content" 818 hx-swap="innerHTML" 819 style="display:none;" 820 @htmx:after-request="handleSearchSuccess(event, 'advanced-search-template')" 821 > 822 <div id="advanced-search-hidden-fields"></div> 823 </form> 824 825 <template id="show-loader-loader-template"> 826 <div style="padding:80px 0; text-align:center; font-size:1.1em; color:#666;"> 827 <div class="loader" style="display:inline-block; vertical-align:middle;"></div> 828 </div> 829 </template>
830<script> 831 document.body.addEventListener('htmx:beforeRequest', function(evt) { 832 // Get the element that will be updated (the swap target) 833 let target = evt.detail.target; 834 835 // If no explicit target, fall back to the requesting element 836 if (!target) { 837 const requestConfig = evt.detail.xhr; // not directly available, so use the elt 838 target = evt.target; 839 } 840 841 // Also check if the actual swap target (from hx-target on trigger) has the class 842 // HTMX sets evt.detail.target to the swap target element 843 if (target && target.classList && target.classList.contains('show-loader')) { 844 const loaderTemplate = document.getElementById('show-loader-loader-template'); 845 846 if (loaderTemplate && !target.querySelector('.htmx-loader-overlay')) { // prevent duplicates 847 const overlay = document.createElement('div'); 848 overlay.className = 'htmx-loader-overlay'; 849 850 Object.assign(overlay.style, { 851 position: 'absolute', 852 top: '0', 853 left: '0', 854 width: '100%', 855 height: '100%', 856 zIndex: '100', 857 display: 'flex', 858 alignItems: 'center', 859 justifyContent: 'center', 860 backgroundColor: 'rgba(255,255,255,0.8)', 861 backdropFilter: 'blur(2px)' 862 }); 863 864 const loaderClone = loaderTemplate.content.cloneNode(true); 865 overlay.appendChild(loaderClone); 866 867 // Ensure relative positioning 868 const computedStyle = window.getComputedStyle(target); 869 if (computedStyle.position === 'static') { 870 target.style.position = 'relative'; 871 } 872 873 target.appendChild(overlay); 874 } 875 } 876 }); 877</script>
877 878
878<script> 879 /** 880 * Creates a reusable object with logic for managing a selection. 881 * This is a "mixin" that we will add to our main Alpine component. 882 * @param {string} initialId - The ID of the item that should be selected on page load. 883 */ 884 function selectionManagerMixin(initialId) { 885 return { 886 // --- Properties --- 887 initialSelectedId: initialId, 888 889 // --- Methods --- 890 initializeSelection() { 891 // Explicit initial id (detail-context pages) â select unconditionally. 892 if (this.initialSelectedId && this.initialSelectedId !== '0') { 893 setTimeout(() => { 894 Alpine.store('selection').select(this.initialSelectedId); 895 }, 0); 896 return; 897 } 898 // Read the URL-persisted selection first: clear() strips ?selrow from the 899 // URL synchronously (history.replaceState), so reading it after is too late. 900 let selrow = ''; 901 try { selrow = new URL(window.location.href).searchParams.get('selrow') || ''; } catch (e) {} 902 903 // No server-preselected row on this page. The selection store is a 904 // global singleton shared across pages, so a selection set on another 905 // screen (e.g. a product-variant idx picked on the custom-price page) 906 // survives an admin partial-swap navigation. Clear it so detail panes 907 // keyed on $store.selection.selectedId don't fetch with a foreign id. 908 Alpine.store('selection').clear(); 909 910 // Then restore that selection so a refresh keeps the same row â but only 911 // if that row is actually present in the current list. 912 if (selrow) this._restorePersistedSelection(selrow); 913 }, 914 915 // A single setTimeout(0) races row render: on list pages where htmx/Alpine 916 // reprocess the table after load, the row can appear a tick or two later, so 917 // the one-shot lookup missed it and the selection was silently dropped (looked 918 // like the selection "jumped" away on refresh). Retry across a few frames until 919 // the row exists, then select it; give up quietly if it never shows (e.g. it's 920 // on another paginated page). 921 _restorePersistedSelection(selrow, attempt = 0) { 922 const sel = (window.CSS && CSS.escape) ? CSS.escape(selrow) : selrow; 923 if (document.querySelector('tr[data-selected-id="' + sel + '"]')) { 924 Alpine.store('selection').select(selrow); 925 return; 926 } 927 if (attempt < 20) { 928 setTimeout(() => this._restorePersistedSelection(selrow, attempt + 1), 30); 929 } 930 } 931 }; 932 } 933 934 /** 935 * The primary handler for after ANY search request completes. 936 * It updates the search tags and resets the selection pane. 937 */ 938 function handleSearchSuccess(event, templateId) { 939 if (event.detail.successful) { 940 941 // Get the raw payload from the request 942 const payload = event.detail.requestConfig.parameters; 943 944 945 // Set the tags using the payload 946 Alpine.store('searchTags').setFromPayload(payload, templateId); 947 948 Alpine.store('selection').clear(); 949 } else { 950 console.error('[handleSearchSuccess] A search request was not successful.'); 951 } 952 } 953 954 955 window.handleAdvancedSearchSuccess = (event, templateId) => { 956 if (event.detail.successful) { 957 Alpine.store('slideout').close(); 958 } 959 handleSearchSuccess(event, templateId); 960 }; 961 962 /** 963 * openBulkModal â click handler for Node::modalBulkForm bulk-action menus. 964 * 965 * Three scopes: 966 * - 'selection' : collect .p-check:checked values from #list_form, bail 967 * out with a notify if none selected, open a form modal 968 * with the checked-id count in the loadUrl query string 969 * and the ids JSON-encoded in `context` for the action 970 * handler. 971 * - 'selection-into' : like 'selection', but also needs the single 972 * highlighted row (`$store.selection.selectedId`) as a 973 * *target*. Bails with a notify if nothing is highlighted, 974 * substitutes the `__TARGET__` sentinel in loadUrl/postUrl 975 * with the target idx (so it lands in the URL path segment 976 * the action handler reads), and excludes the target from 977 * the checked-id sources. Used by the customer "Merge 978 * into selected" action. 979 * - 'results' : no id collection; the current URL's query string is 980 * appended to both loadUrl and postUrl so the action 981 * handler can re-run the current search. `context` is 982 * the literal string 'results'. 983 * 984 * Pairs with: 985 * - Node::modalBulkForm() â renders the trigger @click handler. 986 * - fetch/html/*_bulk.php loader â reads ?count= / query-string 987 * params to render the confirmation message. 988 * - fetch/action/*_bulk.php handler â reads $oREQUEST->context to 989 * distinguish selection (JSON array of ids) vs results (literal 990 * 'results' plus query-string search params). 991 */ 992 window.openBulkModal = function(title, loadUrl, postUrl, confirmText, scope, width) { 993 scope = scope || 'selection'; 994 995 if (scope === 'selection' || scope === 'selection-into') { 996 // 'selection-into' also needs the highlighted row as a target: inject 997 // it into the URL path (__TARGET__ sentinel) up front, then fall 998 // through to the same checked-source collection as plain 'selection'. 999 let target = null; 1000 if (scope === 'selection-into') { 1001 target = Alpine.store('selection')?.selectedId; 1002 if (!target) { 1003 notify('No target customer', 'Click a customer row to pick the merge target first.', 'info'); 1004 return; 1005 } 1006 loadUrl = loadUrl.replaceAll('__TARGET__', target); 1007 postUrl = postUrl.replaceAll('__TARGET__', target); 1008 } 1009 1010 const listForm = document.getElementById('list_form'); 1011 const rootSel = listForm ? listForm.querySelectorAll('input.p-check:checked') 1012 : document.querySelectorAll('#list_form input.p-check:checked'); 1013 let checked = Array.from(rootSel).map(el => el.value); 1014 // the target can't be a source (can't merge a customer into itself) 1015 if (target) checked = checked.filter(id => id !== target); 1016 if (checked.length === 0) { 1017 notify('Nothing selected', 'Tick a checkbox on at least one row first.', 'info'); 1018 return; 1019 } 1020 const sep = loadUrl.includes('?') ? '&' : '?'; 1021 Alpine.store('modals').open({ 1022 type: 'form', 1023 title: title, 1024 loadUrl: loadUrl + sep + 'count=' + checked.length, 1025 postUrl: postUrl, 1026 confirmText: confirmText || 'Confirm', 1027 context: JSON.stringify(checked), 1028 width: width || '' 1029 }); 1030 return; 1031 } 1032 1033 if (scope === 'results') { 1034 // The active filters live in the searchTags store â they're posted 1035 // to the list via htmx.ajax() without ever being written to the URL 1036 // (only chip *removal* prunes the URL). So window.location.search is 1037 // stale and would make the bulk action operate on a different set 1038 // than the one on screen. Prefer the store's authoritative filter 1039 // set; fall back to the URL for legacy pages that don't use tags. 1040 const tagStore = Alpine.store('searchTags'); 1041 let extra; 1042 if (tagStore && Array.isArray(tagStore.tags) && tagStore.tags.length) { 1043 const params = new URLSearchParams();
1044 tagStore.tags.forEach(t => { 1045 if (t.value !== '' && t.value != null) params.append(t.name, t.value); 1046 }); 1047 extra = params.toString(); 1048 } else { 1049 const qs = window.location.search || ''; 1050 extra = qs ? qs.replace(/^\?/, '') : ''; 1051 } 1052 const loadSep = loadUrl.includes('?') ? '&' : '?'; 1053 const postSep = postUrl.includes('?') ? '&' : '?'; 1054 Alpine.store('modals').open({ 1055 type: 'form', 1056 title: title, 1057 loadUrl: loadUrl + (extra ? loadSep + extra : ''), 1058 postUrl: postUrl + (extra ? postSep + extra : ''), 1059 confirmText: confirmText || 'Confirm', 1060 context: 'results', 1061 width: width || '' 1062 }); 1063 return; 1064 } 1065 1066 console.error('openBulkModal: unknown scope', scope); 1067 }; 1068 1069 /** 1070 * Global submit_list bridge for legacy Jaxon list-action dialogs. 1071 * 1072 * Pages converted to the modern htmx + modern-form blueprint no longer 1073 * include the old hidden `<form id="unisearch-form">` that the legacy 1074 * `submit_list({list_action, list_dlg_data})` callback used to populate 1075 * and submit. This shim is invoked by the Jaxon dialog `cbfunction` and 1076 * builds a temporary POST form on the fly so existing PHP `list_action` 1077 * handlers (delete / merge / email / set_group / etc.) keep working 1078 * without per-page rewires. 1079 * 1080 * If a page defines its own `submit_list` (e.g. orders.htm, which has 1081 * its own hidden legacy form for backwards compat), that wins. 1082 */ 1083 if (typeof window.submit_list !== 'function') { 1084 window.submit_list = function(data) { 1085 const checked = []; 1086 document.querySelectorAll('input.p-check:checked').forEach(el => { 1087 checked.push(el.value); 1088 }); 1089 1090 const form = document.createElement('form'); 1091 form.method = 'post'; 1092 form.action = location.pathname + location.search; 1093 form.style.display = 'none'; 1094 1095 const addField = (name, value) => { 1096 const input = document.createElement('input'); 1097 input.type = 'hidden'; 1098 input.name = name; 1099 input.value = value; 1100 form.appendChild(input); 1101 }; 1102 1103 addField('list_action', data.list_action || ''); 1104 addField('list_dlg_data', data.list_dlg_data || ''); 1105 addField('p_checked', checked.join(',')); 1106 1107 document.body.appendChild(form); 1108 form.submit(); 1109 }; 1110 } 1111 </script>
1111 1112 <!-- alpine-init.js is served via AssetCatalog ('alpine.init', registered in 1113 _default ModuleInterface::registerIncludes) so it gets the cache-buster 1114 version query. Do not re-add a hardcoded script tag here. --> 1115</body> 1116</html>
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.