Files
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

167 lines
6.8 KiB
JavaScript

/**
* GeoGraSim Plattform-Standard: Glossar-Tooltip-Pattern
* =====================================================
* Macht beliebige <span class="ggs-g" data-glossar="slug">Begriff</span>-Elemente
* tap-/klickbar: öffnet ein kleines Popup mit kurzer Begriffsklärung + Link zum
* vollen Glossar-Eintrag in einem neuen Tab.
*
* USAGE im Sim-HTML:
* <script src="/assets/js/glossar-tooltip.js" defer></script>
* <script>
* window.addEventListener('DOMContentLoaded', () => {
* GlossarTooltip.init({
* apiBase: '/api', // optional, default '/api'
* moduleId: 'weltkueche', // Glossar-Filter (lädt nur die Sim-eigenen Begriffe)
* fallback: { // optional: lokale Snippets für Begriffe ohne DB-Eintrag
* 'co-pilot': { title:'Co-Pilot:in', text:'...' }
* }
* });
* });
* </script>
*
* USAGE im Markup (auch in dynamisch eingefügten Strings):
* <span class="ggs-g" data-glossar="welthandel">Welthandel</span>
*
* 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 <link>). 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 = '<div class="ggs-g-popup-title">' + escapeHtml(entry.title) + '</div>'
+ '<div class="ggs-g-popup-text">' + escapeHtml(entry.text) + '</div>'
+ '<a class="ggs-g-popup-link" href="' + GLOSSAR_PAGE + '?term=' + encodeURIComponent(key) + '" target="_blank" rel="noopener">Voller Glossar-Eintrag →</a>';
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 { '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[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 '<span class="ggs-g" data-glossar="' + escapeHtml(slug) + '">' + escapeHtml(label) + '</span>';
}
};
})();