feat: Etherpad-Anbindung ausgebaut (D78)
D31 lieh sich fuer die Echtzeit-Zusammenarbeit ein Etherpad: Das Pad war die Schreibflaeche, Werkbaum die Ansicht. Richtig fuer den Zeitpunkt - die schwere Arbeit war dort getan, und Werkbaum hatte kein Backend. Jetzt hat es eins (D76), das dasselbe besser und im Editor selbst kann. D31 bleibt stehen: Was dort gemessen wurde, ist der Grund, warum die Anbindung nie gut wurde - Export-Drosselung (10 je 90 s), SameSite=Lax beim Autoren-Cookie, abgelehnter Socket-Upgrade. Uebrig blieb ein Neu-laden-Knopf und ein schreibgeschuetztes Textfeld. Ein alter ?etherpad=-Link meldet sich, statt still nichts zu tun: Der Parameter bleibt erkannt und ergibt die zeilenlose Warnung padGone, die auf ?live= zeigt (neun Sprachen). Geholt wird nichts - nachgemessen geht keine Anfrage mehr an den Pad-Host. Vorhandene Pad-Dokumente bleiben liegen und werden dabei besser: ohne Schreibschutz sind sie bearbeitbar und sammeln fruehere Staende. Mit ausgebaut, weil sie nur der Anbindung dienten: remote.js samt Tests, der Ansichts-Waehler und sein Splitter, der Neu-laden-Knopf samt Drosselungs-Zaehler, die Warnungen sourceTimeout/padRateLimit, elf i18n- Schluessel x 9 Sprachen - und src.readOnly ganz, denn nur Pad-Dokumente haben es gesetzt (mit ihm updateSnapBtn und sieben tote Waechter). Auch #srcArea faellt weg: den Kasten gab es nur, damit Pad-Rahmen und Spiegel sich einen Bereich teilen. ?sourceUrl= bleibt unangetastet - beide teilten sich einen Fetch-Pfad, das war die Stelle, an der nichts verrutschen durfte. Im Plan wird aus dem #col.pad-Zweig (14 Knoten samt der ganzen "Update by itself"-Gruppe) ein verworfener Knoten mit Begruendung; der gemeinsame Zeigefinger wandert als #col.point heraus, denn !!! gilt jedem geteilten Dokument. 196 Knoten, 0 Warnungen. Nebengewinn: Der README bekommt endlich den Abschnitt zu ?live= - den gab es nie, die Einstiegs-Beschreibung stand weiter beim Pad. Nachgemessen im Browser: ?etherpad= zeigt die Warnung ohne Netz-Anfrage, ?sourceUrl= laedt unveraendert und ist beschreibbar, Textfeld und Zahlenstreifen liegen buendig, Legenden-Splitter teilt wie zuvor (954+10+300), Falten und Staende funktionieren. 501 Tests, 946 Zeilen weniger.
This commit is contained in:
+29
-43
@@ -88,60 +88,46 @@ GitLab-Raw-Links tun das; ein beliebiger Webserver oft nicht. Scheitert das
|
|||||||
Laden, bleibt der bisherige Stand stehen und eine Warnung nennt die Ursache.
|
Laden, bleibt der bisherige Stand stehen und eine Warnung nennt die Ursache.
|
||||||
Zugelassen sind nur `http`/`https`.
|
Zugelassen sind nur `http`/`https`.
|
||||||
|
|
||||||
### Zusammen an einem Plan arbeiten (Etherpad)
|
### Zusammen an einem Plan arbeiten (`?live=`)
|
||||||
|
|
||||||
Für Zusammenarbeit in Echtzeit braucht Werkbaum kein eigenes Backend — es leiht
|
Liegt der Plan auf einem Werkbaum-Backend, bearbeiten ihn alle **im Editor
|
||||||
sich ein **Etherpad**. Angegeben wird die Pad-Adresse, wie sie im Browser steht
|
selbst** und sehen die Änderungen der anderen, ohne neu zu laden:
|
||||||
(ohne Export-Pfad, den hängt Werkbaum selbst an):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/mein-plan
|
https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/<uuid>
|
||||||
```
|
```
|
||||||
|
|
||||||
Alle bearbeiten den Notationstext **im Pad**; ein Neu-laden-Knopf neben dem
|
Angelegt wird so ein Dokument über den Eintrag **„Auf den Server legen"** im
|
||||||
Pad-Knopf holt den aktuellen Stand. Das Zusammenführen gleichzeitiger Änderungen
|
Dokumenten-Menü: Er lädt den aktiven Plan hoch, schaltet dorthin um und schreibt
|
||||||
macht Etherpad — genau darum geht es.
|
den Link in die Adresszeile und in die Zwischenablage. Diesen Link weitergeben —
|
||||||
|
die Adresse ist die Einladung, und wer sie kennt, kommt hinein.
|
||||||
|
|
||||||
Bewusst **kein** Abrufen im Hintergrund: Etherpad drosselt den Export
|
**Das Textfeld bleibt beschreibbar.** Nach 1,5 s Ruhe schickt der Editor die
|
||||||
(serienmäßig 10 Abrufe je 90 s und IP), ein Takt gewinnt dagegen nicht — er
|
Änderung als Zeilen-Diff; ein offener Abruf hält die Gegenrichtung bereit und
|
||||||
*erzeugt* die Drosselung. Nachgemessen: Nach Überschreiten des Budgets hält der
|
spielt fremde Änderungen ein. **Die Schreibmarke wandert mit** — fügt jemand
|
||||||
Server die Verbindung rund zwei Minuten ohne Antwort offen und liefert dann in
|
oberhalb Zeilen ein, bleibt sie an ihrer Stelle im Text.
|
||||||
0,4 s. Der Knopf greift gut mit „Was ist neu?" zusammen: drücken, und was seither
|
|
||||||
in Produktion ging, leuchtet auf.
|
|
||||||
|
|
||||||
Weil das Pad die Schreibfläche ist, ist das Textfeld hier **schreibgeschützt**;
|
Der **Name** ist der Titel des Dokuments (alle sehen denselben), die vollständige
|
||||||
ein Knopf in der Editor-Titelzeile öffnet das Pad im neuen Tab. Ohne den Schutz
|
Adresse steht im Tooltip.
|
||||||
verschwände getippter Text beim nächsten Abruf.
|
|
||||||
|
|
||||||
Getestet gegen Etherpad: Der Klartext-Export sendet
|
**Überschneiden sich zwei Änderungen wirklich** — dieselben Zeilen —, fragt ein
|
||||||
`Access-Control-Allow-Origin: *`, und die Notation kommt **byte-identisch**
|
Band oben, wessen Fassung gelten soll: *Fremde übernehmen* oder *Eigene
|
||||||
zurück — führende Leerzeichen, `-`/`+`/`|`, Statusboxen und `%%` überleben
|
durchsetzen*. Alles andere führt der Server selbst zusammen, ohne zu fragen.
|
||||||
Etherpads Speichermodell, das `-` wird nicht zur Aufzählung umgedeutet.
|
Verloren geht nichts: Der verworfene Stand bleibt in den früheren Ständen, jede
|
||||||
|
Version in der Historie des Servers.
|
||||||
|
|
||||||
Das Pad lässt sich auch **im Editor-Panel einbetten**: Ein Wähler in der
|
**Ein gemeinsamer Zeigefinger:** `!!!` auf einer Zeile hebt diesen Knoten hervor
|
||||||
Titelzeile schaltet reihum zwischen *Pad und Text* (durch einen Splitter frei
|
und holt ihn ins Bild — bei **allen**, die das Dokument ansehen. Das kann ein
|
||||||
geteilt), *nur Pad* und *nur Text*. Der schmal gezogene Textspiegel behält seinen
|
Cursor nicht. Erkannt nur als alleinstehendes Token, `Achtung!!!` bleibt also ein
|
||||||
Zweck — der Sprung zwischen Diagramm und Text arbeitet auf ihm, und in *nur Pad*
|
gewöhnliches Label. Die Marke bleibt stehen, bis jemand sie löscht.
|
||||||
holt ein Sprung ihn selbst zurück. Der Rahmen wird nur geladen, solange er
|
|
||||||
sichtbar ist, denn ein geladenes Pad verbindet sich und zeigt dich in dessen
|
|
||||||
Anwesenden-Liste.
|
|
||||||
|
|
||||||
**Der gemeinsame Zeigefinger:** Schreib `!!!` in eine Zeile, und dieser Knoten
|
Das Backend einrichten: siehe [backend/README.md](backend/README.md) und
|
||||||
wird hervorgehoben und ins Bild geholt — bei **allen**, die auf das Pad schauen.
|
`docs/DECISIONS.md` D76.
|
||||||
Das kann ein Cursor nicht. Erkannt nur als alleinstehendes Token, `Achtung!!!`
|
|
||||||
bleibt also ein gewöhnliches Label. Die Marke bleibt stehen, bis jemand sie
|
|
||||||
löscht.
|
|
||||||
|
|
||||||
**Bedenke:** Der Plantext liegt damit auf fremder Infrastruktur, und ein Pad ist
|
> Dafür lieh sich Werkbaum früher ein **Etherpad** (`?etherpad=`). Das ist
|
||||||
für jeden lesbar, der die Adresse kennt. Im eingebetteten Rahmen wird Etherpads
|
> ausgebaut — das Backend kann dasselbe besser und im Editor selbst. Ein alter
|
||||||
Autoren-Cookie (`SameSite=Lax`) nicht mitgesendet — man gilt bei jedem Laden als
|
> `?etherpad=`-Link zeigt heute einen Hinweis hierher, statt still nichts zu
|
||||||
neuer Autor, was nur serverseitig zu beheben ist (`cookie.sameSite: "None"`).
|
> tun; siehe `docs/DECISIONS.md` D78.
|
||||||
|
|
||||||
Aus demselben Grund ist der eingebettete Rahmen vor allem zum **Mitlesen** gut:
|
|
||||||
Sollte das Bearbeiten darin einmal aufhören zu funktionieren, den Ansichts-Wähler
|
|
||||||
einmal durchschalten (das lädt den Rahmen neu) oder über „im Pad bearbeiten" im
|
|
||||||
eigenen Tab weiterarbeiten, wo das Cookie gilt. Siehe `docs/DECISIONS.md` D31
|
|
||||||
und D32.
|
|
||||||
|
|
||||||
### Lokal ausführen
|
### Lokal ausführen
|
||||||
|
|
||||||
|
|||||||
@@ -83,57 +83,46 @@ http(s) regardless of extension or content type.
|
|||||||
do; an arbitrary web server often does not. If loading fails the previous
|
do; an arbitrary web server often does not. If loading fails the previous
|
||||||
content stays and a warning explains why. Only `http`/`https` are allowed.
|
content stays and a warning explains why. Only `http`/`https` are allowed.
|
||||||
|
|
||||||
### Working on one plan together (Etherpad)
|
### Working on one plan together (`?live=`)
|
||||||
|
|
||||||
For real-time collaboration Werkbaum needs no backend of its own — it borrows an
|
If the plan lives on a Werkbaum backend, everyone edits it **in the editor
|
||||||
**Etherpad**. Pass the pad address as it appears in your browser (no export path,
|
itself** and sees the others' changes without reloading:
|
||||||
Werkbaum appends that itself):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/my-plan
|
https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/<uuid>
|
||||||
```
|
```
|
||||||
|
|
||||||
Everyone edits the notation text **in the pad**; a reload button next to the pad
|
To create such a document, open the document menu and pick **"Put on the
|
||||||
button fetches the current state. Merging simultaneous edits is Etherpad's job —
|
server"**: it uploads the active plan, switches to it, and puts the link in the
|
||||||
that is the whole point.
|
address bar and on your clipboard. Share that link — the address is the
|
||||||
|
invitation, and knowing it is what grants access.
|
||||||
|
|
||||||
Deliberately **no background polling**: Etherpad rate-limits the export (10
|
The text area stays **writable**. After 1.5 s of quiet the editor sends the
|
||||||
fetches per 90 s per IP by default), so a timer does not win against it — it
|
change as a line diff; an open request holds the other direction ready and plays
|
||||||
*causes* the throttling. Measured: after exceeding the budget the server holds
|
foreign changes in. **The caret travels with them** — if someone inserts lines
|
||||||
the connection open with no reply for about two minutes, then answers in 0.4 s.
|
above you, it stays where it was in the text.
|
||||||
The button pairs well with "what's new": press it, and whatever went into
|
|
||||||
production since your last look lights up.
|
|
||||||
|
|
||||||
Because the pad is the writing surface, the text area here is **read-only**; a
|
The document's **title** is its name (everyone sees the same one), the full
|
||||||
button in the editor title bar opens the pad in a new tab. Without that
|
address sits in the tooltip.
|
||||||
protection, anything you typed would vanish on the next fetch.
|
|
||||||
|
|
||||||
Verified against Etherpad: the plain-text export sends
|
When two changes really overlap — the same lines — a bar at the top asks whose
|
||||||
`Access-Control-Allow-Origin: *`, and the notation comes back **byte-identical** —
|
version should win: *take theirs* or *keep mine*. Everything else the server
|
||||||
leading spaces, `-`/`+`/`|`, status boxes and `%%` survive Etherpad's storage
|
merges without asking. Nothing is lost either way: the discarded state stays in
|
||||||
model, and `-` is not turned into a bullet list.
|
the earlier states, and every version is in the server's history.
|
||||||
|
|
||||||
The pad can also be **embedded** in the editor panel: a selector in the title bar
|
|
||||||
cycles through *pad and text* (split by a draggable divider), *pad only* and *text
|
|
||||||
only*. The narrow text mirror keeps its purpose — the jump between diagram and
|
|
||||||
text works on it, and in *pad only* a jump brings it back by itself. The frame is
|
|
||||||
only loaded while it is visible, because a loaded pad connects and shows you in
|
|
||||||
the pad's list of people.
|
|
||||||
|
|
||||||
**A shared pointer:** write `!!!` on a line and that node is highlighted and
|
**A shared pointer:** write `!!!` on a line and that node is highlighted and
|
||||||
scrolled into view — for **everyone** looking at the pad, which is something a
|
scrolled into view — for **everyone** looking at the document, which is
|
||||||
cursor cannot do. Recognised only as a standalone token, so `Careful!!!` stays an
|
something a cursor cannot do. Recognised only as a standalone token, so
|
||||||
ordinary label. It stays in the text until someone deletes it.
|
`Careful!!!` stays an ordinary label. It stays in the text until someone
|
||||||
|
deletes it.
|
||||||
|
|
||||||
**Be aware:** your plan text then lives on third-party infrastructure, and a pad
|
Setting up the backend: see [backend/README.md](backend/README.md) and
|
||||||
is readable by anyone who knows its address. In an embedded frame Etherpad's
|
`docs/DECISIONS.md` D76.
|
||||||
author cookie (`SameSite=Lax`) is not sent, so you count as a new author on every
|
|
||||||
load — fixable only on the server (`cookie.sameSite: "None"`).
|
|
||||||
|
|
||||||
For the same reason the **embedded frame is best for reading along**: if editing
|
> Werkbaum used to borrow an **Etherpad** for this (`?etherpad=`). That is
|
||||||
in it ever stops working, cycle the view selector once (that reloads the frame),
|
> removed — the backend does the same job better and in the editor itself. An
|
||||||
or use the "edit in the pad" button and work in the pad's own tab, where the
|
> old `?etherpad=` link now shows a note pointing here rather than silently
|
||||||
cookie does apply. See `docs/DECISIONS.md` D31 and D32.
|
> doing nothing; see `docs/DECISIONS.md` D78.
|
||||||
|
|
||||||
### Running it locally
|
### Running it locally
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-26
|
## 2026-08-26
|
||||||
|
|
||||||
|
- The Etherpad integration is gone: collaboration now runs through a Werkbaum backend, and an old `?etherpad=` link says so instead of doing nothing
|
||||||
- Plans can live on a Werkbaum backend now: open `?live=<document URL>` and everyone edits the same text, seeing each other's changes without reloading
|
- Plans can live on a Werkbaum backend now: open `?live=<document URL>` and everyone edits the same text, seeing each other's changes without reloading
|
||||||
- When two people change the same lines, a bar asks whose version should win — everything else the server merges by itself
|
- When two people change the same lines, a bar asks whose version should win — everything else the server merges by itself
|
||||||
- The caret stays where you put it when someone else inserts lines above you
|
- The caret stays where you put it when someone else inserts lines above you
|
||||||
|
|||||||
@@ -6660,3 +6660,87 @@ Version), `frontend info` (1.1.162, Commit = HEAD), `backend status`,
|
|||||||
`backend restart`, `backend backup` samt lokalem Wiederanlauf,
|
`backend restart`, `backend backup` samt lokalem Wiederanlauf,
|
||||||
`frontend preview` (nichts geschrieben, HEAD unverändert) und die
|
`frontend preview` (nichts geschrieben, HEAD unverändert) und die
|
||||||
Fehlerpfade (Exit-Code 2 bei unbekanntem Ziel und fehlender Aktion).
|
Fehlerpfade (Exit-Code 2 bei unbekanntem Ziel und fehlender Aktion).
|
||||||
|
|
||||||
|
## D78 — Die Etherpad-Anbindung ist ausgebaut
|
||||||
|
D31 lieh sich für die Echtzeit-Zusammenarbeit ein **Etherpad**: Das Pad war die
|
||||||
|
Schreibfläche, Werkbaum die Ansicht. Das war die richtige Entscheidung für den
|
||||||
|
Zeitpunkt — die schwere Arbeit (gleichzeitige Änderungen zusammenführen) war
|
||||||
|
dort seit Jahren getan, und Werkbaum hatte kein Backend. **Jetzt hat es eins**
|
||||||
|
(D76), und das kann dasselbe besser und im Editor selbst. Also raus damit,
|
||||||
|
statt zwei Wege zur selben Sache zu pflegen.
|
||||||
|
|
||||||
|
**D31 bleibt stehen** — Entscheidungen werden nicht gelöscht, sondern
|
||||||
|
fortgeschrieben. Was dort gemessen wurde, gilt weiter und ist der Grund, warum
|
||||||
|
die Anbindung nie gut wurde: Etherpad **drosselt den Export** (serienmäßig 10
|
||||||
|
Abrufe je 90 s und IP), ein Hintergrund-Takt erzeugt die Drosselung, statt sie
|
||||||
|
zu umgehen; das Autoren-Cookie ist `SameSite=Lax` und kommt im eingebetteten
|
||||||
|
Rahmen nicht an, also ist man bei jedem Laden ein neuer Autor; und ein eigener
|
||||||
|
Socket zum Pad wurde von der Gegenseite abgelehnt (D31-Nachtrag, Code 1006).
|
||||||
|
Übrig blieb ein Neu-laden-Knopf und ein schreibgeschütztes Textfeld — die
|
||||||
|
Rückmeldung „funktioniert sowieso nicht gut" ist die ehrliche Zusammenfassung.
|
||||||
|
|
||||||
|
**Was `?live=` besser macht**, in derselben Reihenfolge: Es wird **im Editor
|
||||||
|
geschrieben** statt in einem fremden Rahmen; die Gegenrichtung ist ein offener
|
||||||
|
Abruf statt eines Takts gegen ein fremdes Limit; der Konflikt wird benannt
|
||||||
|
statt versteckt; und der Plantext liegt auf **eigener** Infrastruktur statt auf
|
||||||
|
einem Pad, das jeder lesen kann, der die Adresse kennt.
|
||||||
|
|
||||||
|
**Ein alter Link meldet sich, statt still nichts zu tun.** `?etherpad=` bleibt
|
||||||
|
als **erkannter** Parameter stehen und ergibt die zeilenlose Warnung `padGone`,
|
||||||
|
die auf `?live=` zeigt — in allen neun Sprachen. Der Parameter war geteilt: Wer
|
||||||
|
ihn irgendwo stehen hat (in einer Mail, einem Wiki, einem Lesezeichen), bekäme
|
||||||
|
sonst ein leeres Werkbaum ohne Erklärung. Genau der stille Fehler, den dieses
|
||||||
|
Projekt sonst überall ablehnt (SPEC §4, D59). Es ist bewusst **kein**
|
||||||
|
Rest-Feature: Geholt wird nichts, der Pad-Host sieht keine Anfrage mehr
|
||||||
|
(nachgemessen).
|
||||||
|
|
||||||
|
**Vorhandene Pad-Dokumente bleiben liegen — und werden dabei besser.** Sie sind
|
||||||
|
im localStorage gewöhnliche Dokumente (`{id, name, text, source}`); ohne den
|
||||||
|
Schreibschutz sind sie ab jetzt **bearbeitbar**, sammeln frühere Stände (D54)
|
||||||
|
und lassen sich falten. Ihr Text ist der zuletzt geholte. Nichts wird gelöscht,
|
||||||
|
niemand verliert seinen Plan.
|
||||||
|
|
||||||
|
**Der Schreibschutz verschwindet ganz, nicht nur seine Ursache.** `src.readOnly`
|
||||||
|
wurde ausschließlich von Pad-Dokumenten gesetzt; danach hätten sieben Wächter
|
||||||
|
in Falten, Kurz-IDs, Autovervollständigung und Ständen auf eine Bedingung
|
||||||
|
geprüft, die nie mehr wahr wird — mit Kommentaren, die auf D31 zeigen. Tote
|
||||||
|
Wächter mit veralteter Begründung sind schlechter als keine; ein künftiger
|
||||||
|
Lesemodus braucht ohnehin seine eigene Entscheidung. Mit ihm fällt
|
||||||
|
`updateSnapBtn()` weg: Der Knopf ist jetzt immer da.
|
||||||
|
|
||||||
|
**Eine Layout-Ebene weniger.** `#srcArea` gab es nur, damit Pad-Rahmen und
|
||||||
|
Textspiegel sich einen Bereich teilen konnten, ohne die Legenden-Aufteilung
|
||||||
|
(D26) anzufassen. Ohne Rahmen ist es ein Kasten mit einem Kind. Nachgemessen
|
||||||
|
nach dem Entfernen: `.editor-body` trägt jetzt direkt `srcWrap` · `hintGutter` ·
|
||||||
|
`agenda`, und die drei kacheln die Breite exakt (954 + 10 + 300 px) — der
|
||||||
|
Legenden-Splitter arbeitet unverändert.
|
||||||
|
|
||||||
|
**Mit ausgebaut**, weil sie nur der Anbindung dienten: `remote.js` samt seinen
|
||||||
|
Tests (die Pad-Adressen normalisieren), der Ansichts-Wähler und sein Splitter
|
||||||
|
(`--pcol`/`--prow`), der Neu-laden-Knopf samt Drosselungs-Zähler, die Warnungen
|
||||||
|
`sourceTimeout` und `padRateLimit`, elf i18n-Schlüssel × 9 Sprachen und der
|
||||||
|
Timeout-Parameter von `fetchRemote()`. **`?sourceUrl=` bleibt unangetastet**
|
||||||
|
(D23) — beide teilten sich einen Fetch-Pfad, und das war die eine Stelle, an der
|
||||||
|
beim Schneiden nichts verrutschen durfte.
|
||||||
|
|
||||||
|
**Der Plan sagt es auch.** Im mitgelieferten `werkbaum.werkbaum` wird aus dem
|
||||||
|
`#col.pad`-Zweig (14 Knoten samt der ganzen „Update by itself"-Gruppe, die nur
|
||||||
|
Etherpads Grenze umgehen wollte) **ein** verworfener Knoten `[-] #col.pad` mit
|
||||||
|
Begründung — dieselbe Form, in der dort schon `#bld.ghpages` und `#not.store`
|
||||||
|
stehen. Der eine Knoten, der weiterlebt, wandert heraus: `#col.pad.point` wird
|
||||||
|
`#col.point`, denn der gemeinsame Zeigefinger `!!!` gilt jedem geteilten
|
||||||
|
Dokument, nicht nur einem Pad. Danach 196 Knoten, 0 Warnungen.
|
||||||
|
|
||||||
|
**Nebengewinn: Der README bekommt endlich den Abschnitt zu `?live=`.** Den gab
|
||||||
|
es nie — D76 ist gebaut und dokumentiert (SPEC §9, DECISIONS), aber die
|
||||||
|
Einstiegs-Beschreibung stand weiter beim Pad. Das Ausbauen hätte sonst ein Loch
|
||||||
|
hinterlassen; jetzt steht dort in beiden Sprachen, wie man ein Server-Dokument
|
||||||
|
anlegt, teilt und was bei einem Konflikt passiert.
|
||||||
|
|
||||||
|
**Nachgemessen** im Browser: `?etherpad=…` zeigt die Warnung, und im
|
||||||
|
Netzwerk-Mitschnitt geht **keine** Anfrage an den Pad-Host; `?sourceUrl=` lädt
|
||||||
|
unverändert und das Dokument ist beschreibbar (früher: schreibgeschützt); das
|
||||||
|
Textfeld liegt mit dem Zahlenstreifen bündig (1151 + 20 px), die Zeilennummern
|
||||||
|
sitzen auf ihren Höhen, Pfad und Stationen werden gezeichnet; der
|
||||||
|
Legenden-Splitter teilt wie zuvor. 501 Tests (die 24 Pad-Adress-Tests sind mit
|
||||||
|
`remote.js` gegangen, `padGone` ist dazugekommen).
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ Tastendruck ein Commit); und wer bei einem Backend eigentlich was darf
|
|||||||
Zu klären ist das Zusammenspiel mit der Update-Anzeige (D45): Sitzt ein
|
Zu klären ist das Zusammenspiel mit der Update-Anzeige (D45): Sitzt ein
|
||||||
Worker vor der Seite, entscheidet **er**, was ausgeliefert wird — „der
|
Worker vor der Seite, entscheidet **er**, was ausgeliefert wird — „der
|
||||||
laufende Build gegen den, den der Server schickt" ist dann nicht mehr
|
laufende Build gegen den, den der Server schickt" ist dann nicht mehr
|
||||||
dasselbe. Dokumente aus `?sourceUrl=`/`?etherpad=` bleiben naturgemäß
|
dasselbe. Dokumente aus `?sourceUrl=`/`?live=` bleiben naturgemäß
|
||||||
online. Im Plan: `#bld.pwa`.
|
online. Im Plan: `#bld.pwa`.
|
||||||
- Schlagworte `&tag` mit einer Filter-/Hervorheben-Linse im Diagramm: Der Baum
|
- Schlagworte `&tag` mit einer Filter-/Hervorheben-Linse im Diagramm: Der Baum
|
||||||
ist genau eine Zerlegung — ein Tag benennt eine Menge von Knoten quer dazu
|
ist genau eine Zerlegung — ein Tag benennt eine Menge von Knoten quer dazu
|
||||||
|
|||||||
+17
-61
@@ -155,7 +155,7 @@ ab, auch außerhalb seines eigenen Teilbaums.
|
|||||||
|
|
||||||
**Fokusmarke `!!!`** — „schau hier hin": Der Knoten wird im Diagramm
|
**Fokusmarke `!!!`** — „schau hier hin": Der Knoten wird im Diagramm
|
||||||
hervorgehoben und ins Bild geholt (§9). Gedacht für das gemeinsame Arbeiten an
|
hervorgehoben und ins Bild geholt (§9). Gedacht für das gemeinsame Arbeiten an
|
||||||
einem Pad (§9, `?etherpad=`): Weil dort niemand den Cursor der anderen sieht,
|
einem Dokument (§9, `?live=`): Weil dort niemand den Cursor der anderen sieht,
|
||||||
ist eine Marke **im Text** der einzige Weg, auf eine Stelle zu zeigen — und sie
|
ist eine Marke **im Text** der einzige Weg, auf eine Stelle zu zeigen — und sie
|
||||||
hat etwas, das ein Cursor nicht hat: **alle** sehen dieselbe Stelle.
|
hat etwas, das ein Cursor nicht hat: **alle** sehen dieselbe Stelle.
|
||||||
|
|
||||||
@@ -770,59 +770,18 @@ Scheitert das Laden (häufigster Fall: das Ziel sendet keinen
|
|||||||
`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der
|
`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der
|
||||||
bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23.
|
bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23.
|
||||||
|
|
||||||
### Gemeinsam an einem Pad arbeiten (`?etherpad=`)
|
### Kein Etherpad mehr (`?etherpad=`, entfallen)
|
||||||
Für Zusammenarbeit in Echtzeit nimmt der Editor die Adresse eines
|
Der Editor konnte den Notationstext aus einem **Etherpad-Pad** beziehen: Alle
|
||||||
**Etherpad-Pads** — die Adresse, die im Browser steht, ohne Export-Pfad:
|
schrieben im Pad, Werkbaum zeigte das Diagramm dazu. Das ist **ausgebaut** —
|
||||||
`…?etherpad=https://pad.example.org/p/mein-plan`. Werkbaum hängt den
|
`?live=` (unten) kann dasselbe besser, und zwar im Editor selbst. Ein alter
|
||||||
Klartext-Export (`/export/txt`) selbst an; ein versehentlich mitgegebener
|
`?etherpad=`-Link ergibt heute eine **Warnung**, die auf `?live=` zeigt, statt
|
||||||
Export- oder `/timeslider`-Pfad wird abgeschnitten.
|
still nichts zu tun. Siehe D78; die frühere Ausführung steht in D31.
|
||||||
|
|
||||||
- **Das Pad ist die Schreibfläche, Werkbaum die Ansicht.** Alle bearbeiten den
|
|
||||||
Notationstext im Pad, jeder Betrachter sieht das Diagramm mitwachsen. Das
|
|
||||||
Zusammenführen gleichzeitiger Änderungen macht Etherpad; Werkbaum tut es
|
|
||||||
nicht.
|
|
||||||
- Deshalb ist das Textfeld für ein solches Dokument **schreibgeschützt** — ein
|
|
||||||
Knopf in der Editor-Titelzeile öffnet das Pad im neuen Tab. Ohne den Schutz
|
|
||||||
verschwände getippter Text beim nächsten Abruf.
|
|
||||||
- Das Pad kann **im Editor-Panel eingebettet** werden. Ein Wähler in der
|
|
||||||
Titelzeile schaltet reihum zwischen drei Ansichten: **Pad und Text** (beide,
|
|
||||||
durch einen eigenen **Splitter** frei geteilt — Doppelklick setzt zurück),
|
|
||||||
**nur Pad** und **nur Text**. Die Aufteilung wird für nebeneinander und
|
|
||||||
gestapelt getrennt gehalten und bleibt erhalten.
|
|
||||||
- Der schmal gezogene Textspiegel behält seinen Zweck: Der Sprung zwischen
|
|
||||||
Diagramm und Text (§9) arbeitet auf ihm. In „nur Pad" ist er ausgeblendet —
|
|
||||||
ein Sprung holt ihn dann selbst zurück, so wie er ein zugeklapptes
|
|
||||||
Editor-Panel aufklappt.
|
|
||||||
- Der Rahmen wird **nur geladen, wenn er sichtbar ist**. Ein geladenes Pad
|
|
||||||
verbindet sich und zeigt dich in dessen Anwesenden-Liste; „nur Text" ist
|
|
||||||
damit die Ansicht, die nichts von dir verrät.
|
|
||||||
- Geholt wird **auf Knopfdruck**, nicht selbsttätig: Ein Neu-laden-Knopf neben
|
|
||||||
dem Pad-Knopf holt den aktuellen Stand. Etherpad **drosselt** den Export
|
|
||||||
(serienmäßig 10 Abrufe je 90 s und IP); ein Hintergrund-Takt läuft dagegen an
|
|
||||||
und bekommt am Ende gar nichts mehr. Läuft ein Abruf, dreht das Symbol — bei
|
|
||||||
gedrosselter Gegenseite kann das bis zum Abbruch (20 s) dauern.
|
|
||||||
- Der Knopf greift gut mit „Was ist neu?" (§9) zusammen: drücken, und was seither
|
|
||||||
in Produktion ging, leuchtet auf.
|
|
||||||
- **Name ist die vollständige Pad-URL** (nicht der bloße Pad-Name — zwei Pads
|
|
||||||
gleichen Namens auf verschiedenen Hosts wären sonst nicht zu unterscheiden),
|
|
||||||
wie bei `?sourceUrl=`. Identität und Name leiten sich von der **Pad**-Adresse
|
|
||||||
ab, nicht von der Export-Adresse — derselbe Pad ergibt damit genau ein
|
|
||||||
Dokument, gleich in welcher Schreibweise der Link kam.
|
|
||||||
- `?sourceUrl=` bleibt unverändert: statische Datei, einmal pro Laden geholt.
|
|
||||||
Der eigene Parameter trägt gerade den Unterschied.
|
|
||||||
- Fehler (CORS, 404, Netz) melden sich wie bei `?sourceUrl=`. Ein **Abbruch**
|
|
||||||
wegen Zeitablauf bekommt eine eigene Meldung, die die Drosselung benennt —
|
|
||||||
die `?sourceUrl=`-Meldung zeigt auf CORS und schickte hier auf die falsche
|
|
||||||
Fährte. Scheitert schon der erste Abruf, bleibt der Neu-laden-Knopf sichtbar
|
|
||||||
und holt es nach; ein Neuladen der Seite ist nicht nötig.
|
|
||||||
|
|
||||||
Siehe D31.
|
|
||||||
|
|
||||||
### Gemeinsam an einem Server-Dokument arbeiten (`?live=`)
|
### Gemeinsam an einem Server-Dokument arbeiten (`?live=`)
|
||||||
Liegt der Plan auf einem Werkbaum-Backend, nimmt der Editor dessen
|
Liegt der Plan auf einem Werkbaum-Backend, nimmt der Editor dessen
|
||||||
Dokument-Adresse: `…?live=https://example.org/api/v1/documents/<uuid>`.
|
Dokument-Adresse: `…?live=https://example.org/api/v1/documents/<uuid>`.
|
||||||
Anders als beim Pad (`?etherpad=`) wird hier **im Editor selbst geschrieben**,
|
Geschrieben wird **im Editor selbst**, und alle sehen die Änderungen der
|
||||||
und alle sehen die Änderungen der anderen, ohne neu zu laden.
|
anderen, ohne neu zu laden.
|
||||||
|
|
||||||
- **Das Textfeld bleibt beschreibbar.** Nach kurzer Ruhe (1,5 s) schickt der
|
- **Das Textfeld bleibt beschreibbar.** Nach kurzer Ruhe (1,5 s) schickt der
|
||||||
Editor die Änderung als Zeilen-Diff; ein offener Abruf hält die Gegenrichtung
|
Editor die Änderung als Zeilen-Diff; ein offener Abruf hält die Gegenrichtung
|
||||||
@@ -852,8 +811,7 @@ Ausrichtungen getrennt erhalten. Die Legende belegt höchstens 85 % des Panels,
|
|||||||
damit das Textfeld nie ganz verschwindet. Siehe D26.
|
damit das Textfeld nie ganz verschwindet. Siehe D26.
|
||||||
|
|
||||||
### Was ist neu? (Dokumente von außen)
|
### Was ist neu? (Dokumente von außen)
|
||||||
Bei Dokumenten, die von außen kommen (mitgeliefert, per `?sourceUrl=` oder
|
Bei Dokumenten, die von außen kommen (mitgeliefert oder per `?sourceUrl=`), wird
|
||||||
`?etherpad=`), wird
|
|
||||||
gezeigt, was sich seit dem letzten Besuch getan hat. **„Neu" heißt: neu in
|
gezeigt, was sich seit dem letzten Besuch getan hat. **„Neu" heißt: neu in
|
||||||
Produktion** — ein Knoten trägt jetzt `[^]` und tat es in der zuletzt gesehenen
|
Produktion** — ein Knoten trägt jetzt `[^]` und tat es in der zuletzt gesehenen
|
||||||
Fassung nicht. Solche Knoten bekommen einen **gelben Strahlenkranz** nach außen
|
Fassung nicht. Solche Knoten bekommen einen **gelben Strahlenkranz** nach außen
|
||||||
@@ -1018,8 +976,7 @@ eigene ID der Zeile.
|
|||||||
weiter: Sie fängt keine Taste ab, solange sie nichts anzeigt, und Tab rückt
|
weiter: Sie fängt keine Taste ab, solange sie nichts anzeigt, und Tab rückt
|
||||||
bei geschlossener Liste unverändert ein.
|
bei geschlossener Liste unverändert ein.
|
||||||
- **Kein Vorschlag** bei bloßem `#` (das *definiert* eine ID), im Kommentar
|
- **Kein Vorschlag** bei bloßem `#` (das *definiert* eine ID), im Kommentar
|
||||||
(`%%`), im Beschreibungsteil hinter `---` und in schreibgeschützten
|
(`%%`) und im Beschreibungsteil hinter `---`.
|
||||||
Pad-Dokumenten (§9).
|
|
||||||
- Der Parser sieht nie etwas davon; `llms.md` (§13) bleibt unberührt. Für
|
- Der Parser sieht nie etwas davon; `llms.md` (§13) bleibt unberührt. Für
|
||||||
Screenreader meldet eine höfliche Live-Region die Trefferzahl und den
|
Screenreader meldet eine höfliche Live-Region die Trefferzahl und den
|
||||||
gewählten Eintrag; das Popup selbst ist `aria-hidden`, normales Tippen
|
gewählten Eintrag; das Popup selbst ist `aria-hidden`, normales Tippen
|
||||||
@@ -1099,13 +1056,12 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
|
|||||||
angefasst, solange die übrigen Marken den Zustand noch richtig beschreiben —
|
angefasst, solange die übrigen Marken den Zustand noch richtig beschreiben —
|
||||||
ein von Hand gesetztes `<` bleibt also stehen. Trifft es nicht mehr zu, werden
|
ein von Hand gesetztes `<` bleibt also stehen. Trifft es nicht mehr zu, werden
|
||||||
alle Marken neu gesetzt und das `<` dabei aufgelöst.
|
alle Marken neu gesetzt und das `<` dabei aufgelöst.
|
||||||
- Wo der Text **nicht beschreibbar** ist — bei einem Pad-Dokument (§9) —, gilt
|
- Lässt sich ein Zustand in Marken **gar nicht ausdrücken** (etwa weil eine
|
||||||
der Eingriff **je Knoten** (Identität = Label-Pfad, wie bei „Was ist neu?“)
|
Fokusmarke `!!!` ihren Knoten immer wieder hervorholt), wird lieber nichts
|
||||||
und **nur für die Sitzung**; ein Dokumentwechsel setzt ihn zurück. Dasselbe
|
geschrieben, als einen Text zu hinterlassen, der etwas anderes sagt als das
|
||||||
gilt für einen Zustand, der sich in Marken gar nicht ausdrücken lässt (etwa
|
Bild. Der Eingriff gilt dann **je Knoten** (Identität = Label-Pfad, wie bei
|
||||||
weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird
|
„Was ist neu?“) und **nur für die Sitzung**; ein Dokumentwechsel setzt ihn
|
||||||
lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes
|
zurück.
|
||||||
sagt als das Bild.
|
|
||||||
- **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Durchschalter**:
|
- **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Durchschalter**:
|
||||||
Jeder Druck stellt die **nächste** von vier Voreinstellungen her, reihum —
|
Jeder Druck stellt die **nächste** von vier Voreinstellungen her, reihum —
|
||||||
1. **ab Größe M abwärts zugeklappt**: jeder Knoten mit Kindern, dessen
|
1. **ab Größe M abwärts zugeklappt**: jeder Knoten mit Kindern, dessen
|
||||||
|
|||||||
+16
-115
@@ -131,30 +131,10 @@
|
|||||||
| [?] #be.auth.token: Single user, one token (S)
|
| [?] #be.auth.token: Single user, one token (S)
|
||||||
| [?] #be.auth.oidc: Log in with OIDC (L)
|
| [?] #be.auth.oidc: Log in with OIDC (L)
|
||||||
- [~] #col: Collaborating on a Diagram (XXL)
|
- [~] #col: Collaborating on a Diagram (XXL)
|
||||||
" The pad merges concurrent edits, Werkbaum renders the plan.
|
" Reading together is solved, changing together too — typing together at the
|
||||||
- [^] #col.pad: Watch a shared Etherpad — it merges, we render (L)
|
" same instant is the expensive one.
|
||||||
- [^] #col.pad.export: Load the pad's plain-text export (XS)
|
- [^] #col.point: A shared pointer: !!! focuses a node for everyone (XS)
|
||||||
- [^] #col.pad.reload: Reload on demand (XS) %% a timer only trips Etherpad's rate limit
|
- [-] #col.pad: Borrow an Etherpad — it merges, we render (L) %% built, then removed, see D78
|
||||||
- [^] #col.pad.embed: Embed the pad, keep the text mirror beside it (M)
|
|
||||||
- [^] #col.pad.views: Three views: pad and text, pad only, text only (S)
|
|
||||||
- [^] #col.pad.split: Splitter between pad and mirror (XS)
|
|
||||||
- [?] #col.pad.cookie: Editing in the frame stops after a while (S) %% SameSite=Lax, see D31
|
|
||||||
| [?] #col.pad.samesite: Ask the admin for cookie.sameSite = None (XS)
|
|
||||||
| [ ] #col.pad.owntab: Live with it: edit in the pad's own tab (XS)
|
|
||||||
- [^] #col.pad.point: A shared pointer: !!! focuses a node for everyone (XS)
|
|
||||||
- [?] > #col.auto: Update by itself, within about 2 s (M)
|
|
||||||
| [?] #col.own: Our own Etherpad beside Werkbaum (L) %% then every setting below is ours
|
|
||||||
- [ ] #col.own.db: Node and a real database in userland (S) %% the flatfile store is not for production
|
|
||||||
- [ ] #col.own.ram: Book the RAM quota for an own daemon (XS)
|
|
||||||
- [ ] #col.own.proxy: Reverse proxy in .htaccess, websockets included (S) %% Mattermost does it, wiki shows how
|
|
||||||
- [ ] #col.own.unit: systemd user unit so it survives a reboot (XS)
|
|
||||||
- [!] #col.own.patch: Keeping a public service patched (S) %% indefinitely, that is the real cost
|
|
||||||
| [?] #col.auto.limit: Admin raises the export rate limit (XS) %% default is 10 per 90 s
|
|
||||||
| [?] #col.auto.plugin: Admin installs a plugin that pushes the text (S) %% postMessage to the page
|
|
||||||
| [?] #col.auto.relay: Our own backend holds one socket and fans out (XXL) :#be.scaffold
|
|
||||||
| [ ] #col.auto.poll: Poll inside the budget, at best every 9 s (S) %% legal, but not 2 s
|
|
||||||
| [-] #col.auto.socket: Our own socket straight to the pad (L) %% measured: upgrade refused
|
|
||||||
| [-] #col.auto.html: Read the pad's HTML instead of the export (M) %% measured: no CORS header
|
|
||||||
- [ ] #col.git: Git as the shared store (L)
|
- [ ] #col.git: Git as the shared store (L)
|
||||||
| [ ] #col.git.pr: A file in a repository, changed by pull request (S) %% works today, no code
|
| [ ] #col.git.pr: A file in a repository, changed by pull request (S) %% works today, no code
|
||||||
| [?] #col.git.auto: The backend commits every change (L) :#be.scaffold
|
| [?] #col.git.auto: The backend commits every change (L) :#be.scaffold
|
||||||
@@ -714,8 +694,8 @@
|
|||||||
#bld.pwa.sw
|
#bld.pwa.sw
|
||||||
A worker lets the editor start without a network. It is deliberately dumb:
|
A worker lets the editor start without a network. It is deliberately dumb:
|
||||||
navigations go network-first, and the cache only keeps the last version it
|
navigations go network-first, and the cache only keeps the last version it
|
||||||
has seen of the one self-contained file for the offline case. Texts pulled from a URL
|
has seen of the one self-contained file for the offline case. Texts pulled from
|
||||||
or a pad pass through untouched and stay online by their nature.
|
a URL or a server pass through untouched and stay online by their nature.
|
||||||
|
|
||||||
#bld.pwa.update
|
#bld.pwa.update
|
||||||
The feared migration turned out unnecessary: because the worker only
|
The feared migration turned out unnecessary: because the worker only
|
||||||
@@ -838,96 +818,17 @@
|
|||||||
machinery. Reading together is solved, changing together mostly is, and
|
machinery. Reading together is solved, changing together mostly is, and
|
||||||
typing together at the same instant is the expensive one.
|
typing together at the same instant is the expensive one.
|
||||||
|
|
||||||
|
#col.point
|
||||||
|
Nobody sees anyone else's caret, so a mark in the text is the only way to
|
||||||
|
point at a place — and it has something a caret has not: everyone sees the
|
||||||
|
same spot.
|
||||||
|
|
||||||
#col.pad
|
#col.pad
|
||||||
Let an Etherpad do the hard part: it merges concurrent edits, and Werkbaum
|
Removed. An Etherpad did the hard part for a while: it merged concurrent
|
||||||
only renders the result. Borrowed rather than rebuilt.
|
edits and Werkbaum only rendered the result, borrowed rather than rebuilt.
|
||||||
|
The borrowing is what it cost — a rate-limited export, no writing from here,
|
||||||
#col.pad.export
|
and an author cookie that a foreign frame never gets. The backend does it
|
||||||
Every pad offers its content as plain text, with the header that lets a
|
better and in our own editor, so this went out again.
|
||||||
foreign page read it. That one endpoint is the entire integration.
|
|
||||||
|
|
||||||
#col.pad.reload
|
|
||||||
Fetching happens on a button press, not on a timer. The export is rate
|
|
||||||
limited, and a timer does nothing but trip it.
|
|
||||||
|
|
||||||
#col.pad.embed
|
|
||||||
The pad in a frame inside the editor, with our own text mirror beside it.
|
|
||||||
The mirror keeps the jumps between diagram and text working, because a
|
|
||||||
foreign frame gives up no access to its content.
|
|
||||||
|
|
||||||
#col.pad.views
|
|
||||||
Pad and text, pad only, text only. Text only is the view that tells the pad
|
|
||||||
nothing about you — a loaded pad announces you in its list of people.
|
|
||||||
|
|
||||||
#col.pad.split
|
|
||||||
A splitter of its own between frame and mirror, remembered separately for
|
|
||||||
side by side and stacked.
|
|
||||||
|
|
||||||
#col.pad.cookie
|
|
||||||
In a foreign frame the pad's author cookie is not sent, so a session can
|
|
||||||
turn read-only after a while. Not fixable from this side.
|
|
||||||
|
|
||||||
#col.pad.samesite
|
|
||||||
One server setting resolves it. Presumes somebody who can change that
|
|
||||||
setting.
|
|
||||||
|
|
||||||
#col.pad.owntab
|
|
||||||
Or live with it: read along in the frame, and write in the pad's own tab
|
|
||||||
where the cookie works.
|
|
||||||
|
|
||||||
#col.pad.point
|
|
||||||
Nobody sees anybody else's cursor across two applications, so a mark in the
|
|
||||||
text is the only way to point at something. It has one thing a cursor
|
|
||||||
hasn't: everyone sees the same spot.
|
|
||||||
|
|
||||||
#col.auto
|
|
||||||
The view should follow the pad by itself, within a couple of seconds. Every
|
|
||||||
route below was measured rather than guessed at.
|
|
||||||
|
|
||||||
#col.own
|
|
||||||
Running the pad ourselves makes every setting below it ours. It also makes
|
|
||||||
every one of them our job.
|
|
||||||
|
|
||||||
#col.own.db
|
|
||||||
The default file store is not meant for production, so a real database and a
|
|
||||||
runtime to host it are the price of admission.
|
|
||||||
|
|
||||||
#col.own.ram
|
|
||||||
A daemon of one's own needs memory booked for it.
|
|
||||||
|
|
||||||
#col.own.proxy
|
|
||||||
Reaching it needs a reverse proxy that passes websockets through as well.
|
|
||||||
|
|
||||||
#col.own.unit
|
|
||||||
And something that starts it again after a reboot.
|
|
||||||
|
|
||||||
#col.own.patch
|
|
||||||
The real cost is not the setup but keeping a public service patched,
|
|
||||||
indefinitely.
|
|
||||||
|
|
||||||
#col.auto.limit
|
|
||||||
The cheapest effective change: raise the export limit on the pad server. Ten
|
|
||||||
fetches per ninety seconds is the default, and the reason a timer fails.
|
|
||||||
|
|
||||||
#col.auto.plugin
|
|
||||||
A pad plugin can push the text to the embedding page. Live, no polling, no
|
|
||||||
limit touched — and nothing to install on our side.
|
|
||||||
|
|
||||||
#col.auto.relay
|
|
||||||
Our own backend holds one connection to the pad and fans out to every
|
|
||||||
viewer. Solves it for everybody at once, and costs a backend.
|
|
||||||
|
|
||||||
#col.auto.poll
|
|
||||||
Staying inside the budget means one fetch every nine seconds at best, shared
|
|
||||||
by everyone behind the same address. Legal, but not two seconds.
|
|
||||||
|
|
||||||
#col.auto.socket
|
|
||||||
Measured and rejected: the pad server refuses the websocket upgrade when it
|
|
||||||
comes from another origin.
|
|
||||||
|
|
||||||
#col.auto.html
|
|
||||||
Measured and rejected: the pad page carries the full text, but sends no
|
|
||||||
header that would let a foreign page read it.
|
|
||||||
|
|
||||||
#col.git
|
#col.git
|
||||||
Keep the plan in a repository. Because it is text, diff, blame, review and
|
Keep the plan in a repository. Because it is text, diff, blame, review and
|
||||||
|
|||||||
+21
-53
@@ -76,7 +76,7 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
(verworfene einblenden, Pfad an/aus) als **Parameter** — keine Globals; nur
|
(verworfene einblenden, Pfad an/aus) als **Parameter** — keine Globals; nur
|
||||||
`cheapPathOn` lebt als UI-State in `app.js`. Tests: `tests/*.test.js`.
|
`cheapPathOn` lebt als UI-State in `app.js`. Tests: `tests/*.test.js`.
|
||||||
- **Was entscheidbar ist, gehört in ein eigenes Modul** — auch bei Features, die
|
- **Was entscheidbar ist, gehört in ein eigenes Modul** — auch bei Features, die
|
||||||
wie reine UI aussehen: `remote.js` (Pad-URLs normalisieren, D31),
|
wie reine UI aussehen:
|
||||||
`live.js` (Server-Dokumente: Adressen, Zeilen-Diff, Cursor-Rechnung, wann eine
|
`live.js` (Server-Dokumente: Adressen, Zeilen-Diff, Cursor-Rechnung, wann eine
|
||||||
Feed-Antwort angewendet werden darf — D76),
|
Feed-Antwort angewendet werden darf — D76),
|
||||||
`warnings.js` (Warnung → Text), `snapshots.js` (frühere Stände: wann entsteht
|
`warnings.js` (Warnung → Text), `snapshots.js` (frühere Stände: wann entsteht
|
||||||
@@ -201,47 +201,18 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
Endung `.werkbaum` ist reine Konvention (D24, SPEC §12). Beispieldateien zum
|
Endung `.werkbaum` ist reine Konvention (D24, SPEC §12). Beispieldateien zum
|
||||||
Ausprobieren: `docs/examples/*.werkbaum` (nacheinander geöffnet ergeben sie
|
Ausprobieren: `docs/examples/*.werkbaum` (nacheinander geöffnet ergeben sie
|
||||||
mehrere Dokumente im Wähler).
|
mehrere Dokumente im Wähler).
|
||||||
- `?etherpad=` (D31): `remoteSource()` liefert **einen** Beschreiber für beide
|
- **Etherpad ist ausgebaut (D78).** `?etherpad=` bleibt als **erkannter**
|
||||||
Eingänge (`?sourceUrl=` und `?etherpad=`), `loadRemoteSource()` holt und legt
|
Parameter stehen und ergibt die zeilenlose Warnung `padGone`, die auf `?live=`
|
||||||
das Dokument an — ein Fetch-Pfad, ein Warnkanal. Die Normalisierung der
|
zeigt — ein geteilter Link soll nicht still ins Leere laufen. Geholt wird
|
||||||
Pad-Adresse steht headless in **`remote.js`** (`padUrls`, Tests in
|
nichts. Mit gegangen sind `remote.js`, der Ansichts-Wähler samt Splitter
|
||||||
`tests/remote.test.js`): Export-/Timeslider-Pfad, Query, Fragment und
|
(`--pcol`/`--prow`), der Neu-laden-Knopf, die Warnungen `sourceTimeout`/
|
||||||
Schrägstriche fallen weg, verlangt wird `/p/<name>` am Ende. Name und id sind
|
`padRateLimit` und **der Schreibschutz**: `src.readOnly` wurde nur von
|
||||||
die **vollständige** Pad-URL (Pad-Namen sind nur pro Instanz eindeutig).
|
Pad-Dokumenten gesetzt, also sind die Wächter in Falten, Kurz-IDs,
|
||||||
`padSource` muss **vor** dem ersten Abruf und vor `loadActiveIntoEditor()`
|
Autovervollständigung und Ständen weg (und mit ihnen `updateSnapBtn()`).
|
||||||
gesetzt werden: Es trägt den Schreibschutz, und scheitert der erste Abruf
|
Wer einen Lesemodus wieder einführt, braucht dafür eine eigene Entscheidung.
|
||||||
(Normalfall bei dieser Gegenseite), bleibt so der Neu-laden-Knopf erreichbar
|
Ebenfalls weg: `#srcArea` — es gab den Kasten nur, damit sich Pad-Rahmen und
|
||||||
statt das Dokument tot liegen zu lassen. Geholt wird **nur auf Knopfdruck**
|
Spiegel einen Bereich teilen; `.editor-body` trägt jetzt direkt
|
||||||
(`refreshPad`) — **kein Hintergrund-Takt wieder einbauen**: Etherpads
|
`srcWrap` · `hintGutter` · `agenda`.
|
||||||
`importExportRateLimiting` lässt serienmäßig 10 Abrufe je 90 s und IP zu, und
|
|
||||||
jenseits davon hält die Gegenseite die Verbindung ohne Antwort offen (kein
|
|
||||||
`429`), bis der eigene Abbruch sie abreißt — gemessen: zwei Minuten tot, danach
|
|
||||||
0,4 s. Ein Takt gewinnt dagegen nicht, er erzeugt die Drosselung. Deshalb auch
|
|
||||||
`setPadBusy()` (Symbol dreht, bis zu 20 s) und ein **eigener** Warnungstyp
|
|
||||||
`sourceTimeout` für den Abbruch — `sourceLoad` zeigt auf CORS und schickte hier
|
|
||||||
auf die falsche Fährte. `deleteDoc()` ruft `stopPad()`, sonst legte ein späterer
|
|
||||||
Abruf das gelöschte Dokument wieder an und aktivierte es auch gleich.
|
|
||||||
Beim Prüfen im Vorschau-Browser: HMR lädt bei jeder Quelländerung neu, und ein
|
|
||||||
Reload holt den Text beim Erstabruf — sieht wie eine geglückte Übernahme aus.
|
|
||||||
Marker auf `window` setzen und hinterher prüfen, sonst beweist der Test nichts.
|
|
||||||
- Pad einbetten, drei Ansichten (D31): `padView` ('both'|'pad'|'text') +
|
|
||||||
`applyPadView()`. Der Rahmen bekommt **nur sichtbar** ein `src`, sonst
|
|
||||||
`about:blank` — ein geladenes Pad verbindet sich per Socket und zeigt dich in
|
|
||||||
dessen Anwesenden-Liste. `PAD_VIEWS`/`padView` stehen **oben** bei
|
|
||||||
`cheapPathOn`, nicht beim übrigen Pad-Code: `saveUI()` liest sie und kann schon
|
|
||||||
aus `applySplit()` heraus laufen, weiter unten stünden sie noch in der
|
|
||||||
temporalen Todeszone. Der Splitter Pad|Spiegel (`#padGutter`, `--pcol`/`--prow`)
|
|
||||||
ist eine Kopie des Legenden-Splitters (D26) samt getrennter Persistenz.
|
|
||||||
`revealEditor()` schaltet 'pad' → 'both', sonst zeigte der Sprung (D25) ins
|
|
||||||
Nichts. Das Markup: `#srcArea` umschließt Rahmen + Splitter + `#src`, damit die
|
|
||||||
Legenden-Aufteilung unberührt bleibt — `.editor-body textarea` ist ein
|
|
||||||
Nachfahren-Selektor und greift weiter. Nebenwirkung, die als Behelf taugt:
|
|
||||||
Einmal durch die Ansichten schalten **lädt den Rahmen neu** (über
|
|
||||||
`about:blank`) — das ist der Ausweg, wenn ein eingebettetes Pad nicht mehr
|
|
||||||
beschreibbar ist. Nicht in den Neu-laden-Knopf legen: der zieht nach dem Tippen
|
|
||||||
Spiegel und Diagramm nach, und ein Rahmen-Reload dabei kostete jedes Mal die
|
|
||||||
Schreibmarke im Pad. **Umhängen im DOM lädt jeden `<iframe>` neu** — wer
|
|
||||||
Container um `#srcArea` herum neu aufbaut, wirft die Pad-Sitzung weg.
|
|
||||||
- Fokusmarke `!!!` (D32, SPEC §1): Parser setzt `focus`, Renderer die Klasse
|
- Fokusmarke `!!!` (D32, SPEC §1): Parser setzt `focus`, Renderer die Klasse
|
||||||
`focusmark`, CSS gibt ihr einen **eigenen Strahlenkranz in Petrol** (Nachtrag zu
|
`focusmark`, CSS gibt ihr einen **eigenen Strahlenkranz in Petrol** (Nachtrag zu
|
||||||
D32; der geteilte Ring mit `.current` war zu leise). Schein in **hellem** Teal
|
D32; der geteilte Ring mit `.current` war zu leise). Schein in **hellem** Teal
|
||||||
@@ -253,7 +224,7 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
Lookbehind (Safari erst ab 16.4), sonst verlöre `Achtung!!!` seine
|
Lookbehind (Safari erst ab 16.4), sonst verlöre `Achtung!!!` seine
|
||||||
Ausrufezeichen. Extraktion **nach** den Tags; der Kommentar fällt vorher weg,
|
Ausrufezeichen. Extraktion **nach** den Tags; der Kommentar fällt vorher weg,
|
||||||
eine Marke hinter `%%` wirkt also nicht. `revealFocusMark()` scrollt **nur bei
|
eine Marke hinter `%%` wirkt also nicht. `revealFocusMark()` scrollt **nur bei
|
||||||
Änderung** (Schlüssel = Label-Text, nicht Zeilennummer — Umsortieren im Pad soll
|
Änderung** (Schlüssel = Label-Text, nicht Zeilennummer — Umsortieren soll
|
||||||
nicht neu scrollen), sonst zöge jeder Neubau den Blick zurück. Die Regel braucht
|
nicht neu scrollen), sonst zöge jeder Neubau den Blick zurück. Die Regel braucht
|
||||||
den `#out`-Präfix wie `.current` (`ul.or .node{box-shadow:none}` ist
|
den `#out`-Präfix wie `.current` (`ul.or .node{box-shadow:none}` ist
|
||||||
spezifischer) **und** die Ausnahme
|
spezifischer) **und** die Ausnahme
|
||||||
@@ -317,10 +288,9 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
diesen Versatz (`drop`) nach oben, sonst stehen sie durchgehend zu tief.
|
diesen Versatz (`drop`) nach oben, sonst stehen sie durchgehend zu tief.
|
||||||
Aufgerufen wird aus `render()` (dort kommt auch die Warnungsmenge her) und aus
|
Aufgerufen wird aus `render()` (dort kommt auch die Warnungsmenge her) und aus
|
||||||
einem `ResizeObserver` auf `#src` (Umbruch hängt an der Breite). Markup:
|
einem `ResizeObserver` auf `#src` (Umbruch hängt an der Breite). Markup:
|
||||||
`#srcWrap` umschließt Streifen + Textfeld; die Pad-Regeln (D31) teilen gegen
|
`#srcWrap` umschließt Streifen + Textfeld. `caretLine` steht **oben** bei den
|
||||||
**diese Spalte** (`.src-area.pv-both>.src-wrap`), nicht gegen das nackte
|
übrigen Ansichts-Variablen — der Streifen liest sie und hängt an `render()`
|
||||||
Textfeld. `caretLine` steht deshalb **oben** bei `padView` — der Streifen liest
|
(temporale Todeszone).
|
||||||
sie und hängt an `render()` (temporale Todeszone, siehe D31).
|
|
||||||
- Touch-Langdruck (D25): Der Timer (500 ms) setzt **nur** die Klasse `armed`
|
- Touch-Langdruck (D25): Der Timer (500 ms) setzt **nur** die Klasse `armed`
|
||||||
(Petrol-Ring); `jumpToLine()` läuft im `touchend`-Handler. Nicht zurück in den
|
(Petrol-Ring); `jumpToLine()` läuft im `touchend`-Handler. Nicht zurück in den
|
||||||
Timer verlegen — **`focus()` aus einem Timer gilt in mobilen Browsern nicht als
|
Timer verlegen — **`focus()` aus einem Timer gilt in mobilen Browsern nicht als
|
||||||
@@ -396,10 +366,8 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
`setRangeText` zerstören die Undo-Historie (gemessen). Zwei Fallen: Ein
|
`setRangeText` zerstören die Undo-Historie (gemessen). Zwei Fallen: Ein
|
||||||
Textfeld mit `display:none`-Vorfahr nimmt **kein** `execCommand` an (Klasse
|
Textfeld mit `display:none`-Vorfahr nimmt **kein** `execCommand` an (Klasse
|
||||||
`writing-fold` schaltet es kurz sichtbar), und der nötige Fokus zieht auf
|
`writing-fold` schaltet es kurz sichtbar), und der nötige Fokus zieht auf
|
||||||
Mobil die Tastatur hoch (`keyboardOnJump(true)`). Bei `src.readOnly` (Pad,
|
Mobil die Tastatur hoch (`keyboardOnJump(true)`). Die Überlagerungen werden
|
||||||
D31) wird nicht geschrieben, dort trägt weiter `foldOverrides` — die
|
nach erfolgreichem Schreiben geleert, sonst maskieren sie den Text. Der **Umschalter** im Diagramm-Kopf (`applyFoldPreset`, D44)
|
||||||
Überlagerungen werden nach erfolgreichem Schreiben geleert, sonst maskieren
|
|
||||||
sie den Text. Der **Umschalter** im Diagramm-Kopf (`applyFoldPreset`, D44)
|
|
||||||
geht denselben Weg: Überlagerungen für ALLE faltbaren Knoten setzen
|
geht denselben Weg: Überlagerungen für ALLE faltbaren Knoten setzen
|
||||||
(`atMostM(n)` bzw. `false`), dann `writeAllFoldMarks()` — der aus
|
(`atMostM(n)` bzw. `false`), dann `writeAllFoldMarks()` — der aus
|
||||||
`writeFoldToText()` herausgelöste Voll-Rewrite; einen minimalen Patch gibt es
|
`writeFoldToText()` herausgelöste Voll-Rewrite; einen minimalen Patch gibt es
|
||||||
@@ -551,7 +519,7 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
im Textbereich die Pfad-Linie weg (gemessen). Die Sprünge (D25) schalten den
|
im Textbereich die Pfad-Linie weg (gemessen). Die Sprünge (D25) schalten den
|
||||||
Bereich selbst um: `revealEditor()` auf Text, `focusNodeOfCaret()` aufs
|
Bereich selbst um: `revealEditor()` auf Text, `focusNodeOfCaret()` aufs
|
||||||
Diagramm — **vor** dem Zentrieren. Zustand in `werkbaum-ui` (`mobilePane`),
|
Diagramm — **vor** dem Zentrieren. Zustand in `werkbaum-ui` (`mobilePane`),
|
||||||
Variable oben bei `padView` deklariert (saveUI liest sie).
|
Variable oben deklariert (saveUI liest sie).
|
||||||
`applyLayout` ruft auf Mobil **kein** `applySplit`.
|
`applyLayout` ruft auf Mobil **kein** `applySplit`.
|
||||||
**Inhalte ~25 % kleiner** (D17-Nachtrag 2): `MOBILE_ZOOM = 0.75` als **Faktor**
|
**Inhalte ~25 % kleiner** (D17-Nachtrag 2): `MOBILE_ZOOM = 0.75` als **Faktor**
|
||||||
auf den Nutzer-Zoom — `effZoom()` ist die Wahrheit, die `applyZoom()` setzt und
|
auf den Nutzer-Zoom — `effZoom()` ist die Wahrheit, die `applyZoom()` setzt und
|
||||||
@@ -647,7 +615,7 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
**aktuellen** Dokuments geht deshalb über `execCommand('insertText')`
|
**aktuellen** Dokuments geht deshalb über `execCommand('insertText')`
|
||||||
(`replaceTextUndoable()` für ganze Texte, `writeAt()` für Bereiche).
|
(`replaceTextUndoable()` für ganze Texte, `writeAt()` für Bereiche).
|
||||||
`src.value =` ist nur beim **Laden eines anderen** Dokuments richtig
|
`src.value =` ist nur beim **Laden eines anderen** Dokuments richtig
|
||||||
(`loadActiveIntoEditor`, Dokumentwechsel, Pad-Abruf) — dorthin gibt es nichts
|
(`loadActiveIntoEditor`, Dokumentwechsel, URL-Abruf) — dorthin gibt es nichts
|
||||||
zurückzunehmen.
|
zurückzunehmen.
|
||||||
- **ID-Vorschläge `:#` (D63):** Die Regeln stehen headless in `autocomplete.js`
|
- **ID-Vorschläge `:#` (D63):** Die Regeln stehen headless in `autocomplete.js`
|
||||||
(`depFragment`/`collectIds`/`matchIds`, Tests); app.js verdrahtet nur Popup,
|
(`depFragment`/`collectIds`/`matchIds`, Tests); app.js verdrahtet nur Popup,
|
||||||
|
|||||||
+1
-18
@@ -103,15 +103,6 @@
|
|||||||
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="copybtn padview" id="padViewBtn" data-view="text" hidden>
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect class="frame" x="2.5" y="5" width="19" height="14" rx="2"/><line class="ic-both" x1="12" y1="5" x2="12" y2="19"/><rect class="ic-pad" x="3.5" y="6" width="8" height="12" rx="1" fill="currentColor" stroke="none"/><rect class="ic-text" x="12.5" y="6" width="8" height="12" rx="1" fill="currentColor" stroke="none"/></svg>
|
|
||||||
</button>
|
|
||||||
<a class="copybtn padlink" id="padLink" href="#" target="_blank" rel="noopener" hidden>
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9.5" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
||||||
</a>
|
|
||||||
<button type="button" class="copybtn padrefresh" id="padRefresh" hidden>
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren">
|
<button type="button" class="copybtn" id="copy" data-i18n-title="copyTooltip" data-i18n-aria="copyTooltip" title="Text in die Zwischenablage kopieren" aria-label="Text in die Zwischenablage kopieren">
|
||||||
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
<svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||||
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
||||||
@@ -126,14 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
<!-- Pad-Rahmen + interner Spiegel teilen sich einen eigenen Bereich, damit die
|
<!-- Zeilennummern + Textfeld sind eine Spalte (D33). -->
|
||||||
Legenden-Aufteilung (D26) unberührt bleibt. Reihenfolge: Pad, Splitter,
|
|
||||||
Spiegel — gezogen wird von der Spiegelseite aus, wie bei der Legende. -->
|
|
||||||
<div class="src-area pv-text" id="srcArea">
|
|
||||||
<iframe class="padframe" id="padFrame" title="Etherpad" hidden></iframe>
|
|
||||||
<div class="pad-gutter" id="padGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="padGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Pad und Texteditor größenverändern" hidden></div>
|
|
||||||
<!-- Zeilennummern + Textfeld sind eine Spalte: der Splitter oben teilt den
|
|
||||||
Rahmen gegen diese Spalte, nicht gegen das nackte Textfeld. -->
|
|
||||||
<div class="src-wrap" id="srcWrap">
|
<div class="src-wrap" id="srcWrap">
|
||||||
<div class="lineno" id="lineNos" aria-hidden="true"><div class="lineno-inner"></div></div>
|
<div class="lineno" id="lineNos" aria-hidden="true"><div class="lineno-inner"></div></div>
|
||||||
<!-- `wrap="off"`: Zeilen werden nicht umbrochen, lange Zeilen bekommen
|
<!-- `wrap="off"`: Zeilen werden nicht umbrochen, lange Zeilen bekommen
|
||||||
@@ -142,7 +126,6 @@
|
|||||||
Rand und lässt die Ebene verschwinden. -->
|
Rand und lässt die Ebene verschwinden. -->
|
||||||
<textarea id="src" wrap="off" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea>
|
<textarea id="src" wrap="off" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="hint-gutter" id="hintGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="hintGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Editor und Legende größenverändern" hidden></div>
|
<div class="hint-gutter" id="hintGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="hintGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Editor und Legende größenverändern" hidden></div>
|
||||||
<div class="agenda" id="agenda">
|
<div class="agenda" id="agenda">
|
||||||
<button type="button" class="agenda-summary" id="agendaToggle" aria-expanded="false" aria-controls="hint" data-i18n="agenda">Agenda</button>
|
<button type="button" class="agenda-summary" id="agendaToggle" aria-expanded="false" aria-controls="hint" data-i18n="agenda">Agenda</button>
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ Files are UTF-8 with LF line endings; the extension `.werkbaum` is a
|
|||||||
convention (plain `.txt` works too). The editor at
|
convention (plain `.txt` works too). The editor at
|
||||||
https://werkbaum.javagil.de renders the notation as a diagram;
|
https://werkbaum.javagil.de renders the notation as a diagram;
|
||||||
`?sourceUrl=<url>` loads any CORS-readable http(s) text file, and
|
`?sourceUrl=<url>` loads any CORS-readable http(s) text file, and
|
||||||
`?etherpad=<pad-url>` follows a live Etherpad.
|
`?live=<document-url>` opens a document on a Werkbaum backend that several
|
||||||
|
people edit at once.
|
||||||
|
|
||||||
## Line format
|
## Line format
|
||||||
|
|
||||||
|
|||||||
+52
-427
@@ -3,7 +3,6 @@ import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js';
|
|||||||
import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, lineTargets } from './model.js';
|
import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, lineTargets } from './model.js';
|
||||||
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
|
||||||
import { formatWarning, warningText } from './warnings.js';
|
import { formatWarning, warningText } from './warnings.js';
|
||||||
import { padUrls } from './remote.js';
|
|
||||||
import * as live from './live.js';
|
import * as live from './live.js';
|
||||||
import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js';
|
import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js';
|
||||||
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
|
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
|
||||||
@@ -69,18 +68,14 @@ let showIds = false; /* Knoten-IDs im Diagramm einblenden (D56) */
|
|||||||
Der Faltzustand steht im Text (D38), die Position ist nur der Zeiger darin. */
|
Der Faltzustand steht im Text (D38), die Position ist nur der Zeiger darin. */
|
||||||
const FOLD_CYCLE = ['small', 'path', 'closed', 'open'];
|
const FOLD_CYCLE = ['small', 'path', 'closed', 'open'];
|
||||||
let foldCycleNext = 0, foldCycleApplied = -1;
|
let foldCycleNext = 0, foldCycleApplied = -1;
|
||||||
/* Ansicht bei einem Pad-Dokument (D31): beide | nur Pad | nur Spiegel. Hier oben
|
|
||||||
deklariert, weil saveUI() sie liest und schon aus applySplit() heraus laufen
|
|
||||||
kann — weiter unten stünde sie dann noch in der temporalen Todeszone. */
|
|
||||||
const PAD_VIEWS = ['both', 'pad', 'text'];
|
|
||||||
let padView = 'both';
|
|
||||||
/* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag): dort ist immer
|
/* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag): dort ist immer
|
||||||
genau einer zu sehen. Aus demselben Grund hier oben deklariert wie `padView`
|
genau einer zu sehen. Hier oben deklariert, weil saveUI() ihn liest und schon
|
||||||
— saveUI() liest ihn und läuft schon aus applySplit() heraus. */
|
aus applySplit() heraus laufen kann — weiter unten stünde er dann noch in der
|
||||||
|
temporalen Todeszone. */
|
||||||
let mobilePane = 'diagram';
|
let mobilePane = 'diagram';
|
||||||
/* Cursor-Zeile (D25) — bleibt `null`, bis der Cursor das erste Mal bewegt wurde,
|
/* Cursor-Zeile (D25) — bleibt `null`, bis der Cursor das erste Mal bewegt wurde,
|
||||||
sonst wäre nach dem Laden ungefragt die Wurzel markiert. Hier oben deklariert
|
sonst wäre nach dem Laden ungefragt die Wurzel markiert. Aus demselben Grund
|
||||||
wie `padView`: der Zeilennummern-Streifen (D33) liest sie, und der hängt an
|
hier oben: der Zeilennummern-Streifen (D33) liest sie, und der hängt an
|
||||||
render(). */
|
render(). */
|
||||||
let caretLine = null, currentNodeEl = null;
|
let caretLine = null, currentNodeEl = null;
|
||||||
/* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */
|
/* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */
|
||||||
@@ -242,7 +237,7 @@ function render(){
|
|||||||
/* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt —
|
/* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt —
|
||||||
aber nur, wenn sich die Marke **geändert** hat. Sonst zöge jeder Neubau des
|
aber nur, wenn sich die Marke **geändert** hat. Sonst zöge jeder Neubau des
|
||||||
Baums den Blick zurück und man käme nicht weg. Verglichen wird das Label des
|
Baums den Blick zurück und man käme nicht weg. Verglichen wird das Label des
|
||||||
Knotens, nicht die Zeilennummer: Umsortieren im Pad soll nicht als neue Marke
|
Knotens, nicht die Zeilennummer: Umsortieren soll nicht als neue Marke
|
||||||
gelten. Die Hervorhebung selbst macht die Klasse `focusmark` aus dem Renderer,
|
gelten. Die Hervorhebung selbst macht die Klasse `focusmark` aus dem Renderer,
|
||||||
hier geht es nur ums Scrollen. */
|
hier geht es nur ums Scrollen. */
|
||||||
let lastFocusMark = null;
|
let lastFocusMark = null;
|
||||||
@@ -1074,14 +1069,6 @@ function scrollEditorToOffset(offset){
|
|||||||
|
|
||||||
/* Ist das Editor-Panel zugeklappt, muss der Sprung es erst öffnen. */
|
/* Ist das Editor-Panel zugeklappt, muss der Sprung es erst öffnen. */
|
||||||
function revealEditor(){
|
function revealEditor(){
|
||||||
/* In der Ansicht „nur Pad" (D31) ist der Spiegel ausgeblendet — dann gibt es
|
|
||||||
keine Zeile zum Markieren. Der Sprung holt ihn deshalb zurück, genau wie er
|
|
||||||
ein zugeklapptes Editor-Panel aufklappt (D25). */
|
|
||||||
if(padView === 'pad' && padSource && activeId === padSource.id){
|
|
||||||
padView = 'both';
|
|
||||||
applyPadView();
|
|
||||||
saveUI();
|
|
||||||
}
|
|
||||||
if(isMobile()){
|
if(isMobile()){
|
||||||
/* Auf Mobil ist der Text womöglich gar nicht vorn — dann holt der Sprung
|
/* Auf Mobil ist der Text womöglich gar nicht vorn — dann holt der Sprung
|
||||||
ihn nach vorn, so wie er auf dem Desktop ein zugeklapptes Panel aufklappt
|
ihn nach vorn, so wie er auf dem Desktop ein zugeklapptes Panel aufklappt
|
||||||
@@ -1133,7 +1120,7 @@ function nodeFromEvent(e){
|
|||||||
Umklappen im Diagramm ändert den Notationstext. Der Text ist damit auch für
|
Umklappen im Diagramm ändert den Notationstext. Der Text ist damit auch für
|
||||||
die Faltung die eine Quelle der Wahrheit (D14) — die Sitzungs-Überlagerung
|
die Faltung die eine Quelle der Wahrheit (D14) — die Sitzungs-Überlagerung
|
||||||
`foldOverrides` bleibt nur für Dokumente, in die nicht geschrieben werden
|
`foldOverrides` bleibt nur für Dokumente, in die nicht geschrieben werden
|
||||||
kann (Pad, schreibgeschützt nach D31). */
|
kann. */
|
||||||
|
|
||||||
/* Der Sollzustand als Menge von Label-Pfad-Schlüsseln: heutiger Anfangszustand
|
/* Der Sollzustand als Menge von Label-Pfad-Schlüsseln: heutiger Anfangszustand
|
||||||
aus dem Text, überlagert von den Eingriffen — dieselbe Rechnung wie in
|
aus dem Text, überlagert von den Eingriffen — dieselbe Rechnung wie in
|
||||||
@@ -1222,7 +1209,6 @@ function withEditorWritable(fn){
|
|||||||
/* Schreibt den Faltzustand in den Text. Liefert false, wenn nicht geschrieben
|
/* Schreibt den Faltzustand in den Text. Liefert false, wenn nicht geschrieben
|
||||||
werden konnte — dann bleibt die Sitzungs-Überlagerung stehen. */
|
werden konnte — dann bleibt die Sitzungs-Überlagerung stehen. */
|
||||||
function writeFoldToText(line, collapsed){
|
function writeFoldToText(line, collapsed){
|
||||||
if(src.readOnly) return false; /* Pad-Dokument (D31) */
|
|
||||||
const roots = parse(src.value).roots;
|
const roots = parse(src.value).roots;
|
||||||
if(!roots.length) return false;
|
if(!roots.length) return false;
|
||||||
const want = desiredFoldKeys(roots);
|
const want = desiredFoldKeys(roots);
|
||||||
@@ -1273,8 +1259,8 @@ function applyFoldPreset(mode){
|
|||||||
if(!n.children.length) return; /* nur faltbare Knoten */
|
if(!n.children.length) return; /* nur faltbare Knoten */
|
||||||
foldOverrides.set(key, want.has(n));
|
foldOverrides.set(key, want.has(n));
|
||||||
});
|
});
|
||||||
if(!src.readOnly && writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear();
|
if(writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear();
|
||||||
else render(); /* Pad o. nicht ausdrückbar: Überlagerung trägt */
|
else render(); /* nicht ausdrückbar: Überlagerung trägt */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text
|
/* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text
|
||||||
@@ -1620,7 +1606,7 @@ function resolveShortId(line){
|
|||||||
writeShortId(line);
|
writeShortId(line);
|
||||||
}
|
}
|
||||||
function writeShortId(line){
|
function writeShortId(line){
|
||||||
if(src.readOnly || line == null) return;
|
if(line == null) return;
|
||||||
/* **Nicht** sofort schreiben: Beide Wege hierher hängen am
|
/* **Nicht** sofort schreiben: Beide Wege hierher hängen am
|
||||||
`input`-Ereignis — der Doppelpunkt unmittelbar, der Zeilenwechsel über die
|
`input`-Ereignis — der Doppelpunkt unmittelbar, der Zeilenwechsel über die
|
||||||
Enter-Taste —, und `execCommand` verweigert den Dienst, wenn es
|
Enter-Taste —, und `execCommand` verweigert den Dienst, wenn es
|
||||||
@@ -1751,7 +1737,7 @@ function closeAc(){
|
|||||||
acCtx = null;
|
acCtx = null;
|
||||||
}
|
}
|
||||||
function updateAc(){
|
function updateAc(){
|
||||||
if(src.readOnly || document.activeElement !== src ||
|
if(document.activeElement !== src ||
|
||||||
src.selectionStart !== src.selectionEnd){ closeAc(); return; }
|
src.selectionStart !== src.selectionEnd){ closeAc(); return; }
|
||||||
const ctx = depFragment(src.value, src.selectionStart);
|
const ctx = depFragment(src.value, src.selectionStart);
|
||||||
if(!ctx){ acSuppress = null; closeAc(); return; }
|
if(!ctx){ acSuppress = null; closeAc(); return; }
|
||||||
@@ -2221,21 +2207,11 @@ const I18N = {
|
|||||||
acHint:"{n} ID-Vorschläge – ↑/↓ wählt, Enter übernimmt",
|
acHint:"{n} ID-Vorschläge – ↑/↓ wählt, Enter übernimmt",
|
||||||
tipClose:"Schließen",
|
tipClose:"Schließen",
|
||||||
tipOpenLink:"Link öffnen",
|
tipOpenLink:"Link öffnen",
|
||||||
padReadonly:"Wird im Pad bearbeitet — hier nur lesen.",
|
|
||||||
padEdit:"Pad zum Bearbeiten öffnen",
|
|
||||||
padRefresh:"Vom Pad neu laden",
|
|
||||||
liveLoadWarn:"Server-Dokument nicht geladen: {url} ({error}). Läuft das Backend, und ist die Adresse eine Dokument-Adresse (…/documents/<uuid>)?",
|
liveLoadWarn:"Server-Dokument nicht geladen: {url} ({error}). Läuft das Backend, und ist die Adresse eine Dokument-Adresse (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Deine Änderung war nicht mehr anwendbar ({error}) — der Stand wurde einmal frisch geholt.",
|
liveStaleWarn:"Deine Änderung war nicht mehr anwendbar ({error}) — der Stand wurde einmal frisch geholt.",
|
||||||
liveConflictText:"Jemand hat dieselben Zeilen geändert. Wessen Fassung soll gelten?",
|
liveConflictText:"Jemand hat dieselben Zeilen geändert. Wessen Fassung soll gelten?",
|
||||||
liveConflictTheirs:"Fremde übernehmen",
|
liveConflictTheirs:"Fremde übernehmen",
|
||||||
liveConflictMine:"Eigene durchsetzen",
|
liveConflictMine:"Eigene durchsetzen",
|
||||||
padWait:"Noch {seconds} s — Etherpad begrenzt die Abrufe",
|
|
||||||
padRateLimitWarn:"Noch nicht neu geladen: Etherpad erlaubt nur wenige Abrufe je Zeitfenster (serienmäßig 10 pro 90 s), so häufiges Nachladen ist leider nicht möglich. In {seconds} s geht es wieder.",
|
|
||||||
padViewTooltip:"Ansicht: {state} — klicken zum Wechseln",
|
|
||||||
padView_both:"Pad und Text",
|
|
||||||
padView_pad:"nur Pad",
|
|
||||||
padView_text:"nur Text",
|
|
||||||
padGutterAria:"Pad und Texteditor größenverändern",
|
|
||||||
riskTooltip:"High Risk – Aufwand noch unklar.",
|
riskTooltip:"High Risk – Aufwand noch unklar.",
|
||||||
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
||||||
cheapTooltip:"Günstigsten Pfad hervorheben – nicht benötigte Alternativen treten zurück",
|
cheapTooltip:"Günstigsten Pfad hervorheben – nicht benötigte Alternativen treten zurück",
|
||||||
@@ -2307,7 +2283,7 @@ const I18N = {
|
|||||||
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
|
||||||
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
|
||||||
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.",
|
padGoneWarn:"Dieser Link zeigt auf ein Etherpad-Pad. Die Etherpad-Anbindung gibt es nicht mehr — gemeinsames Arbeiten läuft jetzt über ein Werkbaum-Backend (?live=…). Der Text des Pads lässt sich dort einmal einfügen und dann zu zweit bearbeiten.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt",
|
||||||
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.",
|
||||||
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen",
|
||||||
@@ -2343,21 +2319,11 @@ const I18N = {
|
|||||||
acHint:"{n} id suggestions – ↑/↓ to choose, Enter to insert",
|
acHint:"{n} id suggestions – ↑/↓ to choose, Enter to insert",
|
||||||
tipClose:"Close",
|
tipClose:"Close",
|
||||||
tipOpenLink:"Open link",
|
tipOpenLink:"Open link",
|
||||||
padReadonly:"Edited in the pad — read-only here.",
|
|
||||||
padEdit:"Open the pad to edit",
|
|
||||||
padRefresh:"Reload from the pad",
|
|
||||||
liveLoadWarn:"Server document not loaded: {url} ({error}). Is the backend running, and is the address a document address (…/documents/<uuid>)?",
|
liveLoadWarn:"Server document not loaded: {url} ({error}). Is the backend running, and is the address a document address (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Your change no longer applied ({error}) — the document was fetched afresh once.",
|
liveStaleWarn:"Your change no longer applied ({error}) — the document was fetched afresh once.",
|
||||||
liveConflictText:"Someone changed the same lines. Whose version should win?",
|
liveConflictText:"Someone changed the same lines. Whose version should win?",
|
||||||
liveConflictTheirs:"Take theirs",
|
liveConflictTheirs:"Take theirs",
|
||||||
liveConflictMine:"Keep mine",
|
liveConflictMine:"Keep mine",
|
||||||
padWait:"{seconds} s to go — Etherpad limits how often we may fetch",
|
|
||||||
padRateLimitWarn:"Not reloaded yet: Etherpad only allows a few fetches per time window (10 per 90 s by default), so syncing this often is unfortunately not possible. Try again in {seconds} s.",
|
|
||||||
padViewTooltip:"View: {state} — click to switch",
|
|
||||||
padView_both:"pad and text",
|
|
||||||
padView_pad:"pad only",
|
|
||||||
padView_text:"text only",
|
|
||||||
padGutterAria:"Resize pad and text editor",
|
|
||||||
riskTooltip:"High risk – effort still unclear.",
|
riskTooltip:"High risk – effort still unclear.",
|
||||||
discardedTooltip:"Show/hide discarded nodes and their subtree",
|
discardedTooltip:"Show/hide discarded nodes and their subtree",
|
||||||
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
||||||
@@ -2428,7 +2394,7 @@ const I18N = {
|
|||||||
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
|
||||||
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
|
||||||
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.",
|
padGoneWarn:"This link points at an Etherpad pad. The Etherpad connection is gone — collaboration now runs through a Werkbaum backend (?live=…). Paste the pad’s text there once, then edit it together.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link",
|
||||||
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.",
|
||||||
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
hint_all:"sub-task, all required", hint_any:"alternative, choose one",
|
||||||
@@ -2464,21 +2430,11 @@ const I18N = {
|
|||||||
acHint:"{n} sugerencias de ID – ↑/↓ elige, Intro inserta",
|
acHint:"{n} sugerencias de ID – ↑/↓ elige, Intro inserta",
|
||||||
tipClose:"Cerrar",
|
tipClose:"Cerrar",
|
||||||
tipOpenLink:"Abrir enlace",
|
tipOpenLink:"Abrir enlace",
|
||||||
padReadonly:"Se edita en el pad — aquí solo lectura.",
|
|
||||||
padEdit:"Abrir el pad para editar",
|
|
||||||
padRefresh:"Recargar desde el pad",
|
|
||||||
liveLoadWarn:"Documento del servidor no cargado: {url} ({error}). ¿Está el backend en marcha y es la dirección la de un documento (…/documents/<uuid>)?",
|
liveLoadWarn:"Documento del servidor no cargado: {url} ({error}). ¿Está el backend en marcha y es la dirección la de un documento (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Tu cambio ya no era aplicable ({error}): se volvió a cargar el estado una vez.",
|
liveStaleWarn:"Tu cambio ya no era aplicable ({error}): se volvió a cargar el estado una vez.",
|
||||||
liveConflictText:"Alguien cambió las mismas líneas. ¿Qué versión debe prevalecer?",
|
liveConflictText:"Alguien cambió las mismas líneas. ¿Qué versión debe prevalecer?",
|
||||||
liveConflictTheirs:"Tomar la ajena",
|
liveConflictTheirs:"Tomar la ajena",
|
||||||
liveConflictMine:"Mantener la mía",
|
liveConflictMine:"Mantener la mía",
|
||||||
padWait:"Faltan {seconds} s — Etherpad limita la frecuencia",
|
|
||||||
padRateLimitWarn:"Aún no se ha recargado: Etherpad solo permite unas pocas descargas por ventana de tiempo (10 por 90 s de forma predeterminada), así que sincronizar tan a menudo no es posible. Vuelve a intentarlo en {seconds} s.",
|
|
||||||
padViewTooltip:"Vista: {state} — clic para cambiar",
|
|
||||||
padView_both:"pad y texto",
|
|
||||||
padView_pad:"solo pad",
|
|
||||||
padView_text:"solo texto",
|
|
||||||
padGutterAria:"Redimensionar pad y editor de texto",
|
|
||||||
riskTooltip:"Alto riesgo – esfuerzo aún incierto.",
|
riskTooltip:"Alto riesgo – esfuerzo aún incierto.",
|
||||||
discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol",
|
discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol",
|
||||||
cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan",
|
cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan",
|
||||||
@@ -2549,7 +2505,7 @@ const I18N = {
|
|||||||
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
|
||||||
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
|
||||||
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.",
|
padGoneWarn:"Este enlace apunta a un pad de Etherpad. La conexión con Etherpad ya no existe: ahora se colabora a través de un backend de Werkbaum (?live=…). Pega allí el texto del pad una vez y editadlo juntos.",
|
||||||
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace",
|
||||||
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.",
|
||||||
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una",
|
||||||
@@ -2585,21 +2541,11 @@ const I18N = {
|
|||||||
acHint:"{n} suggestions d'ID – ↑/↓ pour choisir, Entrée pour insérer",
|
acHint:"{n} suggestions d'ID – ↑/↓ pour choisir, Entrée pour insérer",
|
||||||
tipClose:"Fermer",
|
tipClose:"Fermer",
|
||||||
tipOpenLink:"Ouvrir le lien",
|
tipOpenLink:"Ouvrir le lien",
|
||||||
padReadonly:"Modifié dans le pad — lecture seule ici.",
|
|
||||||
padEdit:"Ouvrir le pad pour modifier",
|
|
||||||
padRefresh:"Recharger depuis le pad",
|
|
||||||
liveLoadWarn:"Document du serveur non chargé : {url} ({error}). Le backend tourne-t-il, et l'adresse est-elle celle d'un document (…/documents/<uuid>) ?",
|
liveLoadWarn:"Document du serveur non chargé : {url} ({error}). Le backend tourne-t-il, et l'adresse est-elle celle d'un document (…/documents/<uuid>) ?",
|
||||||
liveStaleWarn:"Ta modification n'était plus applicable ({error}) — l'état a été rechargé une fois.",
|
liveStaleWarn:"Ta modification n'était plus applicable ({error}) — l'état a été rechargé une fois.",
|
||||||
liveConflictText:"Quelqu'un a modifié les mêmes lignes. Quelle version doit l'emporter ?",
|
liveConflictText:"Quelqu'un a modifié les mêmes lignes. Quelle version doit l'emporter ?",
|
||||||
liveConflictTheirs:"Prendre la sienne",
|
liveConflictTheirs:"Prendre la sienne",
|
||||||
liveConflictMine:"Garder la mienne",
|
liveConflictMine:"Garder la mienne",
|
||||||
padWait:"Encore {seconds} s — Etherpad limite la fréquence",
|
|
||||||
padRateLimitWarn:"Pas encore rechargé : Etherpad n’autorise que quelques récupérations par fenêtre de temps (10 par 90 s par défaut), une synchronisation aussi fréquente n’est donc pas possible. Réessaie dans {seconds} s.",
|
|
||||||
padViewTooltip:"Vue : {state} — cliquer pour changer",
|
|
||||||
padView_both:"pad et texte",
|
|
||||||
padView_pad:"pad seul",
|
|
||||||
padView_text:"texte seul",
|
|
||||||
padGutterAria:"Redimensionner le pad et l’éditeur de texte",
|
|
||||||
riskTooltip:"Risque élevé – effort encore incertain.",
|
riskTooltip:"Risque élevé – effort encore incertain.",
|
||||||
discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre",
|
discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre",
|
||||||
cheapTooltip:"Mettre en évidence le chemin le moins coûteux – les alternatives inutiles s'estompent",
|
cheapTooltip:"Mettre en évidence le chemin le moins coûteux – les alternatives inutiles s'estompent",
|
||||||
@@ -2670,7 +2616,7 @@ const I18N = {
|
|||||||
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
|
||||||
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
|
||||||
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.",
|
padGoneWarn:"Ce lien pointe vers un pad Etherpad. La connexion Etherpad n’existe plus — la collaboration passe désormais par un backend Werkbaum (?live=…). Collez-y une fois le texte du pad, puis modifiez-le à plusieurs.",
|
||||||
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien",
|
||||||
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.",
|
||||||
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une",
|
||||||
@@ -2706,21 +2652,11 @@ const I18N = {
|
|||||||
acHint:"{n} podpowiedzi ID – ↑/↓ wybiera, Enter wstawia",
|
acHint:"{n} podpowiedzi ID – ↑/↓ wybiera, Enter wstawia",
|
||||||
tipClose:"Zamknij",
|
tipClose:"Zamknij",
|
||||||
tipOpenLink:"Otwórz link",
|
tipOpenLink:"Otwórz link",
|
||||||
padReadonly:"Edytowane w padzie — tu tylko do czytania.",
|
|
||||||
padEdit:"Otwórz pad do edycji",
|
|
||||||
padRefresh:"Wczytaj ponownie z padu",
|
|
||||||
liveLoadWarn:"Nie wczytano dokumentu z serwera: {url} ({error}). Czy backend działa i czy adres wskazuje dokument (…/documents/<uuid>)?",
|
liveLoadWarn:"Nie wczytano dokumentu z serwera: {url} ({error}). Czy backend działa i czy adres wskazuje dokument (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Twoja zmiana nie dała się już zastosować ({error}) — stan pobrano raz od nowa.",
|
liveStaleWarn:"Twoja zmiana nie dała się już zastosować ({error}) — stan pobrano raz od nowa.",
|
||||||
liveConflictText:"Ktoś zmienił te same wiersze. Która wersja ma obowiązywać?",
|
liveConflictText:"Ktoś zmienił te same wiersze. Która wersja ma obowiązywać?",
|
||||||
liveConflictTheirs:"Przyjmij cudzą",
|
liveConflictTheirs:"Przyjmij cudzą",
|
||||||
liveConflictMine:"Zachowaj własną",
|
liveConflictMine:"Zachowaj własną",
|
||||||
padWait:"Jeszcze {seconds} s — Etherpad ogranicza częstość",
|
|
||||||
padRateLimitWarn:"Jeszcze nie wczytano ponownie: Etherpad dopuszcza tylko kilka pobrań w okresie (domyślnie 10 na 90 s), więc tak częsta synchronizacja nie jest możliwa. Spróbuj za {seconds} s.",
|
|
||||||
padViewTooltip:"Widok: {state} — kliknij, aby zmienić",
|
|
||||||
padView_both:"pad i tekst",
|
|
||||||
padView_pad:"tylko pad",
|
|
||||||
padView_text:"tylko tekst",
|
|
||||||
padGutterAria:"Zmień rozmiar padu i edytora tekstu",
|
|
||||||
riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.",
|
riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.",
|
||||||
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
||||||
cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone",
|
cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone",
|
||||||
@@ -2791,7 +2727,7 @@ const I18N = {
|
|||||||
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
|
||||||
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
|
||||||
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.",
|
padGoneWarn:"Ten link prowadzi do pada Etherpad. Połączenia z Etherpadem już nie ma — wspólna praca odbywa się teraz przez backend Werkbaum (?live=…). Wklej tam raz tekst pada i edytujcie go razem.",
|
||||||
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem",
|
||||||
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.",
|
||||||
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną",
|
||||||
@@ -2827,21 +2763,11 @@ const I18N = {
|
|||||||
acHint:"{n} подсказок ID – ↑/↓ выбирает, Enter вставляет",
|
acHint:"{n} подсказок ID – ↑/↓ выбирает, Enter вставляет",
|
||||||
tipClose:"Закрыть",
|
tipClose:"Закрыть",
|
||||||
tipOpenLink:"Открыть ссылку",
|
tipOpenLink:"Открыть ссылку",
|
||||||
padReadonly:"Редактируется в паде — здесь только чтение.",
|
|
||||||
padEdit:"Открыть пад для редактирования",
|
|
||||||
padRefresh:"Обновить из пада",
|
|
||||||
liveLoadWarn:"Документ с сервера не загружен: {url} ({error}). Запущен ли бэкенд и является ли адрес адресом документа (…/documents/<uuid>)?",
|
liveLoadWarn:"Документ с сервера не загружен: {url} ({error}). Запущен ли бэкенд и является ли адрес адресом документа (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"Ваше изменение больше не применялось ({error}) — состояние загружено заново.",
|
liveStaleWarn:"Ваше изменение больше не применялось ({error}) — состояние загружено заново.",
|
||||||
liveConflictText:"Кто-то изменил те же строки. Чья версия должна остаться?",
|
liveConflictText:"Кто-то изменил те же строки. Чья версия должна остаться?",
|
||||||
liveConflictTheirs:"Принять чужую",
|
liveConflictTheirs:"Принять чужую",
|
||||||
liveConflictMine:"Оставить свою",
|
liveConflictMine:"Оставить свою",
|
||||||
padWait:"Ещё {seconds} с — Etherpad ограничивает частоту",
|
|
||||||
padRateLimitWarn:"Пока не обновлено: Etherpad разрешает лишь несколько загрузок за окно времени (по умолчанию 10 за 90 с), поэтому столь частая синхронизация невозможна. Повторите через {seconds} с.",
|
|
||||||
padViewTooltip:"Вид: {state} — нажмите для переключения",
|
|
||||||
padView_both:"пад и текст",
|
|
||||||
padView_pad:"только пад",
|
|
||||||
padView_text:"только текст",
|
|
||||||
padGutterAria:"Изменить размер пада и текстового редактора",
|
|
||||||
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
||||||
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
||||||
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
||||||
@@ -2912,7 +2838,7 @@ const I18N = {
|
|||||||
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
|
||||||
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
|
||||||
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).",
|
||||||
sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.",
|
padGoneWarn:"Эта ссылка ведёт на пад Etherpad. Подключения к Etherpad больше нет — совместная работа теперь идёт через бэкенд Werkbaum (?live=…). Вставьте туда текст пада один раз и редактируйте вместе.",
|
||||||
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой",
|
||||||
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.",
|
||||||
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну",
|
||||||
@@ -2948,21 +2874,11 @@ const I18N = {
|
|||||||
acHint:"{n} आईडी सुझाव – ↑/↓ से चुनें, Enter से डालें",
|
acHint:"{n} आईडी सुझाव – ↑/↓ से चुनें, Enter से डालें",
|
||||||
tipClose:"बंद करें",
|
tipClose:"बंद करें",
|
||||||
tipOpenLink:"लिंक खोलें",
|
tipOpenLink:"लिंक खोलें",
|
||||||
padReadonly:"पैड में संपादित होता है — यहाँ केवल पढ़ें।",
|
|
||||||
padEdit:"संपादित करने के लिए पैड खोलें",
|
|
||||||
padRefresh:"पैड से फिर लोड करें",
|
|
||||||
liveLoadWarn:"सर्वर दस्तावेज़ लोड नहीं हुआ: {url} ({error})। क्या बैकएंड चल रहा है और क्या पता दस्तावेज़ का पता है (…/documents/<uuid>)?",
|
liveLoadWarn:"सर्वर दस्तावेज़ लोड नहीं हुआ: {url} ({error})। क्या बैकएंड चल रहा है और क्या पता दस्तावेज़ का पता है (…/documents/<uuid>)?",
|
||||||
liveStaleWarn:"आपका बदलाव अब लागू नहीं हो सका ({error}) — स्थिति एक बार नए सिरे से ली गई।",
|
liveStaleWarn:"आपका बदलाव अब लागू नहीं हो सका ({error}) — स्थिति एक बार नए सिरे से ली गई।",
|
||||||
liveConflictText:"किसी और ने वही पंक्तियाँ बदली हैं। किसका संस्करण रहे?",
|
liveConflictText:"किसी और ने वही पंक्तियाँ बदली हैं। किसका संस्करण रहे?",
|
||||||
liveConflictTheirs:"दूसरे का लें",
|
liveConflictTheirs:"दूसरे का लें",
|
||||||
liveConflictMine:"अपना रखें",
|
liveConflictMine:"अपना रखें",
|
||||||
padWait:"{seconds} स॰ बाकी — Etherpad बार-बार लेने की सीमा रखता है",
|
|
||||||
padRateLimitWarn:"अभी दोबारा लोड नहीं किया: Etherpad प्रति समय-खिड़की केवल कुछ ही बार लेने देता है (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार), इसलिए इतनी बार सिंक करना संभव नहीं है। {seconds} स॰ में फिर कोशिश करें।",
|
|
||||||
padViewTooltip:"दृश्य: {state} — बदलने के लिए क्लिक करें",
|
|
||||||
padView_both:"पैड और टेक्स्ट",
|
|
||||||
padView_pad:"केवल पैड",
|
|
||||||
padView_text:"केवल टेक्स्ट",
|
|
||||||
padGutterAria:"पैड और टेक्स्ट संपादक का आकार बदलें",
|
|
||||||
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
||||||
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
||||||
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
||||||
@@ -3033,7 +2949,7 @@ const I18N = {
|
|||||||
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
|
||||||
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
|
||||||
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।",
|
||||||
sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।",
|
padGoneWarn:"यह लिंक एक Etherpad पैड की ओर इशारा करता है। Etherpad कनेक्शन अब नहीं है — साझा काम अब Werkbaum बैकएंड (?live=…) से होता है। पैड का टेक्स्ट वहाँ एक बार चिपकाएँ और मिलकर संपादित करें।",
|
||||||
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित",
|
||||||
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।",
|
||||||
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें",
|
||||||
@@ -3080,21 +2996,11 @@ const I18N = {
|
|||||||
acHint:"{n} 个 ID 建议 – ↑/↓ 选择,Enter 插入",
|
acHint:"{n} 个 ID 建议 – ↑/↓ 选择,Enter 插入",
|
||||||
tipClose:"关闭",
|
tipClose:"关闭",
|
||||||
tipOpenLink:"打开链接",
|
tipOpenLink:"打开链接",
|
||||||
padReadonly:"在 Pad 中编辑 — 此处只读。",
|
|
||||||
padEdit:"打开 Pad 进行编辑",
|
|
||||||
padRefresh:"从 Pad 重新加载",
|
|
||||||
liveLoadWarn:"未能加载服务器文档:{url}({error})。后端在运行吗?该地址是文档地址(…/documents/<uuid>)吗?",
|
liveLoadWarn:"未能加载服务器文档:{url}({error})。后端在运行吗?该地址是文档地址(…/documents/<uuid>)吗?",
|
||||||
liveStaleWarn:"你的更改已无法应用({error})——已重新获取一次当前状态。",
|
liveStaleWarn:"你的更改已无法应用({error})——已重新获取一次当前状态。",
|
||||||
liveConflictText:"有人改动了同样的行。以谁的版本为准?",
|
liveConflictText:"有人改动了同样的行。以谁的版本为准?",
|
||||||
liveConflictTheirs:"采用对方的",
|
liveConflictTheirs:"采用对方的",
|
||||||
liveConflictMine:"保留我的",
|
liveConflictMine:"保留我的",
|
||||||
padWait:"还需 {seconds} 秒 — Etherpad 限制获取频率",
|
|
||||||
padRateLimitWarn:"尚未重新加载:Etherpad 每个时间窗口只允许少量获取(默认每 90 秒 10 次),因此无法如此频繁地同步。请在 {seconds} 秒后再试。",
|
|
||||||
padViewTooltip:"视图:{state} — 点击切换",
|
|
||||||
padView_both:"Pad 和文本",
|
|
||||||
padView_pad:"仅 Pad",
|
|
||||||
padView_text:"仅文本",
|
|
||||||
padGutterAria:"调整 Pad 与文本编辑器大小",
|
|
||||||
riskTooltip:"高风险 – 工作量尚不明确。",
|
riskTooltip:"高风险 – 工作量尚不明确。",
|
||||||
editorTitle:"结构(文本)", diagramTitle:"图表",
|
editorTitle:"结构(文本)", diagramTitle:"图表",
|
||||||
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
|
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
|
||||||
@@ -3154,7 +3060,7 @@ const I18N = {
|
|||||||
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
|
||||||
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
|
||||||
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。",
|
||||||
sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。",
|
padGoneWarn:"此链接指向一个 Etherpad pad。Etherpad 连接已移除——协作现在通过 Werkbaum 后端(?live=…)进行。把 pad 的文本粘贴过去一次,然后一起编辑。",
|
||||||
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接",
|
||||||
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
hint_indent:"缩进(2 个空格或制表符)定义层级。",
|
||||||
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
hint_all:"子任务,全部必需", hint_any:"备选项,择其一",
|
||||||
@@ -3201,21 +3107,11 @@ const I18N = {
|
|||||||
acHint:"ID候補 {n} 件 – ↑/↓で選択、Enterで挿入",
|
acHint:"ID候補 {n} 件 – ↑/↓で選択、Enterで挿入",
|
||||||
tipClose:"閉じる",
|
tipClose:"閉じる",
|
||||||
tipOpenLink:"リンクを開く",
|
tipOpenLink:"リンクを開く",
|
||||||
padReadonly:"パッドで編集します — ここでは読み取り専用です。",
|
|
||||||
padEdit:"編集するにはパッドを開く",
|
|
||||||
padRefresh:"パッドから再読み込み",
|
|
||||||
liveLoadWarn:"サーバー文書を読み込めませんでした: {url}({error})。バックエンドは動いていますか。アドレスは文書のアドレス(…/documents/<uuid>)ですか。",
|
liveLoadWarn:"サーバー文書を読み込めませんでした: {url}({error})。バックエンドは動いていますか。アドレスは文書のアドレス(…/documents/<uuid>)ですか。",
|
||||||
liveStaleWarn:"あなたの変更はもう適用できませんでした({error})。状態を一度取り直しました。",
|
liveStaleWarn:"あなたの変更はもう適用できませんでした({error})。状態を一度取り直しました。",
|
||||||
liveConflictText:"同じ行が他の人にも変更されました。どちらの版を採りますか。",
|
liveConflictText:"同じ行が他の人にも変更されました。どちらの版を採りますか。",
|
||||||
liveConflictTheirs:"相手の版",
|
liveConflictTheirs:"相手の版",
|
||||||
liveConflictMine:"自分の版",
|
liveConflictMine:"自分の版",
|
||||||
padWait:"あと {seconds} 秒 — Etherpad は取得頻度を制限します",
|
|
||||||
padRateLimitWarn:"まだ再読み込みしていません: Etherpad は一定時間内の取得回数を制限します(既定で 90 秒あたり 10 回)。これほど頻繁な同期はできません。{seconds} 秒後にもう一度お試しください。",
|
|
||||||
padViewTooltip:"表示: {state} — クリックで切り替え",
|
|
||||||
padView_both:"パッドとテキスト",
|
|
||||||
padView_pad:"パッドのみ",
|
|
||||||
padView_text:"テキストのみ",
|
|
||||||
padGutterAria:"パッドとテキストエディターのサイズを変更",
|
|
||||||
riskTooltip:"高リスク – 規模はまだ不明。",
|
riskTooltip:"高リスク – 規模はまだ不明。",
|
||||||
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
||||||
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
|
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
|
||||||
@@ -3275,7 +3171,7 @@ const I18N = {
|
|||||||
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
|
||||||
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
|
||||||
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。",
|
||||||
sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。",
|
padGoneWarn:"このリンクは Etherpad のパッドを指しています。Etherpad 連携は廃止されました。共同編集は Werkbaum バックエンド(?live=…)で行います。パッドの本文を一度貼り付ければ、複数人で編集できます。",
|
||||||
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり",
|
||||||
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。",
|
||||||
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ",
|
||||||
@@ -3555,11 +3451,6 @@ function saveUI(){
|
|||||||
agenda: !!document.querySelector('.agenda.open'),
|
agenda: !!document.querySelector('.agenda.open'),
|
||||||
hcol: app.style.getPropertyValue('--hcol') || null,
|
hcol: app.style.getPropertyValue('--hcol') || null,
|
||||||
hrow: app.style.getPropertyValue('--hrow') || null,
|
hrow: app.style.getPropertyValue('--hrow') || null,
|
||||||
/* Pad-Ansicht + Aufteilung Pad|Spiegel (D31). Global über alle Dokumente,
|
|
||||||
wie der übrige Ansichts-Zustand (D22). */
|
|
||||||
padView: padView,
|
|
||||||
pcol: app.style.getPropertyValue('--pcol') || null,
|
|
||||||
prow: app.style.getPropertyValue('--prow') || null,
|
|
||||||
/* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag) — ebenfalls
|
/* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag) — ebenfalls
|
||||||
global über alle Dokumente wie der übrige Ansichts-Zustand. */
|
global über alle Dokumente wie der übrige Ansichts-Zustand. */
|
||||||
mobilePane: mobilePane,
|
mobilePane: mobilePane,
|
||||||
@@ -3609,9 +3500,6 @@ function restoreState(){
|
|||||||
if(ui && ui.hcol) app.style.setProperty('--hcol', ui.hcol);
|
if(ui && ui.hcol) app.style.setProperty('--hcol', ui.hcol);
|
||||||
if(ui && ui.hrow) app.style.setProperty('--hrow', ui.hrow);
|
if(ui && ui.hrow) app.style.setProperty('--hrow', ui.hrow);
|
||||||
if(ui && (ui.mobilePane === 'diagram' || ui.mobilePane === 'text')) mobilePane = ui.mobilePane;
|
if(ui && (ui.mobilePane === 'diagram' || ui.mobilePane === 'text')) mobilePane = ui.mobilePane;
|
||||||
if(ui && PAD_VIEWS.indexOf(ui.padView) >= 0) padView = ui.padView;
|
|
||||||
if(ui && ui.pcol) app.style.setProperty('--pcol', ui.pcol);
|
|
||||||
if(ui && ui.prow) app.style.setProperty('--prow', ui.prow);
|
|
||||||
setAgendaOpen(!!(ui && ui.agenda));
|
setAgendaOpen(!!(ui && ui.agenda));
|
||||||
applyZoom();
|
applyZoom();
|
||||||
restoring = false;
|
restoring = false;
|
||||||
@@ -3633,7 +3521,6 @@ function updateDocName(){
|
|||||||
? d.source + '\n' + t('docSwitchTooltip')
|
? d.source + '\n' + t('docSwitchTooltip')
|
||||||
: t('docSwitchTooltip');
|
: t('docSwitchTooltip');
|
||||||
}
|
}
|
||||||
updatePadLink(); /* Schreibschutz + Pad-Knopf hängen am aktiven Dokument (D31) */
|
|
||||||
}
|
}
|
||||||
let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder null) */
|
let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder null) */
|
||||||
/* Kam das Umbenennen von einem frisch angelegten Dokument? Dann geht es danach
|
/* Kam das Umbenennen von einem frisch angelegten Dokument? Dann geht es danach
|
||||||
@@ -3741,9 +3628,8 @@ function flushActive(){ const d = activeDoc(); if(d) d.text = src.value; }
|
|||||||
wer weiter zurück will, hat Git.
|
wer weiter zurück will, hat Git.
|
||||||
|
|
||||||
Die Regeln stehen in snapshots.js und sind dort getestet; hier bleibt nur,
|
Die Regeln stehen in snapshots.js und sind dort getestet; hier bleibt nur,
|
||||||
was DOM oder Speicher berührt: welches Dokument aktiv ist, ob sein Text
|
was DOM oder Speicher berührt: welches Dokument aktiv ist und das
|
||||||
beschreibbar ist (Pad-Dokumente, D31, bleiben außen vor — ein alter Stand
|
Nachzeichnen des Menüs. */
|
||||||
ließe sich dort gar nicht einsetzen), und das Nachzeichnen des Menüs. */
|
|
||||||
let snaps = {}; /* {docId: [{t, text}, …]} — ältester zuerst */
|
let snaps = {}; /* {docId: [{t, text}, …]} — ältester zuerst */
|
||||||
let snapBase = ''; /* Text bei Dokumentwechsel; Vergleich, solange es keinen Stand gibt */
|
let snapBase = ''; /* Text bei Dokumentwechsel; Vergleich, solange es keinen Stand gibt */
|
||||||
|
|
||||||
@@ -3751,7 +3637,7 @@ function loadSnaps(){ snaps = parseSnaps(localStorage.getItem(LS_SNAPS)); }
|
|||||||
|
|
||||||
function snapshotNow(manuell){
|
function snapshotNow(manuell){
|
||||||
const d = activeDoc();
|
const d = activeDoc();
|
||||||
if(!d || src.readOnly) return false;
|
if(!d) return false;
|
||||||
const text = src.value;
|
const text = src.value;
|
||||||
if(!addSnapshot(snaps, d.id, text, Date.now(), {base: snapBase, manual: manuell})) return false;
|
if(!addSnapshot(snaps, d.id, text, Date.now(), {base: snapBase, manual: manuell})) return false;
|
||||||
snapBase = text;
|
snapBase = text;
|
||||||
@@ -3796,7 +3682,6 @@ function renderSnapMenu(){
|
|||||||
keine Rückfrage. Der aktuelle Stand wird vorher weggelegt, falls er noch
|
keine Rückfrage. Der aktuelle Stand wird vorher weggelegt, falls er noch
|
||||||
nicht drin ist — sonst wäre er das Einzige, was der Griff verlöre. */
|
nicht drin ist — sonst wäre er das Einzige, was der Griff verlöre. */
|
||||||
function loadSnapshot(s){
|
function loadSnapshot(s){
|
||||||
if(src.readOnly) return;
|
|
||||||
snapshotNow(true); /* bewusstes Wegleg-Ereignis wie der Knopf, nicht der Takt */
|
snapshotNow(true); /* bewusstes Wegleg-Ereignis wie der Knopf, nicht der Takt */
|
||||||
closeSnapMenu();
|
closeSnapMenu();
|
||||||
if(!replaceTextUndoable(s.text)) render();
|
if(!replaceTextUndoable(s.text)) render();
|
||||||
@@ -3811,10 +3696,6 @@ function closeSnapMenu(){
|
|||||||
snapMenu.hidden = true;
|
snapMenu.hidden = true;
|
||||||
snapBtn.setAttribute('aria-expanded', 'false');
|
snapBtn.setAttribute('aria-expanded', 'false');
|
||||||
}
|
}
|
||||||
function updateSnapBtn(){
|
|
||||||
snapBtn.hidden = snapAddBtn.hidden = src.readOnly;
|
|
||||||
if(src.readOnly) closeSnapMenu();
|
|
||||||
}
|
|
||||||
snapBtn.addEventListener('click', e => {
|
snapBtn.addEventListener('click', e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
snapMenu.hidden ? openSnapMenu() : closeSnapMenu();
|
snapMenu.hidden ? openSnapMenu() : closeSnapMenu();
|
||||||
@@ -4010,7 +3891,7 @@ document.addEventListener('pointerdown', e => {
|
|||||||
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : '';
|
||||||
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
||||||
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
|
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
|
||||||
snapBase = src.value; closeSnapMenu(); updateSnapBtn();
|
snapBase = src.value; closeSnapMenu();
|
||||||
render(); updateDocName(); updateFreshBtn(); }
|
render(); updateDocName(); updateFreshBtn(); }
|
||||||
function switchDoc(id){
|
function switchDoc(id){
|
||||||
if(id === activeId) return;
|
if(id === activeId) return;
|
||||||
@@ -4072,7 +3953,6 @@ function deleteDoc(){
|
|||||||
const d = activeDoc();
|
const d = activeDoc();
|
||||||
if(!d) return;
|
if(!d) return;
|
||||||
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
|
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
|
||||||
if(padSource && padSource.id === d.id) stopPad(); /* danach gibt es nichts mehr zu holen (D31) */
|
|
||||||
if(liveState && liveState.id === d.id) stopLive(); /* dito fürs Server-Dokument (D76) */
|
if(liveState && liveState.id === d.id) stopLive(); /* dito fürs Server-Dokument (D76) */
|
||||||
if(fileHandles.has(d.id)){ fileHandles.delete(d.id); idbDeleteHandle(d.id); } /* mit dem Dokument geht sein Datei-Handle (D72) */
|
if(fileHandles.has(d.id)){ fileHandles.delete(d.id); idbDeleteHandle(d.id); } /* mit dem Dokument geht sein Datei-Handle (D72) */
|
||||||
docs = docs.filter(x => x.id !== d.id);
|
docs = docs.filter(x => x.id !== d.id);
|
||||||
@@ -4336,24 +4216,21 @@ function initDocs(){
|
|||||||
src.value = d ? d.text : '';
|
src.value = d ? d.text : '';
|
||||||
snapBase = src.value;
|
snapBase = src.value;
|
||||||
restoring = false;
|
restoring = false;
|
||||||
updateSnapBtn();
|
|
||||||
persistDocs(); /* migrierte/geladene Liste festschreiben (stabil über Reload) */
|
persistDocs(); /* migrierte/geladene Liste festschreiben (stabil über Reload) */
|
||||||
updateDocName();
|
updateDocName();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Text von außen: ?sourceUrl= (D23) und ?etherpad= (D31) ----------
|
/* ---------- Text von außen: ?sourceUrl= (D23) ----------
|
||||||
Beide holen einen Notationstext über http(s) und führen ihn als eigenes
|
Holt einen Notationstext über http(s) und führt ihn als eigenes Dokument. Die
|
||||||
Dokument. Die id leitet sich aus der URL ab: derselbe Link aktualisiert dieses
|
id leitet sich aus der URL ab: derselbe Link aktualisiert dieses Dokument,
|
||||||
Dokument, statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente des
|
statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente des Nutzers
|
||||||
Nutzers bleiben unberührt. Scheitert das Laden (häufigster Fall: das Ziel
|
bleiben unberührt. Scheitert das Laden (häufigster Fall: das Ziel sendet
|
||||||
sendet keinen CORS-Header), bleibt der bisherige Stand stehen und es
|
keinen CORS-Header), bleibt der bisherige Stand stehen und es erscheint eine
|
||||||
erscheint eine Warnung.
|
Warnung. Eine statische Datei, einmal pro Laden geholt.
|
||||||
|
|
||||||
Unterschied: `sourceUrl` ist eine statische Datei, einmal pro Laden geholt
|
`?etherpad=` gab es hier einmal daneben (D31) und ist ausgebaut (D78) — ein
|
||||||
(D23, unverändert). `etherpad` ist ein lebendes Pad — wiederholt geholt und
|
alter Link meldet sich, statt still nichts zu tun. */
|
||||||
hier schreibgeschützt, weil das Zusammenführen gleichzeitiger Änderungen
|
|
||||||
Etherpads Aufgabe ist. Ein Fetch-Pfad, zwei Eingänge. */
|
|
||||||
const SOURCE_PARAM = 'sourceUrl';
|
const SOURCE_PARAM = 'sourceUrl';
|
||||||
const ETHERPAD_PARAM = 'etherpad';
|
const ETHERPAD_PARAM = 'etherpad';
|
||||||
function urlParam(name){
|
function urlParam(name){
|
||||||
@@ -4361,16 +4238,14 @@ function urlParam(name){
|
|||||||
}
|
}
|
||||||
function sourceUrlParam(){ return urlParam(SOURCE_PARAM); }
|
function sourceUrlParam(){ return urlParam(SOURCE_PARAM); }
|
||||||
|
|
||||||
/* Woher kommt der Text? null = kein Parameter, {bad,error} = unbrauchbare
|
/* Woher kommt der Text? null = kein Parameter, {gone} = ausgebauter Eingang,
|
||||||
Angabe, sonst der Beschreiber für loadRemoteSource(). Die Normalisierung der
|
{bad,error} = unbrauchbare Angabe, sonst der Beschreiber für
|
||||||
Pad-Adresse steht headless in remote.js — dort auch ihre Begründung. */
|
loadRemoteSource(). */
|
||||||
function remoteSource(){
|
function remoteSource(){
|
||||||
const padRaw = urlParam(ETHERPAD_PARAM);
|
/* Ein alter `?etherpad=`-Link soll nicht stillschweigend ein leeres
|
||||||
if(padRaw){
|
Werkbaum zeigen: Wer ihn irgendwo stehen hat, erfährt hier, wohin die
|
||||||
const p = padUrls(padRaw, location.href);
|
Zusammenarbeit gezogen ist (D78). */
|
||||||
if(!p) return {bad: padRaw, error: 'not an Etherpad URL'};
|
if(urlParam(ETHERPAD_PARAM)) return {gone: true};
|
||||||
return {fetchUrl: p.text, id: 'url:' + p.pad, name: p.pad, source: p.pad, live: true};
|
|
||||||
}
|
|
||||||
const raw = sourceUrlParam();
|
const raw = sourceUrlParam();
|
||||||
if(!raw) return null;
|
if(!raw) return null;
|
||||||
let url;
|
let url;
|
||||||
@@ -4380,25 +4255,15 @@ function remoteSource(){
|
|||||||
(„Failed to fetch", „HTTP 404") — der Rahmentext ist lokalisiert. */
|
(„Failed to fetch", „HTTP 404") — der Rahmentext ist lokalisiert. */
|
||||||
try{ url = new URL(raw, location.href); }catch(_){ return {bad: raw, error: 'invalid URL'}; }
|
try{ url = new URL(raw, location.href); }catch(_){ return {bad: raw, error: 'invalid URL'}; }
|
||||||
if(url.protocol !== 'http:' && url.protocol !== 'https:') return {bad: url.href, error: url.protocol};
|
if(url.protocol !== 'http:' && url.protocol !== 'https:') return {bad: url.href, error: url.protocol};
|
||||||
return {fetchUrl: url.href, id: 'url:' + url.href, name: url.href, source: url.href, live: false};
|
return {fetchUrl: url.href, id: 'url:' + url.href, name: url.href, source: url.href};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* `timeoutMs` nur beim Pad-Takt (D31): Hängt die Gegenseite, muss der Abruf
|
async function fetchRemote(url){
|
||||||
abbrechen, sonst bliebe der Riegel `padBusy` für immer zu und es käme nie
|
const resp = await fetch(url, {cache:'no-store', credentials:'omit'});
|
||||||
wieder etwas. Der erste Ladeversuch wartet unbegrenzt wie bisher (D23). */
|
if(!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||||
async function fetchRemote(url, timeoutMs){
|
return await resp.text();
|
||||||
const ctl = timeoutMs ? new AbortController() : null;
|
|
||||||
const timer = ctl ? setTimeout(() => ctl.abort(), timeoutMs) : null;
|
|
||||||
try{
|
|
||||||
const resp = await fetch(url, {cache:'no-store', credentials:'omit',
|
|
||||||
signal: ctl ? ctl.signal : undefined});
|
|
||||||
if(!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
||||||
return await resp.text();
|
|
||||||
} finally { if(timer) clearTimeout(timer); }
|
|
||||||
}
|
}
|
||||||
/* Geholten Text als Dokument übernehmen und aktivieren. Gemeinsam genutzt vom
|
/* Geholten Text als Dokument übernehmen und aktivieren. */
|
||||||
ersten Ladeversuch und vom Pad-Takt, wenn dieser einen gescheiterten ersten
|
|
||||||
Versuch nachholt — sonst gäbe es zwei Fassungen derselben sechs Zeilen. */
|
|
||||||
function adoptRemote(s, text){
|
function adoptRemote(s, text){
|
||||||
flushActive(); /* laufende Bearbeitung nicht verlieren */
|
flushActive(); /* laufende Bearbeitung nicht verlieren */
|
||||||
let d = docs.find(x => x.id === s.id);
|
let d = docs.find(x => x.id === s.id);
|
||||||
@@ -4414,28 +4279,23 @@ function adoptRemote(s, text){
|
|||||||
async function loadRemoteSource(){
|
async function loadRemoteSource(){
|
||||||
const s = remoteSource();
|
const s = remoteSource();
|
||||||
if(!s) return;
|
if(!s) return;
|
||||||
|
if(s.gone){
|
||||||
|
sourceWarning = {type:'padGone'};
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(s.bad !== undefined){
|
if(s.bad !== undefined){
|
||||||
sourceWarning = {type:'sourceLoad', url: s.bad, error: s.error};
|
sourceWarning = {type:'sourceLoad', url: s.bad, error: s.error};
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* `padSource` **vor** dem ersten Abruf setzen: Es trägt den Schreibschutz
|
|
||||||
(muss also vor loadActiveIntoEditor() stehen) und macht den Neu-laden-Knopf
|
|
||||||
erreichbar. Scheitert der erste Abruf — die Gegenseite fällt beobachtbar mal
|
|
||||||
aus —, bleibt der Knopf also da und man kommt ohne Neuladen weiter. */
|
|
||||||
if(s.live){ padSource = s; padBusy = true; setPadCooldown(PAD_MIN_GAP_MS); }
|
|
||||||
try{
|
try{
|
||||||
adoptRemote(s, await fetchRemote(s.fetchUrl));
|
adoptRemote(s, await fetchRemote(s.fetchUrl));
|
||||||
}catch(err){
|
}catch(err){
|
||||||
/* CORS-Fehler melden sich als „TypeError: Failed to fetch" ohne Details —
|
/* CORS-Fehler melden sich als „TypeError: Failed to fetch" ohne Details —
|
||||||
der Warntext nennt CORS daher ausdrücklich als wahrscheinliche Ursache.
|
der Warntext nennt CORS daher ausdrücklich als wahrscheinliche Ursache. */
|
||||||
Bei einem Pad ist das nicht das Ende: der Knopf holt es nach und räumt die
|
|
||||||
Warnung weg, sobald ein Abruf gelingt. */
|
|
||||||
sourceWarning = {type:'sourceLoad', url: s.fetchUrl, error: (err && err.message) || String(err)};
|
sourceWarning = {type:'sourceLoad', url: s.fetchUrl, error: (err && err.message) || String(err)};
|
||||||
updatePadLink(); /* Knopf zeigen, obwohl kein Dokument entstanden ist */
|
|
||||||
render();
|
render();
|
||||||
}finally{
|
|
||||||
if(s.live) padBusy = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4886,204 +4746,6 @@ async function putOnServer(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Pad: auf Knopfdruck neu holen (D31) ----------
|
|
||||||
Kein Hintergrund-Takt. Der erste Entwurf holte alle 2,5 s selbsttätig — und
|
|
||||||
lief damit in Etherpads **Drosselung**: `importExportRateLimiting` ist
|
|
||||||
serienmäßig an und lässt 10 Abrufe je 90 s und IP zu, der Takt wollte 36.
|
|
||||||
Danach hält die Gegenseite die Verbindung einfach offen (kein `429`, keine
|
|
||||||
Kopfzeilen), der Abbruch reißt sie ab — im Netzwerk-Mitschnitt „cancelled",
|
|
||||||
und es kommt kaum je etwas an. Ein Knopf ist ohnehin ehrlicher gegenüber
|
|
||||||
fremder Infrastruktur: geholt wird, wenn jemand es will. Zusammen mit
|
|
||||||
„Was ist neu?" (D28) ergibt das die bessere Geschichte — Knopf drücken, und
|
|
||||||
was seither in Produktion ging, leuchtet auf. */
|
|
||||||
const PAD_FETCH_TIMEOUT_MS = 20000;
|
|
||||||
/* Mindestabstand zwischen zwei Abrufen. 10 s ergibt höchstens **9** Abrufe je
|
|
||||||
90 s und bleibt damit beweisbar unter Etherpads Voreinstellung (10 je 90 s) —
|
|
||||||
die Drosselung wird also gar nicht erst ausgelöst. Nach einem Abbruch sind wir
|
|
||||||
schon drüber: dann das ganze Fenster abwarten, statt weiter dagegen zu rennen. */
|
|
||||||
const PAD_MIN_GAP_MS = 10000;
|
|
||||||
const PAD_BACKOFF_MS = 90000;
|
|
||||||
let padSource = null; /* Beschreiber der aktiven Pad-Quelle, oder null */
|
|
||||||
let padBusy = false; /* ein Abruf unterwegs — schützt vor Doppelklick */
|
|
||||||
let padNextAllowed = 0; /* Zeitpunkt (ms), ab dem wieder geholt werden darf */
|
|
||||||
let padCoolTimer = null;
|
|
||||||
/* Wer das Pad-Dokument löscht, meint es: danach gibt es nichts mehr zu holen. */
|
|
||||||
function stopPad(){ padSource = null; }
|
|
||||||
function padCoolLeft(){ return Math.max(0, padNextAllowed - Date.now()); }
|
|
||||||
function setPadCooldown(ms){ padNextAllowed = Date.now() + ms; tickPadCooldown(); }
|
|
||||||
/* Der Sekundenzähler läuft **nur** während der Sperre und hält sich selbst an. */
|
|
||||||
function tickPadCooldown(){
|
|
||||||
if(padCoolTimer){ clearInterval(padCoolTimer); padCoolTimer = null; }
|
|
||||||
updatePadRefreshLabel();
|
|
||||||
if(padCoolLeft() <= 0) return;
|
|
||||||
padCoolTimer = setInterval(() => {
|
|
||||||
updatePadRefreshLabel();
|
|
||||||
if(padCoolLeft() <= 0){ clearInterval(padCoolTimer); padCoolTimer = null; }
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
/* Sperre sichtbar machen, aber den Knopf **klickbar** lassen: Ein `disabled`
|
|
||||||
erklärt nichts. So kann der Klick den Grund melden (Live-Region `#warn`). */
|
|
||||||
function updatePadRefreshLabel(){
|
|
||||||
if(!padRefreshBtn || padRefreshBtn.hidden) return;
|
|
||||||
const left = Math.ceil(padCoolLeft()/1000);
|
|
||||||
padRefreshBtn.classList.toggle('cooling', left > 0);
|
|
||||||
padRefreshBtn.setAttribute('aria-disabled', left > 0 ? 'true' : 'false');
|
|
||||||
const tip = left > 0 ? t('padWait', {seconds: left}) : t('padRefresh');
|
|
||||||
padRefreshBtn.title = tip;
|
|
||||||
padRefreshBtn.setAttribute('aria-label', tip);
|
|
||||||
}
|
|
||||||
/* Rückmeldung während des Abrufs: Bei einer gedrosselten Gegenseite können das
|
|
||||||
die vollen 20 s sein — ohne Zeichen wirkt der Knopf kaputt. */
|
|
||||||
function setPadBusy(on){
|
|
||||||
if(!padRefreshBtn) return;
|
|
||||||
padRefreshBtn.classList.toggle('busy', on);
|
|
||||||
padRefreshBtn.setAttribute('aria-busy', on ? 'true' : 'false');
|
|
||||||
}
|
|
||||||
async function refreshPad(){
|
|
||||||
const s = padSource;
|
|
||||||
if(!s || padBusy) return;
|
|
||||||
if(padCoolLeft() > 0){
|
|
||||||
/* Nicht heimlich nichts tun: Der Grund ist eine fremde Grenze, und die
|
|
||||||
gehört gesagt — sonst wirkt der Knopf kaputt. Die Meldung landet im
|
|
||||||
Warnbereich, der eine Live-Region ist und deshalb angesagt wird. */
|
|
||||||
sourceWarning = {type:'padRateLimit', seconds: Math.ceil(padCoolLeft()/1000)};
|
|
||||||
render();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
padBusy = true;
|
|
||||||
setPadBusy(true);
|
|
||||||
setPadCooldown(PAD_MIN_GAP_MS);
|
|
||||||
let text;
|
|
||||||
try{ text = await fetchRemote(s.fetchUrl, PAD_FETCH_TIMEOUT_MS); }
|
|
||||||
catch(err){
|
|
||||||
/* Anders als ein Hintergrund-Takt ist das eine bewusste Handlung — sie
|
|
||||||
braucht eine Antwort, auch wenn sie schiefgeht. Ein Abbruch bekommt einen
|
|
||||||
**eigenen** Warnungstyp: `sourceLoad` zeigt auf CORS, und bei einer
|
|
||||||
Drosselung schickte das den Leser auf die falsche Fährte. */
|
|
||||||
if(err && err.name === 'AbortError'){
|
|
||||||
/* Abbruch heißt: Wir sind schon in der Drosselung. Weiterklicken hilft
|
|
||||||
nicht, also das ganze Fenster abwarten — und das dem Nutzer sagen. */
|
|
||||||
setPadCooldown(PAD_BACKOFF_MS);
|
|
||||||
sourceWarning = {type:'sourceTimeout', url: s.fetchUrl,
|
|
||||||
seconds: Math.round(PAD_FETCH_TIMEOUT_MS/1000)};
|
|
||||||
} else {
|
|
||||||
sourceWarning = {type:'sourceLoad', url: s.fetchUrl, error: (err && err.message) || String(err)};
|
|
||||||
}
|
|
||||||
render();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
finally{ padBusy = false; setPadBusy(false); }
|
|
||||||
if(padRefreshBtn) flashBtn(padRefreshBtn);
|
|
||||||
const d = docs.find(x => x.id === s.id);
|
|
||||||
if(!d){
|
|
||||||
/* Kein Dokument: der erste Ladeversuch ist gescheitert. Jetzt nachholen —
|
|
||||||
anlegen und aktivieren wie beim Laden. */
|
|
||||||
adoptRemote(s, text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/* Ein geglückter Abruf räumt die Warnung des ersten Versuchs weg — sonst
|
|
||||||
stünde „konnte nicht geladen werden", während der Text längst da ist. */
|
|
||||||
const hadWarning = !!sourceWarning;
|
|
||||||
sourceWarning = null;
|
|
||||||
if(text === d.text){ if(hadWarning) render(); return; }
|
|
||||||
d.text = text;
|
|
||||||
persistDocs();
|
|
||||||
if(activeId !== d.id) return; /* im Hintergrund still aktualisiert */
|
|
||||||
/* Auswahl und Scrollstand erhalten — der Sprung aus dem Diagramm (D25) und
|
|
||||||
die Cursor-Zeile sollen einen Abruf überleben. */
|
|
||||||
const top = src.scrollTop, a = src.selectionStart, b = src.selectionEnd;
|
|
||||||
src.value = text;
|
|
||||||
try{ src.setSelectionRange(a, b); }catch(_){}
|
|
||||||
src.scrollTop = top;
|
|
||||||
computeFresh(d.id, text); /* Basis bleibt die zuletzt bestätigte Fassung */
|
|
||||||
render();
|
|
||||||
updateFreshBtn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Ansicht bei einem Pad-Dokument: beide | nur Pad | nur Spiegel ----
|
|
||||||
Das Pad lässt sich einbetten (nachgemessen: kein `X-Frame-Options`, keine CSP
|
|
||||||
mit `frame-ancestors`), doch es ersetzt nicht einfach das Textfeld: Alt+Klick
|
|
||||||
und die Cursor-Zeile (D25) arbeiten auf **unserem** `<textarea>`, und in einen
|
|
||||||
fremdstämmigen Rahmen kommt kein DOM-Zugriff. Deshalb drei Ansichten statt
|
|
||||||
einer Entscheidung — in „beide" bleibt der Spiegel schmal ziehbar und trägt
|
|
||||||
weiter die Sprünge.
|
|
||||||
|
|
||||||
Ein **Wähler** statt dreier Knöpfe: Die Editor-Titelzeile ist schon voll
|
|
||||||
(Dokument, Pad, Neu laden, Kopieren, Legende, Fenster), und auf kleinem
|
|
||||||
Bildschirm (D17) ist sie es dreifach. Derselbe Reihum-Griff wie beim
|
|
||||||
Modus-Wähler dort.
|
|
||||||
|
|
||||||
Der Rahmen wird **nur geladen, wenn er sichtbar ist** (`about:blank` sonst).
|
|
||||||
Das ist kein Geiz: Ein geladenes Pad verbindet sich per Socket und macht dich
|
|
||||||
in der Anwesenden-Liste sichtbar. „Nur Spiegel" ist damit die Ansicht, die
|
|
||||||
nichts von dir verrät. */
|
|
||||||
const padFrame = document.getElementById('padFrame');
|
|
||||||
const srcArea = document.getElementById('srcArea');
|
|
||||||
const padGutter = document.getElementById('padGutter');
|
|
||||||
const padViewBtn = document.getElementById('padViewBtn');
|
|
||||||
function applyPadView(){
|
|
||||||
const d = activeDoc();
|
|
||||||
const isPad = !!(d && padSource && d.id === padSource.id);
|
|
||||||
const show = isPad ? padView : 'text';
|
|
||||||
if(srcArea) srcArea.className = 'src-area pv-' + show;
|
|
||||||
if(padViewBtn){
|
|
||||||
padViewBtn.hidden = !isPad;
|
|
||||||
padViewBtn.dataset.view = show;
|
|
||||||
const tip = t('padViewTooltip', {state: t('padView_' + show)});
|
|
||||||
padViewBtn.title = tip;
|
|
||||||
padViewBtn.setAttribute('aria-label', tip);
|
|
||||||
}
|
|
||||||
if(padGutter) padGutter.hidden = (show !== 'both');
|
|
||||||
if(!padFrame) return;
|
|
||||||
const wantFrame = isPad && show !== 'text';
|
|
||||||
padFrame.hidden = !wantFrame;
|
|
||||||
const wantSrc = wantFrame ? padViewUrl(padSource.source) : 'about:blank';
|
|
||||||
if(padFrame.getAttribute('src') !== wantSrc) padFrame.setAttribute('src', wantSrc);
|
|
||||||
}
|
|
||||||
/* Zeilennummern und Monospace sind für diese Notation die richtige Darstellung;
|
|
||||||
der Chat kostet in einem schmalen Rahmen nur Platz. */
|
|
||||||
function padViewUrl(padUrl){
|
|
||||||
return padUrl + '?showChat=false&showLineNumbers=true&useMonospaceFont=true';
|
|
||||||
}
|
|
||||||
function cyclePadView(){
|
|
||||||
padView = PAD_VIEWS[(PAD_VIEWS.indexOf(padView) + 1) % PAD_VIEWS.length];
|
|
||||||
applyPadView();
|
|
||||||
saveUI();
|
|
||||||
}
|
|
||||||
if(padViewBtn) padViewBtn.addEventListener('click', cyclePadView);
|
|
||||||
|
|
||||||
/* Ein Pad-Dokument wird im Pad bearbeitet, nicht hier (D31): Textfeld
|
|
||||||
schreibgeschützt, Knopf in der Titelzeile öffnet das Pad im neuen Tab. Ohne
|
|
||||||
den Schutz verschwände getippter Text beim nächsten Abruf. */
|
|
||||||
const padLink = document.getElementById('padLink');
|
|
||||||
const padRefreshBtn = document.getElementById('padRefresh');
|
|
||||||
function updatePadLink(){
|
|
||||||
const d = activeDoc();
|
|
||||||
const isPad = !!(d && padSource && d.id === padSource.id);
|
|
||||||
src.readOnly = isPad;
|
|
||||||
updateSnapBtn(); /* Pad-Dokumente sammeln keine Stände (D54) */
|
|
||||||
src.classList.toggle('readonly', isPad);
|
|
||||||
if(srcWrap) srcWrap.classList.toggle('readonly', isPad); /* Streifen mit abtönen */
|
|
||||||
if(isPad) src.title = t('padReadonly'); else src.removeAttribute('title');
|
|
||||||
if(padLink){
|
|
||||||
padLink.hidden = !isPad;
|
|
||||||
if(isPad){
|
|
||||||
padLink.href = padSource.source;
|
|
||||||
padLink.title = t('padEdit');
|
|
||||||
padLink.setAttribute('aria-label', t('padEdit'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Der Neu-laden-Knopf erscheint auch, wenn es das Pad-Dokument noch **nicht**
|
|
||||||
gibt: Dann ist der erste Abruf gescheitert, und genau dieser Knopf ist der
|
|
||||||
Weg zurück — ohne ihn bliebe nur Neuladen der Seite. */
|
|
||||||
if(padRefreshBtn){
|
|
||||||
const pending = !!(padSource && !docs.some(x => x.id === padSource.id));
|
|
||||||
padRefreshBtn.hidden = !(isPad || pending);
|
|
||||||
updatePadRefreshLabel(); /* trägt auch den laufenden Sperr-Zähler */
|
|
||||||
}
|
|
||||||
applyPadView();
|
|
||||||
}
|
|
||||||
if(padRefreshBtn) padRefreshBtn.addEventListener('click', refreshPad);
|
|
||||||
docTrigger.addEventListener('click', e => {
|
docTrigger.addEventListener('click', e => {
|
||||||
/* Desktop: ist der Editor minimiert, stellt ein Klick ihn wieder her
|
/* Desktop: ist der Editor minimiert, stellt ein Klick ihn wieder her
|
||||||
(Bubbling zur Titelzeile) statt das Menü zu öffnen. */
|
(Bubbling zur Titelzeile) statt das Menü zu öffnen. */
|
||||||
@@ -5228,43 +4890,6 @@ hintGutter.addEventListener('dblclick', () => {
|
|||||||
(D26): gezogen wird von der Spiegelseite aus, Doppelklick setzt zurück,
|
(D26): gezogen wird von der Spiegelseite aus, Doppelklick setzt zurück,
|
||||||
Ausrichtung folgt der Panel-Anordnung. Beide Werte werden getrennt gehalten,
|
Ausrichtung folgt der Panel-Anordnung. Beide Werte werden getrennt gehalten,
|
||||||
damit ein Moduswechsel die jeweils andere Aufteilung nicht zerstört. */
|
damit ein Moduswechsel die jeweils andere Aufteilung nicht zerstört. */
|
||||||
const PAD_MIN = 120; /* schmal darf er werden — aber lesbar bleiben */
|
|
||||||
const PAD_MAX_SHARE = 0.85;
|
|
||||||
let padDragging = false;
|
|
||||||
padGutter.addEventListener('pointerdown', e => {
|
|
||||||
padDragging = true;
|
|
||||||
padGutter.classList.add('dragging');
|
|
||||||
padGutter.setPointerCapture(e.pointerId);
|
|
||||||
document.body.style.userSelect = 'none';
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
padGutter.addEventListener('pointermove', e => {
|
|
||||||
if(!padDragging) return;
|
|
||||||
const b = srcArea.getBoundingClientRect();
|
|
||||||
if(editorStacked()){
|
|
||||||
const h = Math.min(Math.max(b.bottom - e.clientY, PAD_MIN), b.height * PAD_MAX_SHARE);
|
|
||||||
app.style.setProperty('--prow', Math.round(h) + 'px');
|
|
||||||
} else {
|
|
||||||
const w = Math.min(Math.max(b.right - e.clientX, PAD_MIN), b.width * PAD_MAX_SHARE);
|
|
||||||
app.style.setProperty('--pcol', Math.round(w) + 'px');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
function endPadDrag(e){
|
|
||||||
if(!padDragging) return;
|
|
||||||
padDragging = false;
|
|
||||||
padGutter.classList.remove('dragging');
|
|
||||||
document.body.style.userSelect = '';
|
|
||||||
try{ padGutter.releasePointerCapture(e.pointerId); }catch(_){}
|
|
||||||
saveUI();
|
|
||||||
}
|
|
||||||
padGutter.addEventListener('pointerup', endPadDrag);
|
|
||||||
padGutter.addEventListener('pointercancel', endPadDrag);
|
|
||||||
padGutter.addEventListener('dblclick', () => {
|
|
||||||
app.style.removeProperty('--pcol');
|
|
||||||
app.style.removeProperty('--prow');
|
|
||||||
saveUI();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Modus-Wähler auf kleinem Bildschirm: es ist nur das aktive Icon sichtbar,
|
/* Modus-Wähler auf kleinem Bildschirm: es ist nur das aktive Icon sichtbar,
|
||||||
Tippen schaltet zum nächsten Modus (reihum). Auf normaler Größe bleibt es
|
Tippen schaltet zum nächsten Modus (reihum). Auf normaler Größe bleibt es
|
||||||
der Dreier-Umschalter — dort kehrt der Handler sofort zurück. */
|
der Dreier-Umschalter — dort kehrt der Handler sofort zurück. */
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
/* Werkbaum — Adressen für Dokumente von außen (headless, D31).
|
|
||||||
Hier steht nur reine Logik, damit sie testbar ist; das Holen selbst und der
|
|
||||||
ganze DOM-Kram bleiben in app.js. */
|
|
||||||
|
|
||||||
/* Pad-Adresse normalisieren (D31). Eingabe ist die Adresse aus der Browser-
|
|
||||||
Adresszeile — ohne Export-Pfad, den hängt Werkbaum selbst an. Abgeschnitten
|
|
||||||
werden ein versehentlich mitgegebener Export- oder /timeslider-Pfad, Query,
|
|
||||||
Fragment und Schrägstriche am Ende: derselbe Pad soll genau ein Dokument
|
|
||||||
ergeben, gleich in welcher Schreibweise der Link kam.
|
|
||||||
|
|
||||||
Verlangt wird `/p/<name>` am Ende (auch unter einem Unterpfad montiert) — das
|
|
||||||
ist die Prüfung, ob überhaupt eine Pad-Adresse vorliegt. Erlaubt sind nur
|
|
||||||
http(s), wie bei ?sourceUrl= (D23).
|
|
||||||
|
|
||||||
Rückgabe {pad, text} oder null. `pad` ist zugleich Name und Identität des
|
|
||||||
Dokuments — die **vollständige** URL, nicht der bloße Pad-Name: Pad-Namen sind
|
|
||||||
nur pro Instanz eindeutig, zwei Hosts mit je einem Pad `plan` wären sonst im
|
|
||||||
Wähler nicht zu unterscheiden. */
|
|
||||||
export function padUrls(raw, base){
|
|
||||||
let u;
|
|
||||||
try{ u = base ? new URL(raw, base) : new URL(raw); }catch(_){ return null; }
|
|
||||||
if(u.protocol !== 'http:' && u.protocol !== 'https:') return null;
|
|
||||||
const path = u.pathname.replace(/\/+$/, '').replace(/\/(export\/[^/]+|timeslider)$/, '');
|
|
||||||
if(!/\/p\/[^/]+$/.test(path)) return null;
|
|
||||||
u.pathname = path; u.search = ''; u.hash = '';
|
|
||||||
const pad = u.href;
|
|
||||||
return {pad, text: pad + '/export/txt'};
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
dafür ist reine Logik — sie hing nur an `localStorage`, `Date.now()` und
|
dafür ist reine Logik — sie hing nur an `localStorage`, `Date.now()` und
|
||||||
dem `<textarea>` fest und war deshalb nicht prüfbar.
|
dem `<textarea>` fest und war deshalb nicht prüfbar.
|
||||||
|
|
||||||
Aufteilung nach dem Vorbild von remote.js (D31): Hier steht, WAS gilt; in
|
Aufteilung wie bei den übrigen Modulen: Hier steht, WAS gilt; in
|
||||||
app.js bleibt, WOHER die Werte kommen (aktives Dokument, Schreibschutz,
|
app.js bleibt, WOHER die Werte kommen (aktives Dokument, Schreibschutz,
|
||||||
Menü) und WOHIN sie gehen (`localStorage`). Der Speicher wird als
|
Menü) und WOHIN sie gehen (`localStorage`). Der Speicher wird als
|
||||||
`{setItem, removeItem}` hereingereicht, die Uhr als Zahl — beides lässt sich
|
`{setItem, removeItem}` hereingereicht, die Uhr als Zahl — beides lässt sich
|
||||||
|
|||||||
+3
-54
@@ -270,29 +270,8 @@
|
|||||||
Horizontal nebeneinander, vertikal (.side) untereinander. */
|
Horizontal nebeneinander, vertikal (.side) untereinander. */
|
||||||
.editor-body{display:flex;align-items:stretch;flex:1 1 auto;min-height:0}
|
.editor-body{display:flex;align-items:stretch;flex:1 1 auto;min-height:0}
|
||||||
.editor-body textarea{flex:1 1 auto;min-width:0;min-height:0}
|
.editor-body textarea{flex:1 1 auto;min-width:0;min-height:0}
|
||||||
/* Pad-Rahmen | interner Spiegel (D31). Eigener Bereich, damit die
|
/* Zeilennummern + Textfeld sind eine Spalte (`.src-wrap`, D33). */
|
||||||
Legenden-Aufteilung (D26) unangetastet bleibt. Nur in der Ansicht „beide"
|
|
||||||
bekommt der Spiegel eine feste Größe (`--pcol`/`--prow`) — sonst füllt er
|
|
||||||
wie bisher den Bereich. Die 85 %-Grenze steht zusätzlich hier im CSS, weil
|
|
||||||
der gespeicherte Wert ein px-Wert ist und der Bereich später schrumpfen
|
|
||||||
kann (dieselbe Falle wie bei der Legende, D26). */
|
|
||||||
.src-area{display:flex;align-items:stretch;flex:1 1 auto;min-width:0;min-height:0}
|
|
||||||
.padframe{flex:1 1 auto;min-width:0;min-height:0;border:0;background:var(--card)}
|
|
||||||
.padframe[hidden]{display:none}
|
|
||||||
/* Zeilennummern + Textfeld sind eine Spalte (`.src-wrap`). Der Pad-Splitter
|
|
||||||
teilt gegen diese Spalte — nicht gegen das nackte Textfeld, sonst stünde der
|
|
||||||
Zahlenstreifen außerhalb der geteilten Breite. */
|
|
||||||
.src-wrap{display:flex;align-items:stretch;flex:1 1 auto;min-width:0;min-height:0}
|
.src-wrap{display:flex;align-items:stretch;flex:1 1 auto;min-width:0;min-height:0}
|
||||||
.src-area.pv-both>.src-wrap{
|
|
||||||
flex:0 0 var(--pcol,300px);max-width:85%;
|
|
||||||
border-left:1px dashed rgba(36,52,71,.18);
|
|
||||||
}
|
|
||||||
.src-area.pv-pad>.src-wrap{display:none}
|
|
||||||
.side .src-area,body.mobile .src-area{flex-direction:column}
|
|
||||||
.side .src-area.pv-both>.src-wrap,body.mobile .src-area.pv-both>.src-wrap{
|
|
||||||
flex:0 0 var(--prow,180px);max-width:none;max-height:85%;
|
|
||||||
border-left:0;border-top:1px dashed rgba(36,52,71,.18);
|
|
||||||
}
|
|
||||||
/* Legende: bewusst KEIN <details> mehr. Chrome legt den Details-Inhalt in
|
/* Legende: bewusst KEIN <details> mehr. Chrome legt den Details-Inhalt in
|
||||||
`::details-content`; dadurch war `.hint` kein Flex-Kind von `.agenda` mehr,
|
`::details-content`; dadurch war `.hint` kein Flex-Kind von `.agenda` mehr,
|
||||||
`flex:1/min-height:0` liefen ins Leere und der Inhalt wurde geclippt statt
|
`flex:1/min-height:0` liefen ins Leere und der Inhalt wurde geclippt statt
|
||||||
@@ -343,17 +322,6 @@
|
|||||||
.side .agenda.open{width:auto;max-width:none;height:var(--hrow,220px);max-height:85%}
|
.side .agenda.open{width:auto;max-width:none;height:var(--hrow,220px);max-height:85%}
|
||||||
.side .hint-gutter{cursor:row-resize}
|
.side .hint-gutter{cursor:row-resize}
|
||||||
.side .hint-gutter::before{width:38px;height:3px}
|
.side .hint-gutter::before{width:38px;height:3px}
|
||||||
/* Splitter Pad|Spiegel — gleiches Idiom wie der Legenden-Splitter. */
|
|
||||||
.pad-gutter{
|
|
||||||
flex:0 0 10px;align-self:stretch;
|
|
||||||
cursor:col-resize;touch-action:none;
|
|
||||||
display:flex;align-items:center;justify-content:center;
|
|
||||||
}
|
|
||||||
.pad-gutter[hidden]{display:none}
|
|
||||||
.pad-gutter::before{content:'';width:3px;height:38px;border-radius:2px;background:rgba(36,52,71,.18)}
|
|
||||||
.pad-gutter:hover::before,.pad-gutter.dragging::before{background:var(--or)}
|
|
||||||
.side .pad-gutter,body.mobile .pad-gutter{cursor:row-resize}
|
|
||||||
.side .pad-gutter::before,body.mobile .pad-gutter::before{width:38px;height:3px}
|
|
||||||
|
|
||||||
.panel{
|
.panel{
|
||||||
background:var(--card);
|
background:var(--card);
|
||||||
@@ -431,22 +399,9 @@
|
|||||||
kopieren, aber dieselbe Erfolgs-Rückmeldung geben (Stand sichern, D54). */
|
kopieren, aber dieselbe Erfolgs-Rückmeldung geben (Stand sichern, D54). */
|
||||||
.copybtn.done .ic-copy,.copybtn.done .ic-main{display:none}
|
.copybtn.done .ic-copy,.copybtn.done .ic-main{display:none}
|
||||||
.copybtn.done .ic-done{display:block}
|
.copybtn.done .ic-done{display:block}
|
||||||
/* „im Pad bearbeiten" (D31) — nur bei einem ?etherpad=-Dokument sichtbar.
|
|
||||||
Das [hidden] braucht die eigene Regel, weil .copybtn ein display setzt. */
|
|
||||||
.padlink{text-decoration:none}
|
|
||||||
.padlink[hidden],.padrefresh[hidden],.padview[hidden]{display:none}
|
|
||||||
/* Der Ansichts-Wähler zeigt seinen Zustand als Position im Rahmen: geteilt,
|
|
||||||
links gefüllt (Pad) oder rechts gefüllt (Spiegel). Zwei Klassen, damit die
|
|
||||||
Regel die allgemeinere `.copybtn svg{display:block}` sicher schlägt. */
|
|
||||||
.copybtn.padview .ic-both,
|
|
||||||
.copybtn.padview .ic-pad,
|
|
||||||
.copybtn.padview .ic-text{display:none}
|
|
||||||
.copybtn.padview[data-view="both"] .ic-both,
|
|
||||||
.copybtn.padview[data-view="pad"] .ic-pad,
|
|
||||||
.copybtn.padview[data-view="text"] .ic-text{display:block}
|
|
||||||
/* Falt-Durchschalter (SPEC §9/D75): vier Icons, sichtbar ist das des
|
/* Falt-Durchschalter (SPEC §9/D75): vier Icons, sichtbar ist das des
|
||||||
NÄCHSTEN Schritts — `data-next` setzt updateFoldBtn(). Zwei Klassen wie
|
NÄCHSTEN Schritts — `data-next` setzt updateFoldBtn(). Zwei Klassen, damit
|
||||||
beim Pad-Wähler, damit die Regel `.copybtn svg{display:block}` schlägt. */
|
die Regel `.copybtn svg{display:block}` schlägt. */
|
||||||
.copybtn.foldbtn .fc-small,
|
.copybtn.foldbtn .fc-small,
|
||||||
.copybtn.foldbtn .fc-path,
|
.copybtn.foldbtn .fc-path,
|
||||||
.copybtn.foldbtn .fc-closed,
|
.copybtn.foldbtn .fc-closed,
|
||||||
@@ -596,11 +551,6 @@
|
|||||||
color:var(--ink);background:var(--card);
|
color:var(--ink);background:var(--card);
|
||||||
tab-size:2;
|
tab-size:2;
|
||||||
}
|
}
|
||||||
/* Pad-Dokument (D31): hier nur lesen, geschrieben wird im Pad. Abgesetzt über
|
|
||||||
den Hintergrund, damit der Schreibschutz nicht als hängendes Textfeld
|
|
||||||
erscheint — die Schrift bleibt aber Tinte: hier wird gelesen, der Plantext
|
|
||||||
ist der Hauptinhalt und kein ausgegrautes Formularfeld. */
|
|
||||||
textarea.readonly{background:#F1F4F7;cursor:default}
|
|
||||||
/* Zeilennummern (D33). Die Warnungen nennen Zeilennummern — ohne Streifen
|
/* Zeilennummern (D33). Die Warnungen nennen Zeilennummern — ohne Streifen
|
||||||
muss man sie im Textfeld abzählen. Jede Zahl steht auf einer **gemessenen**
|
muss man sie im Textfeld abzählen. Jede Zahl steht auf einer **gemessenen**
|
||||||
Höhe (`renderLineNos()` in app.js), nicht auf „Zeilenhöhe × n"; seit D49
|
Höhe (`renderLineNos()` in app.js), nicht auf „Zeilenhöhe × n"; seit D49
|
||||||
@@ -617,7 +567,6 @@
|
|||||||
color:var(--muted);
|
color:var(--muted);
|
||||||
-webkit-user-select:none;user-select:none;
|
-webkit-user-select:none;user-select:none;
|
||||||
}
|
}
|
||||||
.src-wrap.readonly .lineno{background:#F1F4F7}
|
|
||||||
.lineno-inner{position:absolute;top:0;left:0;right:0}
|
.lineno-inner{position:absolute;top:0;left:0;right:0}
|
||||||
.lineno-inner span{
|
.lineno-inner span{
|
||||||
position:absolute;right:6px;
|
position:absolute;right:6px;
|
||||||
|
|||||||
@@ -39,18 +39,14 @@
|
|||||||
exakte Suche (D42); zeilenlos
|
exakte Suche (D42); zeilenlos
|
||||||
- sourceLoad { url, error } — ?sourceUrl= nicht ladbar (D23); ohne
|
- sourceLoad { url, error } — ?sourceUrl= nicht ladbar (D23); ohne
|
||||||
Zeilennummer, erscheint dadurch zuoberst
|
Zeilennummer, erscheint dadurch zuoberst
|
||||||
- padRateLimit { seconds } — zu früh nachgeladen; Werkbaum hat gar nicht
|
- padGone { } — ein alter ?etherpad=-Link: die Anbindung
|
||||||
erst geholt, um Etherpads Grenze nicht
|
ist ausgebaut (D78), der Text zeigt auf
|
||||||
auszulösen (D31)
|
?live=; zeilenlos
|
||||||
- liveLoad { url, error } — Server-Dokument (?live=, D76) nicht
|
- liveLoad { url, error } — Server-Dokument (?live=, D76) nicht
|
||||||
erreichbar oder keine Dokument-Adresse
|
erreichbar oder keine Dokument-Adresse
|
||||||
- liveStale { error } — eigene Änderung nicht anwendbar
|
- liveStale { error } — eigene Änderung nicht anwendbar
|
||||||
(Prüfsumme, Index, Basis verdichtet);
|
(Prüfsumme, Index, Basis verdichtet);
|
||||||
der Client hat einmal neu geladen (D76)
|
der Client hat einmal neu geladen (D76) */
|
||||||
- sourceTimeout { url, seconds } — Abruf abgebrochen, Gegenseite zu langsam
|
|
||||||
(D31). Eigener Typ, weil `sourceLoad` auf
|
|
||||||
CORS zeigt — bei einem Zeitablauf schickt
|
|
||||||
das den Leser auf die falsche Fährte */
|
|
||||||
|
|
||||||
import { esc } from './render.js';
|
import { esc } from './render.js';
|
||||||
|
|
||||||
@@ -95,14 +91,12 @@ function build(w, t, esc){
|
|||||||
return t('cheapApproxWarn');
|
return t('cheapApproxWarn');
|
||||||
case 'sourceLoad':
|
case 'sourceLoad':
|
||||||
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
return t('sourceLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
||||||
case 'padRateLimit':
|
case 'padGone':
|
||||||
return t('padRateLimitWarn', {seconds: w.seconds});
|
return t('padGoneWarn');
|
||||||
case 'liveLoad':
|
case 'liveLoad':
|
||||||
return t('liveLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
return t('liveLoadWarn', {url: esc(w.url), error: esc(w.error)});
|
||||||
case 'liveStale':
|
case 'liveStale':
|
||||||
return t('liveStaleWarn', {error: esc(w.error)});
|
return t('liveStaleWarn', {error: esc(w.error)});
|
||||||
case 'sourceTimeout':
|
|
||||||
return t('sourceTimeoutWarn', {url: esc(w.url), seconds: w.seconds});
|
|
||||||
default:
|
default:
|
||||||
return `${esc(String(w.type))} (${w.line ?? '?'})`;
|
return `${esc(String(w.type))} (${w.line ?? '?'})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { padUrls } from '../src/remote.js';
|
|
||||||
|
|
||||||
const PAD = 'https://pad.example.org/p/mein-plan';
|
|
||||||
|
|
||||||
describe('padUrls — Pad-Adresse normalisieren (D31)', () => {
|
|
||||||
it('hängt den Klartext-Export an', () => {
|
|
||||||
expect(padUrls(PAD)).toEqual({ pad: PAD, text: PAD + '/export/txt' });
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Derselbe Pad soll genau EIN Dokument ergeben — die Identität leitet sich
|
|
||||||
aus `pad` ab, also müssen alle Schreibweisen darauf zusammenfallen. */
|
|
||||||
it.each([
|
|
||||||
['Schrägstrich am Ende', PAD + '/'],
|
|
||||||
['mehrere Schrägstriche', PAD + '///'],
|
|
||||||
['Export-Pfad mitgegeben', PAD + '/export/txt'],
|
|
||||||
['anderer Export', PAD + '/export/html'],
|
|
||||||
['Timeslider', PAD + '/timeslider'],
|
|
||||||
['Query dran', PAD + '?showChat=false'],
|
|
||||||
['Fragment dran', PAD + '#anker'],
|
|
||||||
['Query und Schrägstrich', PAD + '/?showControls=false'],
|
|
||||||
])('fällt auf dieselbe Pad-URL zusammen: %s', (_name, input) => {
|
|
||||||
expect(padUrls(input)).toEqual({ pad: PAD, text: PAD + '/export/txt' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('erlaubt eine Montage unter einem Unterpfad', () => {
|
|
||||||
const sub = 'https://example.org/etherpad/p/plan';
|
|
||||||
expect(padUrls(sub)).toEqual({ pad: sub, text: sub + '/export/txt' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('erlaubt http neben https', () => {
|
|
||||||
const h = 'http://pad.example.org/p/plan';
|
|
||||||
expect(padUrls(h).pad).toBe(h);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('behält den Port', () => {
|
|
||||||
const h = 'https://pad.example.org:9001/p/plan';
|
|
||||||
expect(padUrls(h)).toEqual({ pad: h, text: h + '/export/txt' });
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Zwei Pads gleichen Namens auf verschiedenen Hosts müssen unterscheidbar
|
|
||||||
bleiben — deshalb ist der Name die vollständige URL, nicht der Pad-Name. */
|
|
||||||
it('unterscheidet gleichnamige Pads verschiedener Hosts', () => {
|
|
||||||
expect(padUrls('https://a.example/p/plan').pad)
|
|
||||||
.not.toBe(padUrls('https://b.example/p/plan').pad);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
['kein /p/-Pfad', 'https://pad.example.org/mein-plan'],
|
|
||||||
['nur der Host', 'https://pad.example.org'],
|
|
||||||
['/p/ ohne Namen', 'https://pad.example.org/p/'],
|
|
||||||
['fremdes Schema', 'file:///tmp/plan.txt'],
|
|
||||||
['javascript:', 'javascript:alert(1)'],
|
|
||||||
['data:', 'data:text/plain,foo'],
|
|
||||||
['gar keine URL', 'nicht mal eine URL'],
|
|
||||||
['leer', ''],
|
|
||||||
])('weist ab: %s', (_name, input) => {
|
|
||||||
expect(padUrls(input)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('löst relative Angaben gegen die Seite auf, wenn eine Basis da ist', () => {
|
|
||||||
expect(padUrls('/p/plan', 'https://pad.example.org/x/y').pad)
|
|
||||||
.toBe('https://pad.example.org/p/plan');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ohne Basis bleibt eine relative Angabe unbrauchbar', () => {
|
|
||||||
expect(padUrls('/p/plan')).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -21,18 +21,11 @@ describe('formatWarning — vereinheitlichtes Warnungs-Modell', () => {
|
|||||||
expect(out).toBe('sourceLoadWarn|{"url":"https://a/?x=1&y=2","error":"HTTP 404"}');
|
expect(out).toBe('sourceLoadWarn|{"url":"https://a/?x=1&y=2","error":"HTTP 404"}');
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Eigener Typ, weil sourceLoadWarn auf CORS zeigt: Bei Etherpads Drosselung
|
/* Ein alter ?etherpad=-Link soll nicht still ins Leere laufen: Die Anbindung
|
||||||
(10 Abrufe je 90 s) schickte das den Leser auf die falsche Fährte (D31). */
|
ist ausgebaut (D78), der Text zeigt auf ?live=. Ohne Platzhalter — die
|
||||||
it('sourceTimeout ist ein eigener Typ und nennt die Sekunden', () => {
|
Meldung hat nichts einzusetzen. */
|
||||||
const out = formatWarning({type: 'sourceTimeout', url: 'https://p/x&y', seconds: 20}, t);
|
it('padGone meldet den ausgebauten Eingang ohne Platzhalter', () => {
|
||||||
expect(out).toBe('sourceTimeoutWarn|{"url":"https://p/x&y","seconds":20}');
|
expect(formatWarning({type: 'padGone'}, t)).toBe('padGoneWarn|undefined');
|
||||||
});
|
|
||||||
|
|
||||||
/* Kein `url` im Objekt: Der Nutzer hat gar nichts angefordert, was scheitern
|
|
||||||
konnte — Werkbaum hat den Abruf verhindert (D31). */
|
|
||||||
it('padRateLimit nennt die Restzeit und braucht keine URL', () => {
|
|
||||||
expect(formatWarning({type: 'padRateLimit', seconds: 7}, t))
|
|
||||||
.toBe('padRateLimitWarn|{"seconds":7}');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unbekannter Typ fällt auf eine generische, escapte Meldung zurück', () => {
|
it('unbekannter Typ fällt auf eine generische, escapte Meldung zurück', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user