feat: Frühere Stände eines Dokuments, alle zehn Minuten (D54)
Alle 10 Minuten wird der Text des aktiven Dokuments weggelegt — aber nur, wenn er sich seit dem letzten Stand geändert hat. Aufgehoben werden die letzten 20 je Dokument (Nutzer-Entscheidung; rund 3,5 Stunden, max. ~800 kB beim größten Dokument). Angeboten werden sie über einen Knopf rechts neben dem Dokumenten-Wähler, neueste zuoberst, je mit Uhrzeit und Zeilenzahl. „Nur bei Änderung" heißt: gegen den letzten Stand, und solange es keinen gibt, gegen den Text beim Aktivieren des Dokuments. Ohne das legte der erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. Die Dokumente sind wichtiger als ihre Stände: Beide teilen sich den localStorage, also wirft persistSnaps() bei Überlauf den jeweils ältesten Stand weg, bis es passt, notfalls alle — statt das Speichern der Dokumente zu gefährden. Ein gelöschtes Dokument nimmt seine Stände mit. Pad-Dokumente (D31) sammeln keine: Ihr Feld ist schreibgeschützt, ein alter Stand ließe sich dort nicht einsetzen. Zurückgeholt wird undo-fähig (replaceTextUndoable, D53) — ein Griff daneben kostet ein Strg+Z, keine Rückfrage. Vorher wird der aktuelle Stand weggelegt, sonst wäre gerade er das Einzige, was das Zurückholen verlöre. Beim Bauen korrigiert: Das Menü war an der Gruppe ausgerichtet (right:0) und begann 190 px weiter links, also außerhalb des Panels (gemessen: linke Kante bei -65 px). Bezug ist jetzt der Knopf. Nachgemessen (Takt für die Prüfung auf 2 s verkürzt, danach zurückgestellt): ohne Eingabe kein Eintrag, nach einer Eingabe genau einer, ohne weitere keiner dazu; nach 29 Änderungen 20 (gedeckelt, Zeitstempel aufsteigend). Klick auf einen älteren Eintrag lädt ihn (36 -> 32 Zeilen), Diagramm zeichnet neu, undo holt den vorherigen Stand zurück. Plan: 155 Knoten, 0 Warnungen. 240 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6935b4882d
commit
8fd595abf9
@@ -3522,3 +3522,63 @@ Undo deshalb über `execCommand('undo')`; das steht jetzt auch in
|
|||||||
`frontend/CLAUDE.md`. Dieselbe Lehre wie D25 (synthetische `TouchEvent`s) und
|
`frontend/CLAUDE.md`. Dieselbe Lehre wie D25 (synthetische `TouchEvent`s) und
|
||||||
D17-Nachtrag 4 (Bildschirmtastatur): Was die Umgebung stellt, stellt der
|
D17-Nachtrag 4 (Bildschirmtastatur): Was die Umgebung stellt, stellt der
|
||||||
Emulator nicht.
|
Emulator nicht.
|
||||||
|
|
||||||
|
## D54 — Frühere Stände: alle zehn Minuten, nur bei Änderung, die letzten zwanzig
|
||||||
|
Ein Sicherheitsnetz gegen Versehen — und der Anlass war ein echtes: Der
|
||||||
|
Tab-Fehler aus D53 konnte eine ganze Auswahl löschen und nahm dabei auch noch
|
||||||
|
das Rückgängig mit. Ohne Netz war der Text dann weg.
|
||||||
|
|
||||||
|
**Aufbewahrt werden die letzten 20 je Dokument** (Nutzer-Entscheidung), also
|
||||||
|
rund 3½ Stunden bei gleichmäßigen Abständen und höchstens ~800 kB beim größten
|
||||||
|
Dokument. Erwogen und verworfen: **ausgedünnt** (alle der letzten Stunde,
|
||||||
|
stündlich für einen Tag, täglich für eine Woche — reicht weiter zurück, kostet
|
||||||
|
aber eine Ausdünn-Regel, die man beim Lesen erst verstehen muss) und
|
||||||
|
**lückenlos 24 Stunden** (bis zu 144 Stände, beim großen Plan ~5,8 MB und damit
|
||||||
|
über dem localStorage-Limit — es bräuchte doch wieder eine Notbremse). Der
|
||||||
|
Anspruch ist bewusst klein: Wer weiter zurück will, hat Git.
|
||||||
|
|
||||||
|
**„Nur bei Änderung" heißt: gegen den letzten Stand, nicht gegen das Laden.**
|
||||||
|
Gibt es noch keinen Stand, wird gegen den Text beim Aktivieren des Dokuments
|
||||||
|
verglichen (`snapBase`). Ohne das legte der erste Takt nach dem Öffnen auch ein
|
||||||
|
**unverändertes** Dokument weg — beim Herumklicken durch mehrere Dokumente
|
||||||
|
sammelte sich so Ballast, den niemand erzeugt hat. Nachgemessen: Ohne Eingabe
|
||||||
|
entsteht der Schlüssel im localStorage gar nicht erst.
|
||||||
|
|
||||||
|
**Die Dokumente sind wichtiger als ihre Stände.** Beide teilen sich den
|
||||||
|
localStorage. Läuft er über, wirft `persistSnaps()` deshalb so lange den
|
||||||
|
jeweils ältesten Stand weg, bis es passt, notfalls alle — statt eine Ausnahme
|
||||||
|
hochzureichen und damit womöglich das Speichern der **Dokumente** zu
|
||||||
|
gefährden. Ein gelöschtes Dokument nimmt seine Stände mit.
|
||||||
|
|
||||||
|
**Zurückgeholt wird undo-fähig** (`replaceTextUndoable`, D53): ein Griff
|
||||||
|
daneben kostet ein Strg+Z, keine Rückfrage — dieselbe Haltung wie beim Falten
|
||||||
|
(D38-Nachtrag 2). Vorher wird der **aktuelle** Stand weggelegt, falls er noch
|
||||||
|
nicht drin ist; sonst wäre ausgerechnet er das Einzige, was das Zurückholen
|
||||||
|
verlöre.
|
||||||
|
|
||||||
|
**Pad-Dokumente (D31) bleiben außen vor.** Ihr Textfeld ist schreibgeschützt —
|
||||||
|
ein alter Stand ließe sich dort gar nicht einsetzen, und Stände zu sammeln,
|
||||||
|
die niemand laden kann, wäre nur Ballast. Der Knopf ist dort verborgen
|
||||||
|
(`src.readOnly` bewacht Sammeln, Laden und Sichtbarkeit).
|
||||||
|
|
||||||
|
**Platzierung: rechts neben dem Dokumenten-Wähler** (Nutzer-Vorgabe), beide in
|
||||||
|
einer Gruppe, die das `margin-right:auto` trägt. Der Positionsbezug des Menüs
|
||||||
|
ist der **Knopf**, nicht die Gruppe: An der Gruppe ausgerichtet (`right:0`)
|
||||||
|
begann es 190 px weiter links und lief aus dem Panel heraus — in der ersten
|
||||||
|
Fassung gebaut, im Bild sofort zu sehen und nachgemessen (linke Kante bei
|
||||||
|
−65 px). Jetzt `position:relative` an einer Hülle um den Knopf und `left:0` am
|
||||||
|
Menü: linke Kante bei 99 px, gleichauf mit dem Knopf, ganz im Panel.
|
||||||
|
|
||||||
|
Jeder Eintrag nennt **Uhrzeit und Zeilenzahl** — die Zeilenzahl sagt auf einen
|
||||||
|
Blick, welchen der ähnlich benannten Stände man greift. Neueste zuoberst,
|
||||||
|
danach sucht man zuerst.
|
||||||
|
|
||||||
|
**Nachgemessen** (Takt für die Prüfung auf 2 s verkürzt, danach zurückgestellt
|
||||||
|
und geprüft): ohne Eingabe kein Eintrag; nach einer Eingabe genau einer; ohne
|
||||||
|
weitere Eingabe kommt keiner dazu; nach 29 Änderungen sind es **20** (gedeckelt,
|
||||||
|
Zeitstempel aufsteigend, der älteste ist der kleinste Text). Menü: 20 Einträge,
|
||||||
|
neueste zuoberst. Klick auf einen älteren Eintrag lädt ihn (36 → 32 Zeilen),
|
||||||
|
schließt das Menü, das Diagramm zeichnet neu (30 Knoten) — und ein `undo` holt
|
||||||
|
den vorherigen Stand zeichengenau zurück. Der **Pad-Fall** ist durch die
|
||||||
|
`readOnly`-Wächter im Code abgedeckt, aber nicht live durchgespielt (er
|
||||||
|
bräuchte ein echtes Pad).
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
- [^] #ed.fold.click: Fold and unfold in the diagram (S)
|
- [^] #ed.fold.click: Fold and unfold in the diagram (S)
|
||||||
- [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step
|
- [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step
|
||||||
- [^] #ed.fold.preset: One switch folds everything from size M downward (S) %% see D44
|
- [^] #ed.fold.preset: One switch folds everything from size M downward (S) %% see D44
|
||||||
|
- [x] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed
|
||||||
- [ ] #ed.files: Open and save .werkbaum files (S)
|
- [ ] #ed.files: Open and save .werkbaum files (S)
|
||||||
+ [^] #ed.fresh: Show what is new since your last visit (S)
|
+ [^] #ed.fresh: Show what is new since your last visit (S)
|
||||||
+ [?] #ed.percolor: A pastel colour per person (S)
|
+ [?] #ed.percolor: A pastel colour per person (S)
|
||||||
@@ -502,6 +503,12 @@
|
|||||||
smaller closed, L and above open. Size rather than depth — depth only says
|
smaller closed, L and above open. Size rather than depth — depth only says
|
||||||
how finely somebody happened to write it down.
|
how finely somebody happened to write it down.
|
||||||
|
|
||||||
|
#ed.snaps
|
||||||
|
A safety net against mishaps: the text is put aside every ten minutes, but
|
||||||
|
only when it differs from the last one kept, and the last twenty are offered
|
||||||
|
beside the document picker. Loading one is a single undo step, so a wrong
|
||||||
|
pick costs nothing.
|
||||||
|
|
||||||
#ed.files
|
#ed.files
|
||||||
Open a .werkbaum file and save one. Today the leading data format is the one
|
Open a .werkbaum file and save one. Today the leading data format is the one
|
||||||
thing that cannot leave the browser as a file, while the diagram can.
|
thing that cannot leave the browser as a file, while the diagram can.
|
||||||
|
|||||||
@@ -52,6 +52,9 @@
|
|||||||
<button type="button" class="copybtn panetoggle" id="paneToDiagram" data-i18n-title="paneToDiagram" data-i18n-aria="paneToDiagram" title="Zum Diagramm wechseln" aria-label="Zum Diagramm wechseln">
|
<button type="button" class="copybtn panetoggle" id="paneToDiagram" data-i18n-title="paneToDiagram" data-i18n-aria="paneToDiagram" title="Zum Diagramm wechseln" aria-label="Zum Diagramm wechseln">
|
||||||
<svg 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="3" width="6" height="4" rx="1"/><rect x="2.5" y="17" width="6" height="4" rx="1"/><rect x="15.5" y="17" width="6" height="4" rx="1"/><path d="M12 7V13M5.5 13H18.5M5.5 13V17M18.5 13V17"/></svg>
|
<svg 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="3" width="6" height="4" rx="1"/><rect x="2.5" y="17" width="6" height="4" rx="1"/><rect x="15.5" y="17" width="6" height="4" rx="1"/><path d="M12 7V13M5.5 13H18.5M5.5 13V17M18.5 13V17"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Dokument-Wähler und Snapshot-Knopf stehen zusammen; die Gruppe trägt
|
||||||
|
das `margin-right:auto` und ist der Bezug für das Snapshot-Menü. -->
|
||||||
|
<span class="docgroup">
|
||||||
<span class="doctrigger" id="docTrigger" role="button" tabindex="0" aria-haspopup="menu" aria-expanded="false" data-i18n-title="docSwitchTooltip" data-i18n-aria="docSwitchTooltip" title="Dokument wählen oder verwalten" aria-label="Dokument wählen oder verwalten">
|
<span class="doctrigger" id="docTrigger" role="button" tabindex="0" aria-haspopup="menu" aria-expanded="false" data-i18n-title="docSwitchTooltip" data-i18n-aria="docSwitchTooltip" title="Dokument wählen oder verwalten" aria-label="Dokument wählen oder verwalten">
|
||||||
<span class="docname" id="docName"></span>
|
<span class="docname" id="docName"></span>
|
||||||
<svg class="doc-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
<svg class="doc-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
||||||
@@ -65,6 +68,13 @@
|
|||||||
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
|
<button type="button" class="docact docdel" id="docDelete" data-i18n="docDelete">Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="snapwrap">
|
||||||
|
<button type="button" class="copybtn snapbtn" id="snapBtn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="snapTooltip" data-i18n-aria="snapTooltip" title="Frühere Stände dieses Dokuments" aria-label="Frühere Stände dieses Dokuments">
|
||||||
|
<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="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3 4v4h4"/><path d="M12 7.5V12l3 1.8"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="snapmenu" id="snapMenu" role="menu" data-i18n-aria="snapMenuAria" aria-label="Frühere Stände" hidden></div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<button type="button" class="copybtn padview" id="padViewBtn" data-view="text" hidden>
|
<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>
|
<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>
|
</button>
|
||||||
|
|||||||
+162
-1
@@ -1744,6 +1744,10 @@ const I18N = {
|
|||||||
brandTooltip:"„Werkbaum“ bedeutet so viel wie ‚Werk-Baum‘ — der Baum des Projektstrukturplans (WBS).",
|
brandTooltip:"„Werkbaum“ bedeutet so viel wie ‚Werk-Baum‘ — der Baum des Projektstrukturplans (WBS).",
|
||||||
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
|
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
|
||||||
docSwitchTooltip:"Dokument wählen oder verwalten", docMenuAria:"Dokumente",
|
docSwitchTooltip:"Dokument wählen oder verwalten", docMenuAria:"Dokumente",
|
||||||
|
/* Frühere Stände (D54) — alle 10 Minuten, nur bei Änderung. */
|
||||||
|
snapTooltip:"Frühere Stände dieses Dokuments", snapMenuAria:"Frühere Stände",
|
||||||
|
snapNone:"Noch keine früheren Stände – gesichert wird alle 10 Minuten, sobald sich etwas geändert hat.",
|
||||||
|
snapLines:"{n} Zeilen",
|
||||||
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
|
docNew:"Neues Dokument", docRename:"Umbenennen", docDelete:"Löschen",
|
||||||
docNewName:"Unbenannt",
|
docNewName:"Unbenannt",
|
||||||
docDeleteConfirm:"Dokument „{name}“ löschen?",
|
docDeleteConfirm:"Dokument „{name}“ löschen?",
|
||||||
@@ -1829,6 +1833,9 @@ const I18N = {
|
|||||||
brandTooltip:"“Werkbaum” means roughly ‘work tree’ — the tree of the work breakdown structure (WBS).",
|
brandTooltip:"“Werkbaum” means roughly ‘work tree’ — the tree of the work breakdown structure (WBS).",
|
||||||
editorTitle:"Structure (text)", diagramTitle:"Diagram",
|
editorTitle:"Structure (text)", diagramTitle:"Diagram",
|
||||||
docSwitchTooltip:"Choose or manage document", docMenuAria:"Documents",
|
docSwitchTooltip:"Choose or manage document", docMenuAria:"Documents",
|
||||||
|
snapTooltip:"Earlier states of this document", snapMenuAria:"Earlier states",
|
||||||
|
snapNone:"No earlier states yet – one is kept every 10 minutes, once something has changed.",
|
||||||
|
snapLines:"{n} lines",
|
||||||
docNew:"New document", docRename:"Rename", docDelete:"Delete",
|
docNew:"New document", docRename:"Rename", docDelete:"Delete",
|
||||||
docNewName:"Untitled",
|
docNewName:"Untitled",
|
||||||
docDeleteConfirm:"Delete document “{name}”?",
|
docDeleteConfirm:"Delete document “{name}”?",
|
||||||
@@ -1914,6 +1921,9 @@ const I18N = {
|
|||||||
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo’ — el árbol de la estructura de desglose del trabajo (EDT).",
|
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo’ — el árbol de la estructura de desglose del trabajo (EDT).",
|
||||||
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
|
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
|
||||||
docSwitchTooltip:"Elegir o gestionar documento", docMenuAria:"Documentos",
|
docSwitchTooltip:"Elegir o gestionar documento", docMenuAria:"Documentos",
|
||||||
|
snapTooltip:"Estados anteriores de este documento", snapMenuAria:"Estados anteriores",
|
||||||
|
snapNone:"Aún no hay estados anteriores: se guarda uno cada 10 minutos, cuando algo ha cambiado.",
|
||||||
|
snapLines:"{n} líneas",
|
||||||
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
|
docNew:"Nuevo documento", docRename:"Renombrar", docDelete:"Eliminar",
|
||||||
docNewName:"Sin título",
|
docNewName:"Sin título",
|
||||||
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
|
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
|
||||||
@@ -1999,6 +2009,9 @@ const I18N = {
|
|||||||
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — l’arbre de l’organigramme des tâches (WBS).",
|
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — l’arbre de l’organigramme des tâches (WBS).",
|
||||||
editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
|
editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
|
||||||
docSwitchTooltip:"Choisir ou gérer le document", docMenuAria:"Documents",
|
docSwitchTooltip:"Choisir ou gérer le document", docMenuAria:"Documents",
|
||||||
|
snapTooltip:"États antérieurs de ce document", snapMenuAria:"États antérieurs",
|
||||||
|
snapNone:"Pas encore d’état antérieur : un état est conservé toutes les 10 minutes, dès que quelque chose a changé.",
|
||||||
|
snapLines:"{n} lignes",
|
||||||
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
|
docNew:"Nouveau document", docRename:"Renommer", docDelete:"Supprimer",
|
||||||
docNewName:"Sans titre",
|
docNewName:"Sans titre",
|
||||||
docDeleteConfirm:"Supprimer le document « {name} » ?",
|
docDeleteConfirm:"Supprimer le document « {name} » ?",
|
||||||
@@ -2084,6 +2097,9 @@ const I18N = {
|
|||||||
brandTooltip:"„Werkbaum” znaczy mniej więcej ‚drzewo pracy’ — drzewo struktury podziału pracy (WBS).",
|
brandTooltip:"„Werkbaum” znaczy mniej więcej ‚drzewo pracy’ — drzewo struktury podziału pracy (WBS).",
|
||||||
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
|
editorTitle:"Struktura (tekst)", diagramTitle:"Diagram",
|
||||||
docSwitchTooltip:"Wybierz lub zarządzaj dokumentem", docMenuAria:"Dokumenty",
|
docSwitchTooltip:"Wybierz lub zarządzaj dokumentem", docMenuAria:"Dokumenty",
|
||||||
|
snapTooltip:"Wcześniejsze stany tego dokumentu", snapMenuAria:"Wcześniejsze stany",
|
||||||
|
snapNone:"Brak wcześniejszych stanów – zapisywany jest co 10 minut, gdy coś się zmieni.",
|
||||||
|
snapLines:"wierszy: {n}",
|
||||||
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
|
docNew:"Nowy dokument", docRename:"Zmień nazwę", docDelete:"Usuń",
|
||||||
docNewName:"Bez nazwy",
|
docNewName:"Bez nazwy",
|
||||||
docDeleteConfirm:"Usunąć dokument „{name}”?",
|
docDeleteConfirm:"Usunąć dokument „{name}”?",
|
||||||
@@ -2169,6 +2185,9 @@ const I18N = {
|
|||||||
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
|
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
|
||||||
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
|
editorTitle:"Структура (текст)", diagramTitle:"Диаграмма",
|
||||||
docSwitchTooltip:"Выбрать документ или управлять им", docMenuAria:"Документы",
|
docSwitchTooltip:"Выбрать документ или управлять им", docMenuAria:"Документы",
|
||||||
|
snapTooltip:"Прежние состояния этого документа", snapMenuAria:"Прежние состояния",
|
||||||
|
snapNone:"Прежних состояний пока нет — они сохраняются каждые 10 минут, если что-то изменилось.",
|
||||||
|
snapLines:"строк: {n}",
|
||||||
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
|
docNew:"Новый документ", docRename:"Переименовать", docDelete:"Удалить",
|
||||||
docNewName:"Без названия",
|
docNewName:"Без названия",
|
||||||
docDeleteConfirm:"Удалить документ «{name}»?",
|
docDeleteConfirm:"Удалить документ «{name}»?",
|
||||||
@@ -2254,6 +2273,9 @@ const I18N = {
|
|||||||
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
|
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
|
||||||
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
|
editorTitle:"संरचना (टेक्स्ट)", diagramTitle:"आरेख",
|
||||||
docSwitchTooltip:"दस्तावेज़ चुनें या प्रबंधित करें", docMenuAria:"दस्तावेज़",
|
docSwitchTooltip:"दस्तावेज़ चुनें या प्रबंधित करें", docMenuAria:"दस्तावेज़",
|
||||||
|
snapTooltip:"इस दस्तावेज़ की पिछली स्थितियाँ", snapMenuAria:"पिछली स्थितियाँ",
|
||||||
|
snapNone:"अभी कोई पिछली स्थिति नहीं — कुछ बदलने पर हर 10 मिनट में एक सहेजी जाती है।",
|
||||||
|
snapLines:"{n} पंक्तियाँ",
|
||||||
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
|
docNew:"नया दस्तावेज़", docRename:"नाम बदलें", docDelete:"हटाएँ",
|
||||||
docNewName:"बिना शीर्षक",
|
docNewName:"बिना शीर्षक",
|
||||||
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
|
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
|
||||||
@@ -2339,6 +2361,9 @@ const I18N = {
|
|||||||
riskTooltip:"高风险 – 工作量尚不明确。",
|
riskTooltip:"高风险 – 工作量尚不明确。",
|
||||||
editorTitle:"结构(文本)", diagramTitle:"图表",
|
editorTitle:"结构(文本)", diagramTitle:"图表",
|
||||||
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
|
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
|
||||||
|
snapTooltip:"此文档的早期状态", snapMenuAria:"早期状态",
|
||||||
|
snapNone:"暂无早期状态——内容有变动时,每 10 分钟保存一次。",
|
||||||
|
snapLines:"{n} 行",
|
||||||
docNew:"新建文档", docRename:"重命名", docDelete:"删除",
|
docNew:"新建文档", docRename:"重命名", docDelete:"删除",
|
||||||
docNewName:"未命名",
|
docNewName:"未命名",
|
||||||
docDeleteConfirm:"删除文档“{name}”?",
|
docDeleteConfirm:"删除文档“{name}”?",
|
||||||
@@ -2424,6 +2449,9 @@ const I18N = {
|
|||||||
riskTooltip:"高リスク – 規模はまだ不明。",
|
riskTooltip:"高リスク – 規模はまだ不明。",
|
||||||
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
||||||
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
|
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
|
||||||
|
snapTooltip:"このドキュメントの以前の状態", snapMenuAria:"以前の状態",
|
||||||
|
snapNone:"以前の状態はまだありません — 変更があれば 10 分ごとに保存されます。",
|
||||||
|
snapLines:"{n} 行",
|
||||||
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
|
docNew:"新規ドキュメント", docRename:"名前を変更", docDelete:"削除",
|
||||||
docNewName:"無題",
|
docNewName:"無題",
|
||||||
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
|
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
|
||||||
@@ -2869,6 +2897,130 @@ function toggleDocMenu(){ docMenu.hidden ? openDocMenu() : closeDocMenu(); }
|
|||||||
Dokument sichern, dann das Ziel laden und neu rendern. */
|
Dokument sichern, dann das Ziel laden und neu rendern. */
|
||||||
function flushActive(){ const d = activeDoc(); if(d) d.text = src.value; }
|
function flushActive(){ const d = activeDoc(); if(d) d.text = src.value; }
|
||||||
|
|
||||||
|
/* ---------- Frühere Stände (Snapshots, D54) ----------
|
||||||
|
Alle zehn Minuten wird der Text des aktiven Dokuments weggelegt — aber nur,
|
||||||
|
wenn er sich seit dem letzten Stand geändert hat. Aufgehoben werden die
|
||||||
|
letzten 20 je Dokument (rund 3½ Stunden Arbeit bei gleichmäßigen Abständen).
|
||||||
|
Es ist ein Sicherheitsnetz gegen Versehen, kein Versionsverwaltungssystem —
|
||||||
|
wer weiter zurück will, hat Git. */
|
||||||
|
const LS_SNAPS = 'werkbaum-snaps';
|
||||||
|
const SNAP_EVERY = 10 * 60 * 1000;
|
||||||
|
const SNAP_KEEP = 20;
|
||||||
|
let snaps = {}; /* {docId: [{t, text}, …]} — ältester zuerst */
|
||||||
|
let snapBase = ''; /* Text bei Dokumentwechsel; Vergleich, solange es keinen Stand gibt */
|
||||||
|
|
||||||
|
function loadSnaps(){
|
||||||
|
try{ const o = JSON.parse(localStorage.getItem(LS_SNAPS) || '{}');
|
||||||
|
snaps = o && typeof o === 'object' ? o : {}; }
|
||||||
|
catch(_){ snaps = {}; }
|
||||||
|
}
|
||||||
|
/* Der Platz im localStorage ist geteilt. Läuft er über, sollen die
|
||||||
|
**Dokumente** überleben, nicht ihre Stände — der Fehlerfall wirft deshalb
|
||||||
|
Stände weg, bis es passt, notfalls alle. */
|
||||||
|
function persistSnaps(){
|
||||||
|
for(;;){
|
||||||
|
try{ localStorage.setItem(LS_SNAPS, JSON.stringify(snaps)); return true; }
|
||||||
|
catch(_){
|
||||||
|
if(!dropOldestSnap()){ try{ localStorage.removeItem(LS_SNAPS); }catch(_){} return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function dropOldestSnap(){
|
||||||
|
let id = null, t = Infinity;
|
||||||
|
for(const k in snaps){
|
||||||
|
const l = snaps[k];
|
||||||
|
if(l && l.length && l[0].t < t){ t = l[0].t; id = k; }
|
||||||
|
}
|
||||||
|
if(id === null) return false;
|
||||||
|
snaps[id].shift();
|
||||||
|
if(!snaps[id].length) delete snaps[id];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/* Legt den aktuellen Text weg, wenn er neu ist. Pad-Dokumente (D31) bleiben
|
||||||
|
außen vor: Ihr Text ist schreibgeschützt, ein alter Stand ließe sich dort
|
||||||
|
gar nicht wieder einsetzen — Stände zu sammeln, die niemand laden kann,
|
||||||
|
wäre nur Ballast. */
|
||||||
|
function snapshotNow(){
|
||||||
|
const d = activeDoc();
|
||||||
|
if(!d || src.readOnly) return false;
|
||||||
|
const text = src.value;
|
||||||
|
const list = snaps[d.id] || (snaps[d.id] = []);
|
||||||
|
const letzter = list.length ? list[list.length-1].text : snapBase;
|
||||||
|
if(text === letzter) return false;
|
||||||
|
list.push({t: Date.now(), text});
|
||||||
|
while(list.length > SNAP_KEEP) list.shift();
|
||||||
|
snapBase = text;
|
||||||
|
persistSnaps();
|
||||||
|
if(!snapMenu.hidden) renderSnapMenu();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
setInterval(snapshotNow, SNAP_EVERY);
|
||||||
|
|
||||||
|
const snapBtn = document.getElementById('snapBtn');
|
||||||
|
const snapMenu = document.getElementById('snapMenu');
|
||||||
|
|
||||||
|
function snapLabel(ms){
|
||||||
|
const d = new Date(ms);
|
||||||
|
const heute = d.toDateString() === new Date().toDateString();
|
||||||
|
try{
|
||||||
|
return heute
|
||||||
|
? d.toLocaleTimeString(lang, {hour: '2-digit', minute: '2-digit'})
|
||||||
|
: d.toLocaleString(lang, {day: '2-digit', month: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit'});
|
||||||
|
}catch(_){ return d.toISOString().slice(0, 16).replace('T', ' '); }
|
||||||
|
}
|
||||||
|
function renderSnapMenu(){
|
||||||
|
const d = activeDoc();
|
||||||
|
const list = d ? (snaps[d.id] || []) : [];
|
||||||
|
snapMenu.innerHTML = '';
|
||||||
|
if(!list.length){
|
||||||
|
const p = document.createElement('div');
|
||||||
|
p.className = 'snapempty';
|
||||||
|
p.textContent = t('snapNone');
|
||||||
|
snapMenu.appendChild(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Neueste zuoberst — danach sucht man zuerst. */
|
||||||
|
list.slice().reverse().forEach(s => {
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.type = 'button';
|
||||||
|
b.className = 'snapitem';
|
||||||
|
b.setAttribute('role', 'menuitem');
|
||||||
|
b.innerHTML = '<span></span><span class="snapsize"></span>';
|
||||||
|
b.firstChild.textContent = snapLabel(s.t);
|
||||||
|
b.lastChild.textContent = t('snapLines', {n: s.text.split('\n').length});
|
||||||
|
b.addEventListener('click', e => { e.stopPropagation(); loadSnapshot(s); });
|
||||||
|
snapMenu.appendChild(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* Zurückgeholt wird **undo-fähig** (D53): Ein Griff daneben kostet ein Strg+Z,
|
||||||
|
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. */
|
||||||
|
function loadSnapshot(s){
|
||||||
|
if(src.readOnly) return;
|
||||||
|
snapshotNow();
|
||||||
|
closeSnapMenu();
|
||||||
|
if(!replaceTextUndoable(s.text)) render();
|
||||||
|
snapBase = src.value;
|
||||||
|
}
|
||||||
|
function openSnapMenu(){
|
||||||
|
renderSnapMenu();
|
||||||
|
snapMenu.hidden = false;
|
||||||
|
snapBtn.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
function closeSnapMenu(){
|
||||||
|
snapMenu.hidden = true;
|
||||||
|
snapBtn.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
function updateSnapBtn(){ snapBtn.hidden = src.readOnly; if(src.readOnly) closeSnapMenu(); }
|
||||||
|
snapBtn.addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
snapMenu.hidden ? openSnapMenu() : closeSnapMenu();
|
||||||
|
});
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if(!snapMenu.hidden && !snapMenu.contains(e.target) && !snapBtn.contains(e.target)) closeSnapMenu();
|
||||||
|
});
|
||||||
|
|
||||||
/* ---------- „Was ist neu?" (D28) ----------
|
/* ---------- „Was ist neu?" (D28) ----------
|
||||||
Verglichen wird gegen die zuletzt **gesehene** Fassung eines Dokuments von
|
Verglichen wird gegen die zuletzt **gesehene** Fassung eines Dokuments von
|
||||||
außen (mitgeliefert oder ?sourceUrl=), nicht gegen die letzte Auslieferung:
|
außen (mitgeliefert oder ?sourceUrl=), nicht gegen die letzte Auslieferung:
|
||||||
@@ -2918,7 +3070,11 @@ function updateFreshBtn(){
|
|||||||
}
|
}
|
||||||
freshBtn.addEventListener('click', acknowledgeFresh);
|
freshBtn.addEventListener('click', acknowledgeFresh);
|
||||||
|
|
||||||
function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : ''; render(); updateDocName(); updateFreshBtn(); }
|
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(); updateSnapBtn();
|
||||||
|
render(); updateDocName(); updateFreshBtn(); }
|
||||||
function switchDoc(id){
|
function switchDoc(id){
|
||||||
if(id === activeId) return;
|
if(id === activeId) return;
|
||||||
flushActive();
|
flushActive();
|
||||||
@@ -2977,6 +3133,7 @@ function deleteDoc(){
|
|||||||
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(padSource && padSource.id === d.id) stopPad(); /* danach gibt es nichts mehr zu holen (D31) */
|
||||||
docs = docs.filter(x => x.id !== d.id);
|
docs = docs.filter(x => x.id !== d.id);
|
||||||
|
if(snaps[d.id]){ delete snaps[d.id]; persistSnaps(); } /* mit dem Dokument gehen seine Stände (D54) */
|
||||||
if(!docs.length) docs = [{ id: EXAMPLE_ID, name: EXAMPLE_NAME, text: INITIAL }];
|
if(!docs.length) docs = [{ id: EXAMPLE_ID, name: EXAMPLE_NAME, text: INITIAL }];
|
||||||
activeId = docs[0].id;
|
activeId = docs[0].id;
|
||||||
foldOverrides.clear();
|
foldOverrides.clear();
|
||||||
@@ -2988,9 +3145,12 @@ function deleteDoc(){
|
|||||||
function initDocs(){
|
function initDocs(){
|
||||||
restoring = true;
|
restoring = true;
|
||||||
loadDocs();
|
loadDocs();
|
||||||
|
loadSnaps();
|
||||||
const d = activeDoc();
|
const d = activeDoc();
|
||||||
src.value = d ? d.text : '';
|
src.value = d ? d.text : '';
|
||||||
|
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();
|
||||||
@@ -3268,6 +3428,7 @@ function updatePadLink(){
|
|||||||
const d = activeDoc();
|
const d = activeDoc();
|
||||||
const isPad = !!(d && padSource && d.id === padSource.id);
|
const isPad = !!(d && padSource && d.id === padSource.id);
|
||||||
src.readOnly = isPad;
|
src.readOnly = isPad;
|
||||||
|
updateSnapBtn(); /* Pad-Dokumente sammeln keine Stände (D54) */
|
||||||
src.classList.toggle('readonly', isPad);
|
src.classList.toggle('readonly', isPad);
|
||||||
if(srcWrap) srcWrap.classList.toggle('readonly', isPad); /* Streifen mit abtönen */
|
if(srcWrap) srcWrap.classList.toggle('readonly', isPad); /* Streifen mit abtönen */
|
||||||
if(isPad) src.title = t('padReadonly'); else src.removeAttribute('title');
|
if(isPad) src.title = t('padReadonly'); else src.removeAttribute('title');
|
||||||
|
|||||||
+35
-1
@@ -414,9 +414,22 @@
|
|||||||
/* Dokument-Wähler: der Name des aktiven Dokuments in der Editor-Titelzeile ist
|
/* Dokument-Wähler: der Name des aktiven Dokuments in der Editor-Titelzeile ist
|
||||||
zugleich der Auslöser eines Dropdowns zum Wechseln/Anlegen/Umbenennen/Löschen
|
zugleich der Auslöser eines Dropdowns zum Wechseln/Anlegen/Umbenennen/Löschen
|
||||||
(localStorage-Dokumente, noch kein Backend). */
|
(localStorage-Dokumente, noch kein Backend). */
|
||||||
|
/* Wähler + Snapshot-Knopf als eine Gruppe: Sie trägt das `margin-right:auto`
|
||||||
|
(schiebt die übrigen Knöpfe nach rechts) und ist der Positionsbezug des
|
||||||
|
Snapshot-Menüs — dadurch klappt es unter seinem Knopf auf, ohne dass JS
|
||||||
|
rechnen muss. Am Wähler selbst darf das auto-Margin nicht mehr stehen,
|
||||||
|
sonst schöbe es den Snapshot-Knopf von ihm weg. */
|
||||||
|
.docgroup{
|
||||||
|
display:inline-flex;align-items:center;gap:4px;
|
||||||
|
margin-right:auto;max-width:62%;min-width:0;
|
||||||
|
}
|
||||||
|
/* Bezug des Menüs ist der KNOPF, nicht die Gruppe: An der Gruppe ausgerichtet
|
||||||
|
(`right:0`) begänne es 190 px weiter links und liefe aus dem Panel heraus —
|
||||||
|
nachgemessen bei der ersten Fassung. */
|
||||||
|
.snapwrap{position:relative;display:inline-flex;flex:0 0 auto}
|
||||||
.doctrigger{
|
.doctrigger{
|
||||||
display:inline-flex;align-items:center;gap:5px;cursor:pointer;
|
display:inline-flex;align-items:center;gap:5px;cursor:pointer;
|
||||||
margin-right:auto;max-width:60%;color:var(--muted);
|
min-width:0;color:var(--muted);
|
||||||
padding:2px 6px;margin-left:-6px;border-radius:6px;
|
padding:2px 6px;margin-left:-6px;border-radius:6px;
|
||||||
text-transform:none;letter-spacing:0;font-family:'IBM Plex Mono',monospace;
|
text-transform:none;letter-spacing:0;font-family:'IBM Plex Mono',monospace;
|
||||||
}
|
}
|
||||||
@@ -450,6 +463,27 @@
|
|||||||
padding:5px 7px;color:var(--ink);background:var(--card);
|
padding:5px 7px;color:var(--ink);background:var(--card);
|
||||||
font-family:inherit;font-size:.86rem;outline:none;
|
font-family:inherit;font-size:.86rem;outline:none;
|
||||||
}
|
}
|
||||||
|
/* Snapshot-Menü (D54): wie das Dokument-Menü, aber am rechten Rand seiner
|
||||||
|
Gruppe ausgerichtet — also unter dem Knopf. */
|
||||||
|
.snapbtn{flex:0 0 auto}
|
||||||
|
.snapmenu{
|
||||||
|
position:absolute;top:calc(100% + 6px);left:0;z-index:50;min-width:190px;
|
||||||
|
max-height:280px;overflow:auto;
|
||||||
|
display:flex;flex-direction:column;gap:2px;
|
||||||
|
background:var(--card);border:1px solid rgba(36,52,71,.15);border-radius:10px;
|
||||||
|
padding:6px;box-shadow:0 8px 24px rgba(36,52,71,.22);
|
||||||
|
text-transform:none;letter-spacing:0;font-family:'IBM Plex Sans',sans-serif;
|
||||||
|
}
|
||||||
|
.snapmenu[hidden]{display:none}
|
||||||
|
.snapitem{
|
||||||
|
display:flex;align-items:baseline;gap:8px;width:100%;
|
||||||
|
border:0;background:transparent;cursor:pointer;text-align:left;
|
||||||
|
padding:6px 9px;border-radius:7px;color:var(--ink);
|
||||||
|
font-family:inherit;font-size:.84rem;white-space:nowrap;
|
||||||
|
}
|
||||||
|
.snapitem:hover{background:rgba(15,118,110,.08)}
|
||||||
|
.snapitem .snapsize{margin-left:auto;color:var(--muted);font-size:.74rem}
|
||||||
|
.snapempty{padding:7px 9px;color:var(--muted);font-size:.8rem}
|
||||||
.docmenu-actions{
|
.docmenu-actions{
|
||||||
display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;padding-top:6px;
|
display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;padding-top:6px;
|
||||||
border-top:1px solid rgba(36,52,71,.12);
|
border-top:1px solid rgba(36,52,71,.12);
|
||||||
|
|||||||
Reference in New Issue
Block a user