Spaces:
Running
Running
| // fullscreen.js | |
| // Intègre un projet PlayCanvas via une balise <script data-src="..." data-aspect="16:9"> | |
| // Gère le plein écran natif (desktop/Android) et fake-fullscreen (iOS). | |
| (function () { | |
| // βββ 1. Localiser la balise <script> ββββββββββββββββββββββββββββββββββββββββ | |
| const scriptTag = | |
| document.currentScript || | |
| (function () { | |
| const all = document.getElementsByTagName('script'); | |
| for (let i = all.length - 1; i >= 0; i--) { | |
| if (all[i].src && all[i].src.includes('fullscreen.js')) return all[i]; | |
| } | |
| return all[all.length - 1]; | |
| })(); | |
| const playcanvasUrl = scriptTag.getAttribute('data-src'); | |
| if (!playcanvasUrl) { | |
| console.warn('[fullscreen.js] Attribut data-src manquant.'); | |
| return; | |
| } | |
| // βββ 2. DΓ©tection plateforme βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const isIOS = | |
| /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; | |
| const isMobile = | |
| isIOS || /Android/i.test(navigator.userAgent); | |
| // βββ 3. ID d'instance (support multi-embed sur la mΓͺme page) βββββββββββββββββ | |
| const id = Math.random().toString(36).substr(2, 8); | |
| // βββ 4. Calcul du padding-bottom (aspect ratio) ββββββββββββββββββββββββββββββ | |
| function computeAspectPadding(aspectStr) { | |
| if (!aspectStr) return null; | |
| if (aspectStr.includes(':')) { | |
| const [w, h] = aspectStr.split(':').map(Number); | |
| if (w > 0 && h > 0) return (h / w) * 100 + '%'; | |
| } else { | |
| const v = parseFloat(aspectStr); | |
| if (v > 0) return (100 / v) + '%'; | |
| } | |
| return null; | |
| } | |
| const aspectPadding = | |
| computeAspectPadding(scriptTag.getAttribute('data-aspect')) || '56.25%'; // dΓ©faut 16:9 | |
| // βββ 5. Injection du CSS βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| .pc-embed-wrapper-${id} { | |
| position: relative; | |
| width: 100%; | |
| height: 0; | |
| padding-bottom: ${aspectPadding}; | |
| overflow: hidden; | |
| background: #000; | |
| box-sizing: border-box; | |
| } | |
| .pc-embed-wrapper-${id}.fake-fullscreen { | |
| position: fixed !important; | |
| top: 0 !important; | |
| left: 0 !important; | |
| width: 100vw !important; | |
| height: 100vh !important; | |
| max-width: 100vw !important; | |
| max-height: 100vh !important; | |
| padding-bottom: 0 !important; | |
| margin: 0 !important; | |
| z-index: 99999; | |
| } | |
| .pc-embed-inner-${id} { | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| } | |
| .pc-embed-inner-${id} iframe { | |
| width: 100%; height: 100%; | |
| border: none; | |
| display: block; | |
| } | |
| .pc-fs-btn-${id} { | |
| position: absolute; | |
| bottom: 10px; | |
| right: 10px; | |
| z-index: 10; | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| border: none; | |
| background: rgba(0,0,0,0.55); | |
| color: #fff; | |
| font-size: 18px; | |
| line-height: 36px; | |
| text-align: center; | |
| cursor: pointer; | |
| user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| transition: background 0.2s; | |
| } | |
| .pc-fs-btn-${id}:hover { | |
| background: rgba(0,0,0,0.8); | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| // βββ 6. Construction du DOM ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = `pc-embed-wrapper-${id}`; | |
| wrapper.setAttribute('data-original-padding', aspectPadding); | |
| const inner = document.createElement('div'); | |
| inner.className = `pc-embed-inner-${id}`; | |
| const iframe = document.createElement('iframe'); | |
| iframe.src = playcanvasUrl; | |
| iframe.setAttribute('allowfullscreen', ''); | |
| iframe.setAttribute('allow', 'autoplay; fullscreen'); | |
| iframe.setAttribute( | |
| 'sandbox', | |
| 'allow-scripts allow-same-origin allow-pointer-lock allow-popups allow-forms' | |
| ); | |
| const fsBtn = document.createElement('button'); | |
| fsBtn.className = `pc-fs-btn-${id}`; | |
| fsBtn.title = 'Plein Γ©cran'; | |
| fsBtn.textContent = 'β±'; | |
| fsBtn.setAttribute('aria-label', 'Plein Γ©cran'); | |
| inner.appendChild(iframe); | |
| wrapper.appendChild(inner); | |
| wrapper.appendChild(fsBtn); | |
| scriptTag.parentNode.insertBefore(wrapper, scriptTag.nextSibling); | |
| // βββ 7. Γtat fullscreen ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let isFullscreen = false; | |
| let savedPadding = aspectPadding; | |
| let savedParent = null; | |
| let savedNextSibling = null; | |
| // βββ 8. Styles communs fullscreen (sans dΓ©placement DOM) βββββββββββββββββββββ | |
| // UtilisΓ© par le fullscreen natif (desktop/Android). | |
| // NE PAS modifier cette fonction pour iOS β voir applyFakeFullscreenStyles. | |
| function applyFullscreenStyles() { | |
| wrapper.style.position = 'fixed'; | |
| wrapper.style.top = '0'; | |
| wrapper.style.left = '0'; | |
| wrapper.style.width = '100vw'; | |
| wrapper.style.height = '100vh'; | |
| wrapper.style.maxWidth = '100vw'; | |
| wrapper.style.maxHeight = '100vh'; | |
| wrapper.style.paddingBottom = '0'; | |
| wrapper.style.margin = '0'; | |
| wrapper.style.zIndex = '99999'; | |
| wrapper.classList.add('fake-fullscreen'); | |
| fsBtn.textContent = 'β²'; | |
| isFullscreen = true; | |
| } | |
| // βββ 9. Fake-fullscreen iOS avec tΓ©lΓ©portation DOM + correction hauteur βββββββ | |
| // Sur iOS Safari, 100vh inclut la barre de navigation β le bas est masquΓ©. | |
| // On utilise 100dvh (dynamic viewport height) si supportΓ©, sinon on compense | |
| // avec env(safe-area-inset-bottom). | |
| // La tΓ©lΓ©portation dans <body> permet d'Γ©chapper aux stacking contexts parents. | |
| function applyFakeFullscreenStyles() { | |
| savedParent = wrapper.parentNode; | |
| savedNextSibling = wrapper.nextSibling; | |
| document.body.appendChild(wrapper); | |
| // Styles communs | |
| applyFullscreenStyles(); | |
| // Correction hauteur iOS uniquement | |
| if (isIOS) { | |
| if (CSS.supports('height', '1dvh')) { | |
| // iOS 16+ : dvh exclut exactement la barre Safari | |
| wrapper.style.height = '100dvh'; | |
| wrapper.style.maxHeight = '100dvh'; | |
| } else { | |
| // iOS 15 et moins : compensation via safe-area-inset-bottom | |
| wrapper.style.height = 'calc(100vh - env(safe-area-inset-bottom, 0px))'; | |
| wrapper.style.maxHeight = 'calc(100vh - env(safe-area-inset-bottom, 0px))'; | |
| } | |
| } | |
| } | |
| // βββ 10. Restaurer les styles normaux ββββββββββββββββββββββββββββββββββββββββ | |
| function restoreStyles() { | |
| wrapper.style.position = ''; | |
| wrapper.style.top = ''; | |
| wrapper.style.left = ''; | |
| wrapper.style.width = '100%'; | |
| wrapper.style.height = '0'; | |
| wrapper.style.maxWidth = ''; | |
| wrapper.style.maxHeight = ''; | |
| wrapper.style.paddingBottom = savedPadding; | |
| wrapper.style.margin = ''; | |
| wrapper.style.zIndex = ''; | |
| wrapper.classList.remove('fake-fullscreen'); | |
| fsBtn.textContent = 'β±'; | |
| isFullscreen = false; | |
| // Remettre en place uniquement si on avait tΓ©lΓ©portΓ© (iOS / fallback) | |
| if (savedParent) { | |
| savedParent.insertBefore(wrapper, savedNextSibling); | |
| savedParent = null; | |
| savedNextSibling = null; | |
| } | |
| } | |
| // βββ 11. EntrΓ©e en plein Γ©cran βββββββββββββββββββββββββββββββββββββββββββββββ | |
| function enterFullscreen() { | |
| savedPadding = | |
| wrapper.getAttribute('data-original-padding') || aspectPadding; | |
| if (isIOS) { | |
| // iOS : fake-fullscreen avec tΓ©lΓ©portation DOM et correction hauteur | |
| applyFakeFullscreenStyles(); | |
| document.body.style.overflow = 'hidden'; | |
| return; | |
| } | |
| // Tentative via l'API Fullscreen standard (desktop / Android) | |
| const el = wrapper; | |
| const req = | |
| el.requestFullscreen || | |
| el.webkitRequestFullscreen || | |
| el.mozRequestFullScreen || | |
| el.msRequestFullscreen; | |
| if (req) { | |
| req.call(el).catch(() => { | |
| // API refusΓ©e β fallback fake avec tΓ©lΓ©portation (sans correction iOS) | |
| applyFakeFullscreenStyles(); | |
| document.body.style.overflow = 'hidden'; | |
| }); | |
| // Sur succès, applyFullscreenStyles est appelé par onFullscreenChange | |
| } else { | |
| applyFakeFullscreenStyles(); | |
| document.body.style.overflow = 'hidden'; | |
| } | |
| } | |
| // βββ 12. Sortie du plein Γ©cran βββββββββββββββββββββββββββββββββββββββββββββββ | |
| function exitFullscreen() { | |
| if ( | |
| document.fullscreenElement === wrapper || | |
| document.webkitFullscreenElement === wrapper | |
| ) { | |
| (document.exitFullscreen || document.webkitExitFullscreen || (() => {})) | |
| .call(document) | |
| .catch(() => {}); | |
| } | |
| restoreStyles(); | |
| document.body.style.overflow = ''; | |
| } | |
| // βββ 13. Bouton fullscreen βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fsBtn.addEventListener('click', function (e) { | |
| e.stopPropagation(); | |
| isFullscreen ? exitFullscreen() : enterFullscreen(); | |
| }); | |
| // Touch explicite pour iOS (Γ©vite le dΓ©lai 300ms) | |
| fsBtn.addEventListener('touchend', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| isFullscreen ? exitFullscreen() : enterFullscreen(); | |
| }); | |
| // βββ 14. Γcouter les changements natifs fullscreen βββββββββββββββββββββββββββ | |
| // NB : on appelle applyFullscreenStyles (sans dvh) car le fullscreen natif | |
| // gΓ¨re lui-mΓͺme la hauteur β pas besoin de correction iOS ici. | |
| function onFullscreenChange() { | |
| const fsEl = | |
| document.fullscreenElement || document.webkitFullscreenElement; | |
| if (!fsEl && isFullscreen) { | |
| restoreStyles(); | |
| document.body.style.overflow = ''; | |
| } else if (fsEl === wrapper && !isFullscreen) { | |
| applyFullscreenStyles(); | |
| } | |
| } | |
| document.addEventListener('fullscreenchange', onFullscreenChange); | |
| document.addEventListener('webkitfullscreenchange', onFullscreenChange); | |
| // βββ 15. Touche Γchap (fake-fullscreen iOS / fallback) βββββββββββββββββββββββ | |
| document.addEventListener('keydown', function (e) { | |
| if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) { | |
| exitFullscreen(); | |
| } | |
| }); | |
| // βββ 16. Resize / orientation ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| window.addEventListener('resize', function () { | |
| if (isFullscreen) { | |
| wrapper.style.width = '100vw'; | |
| wrapper.style.height = '100vh'; | |
| } | |
| }); | |
| window.addEventListener('orientationchange', function () { | |
| if (isFullscreen) { | |
| setTimeout(function () { | |
| wrapper.style.width = '100vw'; | |
| wrapper.style.height = '100vh'; | |
| }, 200); | |
| } | |
| }); | |
| })(); |