frontend: Pad einbetten — drei Ansichten mit Splitter, Spiegel bleibt

Das Einbetten ist möglich, ohne D25 zu opfern. Nachgemessen zuerst, ob der
Server es überhaupt zulässt: pad.hostsharing.net sendet kein
`X-Frame-Options` und keine CSP mit `frame-ancestors`, und im Versuch baute das
Pad in einem fremdstämmigen Rahmen seine inneren Editorframes auf
(`contentWindow.length === 2` = ace_outer/ace_inner), samt Werkzeugleiste,
heiler Einrückung und lebender Socket-Verbindung.

Einbetten heißt aber nicht Ersetzen: Ein cross-origin-iframe gibt keinen
DOM-Zugriff, ein ersetztes Textfeld kostete also beide Richtungen von D25
(Alt+Klick -> Zeile, Cursor-Zeile -> Knoten). Deshalb drei Ansichten, reihum
über einen Wähler in der Editor-Titelzeile:

- Pad und Text, geteilt durch einen eigenen Splitter (Mechanik und Idiom vom
  Legenden-Splitter aus D26: von der Spiegelseite gezogen, Doppelklick setzt
  zurück, `--pcol`/`--prow` getrennt gehalten und persistiert). Der Spiegel darf
  schmal werden und trägt weiter die Sprünge — die arbeiten auf unserem
  eigenen `<textarea>`.
- nur Pad — Spiegel ausgeblendet; `revealEditor()` schaltet bei einem Sprung
  auf „beide", sonst zeigte der Sprung ins Nichts.
- nur Text — wie bisher, kein Rahmen.

Ein Wähler statt dreier Knöpfe, weil die Titelzeile schon Dokument, Pad, Neu
laden, Kopieren, Legende und Fensterknöpfe trägt (mobil dreifach eng, D17) —
derselbe Reihum-Griff wie beim Modus-Wähler dort. Der Zustand steckt im Symbol
(geteilt / links gefüllt / rechts gefüllt), der Tooltip nennt ihn im Klartext.

Der Rahmen bekommt nur sichtbar ein `src`, sonst `about:blank`. Das ist kein
Geiz um Bytes: Ein geladenes Pad verbindet sich per Socket und macht dich in
dessen Anwesenden-Liste sichtbar. „Nur Text" ist damit die Ansicht, die nichts
von dir verrät.

Zwei Dinge, die beim Prüfen auffielen und behoben sind:
- Eine überzählige `}` ließ das Modul gar nicht laden. Die Messung sah trotzdem
  plausibel aus, weil die abgefragten Werte die statischen HTML-Vorgaben waren —
  seither prüft jede Messung zuerst, ob überhaupt Knoten gerendert sind, und
  `node --check` läuft vor dem Blick in den Browser.
- In „nur Pad" war der Spiegel weiterhin sichtbar (sogar breiter als das Pad):
  die Regel `.src-area.pv-pad>textarea{display:none}` fehlte.

Bleibender Preis: `Set-Cookie: token=…; SameSite=Lax`. Das Cookie ist Etherpads
Autoren-Identität und wird im fremdstämmigen Rahmen nicht mitgesendet — man ist
bei jedem Laden ein neuer Autor. Nur serverseitig zu beheben
(`cookie.sameSite: "None"`), nicht in Werkbaum. Steht in D31.

Auf kleinem Bildschirm nachgemessen statt geschätzt: bei 375x812 bleiben Pad
317 px und Spiegel 180 px, per Splitter verschiebbar — brauchbar, also keine
Sonderregel für Mobil.

