Files
geograsim/App/assets/js/ggs-header.js
T
Adminator 7b1bdb8d1b ggs-header Batch 2 Start: Farmer + Komponenten-Vorruestung
- Komponente: Logo-Fallback (Kreis-Logo wenn Sim keins hat), Overlay-Modus
  (data-ggs-overlay fuer Vollbild-Canvas), generische Icon-Button-Optik fuer
  Aktionen ohne .ggs-btn-ghost-Klasse, Ton-IDs erweitert (audio-toggle/btnSound).
- Farmer als erstes Bespoke-Muster: ggs-topbar -> ggs-header, schwebende Pille
  (Fallback-Logo + Titel) + Geld-KPI + Icon-Buttons; interne Fachinfo bleibt
  (data-ggs-no-glossar).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 14:33:53 +02:00

285 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
GGS-HEADER — Einheitliche Sim-Kopfzeile (Entwurf B)
------------------------------------------------------------
Überführt das vorhandene <header class="ggs-header">-Markup
(Design-System) in die schwebende Pillen-Optik OHNE die
Spiel-Logik anzufassen: bestehende IDs/Klassen (#btn-sound,
#btn-reset, [data-speed], #level-badge, #ggs-music …) bleiben
erhalten, die Handler der Sim greifen weiter.
Nutzung: einfach ggs-header.css + ggs-header.js einbinden.
Auto-Init auf DOMContentLoaded für jedes .ggs-header.
Optionen über data-Attribute am <header>:
data-ggs-home Home-Ziel (Default: vorh. Home-Link / "/schueler")
data-ggs-glossar Glossar-URL (Default: "/glossar")
data-ggs-sim Sim-ID (für /glossar?module=<id>)
data-ggs-no-glossar Glossar-Button weglassen
============================================================ */
(function () {
'use strict';
var SPRITE_ID = 'ggs-header-sprite';
var ICONS = {
home: '<path d="M4 11.4 12 4l8 7.4"/><path d="M6 10.3V19a1 1 0 0 0 1 1h3v-5h4v5h3a1 1 0 0 0 1-1v-8.7"/>',
tempo: '<path d="M4.6 16.5a7.5 7.5 0 0 1 14.8 0"/><path d="M12 16 15.2 12"/><circle cx="12" cy="16.3" r="1.3" fill="currentColor" stroke="none"/>',
music: '<path d="M9 17V6.4l9-1.8V15"/><circle cx="6.5" cy="17" r="2.4"/><circle cx="15.5" cy="15" r="2.4"/>',
sound: '<path d="M4 9.5h3.2L11 6v12l-3.8-3.5H4z"/><path d="M14.6 9.6a4 4 0 0 1 0 4.8"/><path d="M17 7.4a7 7 0 0 1 0 9.2"/>',
'sound-off': '<path d="M4 9.5h3.2L11 6v12l-3.8-3.5H4z"/><path d="M15.5 10l4 4"/><path d="M19.5 10l-4 4"/>',
restart: '<path d="M19 12a7 7 0 1 1-2-4.9"/><path d="M19 4.5V9h-4.5"/>',
help: '<circle cx="12" cy="12" r="8.4"/><path d="M9.7 9.3a2.4 2.4 0 0 1 4.6.8c0 1.6-2.3 1.8-2.3 3.2"/><circle cx="12" cy="16.4" r=".7" fill="currentColor" stroke="none"/>',
glossar: '<path d="M12 6.6C10 5.2 7 5.2 5 6.2v11c2-1 5-1 7 .4"/><path d="M12 6.6c2-1.4 5-1.4 7-.4v11c-2-1-5-1-7 .4"/><path d="M12 6.6v11.4"/>',
pause: '<rect x="6" y="5" width="4" height="14" rx="1.2" fill="currentColor"/><rect x="14" y="5" width="4" height="14" rx="1.2" fill="currentColor"/>',
caret: '<path d="M7 10l5 5 5-5"/>'
};
/* SVG-Sprite einmalig ins Dokument legen */
function injectSprite() {
if (document.getElementById(SPRITE_ID)) return;
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('id', SPRITE_ID);
svg.setAttribute('width', '0');
svg.setAttribute('height', '0');
svg.setAttribute('aria-hidden', 'true');
svg.style.position = 'absolute';
var defs = '';
for (var key in ICONS) {
var stroked = (key === 'pause');
defs += '<symbol id="ggic-' + key + '" viewBox="0 0 24 24"' +
(stroked ? '' : ' fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"') +
'>' + ICONS[key] + '</symbol>';
}
svg.innerHTML = '<defs>' + defs + '</defs>';
document.body.insertBefore(svg, document.body.firstChild);
}
function icon(name, cls) {
return '<svg class="ic ' + (cls || '') + '" aria-hidden="true"><use href="#ggic-' + name + '"/></svg>';
}
function setIcon(el, name, title) {
if (!el) return;
el.innerHTML = icon(name);
if (title) el.setAttribute('title', title);
}
/* Ersten passenden Button per ID-Liste finden (bespoke IDs abdecken) */
function byIds(scope, ids) {
for (var i = 0; i < ids.length; i++) {
var el = scope.querySelector('#' + ids[i]);
if (el) return el;
}
return null;
}
var SOUND_IDS = ['btn-sound', 'btn-mute', 'btnVoiceToggle', 'btnAudioToggle', 'btnAudio', 'audio-toggle', 'btnSound', 'btn-ton', 'btn-audio'];
var RESET_IDS = ['btn-reset', 'btnReset', 'btn-restart', 'btn-neustart'];
var HELP_IDS = ['btn-info', 'btnHelp', 'btn-help', 'btn-anleitung', 'btn-hilfe'];
/* Glyph-Buttons (Ton), die die Sim per textContent umschaltet:
Icon nach jeder Änderung nachziehen, Mute-Zustand als Icon spiegeln. */
function watchGlyphBtn(btn, mapFn, title) {
if (!btn) return;
if (title) btn.setAttribute('title', title);
var apply = function () {
if (btn.querySelector('svg.ic')) return; // schon Icon → nichts tun
setIcon(btn, mapFn(btn.textContent || ''));
};
apply();
new MutationObserver(apply).observe(btn, { childList: true, characterData: true, subtree: true });
}
/* ------------------------------------------------------------ */
function enhance(header, opts) {
if (!header || header.hasAttribute('data-ggs-enhanced')) return;
opts = opts || {};
injectSprite();
header.setAttribute('data-ggs-enhanced', '1');
header.classList.add('ggs-header--float');
if (opts.overlay || header.getAttribute('data-ggs-overlay') === '1') header.classList.add('ggs-header--overlay');
var q = function (sel) { return header.querySelector(sel); };
var homeHref = opts.home || header.getAttribute('data-ggs-home') || '/schueler';
var simId = opts.sim || header.getAttribute('data-ggs-sim') || '';
var glossarHref = opts.glossar || header.getAttribute('data-ggs-glossar') ||
('/glossar' + (simId ? '?module=' + encodeURIComponent(simId) : ''));
var actions = q('.ggs-header-actions');
/* --- 1) Marken-Pille: Logo · Titel · Level ---------------- */
var logo = q('.ggs-header-logo');
var pille = document.createElement('div');
pille.className = 'ggs-hpille';
if (logo) {
logo.parentNode.insertBefore(pille, logo);
} else {
header.insertBefore(pille, header.firstChild);
// Fallback: Kreis-Logo erzeugen, falls die Sim keins im Header hat
logo = document.createElement('a');
logo.className = 'ggs-header-logo';
logo.setAttribute('href', homeHref);
var limg = document.createElement('img');
limg.className = 'ggs-header-logo-icon';
limg.src = opts.logo || header.getAttribute('data-ggs-logo') || '/assets/img/bildLogo.png';
limg.alt = '';
logo.appendChild(limg);
}
pille.appendChild(logo);
var mod = q('.ggs-header-module'); if (mod) pille.appendChild(mod);
var badges = header.querySelectorAll('.ggs-header-badge');
for (var i = 0; i < badges.length; i++) pille.appendChild(badges[i]);
var tick = header.querySelector('.kw-tick-dot'); if (tick) pille.appendChild(tick);
/* --- 2) Home-Button links neben die Pille ----------------- */
var homeBtn = null;
var acts = header.querySelectorAll('.ggs-header-actions a, .ggs-header-actions button');
for (var h = 0; h < acts.length; h++) {
var a = acts[h];
var href = a.getAttribute('href') || '';
if (a.id === 'btn-info' || a.id === 'btn-sound' || a.id === 'btn-reset' || a.id === 'ggs-lehrplan-link') continue;
if (/schueler|cockpit|\/$/.test(href) || (a.textContent || '').indexOf('🏠') >= 0) { homeBtn = a; break; }
}
if (!homeBtn) {
homeBtn = document.createElement('a');
if (actions) actions.appendChild(homeBtn);
}
homeBtn.className = 'ggs-btn-ghost';
if (!homeBtn.getAttribute('href')) homeBtn.setAttribute('href', homeHref);
setIcon(homeBtn, 'home', 'Startseite');
header.insertBefore(homeBtn, pille.nextSibling);
/* Spacer sicherstellen (zwischen Home-Gruppe und Aktionen) */
var spacer = q('.ggs-header-spacer');
if (!spacer) {
spacer = document.createElement('div');
spacer.className = 'ggs-header-spacer';
}
header.insertBefore(spacer, actions);
/* --- 3) Aktions-Icons setzen (bespoke IDs via Listen) ----- */
var helpBtn = byIds(header, HELP_IDS);
var soundBtn = byIds(header, SOUND_IDS);
var resetBtn = byIds(header, RESET_IDS);
setIcon(helpBtn, 'help', 'Hilfe / Anleitung');
watchGlyphBtn(soundBtn, function (t) { return /🔇|🔈|🔕|off|aus|mute|stumm/i.test(t) ? 'sound-off' : 'sound'; }, 'Ton an/aus');
setIcon(resetBtn, 'restart', 'Neu starten');
var lehrplan = q('#ggs-lehrplan-link'); if (lehrplan) lehrplan.style.display = 'none';
/* --- 4) Tempo-Panel (nur wenn .ggs-speed vorhanden) ------- */
var speed = q('.ggs-speed');
var tempoWrap = null, spdBtn = null;
if (speed) {
tempoWrap = document.createElement('div');
tempoWrap.className = 'ggs-tempo';
spdBtn = document.createElement('button');
spdBtn.className = 'ggs-spd';
spdBtn.type = 'button';
spdBtn.setAttribute('title', 'Tempo');
var pop = document.createElement('div');
pop.className = 'ggs-pop ggs-pop--tempo';
speed.parentNode.insertBefore(tempoWrap, speed);
pop.appendChild(speed);
tempoWrap.appendChild(pop);
tempoWrap.appendChild(spdBtn);
var syncSpeed = function () {
var active = speed.querySelector('.ggs-speed-btn.active') || speed.querySelector('.ggs-speed-btn[data-speed="1"]');
if (!active) return;
var sp = active.getAttribute('data-speed'); // klassische Speed-Buttons
var txt = (active.textContent || '').replace(/\s+/g, ' ').trim();
var isAuto = /auto/i.test(active.getAttribute('title') || '') || active.hasAttribute('data-auto') || active.getAttribute('data-mode') === 'auto';
var isPause = sp === '0' || active.getAttribute('data-mode') === 'pause' || /^\s*(pause|⏸)/i.test(txt);
if (isPause) {
spdBtn.innerHTML = icon('pause');
} else if (isAuto) {
// Auto-Label (z. B. „Auto 1×") steht schon voll im Text → nicht doppeln
spdBtn.innerHTML = /auto/i.test(txt) ? '<b>' + txt + '</b>' : '<b>' + (txt || 'Auto') + '</b><small>Auto</small>';
} else {
spdBtn.innerHTML = '<b>' + (txt || (sp + '×')) + '</b>';
}
spdBtn.classList.toggle('act', isPause || isAuto || (sp !== null && sp !== '1'));
};
syncSpeed();
// Klick auf ein Segment: Sim-Handler läuft weiter, Label spiegeln, Panel bleibt offen
var spBtns = speed.querySelectorAll('.ggs-speed-btn');
for (var s = 0; s < spBtns.length; s++) {
spBtns[s].addEventListener('click', function () { setTimeout(syncSpeed, 0); });
}
// Ein Observer über den ganzen Speed-Block: fängt Klassen-Flip UND Text-Änderung (Auto 1×↔8×)
var mo = new MutationObserver(syncSpeed);
mo.observe(speed, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['class'] });
spdBtn.addEventListener('click', function (e) {
e.stopPropagation();
tempoWrap.classList.toggle('open');
});
}
/* --- 5) Musik-Panel (nur wenn #ggs-music vorhanden) ------- */
var music = q('#ggs-music');
var musicWrap = null;
if (music) {
musicWrap = document.createElement('div');
musicWrap.className = 'ggs-music-wrap';
var mtrigger = document.createElement('button');
mtrigger.className = 'ggs-mtrigger';
mtrigger.type = 'button';
mtrigger.setAttribute('title', 'Musik');
mtrigger.innerHTML = icon('music');
var mpop = document.createElement('div');
mpop.className = 'ggs-pop ggs-pop--music';
music.parentNode.insertBefore(musicWrap, music);
music.removeAttribute('hidden');
mpop.appendChild(music);
musicWrap.appendChild(mtrigger);
musicWrap.appendChild(mpop);
mtrigger.addEventListener('click', function (e) {
e.stopPropagation();
musicWrap.classList.toggle('open');
});
// Nach Song-Auswahl Panel schließen
var sel = music.querySelector('#ggs-music-sel, select');
if (sel) sel.addEventListener('change', function () { musicWrap.classList.remove('open'); });
}
/* --- 6) Glossar-Button (target=_blank) -------------------- */
if (!opts.noGlossar && !header.hasAttribute('data-ggs-no-glossar') && actions) {
var gl = document.createElement('a');
gl.className = 'ggs-btn-ghost ggs-glossar';
gl.setAttribute('href', glossarHref);
gl.setAttribute('target', '_blank');
gl.setAttribute('rel', 'noopener');
setIcon(gl, 'glossar', 'Glossar (neuer Tab)');
actions.appendChild(gl);
}
/* --- 7) Aktionen in Mockup-Reihenfolge sortieren ---------- */
if (actions) {
var order = [tempoWrap, musicWrap, soundBtn, resetBtn, helpBtn, q('.ggs-glossar')];
for (var o = 0; o < order.length; o++) if (order[o]) actions.appendChild(order[o]);
}
/* --- 8) Panels bei Außen-Klick schließen ------------------ */
document.addEventListener('click', function (e) {
if (musicWrap && !musicWrap.contains(e.target)) musicWrap.classList.remove('open');
// Tempo bleibt bewusst offen (Toggle) — nur Klick auf den Button schließt
});
return header;
}
function autoInit() {
var headers = document.querySelectorAll('.ggs-header:not([data-ggs-enhanced])');
for (var i = 0; i < headers.length; i++) {
if (headers[i].getAttribute('data-ggs-no-float') === '1') continue;
try { enhance(headers[i]); }
catch (err) { if (window.console) console.warn('[ggs-header] enhance fehlgeschlagen:', err); }
}
}
window.GGSHeader = { enhance: enhance, autoInit: autoInit, icon: icon };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoInit);
} else {
autoInit();
}
})();