The editor was a textarea holding raw Markdown, which is the wrong thing to hand someone taking notes during a lesson: nobody types `##` and `**` while a teacher is talking. It now shows the note formatted and puts a toolbar above it — headings, bold, lists, tick boxes, quotes, links, tables — while the file on disk stays exactly what it was, because that is what the indexer reads and what outlives this app. `markdown.js` is the whole translation: `markdownToHtml` on the way in, `markdownFromDom` on the way out. The property that matters is that the round trip settles — one pass may tidy a note, a second must change nothing — because these notes are the only record of what was said in the room and there is nothing to restore a lossy save from. `editor.js` checks exactly that before opening a note formatted, and a note it cannot hold unchanged opens in the Markdown view and says so instead of being quietly reduced. No editor library: the content security policy allows no outside script and the app has no bundler, so this is `contenteditable` and `execCommand` with a tolerant serializer behind it — an element it does not model keeps its words and loses its tag. Pasted HTML is converted to Markdown before it reaches the document, which is the one place where sanitising and formatting are the same operation. Tested against `test/mini-dom.ts`, sixty lines of read-only DOM, rather than a headless browser or a DOM dependency; the toolbar itself was driven by hand in Firefox. WebKit has still never run it.
118 lines
5.7 KiB
HTML
118 lines
5.7 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<meta name="theme-color" content="#1f6feb">
|
||
<title>Schulcloud — Notizen</title>
|
||
<link rel="icon" href="icon.svg" type="image/svg+xml">
|
||
<link rel="manifest" href="manifest.webmanifest">
|
||
<link rel="stylesheet" href="app.css">
|
||
</head>
|
||
<body>
|
||
<noscript>Diese Seite braucht JavaScript.</noscript>
|
||
|
||
<!-- Login. Shown until /app/session says otherwise; everything else stays hidden. -->
|
||
<section id="login" class="screen" hidden>
|
||
<form id="login-form" class="card">
|
||
<h1>Anmelden</h1>
|
||
<label for="password">Passwort</label>
|
||
<input id="password" name="password" type="password" autocomplete="current-password" required autofocus>
|
||
<button type="submit">Anmelden</button>
|
||
<p id="login-error" class="error" role="alert" aria-live="assertive"></p>
|
||
</form>
|
||
</section>
|
||
|
||
<div id="app" class="screen" hidden>
|
||
<header>
|
||
<nav class="tabs">
|
||
<button type="button" id="tab-notes" class="tab" aria-current="page">Notizen</button>
|
||
<button type="button" id="tab-settings" class="tab">Einstellungen</button>
|
||
</nav>
|
||
</header>
|
||
|
||
<!-- Notes: one school day per note, one heading per lesson. -->
|
||
<main id="view-notes" class="view">
|
||
<div class="daybar">
|
||
<button type="button" id="prev" aria-label="Vorheriger Tag">‹</button>
|
||
<div class="daybar-centre">
|
||
<strong id="day-title">…</strong>
|
||
<input id="day-date" type="date" aria-label="Datum">
|
||
</div>
|
||
<button type="button" id="next" aria-label="Nächster Tag">›</button>
|
||
</div>
|
||
|
||
<p id="day-status" class="status" role="status" aria-live="polite"></p>
|
||
<p id="day-conflict" class="conflict" role="alert" hidden></p>
|
||
|
||
<!-- The toolbar writes the Markdown so nobody has to type it. Every button
|
||
carries a word as well as a glyph, because the glyph is the thing a
|
||
screen reader cannot read and a stranger cannot guess. -->
|
||
<div id="toolbar" class="toolbar" role="toolbar" aria-label="Formatierung">
|
||
<button type="button" data-command="h2" aria-pressed="false" aria-label="Stunde (Überschrift)" title="Stunde (Überschrift)"><b>H2</b></button>
|
||
<button type="button" data-command="h3" aria-pressed="false" aria-label="Zwischenüberschrift" title="Zwischenüberschrift"><b>H3</b></button>
|
||
<span class="sep" aria-hidden="true"></span>
|
||
<button type="button" data-command="bold" aria-pressed="false" aria-label="Fett" title="Fett (Strg+B)"><b>F</b></button>
|
||
<button type="button" data-command="italic" aria-pressed="false" aria-label="Kursiv" title="Kursiv (Strg+I)"><i>K</i></button>
|
||
<button type="button" data-command="strike" aria-pressed="false" aria-label="Durchgestrichen" title="Durchgestrichen"><s>S</s></button>
|
||
<button type="button" data-command="code" aria-label="Code" title="Code (Strg+E)"><code><></code></button>
|
||
<span class="sep" aria-hidden="true"></span>
|
||
<button type="button" data-command="ul" aria-pressed="false" aria-label="Aufzählung" title="Aufzählung">• —</button>
|
||
<button type="button" data-command="ol" aria-pressed="false" aria-label="Nummerierte Liste" title="Nummerierte Liste">1. —</button>
|
||
<button type="button" data-command="task" aria-label="Kästchen zum Abhaken" title="Kästchen zum Abhaken">☐</button>
|
||
<button type="button" data-command="quote" aria-pressed="false" aria-label="Zitat" title="Zitat">❝</button>
|
||
<span class="sep" aria-hidden="true"></span>
|
||
<button type="button" data-command="link" aria-label="Link" title="Link (Strg+K)">🔗</button>
|
||
<button type="button" data-command="table" aria-label="Tabelle" title="Tabelle">▦</button>
|
||
<span class="sep" aria-hidden="true"></span>
|
||
<button type="button" data-command="mode" aria-pressed="false" aria-label="Markdown bearbeiten" title="Markdown bearbeiten">MD</button>
|
||
</div>
|
||
|
||
<p id="editor-hint" class="hint" role="status" hidden></p>
|
||
|
||
<!-- The formatted document, and the same note as Markdown. Exactly one of
|
||
the two is visible; both hold the whole note. -->
|
||
<div id="editor" class="editor" contenteditable="true" spellcheck="true" autocapitalize="sentences"
|
||
role="textbox" aria-multiline="true" aria-label="Notizen des Tages"
|
||
data-placeholder="Noch nichts für diesen Tag."></div>
|
||
<textarea id="source" class="source" hidden spellcheck="false" autocapitalize="off"
|
||
aria-label="Notizen des Tages als Markdown"></textarea>
|
||
|
||
<div class="actions">
|
||
<button type="button" id="save">Speichern</button>
|
||
<button type="button" id="fill" hidden>Stunden ergänzen</button>
|
||
<span id="lessons-hint" class="hint"></span>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Settings: the Schulcloud token, and what the server is doing. -->
|
||
<main id="view-settings" class="view" hidden>
|
||
<section class="card">
|
||
<h2>Schulcloud-Token</h2>
|
||
<p id="token-state" class="status">…</p>
|
||
<ol class="steps">
|
||
<li>In einem privaten Fenster bei der Schulcloud anmelden.</li>
|
||
<li>DevTools → Application → Cookies → Wert des Cookies <code>jwt</code> kopieren.</li>
|
||
<li>Hier einsetzen und speichern. Der Server prüft ihn erst bei der Schulcloud.</li>
|
||
<li><strong>Das private Fenster schließen</strong> — offen gelassen meldet es den Token nach etwa zwei Stunden ab.</li>
|
||
</ol>
|
||
<form id="token-form">
|
||
<label for="jwt">Neuer jwt-Cookie</label>
|
||
<input id="jwt" type="password" autocomplete="off" spellcheck="false">
|
||
<button type="submit">Token ersetzen</button>
|
||
</form>
|
||
<p id="token-result" class="status" role="status" aria-live="polite"></p>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2>Server</h2>
|
||
<dl id="server-state"></dl>
|
||
<button type="button" id="logout">Abmelden</button>
|
||
</section>
|
||
</main>
|
||
</div>
|
||
|
||
<script type="module" src="app.js"></script>
|
||
</body>
|
||
</html>
|