feat: Adresszeile folgt dem Dokument, Live-Sitzung ebenso (D80)
Wer bei offenem `?live=` umschaltet, behielt die alte Adresse — optisch falsch, und ein Neuladen holte das falsche Dokument zurück. Der Parameter gehört jetzt zum aktiven Dokument, für `?live=` wie für `?sourceUrl=`; `?etherpad=` wird nur noch weggeräumt. Fremde Parameter (`?server=`) bleiben wörtlich stehen, damit die URL lesbar bleibt. Dabei gefunden: Die Live-Sitzung lief weiter, während ein anderes Dokument vorn stand — `setLiveText()` schrieb fremde Änderungen in dessen Text. Die Sitzung gehört jetzt dem sichtbaren Dokument: Umschalten beendet sie, Umschalten auf ein Server-Dokument nimmt sie auf. Was noch im Debounce steckt, geht vorher raus, und `pushLive()` hält seine Sitzung fest statt anzunehmen, dass sich über ein `await` hinweg nichts ändert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
627f6feca8
commit
fdca934cf0
@@ -110,6 +110,12 @@ oberhalb Zeilen ein, bleibt sie an ihrer Stelle im Text.
|
||||
Der **Name** ist der Titel des Dokuments (alle sehen denselben), die vollständige
|
||||
Adresse steht im Tooltip.
|
||||
|
||||
**Die Adresszeile folgt dem Dokument, das vorn ist.** Umschalten auf einen
|
||||
lokalen Plan räumt `?live=` weg, Umschalten auf ein anderes Server-Dokument
|
||||
trägt dessen Adresse ein — ein Neuladen bringt also zurück, was man vor sich
|
||||
hatte. Wer ein Server-Dokument im Menü auswählt, arbeitet darin auch wieder
|
||||
gemeinsam.
|
||||
|
||||
**Überschneiden sich zwei Änderungen wirklich** — dieselben Zeilen —, fragt ein
|
||||
Band oben, wessen Fassung gelten soll: *Fremde übernehmen* oder *Eigene
|
||||
durchsetzen*. Alles andere führt der Server selbst zusammen, ohne zu fragen.
|
||||
|
||||
@@ -105,6 +105,11 @@ above you, it stays where it was in the text.
|
||||
The document's **title** is its name (everyone sees the same one), the full
|
||||
address sits in the tooltip.
|
||||
|
||||
**The address bar follows the document you are on.** Switch to a local plan and
|
||||
`?live=` goes away; switch to another server document and its address takes its
|
||||
place — so a reload brings back what you were looking at. Picking a server
|
||||
document from the menu also puts you back into the shared session.
|
||||
|
||||
When two changes really overlap — the same lines — a bar at the top asks whose
|
||||
version should win: *take theirs* or *keep mine*. Everything else the server
|
||||
merges without asking. Nothing is lost either way: the discarded state stays in
|
||||
|
||||
@@ -19,6 +19,9 @@ reverse.
|
||||
|
||||
## 2026-08-26
|
||||
|
||||
- The address bar follows the document you switch to: `?live=` and `?sourceUrl=` name what is in front of you, so a reload brings back the same plan
|
||||
- Switching to a server document in the picker now really opens it live — before it only showed its last state
|
||||
- Fix: a foreign change to a server document could land in the text of a local document you had switched to
|
||||
- Changes in a shared document now reach the others after 0.6 s instead of 1.5 s — the wait before sending was almost the whole delay
|
||||
|
||||
- The Etherpad integration is gone: collaboration now runs through a Werkbaum backend, and an old `?etherpad=` link says so instead of doing nothing
|
||||
|
||||
@@ -6819,3 +6819,85 @@ die 600 ms sind gesetzt, und was daneben liegt (Server 39 ms, Rundlauf 130 ms,
|
||||
PATCH → sichtbar 46 ms) ist einzeln gemessen. Dieselbe Lehre wie D25
|
||||
(synthetische `TouchEvent`s), D17-Nachtrag 4 (Bildschirmtastatur) und D53
|
||||
(synthetisches Strg+Z): Was die Umgebung stellt, stellt der Emulator nicht.
|
||||
|
||||
## D80 — Die Adresszeile beschreibt das aktive Dokument, und die Live-Sitzung folgt ihm
|
||||
Gemeldet: Wer bei offenem `?live=…` auf ein anderes Dokument umschaltet, behält
|
||||
die alte Adresse — „das sieht optisch falsch aus, und beim Neuladen würde wohl
|
||||
auch das Dokument aus `live=` wieder geladen". Beides stimmt, und beim
|
||||
Nachsehen kam ein dritter, schwererer Befund dazu.
|
||||
|
||||
**Die Adresse ist kein Andenken an den Aufruf, sondern der Stand.** Sie ist der
|
||||
Link, den man weitergibt, und das, was ein Neuladen wiederherstellt. Zeigt sie
|
||||
auf etwas anderes als der Bildschirm, ist eines von beiden gelogen — und beim
|
||||
Neuladen entscheidet die Adresse. Die Regel lautet deshalb: **Der Parameter
|
||||
gehört zum aktiven Dokument.** Umschalten auf ein lokales Dokument räumt ihn
|
||||
weg, Umschalten auf ein anderes Server-Dokument tauscht ihn aus.
|
||||
|
||||
**Sie gilt für beide Eingänge, nicht nur für `?live=`.** `?sourceUrl=` (D23)
|
||||
hatte dasselbe Problem, und eine Regel, die nur für einen der beiden gilt, ist
|
||||
keine. Beide Eingänge sind ohnehin schon die **Identität** des Dokuments
|
||||
(`live:<url>`, `url:<href>`) — der Parameter lässt sich also aus der id
|
||||
zurückrechnen, statt nebenher geführt zu werden. `?etherpad=` ist ausgebaut
|
||||
(D78) und wird nur noch weggeräumt.
|
||||
|
||||
**Für `?sourceUrl=` ist das keine neue Gefahr**, obwohl der Parameter beim
|
||||
Zurückschalten wiederkommt und ein Neuladen den Text dann erneut holt (D23:
|
||||
„lokale Änderungen daran überleben ein Neuladen nicht"). Bisher stand er
|
||||
**immer** da, unabhängig davon, was vorn war — es wird also nicht mehr
|
||||
überschrieben als vorher, sondern weniger.
|
||||
|
||||
**Fremde Parameter bleiben wörtlich stehen — auch ihre Schreibweise.** Der
|
||||
naheliegende Weg über `URLSearchParams` schriebe jedes `:` und `/` als
|
||||
`%3A`/`%2F` und machte damit gerade die URL unleserlich, um die es hier geht;
|
||||
`?server=` (D76-Nachtrag 8) fiele bei einem Neubau der Adresse ganz weg.
|
||||
Maskiert wird nur, was den Query-String sonst zerrisse (`&`, `#`). Die
|
||||
entscheidbare Hälfte steht als reine Funktion in `docurl.js`
|
||||
(Hausregel D54-Nachtrag 3), die `history.replaceState`-Seite in app.js.
|
||||
|
||||
**Der dritte Befund: Die Live-Sitzung lief weiter, während ein anderes Dokument
|
||||
vorn stand.** `switchDoc()` hat bisher nur `activeId` gewechselt; `liveState`
|
||||
blieb, der Feed lief, und `setLiveText()` schreibt in `src.value` **und** in
|
||||
`activeDoc().text` — eine fremde Änderung am Server-Dokument landete also im
|
||||
Text des Dokuments, das man gerade ansieht. Nachgemessen war die Lücke echt:
|
||||
Die Meldung des Nutzers ist die Tür dazu.
|
||||
|
||||
**Also gehört die Sitzung dem sichtbaren Dokument.** Umschalten beendet sie;
|
||||
Umschalten auf ein Server-Dokument nimmt sie auf (`startLive()`, aus
|
||||
`loadLive()` herausgelöst — derselbe Weg, nur mit der URL aus dem Dokument
|
||||
statt aus dem Parameter). Der Nebengewinn ist der eigentliche: Ein
|
||||
Server-Dokument, das man im Wähler auswählt, ist danach wirklich live. Vorher
|
||||
zeigte es stumm seinen letzten Stand — die Adresse hätte also nicht nur
|
||||
optisch, sondern der Sache nach gelogen, wenn man sie einfach mitgeführt hätte.
|
||||
|
||||
**Verdrahtet an genau einer Stelle:** `loadActiveIntoEditor()` — jeder Weg zu
|
||||
einem anderen aktiven Dokument führt dort durch (Umschalten, Anlegen, Löschen,
|
||||
Datei öffnen, Server-Dokument laden). Während des Starts ruht die Regel
|
||||
(`bootDone`): `loadRemoteSource()` und `loadLive()` lesen ihre Parameter erst,
|
||||
nachdem das zuletzt aktive Dokument wiederhergestellt ist — ein vorschnelles
|
||||
Aufräumen nähme ihnen die Vorlage.
|
||||
|
||||
**Was noch im Debounce steckt, wird beim Umschalten losgeschickt.** Sonst
|
||||
verlöre ein Wechsel innerhalb von 600 ms nach dem letzten Tastendruck genau
|
||||
diese Änderung an den Server. Gesendet wird, **bevor** `activeId` wechselt —
|
||||
`pushLive()` liest `src.value` synchron, danach zeigt das Feld schon den
|
||||
anderen Text.
|
||||
|
||||
**Und `pushLive()` hält jetzt seine Sitzung fest, nicht nur deren Felder.**
|
||||
Wer während des Sendens umschaltet, beendet sie; die Fortsetzung nach dem
|
||||
`await` dürfte danach weder schreiben noch in ein `null` greifen (das
|
||||
`finally` hätte es getan). Dieselbe Sorte Annahme, die D76-Nachtrag 9 schon
|
||||
einmal an dieser Funktion korrigiert hat: dass sich über ein `await` hinweg
|
||||
nichts ändert.
|
||||
|
||||
**Nachgemessen** im Browser gegen ein lokales Backend, mit zwei
|
||||
Server-Dokumenten: Umschalten auf ein lokales Dokument räumt `?live=` weg,
|
||||
Umschalten auf das andere Server-Dokument tauscht die URL aus, `?sourceUrl=`
|
||||
verhält sich symmetrisch und bleibt unmaskiert lesbar. Eine fremde Änderung
|
||||
erreicht das per Wähler geöffnete Server-Dokument ohne Neuladen (die Sitzung
|
||||
läuft also wirklich); dieselbe Änderung, während ein lokales Dokument vorn
|
||||
steht, lässt dessen Text unangetastet (vorher hätte sie ihn überschrieben);
|
||||
beim Zurückschalten steht der Server-Stand da. Eine Zeile, im selben Zug
|
||||
getippt und umgeschaltet, kommt beim Server an. 514 Tests, davon 13 neue in
|
||||
`tests/docurl.test.js`; Gegenproben: fremde Parameter mitwerfen → genau die
|
||||
zwei danach benannten Zusicherungen fallen, `encodeURIComponent` statt der
|
||||
sparsamen Maskierung → genau die sieben, die die Lesbarkeit festhalten.
|
||||
|
||||
@@ -766,6 +766,9 @@ Seite aufgelöst; zugelassen sind nur `http`/`https`). Der Text wird als
|
||||
Tooltip); derselbe Link aktualisiert dieses Dokument, statt ein neues anzulegen.
|
||||
Ist der Parameter gesetzt, wird bei **jedem** Laden neu geholt — die URL ist die
|
||||
Quelle der Wahrheit, lokale Änderungen daran überleben ein Neuladen nicht.
|
||||
Die **Adresszeile folgt dem aktiven Dokument** (§9, `?live=`): Beim Umschalten
|
||||
auf ein anderes verschwindet der Parameter, beim Zurückschalten steht er wieder
|
||||
da.
|
||||
Scheitert das Laden (häufigster Fall: das Ziel sendet keinen
|
||||
`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der
|
||||
bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23.
|
||||
@@ -796,6 +799,13 @@ anderen, ohne neu zu laden.
|
||||
jede Version in der Historie des Servers.
|
||||
- Der Abruf läuft **nur im sichtbaren Tab**; im Hintergrund ruht er und holt
|
||||
beim Zurückkommen den Rückstand.
|
||||
- **Die Adresszeile beschreibt das Dokument, das vorn ist.** Umschalten auf ein
|
||||
lokales Dokument räumt `?live=` weg, Umschalten auf ein anderes
|
||||
Server-Dokument trägt dessen Adresse ein — ein Neuladen bringt also zurück,
|
||||
was man vor sich hatte. Dasselbe gilt für `?sourceUrl=` (oben). Und die
|
||||
gemeinsame Sitzung gehört dem sichtbaren Dokument: Wer ein Server-Dokument im
|
||||
Wähler auswählt, arbeitet darin wieder gemeinsam; wer es verlässt, hört auf,
|
||||
mitzuschreiben. Siehe D80.
|
||||
|
||||
Siehe D76 (Protokoll und Begründung) und
|
||||
`backend/docs/live-editing-proposal.md`.
|
||||
|
||||
+75
-20
@@ -8,6 +8,7 @@ import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomple
|
||||
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
|
||||
from './snapshots.js';
|
||||
import { FILE_ACCEPT, FILE_TYPES, saveFileName } from './localfile.js';
|
||||
import { LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM, docSearch } from './docurl.js';
|
||||
/* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
|
||||
vite.config.js). Zur Laufzeit gibt es kein git — und keinen Server, der
|
||||
nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */
|
||||
@@ -80,6 +81,13 @@ let mobilePane = 'diagram';
|
||||
let caretLine = null, currentNodeEl = null;
|
||||
/* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */
|
||||
let sourceWarning = null;
|
||||
/* Steht die App? Erst danach folgt die Adresszeile dem aktiven Dokument (D80)
|
||||
und wird eine Live-Sitzung beim Umschalten übernommen. Während des Starts
|
||||
dürfen beide nicht laufen: `loadRemoteSource()`/`loadLive()` lesen ihre
|
||||
Parameter erst nach dem Wiederherstellen des zuletzt aktiven Dokuments —
|
||||
ein vorschnelles Aufräumen der Adresszeile nähme ihnen die Vorlage. Hier
|
||||
oben deklariert, weil `loadActiveIntoEditor()` schon beim Start läuft. */
|
||||
let bootDone = false;
|
||||
/* „Was ist neu?" (D28): Knoten, die gegenüber der zuletzt GESEHENEN Fassung neu
|
||||
in Produktion sind. Gilt immer für genau ein Dokument (`freshDocId`) — nur
|
||||
Dokumente von außen (mitgeliefert oder ?sourceUrl=) haben eine
|
||||
@@ -3892,9 +3900,44 @@ function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text :
|
||||
/* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der
|
||||
erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */
|
||||
snapBase = src.value; closeSnapMenu();
|
||||
render(); updateDocName(); updateFreshBtn(); }
|
||||
render(); updateDocName(); updateFreshBtn();
|
||||
/* Ein Dokumentwechsel ist mehr als neuer Text im Feld: Adresszeile und
|
||||
Live-Sitzung gehören dem, was man vor sich hat (D80). Hier, weil jeder
|
||||
Weg zu einem anderen aktiven Dokument durch diese eine Stelle führt —
|
||||
Umschalten, Anlegen, Löschen, Datei öffnen, Server-Dokument laden. */
|
||||
followActiveDoc(); }
|
||||
|
||||
/* Die Adresszeile beschreibt das aktive Dokument (D80). Geschrieben wird nur,
|
||||
wenn sich wirklich etwas ändert — sonst stünde in der Historie des Browsers
|
||||
bei jedem Rendern ein Eintrag mehr. */
|
||||
function syncDocUrl(){
|
||||
let u;
|
||||
try{ u = new URL(location.href); }catch(_){ return; }
|
||||
const neu = docSearch(u.search, activeId);
|
||||
if(neu === u.search) return;
|
||||
try{ history.replaceState(null, '', u.origin + u.pathname + neu + u.hash); }catch(_){}
|
||||
}
|
||||
|
||||
/* Die Live-Sitzung gehört dem sichtbaren Dokument (D80). Ohne das liefe der
|
||||
Feed eines Server-Dokuments weiter, während ein anderes vorn steht — und
|
||||
`setLiveText()` schriebe die fremde Änderung in **dessen** Text. */
|
||||
function followActiveDoc(){
|
||||
if(!bootDone) return;
|
||||
syncDocUrl();
|
||||
if(liveState && liveState.id !== activeId) stopLive();
|
||||
const d = activeDoc();
|
||||
if(!liveState && d && String(d.id).startsWith('live:')) startLive(d.id.slice(5));
|
||||
}
|
||||
|
||||
function switchDoc(id){
|
||||
if(id === activeId) return;
|
||||
/* Was noch im Debounce steckt, ist getippt und gemeint: erst loswerden,
|
||||
solange das Textfeld noch den Text dieses Dokuments zeigt (D80). */
|
||||
if(liveActive() && liveState.pushTimer){
|
||||
clearTimeout(liveState.pushTimer);
|
||||
liveState.pushTimer = null;
|
||||
pushLive(); /* liest src.value synchron; das Ergebnis braucht niemand mehr */
|
||||
}
|
||||
flushActive();
|
||||
activeId = id;
|
||||
foldOverrides.clear(); /* Falt-Eingriffe gelten je Dokument-Sitzung (D38) */
|
||||
@@ -4231,8 +4274,8 @@ function initDocs(){
|
||||
|
||||
`?etherpad=` gab es hier einmal daneben (D31) und ist ausgebaut (D78) — ein
|
||||
alter Link meldet sich, statt still nichts zu tun. */
|
||||
const SOURCE_PARAM = 'sourceUrl';
|
||||
const ETHERPAD_PARAM = 'etherpad';
|
||||
/* Die Parameternamen stehen in docurl.js — dort wird auch entschieden, welcher
|
||||
von ihnen zum aktiven Dokument gehört (D80). */
|
||||
function urlParam(name){
|
||||
try{ return new URLSearchParams(location.search).get(name); }catch(_){ return null; }
|
||||
}
|
||||
@@ -4315,7 +4358,6 @@ async function loadRemoteSource(){
|
||||
Was hier NICHT passiert: Zusammenführen. Überschneiden sich zwei Änderungen
|
||||
wirklich, entscheidet der Mensch (Konflikt-Band unten). Alles andere
|
||||
verschiebt der Server selbst. */
|
||||
const LIVE_PARAM = 'live';
|
||||
const LIVE_DEBOUNCE_MS = 600; /* Ruhe vor dem Senden; D76, D79 */
|
||||
const LIVE_WAIT_S = 25; /* Wartezeit des Feeds; der Server klemmt sie ohnehin */
|
||||
const LIVE_RETRY_MS = 5000; /* nach einem Netzfehler, bevor der Feed erneut fragt */
|
||||
@@ -4367,8 +4409,12 @@ function stopLive(){
|
||||
hideConflictBanner();
|
||||
}
|
||||
|
||||
async function loadLive(){
|
||||
const raw = urlParam(LIVE_PARAM);
|
||||
function loadLive(){ startLive(urlParam(LIVE_PARAM)); }
|
||||
|
||||
/* Eine Sitzung für dieses Server-Dokument aufnehmen: Stand holen, Feed öffnen.
|
||||
Zwei Wege hierher — der `?live=`-Parameter beim Laden und das Umschalten auf
|
||||
ein Server-Dokument im Wähler (D80). */
|
||||
async function startLive(raw){
|
||||
if(!raw) return;
|
||||
const urls = live.liveUrls(raw, location.href);
|
||||
if(!urls){
|
||||
@@ -4444,7 +4490,12 @@ async function pushLive(){
|
||||
const ops = live.computeOps(liveState.shadow, now);
|
||||
if(!ops.length) return;
|
||||
|
||||
liveState.busy = true;
|
||||
/* Die Sitzung selbst festhalten, nicht nur ihre Felder: Wer während des
|
||||
Sendens auf ein anderes Dokument umschaltet, beendet sie (D80) — die
|
||||
Fortsetzung unten dürfte danach weder schreiben noch in ein `null`
|
||||
greifen. Der PATCH ist dann trotzdem draußen, und das ist gewollt. */
|
||||
const sitzung = liveState;
|
||||
sitzung.busy = true;
|
||||
const seq = nextSeq();
|
||||
/* Die Basis, gegen die `ops` gerechnet sind — VOR dem Warten festgehalten.
|
||||
Sie hinterher aus `liveState` zu lesen hieße anzunehmen, dass sich
|
||||
@@ -4452,21 +4503,22 @@ async function pushLive(){
|
||||
Feed dazwischenfunkt (D76-Nachtrag 9). Der Feed lässt sich jetzt aus,
|
||||
solange wir senden — aber eine Rechnung, die nur wegen einer Sperre
|
||||
anderswo stimmt, schreibt man nicht auf. */
|
||||
const basis = liveState.shadow;
|
||||
const basis = sitzung.shadow;
|
||||
try{
|
||||
const body = {
|
||||
baseVersion: liveState.version,
|
||||
checksum: await live.checksum(live.text(liveState.shadow)),
|
||||
baseVersion: sitzung.version,
|
||||
checksum: await live.checksum(live.text(sitzung.shadow)),
|
||||
clientId: clientId(),
|
||||
displayName: displayName(),
|
||||
seq,
|
||||
ops,
|
||||
};
|
||||
const result = await fetchJson(liveState.urls.content, {
|
||||
const result = await fetchJson(sitzung.urls.content, {
|
||||
method: 'PATCH',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if(liveState !== sitzung) return; /* inzwischen umgeschaltet (D80) */
|
||||
/* Angenommen. Hat der Server verschoben, stehen die fremden Operationen in
|
||||
`opsSinceBase`: Die Schattenkopie zieht erst darüber nach, dann kommt
|
||||
unsere eigene Änderung darauf — verschoben um die fremde. Genau diese
|
||||
@@ -4475,14 +4527,14 @@ async function pushLive(){
|
||||
const foreign = (result.opsSinceBase || []);
|
||||
const meine = foreign.length ? live.rebaseOps(ops, foreign) : ops;
|
||||
if(meine == null){ await reloadLive(); return; } /* kann nicht sein - dann lieber neu */
|
||||
liveState.shadow = live.applyOps(
|
||||
sitzung.shadow = live.applyOps(
|
||||
foreign.length ? live.applyOps(basis, foreign) : basis, meine);
|
||||
liveState.version = result.version;
|
||||
if(foreign.length) applyForeign(basis, foreign, liveState.shadow, liveState.version);
|
||||
sitzung.version = result.version;
|
||||
if(foreign.length) applyForeign(basis, foreign, sitzung.shadow, sitzung.version);
|
||||
}catch(err){
|
||||
handlePushError(err);
|
||||
if(liveState === sitzung) handlePushError(err);
|
||||
}finally{
|
||||
liveState.busy = false;
|
||||
sitzung.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4736,10 +4788,10 @@ async function putOnServer(){
|
||||
runFeed();
|
||||
|
||||
/* Die Adresszeile IST der Link — dort sucht man ihn, und ein Neuladen
|
||||
führt zurück ins selbe Dokument. Zusätzlich in die Zwischenablage,
|
||||
weil Weitergeben der eigentliche Zweck ist. */
|
||||
const teilen = location.origin + location.pathname + '?live=' + urls.doc;
|
||||
try{ history.replaceState(null, '', teilen); }catch(_){}
|
||||
führt zurück ins selbe Dokument. Gesetzt hat sie `adoptLive()` schon
|
||||
(D80); in die Zwischenablage geht der Link ohne fremde Parameter, denn
|
||||
ein `?server=` geht den Empfänger nichts an. */
|
||||
const teilen = location.origin + location.pathname + '?' + LIVE_PARAM + '=' + urls.doc;
|
||||
try{ await navigator.clipboard.writeText(teilen); }catch(_){}
|
||||
flashBtn(document.getElementById('docTrigger'));
|
||||
sourceWarning = null;
|
||||
@@ -4979,6 +5031,9 @@ if('launchQueue' in window){
|
||||
applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */
|
||||
loadRemoteSource(); /* ?sourceUrl= / ?etherpad= nachladen (asynchron, D23/D31) */
|
||||
loadLive(); /* ?live= — Server-Dokument samt Feed (asynchron, D76) */
|
||||
/* Beide haben ihren Parameter jetzt gelesen (synchron, vor dem ersten
|
||||
`await`). Ab hier folgt die Adresszeile dem aktiven Dokument (D80). */
|
||||
bootDone = true;
|
||||
|
||||
/* ---------- PWA: Service Worker (D73) ----------
|
||||
Ein reiner Offline-Mantel (public/sw.js): Navigationen network-first, der
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* ---------- Welche Adresse beschreibt das aktive Dokument? (D80) ----------
|
||||
|
||||
Die Adresszeile ist der Link, den man weitergibt, und der Stand, den ein
|
||||
Neuladen wiederherstellt. Sie muss deshalb sagen, welches Dokument **gerade
|
||||
vorn** ist — nicht, mit welchem die Seite einmal aufgerufen wurde. Bleibt
|
||||
ein `?live=` stehen, nachdem man auf ein anderes Dokument umgeschaltet hat,
|
||||
zeigt die Adresse auf etwas anderes als der Bildschirm, und ein Neuladen
|
||||
holt das falsche Dokument zurück.
|
||||
|
||||
Die beiden Eingänge, die ein Dokument adressieren, sind zugleich seine
|
||||
Identität: `live:<url>` (D76) und `url:<href>` (D23). Aus der id lässt sich
|
||||
der Parameter also zurückrechnen — hier steht diese eine Regel. Eigene
|
||||
Dokumente, Dateien und die mitgelieferten haben keine Adresse: Dort fällt
|
||||
der Parameter weg. `?etherpad=` ist ausgebaut (D78) und wird nur noch
|
||||
weggeräumt. */
|
||||
|
||||
export const LIVE_PARAM = 'live';
|
||||
export const SOURCE_PARAM = 'sourceUrl';
|
||||
export const ETHERPAD_PARAM = 'etherpad';
|
||||
|
||||
/* Die Parameter, über die diese Regel bestimmt. Alles andere ist fremd und
|
||||
bleibt unangetastet. */
|
||||
const OWNED = [LIVE_PARAM, SOURCE_PARAM, ETHERPAD_PARAM];
|
||||
|
||||
/* Der Parameter, der dieses Dokument wieder öffnet — oder null. */
|
||||
export function docParam(id){
|
||||
if(typeof id !== 'string') return null;
|
||||
if(id.startsWith('live:')) return {name: LIVE_PARAM, value: id.slice(5)};
|
||||
if(id.startsWith('url:')) return {name: SOURCE_PARAM, value: id.slice(4)};
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Der neue Query-String zu `search` (mit oder ohne führendes `?`) für das
|
||||
Dokument `id`.
|
||||
|
||||
Fremde Parameter (etwa `?server=`, D76-Nachtrag 8) bleiben **wörtlich**
|
||||
stehen, Schreibweise eingeschlossen: Der Umweg über `URLSearchParams`
|
||||
schriebe jedes `:` und `/` als `%3A`/`%2F` und machte damit gerade die URL
|
||||
unleserlich, um die es hier geht. Aus demselben Grund wird der eigene Wert
|
||||
nur dort maskiert, wo er den Query-String sonst zerrisse (`&`, `#`). */
|
||||
export function docSearch(search, id){
|
||||
const roh = String(search == null ? '' : search).replace(/^\?/, '');
|
||||
const rest = roh ? roh.split('&').filter(p => p && OWNED.indexOf(p.split('=')[0]) < 0) : [];
|
||||
const mein = docParam(id);
|
||||
if(mein){
|
||||
const wert = String(mein.value).replace(/&/g, '%26').replace(/#/g, '%23');
|
||||
rest.push(mein.name + '=' + wert);
|
||||
}
|
||||
return rest.length ? '?' + rest.join('&') : '';
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { docParam, docSearch, LIVE_PARAM, SOURCE_PARAM } from '../src/docurl.js';
|
||||
|
||||
const LIVE = 'live:https://werkbaum.example/api/v1/documents/44753df1';
|
||||
const URLDOC = 'url:https://example.org/plan.werkbaum';
|
||||
|
||||
describe('docParam — welcher Parameter öffnet dieses Dokument wieder?', () => {
|
||||
it('ein Server-Dokument wird über ?live= adressiert', () => {
|
||||
expect(docParam(LIVE)).toEqual(
|
||||
{name: LIVE_PARAM, value: 'https://werkbaum.example/api/v1/documents/44753df1'});
|
||||
});
|
||||
|
||||
it('ein geholtes Dokument über ?sourceUrl=', () => {
|
||||
expect(docParam(URLDOC)).toEqual(
|
||||
{name: SOURCE_PARAM, value: 'https://example.org/plan.werkbaum'});
|
||||
});
|
||||
|
||||
it('eigene, mitgelieferte und aus Dateien geöffnete Dokumente haben keine Adresse', () => {
|
||||
expect(docParam('example')).toBe(null);
|
||||
expect(docParam('werkbaum')).toBe(null);
|
||||
expect(docParam('k3f9x1')).toBe(null);
|
||||
expect(docParam(null)).toBe(null);
|
||||
expect(docParam(undefined)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('docSearch — die Adresszeile folgt dem aktiven Dokument', () => {
|
||||
it('setzt den Parameter des Server-Dokuments', () => {
|
||||
expect(docSearch('', LIVE))
|
||||
.toBe('?live=https://werkbaum.example/api/v1/documents/44753df1');
|
||||
});
|
||||
|
||||
it('räumt ihn weg, sobald ein lokales Dokument vorn ist', () => {
|
||||
expect(docSearch('?live=https://werkbaum.example/api/v1/documents/44753df1', 'example'))
|
||||
.toBe('');
|
||||
});
|
||||
|
||||
it('tauscht ihn beim Wechsel auf ein anderes Server-Dokument', () => {
|
||||
expect(docSearch('?live=https://werkbaum.example/api/v1/documents/aaa',
|
||||
'live:https://werkbaum.example/api/v1/documents/bbb'))
|
||||
.toBe('?live=https://werkbaum.example/api/v1/documents/bbb');
|
||||
});
|
||||
|
||||
it('tauscht auch zwischen den beiden Eingängen', () => {
|
||||
expect(docSearch('?sourceUrl=https://example.org/plan.werkbaum', LIVE))
|
||||
.toBe('?live=https://werkbaum.example/api/v1/documents/44753df1');
|
||||
expect(docSearch('?live=https://werkbaum.example/api/v1/documents/44753df1', URLDOC))
|
||||
.toBe('?sourceUrl=https://example.org/plan.werkbaum');
|
||||
});
|
||||
|
||||
it('räumt den ausgebauten ?etherpad= mit weg (D78)', () => {
|
||||
expect(docSearch('?etherpad=https://pad.example/p/plan', 'example')).toBe('');
|
||||
});
|
||||
|
||||
it('lässt fremde Parameter wörtlich stehen — auch ihre Schreibweise', () => {
|
||||
expect(docSearch('?server=http://localhost:8080&live=https://a/x', 'example'))
|
||||
.toBe('?server=http://localhost:8080');
|
||||
expect(docSearch('?server=http://localhost:8080', LIVE))
|
||||
.toBe('?server=http://localhost:8080&live=https://werkbaum.example/api/v1/documents/44753df1');
|
||||
});
|
||||
|
||||
it('schreibt die URL unmaskiert — lesbar ist der Zweck', () => {
|
||||
expect(docSearch('', LIVE)).toContain('https://werkbaum.example/api/v1/documents/44753df1');
|
||||
});
|
||||
|
||||
it('maskiert nur, was den Query-String zerrisse', () => {
|
||||
expect(docSearch('', 'url:https://example.org/p?a=1&b=2#tail'))
|
||||
.toBe('?sourceUrl=https://example.org/p?a=1%26b=2%23tail');
|
||||
});
|
||||
|
||||
it('nimmt den Query-String mit und ohne führendes Fragezeichen', () => {
|
||||
expect(docSearch('server=x', 'example')).toBe('?server=x');
|
||||
expect(docSearch('?server=x', 'example')).toBe('?server=x');
|
||||
expect(docSearch(undefined, 'example')).toBe('');
|
||||
});
|
||||
|
||||
it('ändert nichts, wenn schon das Richtige dasteht', () => {
|
||||
const s = '?live=https://werkbaum.example/api/v1/documents/44753df1';
|
||||
expect(docSearch(s, LIVE)).toBe(s);
|
||||
expect(docSearch('', 'example')).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user