Filipe: „fuer die modis, rechte und linke hand und dogfather eine
kachel hinzufuegst, sie soll: Notizen, heissen. ich will dass du das
auch wie ein notizblock erstellst. ich will dass es uebelst geil und
einzigartig ist."
=================================================================
TEIL 1: DIE FARBE -- UND WAS DABEI AUFFIEL
=================================================================
Fuer die neue Kachel braucht es einen Ton. Beim Suchen fiel auf, dass
tools/kachelton-entzerren.mjs zwoelf der 45 Farben fuer FREI hielt.
Sie sind es nicht. bereicheFuer() gibt fuer die fuenf Rollen des
Agenturhauses null zurueck -- das heisst „nimm die Liste aus dem
Browser", und die steht in workspace/assets/js/bereiche.js. Dort
stehen die Toene 1 bis 22 und 44: Dashboard, Zahlen, Team-Lage,
Scout-Pipeline. Kacheln, die jeden Tag jemand ansieht.
pruef-kachelfarben hatte denselben blinden Fleck. Sie meldete seit
Wochen „33 benutzte Toene" und war gruen. Was sie dadurch NICHT sah:
* Der Farblauf von gestern Nacht hat zwoelf dieser Kacheln
verschoben und drei davon unter die Buntheitsgrenze gedrueckt.
Gruen geblieben.
* Ton 7, 16 und 22 lagen SEIT JEHER unter der Grenze (0,112 / 0,116
/ 0,068). Nie gemeldet.
Das ist die Sorte gruener Haken, vor der die Hausregel warnt: Er sagt
nur, dass die Bedingung erfuellt war -- nicht, dass sie das Richtige
angesehen hat.
BEHOBEN, und zwar an der Wurzel: Die REGELN (Kontrast, Buntheit)
gelten jetzt fuer jeden Ton, der irgendwo an einer Kachel steht --
gelesen aus denselben fuenf Dateien, die auch pruef-kachel-universum
liest. Dazu die Namen der Kacheln, damit „Ton 1 traegt Dashboard"
ueberhaupt pruefbar ist.
DIE PALETTE WURDE NEU GERECHNET, mit dem dritten Verfahren an einem
Tag -- die ersten zwei stehen als Fehler im Kopf des Werkzeugs:
1. Alle 45 neu verteilt. Lief ueber zwei ausdrueckliche Wuensche
hinweg (#ff1a1a, #a8d8ff).
2. Nur die Kollisionen, aber immer nur EINEN der beiden bewegt.
Ergebnis: Ton 1 sprang vom Tuerkis ins Altrosa, 0,197 weit.
3. BEIDE duerfen sich bewegen, und zwar beide nur ein bisschen.
Zwei Toene, die 0,02 auseinanderstehen und 0,09 brauchen, teilen
sich das -- jeder rueckt 0,045, und beide bleiben, was sie waren.
kleinster Abstand 0,0154 -> 0,0905
zu blass 4 -> 0
sichtbar veraendert 7 Kacheln (ueber 0,05)
kaum zu sehen 28 (13 zwischen 0,02 und 0,05, 15 darunter)
EINE AUSNAHME MIT ZAHL, keine mit Achselzucken: Ton 1 (Dashboard)
bleibt acht Tausendstel unter der Buntheitsgrenze. Gemessen ueber ALLE
16,7 Millionen sRGB-Farben ist die naechste, die alle Regeln haelt,
#7a75c8 -- ein Blauviolett, 0,113 entfernt. Tuerkis erreicht in sRGB
schlicht keine hoehere Buntheit, und das schmale Band teilen sich
schon sieben Toene. Eine Kachel, die ihre Farbfamilie behaelt, ist die
bessere Antwort auf „richtig geil und speziell" als eine, die acht
Tausendstel bunter ist und niemand wiedererkennt. blassBis sagt jetzt
bei jeder Ausnahme, WIE WEIT sie reicht -- vorher hiess blassErlaubt
schlicht „hier wird weggesehen".
DIE NEUE FARBE IST GRUEN UND WOLLTE BERNSTEIN SEIN. Gesucht war die
Farbe von Papier. Gemessen gibt es sie nicht mehr: Der beste Bernstein
im ganzen Farbraum haelt 0,0799 Abstand -- unter der Hausgrenze. Und
Platz schaffen hilft nicht: Setzt man ihn fest, muss ein warmer Ton
das Band verlassen (Ton 45 waere 0,212 weit ins Magenta gewandert).
Die groesste wirklich freie Luecke liegt im Gruen, bei 0,0917. Ein
linierter Block in Gruen ist Papier, seit es Papier gibt.
=================================================================
TEIL 2: DER BLOCK
=================================================================
DREI ENTSCHEIDUNGEN, AUS DENEN DER REST FOLGT:
1. ES GIBT KEINEN SPEICHERN-KNOPF. Nirgends. Wer einen Block
aufschlaegt und lostippt, drueckt hinterher nicht auf „sichern";
er klappt ihn zu. Gesichert wird 800 ms nach dem letzten
Tastendruck. Geht das nicht, bleibt der Text im Browser liegen und
wird nachgereicht -- und der Fuss sagt ehrlich „noch nicht
gesichert", statt einen Verlust zu melden, den man gerade nicht
verhindern kann.
2. DAS BLATT IST EIN SCHREIBFELD MIT EINER DECKSCHICHT. Das Feld
traegt Text und Cursor und ist unsichtbar; darueber zeichnet eine
zweite Schicht denselben Text noch einmal -- mit Kaestchen,
Ueberschriften, Strichen und Links.
DARAUS FOLGT EINE EISERNE REGEL, und sie steht dreimal im Code:
KEINE Auszeichnung darf die BREITE eines Zeichens aendern. Kein
Fettdruck, keine andere Groesse, keine Sperrung. Erlaubt sind nur
Farbe, Flaeche, Rahmen und Durchstreichen. Ein einziges
font-weight: 700 verschoebe den Umbruch, und ab der zweiten Zeile
stuende der sichtbare Text neben dem Cursor.
pruef-notizen misst das am echten Umbruch: eine Probe mit allen
Auszeichnungen und einer Zeile, die umbrechen MUSS. Sind beide
Schichten verschieden hoch, sitzt der Umbruch woanders. Mit
Gegenprobe -- Fettdruck auf der Deckschicht bricht die Deckung um
genau eine Zeile (30 px), und die Zeile wird rot.
3. DIE TASTATUR FUEHRT DIE LISTE FORT, NICHT EINE LEISTE. Ein
Spiegelstrich und Enter macht den naechsten Punkt, ein leeres
Kaestchen und Enter das naechste Kaestchen -- und ein GESETZTER
Haken wird dabei nicht mitgenommen, die naechste Aufgabe ist ja
noch nicht erledigt. Zweimal Enter beendet die Liste. Ein Klick
aufs Kaestchen hakt ab. Es gibt keine Werkzeugleiste, weil man beim
Schreiben nie den Stift wechselt.
UND: NIEMAND SIEHT DIE NOTIZEN EINES ANDEREN. Auch DogFather nicht.
Es gibt in workspace-notizen.js keinen siehtAlles()-Zweig, keinen
Umschalter, keine fremde Liste -- jede Abfrage hat person_id = ? fest
eingebaut. Die Kachel steht deshalb in „Fuer dich" und nicht in
„Taeglich": Die Gruppe sagt die wichtigste Eigenschaft, bevor man sie
anfasst.
NUR AUF DER TEAM-ADRESSE. Auf der Agenturadresse ist die Seite ein
404 -- auch fuer DogFather. Das ist dieselbe Trennung wie bei Chat,
Kalender und Aufgaben, und sie steht an zwei Stellen: in
GEHOERT_ZU_ADRESSE (die Datei) und in der Schnittstelle (die Daten).
Die Seite ist nur HTML; die Daten sind die Sache.
Dazu: sechs Papierfarben, Anheften, Suche mit Hervorhebung im Text,
Papierkorb mit dreissig Tagen und einem Eintrag in der
Aufbewahrungsliste (ohne den waere die Frist ein Satz in einem
Kommentar -- genau das ist dem Support am 24.09. passiert).
=================================================================
EIN FUND BEIM BAUEN, DER ALLEN GEHOERT
=================================================================
DELETE /workspace/api/notizen/:id gab es schon -- in
workspace-aufgaben.js, fuer die Notizen AN einer Aufgabe. Weil jener
Router frueher eingehaengt ist, hat er jeden Loeschversuch des Blocks
abgefangen und mit 404 beantwortet.
Von aussen sah das aus wie ein Fehler im neuen Modul: Anlegen ging,
Aendern ging, Loeschen nicht. Man sucht dann im eigenen Code, und dort
ist nichts. Express meldet so etwas nicht -- es nimmt die erste Route,
die passt, und schweigt ueber die zweite.
Der Block liegt jetzt unter /workspace/api/notizblock. Und
pruef-notizen geht seitdem den Routenbaum von express durch und meldet
jedes Paar aus Methode und Pfad, das zweimal vergeben ist. Gemessen:
307 Wege, keine Dublette. Die Pruefung gilt fuers ganze Haus, auch
wenn sie in dieser Datei steht -- hier ist sie gefunden worden.
Nebenbei berichtigt: pruef-crew-adresse verlangte von jedem Eintrag
der Haustafel HTTP 200 mit Inhalt. Das stimmte, solange dort nur
oeffentliche Dateien standen. notizen.html ist die erste Seite hinter
der Anmeldung -- sie antwortet mit 302, und das ist richtig. Gefragt
wird jetzt, was die Tafel wirklich meint: GIBT es das hier.
GEMESSEN, alles nach den Aenderungen:
pruef-notizen 76 Punkte, 0 Fehler (neu)
pruef-kachelfarben 31 Punkte, 0 Fehler (vorher 26)
pruef-kachel-universum 13 Punkte, 0 Fehler
pruef-crew-adresse 157 Punkte, 0 Fehler
pruef-buehne 230 Punkte, 0 Fehler -- notizen.html neu in
der Liste, schlechtester Kontrast 6,73:1,
also 50 % ueber der Grenze
pruef-rechtetafel, -haus-seiten, -struktur, -css-klassen,
-jeder-hat-eine-seite, -workspace-seiten, -kachelraster,
-rueckmeldung alle 0 Fehler
Co-Authored-By: Claude Opus 5 <[email protected]>
905 lines
34 KiB
JavaScript
905 lines
34 KiB
JavaScript
/* =====================================================================
|
||
DER NOTIZBLOCK (26.09.2026)
|
||
=====================================================================
|
||
|
||
Filipe: „ich will dass du das auch wie ein notizblock erstellst.
|
||
ich will dass es übelst geil und einzigartig ist."
|
||
|
||
---------------------------------------------------------------------
|
||
DREI ENTSCHEIDUNGEN, AUS DENEN DER REST FOLGT
|
||
|
||
1. ES GIBT KEINEN SPEICHERN-KNOPF. Wer einen Block aufschlägt und
|
||
lostippt, drückt hinterher nicht auf „sichern"; er klappt ihn zu.
|
||
Gesichert wird 800 ms nach dem letzten Tastendruck. Geht das
|
||
nicht (Funkloch, Server weg), bleibt der Text im Browser liegen
|
||
und wird nachgereicht — und der Fuß sagt ehrlich, dass er noch
|
||
nicht drüben ist.
|
||
|
||
2. DAS BLATT IST EIN SCHREIBFELD, KEIN CONTENTEDITABLE. Die
|
||
Auszeichnung (Haken, Überschriften, Striche) zeichnet eine
|
||
Deckschicht darüber. Warum, steht im Kopf von notizen.css — kurz:
|
||
Ein `contenteditable` bricht beim Einfügen aus fremden
|
||
Programmen, beim Rückgängigmachen und an jeder Handytastatur,
|
||
die über eine Zwischenzeile schreibt.
|
||
|
||
3. DIE TASTATUR FÜHRT DIE LISTE FORT, NICHT EINE LEISTE. Eine
|
||
Werkzeugleiste über einem Notizblock ist eine Leiste zu viel:
|
||
Man schreibt `- ` und drückt Enter, und die nächste Zeile fängt
|
||
wieder mit `- ` an. Genauso bei `[ ] `. Das ist alles, was es an
|
||
„Formatierung" gibt, und es fühlt sich wie Papier an, weil man
|
||
nie den Stift wechselt.
|
||
|
||
---------------------------------------------------------------------
|
||
WAS HIER NICHT PASSIERT
|
||
|
||
Es gibt keine Rolle, die mehr sieht. Kein „Leitung"-Zweig, kein
|
||
Umschalter, keine fremde Liste. Wer das hier liest und eine
|
||
Sichtbarkeitsregel sucht: Es gibt keine, und das ist der Sinn der
|
||
Sache (siehe notiz-tabellen.js).
|
||
===================================================================== */
|
||
(function () {
|
||
'use strict';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (was, klasse, text) => {
|
||
const e = document.createElement(was);
|
||
if (klasse) e.className = klasse;
|
||
if (text !== undefined) e.textContent = text;
|
||
return e;
|
||
};
|
||
|
||
/* ---------- Zustand ---------------------------------------------- */
|
||
let notizen = [];
|
||
let farben = [];
|
||
let aktuell = null; // die offene Notiz (Objekt aus `notizen`)
|
||
let suchwort = '';
|
||
let papierkorbZahl = 0;
|
||
let textMax = 20000;
|
||
let korbTage = 30;
|
||
|
||
const feld = $('feld');
|
||
const deck = $('deck');
|
||
|
||
/* ---------- Fetch-Hülle ------------------------------------------- */
|
||
async function hole(pfad, einstellungen) {
|
||
const a = await fetch(pfad, { credentials: 'same-origin', ...einstellungen });
|
||
if (a.status === 401) { location.href = 'index.html'; throw new Error('weg'); }
|
||
return a;
|
||
}
|
||
async function fehlerText(a, ersatz) {
|
||
try {
|
||
const j = await a.json();
|
||
return typeof j?.fehler === 'string' && j.fehler ? j.fehler : ersatz;
|
||
} catch { return ersatz; }
|
||
}
|
||
function melde(text) {
|
||
const f = $('fehler');
|
||
f.textContent = text || '';
|
||
f.hidden = !text;
|
||
}
|
||
|
||
/* „vor 3 Minuten" statt eines Zeitstempels: Bei einer Notiz ist die
|
||
Frage „wann war ich zuletzt dran?", nicht „welcher Tag war das?".
|
||
Das genaue Datum bleibt als Titel am Element. */
|
||
function herZeit(iso) {
|
||
if (!iso) return '';
|
||
const ms = Date.now() - Date.parse(iso);
|
||
if (!Number.isFinite(ms)) return '';
|
||
const min = Math.floor(ms / 60000);
|
||
if (min < 1) return 'gerade eben';
|
||
if (min < 60) return `vor ${min} Min.`;
|
||
const std = Math.floor(min / 60);
|
||
if (std < 24) return `vor ${std} Std.`;
|
||
const tage = Math.floor(std / 24);
|
||
if (tage === 1) return 'gestern';
|
||
if (tage < 30) return `vor ${tage} Tagen`;
|
||
return new Date(iso).toLocaleDateString('de-DE',
|
||
{ day: '2-digit', month: '2-digit', year: 'numeric' });
|
||
}
|
||
const genau = (iso) => (iso
|
||
? new Date(iso).toLocaleString('de-DE', { dateStyle: 'full', timeStyle: 'short' })
|
||
: '');
|
||
|
||
/* =====================================================================
|
||
DIE DECKSCHICHT
|
||
|
||
Sie zeichnet denselben Text noch einmal — mit Haken, Überschriften
|
||
und Strichen. Die eiserne Regel dabei steht im Kopf von
|
||
notizen.css: KEINE Auszeichnung darf die Breite eines Zeichens
|
||
ändern. Deshalb gibt es hier kein `<b>`, kein `<h1>` und keine
|
||
Zeichen, die hinzukommen oder wegfallen — jedes Zeichen des Textes
|
||
landet genau einmal in der Ausgabe.
|
||
|
||
GEBAUT WIRD MIT createElement UND textContent, nicht mit einer
|
||
Zeichenkette und innerHTML. Ein Notizblock ist der Ort, an dem
|
||
jemand irgendwann `<img onerror=...>` hineinschreibt — nicht aus
|
||
Bosheit, sondern weil er sich etwas notiert. Mit textContent ist
|
||
das ein Text und bleibt einer.
|
||
===================================================================== */
|
||
|
||
/** Wandelt eine Zeile in Knoten um und hängt sie an `ziel`. */
|
||
function zeileZeichnen(ziel, zeile, treffer) {
|
||
/* Die Zeile wird von links nach rechts abgearbeitet; `rest` ist,
|
||
was noch nicht gezeichnet wurde. */
|
||
let rest = zeile;
|
||
let erledigt = false;
|
||
const stueck = (text, klasse, aufbau) => {
|
||
if (!text) return null;
|
||
const s = el('span', klasse, text);
|
||
if (aufbau) aufbau(s);
|
||
ziel.append(s);
|
||
return s;
|
||
};
|
||
|
||
/* 1. Der Zeilenanfang: Überschrift, Aufzählung, Zitat, Haken. */
|
||
let m;
|
||
if ((m = /^(\s*)(#{1,3}\s)/.exec(rest))) {
|
||
stueck(m[1], null);
|
||
stueck(m[2], 'n-marke');
|
||
const inhalt = rest.slice(m[0].length);
|
||
textMitLinks(ziel, inhalt, treffer, 'n-h');
|
||
return;
|
||
}
|
||
if ((m = /^(\s*)(\[[ xX]\])(\s?)/.exec(rest))) {
|
||
stueck(m[1], null);
|
||
const an = m[2][1] !== ' ';
|
||
erledigt = an;
|
||
stueck(m[2], 'n-hk', (s) => {
|
||
s.dataset.an = an ? 'ja' : 'nein';
|
||
s.setAttribute('role', 'checkbox');
|
||
s.setAttribute('aria-checked', an ? 'true' : 'false');
|
||
s.title = an ? 'Erledigt – klicken zum Aufheben' : 'Als erledigt abhaken';
|
||
});
|
||
stueck(m[3], null);
|
||
textMitLinks(ziel, rest.slice(m[0].length), treffer, erledigt ? 'n-erledigt' : null);
|
||
return;
|
||
}
|
||
if ((m = /^(\s*)([-*]\s)/.exec(rest))) {
|
||
stueck(m[1], null);
|
||
stueck(m[2], 'n-punkt');
|
||
textMitLinks(ziel, rest.slice(m[0].length), treffer, null);
|
||
return;
|
||
}
|
||
if ((m = /^(\s*)(>\s?)/.exec(rest))) {
|
||
stueck(m[1], null);
|
||
stueck(m[2], 'n-zitat');
|
||
textMitLinks(ziel, rest.slice(m[0].length), treffer, 'n-zitat');
|
||
return;
|
||
}
|
||
textMitLinks(ziel, rest, treffer, null);
|
||
}
|
||
|
||
/* Adressen werden anklickbar. Das Muster ist absichtlich eng: Es
|
||
trifft nur, was mit http(s):// anfängt. Ein „www." ohne Schema
|
||
wäre eine Vermutung, und eine Notiz ist kein Ort für Vermutungen. */
|
||
const ADRESSE = /https?:\/\/[^\s<>"')\]]+/g;
|
||
|
||
function textMitLinks(ziel, text, treffer, klasse) {
|
||
const stuecke = [];
|
||
let zuletzt = 0;
|
||
ADRESSE.lastIndex = 0;
|
||
let m;
|
||
while ((m = ADRESSE.exec(text))) {
|
||
if (m.index > zuletzt) stuecke.push({ t: text.slice(zuletzt, m.index), link: false });
|
||
stuecke.push({ t: m[0], link: true });
|
||
zuletzt = m.index + m[0].length;
|
||
}
|
||
if (zuletzt < text.length) stuecke.push({ t: text.slice(zuletzt), link: false });
|
||
if (!stuecke.length) stuecke.push({ t: '', link: false });
|
||
|
||
for (const s of stuecke) {
|
||
const klassen = [klasse, s.link ? 'n-link' : null].filter(Boolean).join(' ');
|
||
if (s.link) {
|
||
const a = el('a', klassen, s.t);
|
||
a.href = s.t;
|
||
a.target = '_blank';
|
||
/* `noopener` ist Pflicht: Ohne ihn bekommt die geöffnete Seite
|
||
über `window.opener` Zugriff auf diese hier. */
|
||
a.rel = 'noopener noreferrer';
|
||
ziel.append(a);
|
||
} else if (treffer) {
|
||
trefferZeichnen(ziel, s.t, treffer, klassen);
|
||
} else if (klassen) {
|
||
ziel.append(el('span', klassen, s.t));
|
||
} else {
|
||
ziel.append(document.createTextNode(s.t));
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Hebt die Fundstellen der Suche hervor — mit Fläche, nicht mit Breite. */
|
||
function trefferZeichnen(ziel, text, wort, klassen) {
|
||
const klein = text.toLowerCase();
|
||
let von = 0, ab = 0;
|
||
while ((von = klein.indexOf(wort, ab)) !== -1) {
|
||
if (von > ab) ziel.append(el('span', klassen, text.slice(ab, von)));
|
||
const t = el('span', (klassen ? klassen + ' ' : '') + 'n-treffer',
|
||
text.slice(von, von + wort.length));
|
||
ziel.append(t);
|
||
ab = von + wort.length;
|
||
}
|
||
if (ab < text.length) ziel.append(el('span', klassen, text.slice(ab)));
|
||
}
|
||
|
||
function deckZeichnen() {
|
||
const text = feld.value;
|
||
deck.textContent = '';
|
||
const treffer = suchwort.length >= 2 ? suchwort : '';
|
||
const zeilen = text.split('\n');
|
||
zeilen.forEach((z, i) => {
|
||
zeileZeichnen(deck, z, treffer);
|
||
/* Der Zeilenumbruch gehört zum Text und muss mitgezeichnet
|
||
werden -- sonst stünde alles in einer Zeile. Beim LETZTEN
|
||
Element darf er nicht dazu: Ein Schreibfeld hat nach der
|
||
letzten Zeile keinen Umbruch, und die Deckschicht wäre eine
|
||
Zeile zu hoch. */
|
||
if (i < zeilen.length - 1) deck.append(document.createTextNode('\n'));
|
||
});
|
||
/* Ein Schreibfeld, das auf einen Umbruch endet, zeigt darunter
|
||
noch eine leere Zeile. Ein <div> tut das nicht -- deshalb eine
|
||
Leerzeile anhängen, damit beide gleich hoch bleiben. */
|
||
if (text.endsWith('\n')) deck.append(document.createTextNode(' '));
|
||
|
||
hoeheAnpassen();
|
||
titelSetzen();
|
||
hakenStandSetzen();
|
||
}
|
||
|
||
/** Das Schreibfeld wächst mit dem Text — gescrollt wird der Rahmen.
|
||
*
|
||
* So bleibt die Deckschicht ohne eigenes Scrollen deckungsgleich:
|
||
* Beide stehen im selben scrollenden Kasten. Ein eigener
|
||
* Bildlauf im Schreibfeld würde außerdem eine Bildlaufleiste
|
||
* einblenden, die Zeilen schmaler macht — und ab da bräche der
|
||
* Text in beiden Schichten an verschiedenen Stellen um. */
|
||
function hoeheAnpassen() {
|
||
feld.style.height = 'auto';
|
||
const hoch = Math.max(feld.scrollHeight, 320);
|
||
feld.style.height = hoch + 'px';
|
||
deck.style.minHeight = hoch + 'px';
|
||
}
|
||
|
||
function titelSetzen() {
|
||
const t = (feld.value.split('\n').find((z) => z.trim().length) || '')
|
||
.replace(/^\s*#{1,3}\s*/, '')
|
||
.replace(/^\s*[-*]\s+/, '')
|
||
.replace(/^\s*\[[ xX]\]\s*/, '')
|
||
.trim();
|
||
const k = $('blatt-titel');
|
||
k.textContent = t || 'Ohne Titel';
|
||
k.dataset.leer = t ? 'nein' : 'ja';
|
||
k.title = t || '';
|
||
}
|
||
|
||
/** Wie viele Haken gesetzt sind. Erscheint nur, wenn es welche gibt. */
|
||
function hakenZaehlen(text) {
|
||
const alle = String(text || '').match(/^\s*\[[ xX]\]/gm) || [];
|
||
const an = alle.filter((s) => !/\[ \]/.test(s)).length;
|
||
return { alle: alle.length, an };
|
||
}
|
||
function hakenStandSetzen() {
|
||
const s = $('haken-stand');
|
||
const z = hakenZaehlen(feld.value);
|
||
if (!z.alle) { s.hidden = true; return; }
|
||
s.hidden = false;
|
||
s.textContent = `${z.an} von ${z.alle} erledigt`;
|
||
s.dataset.fertig = z.an === z.alle ? 'ja' : 'nein';
|
||
}
|
||
|
||
/* =====================================================================
|
||
SICHERN
|
||
|
||
Gespeichert wird 800 ms nach dem letzten Tastendruck. Klappt das
|
||
nicht, bleibt der Text hier liegen und wird beim nächsten Versuch
|
||
nachgereicht — und der Fuß sagt es. Ein „konnte nicht speichern"
|
||
ohne Rettung wäre die schlechteste aller Meldungen: Sie erzählt
|
||
von einem Verlust, den man gerade nicht verhindern kann.
|
||
|
||
DER ZWISCHENSPEICHER IM BROWSER ist die zweite Sicherung, für den
|
||
Fall, dass jemand den Reiter schließt, bevor es durchgeht. Beim
|
||
nächsten Öffnen wird er angeboten — nicht heimlich eingespielt:
|
||
Wer zwischendurch auf einem anderen Gerät geschrieben hat, würde
|
||
sonst seine neuere Fassung verlieren.
|
||
===================================================================== */
|
||
const LAGER = 'dogi-notizen-offen';
|
||
let sicherUhr = null;
|
||
let laeuft = false;
|
||
let nochmal = false;
|
||
|
||
function standSetzen(stand, wort) {
|
||
const s = $('stand');
|
||
s.dataset.stand = stand;
|
||
s.textContent = wort;
|
||
}
|
||
|
||
function lagerSchreiben(id, text) {
|
||
try {
|
||
localStorage.setItem(LAGER, JSON.stringify({ id, text, am: Date.now() }));
|
||
} catch { /* Voller Speicher ist kein Grund, das Tippen zu stoppen. */ }
|
||
}
|
||
function lagerLeeren() {
|
||
try { localStorage.removeItem(LAGER); } catch { /* egal */ }
|
||
}
|
||
function lagerLesen() {
|
||
try { return JSON.parse(localStorage.getItem(LAGER) || 'null'); } catch { return null; }
|
||
}
|
||
|
||
function aendernGeplant() {
|
||
if (!aktuell) return;
|
||
lagerSchreiben(aktuell.id, feld.value);
|
||
standSetzen('sichert', 'sichert …');
|
||
clearTimeout(sicherUhr);
|
||
sicherUhr = setTimeout(sichern, 800);
|
||
}
|
||
|
||
async function sichern() {
|
||
if (!aktuell) return;
|
||
if (laeuft) { nochmal = true; return; }
|
||
laeuft = true;
|
||
const id = aktuell.id;
|
||
const text = feld.value;
|
||
try {
|
||
const a = await hole(`/workspace/api/notizblock/${id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text }),
|
||
});
|
||
if (!a.ok) {
|
||
standSetzen('offen', await fehlerText(a, 'noch nicht gesichert'));
|
||
return;
|
||
}
|
||
const j = await a.json();
|
||
const n = notizen.find((x) => x.id === id);
|
||
if (n) { n.text = text; n.geaendert = j.geaendert; n.titel = titelAus(text); }
|
||
lagerLeeren();
|
||
standSetzen('gesichert', 'gesichert');
|
||
wannSetzen();
|
||
stapelZeichnen();
|
||
} catch {
|
||
/* Kein Netz. Der Text liegt im Browser; beim nächsten Tippen
|
||
oder beim nächsten Öffnen geht es weiter. */
|
||
standSetzen('offen', 'noch nicht gesichert – bleibt hier, bis es geht');
|
||
} finally {
|
||
laeuft = false;
|
||
if (nochmal) { nochmal = false; setTimeout(sichern, 200); }
|
||
}
|
||
}
|
||
|
||
const titelAus = (text) => (String(text || '').split('\n').find((z) => z.trim().length) || '')
|
||
.replace(/^\s*#{1,3}\s*/, '').replace(/^\s*[-*]\s+/, '')
|
||
.replace(/^\s*\[[ xX]\]\s*/, '').trim().slice(0, 120);
|
||
|
||
function wannSetzen() {
|
||
const w = $('wann');
|
||
if (!aktuell) { w.textContent = ''; return; }
|
||
w.textContent = herZeit(aktuell.geaendert);
|
||
w.title = genau(aktuell.geaendert);
|
||
}
|
||
|
||
/* =====================================================================
|
||
DER STAPEL
|
||
===================================================================== */
|
||
function passt(n) {
|
||
if (suchwort.length < 2) return true;
|
||
return String(n.text || '').toLowerCase().includes(suchwort);
|
||
}
|
||
|
||
function stapelZeichnen() {
|
||
const liste = $('liste');
|
||
liste.textContent = '';
|
||
const sichtbar = notizen.filter(passt);
|
||
$('anzahl').textContent = suchwort.length >= 2
|
||
? `${sichtbar.length}/${notizen.length}` : String(notizen.length);
|
||
|
||
for (const n of sichtbar) {
|
||
const li = el('li');
|
||
const b = el('button', 'n-zettel');
|
||
b.type = 'button';
|
||
b.style.setProperty('--n-tinte', farbHex(n.farbe));
|
||
if (aktuell && n.id === aktuell.id) b.setAttribute('aria-current', 'true');
|
||
|
||
b.append(el('span', 'n-zettel__titel', n.titel || 'Ohne Titel'));
|
||
const vor = zweiteZeile(n.text);
|
||
if (vor) b.append(el('span', 'n-zettel__vor', vor));
|
||
|
||
const fuss = el('span', 'n-zettel__fuss');
|
||
if (n.angeheftet) {
|
||
const nadel = el('span', 'n-zettel__nadel', '📌');
|
||
nadel.title = 'Angeheftet';
|
||
fuss.append(nadel);
|
||
}
|
||
fuss.append(el('span', null, herZeit(n.geaendert)));
|
||
const h = hakenZaehlen(n.text);
|
||
if (h.alle) {
|
||
const hz = el('span', 'n-zettel__haken', `${h.an}/${h.alle}`);
|
||
hz.dataset.fertig = h.an === h.alle ? 'ja' : 'nein';
|
||
hz.title = `${h.an} von ${h.alle} erledigt`;
|
||
fuss.append(hz);
|
||
}
|
||
b.append(fuss);
|
||
|
||
b.addEventListener('click', () => oeffne(n.id));
|
||
li.append(b);
|
||
liste.append(li);
|
||
}
|
||
|
||
$('leer').hidden = notizen.length > 0;
|
||
$('korb-auf').hidden = papierkorbZahl === 0;
|
||
$('korb-zahl').textContent = String(papierkorbZahl);
|
||
}
|
||
|
||
function zweiteZeile(text) {
|
||
const zeilen = String(text || '').split('\n').filter((z) => z.trim().length);
|
||
return (zeilen[1] || '').replace(/^\s*\[[ xX]\]\s*/, '').replace(/^\s*[-*#>]+\s*/, '')
|
||
.trim().slice(0, 90);
|
||
}
|
||
|
||
const farbHex = (name) => (farben.find((f) => f.name === name) || farben[0] || {}).hex || '#9cde90';
|
||
|
||
/* =====================================================================
|
||
EIN BLATT AUFSCHLAGEN
|
||
===================================================================== */
|
||
function oeffne(id, { fokus = true } = {}) {
|
||
const n = notizen.find((x) => x.id === id);
|
||
if (!n) return;
|
||
/* Was im alten Blatt noch offen ist, geht zuerst raus. Sonst
|
||
landet der letzte Satz beim nächsten Blatt -- oder nirgends. */
|
||
if (aktuell && aktuell.id !== id) { clearTimeout(sicherUhr); sichern(); }
|
||
aktuell = n;
|
||
$('korb').hidden = true;
|
||
$('blatt').hidden = false;
|
||
$('blatt').style.setProperty('--n-tinte', farbHex(n.farbe));
|
||
feld.value = n.text || '';
|
||
feld.maxLength = textMax;
|
||
deckZeichnen();
|
||
wannSetzen();
|
||
standSetzen('gesichert', 'gesichert');
|
||
$('anheften').setAttribute('aria-pressed', n.angeheftet ? 'true' : 'false');
|
||
farbenZeichnen();
|
||
stapelZeichnen();
|
||
if (fokus) {
|
||
feld.focus();
|
||
/* Ans Ende springen, nicht an den Anfang: Wer ein Blatt
|
||
aufschlägt, schreibt weiter -- er liest es nicht von vorn. */
|
||
feld.setSelectionRange(feld.value.length, feld.value.length);
|
||
}
|
||
zurueckKnopf(true);
|
||
}
|
||
|
||
function schliessen() {
|
||
clearTimeout(sicherUhr);
|
||
if (aktuell) sichern();
|
||
aktuell = null;
|
||
$('blatt').hidden = true;
|
||
$('korb').hidden = true;
|
||
stapelZeichnen();
|
||
zurueckKnopf(false);
|
||
}
|
||
|
||
/* Der Zurück-Knopf der Kopfleiste schließt auf dem Handy das Blatt.
|
||
Auf dem Rechner wird er nicht gebraucht -- dort stehen Stapel und
|
||
Blatt nebeneinander, es gibt nichts zu schließen. */
|
||
function zurueckKnopf(an) {
|
||
const k = $('zurueck');
|
||
if (!k) return;
|
||
const schmal = window.matchMedia('(max-width: 860px)').matches;
|
||
k.hidden = !(an && schmal);
|
||
}
|
||
|
||
function farbenZeichnen() {
|
||
const kasten = $('farben');
|
||
kasten.textContent = '';
|
||
for (const f of farben) {
|
||
const b = el('button', 'n-farbe');
|
||
b.type = 'button';
|
||
b.style.setProperty('--f', f.hex);
|
||
b.title = f.wort;
|
||
b.setAttribute('aria-label', `Papierfarbe ${f.wort}`);
|
||
b.setAttribute('aria-pressed', aktuell && aktuell.farbe === f.name ? 'true' : 'false');
|
||
b.addEventListener('click', () => farbeSetzen(f.name));
|
||
kasten.append(b);
|
||
}
|
||
}
|
||
|
||
async function farbeSetzen(name) {
|
||
if (!aktuell) return;
|
||
const alt = aktuell.farbe;
|
||
aktuell.farbe = name;
|
||
$('blatt').style.setProperty('--n-tinte', farbHex(name));
|
||
farbenZeichnen();
|
||
stapelZeichnen();
|
||
try {
|
||
const a = await hole(`/workspace/api/notizblock/${aktuell.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ farbe: name }),
|
||
});
|
||
if (!a.ok) throw new Error('nein');
|
||
} catch {
|
||
/* Zurückdrehen. Eine Farbe, die nach dem Neuladen wieder
|
||
umspringt, ist schlimmer als eine, die gar nicht erst
|
||
umspringt. */
|
||
aktuell.farbe = alt;
|
||
$('blatt').style.setProperty('--n-tinte', farbHex(alt));
|
||
farbenZeichnen(); stapelZeichnen();
|
||
melde('Die Farbe ließ sich nicht sichern. Versuch es gleich noch einmal.');
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
DIE TASTATUR — sie führt die Liste fort
|
||
===================================================================== */
|
||
|
||
/** Setzt Text und Cursor in einem Zug, so dass der Browser es
|
||
* rückgängig machen kann.
|
||
*
|
||
* `document.execCommand('insertText')` ist offiziell veraltet und
|
||
* trotzdem das Einzige, was den Rückgängig-Stapel des Schreibfelds
|
||
* füllt. Wer stattdessen `value` setzt, nimmt dem Benutzer Strg+Z
|
||
* weg -- in einem Notizblock die unfreundlichste aller Ersparnisse.
|
||
* Geht es nicht, wird `value` gesetzt; dann fehlt nur das
|
||
* Rückgängigmachen für diesen einen Schritt. */
|
||
function einfuegen(text) {
|
||
feld.focus();
|
||
let ging = false;
|
||
try { ging = document.execCommand('insertText', false, text); } catch { ging = false; }
|
||
if (!ging) {
|
||
const a = feld.selectionStart, b = feld.selectionEnd;
|
||
feld.value = feld.value.slice(0, a) + text + feld.value.slice(b);
|
||
feld.setSelectionRange(a + text.length, a + text.length);
|
||
}
|
||
deckZeichnen();
|
||
aendernGeplant();
|
||
}
|
||
|
||
function zeileUm(pos) {
|
||
const v = feld.value;
|
||
const von = v.lastIndexOf('\n', pos - 1) + 1;
|
||
let bis = v.indexOf('\n', pos);
|
||
if (bis === -1) bis = v.length;
|
||
return { von, bis, text: v.slice(von, bis) };
|
||
}
|
||
|
||
/** Haken auf der Zeile des Cursors umschalten — oder einen setzen. */
|
||
function hakenUmschalten() {
|
||
const z = zeileUm(feld.selectionStart);
|
||
const m = /^(\s*)\[([ xX])\]/.exec(z.text);
|
||
if (m) {
|
||
const neu = m[2] === ' ' ? 'x' : ' ';
|
||
feld.setSelectionRange(z.von + m[1].length + 1, z.von + m[1].length + 2);
|
||
einfuegen(neu);
|
||
feld.setSelectionRange(z.bis, z.bis);
|
||
return;
|
||
}
|
||
/* Keiner da: einen setzen, und zwar hinter der Einrückung. */
|
||
const e = /^(\s*)/.exec(z.text)[1];
|
||
feld.setSelectionRange(z.von + e.length, z.von + e.length);
|
||
einfuegen('[ ] ');
|
||
}
|
||
|
||
feld.addEventListener('keydown', (e) => {
|
||
/* Strg/Cmd + Enter: neues Blatt. */
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||
e.preventDefault(); neuesBlatt(); return;
|
||
}
|
||
/* Strg/Cmd + Leertaste: Haken auf dieser Zeile. */
|
||
if ((e.ctrlKey || e.metaKey) && (e.key === ' ' || e.code === 'Space')) {
|
||
e.preventDefault(); hakenUmschalten(); return;
|
||
}
|
||
/* Tabulator rückt ein, statt aus dem Feld zu springen.
|
||
MIT UMSCHALT springt er weiter -- sonst käme man mit der
|
||
Tastatur nicht mehr aus dem Blatt heraus, und das wäre eine
|
||
Falle für alle, die nicht mit der Maus arbeiten. */
|
||
if (e.key === 'Tab' && !e.shiftKey) {
|
||
e.preventDefault(); einfuegen(' '); return;
|
||
}
|
||
if (e.key !== 'Enter' || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
|
||
|
||
/* Enter setzt die Liste fort. */
|
||
const z = zeileUm(feld.selectionStart);
|
||
const vorn = /^(\s*)(\[[ xX]\]\s|[-*]\s|>\s?)/.exec(z.text);
|
||
if (!vorn) return;
|
||
const nurZeichen = z.text.trim() === vorn[2].trim();
|
||
e.preventDefault();
|
||
if (nurZeichen) {
|
||
/* Zweimal Enter beendet die Liste -- wie in jedem Programm, das
|
||
Listen kann. Die angefangene Zeile wird dabei geleert. */
|
||
feld.setSelectionRange(z.von, z.bis);
|
||
einfuegen('\n');
|
||
return;
|
||
}
|
||
/* Ein gesetzter Haken wird NICHT mitgenommen: Die nächste Aufgabe
|
||
ist noch nicht erledigt. */
|
||
const fortsetzung = vorn[2].replace(/\[[xX]\]/, '[ ]');
|
||
einfuegen('\n' + vorn[1] + fortsetzung);
|
||
});
|
||
|
||
feld.addEventListener('input', () => { deckZeichnen(); aendernGeplant(); });
|
||
feld.addEventListener('scroll', () => { $('papier').scrollTop = feld.scrollTop; });
|
||
/* Beim Verlassen sofort sichern statt die 800 ms abzuwarten. */
|
||
feld.addEventListener('blur', () => { clearTimeout(sicherUhr); sichern(); });
|
||
|
||
/* Ein Klick auf ein Kästchen schaltet es um. Die Deckschicht lässt
|
||
sonst alles durch; nur die Haken fangen den Klick ab. */
|
||
deck.addEventListener('click', (e) => {
|
||
const hk = e.target.closest('.n-hk');
|
||
if (!hk) return;
|
||
e.preventDefault();
|
||
/* Wo im Text steht dieses Kästchen? Gezählt wird über die Knoten
|
||
davor -- das ist genauer als eine Suche nach dem n-ten „[ ]",
|
||
weil derselbe Text mehrfach vorkommen kann. */
|
||
let pos = 0;
|
||
const gehen = (knoten) => {
|
||
for (const k of knoten.childNodes) {
|
||
if (k === hk) return true;
|
||
if (k.nodeType === 3) pos += k.nodeValue.length;
|
||
else if (gehen(k)) return true;
|
||
}
|
||
return false;
|
||
};
|
||
if (!gehen(deck)) return;
|
||
const an = hk.dataset.an === 'ja';
|
||
feld.focus();
|
||
feld.setSelectionRange(pos + 1, pos + 2);
|
||
einfuegen(an ? ' ' : 'x');
|
||
feld.setSelectionRange(pos + 3, pos + 3);
|
||
});
|
||
|
||
/* =====================================================================
|
||
DIE GRIFFE
|
||
===================================================================== */
|
||
async function neuesBlatt() {
|
||
melde('');
|
||
try {
|
||
const a = await hole('/workspace/api/notizblock', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text: '' }),
|
||
});
|
||
if (!a.ok) { melde(await fehlerText(a, 'Das Blatt ließ sich nicht anlegen.')); return; }
|
||
const j = await a.json();
|
||
notizen.unshift(j.notiz);
|
||
oeffne(j.notiz.id);
|
||
} catch {
|
||
melde('Keine Verbindung. Sobald sie wieder da ist, geht es weiter.');
|
||
}
|
||
}
|
||
|
||
$('neu').addEventListener('click', neuesBlatt);
|
||
$('leer-neu').addEventListener('click', neuesBlatt);
|
||
|
||
$('anheften').addEventListener('click', async () => {
|
||
if (!aktuell) return;
|
||
const neu = !aktuell.angeheftet;
|
||
aktuell.angeheftet = neu;
|
||
$('anheften').setAttribute('aria-pressed', neu ? 'true' : 'false');
|
||
/* Umsortieren, damit man sieht, was der Knopf bewirkt hat. */
|
||
notizen.sort((a, b) => (Number(b.angeheftet) - Number(a.angeheftet))
|
||
|| (Date.parse(b.geaendert) - Date.parse(a.geaendert)));
|
||
stapelZeichnen();
|
||
try {
|
||
const a = await hole(`/workspace/api/notizblock/${aktuell.id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ angeheftet: neu }),
|
||
});
|
||
if (!a.ok) throw new Error('nein');
|
||
} catch {
|
||
aktuell.angeheftet = !neu;
|
||
$('anheften').setAttribute('aria-pressed', !neu ? 'true' : 'false');
|
||
stapelZeichnen();
|
||
melde('Das Anheften ließ sich nicht sichern.');
|
||
}
|
||
});
|
||
|
||
$('wegtun').addEventListener('click', async () => {
|
||
if (!aktuell) return;
|
||
const id = aktuell.id;
|
||
clearTimeout(sicherUhr);
|
||
await sichern();
|
||
try {
|
||
const a = await hole(`/workspace/api/notizblock/${id}`, { method: 'DELETE' });
|
||
if (!a.ok) { melde(await fehlerText(a, 'Ging nicht.')); return; }
|
||
notizen = notizen.filter((x) => x.id !== id);
|
||
papierkorbZahl += 1;
|
||
aktuell = null;
|
||
$('blatt').hidden = true;
|
||
stapelZeichnen();
|
||
zurueckKnopf(false);
|
||
/* KEINE RÜCKFRAGE VORHER, ABER EIN WEG ZURÜCK NACHHER. Eine
|
||
Rückfrage bei jedem Wegtun ist eine Warnung, die immer kommt
|
||
-- die klickt man weg. Der Papierkorb ist die bessere
|
||
Sicherung: Er kostet nichts und hält dreißig Tage. */
|
||
melde(`Weggelegt. Liegt ${korbTage} Tage im Papierkorb.`);
|
||
setTimeout(() => melde(''), 6000);
|
||
} catch {
|
||
melde('Keine Verbindung.');
|
||
}
|
||
});
|
||
|
||
/* =====================================================================
|
||
DER PAPIERKORB
|
||
===================================================================== */
|
||
$('korb-auf').addEventListener('click', korbZeigen);
|
||
$('korb-zu').addEventListener('click', () => {
|
||
$('korb').hidden = true;
|
||
if (aktuell) $('blatt').hidden = false; else zurueckKnopf(false);
|
||
});
|
||
|
||
async function korbZeigen() {
|
||
melde('');
|
||
try {
|
||
const a = await hole('/workspace/api/notizblock/papierkorb');
|
||
if (!a.ok) { melde(await fehlerText(a, 'Der Papierkorb ließ sich nicht öffnen.')); return; }
|
||
const j = await a.json();
|
||
korbTage = j.papierkorb_tage || korbTage;
|
||
$('blatt').hidden = true;
|
||
$('korb').hidden = false;
|
||
zurueckKnopf(true);
|
||
$('korb-satz').textContent = j.notizen.length
|
||
? `Hier bleibt alles ${korbTage} Tage liegen und geht dann von selbst.`
|
||
: 'Nichts drin.';
|
||
const liste = $('korb-liste');
|
||
liste.textContent = '';
|
||
for (const n of j.notizen) {
|
||
const li = el('li');
|
||
const zeile = el('div', 'n-korb-zeile');
|
||
zeile.style.setProperty('--n-tinte', farbHex(n.farbe));
|
||
const t = el('div', 'n-korb-zeile__text');
|
||
t.append(el('span', 'n-korb-zeile__titel', n.titel || 'Ohne Titel'));
|
||
const w = el('span', 'n-korb-zeile__wann',
|
||
`weggelegt ${herZeit(n.weg_am)} · verschwindet ${herTag(n.endgueltig_am)}`);
|
||
w.title = genau(n.endgueltig_am);
|
||
t.append(w);
|
||
zeile.append(t);
|
||
|
||
const zurueck = el('button', 'knopf-still', 'Zurückholen');
|
||
zurueck.type = 'button';
|
||
zurueck.addEventListener('click', () => zurueckholen(n.id));
|
||
zeile.append(zurueck);
|
||
|
||
const weg = el('button', 'n-griff n-griff--weg');
|
||
weg.type = 'button';
|
||
weg.append(el('span', 'n-griff__text', 'Endgültig'));
|
||
weg.addEventListener('click', () => endgueltig(n.id, n.titel));
|
||
zeile.append(weg);
|
||
|
||
li.append(zeile);
|
||
liste.append(li);
|
||
}
|
||
} catch {
|
||
melde('Keine Verbindung.');
|
||
}
|
||
}
|
||
|
||
/** „in 12 Tagen" — dieselbe Idee wie herZeit, nur nach vorn. */
|
||
function herTag(iso) {
|
||
const ms = Date.parse(iso) - Date.now();
|
||
if (!Number.isFinite(ms)) return '';
|
||
const tage = Math.ceil(ms / 86400000);
|
||
if (tage <= 0) return 'demnächst';
|
||
if (tage === 1) return 'morgen';
|
||
return `in ${tage} Tagen`;
|
||
}
|
||
|
||
async function zurueckholen(id) {
|
||
try {
|
||
const a = await hole(`/workspace/api/notizblock/${id}/zurueck`, { method: 'POST' });
|
||
if (!a.ok) { melde(await fehlerText(a, 'Ging nicht.')); return; }
|
||
const j = await a.json();
|
||
notizen.unshift(j.notiz);
|
||
notizen.sort((x, y) => (Number(y.angeheftet) - Number(x.angeheftet))
|
||
|| (Date.parse(y.geaendert) - Date.parse(x.geaendert)));
|
||
papierkorbZahl = Math.max(0, papierkorbZahl - 1);
|
||
$('korb').hidden = true;
|
||
oeffne(j.notiz.id);
|
||
} catch { melde('Keine Verbindung.'); }
|
||
}
|
||
|
||
async function endgueltig(id, titel) {
|
||
/* HIER WIRD GEFRAGT, und nur hier. Der Unterschied zum Wegtun ist
|
||
genau der, der eine Rückfrage rechtfertigt: Danach gibt es
|
||
keinen Weg zurück mehr. */
|
||
const name = titel ? `„${titel}"` : 'dieses Blatt';
|
||
if (!window.confirm(`${name} endgültig löschen?\n\nDanach ist es weg – `
|
||
+ 'auch für dich.')) return;
|
||
try {
|
||
const a = await hole(`/workspace/api/notizblock/${id}/endgueltig`, { method: 'DELETE' });
|
||
if (!a.ok) { melde(await fehlerText(a, 'Ging nicht.')); return; }
|
||
papierkorbZahl = Math.max(0, papierkorbZahl - 1);
|
||
stapelZeichnen();
|
||
korbZeigen();
|
||
} catch { melde('Keine Verbindung.'); }
|
||
}
|
||
|
||
/* =====================================================================
|
||
SUCHE
|
||
===================================================================== */
|
||
$('suche').addEventListener('input', (e) => {
|
||
suchwort = String(e.target.value || '').trim().toLowerCase();
|
||
stapelZeichnen();
|
||
if (aktuell) deckZeichnen();
|
||
});
|
||
$('suche').addEventListener('keydown', (e) => {
|
||
if (e.key !== 'Escape') return;
|
||
e.target.value = ''; suchwort = '';
|
||
stapelZeichnen(); if (aktuell) deckZeichnen();
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
|
||
e.preventDefault(); $('suche').focus(); $('suche').select();
|
||
}
|
||
if (e.key === 'Escape' && document.activeElement === feld) {
|
||
feld.blur();
|
||
if (window.matchMedia('(max-width: 860px)').matches) schliessen();
|
||
}
|
||
});
|
||
|
||
$('zurueck')?.addEventListener('click', (e) => {
|
||
if ($('blatt').hidden && $('korb').hidden) return; // nicht unseres
|
||
e.preventDefault(); e.stopPropagation();
|
||
schliessen();
|
||
}, true);
|
||
|
||
window.addEventListener('resize', () => { zurueckKnopf(!!aktuell); hoeheAnpassen(); });
|
||
|
||
/* Beim Verlassen der Seite noch einmal losschicken. `keepalive`
|
||
sorgt dafür, dass die Anfrage den Seitenwechsel überlebt --
|
||
ohne das wird sie vom Browser abgebrochen, und der letzte Satz
|
||
wäre weg. */
|
||
window.addEventListener('pagehide', () => {
|
||
if (!aktuell) return;
|
||
const n = notizen.find((x) => x.id === aktuell.id);
|
||
if (n && n.text === feld.value) return;
|
||
lagerSchreiben(aktuell.id, feld.value);
|
||
try {
|
||
fetch(`/workspace/api/notizblock/${aktuell.id}`, {
|
||
method: 'PATCH', credentials: 'same-origin', keepalive: true,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text: feld.value }),
|
||
});
|
||
} catch { /* Dann hilft der Zwischenspeicher beim nächsten Mal. */ }
|
||
});
|
||
|
||
/* =====================================================================
|
||
LOS
|
||
===================================================================== */
|
||
async function laden() {
|
||
try {
|
||
const a = await hole('/workspace/api/notizblock');
|
||
if (!a.ok) { melde(await fehlerText(a, 'Der Block ließ sich nicht laden.')); return; }
|
||
const j = await a.json();
|
||
notizen = j.notizen || [];
|
||
farben = j.farben || [];
|
||
papierkorbZahl = j.papierkorb || 0;
|
||
textMax = j.text_max || textMax;
|
||
korbTage = j.papierkorb_tage || korbTage;
|
||
stapelZeichnen();
|
||
|
||
/* Lag noch etwas im Browser? Anbieten, nicht einspielen. */
|
||
const rest = lagerLesen();
|
||
const dazu = rest && notizen.find((x) => x.id === rest.id);
|
||
if (dazu && rest.text !== dazu.text) {
|
||
melde('Auf diesem Gerät liegt noch eine ungesicherte Fassung von '
|
||
+ `„${dazu.titel || 'Ohne Titel'}". Öffne das Blatt – du kannst sie dann übernehmen.`);
|
||
dazu.rest = rest.text;
|
||
} else if (rest) {
|
||
lagerLeeren();
|
||
}
|
||
|
||
/* Auf dem Rechner wird das oberste Blatt gleich aufgeschlagen --
|
||
ein leerer Arbeitsplatz neben einem vollen Stapel ist eine
|
||
Aufforderung zum Suchen. Auf dem Handy nicht: Dort legt sich
|
||
das Blatt über alles, und man käme nie an die Liste. */
|
||
if (notizen.length && !window.matchMedia('(max-width: 860px)').matches) {
|
||
oeffne(notizen[0].id, { fokus: false });
|
||
}
|
||
} catch {
|
||
melde('Keine Verbindung zum Server.');
|
||
}
|
||
}
|
||
|
||
laden();
|
||
})();
|