Wandtafel-Dashboard, Dateibereinigung und Auslastungs-Vereinfachung
Neue Wandtafel (Touch-Dashboard für Kiosk-Anzeige): Auslastung/Besprochen je Gewerk, Aufgaben-Kennzahlen, Ereignis-Feed nach Art gruppiert, vollständige Projektliste zum Scrollen, automatischer Rücksprung nach Untätigkeit mit sichtbarem Countdown. Eigene Kiosk-Rolle ohne Fachrolle und ohne Schreibrechte. Auslastung: Möglichkeit zum Wechsel der ausgewerteten Sitzung entfernt. Aufräumen nicht mehr benötigter Dateien, README auf aktuellen Stand gebracht. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bc30f2900b
commit
db63a29365
47 files changed
+16486
-3768
No files matched your search
+223
-32
@@ -246,7 +246,7 @@ function getWeekNumber(d){
|
||||
// Fett, Farben, Absätze) wird sowohl im Projektboard (js/board/board.js,
|
||||
// Detailpanel) als auch auf der Projektplan-Seite (js/plan/plan-info.js)
|
||||
// verwendet — deshalb liegen die reinen Hilfsfunktionen hier im gemeinsamen
|
||||
// Modul. Nur statusBlur() (board.js, ruft editProj() auf) bzw. der
|
||||
// Modul. Nur der
|
||||
// Plan-Gegenpart (planStatusBlur() in plan-info.js) bleiben modul-eigen, da
|
||||
// sie unterschiedliche Speicherpfade ansteuern.
|
||||
|
||||
@@ -289,10 +289,27 @@ function formatStatus(command, value){
|
||||
// eingeschleust werden können; im style-Attribut sind nur color/font-weight
|
||||
// erlaubt. Parameter: `html` (String). Rückgabe: bereinigter HTML-String ('' bei
|
||||
// leerem Eingabewert).
|
||||
//
|
||||
// DOMParser statt `div.innerHTML = html` — das ist der GANZE Sicherheitsgewinn
|
||||
// dieser Funktion, nicht bloß Geschmackssache: innerHTML lädt eingebettete
|
||||
// Ressourcen und führt deren Fehler-Handler aus, AUCH wenn das Element gar
|
||||
// nicht im Dokument hängt. Ein `<img src=x onerror=…>` lief damit schon
|
||||
// WÄHREND des Bereinigens — also auf jedem Anzeigepfad, der hier
|
||||
// durchkommt, obwohl das Bild danach korrekt entfernt wurde. Der Server
|
||||
// speichert Kommentare und Protokolltexte unverändert, ein angemeldetes
|
||||
// Konto konnte solches HTML also über die API hinterlegen und damit Code in
|
||||
// der Sitzung aller anderen ausführen. parseFromString() erzeugt dagegen ein
|
||||
// inertes Dokument: kein Laden, kein Handler. Nachgemessen — mit innerHTML
|
||||
// feuerte der Handler, mit DOMParser nicht, bei identischem Ergebnisbaum.
|
||||
function sanitizeStatusHtml(html){
|
||||
if(!html) return '';
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
// Die Bereinigung läuft vollständig IM inerten Dokument ab: Würden die
|
||||
// Knoten vorher in ein Element des Hauptdokuments übernommen, zöge
|
||||
// appendChild() ein adoptNode() nach sich — und ein dabei übernommenes
|
||||
// <img src=…> begänne wieder zu laden, also genau das, was hier
|
||||
// verhindert werden soll.
|
||||
const inert = new DOMParser().parseFromString(String(html), 'text/html');
|
||||
const div = inert.body;
|
||||
const allowedTags = new Set(['B','STRONG','I','EM','U','P','BR','DIV','SPAN','FONT']);
|
||||
const allowedAttrs = new Set(['style']);
|
||||
// Elemente, deren INHALT mit verschwindet, statt ausgepackt zu werden:
|
||||
@@ -345,26 +362,27 @@ function sanitizeStatusHtml(html){
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// statusHtmlToEditText: wandelt einen gespeicherten Kommentar (HTML) in den
|
||||
// Klartext, der im Textfeld des Kommentar-Editors steht. Fettschrift wird zu
|
||||
// **Sternchen**, Absätze und <br> werden zu Zeilenumbrüchen, alles andere
|
||||
// fällt weg. Gegenstück zu statusEditTextToHtml(). Nötig, weil der Editor im
|
||||
// Board seit der Kommentar-Überarbeitung ein normales <textarea> ist und kein
|
||||
// contenteditable mehr (document.execCommand ist abgekündigt) — gespeichert
|
||||
// wird aber weiterhin HTML, damit alle vorhandenen Anzeigepfade (Board-
|
||||
// Vorschau, Wochenbericht, PDF-Export, Show-Mode, Verlauf) unverändert
|
||||
// funktionieren und Altbestände lesbar bleiben.
|
||||
// statusTextMitZeilen: wandelt Kommentar-HTML in reinen Text und erhält dabei
|
||||
// die Zeilenumbrüche (<br> und Blockgrenzen werden zu \n). Gegenstück zu
|
||||
// statusPlainText() weiter unten, das Umbrüche bewusst zu Leerzeichen
|
||||
// einebnet — hier werden sie gebraucht, weil der einzige Aufrufer die ERSTE
|
||||
// ZEILE will: openTodoAusKommentar() (js/wochenbericht/wochenbericht.js)
|
||||
// schlägt daraus den Titel einer neuen Aufgabe vor.
|
||||
//
|
||||
// Hieß bis zur Umstellung auf contenteditable statusHtmlToEditText() und gab
|
||||
// Fett als **Sternchen**, Kursiv als *Sternchen* und Farbe als
|
||||
// [[c:…]]…[[/c]] zurück — die Rückrichtung zum damaligen Markup-Editor.
|
||||
// Seit der Editor echtes HTML führt (richtextWickeln() unten), hat dieses
|
||||
// Markup keinen Leser mehr; übrig blieben Sternchen, die unvermittelt im
|
||||
// vorgeschlagenen Aufgabentitel standen, obwohl sie nie jemand getippt hat.
|
||||
// Deshalb liefert die Funktion jetzt schlichten Text.
|
||||
// Parameter: html (String). Rückgabe: String.
|
||||
function statusHtmlToEditText(html){
|
||||
function statusTextMitZeilen(html){
|
||||
if(!html) return '';
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = sanitizeStatusHtml(html);
|
||||
// Fett-Auszeichnungen vor dem Auflösen in Sternchen überführen
|
||||
div.querySelectorAll('b, strong').forEach(el => {
|
||||
const inner = (el.textContent || '').trim();
|
||||
el.replaceWith(document.createTextNode(inner ? '**' + inner + '**' : ''));
|
||||
});
|
||||
// Block-Grenzen zu Zeilenumbrüchen
|
||||
// Block-Grenzen zu Zeilenumbrüchen; alle Auszeichnungen fallen durch
|
||||
// textContent ohnehin weg.
|
||||
div.querySelectorAll('br').forEach(el => el.replaceWith(document.createTextNode('\n')));
|
||||
div.querySelectorAll('p, div').forEach(el => el.append(document.createTextNode('\n')));
|
||||
return (div.textContent || '')
|
||||
@@ -373,19 +391,192 @@ function statusHtmlToEditText(html){
|
||||
.trim();
|
||||
}
|
||||
|
||||
// statusEditTextToHtml: wandelt den Klartext aus dem Kommentar-Editor in das
|
||||
// gespeicherte HTML. Escapt zuerst vollständig (esc()), sodass eingetippte
|
||||
// spitze Klammern niemals als Markup ankommen, und setzt danach die einzige
|
||||
// erlaubte Auszeichnung **fett** sowie Zeilenumbrüche. Gegenstück zu
|
||||
// statusHtmlToEditText(). Leerer oder reiner Leerraum-Text ergibt '' — die
|
||||
// bisherige Bedeutung von "kein Kommentar" bleibt damit erhalten.
|
||||
// Parameter: text (String). Rückgabe: String (HTML).
|
||||
function statusEditTextToHtml(text){
|
||||
const clean = String(text || '').trim();
|
||||
if(clean === '') return '';
|
||||
return esc(clean)
|
||||
.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
||||
.replace(/\n/g, '<br>');
|
||||
// ---------- Formatierbare Textfelder: contenteditable statt Markup-Zeichen ----------
|
||||
//
|
||||
// Erster Anlauf (Markup-Zeichen **fett**/*kursiv*/[[c:…]] in einem normalen
|
||||
// <textarea>, inzwischen vollständig entfernt) zeigte während des
|
||||
// Tippens sichtbare Stern-/Klammer-Zeichen (**fett**, [[c:…]]) statt echter
|
||||
// Formatierung — Eriks Rückmeldung: "statt den zwei Sternchen... den Text
|
||||
// richtig fett machen". Diese Felder sind jetzt contenteditable statt
|
||||
// <textarea>, und richtextWickeln()/richtextFett()/richtextKursiv()/
|
||||
// richtextFarbe() wenden die Auszeichnung direkt auf die Markierung an — über
|
||||
// die Selection/Range-API (range.extractContents()/insertNode()), bewusst
|
||||
// KEIN document.execCommand (abgekündigt, derselbe Grund, der die vorige
|
||||
// contenteditable-Fassung dieser Felder schon einmal verworfen hatte — nur
|
||||
// diesmal ohne execCommand). Der Nutzer sieht dadurch sofort echten fetten/
|
||||
// kursiven/farbigen Text, keine Markup-Zeichen mehr.
|
||||
// richtextToolbarHtml() liefert die dazugehörige, überall wiederverwendbare
|
||||
// Werkzeugleiste; sie erwartet, dass das Zielfeld im selben .richtext-wrap
|
||||
// steht (per closest()+querySelector() gefunden), damit mehrere Instanzen
|
||||
// auf einer Seite (z. B. je Protokoll-Punkt) einander nicht in die Quere
|
||||
// kommen. onmousedown mit preventDefault() verhindert, dass der Knopf-Klick
|
||||
// dem Feld vorher den Fokus (und damit die Markierung) stiehlt.
|
||||
|
||||
function richtextWickeln(feld, tagName, styleAttr){
|
||||
if(!feld) return;
|
||||
const sel = window.getSelection();
|
||||
if(!sel || sel.rangeCount === 0) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
if(range.collapsed || !feld.contains(range.commonAncestorContainer)) return;
|
||||
const wrapper = document.createElement(tagName);
|
||||
if(styleAttr) wrapper.setAttribute('style', styleAttr);
|
||||
wrapper.appendChild(range.extractContents());
|
||||
range.insertNode(wrapper);
|
||||
// Markierung auf den neu eingefügten Inhalt setzen — ein zweiter Klick auf
|
||||
// eine andere Auszeichnung wirkt dadurch sofort auf denselben Text weiter.
|
||||
sel.removeAllRanges();
|
||||
const neueRange = document.createRange();
|
||||
neueRange.selectNodeContents(wrapper);
|
||||
sel.addRange(neueRange);
|
||||
feld.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
function richtextFett(feld){ richtextWickeln(feld, 'b'); }
|
||||
function richtextKursiv(feld){ richtextWickeln(feld, 'i'); }
|
||||
function richtextFarbe(feld, farbe){ richtextWickeln(feld, 'span', 'color:' + farbe); }
|
||||
|
||||
// richtextFeldHtml: liefert ein contenteditable-Feld mit Platzhaltertext
|
||||
// (:empty:before + data-placeholder, siehe styles.css) als Ersatz für ein
|
||||
// <textarea>. html MUSS bereits sanitizeStatusHtml()-bereinigt sein (niemals
|
||||
// escapter Klartext — der Inhalt soll als Markup interpretiert werden, nicht
|
||||
// als Zeichenkette). extraAttrs hängt aufruferspezifische Event-Handler an.
|
||||
// editable=false (gesperrte Sitzung) setzt contenteditable="false" statt das
|
||||
// Feld ganz durch reinen Text zu ersetzen — Auswahl/Kopieren bleibt möglich.
|
||||
// Parameter: cssClass, html, placeholder, extraAttrs (Strings), editable
|
||||
// (Boolean, optional, Default true). Rückgabe: HTML-String.
|
||||
function richtextFeldHtml(cssClass, html, placeholder, extraAttrs, editable){
|
||||
const bearbeitbar = editable === false ? 'false' : 'true';
|
||||
return `<div class="${cssClass}" contenteditable="${bearbeitbar}" data-placeholder="${esc(placeholder || '')}" ${extraAttrs || ''}>${html || ''}</div>`;
|
||||
}
|
||||
|
||||
// richtextCursorAnsEnde: setzt den Fokus in ein contenteditable-Feld und den
|
||||
// Cursor ans Textende — das Gegenstück zu textarea.setSelectionRange(), das
|
||||
// es für contenteditable nicht gibt. Parameter: feld (Element). Keine
|
||||
// Rückgabe.
|
||||
function richtextCursorAnsEnde(feld){
|
||||
if(!feld) return;
|
||||
feld.focus();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(feld);
|
||||
range.collapse(false);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
// Einfügen bereinigen: Ohne diesen Lauscher übernimmt ein contenteditable-Feld
|
||||
// beim Einfügen aus Word, einer Webseite oder einer Mail deren vollständige
|
||||
// Formatierung — Schriftarten, Größen, Tabellen, Bilder. Sichtbar blieb das
|
||||
// bis zum Speichern; erst dort lief sanitizeStatusHtml() und der Text sprang
|
||||
// unvermittelt in eine andere Gestalt. Jetzt wird schon beim Einfügen auf
|
||||
// dieselbe Positivliste reduziert, die auch gespeichert wird — was man sieht,
|
||||
// ist, was bleibt.
|
||||
//
|
||||
// EIN Lauscher am Dokument für alle Felder (gleiche Begründung wie beim
|
||||
// selectionchange-Lauscher in js/plan/plan-protokoll.js): Die Felder werden
|
||||
// beim Rendern ständig neu erzeugt, ein Lauscher je Feld müsste bei jedem
|
||||
// Neuaufbau erneut gesetzt werden.
|
||||
document.addEventListener('paste', function(e){
|
||||
const feld = e.target && e.target.closest ? e.target.closest('[contenteditable="true"]') : null;
|
||||
if(!feld || !e.clipboardData) return;
|
||||
e.preventDefault();
|
||||
// text/html nur, wenn vorhanden — reiner Text wird escaped eingesetzt,
|
||||
// damit darin enthaltene spitze Klammern Zeichen bleiben und nicht als
|
||||
// Markup ankommen.
|
||||
const html = e.clipboardData.getData('text/html');
|
||||
const sauber = html
|
||||
? sanitizeStatusHtml(html)
|
||||
: esc(e.clipboardData.getData('text/plain')).replace(/\r\n|\r|\n/g, '<br>');
|
||||
if(!sauber) return;
|
||||
const sel = window.getSelection();
|
||||
if(!sel || sel.rangeCount === 0) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
// Über ein <template> in Knoten verwandeln: dessen Inhalt liegt in einem
|
||||
// eigenen, inerten Dokumentfragment. `sauber` ist zwar bereits geprüft,
|
||||
// aber so bleibt auch dieser Schritt frei von Ressourcen-Ladevorgängen.
|
||||
const vorlage = document.createElement('template');
|
||||
vorlage.innerHTML = sauber;
|
||||
const fragment = vorlage.content;
|
||||
const letzter = fragment.lastChild;
|
||||
range.insertNode(fragment);
|
||||
// Schreibmarke hinter das Eingefügte setzen, sonst stünde sie davor.
|
||||
if(letzter){
|
||||
const nach = document.createRange();
|
||||
nach.setStartAfter(letzter);
|
||||
nach.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(nach);
|
||||
}
|
||||
feld.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
});
|
||||
|
||||
// Rückmeldung der Formatierungsleiste: Fett/Kursiv/Farbe wirken nur auf
|
||||
// MARKIERTEN Text — ohne Markierung passiert beim Klick nichts, und zwar ohne
|
||||
// jeden Hinweis warum. Die Leiste hebt sich deshalb sichtbar hervor, sobald im
|
||||
// zugehörigen Feld etwas markiert ist, und trägt daneben einen Hinweis, der
|
||||
// von "Erst Text markieren" auf die Bestätigung umspringt.
|
||||
//
|
||||
// EIN dokumentweiter Lauscher für JEDES Vorkommen von .richtext-wrap, nicht
|
||||
// nur für den Protokoll-Schreibbereich (wo diese Funktion ursprünglich nur
|
||||
// dort lebte): der Board-Kommentar nutzt dieselbe Leiste
|
||||
// (richtextToolbarHtml(), renderCommentEditor() in js/board/board.js), zeigte
|
||||
// die Markierung aber nie an — die Leiste blieb dort dauerhaft blass, egal ob
|
||||
// Text markiert war. Anders als im Protokoll (immer höchstens EIN
|
||||
// Schreibbereich) können im Projektboard mehrere Zeilen gleichzeitig
|
||||
// aufgeklappt sein, jede mit eigenem Kommentarfeld — der Lauscher geht
|
||||
// deshalb jedes .richtext-wrap einzeln durch statt nur eines anzunehmen.
|
||||
document.addEventListener('selectionchange', function(){
|
||||
const sel = window.getSelection();
|
||||
const bereich = (sel && sel.rangeCount > 0 && !sel.isCollapsed) ? sel.getRangeAt(0) : null;
|
||||
document.querySelectorAll('.richtext-wrap').forEach(wrap => {
|
||||
const leiste = wrap.querySelector('.richtext-toolbar');
|
||||
const feld = wrap.querySelector('[contenteditable="true"]');
|
||||
if(!leiste || !feld) return;
|
||||
const markiert = !!(bereich && feld.contains(bereich.commonAncestorContainer));
|
||||
leiste.classList.toggle('hat-auswahl', markiert);
|
||||
const hinweis = leiste.querySelector('.richtext-hinweis');
|
||||
if(hinweis) hinweis.textContent = markiert ? '✓ Markiert — jetzt formatieren' : 'Erst Text markieren';
|
||||
});
|
||||
});
|
||||
|
||||
// RICHTEXT_FARBEN: die Palette der Farbknöpfe — bewusst dieselben Werte wie
|
||||
// --red/--green/--accent/--amber (styles.css), damit eine im Text gewählte
|
||||
// Farbe dieselbe Bedeutung trägt wie anderswo in der Anwendung (Rot = Achtung,
|
||||
// Grün = erledigt/positiv, Blau = Hinweis, Orange = offen/Warnung).
|
||||
const RICHTEXT_FARBEN = [
|
||||
{ farbe: '#d70015', titel: 'Rot' },
|
||||
{ farbe: '#248a3d', titel: 'Grün' },
|
||||
{ farbe: '#0071e3', titel: 'Blau' },
|
||||
{ farbe: '#b25000', titel: 'Orange' }
|
||||
];
|
||||
|
||||
// richtextToolbarHtml: liefert die Werkzeugleiste (Fett/Kursiv/Farben) für ein
|
||||
// contenteditable-Feld (richtextFeldHtml()), das im selben .richtext-wrap-
|
||||
// Container steht. targetSelector wählt das Zielfeld innerhalb dieses
|
||||
// Containers aus — jeder Aufrufer übergibt seine eigene Feldklasse (z. B.
|
||||
// '.comment-field'), der Default greift nur, falls das je vergessen wird.
|
||||
// Parameter: targetSelector (String, optional). Rückgabe: HTML-String.
|
||||
function richtextToolbarHtml(targetSelector){
|
||||
const sel = targetSelector || '[contenteditable]';
|
||||
const ziel = `this.closest('.richtext-wrap').querySelector('${sel}')`;
|
||||
const farbButtons = RICHTEXT_FARBEN.map(f =>
|
||||
`<button type="button" class="richtext-farbe-btn" style="background:${f.farbe}" title="${f.titel}"
|
||||
onmousedown="event.preventDefault()" onclick="richtextFarbe(${ziel}, '${f.farbe}')"></button>`).join('');
|
||||
// .richtext-hinweis: Fett/Kursiv/Farbe wirken nur auf MARKIERTEN Text —
|
||||
// ohne Markierung tut ein Klick nichts, und das war bisher nirgends
|
||||
// ablesbar. Der Hinweis steht deshalb fest in der Leiste; wer ihn
|
||||
// aktualisieren will, wenn tatsächlich etwas markiert ist, hängt sich an
|
||||
// selectionchange (so macht es der Protokoll-Schreibbereich,
|
||||
// js/plan/plan-protokoll.js).
|
||||
return `<div class="richtext-toolbar">
|
||||
<span class="richtext-btn-group">
|
||||
<button type="button" class="richtext-btn" title="Fett" onmousedown="event.preventDefault()" onclick="richtextFett(${ziel})"><b>F</b></button>
|
||||
<button type="button" class="richtext-btn" title="Kursiv" onmousedown="event.preventDefault()" onclick="richtextKursiv(${ziel})"><i>K</i></button>
|
||||
</span>
|
||||
<span class="richtext-divider"></span>
|
||||
<span class="richtext-farben">${farbButtons}</span>
|
||||
<span class="richtext-hinweis">Erst Text markieren</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// -------- Projekt-Verlauf über mehrere Sitzungen --------
|
||||
|
||||
Reference in new issue
Block a user