Zettel: ein Textfeld, mehr nicht — InkNote gibt es schon. Gelesen wird gesetzt (Ueberschriften, Haken, Aufzaehlungen, fett/kursiv/code, Strich), geschrieben wird roh. Haken lassen sich ohne Bearbeiten setzen und aendern die Quelle. Gespeichert wird 800 ms nach dem letzten Anschlag, sofort beim Verlassen und beim Schliessen des Tabs (keepalive statt sendBeacon: der kann nur POST und traegt keine Kopfzeilen). Grenze 4000 Zeichen, vom Server durchgesetzt. Bild: genau eines je Nutzer, hochgeladen oder verlinkt. Die erste Datei, die HoneyOS ablegt — in bilder/ neben honeyos.json, benannt nach einem Fingerabdruck der Nutzerkennung. Ein neues Bild ersetzt das alte samt Datei, die Adresse traegt einen Versionsanteil gegen den Zwischen- speicher. SVG kommt nicht herein (Text, kann Skripte tragen), und der Typ wird an den Bytes erkannt, nicht an der Angabe des Browsers. Der Rahmen kann jetzt Widgets mit eigenem DOM: 'stabil' haelt den Inhalt, 'montiert' verdrahtet ihn. Ohne das haette das Anklopfen bei den Apps alle zwei Minuten den Text unter den Fingern ausgetauscht. Nebenbei: Der Aufrufhelfer setzte bei jedem Schreibzugriff Content-Type: application/json — auch fuer Bildbytes.
255 lines
7.9 KiB
JavaScript
255 lines
7.9 KiB
JavaScript
import { api, escapeHtml } from "../core.js";
|
|
import { widgetAnmelden, widgetsZeichnen } from "./rahmen.js";
|
|
|
|
/* ── Der Zettel ────────────────────────────────────────────────────────────
|
|
*
|
|
* Ein Textfeld, mehr nicht. **InkNote gibt es schon** — der Zettel darf keine
|
|
* zweite Notiz-App werden: kein Titel, keine Liste, 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.
|
|
*
|
|
* Zwei Ansichten: gelesen wird gesetzt, geschrieben wird roh. Der Umweg über
|
|
* einen Knopf ist Absicht — ein Feld, das immer im Bearbeitungsmodus steht,
|
|
* frisst jeden Klick, den man eigentlich dem Häkchen zugedacht hatte.
|
|
*
|
|
* Häkchen lassen sich **ohne** Bearbeiten setzen. Das ist der häufigste Griff
|
|
* an einer Liste, und dafür erst in den Text zu springen wäre albern.
|
|
*/
|
|
|
|
const LEER = "Noch nichts notiert.";
|
|
const VERZUG = 800; // ms bis zum Speichern beim Tippen
|
|
|
|
let text = "";
|
|
let bearbeiten = false;
|
|
let stand = ""; // "speichert …" | "gespeichert" | Fehlertext
|
|
let uhr = null;
|
|
|
|
widgetAnmelden({
|
|
id: "notiz",
|
|
titel: "zettel",
|
|
// Der Zettel hält ein Textfeld: Sein 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 (bearbeiten) {
|
|
return `
|
|
<textarea class="zettel-feld" spellcheck="false"
|
|
placeholder="# Einkauf - [ ] Milch - [x] Brot">${escapeHtml(text)}</textarea>
|
|
<div class="zettel-fuss">
|
|
<span class="zettel-stand">${escapeHtml(stand)}</span>
|
|
<button class="btn zettel-fertig">fertig</button>
|
|
</div>`;
|
|
}
|
|
return `
|
|
<div class="zettel-text">${text.trim() ? markdown(text) : `<p class="w-zustand">${LEER}</p>`}</div>
|
|
<div class="zettel-fuss">
|
|
<span class="zettel-stand">${escapeHtml(stand)}</span>
|
|
<button class="btn zettel-auf">${text.trim() ? "bearbeiten" : "schreiben"}</button>
|
|
</div>`;
|
|
},
|
|
|
|
montiert(el) {
|
|
const feld = el.querySelector(".zettel-feld");
|
|
if (feld) {
|
|
feld.focus();
|
|
// Schreibmarke ans Ende, nicht an den Anfang: Man schreibt weiter, man
|
|
// fängt selten neu an.
|
|
feld.setSelectionRange(feld.value.length, feld.value.length);
|
|
feld.addEventListener("input", () => {
|
|
text = feld.value;
|
|
spaeterSpeichern();
|
|
});
|
|
// Beim Verlassen sofort, nicht erst nach dem Verzug — sonst ginge der
|
|
// letzte Satz verloren, wenn man den Tab zumacht.
|
|
feld.addEventListener("blur", jetztSpeichern);
|
|
el.querySelector(".zettel-fertig").addEventListener("click", () => {
|
|
bearbeiten = false;
|
|
jetztSpeichern();
|
|
neu();
|
|
});
|
|
return;
|
|
}
|
|
|
|
const auf = el.querySelector(".zettel-auf");
|
|
if (auf) {
|
|
auf.addEventListener("click", () => {
|
|
bearbeiten = true;
|
|
neu();
|
|
});
|
|
}
|
|
// Häkchen im gesetzten Text — sie ändern die Quelle, nicht nur die Anzeige.
|
|
el.querySelectorAll("[data-hak]").forEach((box) =>
|
|
box.addEventListener("change", () => {
|
|
text = hakenUmlegen(text, Number(box.dataset.hak));
|
|
neu();
|
|
jetztSpeichern();
|
|
})
|
|
);
|
|
},
|
|
});
|
|
|
|
/** Neu zeichnen — der Rahmen lässt stabile Widgets sonst in Ruhe. */
|
|
function neu() {
|
|
const koerper = document.querySelectorAll('[data-widget="notiz"]');
|
|
koerper.forEach((el) => delete el.dataset.zustand);
|
|
widgetsZeichnen();
|
|
}
|
|
|
|
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
|
|
* einen Zettel 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"] .zettel-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 ein
|
|
* Zettel, der `<script>` ausführt, wäre eine Falle für den Tag, an dem man
|
|
* etwas hineinkopiert.
|
|
*/
|
|
export function markdown(quelle) {
|
|
const zeilen = quelle.split("\n");
|
|
let html = "";
|
|
let liste = null; // "ul" | "ol" | null
|
|
let hakenNr = -1; // fortlaufend über den ganzen Text, für data-hak
|
|
|
|
const listeZu = () => {
|
|
if (liste) html += `</${liste}>`;
|
|
liste = null;
|
|
};
|
|
const listeAuf = (art) => {
|
|
if (liste !== art) {
|
|
listeZu();
|
|
html += `<${art}>`;
|
|
liste = art;
|
|
}
|
|
};
|
|
|
|
zeilen.forEach((roh) => {
|
|
const z = roh.trimEnd();
|
|
|
|
if (!z.trim()) {
|
|
listeZu();
|
|
return;
|
|
}
|
|
|
|
const ueber = /^(#{1,3})\s+(.*)$/.exec(z);
|
|
if (ueber) {
|
|
listeZu();
|
|
const n = ueber[1].length + 2; // # → h3, ## → h4, ### → h5
|
|
html += `<h${n}>${inline(ueber[2])}</h${n}>`;
|
|
return;
|
|
}
|
|
|
|
if (/^(-{3,}|\*{3,})$/.test(z.trim())) {
|
|
listeZu();
|
|
html += "<hr />";
|
|
return;
|
|
}
|
|
|
|
const hak = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/.exec(z);
|
|
if (hak) {
|
|
hakenNr++;
|
|
listeAuf("ul");
|
|
const an = hak[1].toLowerCase() === "x";
|
|
html +=
|
|
`<li class="zettel-hak${an ? " ab" : ""}">` +
|
|
`<label><input type="checkbox" data-hak="${hakenNr}"${an ? " checked" : ""} />` +
|
|
`<span>${inline(hak[2])}</span></label></li>`;
|
|
return;
|
|
}
|
|
|
|
const punkt = /^\s*[-*]\s+(.*)$/.exec(z);
|
|
if (punkt) {
|
|
listeAuf("ul");
|
|
html += `<li>${inline(punkt[1])}</li>`;
|
|
return;
|
|
}
|
|
|
|
const zahl = /^\s*\d+[.)]\s+(.*)$/.exec(z);
|
|
if (zahl) {
|
|
listeAuf("ol");
|
|
html += `<li>${inline(zahl[1])}</li>`;
|
|
return;
|
|
}
|
|
|
|
listeZu();
|
|
html += `<p>${inline(z)}</p>`;
|
|
});
|
|
|
|
listeZu();
|
|
return html;
|
|
}
|
|
|
|
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(() => {});
|
|
});
|