feat: Lokale Dateien öffnen und als Datei speichern (D72, Stufe 1)
Zwei Einträge im Dokumenten-Menü: „Datei öffnen…" (Datei-Input, neues Dokument, Dateiname als Name) und „Als Datei speichern" (Blob-Download, Dateiname aus dem Dokumentnamen via localfile.js). Läuft in jedem Browser; Stufe 2 (File System Access, Chromium) folgt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ced3507678
commit
defeaf84d0
@@ -19,6 +19,7 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-25
|
## 2026-08-25
|
||||||
|
|
||||||
|
- Open a local `.werkbaum` file and save the document back as a file, from the document menu
|
||||||
- An AI integration recorded as an idea in the plan — edit the tree in a dialogue, with your own API key and `llms.md` as the model's guide
|
- An AI integration recorded as an idea in the plan — edit the tree in a dialogue, with your own API key and `llms.md` as the model's guide
|
||||||
- A warning flags the bottleneck when one `@name` carries more than half of the open work on the cheapest path — their tag pills turn amber on the open nodes
|
- A warning flags the bottleneck when one `@name` carries more than half of the open work on the cheapest path — their tag pills turn amber on the open nodes
|
||||||
- The estimated size of an unsized node now prices the remaining work — done sub-packages drop out, and a subtree with everything named done counts as XS
|
- The estimated size of an unsized node now prices the remaining work — done sub-packages drop out, and a subtree with everything named done counts as XS
|
||||||
|
|||||||
@@ -5288,3 +5288,54 @@ genau der Solo-Test fällt; Schwelle aufgeweicht → genau die drei
|
|||||||
Schwellen-Tests; Vererbung entfernt → genau der Vererbungs-Test; Teilung
|
Schwellen-Tests; Vererbung entfernt → genau der Vererbungs-Test; Teilung
|
||||||
entfernt → genau der Mehrfach-Tag-Test; Erledigt-Ausnahme der Pille
|
entfernt → genau der Mehrfach-Tag-Test; Erledigt-Ausnahme der Pille
|
||||||
entfernt → genau der Pillen-Test.
|
entfernt → genau der Pillen-Test.
|
||||||
|
|
||||||
|
## D72 — Lokale Dateien öffnen und speichern, in zwei Stufen
|
||||||
|
Der Notationstext ist das führende Datenformat (D14) — und war zugleich das
|
||||||
|
Einzige, das den Browser nicht als Datei verlassen konnte, während das
|
||||||
|
Diagramm zwei Download-Knöpfe hat. D24 hatte den Fall vorgesehen („ein
|
||||||
|
künftiges Öffnen/Speichern im Editor — dann als `accept`-Filter und
|
||||||
|
Download-Endung"). Entschieden (Nutzer): **zwei Stufen** — zuerst der
|
||||||
|
klassische Weg, der in jedem Browser läuft; darauf die File System Access API
|
||||||
|
für Chromium, die aus „Speichern unter" ein echtes „Speichern" macht.
|
||||||
|
|
||||||
|
**Stufe 1: Datei-Input und Blob-Download, im Dokumenten-Menü.** Zwei Einträge
|
||||||
|
neben „Neues Dokument": „Datei öffnen…" (verstecktes
|
||||||
|
`<input type="file" accept=".werkbaum,.txt,text/plain">`, gelesen per
|
||||||
|
`file.text()`) und „Als Datei speichern" (Blob + `<a download>`, über das
|
||||||
|
vorhandene `saveBlob()` des Grafikexports). Das Menü ist der richtige Ort:
|
||||||
|
Beide Aktionen handeln davon, *welcher Text* da ist — wie Anlegen, Umbenennen,
|
||||||
|
Wiederherstellen (D22).
|
||||||
|
|
||||||
|
**Geöffnet wird als NEUES Dokument, nicht per Namens-Identität.** D23 lässt
|
||||||
|
denselben `?sourceUrl=`-Link dasselbe Dokument aktualisieren — dort ist die
|
||||||
|
URL eine echte Adresse. Ein Dateiname ist keine: Zwei verschiedene Dateien
|
||||||
|
gleichen Namens (`plan.werkbaum` aus zwei Ordnern) überschrieben sich still,
|
||||||
|
und der stille Fehler ist der schlimmere (D59-Linie). Wer dieselbe Datei
|
||||||
|
zweimal öffnet, bekommt eben `plan.werkbaum` und `plan.werkbaum (2)` — sichtbar
|
||||||
|
und harmlos; die echte Datei-Identität bringt erst das Handle der Stufe 2.
|
||||||
|
Der Dateiname wird der Dokumentname (über `uniqueName`, wie überall).
|
||||||
|
|
||||||
|
**Der Dateiname beim Speichern entsteht aus dem Dokumentnamen** — headless in
|
||||||
|
`localfile.js` (`saveFileName`, Hausregel D54-Nachtrag 3): verbotene Zeichen
|
||||||
|
und Pfadtrenner werden zu `-` (URL-Namen aus D23 bleiben so lesbar), führende
|
||||||
|
Punkte fallen weg (sonst entstünde eine versteckte Datei), die Endung
|
||||||
|
`.werkbaum` kommt dazu, wenn nicht schon `.werkbaum` oder `.txt` dasteht
|
||||||
|
(D24: `.txt` bleibt zulässig), leerer Rest fällt auf `plan.werkbaum` zurück.
|
||||||
|
Gespeichert wird als `text/plain;charset=utf-8` mit LF — die D24-Konvention;
|
||||||
|
Pad-Dokumente (D31) dürfen ebenso gespeichert werden (der Schreibschutz gilt
|
||||||
|
dem Textfeld, nicht dem Export).
|
||||||
|
|
||||||
|
**Kein SPEC-Eintrag:** Öffnen/Speichern ist Dokumentverwaltung wie der
|
||||||
|
Wähler (D22) und die früheren Stände (D54) — Notation und Darstellung des
|
||||||
|
Plans ändern sich nicht; `llms.md` bleibt unberührt. Die Endungs-Konvention
|
||||||
|
steht seit D24 in SPEC §12.
|
||||||
|
|
||||||
|
**Nachgemessen** im Browser (echte `File` per DataTransfer — das prüft den
|
||||||
|
vollständigen Weg samt `file.text()`; der Download mit abgefangenem
|
||||||
|
Anchor-Klick und zurückgelesenem Blob): Öffnen legt ein drittes Dokument
|
||||||
|
„probe-plan.werkbaum" an, aktiviert es, Diagramm zeigt dessen 2 Knoten, der
|
||||||
|
Input ist geleert (dieselbe Datei bleibt erneut wählbar); Speichern liefert
|
||||||
|
`probe-plan.werkbaum` mit byte-identischem Inhalt. 479 Tests, davon 9 neue in
|
||||||
|
`tests/localfile.test.js`. Werkzeuggrenze wie in D25/D53: Der echte
|
||||||
|
Dateidialog und der echte Download lassen sich nicht automatisiert auslösen —
|
||||||
|
geprüft ist alles bis an diese Kante.
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
- [^] #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
|
||||||
- [^] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed
|
- [^] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed
|
||||||
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
|
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
|
||||||
- [ ] #ed.files: Open and save .werkbaum files (S)
|
- [x] #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.fresh.news: A star in the header, with the last few days (S) %% see D58
|
- [^] #ed.fresh.news: A star in the header, with the last few days (S) %% see D58
|
||||||
+ [?] #ed.percolor: A pastel colour per person (S)
|
+ [?] #ed.percolor: A pastel colour per person (S)
|
||||||
@@ -604,8 +604,9 @@
|
|||||||
unchanged and already at the top of the list.
|
unchanged and already at the top of the list.
|
||||||
|
|
||||||
#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, from the document menu. The leading data
|
||||||
thing that cannot leave the browser as a file, while the diagram can.
|
format was the one thing that could not leave the browser as a file, while
|
||||||
|
the diagram always could.
|
||||||
|
|
||||||
#ed.fresh
|
#ed.fresh
|
||||||
For documents that come from outside, nodes that went to production since
|
For documents that come from outside, nodes that went to production since
|
||||||
|
|||||||
@@ -74,6 +74,8 @@
|
|||||||
<div class="docmenu-list" id="docList"></div>
|
<div class="docmenu-list" id="docList"></div>
|
||||||
<div class="docmenu-actions">
|
<div class="docmenu-actions">
|
||||||
<button type="button" class="docact" id="docNew" data-i18n="docNew">Neues Dokument</button>
|
<button type="button" class="docact" id="docNew" data-i18n="docNew">Neues Dokument</button>
|
||||||
|
<button type="button" class="docact" id="docOpenFile" data-i18n="docOpenFile">Datei öffnen…</button>
|
||||||
|
<button type="button" class="docact" id="docSaveFile" data-i18n="docSaveFile">Als Datei speichern</button>
|
||||||
<button type="button" class="docact" id="docRename" data-i18n="docRename">Umbenennen</button>
|
<button type="button" class="docact" id="docRename" data-i18n="docRename">Umbenennen</button>
|
||||||
<button type="button" class="docact" id="docRestore" data-i18n="docRestore" hidden>Original wiederherstellen</button>
|
<button type="button" class="docact" id="docRestore" data-i18n="docRestore" hidden>Original wiederherstellen</button>
|
||||||
<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>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { padUrls } from './remote.js';
|
|||||||
import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js';
|
import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js';
|
||||||
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
|
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
|
||||||
from './snapshots.js';
|
from './snapshots.js';
|
||||||
|
import { FILE_ACCEPT, saveFileName } from './localfile.js';
|
||||||
/* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
|
/* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
|
||||||
vite.config.js). Zur Laufzeit gibt es kein git — und keinen Server, der
|
vite.config.js). Zur Laufzeit gibt es kein git — und keinen Server, der
|
||||||
nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */
|
nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */
|
||||||
@@ -2191,6 +2192,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"Dokument „{name}“ löschen?",
|
docDeleteConfirm:"Dokument „{name}“ löschen?",
|
||||||
docRestore:"Original wiederherstellen",
|
docRestore:"Original wiederherstellen",
|
||||||
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
|
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
|
||||||
|
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern",
|
||||||
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
|
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
|
||||||
copyDiagramTooltip:"Diagramm als PNG-Bild in die Zwischenablage kopieren",
|
copyDiagramTooltip:"Diagramm als PNG-Bild in die Zwischenablage kopieren",
|
||||||
downloadDiagramTooltip:"Diagramm als SVG-Datei herunterladen (z. B. für LibreOffice: Einfügen → Bild)",
|
downloadDiagramTooltip:"Diagramm als SVG-Datei herunterladen (z. B. für LibreOffice: Einfügen → Bild)",
|
||||||
@@ -2296,6 +2298,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"Delete document “{name}”?",
|
docDeleteConfirm:"Delete document “{name}”?",
|
||||||
docRestore:"Restore original",
|
docRestore:"Restore original",
|
||||||
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
|
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
|
||||||
|
docOpenFile:"Open file…", docSaveFile:"Save as file",
|
||||||
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
|
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
|
||||||
copyDiagramTooltip:"Copy diagram as a PNG image to the clipboard",
|
copyDiagramTooltip:"Copy diagram as a PNG image to the clipboard",
|
||||||
downloadDiagramTooltip:"Download diagram as an SVG file (e.g. for LibreOffice: Insert → Image)",
|
downloadDiagramTooltip:"Download diagram as an SVG file (e.g. for LibreOffice: Insert → Image)",
|
||||||
@@ -2401,6 +2404,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
|
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
|
||||||
docRestore:"Restaurar original",
|
docRestore:"Restaurar original",
|
||||||
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
|
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
|
||||||
|
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo",
|
||||||
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
|
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
|
||||||
copyDiagramTooltip:"Copiar el diagrama como imagen PNG al portapapeles",
|
copyDiagramTooltip:"Copiar el diagrama como imagen PNG al portapapeles",
|
||||||
downloadDiagramTooltip:"Descargar el diagrama como archivo SVG (p. ej. para LibreOffice: Insertar → Imagen)",
|
downloadDiagramTooltip:"Descargar el diagrama como archivo SVG (p. ej. para LibreOffice: Insertar → Imagen)",
|
||||||
@@ -2506,6 +2510,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"Supprimer le document « {name} » ?",
|
docDeleteConfirm:"Supprimer le document « {name} » ?",
|
||||||
docRestore:"Restaurer l’original",
|
docRestore:"Restaurer l’original",
|
||||||
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
|
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
|
||||||
|
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier",
|
||||||
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
|
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
|
||||||
copyDiagramTooltip:"Copier le diagramme comme image PNG dans le presse-papiers",
|
copyDiagramTooltip:"Copier le diagramme comme image PNG dans le presse-papiers",
|
||||||
downloadDiagramTooltip:"Télécharger le diagramme en fichier SVG (p. ex. pour LibreOffice : Insertion → Image)",
|
downloadDiagramTooltip:"Télécharger le diagramme en fichier SVG (p. ex. pour LibreOffice : Insertion → Image)",
|
||||||
@@ -2611,6 +2616,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"Usunąć dokument „{name}”?",
|
docDeleteConfirm:"Usunąć dokument „{name}”?",
|
||||||
docRestore:"Przywróć oryginał",
|
docRestore:"Przywróć oryginał",
|
||||||
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
|
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
|
||||||
|
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik",
|
||||||
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
|
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
|
||||||
copyDiagramTooltip:"Kopiuj diagram jako obraz PNG do schowka",
|
copyDiagramTooltip:"Kopiuj diagram jako obraz PNG do schowka",
|
||||||
downloadDiagramTooltip:"Pobierz diagram jako plik SVG (np. dla LibreOffice: Wstaw → Obraz)",
|
downloadDiagramTooltip:"Pobierz diagram jako plik SVG (np. dla LibreOffice: Wstaw → Obraz)",
|
||||||
@@ -2716,6 +2722,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"Удалить документ «{name}»?",
|
docDeleteConfirm:"Удалить документ «{name}»?",
|
||||||
docRestore:"Восстановить оригинал",
|
docRestore:"Восстановить оригинал",
|
||||||
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
|
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
|
||||||
|
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл",
|
||||||
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
|
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
|
||||||
copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена",
|
copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена",
|
||||||
downloadDiagramTooltip:"Скачать диаграмму как файл SVG (напр. для LibreOffice: Вставка → Изображение)",
|
downloadDiagramTooltip:"Скачать диаграмму как файл SVG (напр. для LibreOffice: Вставка → Изображение)",
|
||||||
@@ -2821,6 +2828,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
|
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
|
||||||
docRestore:"मूल पुनर्स्थापित करें",
|
docRestore:"मूल पुनर्स्थापित करें",
|
||||||
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
|
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
|
||||||
|
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें",
|
||||||
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
|
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
|
||||||
copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें",
|
copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें",
|
||||||
downloadDiagramTooltip:"आरेख को SVG फ़ाइल के रूप में डाउनलोड करें (जैसे LibreOffice: सम्मिलित करें → छवि)",
|
downloadDiagramTooltip:"आरेख को SVG फ़ाइल के रूप में डाउनलोड करें (जैसे LibreOffice: सम्मिलित करें → छवि)",
|
||||||
@@ -2926,6 +2934,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"删除文档“{name}”?",
|
docDeleteConfirm:"删除文档“{name}”?",
|
||||||
docRestore:"恢复原始版本",
|
docRestore:"恢复原始版本",
|
||||||
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
|
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
|
||||||
|
docOpenFile:"打开文件…", docSaveFile:"另存为文件",
|
||||||
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
|
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
|
||||||
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
|
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
|
||||||
downloadDiagramTooltip:"将图表下载为 SVG 文件(例如用于 LibreOffice:插入 → 图像)",
|
downloadDiagramTooltip:"将图表下载为 SVG 文件(例如用于 LibreOffice:插入 → 图像)",
|
||||||
@@ -3031,6 +3040,7 @@ const I18N = {
|
|||||||
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
|
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
|
||||||
docRestore:"オリジナルを復元",
|
docRestore:"オリジナルを復元",
|
||||||
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
|
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
|
||||||
|
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存",
|
||||||
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
|
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
|
||||||
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
|
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
|
||||||
downloadDiagramTooltip:"ダイアグラムを SVG ファイルとしてダウンロード(例:LibreOffice の 挿入 → 画像)",
|
downloadDiagramTooltip:"ダイアグラムを SVG ファイルとしてダウンロード(例:LibreOffice の 挿入 → 画像)",
|
||||||
@@ -3845,6 +3855,43 @@ function deleteDoc(){
|
|||||||
persistDocs();
|
persistDocs();
|
||||||
closeDocMenu();
|
closeDocMenu();
|
||||||
}
|
}
|
||||||
|
/* ---------- Lokale Dateien öffnen und speichern (D72, Stufe 1) ----------
|
||||||
|
Der klassische Weg, der in jedem Browser läuft: Datei-Input zum Öffnen,
|
||||||
|
Blob-Download zum Speichern. Geöffnet wird als NEUES Dokument (D22) — eine
|
||||||
|
Identität „gleicher Dateiname = gleiches Dokument" wäre eine Vermutung, und
|
||||||
|
zwei verschiedene Dateien gleichen Namens überschrieben sich still. Der
|
||||||
|
Dateiname wird der Dokumentname; beim Speichern entsteht er daraus zurück
|
||||||
|
(saveFileName, localfile.js). */
|
||||||
|
const fileOpenInput = document.createElement('input');
|
||||||
|
fileOpenInput.type = 'file';
|
||||||
|
fileOpenInput.accept = FILE_ACCEPT;
|
||||||
|
fileOpenInput.hidden = true;
|
||||||
|
document.body.appendChild(fileOpenInput);
|
||||||
|
fileOpenInput.addEventListener('change', async () => {
|
||||||
|
const f = fileOpenInput.files && fileOpenInput.files[0];
|
||||||
|
fileOpenInput.value = ''; /* dieselbe Datei soll erneut wählbar sein */
|
||||||
|
if(!f) return;
|
||||||
|
let text;
|
||||||
|
try{ text = await f.text(); }catch(_){ return; }
|
||||||
|
flushActive();
|
||||||
|
const d = { id: uid(), name: uniqueName(f.name), text };
|
||||||
|
docs.push(d);
|
||||||
|
activeId = d.id;
|
||||||
|
foldOverrides.clear();
|
||||||
|
loadActiveIntoEditor();
|
||||||
|
persistDocs();
|
||||||
|
closeDocMenu();
|
||||||
|
});
|
||||||
|
function openLocalFile(){ fileOpenInput.click(); }
|
||||||
|
/* Speichern über den vorhandenen Blob-Download (saveBlob, Grafikexport):
|
||||||
|
UTF-8, LF — das Textfeld normalisiert Zeilenenden ohnehin auf \n (D24). */
|
||||||
|
function saveLocalFile(){
|
||||||
|
flushActive();
|
||||||
|
const d = activeDoc();
|
||||||
|
if(!d) return;
|
||||||
|
saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name));
|
||||||
|
closeDocMenu();
|
||||||
|
}
|
||||||
/* Dokumente laden + aktiven Text in den Editor holen (nach applyLang). */
|
/* Dokumente laden + aktiven Text in den Editor holen (nach applyLang). */
|
||||||
function initDocs(){
|
function initDocs(){
|
||||||
restoring = true;
|
restoring = true;
|
||||||
@@ -4176,6 +4223,8 @@ docList.addEventListener('click', e => {
|
|||||||
/* `newDoc()` zeichnet das Menü selbst neu — es geht direkt ins Umbenennen
|
/* `newDoc()` zeichnet das Menü selbst neu — es geht direkt ins Umbenennen
|
||||||
(D51), und ein zweiter Durchlauf baute das Eingabefeld nur noch einmal auf. */
|
(D51), und ein zweiter Durchlauf baute das Eingabefeld nur noch einmal auf. */
|
||||||
document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); });
|
document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); });
|
||||||
|
document.getElementById('docOpenFile').addEventListener('click', e => { e.stopPropagation(); openLocalFile(); });
|
||||||
|
document.getElementById('docSaveFile').addEventListener('click', e => { e.stopPropagation(); saveLocalFile(); });
|
||||||
document.getElementById('docRename').addEventListener('click', e => { e.stopPropagation(); renameDoc(); });
|
document.getElementById('docRename').addEventListener('click', e => { e.stopPropagation(); renameDoc(); });
|
||||||
document.getElementById('docDelete').addEventListener('click', e => { e.stopPropagation(); deleteDoc(); });
|
document.getElementById('docDelete').addEventListener('click', e => { e.stopPropagation(); deleteDoc(); });
|
||||||
document.getElementById('docRestore').addEventListener('click', e => { e.stopPropagation(); restoreDoc(); });
|
document.getElementById('docRestore').addEventListener('click', e => { e.stopPropagation(); restoreDoc(); });
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* Öffnen und Speichern lokaler Dateien (D72).
|
||||||
|
Hier liegt die entscheidbare Hälfte (Hausregel, D54-Nachtrag 3): welcher
|
||||||
|
Dateiname aus einem Dokumentnamen wird. Die I/O — Datei-Input, Blob-Download,
|
||||||
|
ab Stufe 2 die File System Access API — bleibt in app.js. */
|
||||||
|
|
||||||
|
/* Der accept-Filter des Öffnen-Dialogs (D24: die Endung macht Dateien
|
||||||
|
zuordenbar; .txt bleibt zulässig — die Endung ist Konvention, kein Vertrag). */
|
||||||
|
export const FILE_ACCEPT = '.werkbaum,.txt,text/plain';
|
||||||
|
|
||||||
|
/* Dateiname für das Speichern, abgeleitet aus dem Dokumentnamen.
|
||||||
|
Dokumentnamen sind freier Text — bei ?sourceUrl=-Dokumenten sogar die volle
|
||||||
|
URL (D23). Zeichen, die auf gängigen Dateisystemen verboten sind, werden zu
|
||||||
|
`-`; die Endung `.werkbaum` kommt dazu, wenn nicht schon `.werkbaum` oder
|
||||||
|
`.txt` dasteht (D24). Ein leerer Rest fällt auf `plan` zurück. */
|
||||||
|
export function saveFileName(docName){
|
||||||
|
let base = String(docName == null ? '' : docName)
|
||||||
|
.replace(/[\u0000-\u001f/\\:*?"<>|]+/g, '-') /* verbotene Zeichen, auch Pfadtrenner */
|
||||||
|
.replace(/-{2,}/g, '-')
|
||||||
|
.replace(/^[-. ]+|[-. ]+$/g, '') /* Ränder: führende Punkte wären versteckte Dateien */
|
||||||
|
.trim();
|
||||||
|
if(!base) base = 'plan';
|
||||||
|
if(/\.(werkbaum|txt)$/i.test(base)) return base;
|
||||||
|
return base + '.werkbaum';
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { saveFileName, FILE_ACCEPT } from '../src/localfile.js';
|
||||||
|
|
||||||
|
/* Öffnen/Speichern lokaler Dateien (D72): der Dateiname beim Speichern wird
|
||||||
|
aus dem Dokumentnamen abgeleitet — Endung .werkbaum (D24), verbotene
|
||||||
|
Zeichen bereinigt, URL-Namen (D23) bleiben lesbar. */
|
||||||
|
|
||||||
|
describe('saveFileName', () => {
|
||||||
|
it('gewöhnlicher Name bekommt die Endung .werkbaum', () => {
|
||||||
|
expect(saveFileName('Mein Plan')).toBe('Mein Plan.werkbaum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('vorhandene Endung .werkbaum wird nicht verdoppelt', () => {
|
||||||
|
expect(saveFileName('plan.werkbaum')).toBe('plan.werkbaum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.txt bleibt zulässig — die Endung ist Konvention, kein Vertrag (D24)', () => {
|
||||||
|
expect(saveFileName('notizen.txt')).toBe('notizen.txt');
|
||||||
|
expect(saveFileName('notizen.TXT')).toBe('notizen.TXT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('URL-Namen (D23) werden zu brauchbaren Dateinamen', () => {
|
||||||
|
expect(saveFileName('https://example.org/p/plan'))
|
||||||
|
.toBe('https-example.org-p-plan.werkbaum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verbotene Zeichen fallen zu einem einzigen Strich zusammen', () => {
|
||||||
|
expect(saveFileName('a<b>c:d*e?f"g|h\\i')).toBe('a-b-c-d-e-f-g-h-i.werkbaum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Leerzeichen und Klammern bleiben — sie sind in Dateinamen erlaubt', () => {
|
||||||
|
expect(saveFileName('Sprint 15 (Q3)')).toBe('Sprint 15 (Q3).werkbaum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('führende Punkte fallen weg — sonst entstünde eine versteckte Datei', () => {
|
||||||
|
expect(saveFileName('.geheim')).toBe('geheim.werkbaum');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leerer oder unbrauchbarer Name fällt auf plan.werkbaum zurück', () => {
|
||||||
|
expect(saveFileName('')).toBe('plan.werkbaum');
|
||||||
|
expect(saveFileName('...')).toBe('plan.werkbaum');
|
||||||
|
expect(saveFileName(null)).toBe('plan.werkbaum');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FILE_ACCEPT', () => {
|
||||||
|
it('nennt die Endung der Notation und lässt .txt zu (D24)', () => {
|
||||||
|
expect(FILE_ACCEPT).toContain('.werkbaum');
|
||||||
|
expect(FILE_ACCEPT).toContain('.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user