1e51ef7def
- 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>
167 lines
6.8 KiB
JavaScript
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 { '<': '<', '>': '>', '&': '&', '"': '"' }[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>';
|
|
}
|
|
};
|
|
})();
|