hochbegabt - andererseits.org
Zum Inhalt springen
Um dir ein optimales Erlebnis zu bieten, verwenden wir Technologien wie Cookies, um Geräteinformationen zu speichern und/oder darauf zuzugreifen. Wenn du diesen Technologien zustimmst, können wir Daten wie das Surfverhalten oder eindeutige IDs auf dieser Website verarbeiten. Wenn du deine Zustimmung nicht erteilst oder zurückziehst, können bestimmte Merkmale und Funktionen beeinträchtigt werden.
Die technische Speicherung oder der Zugang ist unbedingt erforderlich für den rechtmäßigen Zweck, die Nutzung eines bestimmten Dienstes zu ermöglichen, der vom Teilnehmer oder Nutzer ausdrücklich gewünscht wird, oder für den alleinigen Zweck, die Übertragung einer Nachricht über ein elektronisches Kommunikationsnetz durchzuführen.
Die technische Speicherung oder der Zugriff ist für den rechtmäßigen Zweck der Speicherung von Präferenzen erforderlich, die nicht vom Abonnenten oder Benutzer angefordert wurden.
Die technische Speicherung oder der Zugriff, der ausschließlich zu statistischen Zwecken erfolgt.
Die technische Speicherung oder der Zugriff, der ausschließlich zu anonymen statistischen Zwecken verwendet wird. Ohne eine Vorladung, die freiwillige Zustimmung deines Internetdienstanbieters oder zusätzliche Aufzeichnungen von Dritten können die zu diesem Zweck gespeicherten oder abgerufenen Informationen allein in der Regel nicht dazu verwendet werden, dich zu identifizieren.
Die technische Speicherung oder der Zugriff ist erforderlich, um Nutzerprofile zu erstellen, um Werbung zu versenden oder um den Nutzer auf einer Website oder über mehrere Websites hinweg zu ähnlichen Marketingzwecken zu verfolgen.
Zustimmung verwalten
. No dependencies.
* Keeps WordPress/MediaElement playback; uses native inputs for accessibility.
* Optional on : data-audio-title="Titel des Artikels".
*/
(() => {
"use strict";
if (window.andererseitsAudio) {
window.andererseitsAudio.scan();
return;
}
let nextId = 0;
const enhanced = new WeakSet();
const players = new Set();
const clock = (value) => {
const seconds = Math.max(0, Math.floor(value || 0));
const parts = [Math.floor(seconds / 60) % 60, seconds % 60];
if (seconds >= 3600) parts.unshift(Math.floor(seconds / 3600));
return parts.map((part) => String(part).padStart(2, "0")).join(":");
};
const spokenTime = (value) => {
const seconds = Math.max(0, Math.floor(value || 0));
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return [
hours && `${hours} ${hours === 1 ? "Stunde" : "Stunden"}`,
minutes && `${minutes} ${minutes === 1 ? "Minute" : "Minuten"}`,
`${seconds % 60} ${seconds % 60 === 1 ? "Sekunde" : "Sekunden"}`,
]
.filter(Boolean)
.join(", ");
};
function enhance(audio) {
const root = audio.closest(".mejs-container.mejs-audio.wp-audio-shortcode");
const player = audio.player;
if (!root || !player || !player.media || enhanced.has(root)) return;
const controls = root.querySelector(".mejs-controls");
const rail = root.querySelector(".mejs-time-rail");
const play = root.querySelector(".mejs-playpause-button > button");
let muteBox = root.querySelector(".mejs-volume-button");
if (!controls || !rail || !play) return;
enhanced.add(root);
const id = `ae-audio-${++nextId}`;
if (!audio.id) audio.id = `${id}-media`;
const precedingWidget =
root.closest(".elementor-widget")?.previousElementSibling;
const nearbyTitle =
precedingWidget?.matches(
".elementor-widget-text-editor, .elementor-widget-heading",
) && precedingWidget.textContent.trim().length <= 120
? precedingWidget.textContent.trim()
: "";
const explicitTitle =
audio.dataset.audioTitle ||
root.closest("[data-audio-title]")?.dataset.audioTitle ||
audio.getAttribute("aria-label");
const pageTitle = (
root.closest("article")?.querySelector("h1") ||
document.querySelector("h1")
)?.textContent.trim();
const title =
explicitTitle ||
nearbyTitle ||
(pageTitle ? `${pageTitle}, Audio ${nextId}` : "");
root.setAttribute("role", "group");
root.setAttribute(
"aria-label",
title ? `Audio: ${title}` : `Audioplayer ${nextId}`,
);
root.removeAttribute("tabindex");
const oldTitle = root.previousElementSibling;
if (oldTitle?.classList.contains("mejs-offscreen")) oldTitle.hidden = true;
// MediaElement's global shortcuts interfere with buttons/selects and
// restart paused audio when seeking. Native controls own their keys.
player.options.enableKeyboard = false;
player.options.autoRewind = false;
player.options.playText = "Wiedergeben";
player.options.pauseText = "Pausieren";
// −10 / +10 flank Play, so visual and tab order match.
const skipBack = document.createElement("div");
skipBack.className = "ae-audio-skips";
skipBack.innerHTML = `10 `;
const skipForward = document.createElement("div");
skipForward.className = "ae-audio-skips";
skipForward.innerHTML = `10 `;
const tools = document.createElement("div");
tools.className = "ae-audio-tools";
tools.innerHTML = `
1×
Lautstärke
`;
const status = document.createElement("div");
status.className = "ae-audio-status ae-audio-sr";
status.setAttribute("role", "status");
status.setAttribute("aria-live", "polite");
status.setAttribute("aria-atomic", "true");
const error = document.createElement("p");
error.className = "ae-audio-error";
error.hidden = true;
const help = document.createElement("span");
help.id = `${id}-help`;
help.className = "ae-audio-sr";
help.textContent =
"Pfeiltasten: 10 Sekunden. Bild auf oder ab: 30 Sekunden. Pos1: Anfang. Ende: Schluss.";
const seek = document.createElement("input");
seek.type = "range";
seek.className = "ae-audio-range ae-audio-seek";
seek.min = "0";
seek.max = "0";
seek.step = "1";
seek.value = "0";
seek.setAttribute("aria-label", "Wiedergabeposition");
seek.setAttribute("aria-describedby", help.id);
const tooltip = document.createElement("span");
tooltip.className = "ae-audio-tooltip";
tooltip.setAttribute("aria-hidden", "true");
const originalSlider = root.querySelector(".mejs-time-total");
originalSlider?.setAttribute("aria-hidden", "true");
originalSlider?.setAttribute("tabindex", "-1");
rail.append(seek, tooltip, help);
const inner = root.querySelector(".mejs-inner");
play.parentElement.before(skipBack);
play.parentElement.after(skipForward);
controls.append(tools);
inner.append(error, status);
if (!muteBox) {
muteBox = document.createElement("div");
muteBox.className = "mejs-button mejs-volume-button mejs-mute";
muteBox.innerHTML =
' ';
}
const volumeBox = tools.querySelector(".ae-audio-volume");
volumeBox.prepend(muteBox);
const mute = muteBox?.querySelector("button");
const volume = tools.querySelector("input");
const speed = tools.querySelector(".ae-audio-rate");
const skipButtons = [skipBack, skipForward].map((box) => box.firstChild);
let previousVolume = audio.volume || 0.8;
root.querySelectorAll("button, input, select").forEach((control) => {
control.setAttribute("aria-controls", audio.id);
});
// Retain tab order and browser/AT default behavior. No single-letter
// shortcuts, application mode, focus trapping or document key listeners.
let announcementTimer;
function announce(message) {
clearTimeout(announcementTimer);
status.textContent = "";
announcementTimer = setTimeout(() => {
if (root.isConnected) status.textContent = message;
}, 60);
}
const duration = () =>
Number.isFinite(audio.duration) && audio.duration > 0
? audio.duration
: 0;
function loadMetadata() {
if (audio.preload === "none" && !audio.readyState && !audio.error) {
audio.preload = "metadata";
audio.load();
}
}
// Respect lazy loading: only request metadata for the player a person
// enters, rather than downloading every clip on a page with many players.
root.addEventListener("focusin", loadMetadata);
root.addEventListener("pointerenter", loadMetadata, { once: true });
// Handle rejected play promises (network errors, browser restrictions)
// instead of letting MediaElement silently discard them.
play.addEventListener("click", (event) => {
event.stopPropagation();
if (!audio.paused && !audio.ended) {
audio.pause();
return;
}
if (audio.ended) audio.currentTime = 0;
const attempt = audio.play();
attempt?.catch((reason) => {
if (reason.name === "AbortError") return;
error.hidden = false;
error.textContent =
"Wiedergabe nicht möglich. Bitte erneut versuchen oder die Seite neu laden.";
announce(error.textContent);
syncButtons();
});
});
function paintRange(input, value, maximum) {
input.style.setProperty(
"--ae-fill",
`${maximum ? (value / maximum) * 100 : 0}%`,
);
}
function updateTime() {
const maximum = duration();
const current = Math.min(maximum, Math.max(0, audio.currentTime || 0));
seek.max = String(maximum);
seek.value = String(current);
seek.disabled = !maximum || !!audio.error;
seek.setAttribute(
"aria-valuetext",
maximum
? `${spokenTime(current)} von ${spokenTime(maximum)}`
: "Dauer noch nicht verfügbar",
);
paintRange(seek, current, maximum);
tooltip.textContent = clock(current);
skipButtons.forEach((button) =>
button.setAttribute("aria-disabled", String(!maximum || !!audio.error)),
);
}
function syncButtons() {
const playLabel =
audio.paused || audio.ended ? "Wiedergeben" : "Pausieren";
if (play.getAttribute("aria-label") !== playLabel)
play.setAttribute("aria-label", playLabel);
if (play.title !== playLabel) play.title = playLabel;
if (mute) {
const silent = audio.muted || audio.volume === 0;
const label = silent ? "Ton einschalten" : "Stummschalten";
if (mute.getAttribute("aria-label") !== label)
mute.setAttribute("aria-label", label);
if (mute.title !== label) mute.title = label;
muteBox.classList.toggle("mejs-mute", !silent);
muteBox.classList.toggle("mejs-unmute", silent);
}
}
function syncVolume() {
if (audio.volume > 0) previousVolume = audio.volume;
const value = audio.muted ? 0 : Math.round(audio.volume * 100);
volume.value = String(value);
volume.setAttribute(
"aria-valuetext",
`${value} Prozent${audio.muted ? ", stummgeschaltet" : ""}`,
);
paintRange(volume, value, 100);
syncButtons();
}
function seekTo(value, announcePosition = false) {
if (!duration() || audio.error) {
loadMetadata();
if (announcePosition)
announce(
audio.error
? "Audio ist nicht verfügbar."
: "Audio wird geladen. Bitte kurz warten.",
);
return;
}
try {
audio.currentTime = Math.max(0, Math.min(duration(), value));
updateTime();
if (announcePosition)
announce(
`Position: ${spokenTime(audio.currentTime)} von ${spokenTime(duration())}.`,
);
} catch {
announce("Diese Position ist noch nicht verfügbar.");
}
}
skipButtons.forEach((button) =>
button.addEventListener("click", () => {
seekTo(audio.currentTime + Number(button.dataset.seconds), true);
}),
);
seek.addEventListener("input", () => seekTo(Number(seek.value)));
seek.addEventListener("keydown", (event) => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
return;
const offsets = {
ArrowLeft: -10,
ArrowDown: -10,
ArrowRight: 10,
ArrowUp: 10,
PageDown: -30,
PageUp: 30,
};
const target =
event.key === "Home"
? 0
: event.key === "End"
? duration()
: event.key in offsets
? audio.currentTime + offsets[event.key]
: null;
if (target === null) return;
event.preventDefault();
event.stopPropagation();
seekTo(target);
});
seek.addEventListener("pointermove", (event) => {
const box = seek.getBoundingClientRect();
const fraction = Math.max(
0,
Math.min(1, (event.clientX - box.left) / box.width),
);
tooltip.textContent = clock(fraction * duration());
});
volume.addEventListener("input", () => {
const value = Number(volume.value) / 100;
audio.volume = value;
audio.muted = value === 0;
if (Math.abs(audio.volume - value) > 0.01) {
announce(
"Bitte die Lautstärketasten des Geräts verwenden. Dieser Browser erlaubt keinen Lautstärkeregler.",
);
}
syncVolume();
});
mute.addEventListener(
"click",
(event) => {
event.stopImmediatePropagation();
if (audio.muted || audio.volume === 0) {
if (audio.volume === 0) audio.volume = previousVolume;
audio.muted = false;
} else audio.muted = true;
syncVolume();
},
true,
);
// Native range supplies arrows/Home/End, including AT increment actions.
// Escape hides the slider shown on hover/focus (WCAG 1.4.13) and returns
// focus to the mute button; it reappears once focus or pointer re-enter.
function dismissVolume(event) {
if (event.key !== "Escape" || getComputedStyle(volume).opacity === "0")
return;
event.preventDefault();
event.stopPropagation();
volumeBox.classList.add("ae-audio-volume-dismissed");
if (document.activeElement === volume) mute.focus();
}
const restoreVolume = () =>
volumeBox.classList.remove("ae-audio-volume-dismissed");
volumeBox.addEventListener("keydown", dismissVolume);
// While hovered, focus may be elsewhere on the page.
volumeBox.addEventListener("pointerenter", () =>
document.addEventListener("keydown", dismissVolume),
);
volumeBox.addEventListener("pointerleave", () => {
document.removeEventListener("keydown", dismissVolume);
restoreVolume();
});
volume.addEventListener("focus", restoreVolume);
volumeBox.addEventListener("focusout", (event) => {
if (!volumeBox.contains(event.relatedTarget)) restoreVolume();
});
// One button cycles the tempo; the visible value is part of its name.
const rates = [1, 1.25, 1.5, 2, 0.5, 0.75];
const rateText = (rate) => `${String(rate).replace(".", ",")}×`;
function syncRate() {
speed.textContent = rateText(audio.playbackRate);
speed.setAttribute(
"aria-label",
`Abspielgeschwindigkeit ${rateText(audio.playbackRate)}`,
);
}
speed.addEventListener("click", () => {
const next =
rates[(rates.indexOf(audio.playbackRate) + 1) % rates.length];
try {
audio.playbackRate = next;
announce(
`Abspielgeschwindigkeit: ${String(audio.playbackRate).replace(".", ",")} mal.`,
);
} catch {
announce(
"Diese Abspielgeschwindigkeit wird vom Browser nicht unterstützt.",
);
}
syncRate();
});
audio.addEventListener("ratechange", syncRate);
[
"timeupdate",
"durationchange",
"loadedmetadata",
"emptied",
"seeked",
].forEach((name) => audio.addEventListener(name, updateTime));
["play", "playing", "pause", "ended", "loadedmetadata"].forEach((name) =>
audio.addEventListener(name, syncButtons),
);
// MediaElement can overwrite localized labels in a later renderer event.
// Observe only those two attributes, and write only when different.
const labelObserver = new MutationObserver(syncButtons);
[play, mute].forEach((button) =>
labelObserver.observe(button, {
attributes: true,
attributeFilter: ["aria-label", "title"],
}),
);
audio.addEventListener("volumechange", syncVolume);
audio.addEventListener("play", () => {
error.hidden = true;
for (const other of players) {
if (!other.isConnected) players.delete(other);
else if (other !== audio && !other.paused) other.pause();
}
});
audio.addEventListener("ended", () => announce("Wiedergabe beendet."));
const showError = () => {
error.hidden = false;
error.textContent =
"Audio konnte nicht geladen werden. Bitte die Seite neu laden oder es später erneut versuchen.";
announce(error.textContent);
updateTime();
};
audio.addEventListener("error", showError);
audio.addEventListener("loadstart", () => {
error.hidden = true;
});
players.add(audio);
root.classList.add("ae-audio-enhanced");
updateTime();
syncVolume();
syncRate();
if (audio.error) showError();
}
function scan() {
document.querySelectorAll("audio.wp-audio-shortcode").forEach(enhance);
}
window.andererseitsAudio = { scan };
// WordPress and Elementor may initialize/add players after this snippet.
let queued = false;
const observer = new MutationObserver((records) => {
if (
queued ||
!records.some((record) =>
[...record.addedNodes].some(
(node) =>
node.nodeType === 1 &&
(node.matches("audio, .mejs-controls") ||
node.querySelector("audio, .mejs-controls")),
),
)
)
return;
queued = true;
requestAnimationFrame(() => {
queued = false;
scan();
});
});
function start() {
scan();
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", start, { once: true });
else start();
})();
/* andererseits Audio-Player Erweiterung – ENDE */