frontend: Diagramm als Text-Baum in die Zwischenablage kopieren

Neue Icon-Schaltfläche im Diagramm-Kopf exportiert die sichtbare Struktur
als einfachen, überall einsetzbaren ASCII-Baum: ├─/└─ für all-of,
├◇/└◇ für any-of-Alternativen, je Knoten [Status] Label (Größe) @tags.
Respektiert den verworfene-Filter. Clipboard-Logik in writeClipboard/
flashCopied ausgelagert und von beiden Kopieren-Buttons genutzt.
SPEC §9 um den Textexport ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-21 12:16:09 +02:00
co-authored by Claude Opus 4.8
parent 1479bd9717
commit 6e33591b84
2 changed files with 83 additions and 17 deletions
+11
View File
@@ -134,6 +134,17 @@ ist grau (`#6B7A8C`) — kein Petrol mehr im Diagramm. Der Modus ändert nur die
+ halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte
des Teilbaums.
### Textexport des Diagramms
Das Diagramm ist per Icon-Schaltfläche als einfacher, überall einsetzbarer
ASCII-Baum in die Zwischenablage kopierbar. Format:
- Eingerückter Baum mit `├─`/`└─` (all-of-Abzweige) bzw. `├◇`/`└◇`
(any-of-Alternativen); Fortsetzungsspalten mit `│`.
- Je Knoten: `[Status] Label (Größe) @tags` — jeder Teil optional, Reihenfolge
wie hier. Der Status-Code entspricht §4 (`[ ]`, `[x]`, `[~]`, …).
- Es wird genau die **sichtbare** Struktur exportiert (der „verworfene
einblenden“-Filter wirkt auch hier). Mehrere Wurzeln durch Leerzeile getrennt.
## 10. Beispiel (kanonisch)
```
+72 -17
View File
@@ -530,6 +530,10 @@
<span><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="9.5" width="5" height="5" rx="1"/><rect x="16" y="3" width="6" height="4" rx="1"/><rect x="16" y="17" width="6" height="4" rx="1"/><path d="M7.5 12H12M12 5V19M12 5H16M12 19H16"/></svg></span>
</label>
</div>
<button type="button" class="copybtn" id="copyDiagram" data-i18n-title="copyDiagramTitle" data-i18n-aria="copyDiagramTitle" title="Diagramm als Textbaum kopieren" aria-label="Diagramm als Textbaum kopieren">
<svg class="ic-copy" 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="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg class="ic-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
</button>
<div class="winbtns" aria-label="Diagramm">
<button type="button" class="winbtn" data-state="a" data-i18n-title="minimize" title="minimieren"></button>
<button type="button" class="winbtn" data-state="normal" data-i18n-title="normal" title="normal"></button>
@@ -577,6 +581,9 @@ const STATUS_BY_CODE = {
'-': {key:'verworfen', name:'verworfen'}
};
/* Rück-Abbildung Status-Key → Code (für den Text-Export des Diagramms) */
const CODE_BY_KEY = {idee:'?', geplant:' ', arbeit:'~', durchstich:'/', fertig:'x', prod:'^', verworfen:'-'};
const src = document.getElementById('src');
const out = document.getElementById('out');
const warnBox = document.getElementById('warn');
@@ -696,6 +703,43 @@ function render(){
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${w}</div>`).join('');
}
/* ---------- Diagramm als einfacher Text-Baum ---------- */
/* Einfachstes, aber überall einsetzbares Format: eingerückter ASCII-Baum.
all-of-Abzweige mit ├─/└─, any-of-Alternativen mit ├◇/└◇. Je Knoten:
[Status] Label (Größe) @tags. Respektiert den „verworfene“-Filter, gibt
also genau die sichtbare Struktur wieder. */
function nodeLabelText(n){
let s = n.status ? '[' + (CODE_BY_KEY[n.status.key] ?? ' ') + '] ' : '';
s += n.label;
if(n.size) s += ' (' + n.size + ')';
if(n.tags && n.tags.length) s += ' ' + n.tags.map(x => '@' + x).join(' ');
return s;
}
function walkText(node, prefix, lines){
const kids = visibleChildren(node);
const gate = gateOf(kids);
kids.forEach((k, i) => {
const last = i === kids.length - 1;
const conn = gate === 'or'
? (last ? '└◇ ' : '├◇ ')
: (last ? '└─ ' : '├─ ');
lines.push(prefix + conn + nodeLabelText(k));
walkText(k, prefix + (last ? ' ' : '│ '), lines);
});
}
function treeToText(){
let {roots} = parse(src.value);
if(!showc.checked){
roots = roots.filter(r => !r.status || r.status.key !== 'verworfen');
}
const blocks = roots.map(root => {
const lines = [nodeLabelText(root)];
walkText(root, '', lines);
return lines.join('\n');
});
return blocks.join('\n\n'); /* mehrere Wurzeln durch Leerzeile trennen */
}
/* Tab-Taste rückt ein statt den Fokus zu wechseln */
src.addEventListener('keydown', e => {
if(e.key === 'Tab'){
@@ -834,25 +878,32 @@ diagramEl.addEventListener('wheel', e => {
const showc = document.getElementById('showc');
showc.addEventListener('change', render);
/* ---------- Text in die Zwischenablage kopieren ---------- */
const copyBtn = document.getElementById('copy');
copyBtn.addEventListener('click', async () => {
try{
await navigator.clipboard.writeText(src.value);
}catch(_){
src.select();
document.execCommand('copy');
src.setSelectionRange(0, 0);
src.blur();
}
copyBtn.classList.add('done');
copyBtn.title = t('copyDone');
copyBtn.setAttribute('aria-label', t('copyDone'));
/* ---------- In die Zwischenablage kopieren ---------- */
async function writeClipboard(text){
try{ await navigator.clipboard.writeText(text); return; }catch(_){}
const ta = document.createElement('textarea'); /* Fallback ohne Clipboard-API */
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try{ document.execCommand('copy'); }catch(_){}
document.body.removeChild(ta);
}
function flashCopied(btn, restoreTitleKey){
btn.classList.add('done');
btn.title = t('copyDone');
btn.setAttribute('aria-label', t('copyDone'));
setTimeout(() => {
copyBtn.classList.remove('done');
copyBtn.title = t('copyTitle');
copyBtn.setAttribute('aria-label', t('copyTitle'));
btn.classList.remove('done');
btn.title = t(restoreTitleKey);
btn.setAttribute('aria-label', t(restoreTitleKey));
}, 1500);
}
document.getElementById('copy').addEventListener('click', async () => {
await writeClipboard(src.value);
flashCopied(document.getElementById('copy'), 'copyTitle');
});
document.getElementById('copyDiagram').addEventListener('click', async () => {
await writeClipboard(treeToText());
flashCopied(document.getElementById('copyDiagram'), 'copyDiagramTitle');
});
/* ---------- Internationalisierung (DE/EN/ES/FR) ---------- */
@@ -861,6 +912,7 @@ const I18N = {
subtitle:"Werkbaum Ein Text-basierter WBS/Projektstrukturplan/Feature-Tree Diagramm-Editor.",
editorTitle:"Struktur (Text)", diagramTitle:"Diagramm",
copy:"kopieren", copyDone:"kopiert ✓", copyTitle:"Text in die Zwischenablage kopieren",
copyDiagramTitle:"Diagramm als Textbaum kopieren",
minimize:"minimieren", normal:"normal", maximize:"maximieren",
agenda:"Agenda", discarded:"verworfene",
modeHorizontal:"Horizontal Organigramm, Diagramm über dem Editor",
@@ -885,6 +937,7 @@ const I18N = {
subtitle:"Werkbaum A text-based WBS / work breakdown structure / feature-tree diagram editor.",
editorTitle:"Structure (text)", diagramTitle:"Diagram",
copy:"copy", copyDone:"copied ✓", copyTitle:"Copy text to clipboard",
copyDiagramTitle:"Copy diagram as a text tree",
minimize:"minimize", normal:"normal", maximize:"maximize",
agenda:"Legend", discarded:"discarded",
modeHorizontal:"Horizontal org chart, diagram above the editor",
@@ -909,6 +962,7 @@ const I18N = {
subtitle:"Werkbaum Un editor de diagramas basado en texto para EDT / estructura de desglose del trabajo / árbol de características.",
editorTitle:"Estructura (texto)", diagramTitle:"Diagrama",
copy:"copiar", copyDone:"copiado ✓", copyTitle:"Copiar el texto al portapapeles",
copyDiagramTitle:"Copiar el diagrama como árbol de texto",
minimize:"minimizar", normal:"normal", maximize:"maximizar",
agenda:"Leyenda", discarded:"descartados",
modeHorizontal:"Horizontal organigrama, diagrama sobre el editor",
@@ -933,6 +987,7 @@ const I18N = {
subtitle:"Werkbaum Un éditeur de diagrammes basé sur le texte pour WBS / organigramme des tâches / arbre de fonctionnalités.",
editorTitle:"Structure (texte)", diagramTitle:"Diagramme",
copy:"copier", copyDone:"copié ✓", copyTitle:"Copier le texte dans le presse-papiers",
copyDiagramTitle:"Copier le diagramme en arbre de texte",
minimize:"réduire", normal:"normal", maximize:"agrandir",
agenda:"Légende", discarded:"abandonnés",
modeHorizontal:"Horizontal organigramme, diagramme au-dessus de l'éditeur",