/** * GeoGraSim Plattform-Standard: Glossar-Tooltip-Pattern * ===================================================== * Macht beliebige Begriff-Elemente * tap-/klickbar: öffnet ein kleines Popup mit kurzer Begriffsklärung + Link zum * vollen Glossar-Eintrag in einem neuen Tab. * * USAGE im Sim-HTML: * * * * USAGE im Markup (auch in dynamisch eingefügten Strings): * Welthandel * * VERHALTEN: * - Tap/Klick auf Begriff → Popup unter dem Element, Click-outside schließt * - Esc-Taste → schließt Popup * - Popup-Inhalt: Titel, Kurztext, „Voller Glossar-Eintrag →"-Link (öffnet im neuen Tab) * - Begriffe ohne DB-Eintrag UND ohne Fallback werden nicht reagiert (kein Popup) * * STYLE: CSS wird automatisch injiziert (kein extra ). Optisch: * - Begriff bekommt gestrichelte Unterlinie + Hover-Highlight * - Popup: weiß, leichter Schatten, max 320px, fade-in * - Mobile-/Touch-freundlich, kein Hover-only * * KOMPATIBILITÄT: Pure Vanilla-JS, kein jQuery, kein React. Funktioniert auf * iPad Safari, Chrome, Firefox. */ (function () { 'use strict'; var CSS_INJECTED = false; function injectCss() { if (CSS_INJECTED) return; var css = '.ggs-g{cursor:help;border-bottom:1px dashed currentColor;text-decoration:none;color:inherit;background:rgba(0,0,0,.02);padding:0 2px;border-radius:2px;transition:background .15s,border-color .15s}' + '.ggs-g:hover,.ggs-g:focus{background:rgba(31,75,55,.10);border-bottom-style:solid;outline:none}' + '.ggs-g-popup{position:fixed;z-index:9999;max-width:320px;background:#fff;color:#222;border-radius:10px;padding:12px 14px 10px;font-size:.86rem;line-height:1.45;font-family:inherit;box-shadow:0 8px 24px rgba(0,0,0,.18),0 2px 4px rgba(0,0,0,.08);border:1px solid #d9d3c4;animation:ggs-g-pop .18s ease-out}' + '.ggs-g-popup-title{font-weight:800;color:#1f4b37;font-size:.92rem;margin-bottom:4px}' + '.ggs-g-popup-text{color:#3a3a3a;margin-bottom:8px}' + '.ggs-g-popup-link{display:inline-block;color:#1f4b37;font-size:.78rem;font-weight:700;text-decoration:none;border-bottom:1px solid #1f4b37;padding-bottom:1px}' + '.ggs-g-popup-link:hover{color:#0e2d20}' + '@keyframes ggs-g-pop{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}'; var s = document.createElement('style'); s.id = 'ggs-glossar-tooltip-css'; s.textContent = css; document.head.appendChild(s); CSS_INJECTED = true; } var CACHE = {}; var FALLBACK = {}; var GLOSSAR_PAGE = '/glossar'; var MODULE_ID = null; var _popup = null; function loadGlossar(apiBase, moduleId) { var url = apiBase + '/glossar.php' + (moduleId ? '?module=' + encodeURIComponent(moduleId) : ''); fetch(url) .then(function (r) { return r.ok ? r.json() : []; }) .then(function (arr) { if (!Array.isArray(arr)) return; arr.forEach(function (e) { CACHE[e.key_slug] = { title: e.title, text: e.short || e.text || '' }; }); }) .catch(function () { /* still — Fallback greift, sonst kein Popup */ }); } function resolve(key) { return CACHE[key] || FALLBACK[key] || null; } function closePopup() { if (_popup) { _popup.remove(); _popup = null; } } function openPopup(targetEl, key) { var entry = resolve(key); if (!entry) return; closePopup(); var popup = document.createElement('div'); popup.className = 'ggs-g-popup'; popup.setAttribute('role', 'tooltip'); var html = '
' + escapeHtml(entry.title) + '
' + '
' + escapeHtml(entry.text) + '
' + 'Voller Glossar-Eintrag →'; popup.innerHTML = html; document.body.appendChild(popup); _popup = popup; // Position: unter dem Element, innerhalb Viewport var rect = targetEl.getBoundingClientRect(); var pw = popup.offsetWidth; var ph = popup.offsetHeight; var left = Math.max(8, Math.min(rect.left, window.innerWidth - pw - 8)); var top = rect.bottom + 8; // wenn unten zu wenig Platz: oben anzeigen if (top + ph > window.innerHeight - 8 && rect.top - ph - 8 > 8) { top = rect.top - ph - 8; } popup.style.left = left + 'px'; popup.style.top = top + 'px'; } function escapeHtml(s) { return String(s == null ? '' : s).replace(/[<>&"]/g, function (c) { return { '<': '<', '>': '>', '&': '&', '"': '"' }[c]; }); } function onDocClick(e) { // Click auf Popup → durchlassen (Link funktioniert) if (_popup && _popup.contains(e.target)) return; // Click auf Glossar-Begriff → öffnen var el = e.target.closest('[data-glossar]'); if (el) { var key = el.dataset.glossar; if (!key) return; var entry = resolve(key); if (!entry) return; e.preventDefault(); e.stopPropagation(); openPopup(el, key); } else { // Click outside → schließen closePopup(); } } function onKeydown(e) { if (e.key === 'Escape') closePopup(); } window.GlossarTooltip = { init: function (opts) { opts = opts || {}; var apiBase = (opts.apiBase || '/api').replace(/\/+$/, ''); // Strippe BEIDE Endungen — sowohl '/php/api' (Plattform-Standard) als // auch '/api' (legacy / Subroute). Sonst wird aus '/php/api' nur '/api' // entfernt und der Glossar-Link landet auf '/php/glossar' → 404. GLOSSAR_PAGE = apiBase.replace(/\/php\/api$/, '').replace(/\/api$/, '') + '/glossar'; MODULE_ID = opts.moduleId || null; if (opts.fallback) FALLBACK = opts.fallback; injectCss(); loadGlossar(apiBase, MODULE_ID); document.addEventListener('click', onDocClick, true); document.addEventListener('keydown', onKeydown); }, // Manuelles Snippet hinzufügen (für dynamische Fälle ohne API-Load) addFallback: function (key, entry) { FALLBACK[key] = entry; }, // Hilfsfunktion für dynamische Inhalte: gib markup zurück markup: function (label, slug) { return '' + escapeHtml(label) + ''; } }; })();