Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb
- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4fb3c550b
commit
b7e0bd0d92
56 files changed
+12265
-2974
No files matched your search
+365
-12
@@ -309,6 +309,104 @@ function statusFocus(el, pid){
|
||||
currentStatusPid = pid;
|
||||
}
|
||||
|
||||
// ---------- Kommentar und "besprochen" je Gewerk ----------
|
||||
//
|
||||
// ELT, HLS und ARCH halten ihre Besprechung getrennt (Eriks Wunsch: "Die
|
||||
// Kommentare und Anwesenheiten von ELT brauchen nicht in HLS oder Arch zu
|
||||
// sein" — und "auch wenn das Projekt in ELT besprochen ist, heißt das nicht,
|
||||
// dass HLS das Projekt auch besprochen hat"). Ein Projekt mit mehreren
|
||||
// Gewerken trägt deshalb je Gewerk einen eigenen Sitzungskommentar samt
|
||||
// Verfasser/Zeitstempel und einen eigenen Besprochen-Haken:
|
||||
// p.kommentare = { ELT: { status, statusAuthor, statusAt, besprochen }, … }
|
||||
// (Tabelle project_kommentare, server.js). Die früheren Felder
|
||||
// p.status/statusAuthor/statusAt/besprochen werden nicht mehr gelesen oder
|
||||
// geschrieben — alles läuft über die drei Helfer unten.
|
||||
|
||||
// kommentarGewerk: das Gewerk, in dessen Zusammenhang gerade gelesen bzw.
|
||||
// geschrieben wird — im Wochenbericht das dort gewählte, überall sonst null
|
||||
// (= alle Gewerke zusammen, nur zur Anzeige). Keine Parameter.
|
||||
function kommentarGewerk(){
|
||||
if(typeof currentTab !== 'undefined' && currentTab === 'wochenbericht'
|
||||
&& typeof wochenberichtGewerk !== 'undefined' && wochenberichtGewerk) return wochenberichtGewerk;
|
||||
return null;
|
||||
}
|
||||
|
||||
// kommentarVon: der Kommentar eines Projekts für ein Gewerk. Ohne Gewerk die
|
||||
// gewerkübergreifende Anzeige: die Kommentare aller Gewerke untereinander,
|
||||
// jeweils mit dem Gewerk davor (bei nur einem Gewerk ohne Beschriftung),
|
||||
// Verfasser/Zeit des jüngsten, besprochen nur, wenn ALLE Gewerke des
|
||||
// Projekts es abgehakt haben. Parameter: p (Projekt), g (Gewerk oder
|
||||
// null/undefined). Rückgabe: { status, statusAuthor, statusAt, besprochen }.
|
||||
function kommentarVon(p, g){
|
||||
const alle = (p && p.kommentare) || {};
|
||||
if(g){
|
||||
const e = alle[g] || {};
|
||||
return { status: e.status || '', statusAuthor: e.statusAuthor || '', statusAt: e.statusAt || '', besprochen: !!e.besprochen };
|
||||
}
|
||||
const reihenfolge = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH'];
|
||||
Object.keys(alle).forEach(k => { if(!reihenfolge.includes(k)) reihenfolge.push(k); });
|
||||
const mitText = reihenfolge.filter(k => alle[k] && statusPlainText(alle[k].status) !== '');
|
||||
const gewerke = Array.isArray(p && p.gewerke) ? p.gewerke : [];
|
||||
let status = '';
|
||||
if(mitText.length === 1 && gewerke.length <= 1) status = alle[mitText[0]].status;
|
||||
// Jeder Kommentar in einem eigenen Block: Die Klartext-Vorschau
|
||||
// (statusPlainText/statusPreviewText) trennt nur an Blockgrenzen — ohne
|
||||
// eigenen Block klebten die Gewerke dort aneinander ("…fettHLS: …").
|
||||
else status = mitText.map(k => `<div><b>${esc(k)}:</b></div><div>${alle[k].status}</div>`).join('');
|
||||
const juengster = mitText.map(k => alle[k]).sort((a, b) => String(b.statusAt || '').localeCompare(String(a.statusAt || '')))[0] || {};
|
||||
const pruefen = gewerke.length ? gewerke : Object.keys(alle);
|
||||
const besprochen = pruefen.length > 0 && pruefen.every(k => alle[k] && alle[k].besprochen);
|
||||
return { status, statusAuthor: juengster.statusAuthor || '', statusAt: juengster.statusAt || '', besprochen };
|
||||
}
|
||||
|
||||
// kommentarAnzeigeHtml: der Kommentar zur ANZEIGE, bereinigt. Mit Gewerk g
|
||||
// nur dessen Text; ohne Gewerk die Texte aller Gewerke, bei mehreren je mit
|
||||
// dem Gewerk als eigener, dezenter Beschriftung (.kommentar-gewerk) —
|
||||
// Systemtext, nicht Teil des Kommentars (anders als die fette "ELT:"-Zeile,
|
||||
// die kommentarVon() für Klartext-Vorschauen einfügt). Rückgabe: HTML ('' ohne Text).
|
||||
function kommentarAnzeigeHtml(p, g){
|
||||
if(g) return sanitizeStatusHtml(kommentarVon(p, g).status || '');
|
||||
const alle = (p && p.kommentare) || {};
|
||||
const reihenfolge = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH'];
|
||||
Object.keys(alle).forEach(k => { if(!reihenfolge.includes(k)) reihenfolge.push(k); });
|
||||
const mitText = reihenfolge.filter(k => alle[k] && statusPlainText(alle[k].status) !== '');
|
||||
if(mitText.length === 0) return '';
|
||||
if(mitText.length === 1) return sanitizeStatusHtml(alle[mitText[0]].status);
|
||||
return mitText.map(k => `<div class="kommentar-gewerk">${esc(k)}</div><div>${sanitizeStatusHtml(alle[k].status)}</div>`).join('');
|
||||
}
|
||||
|
||||
// kommentarSetzen: ändert den Kommentar-Eintrag eines Gewerks (nur die in
|
||||
// patch genannten Felder). Parameter: p (Projekt), g (Gewerk), patch
|
||||
// (Objekt). Keine Rückgabe.
|
||||
function kommentarSetzen(p, g, patch){
|
||||
if(!p || !g) return;
|
||||
p.kommentare = p.kommentare || {};
|
||||
p.kommentare[g] = Object.assign(kommentarVon(p, g), patch || {});
|
||||
}
|
||||
|
||||
// personGewerk: das Gewerk einer Person aus dem Teilnehmerkreis einer
|
||||
// Sitzung. week.people kennt es selbst nicht; es kommt aus dem Konto
|
||||
// (db.globalPeople, users.department) — gesucht über die Kennung, ersatzweise
|
||||
// über den vollen Namen (Altbestand mit abweichender Kennung, siehe
|
||||
// kapazitaetsFaktor()). Parameter: person. Rückgabe: Gewerk oder ''.
|
||||
function personGewerk(person){
|
||||
const liste = (typeof db !== 'undefined' && Array.isArray(db.globalPeople)) ? db.globalPeople : [];
|
||||
const treffer = liste.find(x => x.id === person.id)
|
||||
|| (person.fullName ? liste.find(x => x.fullName === person.fullName) : null);
|
||||
return (treffer && treffer.gewerk) || '';
|
||||
}
|
||||
|
||||
// personenDesGewerks: der Teilnehmerkreis einer Sitzung, eingeschränkt auf
|
||||
// ein Gewerk — für die Anwesenheit, die je Gewerk getrennt geführt wird.
|
||||
// Personen ohne bekanntes Gewerk bleiben überall sichtbar, damit niemand
|
||||
// unauffindbar wird. Ohne Gewerk alle. Parameter: w (Woche), g.
|
||||
// Rückgabe: Array.
|
||||
function personenDesGewerks(w, g){
|
||||
const leute = Array.isArray(w && w.people) ? w.people : [];
|
||||
if(!g) return leute;
|
||||
return leute.filter(person => { const pg = personGewerk(person); return !pg || pg === g; });
|
||||
}
|
||||
|
||||
// statusValueFromHtml: normalisiert den HTML-Inhalt eines Status-Editors.
|
||||
// Liefert bei reinem Leerraum '' zurück, sonst den unveränderten HTML-String
|
||||
// (Formatierung bleibt erhalten). Parameter: `html`, Rückgabe: String.
|
||||
@@ -345,6 +443,17 @@ function formatStatus(command, value){
|
||||
// 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.
|
||||
// fontFarbeErlaubt: nur echte Hex-Farben, und keine Grau-/Schwarz-/Weißtöne —
|
||||
// eingefügter Text bringt oft color="#1a1a1a" mit, das im Dunkelmodus
|
||||
// unsichtbar wäre und ohnehin nichts hervorhebt. Rückgabe: Boolean.
|
||||
function fontFarbeErlaubt(wert){
|
||||
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(wert || '').trim());
|
||||
if(!m) return false;
|
||||
const h = m[1].length === 3 ? m[1].replace(/./g, c => c + c) : m[1];
|
||||
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
||||
return Math.max(r, g, b) - Math.min(r, g, b) > 40;
|
||||
}
|
||||
|
||||
function sanitizeStatusHtml(html){
|
||||
if(!html) return '';
|
||||
// Die Bereinigung läuft vollständig IM inerten Dokument ab: Würden die
|
||||
@@ -386,6 +495,10 @@ function sanitizeStatusHtml(html){
|
||||
}
|
||||
for(let j = child.attributes.length - 1; j >= 0; j--){
|
||||
const attr = child.attributes[j].name;
|
||||
// <font color="…"> (ältere Kommentare, Browser-Farbbefehl) behält
|
||||
// seine Farbe — vorher fiel sie hier still weg, und farbige
|
||||
// Bestandskommentare erschienen überall einfarbig.
|
||||
if(tag === 'FONT' && attr.toLowerCase() === 'color' && fontFarbeErlaubt(child.getAttribute(attr))) continue;
|
||||
if(!allowedAttrs.has(attr.toLowerCase())) child.removeAttribute(attr);
|
||||
}
|
||||
if(child.hasAttribute('style')){
|
||||
@@ -474,9 +587,239 @@ function richtextWickeln(feld, tagName, styleAttr){
|
||||
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); }
|
||||
|
||||
// ---------- Auszeichnung wieder entfernen (Umschalten) ----------
|
||||
//
|
||||
// Eriks Wunsch: Ein zweiter Klick auf denselben Knopf nimmt die Auszeichnung
|
||||
// wieder zurück — rot markierter Text wird durch erneutes Klicken auf Rot
|
||||
// wieder schwarz, ebenso Fett/Kursiv und die übrigen Farben. Vorher wickelte
|
||||
// jeder Klick nur eine weitere Hülle um den Text, rückgängig machen ließ sich
|
||||
// nichts.
|
||||
//
|
||||
// Eine Auszeichnungsart ist ein kleines Objekt { trifft(el), entferne(el) }:
|
||||
// trifft() erkennt ein Element dieser Art, entferne() nimmt NUR diese Art
|
||||
// heraus — ein <span style="color:red;font-weight:bold"> verliert beim
|
||||
// Entfernen der Farbe nicht auch das Fett.
|
||||
|
||||
// richtextFarbWert: normalisiert eine Farbangabe auf die Schreibweise des
|
||||
// Browsers ("rgb(215, 0, 21)"), damit '#d70015' und der gespeicherte Wert
|
||||
// vergleichbar sind. Parameter: farbe (String). Rückgabe: String.
|
||||
function richtextFarbWert(farbe){
|
||||
const t = document.createElement('span');
|
||||
t.style.color = farbe || '';
|
||||
return t.style.color;
|
||||
}
|
||||
|
||||
// richtextAuspacken: ersetzt ein Element durch seine Kinder.
|
||||
function richtextAuspacken(el){
|
||||
if(el.parentNode) el.replaceWith(...Array.from(el.childNodes));
|
||||
}
|
||||
|
||||
// richtextStilEntfernen: nimmt eine CSS-Eigenschaft aus dem style-Attribut
|
||||
// und packt ein dann bedeutungsloses <span>/<font> aus.
|
||||
function richtextStilEntfernen(el, prop){
|
||||
el.style.removeProperty(prop);
|
||||
if(!el.getAttribute('style')) el.removeAttribute('style');
|
||||
const tag = el.tagName;
|
||||
if((tag === 'SPAN' || tag === 'FONT') && el.attributes.length === 0) richtextAuspacken(el);
|
||||
}
|
||||
|
||||
const RICHTEXT_ART_FETT = {
|
||||
trifft: el => el.tagName === 'B' || el.tagName === 'STRONG'
|
||||
|| /^(bold|[6-9]00)$/.test((el.style && el.style.fontWeight) || ''),
|
||||
entferne: el => (el.tagName === 'B' || el.tagName === 'STRONG') ? richtextAuspacken(el) : richtextStilEntfernen(el, 'font-weight')
|
||||
};
|
||||
const RICHTEXT_ART_KURSIV = {
|
||||
trifft: el => el.tagName === 'I' || el.tagName === 'EM',
|
||||
entferne: el => richtextAuspacken(el)
|
||||
};
|
||||
// richtextArtFarbe: eine bestimmte Farbe (farbe gesetzt) oder jede Farbe
|
||||
// (farbe leer — zum Umfärben: erst die alte Farbe entfernen, dann die neue
|
||||
// setzen, statt Hüllen ineinander zu schachteln).
|
||||
function richtextArtFarbe(farbe){
|
||||
const ziel = farbe ? richtextFarbWert(farbe) : '';
|
||||
const farbeVon = el => (el.style && el.style.color) || (el.tagName === 'FONT' ? richtextFarbWert(el.getAttribute('color')) : '');
|
||||
return {
|
||||
trifft: el => { const f = farbeVon(el); return !!f && (!ziel || f === ziel); },
|
||||
entferne: el => {
|
||||
if(el.tagName === 'FONT') el.removeAttribute('color');
|
||||
richtextStilEntfernen(el, 'color');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// richtextHatArt: steht in einem Element (zwischen node und feld) eine
|
||||
// Auszeichnung dieser Art?
|
||||
function richtextHatArt(node, feld, art){
|
||||
for(let n = node.nodeType === 1 ? node : node.parentNode; n && n !== feld; n = n.parentNode){
|
||||
if(n.nodeType === 1 && art.trifft(n)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// richtextAuswahl: die aktuelle, nicht leere Markierung innerhalb von feld
|
||||
// oder null.
|
||||
function richtextAuswahl(feld){
|
||||
if(!feld) return null;
|
||||
const sel = window.getSelection();
|
||||
if(!sel || sel.rangeCount === 0) return null;
|
||||
const range = sel.getRangeAt(0);
|
||||
if(range.collapsed || !feld.contains(range.commonAncestorContainer)) return null;
|
||||
return range;
|
||||
}
|
||||
|
||||
// richtextIstAktiv: trägt JEDER markierte (sichtbare) Text die Auszeichnung?
|
||||
// Nur dann schaltet ein Klick sie ab; ist sie nur teilweise vorhanden, wird
|
||||
// sie auf die ganze Markierung ausgedehnt — wie in jeder Textverarbeitung.
|
||||
function richtextIstAktiv(feld, range, art){
|
||||
const wurzel = range.commonAncestorContainer;
|
||||
const texte = [];
|
||||
if(wurzel.nodeType === 3) texte.push(wurzel);
|
||||
else {
|
||||
const walker = document.createTreeWalker(wurzel, NodeFilter.SHOW_TEXT);
|
||||
while(walker.nextNode()) if(range.intersectsNode(walker.currentNode)) texte.push(walker.currentNode);
|
||||
}
|
||||
const relevant = texte.filter(t => t.textContent.trim() !== '');
|
||||
return relevant.length > 0 && relevant.every(t => richtextHatArt(t, feld, art));
|
||||
}
|
||||
|
||||
// richtextLeereHuellenEntfernen: Beim Aufteilen bleiben leere <b>/<span>
|
||||
// zurück — unsichtbar, aber sie würden mitgespeichert.
|
||||
function richtextLeereHuellenEntfernen(feld){
|
||||
feld.querySelectorAll('b, strong, i, em, u, span, font').forEach(el => {
|
||||
if(el.textContent === '' && !el.querySelector('br')) el.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// richtextArtEntfernen: entfernt eine Auszeichnungsart von der Markierung.
|
||||
// Der markierte Teil wird herausgelöst und von dieser Art befreit; liegt er
|
||||
// INNERHALB eines solchen Elements (rot markiertes Wort mitten in einem roten
|
||||
// Satz), wird dieses Element um ihn herum aufgeteilt, sodass Text davor und
|
||||
// dahinter seine Auszeichnung behält. Die Markierung bleibt danach auf
|
||||
// demselben Text stehen.
|
||||
function richtextArtEntfernen(feld, range, art){
|
||||
const mitte = document.createElement('span');
|
||||
mitte.appendChild(range.extractContents());
|
||||
// Auszeichnungen dieser Art INNERHALB der Markierung
|
||||
Array.from(mitte.querySelectorAll('*')).reverse().forEach(el => { if(art.trifft(el)) art.entferne(el); });
|
||||
range.insertNode(mitte);
|
||||
|
||||
// Umgebende Elemente dieser Art um die Markierung herum aufteilen
|
||||
for(;;){
|
||||
let huelle = null;
|
||||
for(let n = mitte.parentNode; n && n !== feld; n = n.parentNode){
|
||||
if(n.nodeType === 1 && art.trifft(n)){ huelle = n; break; }
|
||||
}
|
||||
if(!huelle) break;
|
||||
const vorher = document.createRange();
|
||||
vorher.setStart(huelle, 0); vorher.setEndBefore(mitte);
|
||||
const vorderteil = vorher.extractContents();
|
||||
const nachher = document.createRange();
|
||||
nachher.setStartAfter(mitte); nachher.setEnd(huelle, huelle.childNodes.length);
|
||||
const hinterteil = nachher.extractContents();
|
||||
if(vorderteil.textContent !== '' || vorderteil.querySelector && vorderteil.querySelector('br')){
|
||||
const k = huelle.cloneNode(false); k.appendChild(vorderteil); huelle.before(k);
|
||||
}
|
||||
if(hinterteil.textContent !== '' || hinterteil.querySelector && hinterteil.querySelector('br')){
|
||||
const k = huelle.cloneNode(false); k.appendChild(hinterteil); huelle.after(k);
|
||||
}
|
||||
art.entferne(huelle);
|
||||
}
|
||||
|
||||
const kinder = Array.from(mitte.childNodes);
|
||||
richtextAuspacken(mitte);
|
||||
richtextLeereHuellenEntfernen(feld);
|
||||
const sel = window.getSelection();
|
||||
const neu = document.createRange();
|
||||
const erstes = kinder.find(k => k.parentNode);
|
||||
const letztes = kinder.slice().reverse().find(k => k.parentNode);
|
||||
if(erstes && letztes){
|
||||
neu.setStartBefore(erstes); neu.setEndAfter(letztes);
|
||||
sel.removeAllRanges(); sel.addRange(neu);
|
||||
}
|
||||
}
|
||||
|
||||
// richtextUmschalten: setzt die Auszeichnung, oder nimmt sie zurück, wenn die
|
||||
// ganze Markierung sie schon trägt. ersetzt (optional) ist eine Art, die
|
||||
// vorher entfernt wird — für Farben: Blau auf rotem Text färbt um, statt eine
|
||||
// blaue Hülle in die rote zu legen.
|
||||
function richtextUmschalten(feld, art, wickeln, ersetzt){
|
||||
const range = richtextAuswahl(feld);
|
||||
if(!range) return;
|
||||
if(richtextIstAktiv(feld, range, art)){
|
||||
richtextArtEntfernen(feld, range, art);
|
||||
} else {
|
||||
if(ersetzt) richtextArtEntfernen(feld, range, ersetzt);
|
||||
else richtextArtEntfernen(feld, range, art); // keine doppelten Hüllen
|
||||
wickeln();
|
||||
}
|
||||
feld.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
function richtextFett(feld){
|
||||
richtextUmschalten(feld, RICHTEXT_ART_FETT, () => richtextWickeln(feld, 'b'));
|
||||
}
|
||||
function richtextKursiv(feld){
|
||||
richtextUmschalten(feld, RICHTEXT_ART_KURSIV, () => richtextWickeln(feld, 'i'));
|
||||
}
|
||||
function richtextFarbe(feld, farbe){
|
||||
richtextUmschalten(feld, richtextArtFarbe(farbe),
|
||||
() => richtextWickeln(feld, 'span', 'color:' + farbe), richtextArtFarbe(''));
|
||||
}
|
||||
|
||||
// richtextFormatLoeschen: setzt Text auf das Standardformat zurück — kein
|
||||
// Fett, kein Kursiv, keine Farbe. Mit Markierung nur diese, ohne Markierung
|
||||
// das ganze Feld. Zeilenumbrüche und Absätze bleiben erhalten, nur die
|
||||
// Auszeichnung fällt weg. Parameter: feld (Element). Keine Rückgabe.
|
||||
function richtextFormatLoeschen(feld){
|
||||
if(!feld || feld.getAttribute('contenteditable') !== 'true') return;
|
||||
const saeubern = wurzel => {
|
||||
Array.from(wurzel.querySelectorAll('b, strong, i, em, u, span, font')).reverse().forEach(richtextAuspacken);
|
||||
wurzel.querySelectorAll('[style]').forEach(el => el.removeAttribute('style'));
|
||||
};
|
||||
const range = richtextAuswahl(feld);
|
||||
if(range){
|
||||
// Alle Arten nacheinander von der Markierung nehmen — teilt umgebende
|
||||
// Hüllen sauber auf, statt den Text davor/dahinter mit zu entformatieren.
|
||||
[RICHTEXT_ART_FETT, RICHTEXT_ART_KURSIV, richtextArtFarbe(''),
|
||||
{ trifft: el => el.tagName === 'U', entferne: richtextAuspacken }]
|
||||
.forEach(art => { const r = richtextAuswahl(feld); if(r) richtextArtEntfernen(feld, r, art); });
|
||||
} else {
|
||||
saeubern(feld);
|
||||
}
|
||||
feld.normalize();
|
||||
feld.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
// richtextNeueZeileMitStrich: Enter beginnt die neue Zeile mit "- " (Eriks
|
||||
// Wunsch für den Wochenbericht-Kommentar — Stichpunkte sind dort die Regel).
|
||||
// Der Strich ist gewöhnlicher Text: Wer eine Zeile ohne ihn will, löscht ihn
|
||||
// mit der Rücktaste. Umschalt+Enter bleibt beim Browser und fügt eine Zeile
|
||||
// ganz ohne Strich ein.
|
||||
// Parameter: e (keydown-Event), feld (Element). Rückgabe: true, wenn das
|
||||
// Ereignis behandelt wurde.
|
||||
function richtextNeueZeileMitStrich(e, feld){
|
||||
if(e.key !== 'Enter' || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return false;
|
||||
const sel = window.getSelection();
|
||||
if(!sel || sel.rangeCount === 0 || !feld.contains(sel.getRangeAt(0).commonAncestorContainer)) return false;
|
||||
e.preventDefault();
|
||||
const range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
const br = document.createElement('br');
|
||||
// Nach dem <br> steht immer Text (der Strich) — ein <br> am Feldende
|
||||
// würde der Browser sonst nicht als neue Zeile anzeigen.
|
||||
// Geschütztes Leerzeichen: Ein gewöhnliches am Zeilenende verwirft der
|
||||
// Browser beim Weitertippen, aus "- Text" würde "-Text".
|
||||
const text = document.createTextNode('-\u00a0');
|
||||
range.insertNode(text);
|
||||
range.insertNode(br);
|
||||
const neu = document.createRange();
|
||||
neu.setStart(text, text.textContent.length);
|
||||
neu.collapse(true);
|
||||
sel.removeAllRanges(); sel.addRange(neu);
|
||||
feld.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// richtextFeldHtml: liefert ein contenteditable-Feld mit Platzhaltertext
|
||||
// (:empty:before + data-placeholder, siehe styles.css) als Ersatz für ein
|
||||
@@ -579,7 +922,7 @@ document.addEventListener('selectionchange', function(){
|
||||
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';
|
||||
if(hinweis) hinweis.textContent = markiert ? '✓ Markiert — jetzt formatieren' : 'Erst Text markieren · erneut klicken hebt auf';
|
||||
});
|
||||
});
|
||||
|
||||
@@ -619,7 +962,10 @@ function richtextToolbarHtml(targetSelector){
|
||||
</span>
|
||||
<span class="richtext-divider"></span>
|
||||
<span class="richtext-farben">${farbButtons}</span>
|
||||
<span class="richtext-hinweis">Erst Text markieren</span>
|
||||
<span class="richtext-divider"></span>
|
||||
<button type="button" class="richtext-btn richtext-btn-loeschen" title="Formatierung entfernen — die Markierung, ohne Markierung den ganzen Text"
|
||||
onmousedown="event.preventDefault()" onclick="richtextFormatLoeschen(${ziel})">Standard</button>
|
||||
<span class="richtext-hinweis">Erst Text markieren · erneut klicken hebt auf</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -672,18 +1018,22 @@ function statusPlainText(html){
|
||||
// wurden, tragen den Text der Vorwoche unverändert mit. Parameter: uid
|
||||
// (String). Rückgabe: Array von { week, text, html, author, at, isFirst },
|
||||
// neueste zuerst.
|
||||
function projectStatusHistory(uid){
|
||||
function projectStatusHistory(uid, g){
|
||||
// Gewerk: ausdrücklich übergeben, sonst das des Zusammenhangs (im
|
||||
// Wochenbericht das gewählte, siehe kommentarGewerk()).
|
||||
const gewerk = g === undefined ? kommentarGewerk() : g;
|
||||
const versions = projectVersionsForUid(uid);
|
||||
const entries = [];
|
||||
let lastText = null;
|
||||
versions.forEach((v, idx) => {
|
||||
const text = statusPlainText(v.project.status);
|
||||
const k = kommentarVon(v.project, gewerk);
|
||||
const text = statusPlainText(k.status);
|
||||
const entry = {
|
||||
week: v.week,
|
||||
text,
|
||||
html: sanitizeStatusHtml(v.project.status || ''),
|
||||
author: v.project.statusAuthor || '',
|
||||
at: v.project.statusAt || '',
|
||||
html: sanitizeStatusHtml(k.status || ''),
|
||||
author: k.statusAuthor || '',
|
||||
at: k.statusAt || '',
|
||||
isFirst: idx === 0
|
||||
};
|
||||
if(idx === 0){
|
||||
@@ -829,7 +1179,10 @@ function renderProjektVerlauf(p, weekLabel, locked){
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const text = statusPlainText(v.project.status);
|
||||
const text = statusPlainText(kommentarVon(v.project, kommentarGewerk()).status);
|
||||
// Angezeigt wird der Kommentar MIT Formatierung (Farben, fett, Absätze),
|
||||
// das Gewerk als eigene Beschriftung; text dient nur Tooltip und Prüfung.
|
||||
const html = text ? kommentarAnzeigeHtml(v.project, kommentarGewerk()) : '';
|
||||
const termine = verlaufTermine(v.project);
|
||||
const termineDavor = davor ? verlaufTermine(davor.project) : { ende:'', meilenstein:'' };
|
||||
const beteiligte = verlaufBeteiligte(v);
|
||||
@@ -863,7 +1216,7 @@ function renderProjektVerlauf(p, weekLabel, locked){
|
||||
<span class="timeline-kw">${esc(v.week.label)}</span>
|
||||
<span class="timeline-datum">${esc(v.week.date ? formatWeekDate(v.week.date) : '')}</span>
|
||||
</header>
|
||||
<div class="timeline-text${text ? '' : ' timeline-leer'}" title="${esc(text)}">${text ? esc(text) : 'Kein Kommentar'}</div>
|
||||
<div class="timeline-text${text ? '' : ' timeline-leer'}" title="${esc(text)}">${text ? html : 'Kein Kommentar'}</div>
|
||||
${uebernehmen}
|
||||
<div class="timeline-lph-list">${lphHtml}</div>
|
||||
${zeilen.length ? `<div class="timeline-termine">${zeilen.join('')}</div>` : ''}
|
||||
|
||||
Reference in new issue
Block a user