7b1bdb8d1b
- 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>
285 lines
13 KiB
JavaScript
285 lines
13 KiB
JavaScript
/* ============================================================
|
||
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();
|
||
}
|
||
})();
|