feat: Speichern schreibt in die geöffnete Datei zurück (D72-Nachtrag, Stufe 2)
File System Access API, wo vorhanden (Chromium): Öffnen merkt sich das FileSystemFileHandle (IndexedDB, überlebt den Neustart), Speichern schreibt in dieselbe Datei; dieselbe Datei öffnet per isSameEntry wieder in dasselbe Dokument. Firefox/Safari behalten den Stufe-1-Download. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
defeaf84d0
commit
2648d72e45
@@ -19,6 +19,7 @@ reverse.
|
|||||||
|
|
||||||
## 2026-08-25
|
## 2026-08-25
|
||||||
|
|
||||||
|
- In Chromium browsers, saving writes back to the opened file, and the same file reopens into the same document
|
||||||
- Open a local `.werkbaum` file and save the document back as a file, from the document menu
|
- 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
|
||||||
|
|||||||
@@ -5339,3 +5339,53 @@ Input ist geleert (dieselbe Datei bleibt erneut wählbar); Speichern liefert
|
|||||||
`tests/localfile.test.js`. Werkzeuggrenze wie in D25/D53: Der echte
|
`tests/localfile.test.js`. Werkzeuggrenze wie in D25/D53: Der echte
|
||||||
Dateidialog und der echte Download lassen sich nicht automatisiert auslösen —
|
Dateidialog und der echte Download lassen sich nicht automatisiert auslösen —
|
||||||
geprüft ist alles bis an diese Kante.
|
geprüft ist alles bis an diese Kante.
|
||||||
|
|
||||||
|
**Nachtrag — Stufe 2: die File System Access API macht aus „Speichern unter"
|
||||||
|
ein „Speichern".** Wo die Picker existieren (`showOpenFilePicker` als
|
||||||
|
Feature-Detection — Chromium; Firefox und Safari haben sie bewusst nicht),
|
||||||
|
ändert sich hinter denselben zwei Menü-Einträgen das Verhalten:
|
||||||
|
|
||||||
|
- **Öffnen** liefert ein `FileSystemFileHandle`. Damit gibt es die
|
||||||
|
Datei-Identität, die Stufe 1 nicht hatte: `isSameEntry` prüft gegen die
|
||||||
|
gemerkten Handles, **dieselbe Datei öffnet wieder in dasselbe Dokument**
|
||||||
|
(aktualisiert den Text), eine andere Datei gleichen Namens bleibt ein
|
||||||
|
eigenes. `adoptFile()` trägt beide Wege — der Stufe-1-Input ruft es ohne
|
||||||
|
Handle, dann entsteht immer ein neues Dokument wie bisher.
|
||||||
|
- **Speichern** schreibt mit gemerktem Handle **in dieselbe Datei zurück**
|
||||||
|
(`createWritable`), ohne Dialog. Ohne Handle fragt `showSaveFilePicker`
|
||||||
|
(mit `saveFileName()` als Vorschlag) und merkt sich das Ergebnis — der
|
||||||
|
Komfort greift ab dem zweiten Speichern. Ein **Abbruch** des Dialogs tut
|
||||||
|
nichts — bewusst auch kein Download hinterher: Wer abbricht, will nicht
|
||||||
|
woandershin speichern. Der Menü-Eintrag trägt den Dateinamen des Handles
|
||||||
|
als Tooltip (Dateinamen sind Daten, kein i18n).
|
||||||
|
- **Handles überleben den Neustart in IndexedDB** — localStorage kann sie
|
||||||
|
nicht halten (nicht JSON-serialisierbar), IndexedDB kann es (structured
|
||||||
|
clone). Beim Start werden sie zurückgeholt; verwaiste Einträge (Dokument
|
||||||
|
gelöscht) und Fremdes ohne `createWritable` räumen sich dabei weg. Nach dem
|
||||||
|
Neustart steht die Berechtigung auf `prompt` — der Browser fragt beim
|
||||||
|
ersten Speichern einmal nach (der Menü-Klick ist die nötige Nutzergeste);
|
||||||
|
verweigert er, entscheidet der Dialog neu. **Alles daran ist Komfort, keine
|
||||||
|
Pflicht**: Jeder IndexedDB-Fehler wird geschluckt, Speichern funktioniert
|
||||||
|
dann eben wieder über den Dialog.
|
||||||
|
- `deleteDoc()` nimmt das Handle mit (Map und IndexedDB) — wie die Stände
|
||||||
|
(D54): Mit dem Dokument geht, was an ihm hängt.
|
||||||
|
|
||||||
|
**Kein Strg+S** — erwogen und zurückgestellt: Der Browser-Default (Seite
|
||||||
|
speichern) müsste abgefangen werden, und ohne Handle öffnete die Geste
|
||||||
|
unvermittelt einen Dialog; wenn, dann als eigene Entscheidung mit
|
||||||
|
Legenden-Zeile.
|
||||||
|
|
||||||
|
**Nachgemessen** im Browser (echtes Chromium, `hasFsAccess === true`; die
|
||||||
|
Picker gestubbt — den nativen Dialog kann die Automatisierung nicht bedienen,
|
||||||
|
die Logik dahinter schon; Werkzeuggrenze wie D52): Öffnen legt das Dokument
|
||||||
|
mit Handle an, der Speichern-Eintrag trägt den Dateinamen als Tooltip,
|
||||||
|
Speichern schreibt in place (**0** Dialog-Aufrufe), dieselbe Datei erneut
|
||||||
|
geöffnet aktualisiert **dasselbe** Dokument (Anzahl unverändert, Text auf
|
||||||
|
Version 2, Diagramm folgt); ein Dokument ohne Handle bekommt beim ersten
|
||||||
|
Speichern den Dialog (`suggestedName` korrekt) und beim zweiten nicht mehr
|
||||||
|
(1 Aufruf, 2 Schreibvorgänge); Abbruch bzw. verweigerte Berechtigung
|
||||||
|
schreiben nichts und laden nichts herunter. **Nicht messbar** blieb die
|
||||||
|
IndexedDB-Rundreise über einen Neustart: Stub-Handles überleben den
|
||||||
|
Structured Clone nicht (`DataCloneError`, planmäßig geschluckt) — echte
|
||||||
|
Handles sind gerade dafür klonbar; dieser eine Pfad ist Code-Review statt
|
||||||
|
Messung. 480 Tests.
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
- [^] #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
|
||||||
- [x] #ed.files: Open and save .werkbaum files (S)
|
- [x] #ed.files: Open and save .werkbaum files (S)
|
||||||
|
- [x] #ed.files.inplace: Saving writes back into the opened file (S) %% File System Access, Chromium
|
||||||
+ [^] #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)
|
||||||
@@ -608,6 +609,11 @@
|
|||||||
format was the one thing that could not leave the browser as a file, while
|
format was the one thing that could not leave the browser as a file, while
|
||||||
the diagram always could.
|
the diagram always could.
|
||||||
|
|
||||||
|
#ed.files.inplace
|
||||||
|
Where the File System Access API exists (Chromium), opening keeps a file
|
||||||
|
handle: saving writes back into the same file, and the same file reopens
|
||||||
|
into the same document. Firefox and Safari keep the plain download.
|
||||||
|
|
||||||
#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
|
||||||
your last visit get a yellow halo. New means live, not "line added" — a line
|
your last visit get a yellow halo. New means live, not "line added" — a line
|
||||||
|
|||||||
+132
-8
@@ -7,7 +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';
|
import { FILE_ACCEPT, FILE_TYPES, 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. */
|
||||||
@@ -3499,7 +3499,14 @@ function restoreDoc(){
|
|||||||
persistDocs();
|
persistDocs();
|
||||||
closeDocMenu();
|
closeDocMenu();
|
||||||
}
|
}
|
||||||
function openDocMenu(){ renderDocMenu(); updateRestoreBtn(); docMenu.hidden = false; docTrigger.setAttribute('aria-expanded', 'true'); }
|
function openDocMenu(){
|
||||||
|
renderDocMenu(); updateRestoreBtn();
|
||||||
|
/* Wohin „Als Datei speichern" schreiben würde: der Dateiname des gemerkten
|
||||||
|
Handles als Tooltip (D72, Stufe 2) — Dateinamen sind Daten, kein i18n. */
|
||||||
|
const h = fileHandles.get(activeId);
|
||||||
|
document.getElementById('docSaveFile').title = h ? h.name : '';
|
||||||
|
docMenu.hidden = false; docTrigger.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
function closeDocMenu(){ renamingId = null; renameIsNew = false; docMenu.hidden = true; docTrigger.setAttribute('aria-expanded', 'false'); }
|
function closeDocMenu(){ renamingId = null; renameIsNew = false; docMenu.hidden = true; docTrigger.setAttribute('aria-expanded', 'false'); }
|
||||||
function toggleDocMenu(){ docMenu.hidden ? openDocMenu() : closeDocMenu(); }
|
function toggleDocMenu(){ docMenu.hidden ? openDocMenu() : closeDocMenu(); }
|
||||||
/* Beim Wechseln/Anlegen/Löschen zuerst den aktuellen Editortext ins aktive
|
/* Beim Wechseln/Anlegen/Löschen zuerst den aktuellen Editortext ins aktive
|
||||||
@@ -3846,6 +3853,7 @@ function deleteDoc(){
|
|||||||
if(!d) return;
|
if(!d) return;
|
||||||
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
|
if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return;
|
||||||
if(padSource && padSource.id === d.id) stopPad(); /* danach gibt es nichts mehr zu holen (D31) */
|
if(padSource && padSource.id === d.id) stopPad(); /* danach gibt es nichts mehr zu holen (D31) */
|
||||||
|
if(fileHandles.has(d.id)){ fileHandles.delete(d.id); idbDeleteHandle(d.id); } /* mit dem Dokument geht sein Datei-Handle (D72) */
|
||||||
docs = docs.filter(x => x.id !== d.id);
|
docs = docs.filter(x => x.id !== d.id);
|
||||||
if(snaps[d.id]){ delete snaps[d.id]; persistSnaps(snaps, localStorage); } /* mit dem Dokument gehen seine Stände (D54) */
|
if(snaps[d.id]){ delete snaps[d.id]; persistSnaps(snaps, localStorage); } /* 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 }];
|
||||||
@@ -3873,23 +3881,138 @@ fileOpenInput.addEventListener('change', async () => {
|
|||||||
if(!f) return;
|
if(!f) return;
|
||||||
let text;
|
let text;
|
||||||
try{ text = await f.text(); }catch(_){ return; }
|
try{ text = await f.text(); }catch(_){ return; }
|
||||||
|
adoptFile(null, f.name, text);
|
||||||
|
});
|
||||||
|
/* ---------- Stufe 2 (D72-Nachtrag): File System Access API ----------
|
||||||
|
Wo die Picker existieren (Chromium), liefert das Öffnen ein
|
||||||
|
FileSystemFileHandle: Speichern schreibt in DIESELBE Datei zurück, und
|
||||||
|
dieselbe Datei öffnet wieder in dasselbe Dokument (isSameEntry) — die
|
||||||
|
Datei-Identität, die Stufe 1 nicht hatte. Firefox/Safari behalten den
|
||||||
|
Stufe-1-Weg; die Bedienung ist in beiden Fällen dieselbe. */
|
||||||
|
const hasFsAccess = typeof window.showOpenFilePicker === 'function';
|
||||||
|
const fileHandles = new Map(); /* docId → FileSystemFileHandle */
|
||||||
|
/* Handles überleben den Neustart nur in IndexedDB (localStorage kann sie
|
||||||
|
nicht halten — sie sind nicht JSON-serialisierbar). Alles hier ist Komfort,
|
||||||
|
keine Pflicht: Scheitert IndexedDB, funktioniert Speichern weiter über den
|
||||||
|
Dialog — deshalb schlucken die Helfer ihre Fehler. */
|
||||||
|
const IDB_STORE = 'fileHandles';
|
||||||
|
function idbOpen(){
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
const rq = indexedDB.open('werkbaum', 1);
|
||||||
|
rq.onupgradeneeded = () => rq.result.createObjectStore(IDB_STORE);
|
||||||
|
rq.onsuccess = () => res(rq.result);
|
||||||
|
rq.onerror = () => rej(rq.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function idbHandleOp(mode, fn){
|
||||||
|
try{
|
||||||
|
const db = await idbOpen();
|
||||||
|
const out = await new Promise((res, rej) => {
|
||||||
|
const tx = db.transaction(IDB_STORE, mode);
|
||||||
|
const r = fn(tx.objectStore(IDB_STORE));
|
||||||
|
tx.oncomplete = () => res(r);
|
||||||
|
tx.onerror = () => rej(tx.error);
|
||||||
|
});
|
||||||
|
db.close();
|
||||||
|
return out;
|
||||||
|
}catch(_){ return null; }
|
||||||
|
}
|
||||||
|
function idbPutHandle(docId, handle){ return idbHandleOp('readwrite', s => { s.put(handle, docId); }); }
|
||||||
|
function idbDeleteHandle(docId){ return idbHandleOp('readwrite', s => { s.delete(docId); }); }
|
||||||
|
/* Beim Start die gemerkten Handles zurückholen — nur für Dokumente, die es
|
||||||
|
noch gibt (verwaiste Einträge räumen sich dabei weg), und nur, was sich wie
|
||||||
|
ein Handle verhält (defensiv gegen fremden Speicherinhalt). */
|
||||||
|
async function idbLoadHandles(){
|
||||||
|
const pair = await idbHandleOp('readonly', s => [s.getAllKeys(), s.getAll()]);
|
||||||
|
if(!pair) return;
|
||||||
|
const keys = pair[0].result || [], vals = pair[1].result || [];
|
||||||
|
keys.forEach((k, i) => {
|
||||||
|
const h = vals[i];
|
||||||
|
if(h && typeof h.createWritable === 'function' && docs.some(d => d.id === k)){
|
||||||
|
fileHandles.set(k, h);
|
||||||
|
} else {
|
||||||
|
idbDeleteHandle(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* Text übernehmen: dieselbe Datei (Handle-Identität) aktualisiert ihr
|
||||||
|
Dokument, sonst entsteht ein neues — dieselbe Zweiteilung wie adoptRemote(). */
|
||||||
|
async function adoptFile(handle, name, text){
|
||||||
flushActive();
|
flushActive();
|
||||||
const d = { id: uid(), name: uniqueName(f.name), text };
|
let targetId = null;
|
||||||
|
if(handle){
|
||||||
|
for(const [id, h] of fileHandles){
|
||||||
|
try{ if(await handle.isSameEntry(h) && docs.some(d => d.id === id)){ targetId = id; break; } }
|
||||||
|
catch(_){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(targetId){
|
||||||
|
docs.find(d => d.id === targetId).text = text;
|
||||||
|
activeId = targetId;
|
||||||
|
} else {
|
||||||
|
const d = { id: uid(), name: uniqueName(name), text };
|
||||||
docs.push(d);
|
docs.push(d);
|
||||||
activeId = d.id;
|
activeId = d.id;
|
||||||
|
}
|
||||||
|
if(handle){ fileHandles.set(activeId, handle); idbPutHandle(activeId, handle); }
|
||||||
foldOverrides.clear();
|
foldOverrides.clear();
|
||||||
loadActiveIntoEditor();
|
loadActiveIntoEditor();
|
||||||
persistDocs();
|
persistDocs();
|
||||||
closeDocMenu();
|
closeDocMenu();
|
||||||
});
|
}
|
||||||
function openLocalFile(){ fileOpenInput.click(); }
|
async function openWithPicker(){
|
||||||
/* Speichern über den vorhandenen Blob-Download (saveBlob, Grafikexport):
|
let handle;
|
||||||
UTF-8, LF — das Textfeld normalisiert Zeilenenden ohnehin auf \n (D24). */
|
try{ [handle] = await window.showOpenFilePicker({types: FILE_TYPES}); }
|
||||||
function saveLocalFile(){
|
catch(_){ return; } /* Abbruch des Dialogs */
|
||||||
|
let text;
|
||||||
|
try{ text = await (await handle.getFile()).text(); }catch(_){ return; }
|
||||||
|
await adoptFile(handle, handle.name, text);
|
||||||
|
}
|
||||||
|
function openLocalFile(){
|
||||||
|
if(hasFsAccess) openWithPicker();
|
||||||
|
else fileOpenInput.click();
|
||||||
|
}
|
||||||
|
async function writeToHandle(handle, text){
|
||||||
|
const w = await handle.createWritable();
|
||||||
|
await w.write(text);
|
||||||
|
await w.close();
|
||||||
|
}
|
||||||
|
/* In das gemerkte Handle zurückschreiben. Nach einem Neustart steht die
|
||||||
|
Berechtigung auf 'prompt' — requestPermission fragt einmal nach (der
|
||||||
|
Menü-Klick ist die nötige Nutzergeste). false heißt: kein Handle oder
|
||||||
|
nicht (mehr) beschreibbar — dann entscheidet der Dialog neu. */
|
||||||
|
async function saveToKnownFile(d){
|
||||||
|
const h = fileHandles.get(d.id);
|
||||||
|
if(!h) return false;
|
||||||
|
try{
|
||||||
|
let perm = await h.queryPermission({mode: 'readwrite'});
|
||||||
|
if(perm === 'prompt') perm = await h.requestPermission({mode: 'readwrite'});
|
||||||
|
if(perm !== 'granted') return false;
|
||||||
|
await writeToHandle(h, d.text);
|
||||||
|
return true;
|
||||||
|
}catch(_){ return false; }
|
||||||
|
}
|
||||||
|
async function saveWithPicker(d){
|
||||||
|
let handle;
|
||||||
|
try{
|
||||||
|
handle = await window.showSaveFilePicker({suggestedName: saveFileName(d.name), types: FILE_TYPES});
|
||||||
|
}catch(_){ return; } /* Abbruch: bewusst kein Download hinterher */
|
||||||
|
try{ await writeToHandle(handle, d.text); }catch(_){ return; }
|
||||||
|
fileHandles.set(d.id, handle);
|
||||||
|
idbPutHandle(d.id, handle);
|
||||||
|
}
|
||||||
|
/* Speichern: mit Handle in dieselbe Datei, sonst Dialog (Stufe 2) bzw.
|
||||||
|
Blob-Download (Stufe 1). UTF-8, LF — das Textfeld normalisiert Zeilenenden
|
||||||
|
ohnehin auf \n (D24). */
|
||||||
|
async function saveLocalFile(){
|
||||||
flushActive();
|
flushActive();
|
||||||
const d = activeDoc();
|
const d = activeDoc();
|
||||||
if(!d) return;
|
if(!d) return;
|
||||||
|
if(hasFsAccess){
|
||||||
|
if(!(await saveToKnownFile(d))) await saveWithPicker(d);
|
||||||
|
} else {
|
||||||
saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name));
|
saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name));
|
||||||
|
}
|
||||||
closeDocMenu();
|
closeDocMenu();
|
||||||
}
|
}
|
||||||
/* Dokumente laden + aktiven Text in den Editor holen (nach applyLang). */
|
/* Dokumente laden + aktiven Text in den Editor holen (nach applyLang). */
|
||||||
@@ -4444,6 +4567,7 @@ let startLang = 'de';
|
|||||||
try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_){ startLang = detectLang(); }
|
try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_){ startLang = detectLang(); }
|
||||||
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
|
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
|
||||||
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
|
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
|
||||||
|
if(hasFsAccess) idbLoadHandles(); /* gemerkte Datei-Handles zurückholen (D72, Stufe 2) */
|
||||||
applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */
|
applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */
|
||||||
loadRemoteSource(); /* ?sourceUrl= / ?etherpad= nachladen (asynchron, D23/D31) */
|
loadRemoteSource(); /* ?sourceUrl= / ?etherpad= nachladen (asynchron, D23/D31) */
|
||||||
|
|
||||||
|
|||||||
@@ -22,3 +22,9 @@ export function saveFileName(docName){
|
|||||||
if(/\.(werkbaum|txt)$/i.test(base)) return base;
|
if(/\.(werkbaum|txt)$/i.test(base)) return base;
|
||||||
return base + '.werkbaum';
|
return base + '.werkbaum';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dialog-Typen der File System Access API (Stufe 2): derselbe Filter wie
|
||||||
|
FILE_ACCEPT, in der Form von showOpenFilePicker/showSaveFilePicker. */
|
||||||
|
export const FILE_TYPES = [
|
||||||
|
{description: 'Werkbaum', accept: {'text/plain': ['.werkbaum', '.txt']}},
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { saveFileName, FILE_ACCEPT } from '../src/localfile.js';
|
import { saveFileName, FILE_ACCEPT, FILE_TYPES } from '../src/localfile.js';
|
||||||
|
|
||||||
/* Öffnen/Speichern lokaler Dateien (D72): der Dateiname beim Speichern wird
|
/* Öffnen/Speichern lokaler Dateien (D72): der Dateiname beim Speichern wird
|
||||||
aus dem Dokumentnamen abgeleitet — Endung .werkbaum (D24), verbotene
|
aus dem Dokumentnamen abgeleitet — Endung .werkbaum (D24), verbotene
|
||||||
@@ -49,3 +49,11 @@ describe('FILE_ACCEPT', () => {
|
|||||||
expect(FILE_ACCEPT).toContain('.txt');
|
expect(FILE_ACCEPT).toContain('.txt');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('FILE_TYPES (Stufe 2, Picker-Filter)', () => {
|
||||||
|
it('führt dieselben Endungen wie FILE_ACCEPT', () => {
|
||||||
|
const exts = FILE_TYPES[0].accept['text/plain'];
|
||||||
|
expect(exts).toContain('.werkbaum');
|
||||||
|
expect(exts).toContain('.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user