Sog_Viewer / interface.js
MikaFil's picture
Update interface.js
99fd21a verified
// interface.js
// ==============================
const currentScriptTag = document.currentScript;
(async function () {
// 1) Localiser la balise <script> et lire data-config
let scriptTag = currentScriptTag;
if (!scriptTag) {
const scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
if (
scripts[i].src.includes('interface.js') &&
scripts[i].hasAttribute('data-config')
) {
scriptTag = scripts[i];
break;
}
}
if (!scriptTag && scripts.length > 0) {
scriptTag = scripts[scripts.length - 1];
}
}
const configUrl = scriptTag.getAttribute('data-config');
let config = {};
if (configUrl) {
try {
const response = await fetch(configUrl);
config = await response.json();
} catch (error) {
return;
}
} else {
return;
}
// 2) CSS optionnelle
if (config.css_url) {
const linkEl = document.createElement('link');
linkEl.rel = 'stylesheet';
linkEl.href = config.css_url;
document.head.appendChild(linkEl);
}
// 3) ID d'instance
const instanceId = Math.random().toString(36).substr(2, 8);
// 4) Aspect ratio
const _isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const _isMobile = _isIOS || /Android/i.test(navigator.userAgent);
const activeAspectValue = (_isMobile && config['mobile-aspect'])
? config['mobile-aspect']
: config.aspect;
let aspectPercent = '100%';
if (activeAspectValue) {
if (activeAspectValue.includes(':')) {
const parts = activeAspectValue.split(':');
const w = parseFloat(parts[0]);
const h = parseFloat(parts[1]);
if (!isNaN(w) && !isNaN(h) && w > 0) {
aspectPercent = (h / w) * 100 + '%';
}
} else {
const aspectValue = parseFloat(activeAspectValue);
if (!isNaN(aspectValue) && aspectValue > 0) {
aspectPercent = 100 / aspectValue + '%';
}
}
} else {
const parentContainer = scriptTag.parentNode;
const containerWidth = parentContainer.offsetWidth;
const containerHeight = parentContainer.offsetHeight;
if (containerWidth > 0 && containerHeight > 0) {
aspectPercent = (containerHeight / containerWidth) * 100 + '%';
}
}
// 5) Conteneur widget
const widgetContainer = document.createElement('div');
widgetContainer.id = 'ply-widget-container-' + instanceId;
widgetContainer.classList.add('ply-widget-container');
widgetContainer.style.height = '0';
widgetContainer.style.paddingBottom = aspectPercent;
widgetContainer.setAttribute('data-original-aspect', aspectPercent);
const tooltipsButtonHTML = config.tooltips_url
? `<button id="tooltips-toggle-${instanceId}" class="widget-button tooltips-toggle">β¦Ώ</button>`
: '';
widgetContainer.innerHTML = `
<div id="viewer-container-${instanceId}" class="viewer-container">
<div id="progress-dialog-${instanceId}" class="progress-dialog">
<progress id="progress-indicator-${instanceId}" max="100" value="0"></progress>
</div>
<button id="fullscreen-toggle-${instanceId}" class="widget-button fullscreen-toggle">⇱</button>
<button id="help-toggle-${instanceId}" class="widget-button help-toggle">?</button>
<button id="reset-camera-btn-${instanceId}" class="widget-button reset-camera-btn">
<span class="reset-icon">⟲</span>
</button>
${tooltipsButtonHTML}
<div id="menu-content-${instanceId}" class="menu-content">
<span id="help-close-${instanceId}" class="help-close">Γ—</span>
<div class="help-text"></div>
</div>
</div>
`;
scriptTag.parentNode.appendChild(widgetContainer);
// 6) RΓ©fΓ©rences DOM
const viewerContainerElem = document.getElementById('viewer-container-' + instanceId);
const fullscreenToggle = document.getElementById('fullscreen-toggle-' + instanceId);
const helpToggle = document.getElementById('help-toggle-' + instanceId);
const helpCloseBtn = document.getElementById('help-close-' + instanceId);
const resetCameraBtn = document.getElementById('reset-camera-btn-' + instanceId);
const tooltipsToggleBtn = document.getElementById('tooltips-toggle-' + instanceId);
const menuContent = document.getElementById('menu-content-' + instanceId);
const helpTextDiv = menuContent.querySelector('.help-text');
if (config.btnRight === false) {
viewerContainerElem.classList.add('btn-left');
fullscreenToggle.style.display = 'none';
viewerContainerElem.classList.add('no-fullscreen');
}
// 7) Aide / textes
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isMobile = isIOS || /Android/i.test(navigator.userAgent);
const tooltipInstruction = config.tooltips_url ? '- β¦Ώ : annotations.<br>' : '';
if (isMobile) {
helpTextDiv.innerHTML =
'- DΓ©placement avec deux doigts.<br>' +
'- Rotation avec un doigt.<br>' +
'- Zoom en pinΓ§ant avec deux doigts.<br>' +
tooltipInstruction +
'- ⟲ réinitialisation de la caméra.<br>' +
'- ⇱ plein Γ©cran.<br>';
} else {
helpTextDiv.innerHTML =
'- Rotation avec le clic droit ou maj + ←↑↓→<br>' +
'- Zoom avec la molette ou ctrl + ↑↓<br>' +
'- DΓ©placement avec le clic gauche ou ←↑↓→<br>' +
tooltipInstruction +
'- ⟲ réinitialisation de la caméra.<br>' +
'- ⇱ plein Γ©cran.<br>';
}
// 8) Sizing dynamique du panneau d'aide
function setMenuContentMaxSize() {
if (!isMobile) {
menuContent.style.maxWidth = '';
menuContent.style.maxHeight = '';
menuContent.style.width = '';
menuContent.style.height = '';
menuContent.style.overflowY = '';
menuContent.style.overflowX = '';
return;
}
const vw = viewerContainerElem.offsetWidth;
const vh = viewerContainerElem.offsetHeight;
if (vw && vh) {
menuContent.style.maxWidth = Math.round(vw * 0.8) + 'px';
menuContent.style.maxHeight = Math.round(vh * 0.8) + 'px';
menuContent.style.width = '';
menuContent.style.height = '';
menuContent.style.overflowY = 'auto';
menuContent.style.overflowX = 'auto';
} else {
menuContent.style.maxWidth = '80vw';
menuContent.style.maxHeight = '80vh';
menuContent.style.overflowY = 'auto';
menuContent.style.overflowX = 'auto';
}
}
setMenuContentMaxSize();
window.addEventListener('resize', setMenuContentMaxSize);
document.addEventListener('fullscreenchange', setMenuContentMaxSize);
window.addEventListener('orientationchange', setMenuContentMaxSize);
// 9) Aide visible par dΓ©faut
menuContent.style.display = 'none';
viewerContainerElem.style.display = 'block';
// ─────────────────────────────────────────────────────────────────────────────
// 10) Overlay d'annotation
// ─────────────────────────────────────────────────────────────────────────────
const annOverlay = document.createElement('div');
annOverlay.className = 'ann-overlay';
const annTooltip = document.createElement('div');
annTooltip.className = 'ann-tooltip';
const annCloseBtn = document.createElement('button');
annCloseBtn.className = 'ann-close-btn-base ann-close-btn';
annCloseBtn.textContent = 'Γ—';
annCloseBtn.setAttribute('aria-label', 'Fermer');
const annImgWrap = document.createElement('div');
annImgWrap.className = 'ann-img-wrap';
const annImg = document.createElement('img');
annImg.alt = '';
annImgWrap.appendChild(annImg);
const annScroll = document.createElement('div');
annScroll.className = 'ann-scroll';
const annTitle = document.createElement('div');
annTitle.className = 'ann-title';
const annBody = document.createElement('div');
annBody.className = 'ann-body';
annScroll.appendChild(annTitle);
annScroll.appendChild(annBody);
// β€” Footer avec bouton lien β€”
const annFooter = document.createElement('div');
annFooter.className = 'ann-footer';
annFooter.style.display = 'none'; // cachΓ© par dΓ©faut, affichΓ© par JS si linkUrl prΓ©sent
const annLinkBtn = document.createElement('button');
annLinkBtn.className = 'ann-link-btn';
annFooter.appendChild(annLinkBtn);
annTooltip.appendChild(annCloseBtn);
annTooltip.appendChild(annImgWrap);
annTooltip.appendChild(annScroll);
annTooltip.appendChild(annFooter);
annOverlay.appendChild(annTooltip);
// β€” Lightbox β€”
const annLightbox = document.createElement('div');
annLightbox.className = 'ann-lightbox';
const annLbWrap = document.createElement('div');
annLbWrap.className = 'ann-lb-content-wrap';
const annLbImg = document.createElement('img');
annLbImg.alt = '';
const annLbClose = document.createElement('button');
annLbClose.className = 'ann-close-btn-base ann-lb-close';
annLbClose.textContent = 'Γ—';
annLbClose.setAttribute('aria-label', 'Fermer');
annLbWrap.appendChild(annLbImg);
annLbWrap.appendChild(annLbClose);
annLightbox.appendChild(annLbWrap);
widgetContainer.appendChild(annOverlay);
widgetContainer.appendChild(annLightbox);
// ─────────────────────────────────────────────────────────────────────────────
// 11) Charger viewer.js
// ─────────────────────────────────────────────────────────────────────────────
let viewerModule;
try {
const viewerUrl = `https://mikafil-sog-viewer.static.hf.space/viewer.js?inst=${instanceId}`;
viewerModule = await import(viewerUrl);
await viewerModule.initializeViewer(config, instanceId);
} catch (err) {
return;
}
// 12) Bouton tooltips : cacher si URL non valide
if (tooltipsToggleBtn) {
if (!config.tooltips_url) {
tooltipsToggleBtn.style.display = 'none';
} else {
fetch(config.tooltips_url)
.then((resp) => { if (!resp.ok) tooltipsToggleBtn.style.display = 'none'; })
.catch(() => { tooltipsToggleBtn.style.display = 'none'; });
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 13) Gestion de l'annotation (tooltip-selected β†’ overlay)
// ─────────────────────────────────────────────────────────────────────────────
let currentLinkUrl = '';
let annIsOpen = false;
function hideAnnotation() {
annIsOpen = false;
annOverlay.classList.remove('ann-open');
annLightbox.classList.remove('ann-lb-open');
}
function hideHelpPanel() {
menuContent.style.display = 'none';
}
document.addEventListener('tooltip-selected', (evt) => {
const detail = evt.detail || {};
console.log('detail reΓ§u :', JSON.stringify(detail));
const { title, description, imgUrl, linkUrl, linkText } = detail;
// ── DEBUG ──────────────────────────────────────────────────────────────────
console.log('[interface.js] tooltip-selected reΓ§u β€” detail complet :', detail);
console.log(' linkText :', linkText);
console.log(' linkUrl :', linkUrl);
// ──────────────────────────────────────────────────────────────────────────
annTitle.textContent = title || '';
annBody.innerHTML = (description || '')
.replace(/\\n/g, '<br>')
.replace(/\n/g, '<br>');
annScroll.scrollTop = 0;
currentLinkUrl = linkUrl || '';
// Image
if (imgUrl) {
annImg.src = imgUrl;
annLbImg.src = imgUrl;
annImgWrap.style.display = '';
} else {
annImg.src = '';
annLbImg.src = '';
annImgWrap.style.display = 'none';
}
// ── Bouton lien ────────────────────────────────────────────────────────────
if (currentLinkUrl) {
annLinkBtn.textContent = linkText || 'En savoir plus';
annFooter.style.display = 'flex';
console.log('[interface.js] Footer affichΓ© β€” bouton :', annLinkBtn.textContent);
} else {
annFooter.style.display = 'none';
console.log('[interface.js] Footer masquΓ© (linkUrl vide ou absent)');
}
// ──────────────────────────────────────────────────────────────────────────
annIsOpen = true;
annOverlay.classList.add('ann-open');
});
// Fermeture annotation
annCloseBtn.addEventListener('click', (e) => { e.stopPropagation(); hideAnnotation(); });
annOverlay.addEventListener('click', (e) => { if (e.target === annOverlay) hideAnnotation(); });
// Lien externe
annLinkBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (currentLinkUrl) window.open(currentLinkUrl, '_blank');
});
// Clic sur image β†’ lightbox
annImgWrap.addEventListener('click', (e) => {
e.stopPropagation();
if (annImg.src) annLightbox.classList.add('ann-lb-open');
});
// Fermeture lightbox
annLbClose.addEventListener('click', (e) => {
e.stopPropagation();
annLightbox.classList.remove('ann-lb-open');
});
annLightbox.addEventListener('click', (e) => {
if (e.target === annLightbox || e.target === annLbWrap) {
annLightbox.classList.remove('ann-lb-open');
}
});
// ── Bloqueurs d'input camΓ©ra ──────────────────────────────────────────────
widgetContainer.addEventListener('wheel', (e) => {
if (!annIsOpen) return;
if (annTooltip.contains(e.target) || annLightbox.contains(e.target)) {
e.stopImmediatePropagation();
if (!annScroll.contains(e.target)) e.preventDefault();
}
}, { capture: true, passive: false });
widgetContainer.addEventListener('touchmove', (e) => {
if (!annIsOpen) return;
const inTooltip = annTooltip.contains(e.target);
const lbOpen = annLightbox.classList.contains('ann-lb-open');
const inLightbox = lbOpen && annLightbox.contains(e.target);
if (inTooltip || inLightbox) {
e.stopImmediatePropagation();
if (!annScroll.contains(e.target)) e.preventDefault();
}
}, { capture: true, passive: false });
// ─────────────────────────────────────────────────────────────────────────────
// 14) Fullscreen
// ─────────────────────────────────────────────────────────────────────────────
let isFullscreen = false;
let savedState = null;
let savedParent = null;
let savedSibling = null;
function getHeightUnit() {
return (CSS && CSS.supports && CSS.supports('height', '100dvh')) ? '100dvh' : '100vh';
}
function saveCurrentState() {
if (isFullscreen) return;
const originalAspect = widgetContainer.getAttribute('data-original-aspect') || aspectPercent;
savedState = {
widget: {
position: widgetContainer.style.position,
top: widgetContainer.style.top,
left: widgetContainer.style.left,
width: widgetContainer.style.width,
height: widgetContainer.style.height,
maxWidth: widgetContainer.style.maxWidth,
maxHeight: widgetContainer.style.maxHeight,
paddingBottom: widgetContainer.style.paddingBottom || originalAspect,
margin: widgetContainer.style.margin
},
viewer: {
borderRadius: viewerContainerElem.style.borderRadius,
border: viewerContainerElem.style.border
}
};
}
function restoreOriginalStyles() {
if (!savedState) return;
const aspectToUse = savedState.widget.paddingBottom;
widgetContainer.style.position = savedState.widget.position || '';
widgetContainer.style.top = savedState.widget.top || '';
widgetContainer.style.left = savedState.widget.left || '';
widgetContainer.style.width = '100%';
widgetContainer.style.height = '0';
widgetContainer.style.maxWidth = savedState.widget.maxWidth || '';
widgetContainer.style.maxHeight = savedState.widget.maxHeight || '';
widgetContainer.style.paddingBottom = aspectToUse;
widgetContainer.style.margin = savedState.widget.margin || '';
widgetContainer.style.zIndex = '';
widgetContainer.classList.remove('fake-fullscreen');
viewerContainerElem.style.position = 'absolute';
viewerContainerElem.style.top = '0';
viewerContainerElem.style.left = '0';
viewerContainerElem.style.right = '0';
viewerContainerElem.style.bottom = '0';
viewerContainerElem.style.width = '100%';
viewerContainerElem.style.height = '100%';
viewerContainerElem.style.borderRadius = savedState.viewer.borderRadius || '';
viewerContainerElem.style.border = savedState.viewer.border || '';
if (savedParent) {
savedParent.insertBefore(widgetContainer, savedSibling);
savedParent = null;
savedSibling = null;
document.body.style.overflow = '';
}
if (viewerModule.app) {
viewerModule.app.resizeCanvas(viewerContainerElem.clientWidth, viewerContainerElem.clientHeight);
}
if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
savedState = null;
setMenuContentMaxSize();
}
function applyFullscreenStyles() {
const h = isIOS ? getHeightUnit() : '100vh';
widgetContainer.style.position = 'fixed';
widgetContainer.style.top = '0';
widgetContainer.style.left = '0';
widgetContainer.style.width = '100vw';
widgetContainer.style.height = h;
widgetContainer.style.maxWidth = '100vw';
widgetContainer.style.maxHeight = h;
widgetContainer.style.paddingBottom = '0';
widgetContainer.style.margin = '0';
widgetContainer.style.zIndex = '99999';
widgetContainer.style.border = 'none';
widgetContainer.style.borderRadius = '0';
viewerContainerElem.style.width = '100%';
viewerContainerElem.style.height = '100%';
viewerContainerElem.style.borderRadius = '0';
viewerContainerElem.style.border = 'none';
if (viewerModule.app) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
}
if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
isFullscreen = true;
setMenuContentMaxSize();
}
function enterFullscreen() {
if (!savedState) saveCurrentState();
if (isIOS) {
savedParent = widgetContainer.parentNode;
savedSibling = widgetContainer.nextSibling;
document.body.appendChild(widgetContainer);
document.body.style.overflow = 'hidden';
widgetContainer.classList.add('fake-fullscreen');
applyFullscreenStyles();
} else if (widgetContainer.requestFullscreen) {
widgetContainer
.requestFullscreen()
.then(applyFullscreenStyles)
.catch(() => {
widgetContainer.classList.add('fake-fullscreen');
applyFullscreenStyles();
});
} else {
widgetContainer.classList.add('fake-fullscreen');
applyFullscreenStyles();
}
}
function exitFullscreen() {
if (document.fullscreenElement === widgetContainer && document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
}
widgetContainer.classList.remove('fake-fullscreen');
restoreOriginalStyles();
isFullscreen = false;
if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
setMenuContentMaxSize();
}
fullscreenToggle.addEventListener('click', () => {
hideAnnotation();
isFullscreen ? exitFullscreen() : enterFullscreen();
});
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement && isFullscreen) {
isFullscreen = false;
restoreOriginalStyles();
if (fullscreenToggle) fullscreenToggle.textContent = '⇱';
} else if (document.fullscreenElement === widgetContainer) {
if (fullscreenToggle) fullscreenToggle.textContent = '⇲';
}
setMenuContentMaxSize();
});
window.addEventListener('resize', () => {
if (viewerModule.app) {
if (isFullscreen) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
if (isIOS) {
const h = getHeightUnit();
widgetContainer.style.height = h;
widgetContainer.style.maxHeight = h;
}
} else {
viewerModule.app.resizeCanvas(viewerContainerElem.clientWidth, viewerContainerElem.clientHeight);
}
}
setMenuContentMaxSize();
});
// ─────────────────────────────────────────────────────────────────────────────
// 15) Boutons d'interface
// ─────────────────────────────────────────────────────────────────────────────
helpToggle.addEventListener('click', (e) => {
hideAnnotation();
e.stopPropagation();
if (menuContent.style.display === 'block') {
menuContent.style.display = 'none';
} else {
menuContent.style.display = 'block';
setMenuContentMaxSize();
}
});
helpCloseBtn.addEventListener('click', hideHelpPanel);
resetCameraBtn.addEventListener('click', () => {
hideAnnotation();
if (viewerModule.resetViewerCamera) viewerModule.resetViewerCamera();
});
if (tooltipsToggleBtn) {
let tooltipsVisible = !!config.showTooltipsDefault;
tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
tooltipsToggleBtn.addEventListener('click', () => {
hideAnnotation();
tooltipsVisible = !tooltipsVisible;
tooltipsToggleBtn.style.opacity = tooltipsVisible ? '1' : '0.5';
document.dispatchEvent(new CustomEvent('toggle-tooltips', { detail: { visible: tooltipsVisible } }));
});
}
// 16) Γ‰chappement
document.addEventListener('keydown', (e) => {
if ((e.key === 'Escape' || e.key === 'Esc') && isFullscreen) exitFullscreen();
});
window.addEventListener('resize', () => {
if (viewerModule.app) {
if (isFullscreen) {
viewerModule.app.resizeCanvas(window.innerWidth, window.innerHeight);
} else {
viewerModule.app.resizeCanvas(viewerContainerElem.clientWidth, viewerContainerElem.clientHeight);
}
}
setMenuContentMaxSize();
});
// 17) Init par dΓ©faut
setTimeout(() => {
saveCurrentState();
document.dispatchEvent(
new CustomEvent('toggle-tooltips', { detail: { visible: !!config.showTooltipsDefault } })
);
setMenuContentMaxSize();
}, 200);
})();