2e9ad6a83f
- Komponente: Tempo-Steuerung an erste Aktions-Position (ganz links), Rest ans Ende. - Region: 🔔/🐢/🎯 durch eigene SVG-Icons ersetzt (logBadge erhalten). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
338 lines
16 KiB
JavaScript
338 lines
16 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'];
|
||
var GLOSSAR_IDS = ['glossar-btn', 'btn-glossar', 'btnGlossar', 'btnGlossary'];
|
||
var MUSIC_IDS = ['btnMusic', 'btnMusik', 'music-toggle-btn']; // sim-eigener Musik-Toggle (öffnet eigenes Panel)
|
||
var HOME_IDS = ['btnHome', 'btn-home', 'ggs-home-link'];
|
||
|
||
/* 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 });
|
||
}
|
||
|
||
/* Standard-Musikplayer (Klima-Tracks als Plattform-Default) für Sims
|
||
ohne eigenen Player. Aktiviert per data-ggs-music="default". */
|
||
// Plattform-Standardmusik (Platzhalter: Klima-Tracks, dort abgelegt)
|
||
var DEFAULT_TRACKS = [
|
||
{ label: '🎹 Lounge', file: '/sims/klima/assets/music/piano-lobby.mp3' },
|
||
{ label: '🌿 Ambient', file: '/sims/klima/assets/music/fingertip-rain.mp3' },
|
||
{ label: '🌙 Ruhig', file: '/sims/klima/assets/music/limestone-lullaby.mp3' },
|
||
{ label: '☕ Chill', file: '/sims/klima/assets/music/vibrocoffee-calm.mp3' },
|
||
{ label: '🎭 Drama', file: '/sims/klima/assets/music/rising-pressure.mp3' }
|
||
];
|
||
function buildDefaultMusic() {
|
||
var wrap = document.createElement('div');
|
||
wrap.className = 'ggs-music';
|
||
wrap.id = 'ggs-music';
|
||
var sel = document.createElement('select');
|
||
sel.className = 'ggs-music-sel'; sel.id = 'ggs-music-sel'; sel.title = 'Musik wählen';
|
||
DEFAULT_TRACKS.forEach(function (t) {
|
||
var o = document.createElement('option'); o.value = t.file; o.textContent = t.label; sel.appendChild(o);
|
||
});
|
||
var btn = document.createElement('button');
|
||
btn.className = 'ggs-music-btn'; btn.id = 'ggs-music-toggle'; btn.type = 'button'; btn.title = 'Play / Pause'; btn.textContent = '▶';
|
||
var vol = document.createElement('input');
|
||
vol.type = 'range'; vol.className = 'ggs-music-vol'; vol.id = 'ggs-music-vol';
|
||
vol.min = 0; vol.max = 100; vol.value = 30; vol.title = 'Lautstärke';
|
||
wrap.appendChild(sel); wrap.appendChild(btn); wrap.appendChild(vol);
|
||
|
||
var audio = new Audio();
|
||
audio.loop = true; audio.volume = 0.3; audio.src = DEFAULT_TRACKS[0].file;
|
||
var playing = false;
|
||
function play() { audio.play().then(function () { playing = true; btn.textContent = '⏸'; btn.classList.add('playing'); }).catch(function () {}); }
|
||
function pause() { audio.pause(); playing = false; btn.textContent = '▶'; btn.classList.remove('playing'); }
|
||
btn.addEventListener('click', function (e) { e.stopPropagation(); playing ? pause() : play(); });
|
||
sel.addEventListener('change', function () { audio.src = sel.value; if (playing) play(); });
|
||
vol.addEventListener('input', function () { audio.volume = (vol.value || 0) / 100; });
|
||
return wrap;
|
||
}
|
||
|
||
/* ------------------------------------------------------------ */
|
||
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 = byIds(header, HOME_IDS);
|
||
if (!homeBtn) {
|
||
var acts = header.querySelectorAll('a, button');
|
||
for (var h = 0; h < acts.length; h++) {
|
||
var a = acts[h];
|
||
var href = a.getAttribute('href') || '';
|
||
var t = a.textContent || '';
|
||
if (a.id === 'btn-info' || a.id === 'btn-sound' || a.id === 'btn-reset' || a.id === 'ggs-lehrplan-link') continue;
|
||
if (/schueler|cockpit|\/$/.test(href) || t.indexOf('🏠') >= 0 || t.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');
|
||
setIcon(byIds(header, GLOSSAR_IDS), 'glossar'); // sim-interner Glossar/Fachinfo-Button
|
||
setIcon(byIds(header, MUSIC_IDS), 'music'); // sim-eigener Musik-Toggle (öffnet eigenes Panel)
|
||
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 ---------------------------------------- */
|
||
// Default-Player erzeugen, falls die Sim keinen eigenen Player hat
|
||
var musicSpec = opts.music || header.getAttribute('data-ggs-music');
|
||
if (musicSpec && !q('#ggs-music') && actions) {
|
||
actions.appendChild(buildDefaultMusic());
|
||
}
|
||
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) {
|
||
// Standard-Steuerung ans Ende (Musik · Ton · Neustart · Hilfe · Glossar)
|
||
var tail = [musicWrap, soundBtn, resetBtn, helpBtn, q('.ggs-glossar')];
|
||
for (var o = 0; o < tail.length; o++) if (tail[o]) actions.appendChild(tail[o]);
|
||
// Tempo IMMER ganz links (erste Position der Aktionen)
|
||
if (tempoWrap) actions.insertBefore(tempoWrap, actions.firstChild);
|
||
}
|
||
|
||
/* --- 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();
|
||
}
|
||
})();
|