1// Modal for wistia video 2 3const modalHtml = ` 4 <div class="modal-main | js-modal-main"> 5 <div class="modal-main__in | js-modal-main-backdrop"> 6 <div class="modal-main__main"> 7 8 <div class="modal-main__content | js-modal-main-to"></div> 9 10 <div class="modal-main__close js-modal-main-close"> 11 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="Group"><path id="Vector" d="M12 2L2 12" stroke="#7F9AB9" stroke-width="2" stroke-linecap="square" stroke-linejoin="round"/><path id="Vector_2" d="M2 2L12 12" stroke="#7F9AB9" stroke-width="2" stroke-linecap="square" stroke-linejoin="round"/></g> 12 </svg> 13 </div> 14 15 </div> 16 </div> 17 </div> 18 19 <div class="modal-main-backdrop | js-modal-backdrop"></div> 20`; 21 22document.body.appendChild(document.createRange().createContextualFragment(modalHtml)); 23 24const modal = (elem, target) => { 25 const modalEl = document.querySelector(elem); 26 const backdrop = document.querySelector('.js-modal-backdrop'); 27 28 let contentFrom; 29 const contentTo = modalEl.querySelector(`${elem}-to`); 30 const modalMain = modalEl.querySelector('.modal-main__main'); 31 const modalInner = modalEl.querySelector('.modal-main__in'); 32 const closeButton = document.querySelector(`${elem}-close`); 33 const defaultVideoRatio = 16 / 9; 34 let currentVideoRatio = defaultVideoRatio; 35 let resizeHandler = null; 36 37 const getPaddingTopByRatio = (ratio) => { 38 if (!ratio || ratio <= 0) { 39 return '56.25%'; 40 } 41 42 return `${(1 / ratio) * 100}%`; 43 }; 44 45 const getProviderMeta = (videoId) => { 46 if (videoId.includes('youtube') || videoId.includes('youtu.be')) { 47 let id = null; 48 49 try { 50 const urlObj = new URL(videoId); 51 52 // https://www.youtube.com/watch?v=ID 53 id = urlObj.searchParams.get('v'); 54 55 if (!id) { 56 // https://youtu.be/ID, https://www.youtube.com/embed/ID, 57 // https://www.youtube.com/shorts/ID, https://www.youtube.com/v/ID 58 const match = urlObj.pathname.match(/\/(?:embed|shorts|v)\/([^/?#]+)|^\/([^/?#]+)/); 59 id = match ? (match[1] || match[2]) : null; 60 } 61 } catch (e) { 62 // not a full URL â fall back to extracting an id from the raw string 63 const match = videoId.match(/(?:v=|embed\/|shorts\/|youtu\.be\/)([^&/?#]+)/); 64 id = match ? match[1] : null; 65 } 66 67 if (!id) { 68 return null; 69 } 70 71 return { 72 type: 'youtube', 73 id, 74 embedSrc: `https://www.youtube.com/embed/${id}`, 75 oembedUrl: `https://www.youtube.com/oembed?url=${encodeURIComponent(`https://www.youtube.com/watch?v=${id}`)}&format=json` 76 }; 77 } 78 79 if (videoId.includes('vimeo')) { 80 const regex = /vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/; 81 const match = videoId.match(regex); 82 const id = match ? match[3] : null; 83 84 if (!id) { 85 return null; 86 } 87 88 return { 89 type: 'vimeo', 90 id, 91 embedSrc: `https://player.vimeo.com/video/${id}`, 92 oembedUrl: `https://vimeo.com/api/oembed.json?url=${encodeURIComponent(`https://vimeo.com/${id}`)}` 93 }; 94 } 95 96 return { 97 type: 'wistia', 98 id: videoId, 99 embedSrc: `https://fast.wistia.net/embed/iframe/${videoId}?videoFoam=true`, 100 oembedUrl: `https://fast.wistia.net/oembed?url=${encodeURIComponent(`https://home.wistia.com/medias/${videoId}`)}` 101 }; 102 }; 103 104 const getVideoRatio = async (providerMeta) => { 105 if (!providerMeta || !providerMeta.oembedUrl) { 106 return null; 107 } 108 109 try { 110 const res = await fetch(providerMeta.oembedUrl); 111 112 if (!res.ok) { 113 return null; 114 } 115 116 const data = await res.json(); 117 const width = Number(data.width); 118 const height = Number(data.height); 119 120 if (width > 0 && height > 0) { 121 return width / height; 122 } 123 } catch (e) {} 124 125 return null; 126 }; 127 128 const getVideoEmbedConfig = (providerMeta) => { 129 const paddingTop = getPaddingTopByRatio(defaultVideoRatio); 130 131 const responsiveWrapper = (iframe) => ` 132 <div class="js-video-responsive-padding" style="position:relative;padding:${paddingTop} 0 0 0;"> 133 <div style="width:100%;height:100%;position:absolute;top:0;left:0;"> 134 ${iframe} 135 </div> 136 </div> 137 `; 138 139 const iframe = (src) => ` 140 <iframe src="${src}" width="100%" height="100%" allowfullscreen style="width:100%;height:100%;border:none;"></iframe> 141 `; 142 143 if (!providerMeta || providerMeta.type === 'youtube' || providerMeta.type === 'vimeo') { 144 return { html: responsiveWrapper(iframe(providerMeta ? providerMeta.embedSrc : '')) }; 145 } 146 147 // Wistia default 148 return { 149 html: ` 150 <div class="wistia_responsive_padding js-video-responsive-padding" style="padding:${paddingTop} 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><iframe src="${providerMeta.embedSrc}" allow="autoplay; fullscreen" allowtransparency="true" frameborder="0" scrolling="no" class="wistia_embed" name="wistia_embed" allowfullscreen msallowfullscreen width="100%" height="100%"></iframe></div> 151 </div> 152 <script src="https://fast.wistia.net/assets/external/E-v1.js" async></script> 153 ` 154 }; 155 }; 156 157 const fitVideoToViewport = (ratio) => { 158 if (!modalMain || !modalInner || !ratio || ratio <= 0) { 159 return; 160 } 161 162 const innerStyles = window.getComputedStyle(modalInner); 163 const horizontalPadding = parseFloat(innerStyles.paddingLeft) + parseFloat(innerStyles.paddingRight); 164 const verticalPadding = parseFloat(innerStyles.paddingTop) + parseFloat(innerStyles.paddingBottom); 165 166 const availableWidth = Math.max(window.innerWidth - horizontalPadding, 320); 167 const availableHeight = Math.max(window.innerHeight - verticalPadding, 180); 168 169 let fittedWidth = Math.min(availableWidth, 970); 170 let fittedHeight = fittedWidth / ratio; 171 172 if (fittedHeight > availableHeight) { 173 fittedHeight = availableHeight; 174 fittedWidth = fittedHeight * ratio; 175 } 176 177 modalMain.style.width = `${Math.floor(fittedWidth)}px`; 178 modalMain.style.maxWidth = 'none'; 179 }; 180 181 const bindViewportResize = () => { 182 if (resizeHandler) { 183 window.removeEventListener('resize', resizeHandler); 184 } 185 186 resizeHandler = () => { 187 fitVideoToViewport(currentVideoRatio); 188 }; 189 190 window.addEventListener('resize', resizeHandler); 191 resizeHandler(); 192 }; 193 194 const unbindViewportResize = () => { 195 if (resizeHandler) { 196 window.removeEventListener('resize', resizeHandler); 197 resizeHandler = null; 198 } 199 200 modalMain.style.width = ''; 201 modalMain.style.maxWidth = ''; 202 }; 203 204 const applyVideoRatio = async (providerMeta) => { 205 const ratio = await getVideoRatio(providerMeta); 206 const responsivePadding = contentTo.querySelector('.js-video-responsive-padding'); 207 208 if (!responsivePadding || !ratio) { 209 return; 210 } 211 212 currentVideoRatio = ratio; 213 responsivePadding.style.padding = `${getPaddingTopByRatio(ratio)} 0 0 0`; 214 fitVideoToViewport(currentVideoRatio); 215 }; 216 217 const open = () => { 218 modalEl.classList.add('open'); 219 backdrop.classList.add('show'); 220 }; 221 222 const close = () => { 223 modalEl.classList.remove('open'); 224 backdrop.classList.remove('show'); 225 unbindViewportResize(); 226 currentVideoRatio = defaultVideoRatio; 227 if (contentFrom) { 228 contentFrom.classList.add('hidden'); 229 document.body.appendChild(contentFrom); 230 } 231 setTimeout(() => contentTo.innerHTML = '', 200); 232 }; 233 234 closeButton.addEventListener('click', close); 235 backdrop.addEventListener('click', close); 236 237 document.addEventListener('click', (e) => { 238 if (e.target.classList.contains((`${elem}-backdrop`).slice(1))) { 239 close(); 240 } 241 }); 242 243 document.documentElement.addEventListener('keydown', (e) => { 244 if (e.key === 'Escape') { 245 close(); 246 } 247 }); 248 249 // init 250 if (target.dataset.videoId) { 251 const videoId = target.dataset.videoId; 252 const providerMeta = getProviderMeta(videoId);
253 const embedConfig = getVideoEmbedConfig(providerMeta); 254 255 contentTo.appendChild(document.createRange().createContextualFragment(embedConfig.html)); 256 currentVideoRatio = defaultVideoRatio; 257 bindViewportResize(); 258 applyVideoRatio(providerMeta); 259 } else { 260 contentFrom = document.querySelector(`.${target.dataset.modal}`); 261 262 if (contentFrom) { 263 contentFrom.classList.remove('hidden'); 264 contentTo.appendChild(contentFrom); 265 } 266 } 267 268 open(); 269}; 270 271document.querySelectorAll('.js-modal-main-open').forEach((btn) => { 272 btn.addEventListener('click', (e) => { 273 e.preventDefault(); 274 modal('.js-modal-main', btn); 275 }); 276});
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.