Aus dem Zettel wird die Notiz: Der Text steht immer formatiert da, roh ist nur
die Zeile mit der Schreibmarke. Dazu eine kleine Werkzeugleiste (Aufgabe,
Aufzaehlung, Nummer, Ueberschrift, fett, Linie), Enter fuehrt Listen fort,
Backspace am Zeilenanfang nimmt erst den Vorsatz. Fuer groessere Umbauten
bleibt der Quelltext-Knopf.
Kein contenteditable: Der Quelltext bleibt die Wahrheit und wird nie aus dem
HTML zurueckgerechnet, das der Browser beim Tippen erzeugt.
Reihenfolge und An/Aus der Widgets liegen jetzt je Nutzer auf dem Server, in
einer Einstellung ("benutzer,notiz,-system,wetter,bild"). Der Schalter fuer das
Wetter auf dem Schreibtisch faellt damit weg — es ist ein Widget wie jedes
andere.
Die Einstellungen sind in Abschnitte sortiert (Darstellung, HoneyBar, Widgets,
Wetter, Programme, Konto), Beschriftung links, Schalter rechts.
543 lines
17 KiB
JavaScript
543 lines
17 KiB
JavaScript
import { api, escapeHtml } from "../core.js";
|
|
import { widgetAnmelden, widgetsZeichnen } from "./rahmen.js";
|
|
|
|
/* ── Die Notiz ─────────────────────────────────────────────────────────────
|
|
*
|
|
* Ein Text, mehr nicht. **InkNote gibt es schon** — die Notiz darf keine zweite
|
|
* Notiz-App werden: kein Titel, keine Liste von Notizen, keine Ordner, keine
|
|
* Suche. Genau ein Text, den man im Vorbeigehen benutzt: Einkaufszettel, eine
|
|
* Nummer, ein Gedanke. Alles, was länger als einen Tag lebt, gehört nach
|
|
* InkNote.
|
|
*
|
|
* SOFORT-ANSICHT — warum zeilenweise?
|
|
*
|
|
* Vorher gab es zwei Ansichten: gelesen gesetzt, geschrieben roh. Wer schrieb,
|
|
* sah `- [ ] Milch` und musste sich merken, wie das gleich aussehen wird. Das
|
|
* ist die Verwirrung, die hier weg soll.
|
|
*
|
|
* Der Text ist deshalb **immer** gesetzt zu sehen; roh ist nur die eine Zeile,
|
|
* in der die Schreibmarke steht. Man tippt also in einer Aufzählung, sieht aber
|
|
* ringsum schon Überschriften, Häkchen und Punkte.
|
|
*
|
|
* Der Weg dorthin wäre auch anders gegangen — ein `contenteditable`, das
|
|
* fertiges HTML entgegennimmt und daraus wieder Markdown macht. Das ist die
|
|
* Bauform, die in jedem Editor Ärger macht: Der Browser erzeugt beim Tippen
|
|
* eigenes HTML (`<div>`, `<span style>`, beim Einfügen ganze Fremdkörper), und
|
|
* aus dem muss man den Quelltext *zurückraten*. Ein Fehler dabei kostet Text.
|
|
*
|
|
* Hier bleibt der Quelltext die Wahrheit und wird nie zurückgerechnet: Die
|
|
* Anzeige entsteht aus ihm, und geändert wird immer nur genau eine Zeile.
|
|
* Deshalb kann hier nichts verlorengehen, was man nicht selbst gelöscht hat.
|
|
*
|
|
* Für größere Umbauten (alles markieren, Absätze verschieben) gibt es den
|
|
* Knopf „quelltext": dann doch das ganze Feld, roh.
|
|
*/
|
|
|
|
const LEER = "Noch nichts notiert.";
|
|
const VERZUG = 800; // ms bis zum Speichern beim Tippen
|
|
|
|
let text = "";
|
|
let zeile = -1; // welche Zeile gerade roh bearbeitet wird, -1 = keine
|
|
let marke = 0; // wohin die Schreibmarke nach dem Neuzeichnen gehört
|
|
let roh = false; // ganzes Feld roh (der Umweg für größere Umbauten)
|
|
let stand = ""; // "speichert …" | "gespeichert" | Fehlertext
|
|
let uhr = null;
|
|
|
|
const zeilen = () => text.split("\n");
|
|
const setzen = (arr) => {
|
|
text = arr.join("\n");
|
|
};
|
|
|
|
widgetAnmelden({
|
|
id: "notiz",
|
|
titel: "notiz",
|
|
// Die Notiz hält ein Textfeld: Ihr Inhalt darf nicht bei jedem Neuzeichnen
|
|
// ersetzt werden, sonst verliert man beim Tippen Text und Schreibmarke.
|
|
stabil: true,
|
|
|
|
async laden() {
|
|
const d = await api("/api/note");
|
|
text = (d && d.text) || "";
|
|
},
|
|
|
|
inhalt() {
|
|
if (roh) {
|
|
return `
|
|
${werkzeuge()}
|
|
<textarea class="notiz-feld" spellcheck="false"
|
|
placeholder="# Einkauf - [ ] Milch - [x] Brot">${escapeHtml(text)}</textarea>
|
|
${fuss("fertig")}`;
|
|
}
|
|
const koerper = text.trim() || zeile >= 0
|
|
? `<div class="notiz-text">${markdown(text, zeile)}</div>`
|
|
: `<div class="notiz-text notiz-leer"><p class="w-zustand">${LEER}</p></div>`;
|
|
return `${werkzeuge()}${koerper}${fuss("quelltext")}`;
|
|
},
|
|
|
|
montiert(el) {
|
|
el.querySelectorAll("[data-tat]").forEach((b) => {
|
|
// Nicht auf „click": Ein Mausklick nimmt dem Textfeld vorher den Fokus,
|
|
// und mit ihm wüsste man nicht mehr, für welche Zeile der Knopf gilt.
|
|
b.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
tat(b.dataset.tat);
|
|
});
|
|
});
|
|
|
|
if (roh) {
|
|
const feld = el.querySelector(".notiz-feld");
|
|
feld.focus();
|
|
feld.setSelectionRange(feld.value.length, feld.value.length);
|
|
feld.addEventListener("input", () => {
|
|
text = feld.value;
|
|
spaeterSpeichern();
|
|
});
|
|
return;
|
|
}
|
|
|
|
const eingabe = el.querySelector(".notiz-zeile");
|
|
if (eingabe) {
|
|
eingabe.focus();
|
|
eingabe.setSelectionRange(marke, marke);
|
|
wachsen(eingabe);
|
|
eingabe.addEventListener("input", () => {
|
|
const z = zeilen();
|
|
z[zeile] = eingabe.value;
|
|
setzen(z);
|
|
wachsen(eingabe);
|
|
spaeterSpeichern();
|
|
});
|
|
eingabe.addEventListener("keydown", (e) => taste(e, eingabe));
|
|
}
|
|
|
|
// Anklicken heißt: hier weiterschreiben.
|
|
el.querySelectorAll("[data-z]").forEach((b) =>
|
|
b.addEventListener("mousedown", (e) => {
|
|
if (e.target.closest(".notiz-zeile, .notiz-hakbox")) return;
|
|
e.preventDefault();
|
|
const i = Number(b.dataset.z);
|
|
springen(i, zeilen()[i].length);
|
|
})
|
|
);
|
|
el.querySelector(".notiz-leer")?.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
springen(0, 0);
|
|
});
|
|
|
|
// Häkchen lassen sich setzen, ohne in den Text zu springen — das ist der
|
|
// häufigste Griff an einer Liste.
|
|
el.querySelectorAll(".notiz-hakbox").forEach((box) =>
|
|
box.addEventListener("change", () => {
|
|
text = hakenUmlegen(text, Number(box.dataset.hak));
|
|
neu();
|
|
jetztSpeichern();
|
|
})
|
|
);
|
|
},
|
|
});
|
|
|
|
/* ── Die kleine Werkzeugleiste ─────────────────────────────────────────────
|
|
* Sechs Knöpfe, keine Auswahlfelder. Alles, was das kleine Markdown kann, und
|
|
* nichts darüber hinaus — eine Leiste mit Knöpfen für Dinge, die der Zettel
|
|
* gar nicht kennt, wäre schlimmer als keine.
|
|
*/
|
|
const KNOEPFE = [
|
|
["haken", "☐", "Aufgabe"],
|
|
["punkt", "•", "Aufzählung"],
|
|
["nummer", "1.", "Nummerierte Liste"],
|
|
["ueber", "H", "Überschrift"],
|
|
["fett", "B", "Fett"],
|
|
["linie", "—", "Trennlinie"],
|
|
];
|
|
|
|
function werkzeuge() {
|
|
return (
|
|
`<div class="notiz-werkzeuge">` +
|
|
KNOEPFE.map(
|
|
([tat, zeichen, titel]) =>
|
|
`<button type="button" class="notiz-wz nw-${tat}" data-tat="${tat}" title="${titel}" ${
|
|
roh ? "disabled" : ""
|
|
}>${zeichen}</button>`
|
|
).join("") +
|
|
`</div>`
|
|
);
|
|
}
|
|
|
|
function fuss(knopf) {
|
|
return `
|
|
<div class="notiz-fuss">
|
|
<span class="notiz-stand">${escapeHtml(stand)}</span>
|
|
<button type="button" class="btn notiz-modus" data-tat="modus">${knopf}</button>
|
|
</div>`;
|
|
}
|
|
|
|
/** Was die Knöpfe tun. Alle arbeiten auf der Zeile, in der man gerade steht. */
|
|
function tat(name) {
|
|
if (name === "modus") {
|
|
roh = !roh;
|
|
zeile = -1;
|
|
jetztSpeichern();
|
|
neu();
|
|
return;
|
|
}
|
|
|
|
// Ohne Schreibmarke gibt es keine „aktuelle Zeile" — dann eine neue am Ende.
|
|
if (zeile < 0) {
|
|
const z = zeilen();
|
|
if (z[z.length - 1].trim()) z.push("");
|
|
setzen(z);
|
|
zeile = z.length - 1;
|
|
marke = 0;
|
|
}
|
|
|
|
const z = zeilen();
|
|
if (name === "linie") {
|
|
z.splice(zeile + (z[zeile].trim() ? 1 : 0), 0, "---", "");
|
|
setzen(z);
|
|
zeile += z[zeile].trim() ? 2 : 1;
|
|
marke = 0;
|
|
neu();
|
|
jetztSpeichern();
|
|
return;
|
|
}
|
|
|
|
if (name === "fett") {
|
|
const feld = document.querySelector(".notiz-zeile");
|
|
const a = feld ? feld.selectionStart : z[zeile].length;
|
|
const b = feld ? feld.selectionEnd : a;
|
|
const wort = z[zeile].slice(a, b);
|
|
z[zeile] = z[zeile].slice(0, a) + "**" + wort + "**" + z[zeile].slice(b);
|
|
marke = wort ? b + 4 : a + 2; // ohne Auswahl: zwischen die Sterne
|
|
setzen(z);
|
|
neu();
|
|
jetztSpeichern();
|
|
return;
|
|
}
|
|
|
|
const VORSATZ = { haken: "- [ ] ", punkt: "- ", nummer: "1. ", ueber: "# " };
|
|
const alt = vorsatz(z[zeile]);
|
|
const rest = z[zeile].slice(alt.length);
|
|
const neuerVorsatz = alt === VORSATZ[name] ? "" : VORSATZ[name];
|
|
z[zeile] = neuerVorsatz + rest;
|
|
marke = Math.max(0, marke - alt.length) + neuerVorsatz.length;
|
|
setzen(z);
|
|
neu();
|
|
jetztSpeichern();
|
|
}
|
|
|
|
/** Der Listen- oder Überschriften-Vorsatz einer Zeile, samt Leerzeichen. */
|
|
function vorsatz(z) {
|
|
const m = /^(\s*(?:[-*]\s+\[[ xX]\]\s+|[-*]\s+|\d+[.)]\s+|#{1,3}\s+))/.exec(z);
|
|
return m ? m[1] : "";
|
|
}
|
|
|
|
/* ── Tastatur in der einen rohen Zeile ─────────────────────────────────── */
|
|
|
|
function taste(e, feld) {
|
|
const i = zeile;
|
|
const z = zeilen();
|
|
const anfang = feld.selectionStart === 0 && feld.selectionEnd === 0;
|
|
const ende = feld.selectionStart === feld.value.length && feld.selectionEnd === feld.value.length;
|
|
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
const links = feld.value.slice(0, feld.selectionStart);
|
|
const rechts = feld.value.slice(feld.selectionEnd);
|
|
const v = vorsatz(links);
|
|
|
|
// Enter auf einer leeren Aufzählungszeile beendet die Liste, statt einen
|
|
// weiteren leeren Punkt anzuhängen.
|
|
if (v && !links.slice(v.length).trim() && !rechts) {
|
|
z[i] = "";
|
|
setzen(z);
|
|
springen(i, 0);
|
|
return;
|
|
}
|
|
z.splice(i, 1, links, weiterfuehren(v) + rechts);
|
|
setzen(z);
|
|
springen(i + 1, weiterfuehren(v).length);
|
|
return;
|
|
}
|
|
|
|
if (e.key === "Backspace" && anfang) {
|
|
const v = vorsatz(feld.value);
|
|
if (v) {
|
|
// Erst den Vorsatz weg, dann erst die Zeile: „Backspace am Zeilenanfang"
|
|
// heißt meistens „das ist doch kein Aufzählungspunkt".
|
|
e.preventDefault();
|
|
z[i] = feld.value.slice(v.length);
|
|
setzen(z);
|
|
springen(i, 0);
|
|
return;
|
|
}
|
|
if (i > 0) {
|
|
e.preventDefault();
|
|
const stelle = z[i - 1].length;
|
|
z.splice(i - 1, 2, z[i - 1] + z[i]);
|
|
setzen(z);
|
|
springen(i - 1, stelle);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (e.key === "Delete" && ende && i < z.length - 1) {
|
|
e.preventDefault();
|
|
const stelle = z[i].length;
|
|
z.splice(i, 2, z[i] + z[i + 1]);
|
|
setzen(z);
|
|
springen(i, stelle);
|
|
return;
|
|
}
|
|
|
|
if (e.key === "ArrowUp" && anfang && i > 0) {
|
|
e.preventDefault();
|
|
springen(i - 1, z[i - 1].length);
|
|
return;
|
|
}
|
|
if (e.key === "ArrowDown" && ende && i < z.length - 1) {
|
|
e.preventDefault();
|
|
springen(i + 1, 0);
|
|
return;
|
|
}
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
beenden();
|
|
}
|
|
}
|
|
|
|
/** Was nach Enter am Anfang der neuen Zeile steht. */
|
|
function weiterfuehren(v) {
|
|
if (!v || v.trim().startsWith("#")) return "";
|
|
const haken = /^(\s*[-*]\s+)\[[ xX]\]\s+$/.exec(v);
|
|
if (haken) return haken[1] + "[ ] "; // eine neue Aufgabe ist offen
|
|
const nummer = /^(\s*)(\d+)([.)]\s+)$/.exec(v);
|
|
if (nummer) return nummer[1] + (Number(nummer[2]) + 1) + nummer[3];
|
|
return v;
|
|
}
|
|
|
|
function springen(i, stelle) {
|
|
zeile = i;
|
|
marke = stelle;
|
|
neu();
|
|
}
|
|
|
|
function beenden() {
|
|
zeile = -1;
|
|
neu();
|
|
jetztSpeichern();
|
|
}
|
|
|
|
// Ein Klick irgendwo anders hin heißt: fertig mit dieser Zeile. Nicht über
|
|
// „blur" — den löst auch jeder Knopf in der Werkzeugleiste aus.
|
|
//
|
|
// In der **Erfassungsphase**, und das ist hier keine Feinheit: Ein Klick in die
|
|
// Notiz zeichnet sie neu, und danach hängt das angeklickte Element nicht mehr
|
|
// im Dokument. `closest()` liefe dann ins Leere und dieser Zuhörer hielte einen
|
|
// Klick auf die Notiz für einen Klick nach draußen — er würde also genau das
|
|
// beenden, was der Klick gerade geöffnet hat.
|
|
document.addEventListener(
|
|
"mousedown",
|
|
(e) => {
|
|
if (zeile < 0 || e.target.closest('[data-widget="notiz"]')) return;
|
|
beenden();
|
|
},
|
|
true
|
|
);
|
|
|
|
/** Neu zeichnen — der Rahmen lässt stabile Widgets sonst in Ruhe. */
|
|
function neu() {
|
|
document.querySelectorAll('[data-widget="notiz"]').forEach((el) => delete el.dataset.zustand);
|
|
widgetsZeichnen();
|
|
}
|
|
|
|
/** Ein Zeilenfeld ist so hoch wie sein Text — es soll aussehen wie die Zeile,
|
|
* die es ersetzt, nicht wie ein Formular. */
|
|
function wachsen(feld) {
|
|
feld.style.height = "auto";
|
|
feld.style.height = feld.scrollHeight + "px";
|
|
}
|
|
|
|
/* ── Speichern ─────────────────────────────────────────────────────────── */
|
|
|
|
function spaeterSpeichern() {
|
|
stand = "";
|
|
clearTimeout(uhr);
|
|
uhr = setTimeout(jetztSpeichern, VERZUG);
|
|
}
|
|
|
|
/**
|
|
* Speichert, wenn sich etwas geändert hat.
|
|
*
|
|
* Zwei offene Geräte überschreiben einander; der letzte Stand gewinnt. Für eine
|
|
* Notiz ist das vertretbar — man sollte es nur wissen und nicht später
|
|
* „Datenverlust" nennen.
|
|
*/
|
|
let gespeichert = null;
|
|
async function jetztSpeichern() {
|
|
clearTimeout(uhr);
|
|
if (text === gespeichert) return;
|
|
const wollte = text;
|
|
standSetzen("speichert …");
|
|
try {
|
|
await api("/api/note", { method: "PUT", body: JSON.stringify({ text: wollte }) });
|
|
gespeichert = wollte;
|
|
standSetzen("gespeichert");
|
|
} catch (e) {
|
|
standSetzen(e.message || "nicht gespeichert");
|
|
}
|
|
}
|
|
|
|
function standSetzen(s) {
|
|
stand = s;
|
|
document.querySelectorAll('[data-widget="notiz"] .notiz-stand').forEach((el) => {
|
|
el.textContent = s;
|
|
});
|
|
}
|
|
|
|
/* ── Das kleine Markdown ───────────────────────────────────────────────────
|
|
*
|
|
* Absichtlich von Hand und absichtlich klein: Überschriften, Häkchen, Striche,
|
|
* Aufzählungen, fett, kursiv, Code. Keine Bibliothek — HoneyOS hat keinen
|
|
* Bauschritt, und für sechs Regeln lohnt kein Nachladen.
|
|
*
|
|
* **Zuerst wird escapt, dann gesetzt.** Der Text ist zwar der eigene, aber eine
|
|
* Notiz, die `<script>` ausführt, wäre eine Falle für den Tag, an dem man etwas
|
|
* hineinkopiert.
|
|
*
|
|
* `bearbeitet` ist die Zeile, die roh gezeigt wird. Sie behält ihren Platz in
|
|
* der Liste, in der sie steht — sonst spränge der Punkt beim Hineinklicken aus
|
|
* der Aufzählung heraus.
|
|
*/
|
|
export function markdown(quelle, bearbeitet = -1) {
|
|
const zln = quelle.split("\n");
|
|
let html = "";
|
|
let liste = null; // "ul" | "ol" | null
|
|
|
|
// Die Häkchen werden über den **Quelltext** gezählt, nicht über das, was man
|
|
// sieht: Die gerade bearbeitete Zeile zeigt keines, ihre Nummer muss aber
|
|
// vergeben bleiben, sonst schaltet ein Klick darunter das falsche um.
|
|
const hakNr = {};
|
|
let n = -1;
|
|
zln.forEach((z, i) => {
|
|
if (HAKEN.test(z)) hakNr[i] = ++n;
|
|
});
|
|
|
|
const listeZu = () => {
|
|
if (liste) html += `</${liste}>`;
|
|
liste = null;
|
|
};
|
|
const listeAuf = (art) => {
|
|
if (liste !== art) {
|
|
listeZu();
|
|
html += `<${art}>`;
|
|
liste = art;
|
|
}
|
|
};
|
|
|
|
zln.forEach((rohzeile, i) => {
|
|
const z = rohzeile.trimEnd();
|
|
|
|
if (i === bearbeitet) {
|
|
const feld =
|
|
`<textarea class="notiz-zeile" rows="1" spellcheck="false" ` +
|
|
`placeholder="schreiben …">${escapeHtml(rohzeile)}</textarea>`;
|
|
const art = HAKEN.test(z) || PUNKT.test(z) ? "ul" : ZAHL.test(z) ? "ol" : null;
|
|
if (art) {
|
|
listeAuf(art);
|
|
html += `<li class="notiz-rohli">${feld}</li>`;
|
|
} else {
|
|
listeZu();
|
|
html += feld;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!z.trim()) {
|
|
listeZu();
|
|
return;
|
|
}
|
|
|
|
const ueber = /^(#{1,3})\s+(.*)$/.exec(z);
|
|
if (ueber) {
|
|
listeZu();
|
|
const g = ueber[1].length + 2; // # → h3, ## → h4, ### → h5
|
|
html += `<h${g} data-z="${i}">${inline(ueber[2])}</h${g}>`;
|
|
return;
|
|
}
|
|
|
|
if (/^(-{3,}|\*{3,})$/.test(z.trim())) {
|
|
listeZu();
|
|
html += `<hr data-z="${i}" />`;
|
|
return;
|
|
}
|
|
|
|
const hak = HAKEN.exec(z);
|
|
if (hak) {
|
|
listeAuf("ul");
|
|
const an = hak[1].toLowerCase() === "x";
|
|
html +=
|
|
`<li class="notiz-hak${an ? " ab" : ""}" data-z="${i}">` +
|
|
`<label><input class="notiz-hakbox" type="checkbox" data-hak="${hakNr[i]}"${an ? " checked" : ""} />` +
|
|
`<span>${inline(hak[2])}</span></label></li>`;
|
|
return;
|
|
}
|
|
|
|
const punkt = PUNKT.exec(z);
|
|
if (punkt) {
|
|
listeAuf("ul");
|
|
html += `<li data-z="${i}">${inline(punkt[1])}</li>`;
|
|
return;
|
|
}
|
|
|
|
const zahl = ZAHL.exec(z);
|
|
if (zahl) {
|
|
listeAuf("ol");
|
|
html += `<li data-z="${i}">${inline(zahl[1])}</li>`;
|
|
return;
|
|
}
|
|
|
|
listeZu();
|
|
html += `<p data-z="${i}">${inline(z)}</p>`;
|
|
});
|
|
|
|
listeZu();
|
|
return html;
|
|
}
|
|
|
|
const HAKEN = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/;
|
|
const PUNKT = /^\s*[-*]\s+(.*)$/;
|
|
const ZAHL = /^\s*\d+[.)]\s+(.*)$/;
|
|
|
|
function inline(s) {
|
|
return escapeHtml(s)
|
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>")
|
|
.replace(/(^|\W)\*([^*\n]+)\*(?=\W|$)/g, "$1<i>$2</i>");
|
|
}
|
|
|
|
/** Legt das n-te Häkchen im Quelltext um — gezählt wie beim Setzen. */
|
|
export function hakenUmlegen(quelle, n) {
|
|
let i = -1;
|
|
return quelle
|
|
.split("\n")
|
|
.map((z) => {
|
|
const hak = /^(\s*[-*]\s+\[)([ xX])(\]\s+.*)$/.exec(z);
|
|
if (!hak) return z;
|
|
i++;
|
|
if (i !== n) return z;
|
|
return hak[1] + (hak[2].toLowerCase() === "x" ? " " : "x") + hak[3];
|
|
})
|
|
.join("\n");
|
|
}
|
|
|
|
// Beim Verlassen der Seite noch schnell sichern — der Verzug beim Tippen darf
|
|
// keinen Satz kosten, nur weil der Tab zugeht.
|
|
//
|
|
// `keepalive` statt sendBeacon: Der Beacon kann nur POST und trägt keine
|
|
// eigenen Kopfzeilen — unser Weg ist ein PUT mit Prüfwort. Ein Beacon wäre
|
|
// hier also nicht schneller, sondern schlicht abgewiesen worden.
|
|
window.addEventListener("pagehide", () => {
|
|
if (text === gespeichert) return;
|
|
api("/api/note", { method: "PUT", body: JSON.stringify({ text }), keepalive: true }).catch(() => {});
|
|
});
|