Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
274 lines
12 KiB
JavaScript
274 lines
12 KiB
JavaScript
/* =====================================================================
|
||
installieren.js — die Seite als App aufs Gerät holen
|
||
=====================================================================
|
||
|
||
ZWEI DINGE IN EINER DATEI, und sie gehoeren zusammen:
|
||
|
||
1. Der Service Worker wird angemeldet -- IMMER, auf jeder Seite.
|
||
2. Der Knopf, der das Installieren anbietet.
|
||
|
||
WARUM BEIDES HIER STEHT (22.09.2026, nach Filipes drittem Anlauf:
|
||
"ich will einen installieren button ... das hab ich auch schon paar
|
||
mal gefragt. mach das es ist seeeeeeeehr wichtig."):
|
||
|
||
Der Knopf gab es seit dem 20.09. Er war nur NIE ZU SEHEN. Chrome
|
||
zeigt sein Installationsangebot (`beforeinstallprompt`) erst, wenn
|
||
ein Service Worker laeuft -- und der wurde ausschliesslich in
|
||
`glocke.js` angemeldet, also erst, wenn jemand Benachrichtigungen
|
||
ERLAUBT. Auf Filipes Rechner stehen sie auf "Vom Browser blockiert".
|
||
Damit lief nie einer, damit kam das Ereignis nie, damit blieb der
|
||
Knopf `hidden`. Fuer immer.
|
||
|
||
Die Anmeldung gehoert also nicht an die Glocke, sondern an das
|
||
Installieren -- und beides an jede Seite. Deshalb eine Datei, die
|
||
ueberall geladen wird, statt einer Abhaengigkeit, die kaum jemand
|
||
erfuellt.
|
||
|
||
UND DESHALB LAEDT AUCH DIE ANMELDEWAND SIE. Sie hat keine
|
||
Kopfleiste und deshalb frueher kein Angebot bekommen -- dabei ist
|
||
sie die Seite, auf der jeder zuerst landet.
|
||
===================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
/* ---------------------------------------------------------------------
|
||
DER SERVICE WORKER -- ohne Bedingung, aber auch ohne Laerm
|
||
---------------------------------------------------------------------
|
||
Er scheitert an genug Stellen, an denen niemand etwas falsch
|
||
gemacht hat: Privates Fenster, abgeschaltete Worker,
|
||
Unternehmens-Richtlinie, veralteter Browser. Nichts davon ist ein
|
||
Fehler, den ein Besucher sehen soll -- die Seite funktioniert auch
|
||
ohne, sie laesst sich dann nur nicht installieren.
|
||
|
||
`load` abwarten: Die Anmeldung zieht Bandbreite, und zwar genau in
|
||
dem Moment, in dem die Seite noch laedt. */
|
||
if ('serviceWorker' in navigator) {
|
||
const anmelden = () => {
|
||
navigator.serviceWorker.register('/workspace/sw.js', { scope: '/workspace/' })
|
||
.catch(() => { /* geht hier nicht -- die Seite laeuft trotzdem */ });
|
||
};
|
||
if (document.readyState === 'complete') anmelden();
|
||
else window.addEventListener('load', anmelden, { once: true });
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE SEITE ALS APP INSTALLIEREN (20.09.2026)
|
||
|
||
Filipe: „kannst du da dazwischen in der starseite oben, ein button
|
||
reinsetzen damit die leute die seite installieren koennen auf dem
|
||
handy oder pc. das soll auch verschwinden wenn die leute es
|
||
installiert haben."
|
||
|
||
WARUM ES DEN KNOPF BRAUCHT: Das Manifest ist seit Langem
|
||
vollstaendig -- die App LIESS sich installieren. Nur bot das
|
||
ausschliesslich der Browser selbst an, versteckt in seinem
|
||
Dreipunktemenue. Wer es nicht sucht, findet es nie. Und ohne
|
||
installierte App gibt es auf dem Handy keine verlaesslichen
|
||
Benachrichtigungen; genau daran hing im September das Telefonieren.
|
||
|
||
DREI ZUSTAENDE, UND JEDER BRAUCHT EINE ANDERE ANTWORT:
|
||
|
||
1. SCHON INSTALLIERT -> gar kein Knopf. Ein Angebot fuer etwas,
|
||
das man hat, ist Rauschen. Erkannt an `display-mode:
|
||
standalone` (und an Safaris eigenem `navigator.standalone`).
|
||
|
||
2. DER BROWSER BIETET ES AN (Chrome, Edge, Android) -> der Knopf
|
||
fragt ihn. `beforeinstallprompt` kommt EINMAL und muss
|
||
aufgehoben werden; wer ihn nicht festhaelt, kann spaeter
|
||
nicht mehr fragen.
|
||
|
||
3. SAFARI AUF DEM iPHONE -> dort gibt es `beforeinstallprompt`
|
||
NICHT, und es wird ihn nicht geben. Apple laesst nur den Weg
|
||
ueber „Teilen -> Zum Home-Bildschirm". Ein Knopf, der dort
|
||
nichts tut, waere schlimmer als keiner -- also sagt er in
|
||
diesem Fall, was zu tun ist, statt es zu versuchen.
|
||
|
||
WARUM AUF JEDER SEITE UND NICHT NUR AUF DER STARTSEITE: Wer den
|
||
Knopf einmal uebersieht, sieht ihn beim naechsten Mal wieder -- und
|
||
er verschwindet ohnehin, sobald er ueberfluessig ist.
|
||
|
||
(Hier stand "auf allen 32 Seiten". Es sind inzwischen mehr, und
|
||
seit dem 22.09. auch die Anmeldewand. Eine gezaehlte Zahl in einem
|
||
Kommentar wird still falsch, sobald jemand eine Seite anlegt --
|
||
also steht jetzt keine mehr da.)
|
||
===================================================================== */
|
||
|
||
function knopfBauen(wohin) {
|
||
/* ZWEIMAL AUFGERUFEN HEISST NICHT ZWEI KNOEPFE. Seit der Aufruf
|
||
nicht mehr an der Reihenfolge der Skriptzeilen haengt, kann er
|
||
aus zwei Richtungen kommen -- aus kopf.js und aus dem Platz
|
||
unten in dieser Datei. Zwei Angebote fuer dieselbe Sache sind
|
||
eines zu viel. */
|
||
if (!wohin || wohin.querySelector('.install-knopf')) return;
|
||
|
||
const el = (tag, klasse, text) => {
|
||
const k = document.createElement(tag);
|
||
if (klasse) k.className = klasse;
|
||
if (text !== undefined) k.textContent = text;
|
||
return k;
|
||
};
|
||
|
||
/* ---- Zustand 1: schon installiert -> nichts tun --------------- */
|
||
const schonDrin = () =>
|
||
(window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|
||
|| window.navigator.standalone === true;
|
||
if (schonDrin()) return;
|
||
|
||
/* Apple: kein `beforeinstallprompt`, aber ein Weg. */
|
||
const istApple = /iphone|ipad|ipod/i.test(navigator.userAgent)
|
||
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||
|
||
let angebot = null; // der aufgehobene Browser-Vorschlag
|
||
|
||
const knopf = el('button', 'install-knopf');
|
||
knopf.type = 'button';
|
||
knopf.hidden = true;
|
||
knopf.title = 'Diese Seite als App installieren';
|
||
/* DAS WORT VERSCHWINDET AM HANDY (siehe CSS) -- der Satz fuer
|
||
Vorleseprogramme nicht. Ein Knopf, der dort nur aus einem
|
||
Pfeil besteht, heisst sonst "Schaltflaeche". */
|
||
knopf.setAttribute('aria-label', 'Diese Seite als App installieren');
|
||
const zeichen = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
zeichen.setAttribute('viewBox', '0 0 24 24');
|
||
zeichen.setAttribute('class', 'install-knopf__zeichen');
|
||
zeichen.setAttribute('aria-hidden', 'true');
|
||
zeichen.innerHTML = '<path d="M12 3.5v11M8 11l4 4 4-4"/><path d="M5 18.5h14"/>';
|
||
knopf.append(zeichen, el('span', 'install-knopf__text', 'Installieren'));
|
||
|
||
knopf.addEventListener('click', async () => {
|
||
if (angebot) {
|
||
/* Zustand 2: den Browser fragen. Die Antwort interessiert uns
|
||
nur insofern, als ein angenommener Vorschlag den Knopf
|
||
ueberfluessig macht -- ein abgelehnter nicht: Vielleicht
|
||
ueberlegt man es sich morgen anders. */
|
||
knopf.disabled = true;
|
||
try {
|
||
angebot.prompt();
|
||
const { outcome } = await angebot.userChoice;
|
||
if (outcome === 'accepted') knopf.remove();
|
||
} catch { /* dann eben nicht */ }
|
||
angebot = null;
|
||
knopf.disabled = false;
|
||
return;
|
||
}
|
||
/* Zustand 3: erklaeren statt versuchen. */
|
||
erklaeren();
|
||
});
|
||
|
||
/* WARUM DAS NICHT EINFACH `frageNach` AUFRUFT (22.09.2026).
|
||
|
||
Der erste Entwurf tat genau das -- und haette auf der
|
||
Anmeldewand gar nichts getan: Sie laedt `nachfrage.js` nicht,
|
||
also ist `window.frageNach` dort `undefined`, und `?.()`
|
||
verschluckt das lautlos. Auf dem iPhone steht der Knopf aber von
|
||
Anfang an da. Ein Knopf, der beim Antippen nichts tut, ist
|
||
schlimmer als keiner.
|
||
|
||
Die Datei nachzuladen waere die falsche Antwort: 22 KB und die
|
||
halbe Stilvorlage `module.css` fuer eine Seite, die eine einzige
|
||
Aufgabe hat. Und ein Kasten, der sich ueber das Codefeld legt,
|
||
waere dort ohnehin im Weg.
|
||
|
||
Also: Wo es den Baustein gibt, wird er benutzt -- die Seite soll
|
||
eine Sprache sprechen. Wo nicht, steht die Erklaerung als
|
||
schlichter Absatz unter dem Knopf. Beides sagt dasselbe. */
|
||
let hinweis = null;
|
||
function erklaeren() {
|
||
const titel = 'Als App auf dem Home-Bildschirm';
|
||
const weg = istApple
|
||
? 'Tippe unten in Safari auf das Teilen-Zeichen (Viereck mit Pfeil nach oben), '
|
||
+ 'dann auf „Zum Home-Bildschirm“.'
|
||
: 'Öffne das Menü deines Browsers (die drei Punkte) und wähle dort '
|
||
+ '„App installieren“ oder „Zum Startbildschirm hinzufügen“.';
|
||
const dazu = 'Danach liegt die Seite wie eine App auf deinem Gerät – und '
|
||
+ 'Anrufe und Nachrichten kommen zuverlässiger an.';
|
||
|
||
if (typeof window.frageNach === 'function') {
|
||
window.frageNach({ titel, was: weg, bleibt: dazu, ja: 'Alles klar', nein: null });
|
||
return;
|
||
}
|
||
|
||
/* Ohne Dialogbaustein: ein Absatz, der beim zweiten Antippen
|
||
wieder verschwindet. Kein zweiter, wenn schon einer dasteht. */
|
||
if (hinweis) { hinweis.remove(); hinweis = null; knopf.setAttribute('aria-expanded', 'false'); return; }
|
||
hinweis = el('p', 'install-hinweis');
|
||
hinweis.id = 'install-hinweis';
|
||
hinweis.append(el('strong', 'install-hinweis__titel', titel));
|
||
hinweis.append(el('span', 'install-hinweis__weg', weg));
|
||
hinweis.append(el('span', 'install-hinweis__dazu', dazu));
|
||
knopf.setAttribute('aria-expanded', 'true');
|
||
knopf.setAttribute('aria-controls', 'install-hinweis');
|
||
knopf.insertAdjacentElement('afterend', hinweis);
|
||
}
|
||
|
||
wohin.prepend(knopf);
|
||
|
||
/* Der Browser meldet sich, wenn er es anbieten kann. */
|
||
window.addEventListener('beforeinstallprompt', (e) => {
|
||
/* Ohne `preventDefault` zeigt der Browser seine eigene Leiste --
|
||
zwei Angebote fuer dieselbe Sache sind eines zu viel. */
|
||
e.preventDefault();
|
||
angebot = e;
|
||
knopf.hidden = false;
|
||
});
|
||
|
||
/* WANN DER KNOPF ZU SEHEN IST -- und warum nicht erst nach dem
|
||
Angebot des Browsers (geaendert 22.09.2026).
|
||
|
||
Bis heute wurde er ausschliesslich durch `beforeinstallprompt`
|
||
sichtbar. Das hat zwei Loecher:
|
||
|
||
* Apple schickt das Ereignis nie. (Das war bekannt und ist
|
||
unten schon abgefangen.)
|
||
* Chrome schickt es zwar, aber nicht verlaesslich sofort -- und
|
||
gar nicht, wenn es kurz vorher weggeklickt wurde. Dann steht
|
||
der Knopf da, wo er gebraucht wird, und ist unsichtbar.
|
||
|
||
Filipe hat dreimal danach gefragt. Ein Angebot, das man nur unter
|
||
guenstigen Umstaenden sieht, ist fuer ihn keins.
|
||
|
||
GEPRUEFT WIRD DIE FAEHIGKEIT, NICHT DER NAME DES BROWSERS:
|
||
`'onbeforeinstallprompt' in window` ist bei allen Chromium-
|
||
Browsern wahr -- Chrome, Edge, Brave, Samsung Internet, Android.
|
||
Eine Liste von Browsernamen waere die, die beim naechsten
|
||
veraltet.
|
||
|
||
Wo BEIDES nicht zutrifft -- Firefox am Rechner etwa, der
|
||
Webanwendungen schlicht nicht installiert --, bleibt der Knopf
|
||
weg. Ein Knopf, der eine Anleitung fuer etwas zeigt, das dieser
|
||
Browser nicht kann, ist schlimmer als keiner. */
|
||
const kannVielleicht = 'onbeforeinstallprompt' in window;
|
||
if (istApple || kannVielleicht) knopf.hidden = false;
|
||
|
||
/* Und wenn es geklappt hat, ist er fertig. */
|
||
window.addEventListener('appinstalled', () => knopf.remove());
|
||
/* Manche Browser melden `appinstalled` nicht -- dann faellt der
|
||
Knopf beim naechsten Oeffnen ueber `schonDrin()` weg. Und wer die
|
||
App waehrend der Sitzung installiert und zurueckwechselt, merkt
|
||
es hier: */
|
||
try {
|
||
window.matchMedia('(display-mode: standalone)')
|
||
.addEventListener('change', (m) => { if (m.matches) knopf.remove(); });
|
||
} catch { /* aeltere Browser koennen das nicht -- kein Grund zu scheitern */ }
|
||
}
|
||
|
||
/* ---------------------------------------------------------------------
|
||
WOHIN DER KNOPF GEHOERT
|
||
---------------------------------------------------------------------
|
||
Zwei Faelle, und sie schliessen sich aus:
|
||
|
||
* Es gibt eine Kopfleiste -> kopf.js sagt, wohin (es kennt die
|
||
Reihenfolge der Knoepfe darin).
|
||
* Es gibt keine -> dann steht auf der Seite ein Platz mit
|
||
`data-install-platz`, und wir setzen uns selbst hinein.
|
||
|
||
Der zweite Fall wird NICHT geraten: Ohne diesen Platz passiert
|
||
nichts. Ein Knopf, der sich irgendwohin haengt, weil er sonst
|
||
nirgends hinpasst, landet frueher oder spaeter mitten im Text.
|
||
--------------------------------------------------------------------- */
|
||
window.installKnopfBauen = knopfBauen;
|
||
|
||
const platz = document.querySelector('[data-install-platz]');
|
||
if (platz) knopfBauen(platz);
|
||
})();
|