SPEC §9 und D31 nachgezogen; 5 neue i18n-Schlüssel in allen 9 Sprachen.
This commit is contained in:
mhoennig
2026-07-30 13:14:40 +02:00
parent 793561c873
commit 0a08706877
7 changed files with 286 additions and 28 deletions
+49 -26
View File
@@ -1105,32 +1105,7 @@ Link baut — Werkbaum legt von sich aus kein Pad an.
**Verworfene und aufgeschobene Alternativen:**
- **`?sourceUrl=` um wiederholtes Abrufen erweitern** — hätte bestehenden Links
ungefragt Requests verpasst und D23 seine klare Semantik gekostet.
- **Das Pad als `<iframe>` einbetten** (aufgeschoben, nicht verworfen). Ob der
Server das überhaupt zulässt, ist **nachgemessen**: `pad.hostsharing.net`
sendet **kein** `X-Frame-Options` und keine CSP mit `frame-ancestors`, und im
Versuch hat das Pad in einem fremdstämmigen Rahmen seine inneren Editorframes
aufgebaut (`contentWindow.length === 2`, das sind `ace_outer`/`ace_inner`),
samt Werkzeugleiste, heiler Einrückung und lebender Socket-Verbindung
(Anwesenden-Zähler). Technisch geht es also.
Zwei Preise. Erstens **`Set-Cookie: token=…; SameSite=Lax`**: Dieses Cookie ist
Etherpads Autoren-Identität und wird in einem fremdstämmigen Rahmen **nicht
mitgesendet** (Safari blockt Dritt-Cookies grundsätzlich, Chrome je nach
Einstellung). Bearbeiten geht, aber man ist bei jedem Laden ein neuer Autor —
Name und Farbe halten nicht. Reparieren lässt sich das nur serverseitig
(`cookie.sameSite: "None"` in Etherpads `settings.json`), nicht in Werkbaum.
Zweitens: Einbetten heißt das **Textpanel ersetzen**, und damit fallen **beide**
Richtungen von D25 weg — kein Alt+Klick → Zeile, keine Cursor-Zeile → Knoten,
weil ein cross-origin-iframe keinen DOM-Zugriff erlaubt. Bei 75 Knoten ist das
die Orientierung.
Der Ausweg, der sich beim Messen zeigte: das schreibgeschützte Textfeld
**behalten** und das Pad daneben stellen. Dann bleibt der Spiegel die Fläche
zum Hinschauen (Alt+Klick und Cursor-Synchronisierung arbeiten auf unserem
eigenen `<textarea>`), das Pad ist die Fläche zum Schreiben. Preis ist
Bildschirmfläche; auf kleinem Bildschirm (D17) sind es dann drei Bereiche.
Der Knopf „im Pad bearbeiten" ist bis dahin die kleine Lösung desselben
Bedürfnisses; das Einbetten bleibt möglich, weil die Pad-URL vorliegt.
- **Das Pad einbetten** — nicht verworfen, sondern gebaut; siehe Nachtrag unten.
- **Echter Etherpad-Client** (socket.io + Easysync-Changesets): Rettet D25 und
erlaubt Schreiben aus Werkbaum heraus, kostet aber zwei **Laufzeit**-
Abhängigkeiten und damit „eine self-contained Datei ohne
@@ -1141,3 +1116,51 @@ Link baut — Werkbaum legt von sich aus kein Pad an.
- **Etherpads HTTP-API** (`/api/1/getText?apikey=…`) — der API-Schlüssel ist ein
Administrationsschlüssel für **alle** Pads der Instanz und hat in einer
Client-Anwendung nichts zu suchen. Der Export-Endpunkt braucht ihn nicht.
**Nachtrag — das Pad wird eingebettet, der Textspiegel bleibt daneben.**
Ob der Server das Einbetten zulässt, war die erste Frage. Nachgemessen:
`pad.hostsharing.net` sendet **kein** `X-Frame-Options` und keine CSP mit
`frame-ancestors`, und im Versuch hat das Pad in einem fremdstämmigen Rahmen
seine inneren Editorframes aufgebaut (`contentWindow.length === 2`, das sind
`ace_outer`/`ace_inner`) — samt Werkzeugleiste, heiler Einrückung und lebender
Socket-Verbindung (der Anwesenden-Zähler sprang auf 2). Technisch geht es also.
**Aber Einbetten heißt nicht Ersetzen.** Der naheliegende Schritt wäre, das
Textfeld durch den Rahmen zu ersetzen. Das kostete **beide** Richtungen von D25
— kein Alt+Klick → Zeile, keine Cursor-Zeile → Knoten —, weil in einen
cross-origin-iframe kein DOM-Zugriff führt. Bei 75 Knoten ist das die
Orientierung. Deshalb **drei Ansichten** statt einer Entscheidung, reihum über
einen Wähler in der Editor-Titelzeile:
- **Pad und Text**, geteilt durch einen eigenen Splitter (Idiom und Mechanik wie
beim Legenden-Splitter aus D26: von der Spiegelseite gezogen, Doppelklick
setzt zurück, `--pcol`/`--prow` getrennt gehalten und persistiert). Der Spiegel
darf schmal werden — er trägt weiter die Sprünge, denn die arbeiten auf
**unserem** `<textarea>`.
- **nur Pad** — der Spiegel ist ausgeblendet. Ein Sprung aus dem Diagramm holt
ihn selbst zurück, so wie `revealEditor()` ein zugeklapptes Panel aufklappt
(D25). Ohne das zeigte der Sprung ins Nichts.
- **nur Text** — wie bisher, kein Rahmen.
**Ein Wähler statt dreier Knöpfe:** Die Editor-Titelzeile trägt schon Dokument,
Pad, Neu laden, Kopieren, Legende und die Fensterknöpfe; auf kleinem Bildschirm
(D17) ist sie dreifach eng. Derselbe Reihum-Griff wie beim Modus-Wähler dort. Der
Zustand steckt im Symbol (geteilter Rahmen / links gefüllt / rechts gefüllt), der
Tooltip nennt ihn im Klartext.
**Der Rahmen wird nur geladen, wenn er sichtbar ist** (`about:blank` sonst). Das
ist kein Geiz um Bytes: Ein geladenes Pad verbindet sich per Socket und macht
dich in dessen Anwesenden-Liste sichtbar. „Nur Text" ist damit die Ansicht, die
nichts von dir verrät — und die Voreinstellung „beide" ist eine bewusste
Entscheidung, weil wer `?etherpad=` aufruft, das Pad auch will.
**Der Preis, der bleibt: `Set-Cookie: token=…; SameSite=Lax`.** Dieses Cookie ist
Etherpads Autoren-Identität und wird in einem fremdstämmigen Rahmen **nicht
mitgesendet** (Safari blockt Dritt-Cookies grundsätzlich, Chrome je nach
Einstellung). Bearbeiten geht, aber man ist bei jedem Laden ein neuer Autor —
Name und Farbe halten nicht. Reparieren lässt sich das nur **serverseitig**
(`cookie.sameSite: "None"` in Etherpads `settings.json`), nicht in Werkbaum.
Auf kleinem Bildschirm wurde „beide" nachgemessen statt geschätzt: bei 375 × 812
bleiben Pad 317 px und Spiegel 180 px, per Splitter verschiebbar — knapp, aber
brauchbar. Eine Sonderregel für Mobil braucht es deshalb nicht.
+12
View File
@@ -290,6 +290,18 @@ Export- oder `/timeslider`-Pfad wird abgeschnitten.
- 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
+3 -1
View File
@@ -65,7 +65,9 @@
- [x] Watch a shared Etherpad — it merges, we render (S)
- [x] Load the pad's plain-text export (XS)
- [x] Reload on demand (XS) %% a timer only trips Etherpad's rate limit
+ [?] Embed the pad beside the diagram (M) %% costs the jump to the line
- [x] Embed the pad, keep the text mirror beside it (M)
- [x] Three views: pad and text, pad only, text only (S)
- [x] Splitter between pad and mirror (XS)
- [ ] Git as the shared store (L)
| [ ] A file in a repository, changed by pull request (S) %% works today, no code
| [?] The backend commits every change (L)
+12
View File
@@ -158,6 +158,18 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
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.
- Sprung Diagramm ↔ Text (D25): `render.js` schreibt die Parser-Zeilennummer als
`data-line` an jeden Knoten (Geister-Knoten bekommen keine). `jumpToLine()` in
`app.js` klappt bei Bedarf das Editor-Panel auf (`revealEditor()`), markiert die
+10
View File
@@ -58,6 +58,9 @@
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
</div>
</div>
<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>
@@ -78,7 +81,14 @@
</div>
</div>
<div class="editor-body">
<!-- Pad-Rahmen + interner Spiegel teilen sich einen eigenen Bereich, damit die
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>
<textarea id="src" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea>
</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">
<button type="button" class="agenda-summary" id="agendaToggle" aria-expanded="false" aria-controls="hint" data-i18n="agenda">Agenda</button>
+160
View File
@@ -39,6 +39,11 @@ const warnBox = document.getElementById('warn');
Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt
nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */
let cheapPathOn = true;
/* 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';
/* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */
let sourceWarning = null;
/* „Was ist neu?" (D28): Knoten, die gegenüber der zuletzt GESEHENEN Fassung neu
@@ -499,6 +504,14 @@ function scrollEditorToOffset(offset){
/* Ist das Editor-Panel zugeklappt, muss der Sprung es erst öffnen. */
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()){
const raw = app.style.getPropertyValue('--drow');
const collapsed = raw ? parseFloat(raw) > mobileMaxDrow() - 40 : splitState === 'b';
@@ -897,6 +910,11 @@ const I18N = {
padRefresh:"Vom Pad neu laden",
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.",
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
cheapTooltip:"Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück",
@@ -955,6 +973,11 @@ const I18N = {
padRefresh:"Reload from the pad",
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.",
discardedTooltip:"Show/hide discarded nodes and their subtree",
cheapTooltip:"Highlight the cheapest path unneeded alternatives recede",
@@ -1013,6 +1036,11 @@ const I18N = {
padRefresh:"Recargar desde el pad",
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.",
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",
@@ -1071,6 +1099,11 @@ const I18N = {
padRefresh:"Recharger depuis le pad",
padWait:"Encore {seconds} s — Etherpad limite la fréquence",
padRateLimitWarn:"Pas encore rechargé : Etherpad nautorise que quelques récupérations par fenêtre de temps (10 par 90 s par défaut), une synchronisation aussi fréquente nest 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.",
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",
@@ -1129,6 +1162,11 @@ const I18N = {
padRefresh:"Wczytaj ponownie z padu",
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.",
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
cheapTooltip:"Wyróżnij najtańszą ścieżkę niepotrzebne alternatywy są przygaszone",
@@ -1187,6 +1225,11 @@ const I18N = {
padRefresh:"Обновить из пада",
padWait:"Ещё {seconds} с — Etherpad ограничивает частоту",
padRateLimitWarn:"Пока не обновлено: Etherpad разрешает лишь несколько загрузок за окно времени (по умолчанию 10 за 90 с), поэтому столь частая синхронизация невозможна. Повторите через {seconds} с.",
padViewTooltip:"Вид: {state} — нажмите для переключения",
padView_both:"пад и текст",
padView_pad:"только пад",
padView_text:"только текст",
padGutterAria:"Изменить размер пада и текстового редактора",
riskTooltip:"Высокий риск – оценка ещё не ясна.",
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
@@ -1245,6 +1288,11 @@ const I18N = {
padRefresh:"पैड से फिर लोड करें",
padWait:"{seconds} स॰ बाकी — Etherpad बार-बार लेने की सीमा रखता है",
padRateLimitWarn:"अभी दोबारा लोड नहीं किया: Etherpad प्रति समय-खिड़की केवल कुछ ही बार लेने देता है (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार), इसलिए इतनी बार सिंक करना संभव नहीं है। {seconds} स॰ में फिर कोशिश करें।",
padViewTooltip:"दृश्य: {state} — बदलने के लिए क्लिक करें",
padView_both:"पैड और टेक्स्ट",
padView_pad:"केवल पैड",
padView_text:"केवल टेक्स्ट",
padGutterAria:"पैड और टेक्स्ट संपादक का आकार बदलें",
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
@@ -1308,6 +1356,11 @@ const I18N = {
padRefresh:"从 Pad 重新加载",
padWait:"还需 {seconds} 秒 — Etherpad 限制获取频率",
padRateLimitWarn:"尚未重新加载:Etherpad 每个时间窗口只允许少量获取(默认每 90 秒 10 次),因此无法如此频繁地同步。请在 {seconds} 秒后再试。",
padViewTooltip:"视图:{state} — 点击切换",
padView_both:"Pad 和文本",
padView_pad:"仅 Pad",
padView_text:"仅文本",
padGutterAria:"调整 Pad 与文本编辑器大小",
riskTooltip:"高风险 – 工作量尚不明确。",
editorTitle:"结构(文本)", diagramTitle:"图表",
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
@@ -1366,6 +1419,11 @@ const I18N = {
padRefresh:"パッドから再読み込み",
padWait:"あと {seconds} 秒 — Etherpad は取得頻度を制限します",
padRateLimitWarn:"まだ再読み込みしていません: Etherpad は一定時間内の取得回数を制限します(既定で 90 秒あたり 10 回)。これほど頻繁な同期はできません。{seconds} 秒後にもう一度お試しください。",
padViewTooltip:"表示: {state} — クリックで切り替え",
padView_both:"パッドとテキスト",
padView_pad:"パッドのみ",
padView_text:"テキストのみ",
padGutterAria:"パッドとテキストエディターのサイズを変更",
riskTooltip:"高リスク – 規模はまだ不明。",
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
@@ -1635,6 +1693,11 @@ function saveUI(){
agenda: !!document.querySelector('.agenda.open'),
hcol: app.style.getPropertyValue('--hcol') || 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,
zoom: zoom,
fullscreen: document.body.classList.contains('fullscreen')
}));
@@ -1672,6 +1735,9 @@ function restoreState(){
/* Legende (D26): Aufteilung gilt unabhängig vom Preset des großen Splitters. */
if(ui && ui.hcol) app.style.setProperty('--hcol', ui.hcol);
if(ui && ui.hrow) app.style.setProperty('--hrow', ui.hrow);
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));
applyZoom();
restoring = false;
@@ -2048,6 +2114,58 @@ async function refreshPad(){
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. */
@@ -2075,6 +2193,7 @@ function updatePadLink(){
padRefreshBtn.hidden = !(isPad || pending);
updatePadRefreshLabel(); /* trägt auch den laufenden Sperr-Zähler */
}
applyPadView();
}
if(padRefreshBtn) padRefreshBtn.addEventListener('click', refreshPad);
docTrigger.addEventListener('click', e => {
@@ -2185,6 +2304,47 @@ hintGutter.addEventListener('dblclick', () => {
app.style.removeProperty('--hrow');
saveUI();
});
/* Splitter Pad|Spiegel (D31) — dieselbe Mechanik wie beim Legenden-Splitter
(D26): gezogen wird von der Spiegelseite aus, Doppelklick setzt zurück,
Ausrichtung folgt der Panel-Anordnung. Beide Werte werden getrennt gehalten,
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,
Tippen schaltet zum nächsten Modus (reihum). Auf normaler Größe bleibt es
der Dreier-Umschalter — dort kehrt der Handler sofort zurück. */
+40 -1
View File
@@ -208,6 +208,25 @@
Horizontal nebeneinander, vertikal (.side) untereinander. */
.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}
/* Pad-Rahmen | interner Spiegel (D31). Eigener Bereich, damit die
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}
.src-area.pv-both>textarea{
flex:0 0 var(--pcol,300px);max-width:85%;
border-left:1px dashed rgba(36,52,71,.18);
}
.src-area.pv-pad>textarea{display:none}
.side .src-area,body.mobile .src-area{flex-direction:column}
.side .src-area.pv-both>textarea,body.mobile .src-area.pv-both>textarea{
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
`::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
@@ -259,6 +278,17 @@
.side .agenda.open{width:auto;max-width:none;height:var(--hrow,220px);max-height:85%}
.side .hint-gutter{cursor:row-resize}
.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{
background:var(--card);
@@ -332,7 +362,16 @@
/* 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]{display: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}
/* Läuft ein Abruf, dreht das Symbol: bei gedrosselter Gegenseite (D31) können
das die vollen 20 s sein, und ein Knopf ohne Rückmeldung wirkt kaputt. */
/* Sperre nach einem Abruf (D31): sichtbar gedämpft, aber **klickbar** der