-Feature: Ab sofort werden alle Configdateien über eine Example.config.php definiert. über das Dashboard kann nun über den neuen Punkt "Config" die eigentliche config.php bearbeitet werden. Änderungen durch Programmupdates werden jetzt automatisch in der example.config.php definiert und beim nächsten Speichern der config-datei über die Website angepasst.

-Feature: Scrollen der Listen können einzeln abeschalten werden
This commit is contained in:
2025-10-10 10:46:33 +02:00
parent 91c0a2d9d9
commit 36944c71bf
6 changed files with 1004 additions and 336 deletions

View File

@@ -23,15 +23,71 @@ $Epi = new Epirent();
<script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>
<script type="text/javascript">
// === Höhe der Scroll-Container bis Viewport-Ende anpassen ===
// === PHP-Flags in JS bringen ===
const SCROLL_FLAGS = {
checkout: <?php echo (defined('EnableScrollingCheckOut') && EnableScrollingCheckOut) ? 'true' : 'false'; ?>,
checkin: <?php echo (defined('EnableScrollingCheckIn') && EnableScrollingCheckIn) ? 'true' : 'false'; ?>,
aufgaben: <?php echo (defined('EnableScrollingAufgaben') && EnableScrollingAufgaben) ? 'true' : 'false'; ?>
};
// Mapping: DOM-ID -> Flag-Key
const SCROLLER_KEYS = new Map([
['checkout-scroll', 'checkout'],
['checkin-scroll', 'checkin'],
['aufgaben-scroll', 'aufgaben']
]);
const TABLE_KEYS = new Map([
['checkout-table', 'checkout'],
['checkin-table', 'checkin'],
['aufgaben-table', 'aufgaben']
]);
function keyForEl(el, map) {
if (!el || !el.id) return null;
return map.get(el.id) || null;
}
function isEnabledByEl(el) {
const k = keyForEl(el, SCROLLER_KEYS);
return !!(k && SCROLL_FLAGS[k]);
}
function isEnabledByKey(key) {
return !!SCROLL_FLAGS[key];
}
// === Höhe der Scroll-Container: nebeneinander = volle Höhe; gestapelt = Drittel ===
function sizeScrollContainers() {
$('.tableFixHead').each(function () {
const rect = this.getBoundingClientRect();
const bottomMargin = 16;
const available = window.innerHeight - rect.top - bottomMargin;
this.style.maxHeight = (available > 120 ? available : 120) + 'px';
const nodes = [
document.querySelector('#checkout-scroll'),
document.querySelector('#checkin-scroll'),
document.querySelector('#aufgaben-scroll')
].filter(Boolean);
if (nodes.length === 0) return;
// Ermitteln, ob alles in einer Zeile (nebeneinander) oder gestapelt
const tops = nodes.map(n => n.getBoundingClientRect().top);
const ROW_TOL = 8; // px
const uniqueRows = [];
tops.forEach(t => {
const exists = uniqueRows.some(rt => Math.abs(rt - t) <= ROW_TOL);
if (!exists) uniqueRows.push(t);
});
const isOneRow = uniqueRows.length === 1;
const bottomMargin = 16;
const minH = 120;
nodes.forEach(node => {
// Wenn Scrolling für diesen Bereich deaktiviert ist, Höhe trotzdem setzen (wie gefordert)
const rect = node.getBoundingClientRect();
if (isOneRow) {
const available = window.innerHeight - rect.top - bottomMargin;
node.style.maxHeight = Math.max(minH, available) + 'px';
} else {
const third = Math.floor(window.innerHeight / 3) - bottomMargin;
node.style.maxHeight = Math.max(minH, third) + 'px';
}
});
}
@@ -46,6 +102,7 @@ $Epi = new Epirent();
function attachScrollGuards($scroller) {
const el = $scroller.get(0);
if (!el || el.__guardsBound) return;
if (!isEnabledByEl(el)) return; // nur wenn Feature aktiv
const state = ensureState(el);
const markActive = () => {
@@ -60,45 +117,40 @@ $Epi = new Epirent();
el.__guardsBound = true;
}
// === Loop aufbauen: nahtloses Doppel nur bei Bedarf ===
// Klont den ersten <tbody> als __loopClone ans Tabellenende, wenn Inhalt > Sichtbereich.
// === Loop aufbauen: nahtloses Doppel nur bei Bedarf & nur wenn aktiv ===
function buildSeamlessLoop($table, $scroller) {
$table.find('tbody.__loopClone').remove();
const $main = $table.find('tbody').first();
const scEl = $scroller.get(0);
const enabled = isEnabledByEl(scEl);
const st = ensureState(scEl);
st.loopHeight = 0;
if ($main.length === 0 || $main.children().length === 0) {
st.loopHeight = 0;
return;
}
if (!enabled) return; // deaktiviert -> keine Loops
const mainH = $main.get(0).offsetHeight;
const needScroll = mainH > scEl.clientHeight + 1;
const $main = $table.find('tbody').first();
if ($main.length === 0 || $main.children().length === 0) return;
if (!needScroll) {
st.loopHeight = 0;
return;
}
const needScroll = $main.get(0).offsetHeight > scEl.clientHeight + 1;
if (!needScroll) return;
const $clone = $main.clone(false, false).addClass('__loopClone').attr('aria-hidden', 'true');
$table.append($clone);
st.loopHeight = mainH; // Höhe des Original-Inhalts als Loop-Länge
st.loopHeight = $main.get(0).offsetHeight; // Höhe des Original-Inhalts als Loop-Länge
}
// === Auto-Scroll (nahtlos) ===
function startAutoScroll($scroller, speedPx = 1, stepMs = 40) {
const el = $scroller.get(0);
const st = ensureState(el);
if (!isEnabledByEl(el)) return;
const st = ensureState(el);
if (st.autoTimer) return; // schon aktiv
if (st.loopHeight <= 0) return; // kein Overflow => nicht scrollen
const tick = () => {
if (st.userActive) { stopAutoScroll($scroller); return; }
el.scrollTop += speedPx;
// Nahtlos: sobald wir das Ende des Original-Blocks überschreiten, ziehen wir loopHeight ab
if (el.scrollTop >= st.loopHeight) {
el.scrollTop -= st.loopHeight;
}
@@ -118,6 +170,7 @@ $Epi = new Epirent();
function maybeStartAutoScroll($scroller) {
const el = $scroller.get(0);
if (!isEnabledByEl(el)) { stopAutoScroll($scroller); return; }
const st = ensureState(el);
if (st.userActive) return;
if (st.loopHeight > 0) startAutoScroll($scroller, 1, 80);
@@ -125,33 +178,33 @@ $Epi = new Epirent();
}
// === AJAX-Reload: relative Position innerhalb der Loop erhalten ===
// Wir merken die Position modulo loopHeight; nach dem Reload setzen wir proportional um.
function smartLoad($scroller, $table, $target, url, intervalMs) {
const scEl = $scroller.get(0);
const st = ensureState(scEl);
const enabled = isEnabledByEl(scEl);
const oldLoop = st.loopHeight > 0 ? st.loopHeight : Math.max(1, scEl.scrollHeight - scEl.clientHeight);
const posInLoop = st.loopHeight > 0 ? (scEl.scrollTop % oldLoop) : scEl.scrollTop;
const posRatio = Math.min(1, posInLoop / oldLoop);
$target.load(url, function () {
// Loop neu bewerten/aufbauen
buildSeamlessLoop($table, $scroller);
if (st.loopHeight > 0) {
// Neue relative Position setzen (proportional)
const newPos = Math.floor(posRatio * st.loopHeight);
if (Math.abs(scEl.scrollTop - newPos) > 1) {
// rAF für flüssiges Setzen außerhalb des load()-Layouts
requestAnimationFrame(() => { scEl.scrollTop = newPos; });
if (enabled) {
buildSeamlessLoop($table, $scroller);
if (st.loopHeight > 0) {
const newPos = Math.floor(posRatio * st.loopHeight);
if (Math.abs(scEl.scrollTop - newPos) > 1) {
requestAnimationFrame(() => { scEl.scrollTop = newPos; });
}
stopAutoScroll($scroller);
startAutoScroll($scroller, 1, 80);
} else {
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
stopAutoScroll($scroller);
}
// Auto-Scroll (wieder) starten
stopAutoScroll($scroller);
startAutoScroll($scroller, 1, 80);
} else {
// kein Overflow -> ganz oben und Auto-Scroll aus
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
$table.find('tbody.__loopClone').remove();
stopAutoScroll($scroller);
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
}
setTimeout(() => smartLoad($scroller, $table, $target, url, intervalMs), intervalMs);
@@ -161,9 +214,9 @@ $Epi = new Epirent();
// === Initialisierung ===
$(document).ready(function () {
sizeScrollContainers();
$(window).on('resize', function () {
sizeScrollContainers();
// Nach Layoutwechsel neu entscheiden
['#checkout', '#checkin', '#aufgaben'].forEach(prefix => {
const $scroller = $(`${prefix}-scroll`);
const $table = $(`${prefix}-table`);
@@ -172,32 +225,39 @@ $Epi = new Epirent();
});
});
// Guards pro Scroller
attachScrollGuards($('#checkout-scroll'));
attachScrollGuards($('#checkin-scroll'));
attachScrollGuards($('#aufgaben-scroll'));
// Guards nur für aktivierte Scroller
[['#checkout-scroll','checkout'], ['#checkin-scroll','checkin'], ['#aufgaben-scroll','aufgaben']].forEach(([sel,key]) => {
if (isEnabledByKey(key)) attachScrollGuards($(sel));
});
// Erstladen je Tabelle: laden -> Loop bauen -> ggf. Auto-Scroll -> zyklisch refreshen
function initOne(scrollerSel, tableSel, tbodySel, url, ms) {
// Erstladen je Tabelle
function initOne(scrollerSel, tableSel, tbodySel, url, ms, key) {
const $scroller = $(scrollerSel);
const $table = $(tableSel);
const $target = $(tbodySel);
const enabled = isEnabledByKey(key);
$target.load(url, function () {
buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
if (enabled) {
buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
} else {
$table.find('tbody.__loopClone').remove();
stopAutoScroll($scroller);
}
setTimeout(() => smartLoad($scroller, $table, $target, url, ms), ms);
});
}
initOne('#checkout-scroll', '#checkout-table', '#getCheckOutTableHolder', 'sources/getCheckOutTable.php', 5000);
initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 5000);
initOne('#aufgaben-scroll', '#aufgaben-table', '#AufgabenTableHolder', 'sources/getAufgabenTable.php', 30000);
initOne('#checkout-scroll', '#checkout-table', '#getCheckOutTableHolder', 'sources/getCheckOutTable.php', 5000, 'checkout');
initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 5000, 'checkin');
initOne('#aufgaben-scroll', '#aufgaben-table', '#AufgabenTableHolder', 'sources/getAufgabenTable.php', 30000, 'aufgaben');
});
</script>
<style>
/* Scroll-Wrapper je Tabelle */
.tableFixHead {