Also hier mein neuster Block:
Testseite: https://www.niederastroth.de/dataform/
STRG&Doppelklick - Passwort: volker04




Code: Alles auswählen
https://www.mobirise-tutorials.com/homepages/12/d22935777/htdocs/Mobirise-Tutorials/
VN-Block-Galerie/assets/vn-dataform/uploads/form-1/img_6a5b20516bce7.pngCode: Alles auswählen
https://www.mobirise-tutorials.com/VN-Block-Galerie/assets/vn-dataform/
uploads/form-1/img_6a5b20516bce7.png
Code: Alles auswählen
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
date_default_timezone_set('Europe/Berlin');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
/*
* Datenverzeichnis
*/
$dataDir = __DIR__ . '/data';
if (!is_dir($dataDir)) {
mkdir($dataDir, 0755, true);
}
file_put_contents(
$dataDir . '/.htaccess',
"Deny from all\n",
LOCK_EX
);
/*
* Galerie-/Formular-ID bereinigen
*/
$gid = preg_replace(
'/[^a-z0-9_-]/i',
'',
$_GET['gid'] ?? 'default'
);
if ($gid === '') {
$gid = 'default';
}
$dbFile = $dataDir . '/' . $gid . '.sqlite';
$pwFile = $dataDir . '/' . $gid . '.hash';
/*
* JSON-Antwort ausgeben
*/
function respond(bool $ok, array $extra = []): void
{
echo json_encode(
array_merge(['success' => $ok], $extra),
JSON_UNESCAPED_UNICODE
);
exit;
}
/*
* Adminstatus prüfen
*/
function isAdmin(string $gid): bool
{
return !empty($_SESSION['vndf_admin_' . $gid]);
}
/*
* Öffentlichen URL-Pfad des aktuellen PHP-Verzeichnisses ermitteln.
*
* Beispiel:
* SCRIPT_NAME:
* /VN-Block-Galerie/assets/vn-dataform/dataform.php
*
* Ergebnis:
* /VN-Block-Galerie/assets/vn-dataform
*/
function getPublicScriptDirectory(): string
{
$scriptName = $_SERVER['SCRIPT_NAME']
?? $_SERVER['PHP_SELF']
?? '';
$scriptName = str_replace('\\', '/', $scriptName);
/*
* Querystrings vorsorglich entfernen.
*/
$scriptPath = parse_url($scriptName, PHP_URL_PATH);
if (!is_string($scriptPath) || $scriptPath === '') {
return '';
}
$directory = dirname($scriptPath);
$directory = str_replace('\\', '/', $directory);
/*
* dirname('/') kann "/" ergeben.
*/
if ($directory === '/' || $directory === '.') {
return '';
}
return '/' . trim($directory, '/');
}
/*
* Datenbank initialisieren
*/
$db = new PDO('sqlite:' . $dbFile);
$db->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION
);
$db->exec(
"CREATE TABLE IF NOT EXISTS vndf_fields (
id INTEGER PRIMARY KEY,
sort_order INTEGER DEFAULT 0,
field_key TEXT UNIQUE,
label TEXT,
type TEXT DEFAULT 'text',
required INTEGER DEFAULT 0,
searchable INTEGER DEFAULT 1,
show_in_list INTEGER DEFAULT 1
)"
);
$db->exec(
"CREATE TABLE IF NOT EXISTS vndf_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT DEFAULT (datetime('now','localtime')),
data TEXT
)"
);
/*
* sort_order-Spalte bei älteren Datenbanken ergänzen.
*/
try {
$db->exec(
"ALTER TABLE vndf_records
ADD COLUMN sort_order INTEGER DEFAULT 0"
);
} catch (Exception $e) {
/*
* Die Spalte existiert bereits.
*/
}
/*
* JSON-Eingabe lesen
*/
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput ?: '', true);
if (!is_array($input)) {
$input = [];
}
$action = $_GET['action']
?? $input['action']
?? '';
/*
* ─────────────────────────────────────────────
* Authentifizierung
* ─────────────────────────────────────────────
*/
if ($action === 'check') {
respond(
true,
[
'loggedIn' => isAdmin($gid),
'firstRun' => !file_exists($pwFile)
]
);
}
if ($action === 'login') {
$pw = (string)($input['password'] ?? '');
/*
* Beim ersten Aufruf Passwort anlegen.
*/
if (!file_exists($pwFile)) {
if (strlen($pw) < 4) {
respond(
false,
['error' => 'Min. 4 Zeichen.']
);
}
$passwordHash = password_hash(
$pw,
PASSWORD_BCRYPT
);
if ($passwordHash === false) {
respond(
false,
['error' => 'Passwort konnte nicht gespeichert werden.']
);
}
if (
file_put_contents(
$pwFile,
$passwordHash,
LOCK_EX
) === false
) {
respond(
false,
['error' => 'Passwortdatei konnte nicht geschrieben werden.']
);
}
$_SESSION['vndf_admin_' . $gid] = true;
respond(
true,
['firstRun' => true]
);
}
$storedHash = trim(
(string)file_get_contents($pwFile)
);
if (
$storedHash !== ''
&& password_verify($pw, $storedHash)
) {
$_SESSION['vndf_admin_' . $gid] = true;
respond(true);
}
respond(
false,
['error' => 'Falsches Passwort.']
);
}
if ($action === 'logout') {
unset($_SESSION['vndf_admin_' . $gid]);
respond(true);
}
/*
* ─────────────────────────────────────────────
* Felder
* ─────────────────────────────────────────────
*/
if ($action === 'get_fields') {
$rows = $db
->query(
"SELECT *
FROM vndf_fields
ORDER BY sort_order, id"
)
->fetchAll(PDO::FETCH_ASSOC);
respond(
true,
['fields' => $rows]
);
}
if ($action === 'save_fields') {
if (!isAdmin($gid)) {
respond(
false,
['error' => 'Nicht autorisiert.']
);
}
$fields = $input['fields'] ?? [];
if (!is_array($fields)) {
$fields = [];
}
$db->beginTransaction();
try {
$db->exec(
"DELETE FROM vndf_fields"
);
$stmt = $db->prepare(
"INSERT INTO vndf_fields (
sort_order,
field_key,
label,
type,
required,
searchable,
show_in_list
) VALUES (?, ?, ?, ?, ?, ?, ?)"
);
foreach ($fields as $i => $f) {
if (!is_array($f)) {
continue;
}
$label = trim(
(string)($f['label'] ?? '')
);
if ($label === '') {
$label = 'Feld ' . ($i + 1);
}
$key = preg_replace(
'/[^a-z0-9_]/i',
'_',
strtolower($label)
);
if ($key === null || $key === '') {
$key = 'feld_' . $i;
}
$stmt->execute(
[
$i,
$key,
$label,
$f['type'] ?? 'text',
(int)($f['required'] ?? 0),
(int)($f['searchable'] ?? 1),
(int)($f['show_in_list'] ?? 1)
]
);
}
$db->commit();
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
respond(
false,
['error' => 'Felder konnten nicht gespeichert werden.']
);
}
respond(
true,
['message' => '✅ Felder gespeichert!']
);
}
/*
* ─────────────────────────────────────────────
* Datensätze
* ─────────────────────────────────────────────
*/
if ($action === 'get_records') {
$search = trim(
(string)($input['search'] ?? '')
);
$page = max(
0,
(int)($input['page'] ?? 0)
);
$limit = max(
5,
min(
100,
(int)($input['limit'] ?? 20)
)
);
$all = $db
->query(
"SELECT *
FROM vndf_records
ORDER BY sort_order ASC, id ASC"
)
->fetchAll(PDO::FETCH_ASSOC);
$rows = [];
foreach ($all as $row) {
$data = json_decode(
$row['data'] ?? '{}',
true
);
if (!is_array($data)) {
$data = [];
}
if ($search !== '') {
$found = false;
foreach ($data as $value) {
if (
stripos(
(string)$value,
$search
) !== false
) {
$found = true;
break;
}
}
if (!$found) {
continue;
}
}
$rows[] = [
'id' => $row['id'],
'created_at' => $row['created_at'],
'data' => $data
];
}
$total = count($rows);
$slice = array_slice(
$rows,
$page * $limit,
$limit
);
respond(
true,
[
'records' => $slice,
'total' => $total,
'page' => $page,
'limit' => $limit
]
);
}
if ($action === 'save_record') {
if (!isAdmin($gid)) {
respond(
false,
['error' => 'Nicht autorisiert.']
);
}
$data = $input['data'] ?? [];
if (!is_array($data)) {
$data = [];
}
$id = (int)($input['id'] ?? 0);
$json = json_encode(
$data,
JSON_UNESCAPED_UNICODE
);
if ($json === false) {
respond(
false,
['error' => 'Datensatz konnte nicht verarbeitet werden.']
);
}
if ($id > 0) {
$stmt = $db->prepare(
"UPDATE vndf_records
SET data = ?,
updated_at = datetime('now','localtime')
WHERE id = ?"
);
$stmt->execute(
[$json, $id]
);
} else {
$stmt = $db->prepare(
"INSERT INTO vndf_records (data)
VALUES (?)"
);
$stmt->execute(
[$json]
);
$id = (int)$db->lastInsertId();
}
respond(
true,
[
'message' => '✅ Gespeichert!',
'id' => $id
]
);
}
if ($action === 'delete_record') {
if (!isAdmin($gid)) {
respond(
false,
['error' => 'Nicht autorisiert.']
);
}
$id = (int)($input['id'] ?? 0);
if ($id > 0) {
$stmt = $db->prepare(
"DELETE FROM vndf_records
WHERE id = ?"
);
$stmt->execute(
[$id]
);
}
respond(
true,
['message' => '🗑 Gelöscht!']
);
}
/*
* ─────────────────────────────────────────────
* CSV-Export
* ─────────────────────────────────────────────
*/
if ($action === 'export_csv') {
if (!isAdmin($gid)) {
respond(
false,
['error' => 'Nicht autorisiert.']
);
}
$fields = $db
->query(
"SELECT *
FROM vndf_fields
ORDER BY sort_order, id"
)
->fetchAll(PDO::FETCH_ASSOC);
$records = $db
->query(
"SELECT *
FROM vndf_records
ORDER BY sort_order ASC, id ASC"
)
->fetchAll(PDO::FETCH_ASSOC);
header(
'Content-Type: text/csv; charset=utf-8'
);
header(
'Content-Disposition: attachment; filename="' .
$gid .
'_export.csv"'
);
$out = fopen(
'php://output',
'w'
);
if ($out === false) {
respond(
false,
['error' => 'CSV-Datei konnte nicht erstellt werden.']
);
}
/*
* UTF-8-BOM für Excel.
*/
fprintf(
$out,
chr(0xEF) .
chr(0xBB) .
chr(0xBF)
);
fputcsv(
$out,
array_column($fields, 'label'),
';'
);
foreach ($records as $record) {
$data = json_decode(
$record['data'] ?? '{}',
true
);
if (!is_array($data)) {
$data = [];
}
$csvRow = array_map(
static function (array $field) use ($data): string {
return (string)(
$data[$field['field_key']] ?? ''
);
},
$fields
);
fputcsv(
$out,
$csvRow,
';'
);
}
fclose($out);
exit;
}
/*
* ─────────────────────────────────────────────
* Bild hochladen
* ─────────────────────────────────────────────
*/
if ($action === 'upload_image') {
if (!isAdmin($gid)) {
respond(
false,
['error' => 'Nicht autorisiert.']
);
}
if (
!isset($_FILES['img'])
|| !is_array($_FILES['img'])
|| empty($_FILES['img']['tmp_name'])
) {
respond(
false,
['error' => 'Keine Datei.']
);
}
$uploadError = (int)(
$_FILES['img']['error']
?? UPLOAD_ERR_NO_FILE
);
if ($uploadError !== UPLOAD_ERR_OK) {
respond(
false,
['error' => 'Fehler beim Hochladen der Datei. Fehlercode: ' . $uploadError]
);
}
$tmpFile = (string)$_FILES['img']['tmp_name'];
if (!is_uploaded_file($tmpFile)) {
respond(
false,
['error' => 'Ungültige Upload-Datei.']
);
}
/*
* Dateityp anhand des tatsächlichen MIME-Typs prüfen.
*/
$allowedTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp'
];
$mimeType = '';
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo !== false) {
$detectedType = finfo_file(
$finfo,
$tmpFile
);
if (is_string($detectedType)) {
$mimeType = $detectedType;
}
finfo_close($finfo);
}
}
/*
* Fallback, falls Fileinfo nicht verfügbar ist.
*/
if ($mimeType === '') {
$imageInfo = @getimagesize($tmpFile);
if (
is_array($imageInfo)
&& isset($imageInfo['mime'])
) {
$mimeType = (string)$imageInfo['mime'];
}
}
if (!isset($allowedTypes[$mimeType])) {
respond(
false,
['error' => 'Ungültiges Bildformat.']
);
}
$extension = $allowedTypes[$mimeType];
/*
* Physischer Upload-Ordner:
* assets/vn-dataform/uploads/<gid>/
*/
$uploadDir = __DIR__
. '/uploads/'
. $gid;
if (
!is_dir($uploadDir)
&& !mkdir($uploadDir, 0755, true)
&& !is_dir($uploadDir)
) {
respond(
false,
['error' => 'Upload-Ordner konnte nicht angelegt werden.']
);
}
/*
* Sicheren, eindeutigen Dateinamen erzeugen.
*/
try {
$randomName = bin2hex(
random_bytes(8)
);
} catch (Throwable $e) {
$randomName = uniqid('', true);
$randomName = preg_replace(
'/[^a-z0-9]/i',
'',
$randomName
);
}
$fileName = 'img_'
. $randomName
. '.'
. $extension;
$target = $uploadDir
. '/'
. $fileName;
if (
!move_uploaded_file(
$tmpFile,
$target
)
) {
respond(
false,
['error' => 'Upload fehlgeschlagen.']
);
}
/*
* Öffentliche URL aus dem URL-Pfad des PHP-Scripts erzeugen.
*
* Es wird ausdrücklich NICHT mehr mit DOCUMENT_ROOT
* oder realpath() gearbeitet.
*/
$scriptDirectory = getPublicScriptDirectory();
$publicUrl = $scriptDirectory
. '/uploads/'
. rawurlencode($gid)
. '/'
. rawurlencode($fileName);
/*
* Sicherstellen, dass die URL mit genau einem Slash beginnt.
*/
$publicUrl = '/'
. ltrim($publicUrl, '/');
respond(
true,
[
'url' => $publicUrl,
'message' => '✅ Hochgeladen!'
]
);
}
/*
* ─────────────────────────────────────────────
* Passwort ändern
* ─────────────────────────────────────────────
*/
if ($action === 'change_password') {
if (!isAdmin($gid)) {
respond(
false,
['error' => 'Nicht autorisiert.']
);
}
$oldPw = (string)(
$input['old_password'] ?? ''
);
$newPw = (string)(
$input['new_password'] ?? ''
);
$hash = file_exists($pwFile)
? trim((string)file_get_contents($pwFile))
: '';
if (
$hash !== ''
&& !password_verify($oldPw, $hash)
) {
respond(
false,
['error' => 'Altes Passwort falsch.']
);
}
if (strlen($newPw) < 4) {
respond(
false,
['error' => 'Min. 4 Zeichen.']
);
}
$newHash = password_hash(
$newPw,
PASSWORD_BCRYPT
);
if ($newHash === false) {
respond(
false,
['error' => 'Neues Passwort konnte nicht erzeugt werden.']
);
}
if (
file_put_contents(
$pwFile,
$newHash,
LOCK_EX
) === false
) {
respond(
false,
['error' => 'Neues Passwort konnte nicht gespeichert werden.']
);
}
respond(
true,
['message' => '✅ Passwort geändert!']
);
}
/*
* ─────────────────────────────────────────────
* Reihenfolge speichern
* ─────────────────────────────────────────────
*/
if ($action === 'reorder') {
if (!isAdmin($gid)) {
respond(
false,
['error' => 'Nicht autorisiert.']
);
}
$ids = $input['ids'] ?? [];
if (!is_array($ids)) {
$ids = [];
}
$stmt = $db->prepare(
"UPDATE vndf_records
SET sort_order = ?
WHERE id = ?"
);
$db->beginTransaction();
try {
foreach ($ids as $i => $id) {
$stmt->execute(
[
$i,
(int)$id
]
);
}
$db->commit();
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
respond(
false,
['error' => 'Reihenfolge konnte nicht gespeichert werden.']
);
}
respond(
true,
['message' => '✅ Reihenfolge gespeichert!']
);
}
/*
* Keine bekannte Aktion gefunden.
*/
respond(
false,
['error' => 'Unbekannte Aktion: ' . $action]
);

Code: Alles auswählen
window.dfOpenLogin = openLogin;Code: Alles auswählen
<section data-bs-version="5.1" class="vn-dataform" data-showsearch="true" group="Content" plugins="vn-dataform" data-gid="{{formId}}" data-mobiletrigger="{{mobileTrigger}}" data-viewmode="{{viewMode}}" data-perpage="{{perPage}}" data-imgsize="{{imgSize}}" data-imgpos="{{imgPos}}" data-shadowsize="{{shadowSize}}" data-shadowcolor="{{shadowColor}}">
<mbr-parameters>
<header>Formular</header>
<input type="text" title="Formular-ID (eindeutig)" name="formId" value="form-1">
<input type="checkbox" title="Suche anzeigen" name="showSearch" checked>
<select title="Ansicht" name="viewMode">
<option value="cards" selected>Karten</option>
<option value="table">Tabelle</option>
</select>
<input type="range" inline title="Einträge/Seite" name="perPage" min="5" max="50" step="1" value="10">
<header>Handy Admin-Trigger</header>
<select title="Handy Admin-Trigger" name="mobileTrigger">
<option value="shake" selected>Schütteln</option>
<option value="tap4">4x Tippen</option>
<option value="button">⚙️ Button</option>
</select>
<header>Abstände</header>
<input type="range" inline title="Oben" name="paddingTop" min="2" max="12" step="1" value="7">
<input type="range" inline title="Unten" name="paddingBottom" min="2" max="12" step="1" value="2">
<input type="range" inline title="Ecken" name="cardRadius" min="0" max="20" step="2" value="10">
<header>Schatten</header>
<input type="range" inline title="Schatten Stärke" name="shadowSize" min="0" max="40" step="4" value="8">
<input type="color" title="Schatten Farbe" name="shadowColor" value="#ffffff" selected>
<header>Bilder</header>
<input type="range" inline title="Bildgröße (px)" name="imgSize" min="60" max="300" step="10" value="110">
<select title="Bild-Position" name="imgPos">
<option value="top" selected>Oben in Karte</option>
<option value="row">Als Zeile</option>
</select>
<header>Schriftgrößen</header>
<input type="range" inline title="Label (px)" name="labelSize" min="10" max="22" step="1" value="18">
<input type="range" inline title="Wert (px)" name="valueSize" min="10" max="22" step="1" value="21">
<input type="range" inline title="Header (px)" name="headerSize" min="12" max="30" step="1" value="21">
<header>Farben</header>
<input type="color" title="Hintergrund" name="bgColor" value="#0a0a0a" selected>
<input type="color" title="Karte" name="cardColor" value="#272727" selected>
<input type="color" title="Kopf Hintergrund" name="headBg" value="#08378e" selected>
<input type="color" title="Kopf Text" name="headText" value="#ffffff" selected>
<input type="color" title="Label-Farbe" name="labelColor" value="#bed3f9" selected>
<input type="color" title="Wert-Farbe" name="valueColor" value="#e0e0e0" selected>
<input type="color" title="Rahmen" name="borderColor" value="#222222" selected>
<input type="color" title="Akzent" name="accentColor" value="#0d41a0" selected>
<input type="color" title="Suche Hintergrund" name="searchBg" value="#001a47" selected>
</mbr-parameters>
<div class="df-wrap">
<!-- Suchbereich -->
<div class="df-search-wrap" id="df-search-wrap">
<input type="text" id="df-search" class="df-search-inp" placeholder="🔍 Suche…" autocomplete="off" spellcheck="false">
</div>
<!-- Ergebnisse -->
<div id="df-results"></div>
<!-- Admin Panel -->
<div id="df-admin" style="display:none;">
<!-- Tabs -->
<div class="df-tabs">
<button class="df-tab df-tab-active" data-tab="records" onclick="dfTab('records')">📋 Einträge</button>
<button class="df-tab" data-tab="fields" onclick="dfTab('fields')">🔧 Felder</button>
<button class="df-tab" data-tab="new" onclick="dfTab('new')">➕ Neu</button>
</div>
<!-- Tab: Einträge -->
<div id="df-tab-records" class="df-tab-content">
<div id="df-admin-list"></div>
</div>
<!-- Tab: Felder definieren -->
<div id="df-tab-fields" class="df-tab-content" style="display:none;">
<div style="margin-bottom:0.75rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
<button class="df-btn-accent" onclick="dfAddField()">+ Feld</button>
<button class="df-btn-primary" onclick="dfSaveFields()">💾 Felder speichern</button>
</div>
<div id="df-field-list"></div>
</div>
<!-- Tab: Neuer Eintrag -->
<div id="df-tab-new" class="df-tab-content" style="display:none;max-height:60vh;overflow-y:auto;">
<div id="df-new-form"></div>
<button class="df-btn-primary" style="margin-top:1rem;" onclick="dfSaveRecord()">💾 Speichern</button>
</div>
<div style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
<a id="df-export-link" href="#" class="df-btn-sm" onclick="dfExport(event)">⬇ CSV</a>
<button class="df-btn-ghost" onclick="document.getElementById('df-pw-modal').style.display='flex'">🔑 Passwort</button>
<button class="df-btn-ghost" onclick="dfLogout()">Ausloggen</button>
</div>
</div>
<!-- Dummy im Editor -->
<div id="df-dummy">
<div class="df-card">
<div class="df-card-head">Max Mustermann</div>
<div class="df-card-body">
<div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">max@beispiel.de</span></div>
<div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 123 456789</span></div>
<div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">Stuttgart</span></div>
</div>
</div>
<div class="df-card">
<div class="df-card-head">Erika Musterfrau</div>
<div class="df-card-body">
<div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">erika@beispiel.de</span></div>
<div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 987 654321</span></div>
<div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">München</span></div>
</div>
</div>
<p class="df-hint">📊 Vorschau – echte Daten nach Publish</p>
</div>
</div>
<!-- Login Modal -->
<div id="df-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
<div class="df-modal-box">
<h3 id="df-modal-title" style="margin:0 0 0.75rem;font-size:1rem;">🔐 Admin-Login</h3>
<input type="password" id="df-pw" class="df-inp" placeholder="Passwort">
<p id="df-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
<button id="df-login-btn" class="df-btn-primary" style="flex:1;">Anmelden</button>
<button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-modal').style.display='none'">Abbrechen</button>
</div>
</div>
</div>
<!-- Passwort-Modal -->
<div id="df-pw-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
<div class="df-modal-box">
<h3 style="margin:0 0 0.75rem;font-size:1rem;">🔑 Passwort ändern</h3>
<input type="password" id="df-old-pw" class="df-inp" placeholder="Altes Passwort" style="margin-bottom:0.5rem;">
<input type="password" id="df-new-pw" class="df-inp" placeholder="Neues Passwort (min. 4 Zeichen)" style="margin-bottom:0.5rem;">
<input type="password" id="df-new-pw2" class="df-inp" placeholder="Neues Passwort bestätigen">
<p id="df-pw-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
<button onclick="dfChangePw()" class="df-btn-primary" style="flex:1;">Speichern</button>
<button onclick="document.getElementById('df-pw-modal').style.display='none'" class="df-btn-ghost" style="flex:1;">Abbrechen</button>
</div>
</div>
</div>
<div id="df-toast" style="display:none;position:fixed;bottom:1.5rem;right:1.5rem;padding:0.75rem 1.25rem;border-radius:8px;z-index:99999;font-size:0.88rem;font-weight:600;color:#fff;"></div>
<!-- Edit Modal -->
<div id="df-edit-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99998;align-items:center;justify-content:center;">
<div class="df-modal-box" style="max-width:500px;max-height:85vh;overflow-y:auto;">
<h3 style="margin:0 0 0.75rem;font-size:1rem;">✏️ Eintrag bearbeiten</h3>
<div id="df-edit-form"></div>
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
<button class="df-btn-primary" style="flex:1;" onclick="dfSaveEdit()">💾 Speichern</button>
<button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-edit-modal').style.display='none'">Abbrechen</button>
</div>
</div>
</div>
<script>
(function(){
var sec = document.currentScript ? document.currentScript.closest('section.vn-dataform') : document.querySelector('section.vn-dataform');
if (!sec) return;
var inBuilder = (function(){
if (document.querySelector('html').classList.contains('is-builder')) return true;
if (window.location.port === '409' || window.location.protocol === 'file:') return true;
var all = document.querySelectorAll('link[href],script[src]');
for (var i=0;i<all.length;i++){
var h=all[i].getAttribute('href')||all[i].getAttribute('src')||'';
if (h.indexOf('file:')===0||h.indexOf('AppData')!==-1||h.indexOf('Mobirise')!==-1) return true;
}
return false;
})();
if (inBuilder) return;
// Live: Dummy verstecken
var dummy = document.getElementById('df-dummy');
if (dummy) dummy.style.display = 'none';
// Loading-Div dynamisch einfügen
var ld = document.createElement('div');
ld.id = 'df-loading';
ld.className = 'df-loading';
ld.textContent = '⟳ Lade…';
document.getElementById('df-results').before(ld);
var GID = sec.dataset.gid || 'form-1';
var VIEWMODE = sec.dataset.viewmode || 'cards';
var PERPAGE = parseInt(sec.dataset.perpage) || 10;
var SEARCH = sec.dataset.showsearch === 'true' || sec.dataset.showsearch === '1';
var IMGSIZE = parseInt(sec.dataset.imgsize) || 120;
var IMGPOS = sec.dataset.imgpos || 'top';
var isAdmin = false;
var fields = [];
var curPage = 0;
var editId = 0;
var ep = (function(){
var links = document.querySelectorAll('link[href]');
for (var i=0;i<links.length;i++){
var h=links[i].getAttribute('href')||'';
if (h.indexOf('http')===0 && h.indexOf('assets/')!==-1)
return h.split('assets/')[0]+'assets/vn-dataform/api.php';
}
return 'assets/vn-dataform/api.php';
})();
function api(payload, cb) {
fetch(ep + '?action=' + payload.action + '&gid=' + encodeURIComponent(GID), {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
}).then(function(r){ return r.json(); }).then(cb).catch(function(e){ cb({success:false,error:String(e)}); });
}
function toast(msg, err) {
var t = document.getElementById('df-toast');
t.textContent = msg; t.style.display = 'block';
t.style.background = err ? '#e74c3c' : '#1d9e75';
setTimeout(function(){ t.style.display='none'; }, 3000);
}
function esc(s){ return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
function fmtDate(val) {
if (!val) return '';
// YYYY-MM-DD → TT.MM.JJJJ
var m = val.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (m) return m[3] + '.' + m[2] + '.' + m[1];
return val;
}
// ── Felder laden ──────────────────────────────────
function loadFields(cb) {
api({action:'get_fields'}, function(d){
fields = d.fields || [];
if (cb) cb();
});
}
// ── Suchefeld ─────────────────────────────────────
function initSearch() {
var srch = document.getElementById('df-search');
if (!srch) return;
// Suchfeld immer zurücksetzen
srch.value = "";
srch.setAttribute("autocomplete", "off");
srch.setAttribute("autocorrect", "off");
srch.setAttribute("autocapitalize", "off");
srch.setAttribute("spellcheck", "false");
// Browser-Autofill zusätzlich verhindern
setTimeout(function () {
srch.value = "";
}, 50);
// Suche verstecken wenn Checkbox aus
if (!SEARCH) {
var wrap = document.getElementById('df-search-wrap');
if (wrap) wrap.style.display = 'none';
return;
}
srch.oninput = function () {
curPage = 0;
loadRecords();
};
}
// ── Datensätze laden und anzeigen ─────────────────
function loadRecords() {
var search = (document.getElementById('df-search')||{value:''}).value;
api({action:'get_records', search:search, page:parseInt(curPage), limit:parseInt(PERPAGE)}, function(d){
renderResults(d.records||[], d.total||0);
});
}
function renderResults(records, total) {
console.log("Seite:", curPage, "Records:", records.length, "Total:", total);
var ldEl = document.getElementById('df-loading');
if (ldEl) ldEl.style.display = 'none';
var div = document.getElementById('df-results');
if (!records.length) {
if (total > 0 && curPage > 0) {
// Seite existiert nicht mehr → zurück zu Seite 0
curPage = 0;
loadRecords();
return;
}
div.innerHTML = '<p class="df-hint">Keine Einträge gefunden.</p>';
document.getElementById('df-pager').innerHTML = '';
return;
}
var listFields = fields.filter(function(f){ return f.show_in_list == 1; });
var html = '';
if (VIEWMODE === 'table') {
var tableFields = fields.length ? fields : listFields;
html = '<div class="df-table-wrap"><table class="df-table"><thead><tr>';
tableFields.forEach(function(f){ html += '<th>'+esc(f.label)+'</th>'; });
if (isAdmin) html += '<th></th>';
html += '</tr></thead><tbody>';
records.forEach(function(r){
html += '<tr>';
tableFields.forEach(function(f){
var tdval = r.data[f.field_key]||'';
var tdhtml = f.type==='image' && tdval
? '<img src="'+tdval+'" style="height:'+Math.round(IMGSIZE*0.5)+'px;max-width:120px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+tdval+'">'
: (f.type==='date' ? fmtDate(tdval) : esc(tdval));
html += '<td>'+tdhtml+'</td>';
});
if (isAdmin) html += '<td><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></td>';
html += '</tr>';
});
html += '</tbody></table></div>';
} else {
html = '<div class="df-cards">';
records.forEach(function(r){
var firstField = listFields[0];
var title = firstField ? esc(r.data[firstField.field_key]||'—') : '#'+r.id;
// Bild-Felder für diese Karte
var imgField = fields.find(function(f){ return f.type==='image' && r.data[f.field_key]; });
var imgUrl = imgField ? r.data[imgField.field_key] : '';
html += '<div class="df-card">';
// Bild oben
if (imgUrl && IMGPOS==='top') {
html += '<div style="overflow:hidden;max-height:'+IMGSIZE+'px;cursor:zoom-in;" onclick="dfLightbox(this.dataset.lbsrc)" data-lbsrc="'+imgUrl+'">'
+ '<img src="'+imgUrl+'" class="df-card-img" style="width:100%;height:'+IMGSIZE+'px;object-fit:cover;display:block;">'
+ '</div>';
}
html += '<div class="df-card-head">'+title;
if (isAdmin) html += '<span style="float:right;"><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></span>';
html += '</div><div class="df-card-body">';
listFields.slice(1).forEach(function(f){
var val = r.data[f.field_key]||'';
if (!val) return;
var link = '';
if (f.type==='email') link='<a href="mailto:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
else if (f.type==='tel') link='<a href="tel:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
else if (f.type==='url') link='<a href="'+esc(val)+'" target="_blank" style="color:inherit;">'+esc(val)+'</a>';
else if (f.type==='image') {
if (IMGPOS==='top') return; // schon oben gezeigt
link='<img src="'+val+'" style="max-width:100%;max-height:'+IMGSIZE+'px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+val+'">';
}
else if (f.type==='date') link=fmtDate(val);
else link=esc(val);
html += '<div class="df-row"><span class="df-lbl">'+esc(f.label)+'</span><span class="df-val">'+link+'</span></div>';
});
html += '</div></div>';
});
html += '</div>';
}
// ---------------- Pagination ----------------
var pages = Math.ceil(total / PERPAGE);
if (pages > 1) {
var phtml = '<div class="df-pager"><div class="df-pager-inner">';
// Erste
phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage(0)')+'">« Erste</button>';
// Zurück
phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage('+(curPage-1)+')')+'">‹ Zurück</button>';
// Seitenbereich
var start = Math.max(0, curPage-2);
var end = Math.min(pages-1, curPage+2);
if (start > 0) {
phtml += '<button class="df-page-btn" onclick="dfGoPage(0)">1</button>';
if (start > 1)
phtml += '<span class="df-page-dots">…</span>';
}
for (var i=start;i<=end;i++) {
phtml += '<button class="df-page-btn'+(i==curPage?' df-page-active':'')+'" onclick="dfGoPage('+i+')">'+(i+1)+'</button>';
}
if (end < pages-1) {
if (end < pages-2)
phtml += '<span class="df-page-dots">…</span>';
phtml += '<button class="df-page-btn" onclick="dfGoPage('+(pages-1)+')">'+pages+'</button>';
}
// Weiter
phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(curPage+1)+')')+'">Weiter ›</button>';
// Letzte
phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(pages-1)+')')+'">Letzte »</button>';
phtml += '</div></div>';
html += phtml;
}
console.log(html);
div.innerHTML = html;
// Lightbox-Delegation
div.querySelectorAll('img.df-lb-img').forEach(function(img){
img.style.cursor = 'zoom-in';
img.onclick = function(){ dfLightbox(this.dataset.lbsrc); };
});
}
window.dfGoPage = function(p){
if (p < 0) p = 0;
curPage = p;
loadRecords();
var el = document.getElementById("df-results");
if(el){
el.scrollIntoView({
behavior:"smooth",
block:"start"
});
}
};
// ── Admin: Datensatz bearbeiten ───────────────────
window.dfEditRecord = function(id) {
editId = id;
api({action:'get_records', page:0, limit:9999}, function(d){
var rec = (d.records||[]).find(function(r){ return r.id===id; });
if (!rec) return;
var html = '';
fields.forEach(function(f){
var v = esc(rec.data[f.field_key]||'');
var einp;
if (f.type==='textarea') {
einp = '<textarea class="df-inp" id="dfe_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;">'+v+'</textarea>';
} else if (f.type==='image') {
einp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
+ (v ? '<img src="'+v+'" style="max-height:60px;border-radius:4px;object-fit:cover;margin-bottom:4px;">' : '')
+ '<input type="file" accept="image/*" class="df-inp" id="dfe_file_'+f.field_key+'" style="padding:4px;">'
+ '<input type="hidden" id="dfe_'+f.field_key+'" value="'+v+'">'
+ '</div>';
} else {
einp = '<input type="'+f.type+'" class="df-inp" id="dfe_'+f.field_key+'" value="'+v+'">';
}
html += '<label class="df-field-lbl">'+esc(f.label)+einp+'</label>';
});
document.getElementById('df-edit-form').innerHTML = html;
document.getElementById('df-edit-modal').style.display = 'flex';
});
};
window.dfSaveEdit = function() {
uploadImages('dfe_', function(){
var data = {};
fields.forEach(function(f){
var el = document.getElementById('dfe_'+f.field_key);
if (el) data[f.field_key] = el.value;
});
api({action:'save_record', id:editId, data:data}, function(d){
if (d.success) {
toast(d.message||'✅ Gespeichert!');
document.getElementById('df-edit-modal').style.display='none';
loadRecords();
renderAdminList();
} else toast(d.error||'Fehler', true);
});
});
};
window.dfDeleteRecord = function(id) {
if (!confirm('Eintrag löschen?')) return;
api({action:'delete_record', id:id}, function(d){
if (d.success) { toast(d.message||'Gelöscht!'); loadRecords(); renderAdminList(); }
else toast(d.error||'Fehler', true);
});
};
// ── Admin: Eintrags-Liste ─────────────────────────
function renderAdminList() {
api({action:'get_records', page:0, limit:999}, function(d){
var records = d.records||[];
var div = document.getElementById('df-admin-list');
if (!records.length) { div.innerHTML='<p class="df-hint">Noch keine Einträge.</p>'; return; }
var html = '<table class="df-table"><thead><tr>';
fields.slice(0,3).forEach(function(f){ html+='<th>'+esc(f.label)+'</th>'; });
html += '<th></th></tr></thead><tbody>';
records.forEach(function(r){
html += '<tr data-id="'+r.id+'">';
fields.slice(0,3).forEach(function(f){ html+='<td>'+esc(r.data[f.field_key]||'')+'</td>'; });
html += '<td style="white-space:nowrap;">'
+'<button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> '
+'<button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button>'
+'</td></tr>';
});
html += '</tbody></table>';
div.innerHTML = html;
// Drag & Drop Zeilen
var dragSrcRow = null;
div.querySelectorAll('tbody tr').forEach(function(tr){
tr.draggable = true;
tr.style.cursor = 'grab';
tr.addEventListener('dragstart',function(){ dragSrcRow=tr; setTimeout(function(){tr.style.opacity='0.4';},0); });
tr.addEventListener('dragend',function(){ tr.style.opacity='1'; div.querySelectorAll('tbody tr').forEach(function(r){r.style.outline='';}); });
tr.addEventListener('dragover',function(e){ e.preventDefault(); tr.style.outline='2px solid #1d9e75'; });
tr.addEventListener('dragleave',function(){ tr.style.outline=''; });
tr.addEventListener('drop',function(e){
e.preventDefault(); tr.style.outline='';
if (!dragSrcRow || dragSrcRow===tr) return;
var tbody = tr.closest('tbody');
var rows = Array.from(tbody.querySelectorAll('tr'));
var si = rows.indexOf(dragSrcRow), ti = rows.indexOf(tr);
if (si < ti) tbody.insertBefore(dragSrcRow, tr.nextSibling);
else tbody.insertBefore(dragSrcRow, tr);
dragSrcRow = null;
// Neue Reihenfolge speichern
var newOrder = Array.from(tbody.querySelectorAll('tr')).map(function(r){
return parseInt(r.dataset.id);
}).filter(function(id){ return !isNaN(id); });
if (newOrder.length) {
api({action:'reorder', ids:newOrder}, function(d){
if (d.success) toast('✅ Reihenfolge gespeichert!');
});
}
});
});
});
}
// ── Admin: Felder ─────────────────────────────────
window.dfTab = function(tab) {
['records','fields','new'].forEach(function(t){
document.getElementById('df-tab-'+t).style.display = t===tab ? 'block' : 'none';
var btn = document.querySelector('[data-tab="'+t+'"]');
if (btn) btn.classList.toggle('df-tab-active', t===tab);
});
if (tab==='records') renderAdminList();
if (tab==='fields') renderFieldEditor();
if (tab==='new') renderNewForm();
};
function renderFieldEditor() {
var html = '';
fields.forEach(function(f,i){
html += '<div class="df-field-row" data-idx="'+i+'">'
+ '<span class="df-drag-handle">⠿</span>'
+ '<input type="text" class="df-inp df-fl-name" value="'+esc(f.label)+'" placeholder="Feldname">'
+ '<select class="df-inp df-fl-type">'
+ ['text','email','tel','url','number','date','textarea','image'].map(function(t){
return '<option value="'+t+'"'+(f.type===t?' selected':'')+'>'+t+'</option>';
}).join('')
+ '</select>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
+ '<input type="checkbox" class="df-fl-req"'+(f.required==1?' checked':'')+'> Pflicht</label>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
+ '<input type="checkbox" class="df-fl-show"'+(f.show_in_list==1?' checked':'')+'> Anzeigen</label>'
+ '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>'
+ '</div>';
});
document.getElementById('df-field-list').innerHTML = html;
}
window.dfAddField = function() {
var li = document.getElementById('df-field-list');
var div = document.createElement('div');
div.className = 'df-field-row';
div.innerHTML = '<span class="df-drag-handle">⠿</span>'
+ '<input type="text" class="df-inp df-fl-name" placeholder="Feldname">'
+ '<select class="df-inp df-fl-type">'
+ ['text','email','tel','url','number','date','textarea','image'].map(function(t){
return '<option value="'+t+'">'+t+'</option>';
}).join('')
+ '</select>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-req"> Pflicht</label>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-show" checked> Anzeigen</label>'
+ '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>';
li.appendChild(div);
};
window.dfSaveFields = function() {
var rows = document.querySelectorAll('#df-field-list .df-field-row');
var newFields = [];
rows.forEach(function(row){
var name = row.querySelector('.df-fl-name').value.trim();
if (!name) return;
newFields.push({
label: name,
type: row.querySelector('.df-fl-type').value,
required: row.querySelector('.df-fl-req').checked ? 1 : 0,
show_in_list:row.querySelector('.df-fl-show').checked ? 1 : 0,
});
});
api({action:'save_fields', fields:newFields}, function(d){
if (d.success) {
toast(d.message||'✅ Felder gespeichert!');
loadFields(function(){ loadRecords(); renderNewForm(); });
} else toast(d.error||'Fehler', true);
});
};
// ── Admin: Neuer Eintrag ──────────────────────────
function renderNewForm() {
editId = 0;
var html = '';
fields.forEach(function(f){
var inp;
if (f.type==='textarea') {
inp = '<textarea class="df-inp" id="dfn_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;"></textarea>';
} else if (f.type==='image') {
inp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
+ '<input type="file" accept="image/*" class="df-inp" id="dfn_file_'+f.field_key+'" style="padding:4px;">'
+ '<input type="hidden" id="dfn_'+f.field_key+'">'
+ '<img id="dfn_prev_'+f.field_key+'" src style="display:none;max-height:80px;border-radius:4px;margin-top:4px;object-fit:cover;">'
+ '</div>';
} else {
inp = '<input type="'+f.type+'" class="df-inp" id="dfn_'+f.field_key+'">';
}
html += '<label class="df-field-lbl">'+esc(f.label)+(f.required==1?' <span style="color:#e74c3c;">*</span>':'')+inp+'</label>';
});
if (!html) html = '<p class="df-hint">Zuerst Felder unter 🔧 Felder anlegen!</p>';
document.getElementById('df-new-form').innerHTML = html;
}
window.dfSaveRecord = function() {
uploadImages('dfn_', function(){
var data = {};
fields.forEach(function(f){
var el = document.getElementById('dfn_'+f.field_key);
if (el) data[f.field_key] = el.value;
});
api({action:'save_record', id:0, data:data}, function(d){
if (d.success) {
toast(d.message||'✅ Gespeichert!');
renderNewForm();
loadRecords();
} else toast(d.error||'Fehler', true);
});
});
};
// Alle Bild-Felder vor dem Speichern hochladen
function uploadImages(prefix, cb) {
var imgFields = fields.filter(function(f){ return f.type==='image'; });
var pending = 0;
if (!imgFields.length) { cb(); return; }
imgFields.forEach(function(f){
var fileInp = document.getElementById(prefix+'file_'+f.field_key);
if (!fileInp || !fileInp.files || !fileInp.files[0]) return;
pending++;
var fd = new FormData();
fd.append('img', fileInp.files[0]);
fetch(ep + '?action=upload_image&gid=' + encodeURIComponent(GID), {method:'POST', body:fd})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success) {
var hidden = document.getElementById(prefix+f.field_key);
if (hidden) hidden.value = d.url;
}
pending--;
if (pending===0) cb();
})
.catch(function(){ pending--; if(pending===0) cb(); });
});
if (pending===0) cb();
}
window.dfLightbox = function(url) {
var lb = document.getElementById('df-lightbox');
var img = document.getElementById('df-lightbox-img');
if (!lb || !img) return;
img.src = url;
lb.style.display = 'flex';
};
document.addEventListener('keydown', function(e){
if (e.key==='Escape') {
var lb = document.getElementById('df-lightbox');
if (lb) lb.style.display = 'none';
}
});
window.dfExport = function(e) {
e.preventDefault();
window.open(ep.replace('api.php','') + '?action=export_csv&gid='+encodeURIComponent(GID), '_blank');
};
window.dfChangePw = function() {
var old = document.getElementById('df-old-pw').value;
var nw = document.getElementById('df-new-pw').value;
var nw2 = document.getElementById('df-new-pw2').value;
var err = document.getElementById('df-pw-err');
if (nw !== nw2) { err.textContent='Passwörter stimmen nicht überein.'; err.style.display='block'; return; }
api({action:'change_password', old_password:old, new_password:nw}, function(d){
if (d.success) {
toast(d.message||'✅ Passwort geändert!');
document.getElementById('df-pw-modal').style.display='none';
['df-old-pw','df-new-pw','df-new-pw2'].forEach(function(id){
var el=document.getElementById(id); if(el) el.value='';
});
} else {
err.textContent = d.error||'Fehler';
err.style.display = 'block';
}
});
};
window.dfLogout = function() {
api({action:'logout'}, function(){
isAdmin=false;
document.getElementById('df-admin').style.display='none';
loadRecords();
});
};
// ── Login ─────────────────────────────────────────
function openLogin() {
api({action:'check'}, function(d){
if (d.loggedIn) { isAdmin=true; showAdmin(); return; }
var modal = document.getElementById('df-modal');
var title = document.getElementById('df-modal-title');
var err = document.getElementById('df-err');
var pw = document.getElementById('df-pw');
if (!modal) return;
if (title) title.textContent = d.firstRun ? '⚙️ Erst-Einrichtung' : '🔐 Admin-Login';
if (err) err.style.display = 'none';
if (pw) pw.value = '';
modal.style.display = 'flex';
setTimeout(function(){ if(pw) pw.focus(); }, 100);
});
}
/* EDIT Tommy - Login auch über externen Textlink aufrufbar machen */
window.dfOpenLogin = openLogin;
function showAdmin() {
document.getElementById('df-admin').style.display = 'block';
dfTab('records');
}
document.getElementById('df-login-btn').onclick = function(){
var pw = document.getElementById('df-pw');
var err = document.getElementById('df-err');
api({action:'login', password: pw ? pw.value : ''}, function(d){
if (d.success) {
isAdmin = true;
document.getElementById('df-modal').style.display = 'none';
showAdmin();
} else {
if (err) { err.textContent = d.error||'Falsches Passwort.'; err.style.display='block'; }
}
});
};
var pwEl = document.getElementById('df-pw');
if (pwEl) pwEl.addEventListener('keypress', function(e){ if(e.key==='Enter') document.getElementById('df-login-btn').click(); });
document.addEventListener('dblclick', function(e){ if(e.ctrlKey){ e.preventDefault(); openLogin(); }});
document.addEventListener('keydown', function(e){ if(e.ctrlKey&&e.shiftKey&&(e.key==='A'||e.key==='a')){ e.preventDefault(); openLogin(); }});
// Handy-Trigger
var mt = sec.dataset.mobiletrigger || 'shake';
var isTouch = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
if (mt==='button' && isTouch) {
var fab = document.createElement('button');
fab.innerHTML='⚙️';
fab.style.cssText='position:fixed;bottom:1.5rem;left:1rem;width:44px;height:44px;border-radius:50%;background:#1d9e75;border:none;color:#fff;font-size:1.2rem;cursor:pointer;z-index:9998;';
fab.onclick=openLogin;
document.body.appendChild(fab);
} else if (mt==='tap4') {
var _tc=0,_tt=null;
sec.addEventListener('touchend',function(){
_tc++;clearTimeout(_tt);
_tt=setTimeout(function(){_tc=0;},1500);
if(_tc>=4){_tc=0;openLogin();}
});
} else if (mt==='shake') {
var _sx=0,_sy=0,_sz=0,_st=0;
if (window.DeviceMotionEvent) {
window.addEventListener('devicemotion',function(e){
var a=e.accelerationIncludingGravity;if(!a)return;
var dv=Math.abs((a.x||0)-_sx)+Math.abs((a.y||0)-_sy)+Math.abs((a.z||0)-_sz);
_sx=a.x||0;_sy=a.y||0;_sz=a.z||0;
if(dv>25){var n=Date.now();if(n-_st>1500){_st=n;openLogin();}}
});
}
}
// Start
loadFields(function(){
initSearch();
loadRecords();
});
})();
</script>
<!-- Lightbox -->
<div id="df-lightbox" style="display:none;position:fixed;inset:0;background:#000000ee;z-index:99999;align-items:center;justify-content:center;cursor:zoom-out;" onclick="document.getElementById('df-lightbox').style.display='none'">
<img id="df-lightbox-img" src style="max-width:92%;max-height:92%;border-radius:10px;box-shadow:0 0 40px #000;object-fit:contain;">
</div>
</section>




Code: Alles auswählen
<section data-bs-version="5.1" class="vn-dataform" data-showsearch="true" group="Content" plugins="vn-dataform" data-gid="{{formId}}" data-mobiletrigger="{{mobileTrigger}}" data-viewmode="{{viewMode}}" data-perpage="{{perPage}}" data-imgsize="{{imgSize}}" data-imgpos="{{imgPos}}" data-shadowsize="{{shadowSize}}" data-shadowcolor="{{shadowColor}}">
<mbr-parameters>
<header>Formular</header>
<input type="text" title="Formular-ID (eindeutig)" name="formId" value="form-1">
<input type="checkbox" title="Suche anzeigen" name="showSearch" checked>
<select title="Ansicht" name="viewMode">
<option value="cards" selected>Karten</option>
<option value="table">Tabelle</option>
</select>
<input type="range" inline title="Einträge/Seite" name="perPage" min="4" max="20" step="1" value="6">
<header>Handy Admin-Trigger</header>
<select title="Handy Admin-Trigger" name="mobileTrigger">
<option value="shake" selected>Schütteln</option>
<option value="tap4">4x Tippen</option>
<option value="button">⚙️ Button</option>
</select>
<header>Abstände</header>
<input type="range" inline title="Oben" name="paddingTop" min="2" max="12" step="1" value="7">
<input type="range" inline title="Unten" name="paddingBottom" min="2" max="12" step="1" value="2">
<input type="range" inline title="Ecken" name="cardRadius" min="0" max="20" step="2" value="10">
<header>Schatten</header>
<input type="range" inline title="Schatten Stärke" name="shadowSize" min="0" max="40" step="4" value="20">
<input type="color" title="Schatten Farbe" name="shadowColor" value="#bbbbbb" selected>
<header>Bilder</header>
<input type="range" inline title="Bildgröße (px)" name="imgSize" min="60" max="300" step="10" value="120">
<select title="Bild-Position" name="imgPos">
<option value="top" selected>Oben in Karte</option>
<option value="row">Als Zeile</option>
</select>
<header>Schriftgrößen</header>
<input type="range" inline title="Label (px)" name="labelSize" min="10" max="22" step="1" value="18">
<input type="range" inline title="Wert (px)" name="valueSize" min="10" max="22" step="1" value="21">
<input type="range" inline title="Header (px)" name="headerSize" min="12" max="30" step="1" value="21">
<header>Farben</header>
<input type="color" title="Hintergrund" name="bgColor" value="#0a0a0a" selected>
<input type="color" title="Karte" name="cardColor" value="#272727" selected>
<input type="color" title="Kopf Hintergrund" name="headBg" value="#08378e" selected>
<input type="color" title="Kopf Text" name="headText" value="#ffffff" selected>
<input type="color" title="Label-Farbe" name="labelColor" value="#bed3f9" selected>
<input type="color" title="Wert-Farbe" name="valueColor" value="#e0e0e0" selected>
<input type="color" title="Rahmen" name="borderColor" value="#222222" selected>
<input type="color" title="Akzent" name="accentColor" value="#0d41a0" selected>
<input type="color" title="Suche Hintergrund" name="searchBg" value="#001a47" selected>
</mbr-parameters>
<div class="df-wrap">
<!-- Suchbereich -->
<div class="df-search-wrap" id="df-search-wrap">
<input type="text" id="df-search" class="df-search-inp" placeholder="🔍 Suche…" autocomplete="off" spellcheck="false">
</div>
<!-- Ergebnisse -->
<div id="df-results"></div>
<!-- Admin Panel -->
<div id="df-admin" style="display:none;">
<!-- Tabs -->
<div class="df-tabs">
<button class="df-tab df-tab-active" data-tab="records" onclick="dfTab('records')">📋 Einträge</button>
<button class="df-tab" data-tab="fields" onclick="dfTab('fields')">🔧 Felder</button>
<button class="df-tab" data-tab="new" onclick="dfTab('new')">➕ Neu</button>
</div>
<!-- Tab: Einträge -->
<div id="df-tab-records" class="df-tab-content">
<div id="df-admin-list"></div>
</div>
<!-- Tab: Felder definieren -->
<div id="df-tab-fields" class="df-tab-content" style="display:none;">
<div style="margin-bottom:0.75rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
<button class="df-btn-accent" onclick="dfAddField()">+ Feld</button>
<button class="df-btn-primary" onclick="dfSaveFields()">💾 Felder speichern</button>
</div>
<div id="df-field-list"></div>
</div>
<!-- Tab: Neuer Eintrag -->
<div id="df-tab-new" class="df-tab-content" style="display:none;max-height:60vh;overflow-y:auto;">
<div id="df-new-form"></div>
<button class="df-btn-primary" style="margin-top:1rem;" onclick="dfSaveRecord()">💾 Speichern</button>
</div>
<div style="margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
<a id="df-export-link" href="#" class="df-btn-sm" onclick="dfExport(event)">⬇ CSV</a>
<button class="df-btn-ghost" onclick="document.getElementById('df-pw-modal').style.display='flex'">🔑 Passwort</button>
<button class="df-btn-ghost" onclick="dfLogout()">Ausloggen</button>
</div>
</div>
<!-- Dummy im Editor -->
<div id="df-dummy">
<div class="df-card">
<div class="df-card-head">Max Mustermann</div>
<div class="df-card-body">
<div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">max@beispiel.de</span></div>
<div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 123 456789</span></div>
<div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">Stuttgart</span></div>
</div>
</div>
<div class="df-card">
<div class="df-card-head">Erika Musterfrau</div>
<div class="df-card-body">
<div class="df-row"><span class="df-lbl">E-Mail</span><span class="df-val">erika@beispiel.de</span></div>
<div class="df-row"><span class="df-lbl">Telefon</span><span class="df-val">+49 987 654321</span></div>
<div class="df-row"><span class="df-lbl">Stadt</span><span class="df-val">München</span></div>
</div>
</div>
<p class="df-hint">📊 Vorschau – echte Daten nach Publish</p>
</div>
</div>
<!-- Login Modal -->
<div id="df-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
<div class="df-modal-box">
<h3 id="df-modal-title" style="margin:0 0 0.75rem;font-size:1rem;">🔐 Admin-Login</h3>
<input type="password" id="df-pw" class="df-inp" placeholder="Passwort">
<p id="df-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
<button id="df-login-btn" class="df-btn-primary" style="flex:1;">Anmelden</button>
<button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-modal').style.display='none'">Abbrechen</button>
</div>
</div>
</div>
<!-- Passwort-Modal -->
<div id="df-pw-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99999;align-items:center;justify-content:center;">
<div class="df-modal-box">
<h3 style="margin:0 0 0.75rem;font-size:1rem;">🔑 Passwort ändern</h3>
<input type="password" id="df-old-pw" class="df-inp" placeholder="Altes Passwort" style="margin-bottom:0.5rem;">
<input type="password" id="df-new-pw" class="df-inp" placeholder="Neues Passwort (min. 4 Zeichen)" style="margin-bottom:0.5rem;">
<input type="password" id="df-new-pw2" class="df-inp" placeholder="Neues Passwort bestätigen">
<p id="df-pw-err" style="display:none;color:#e74c3c;font-size:0.82rem;margin:0.4rem 0 0;"></p>
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
<button onclick="dfChangePw()" class="df-btn-primary" style="flex:1;">Speichern</button>
<button onclick="document.getElementById('df-pw-modal').style.display='none'" class="df-btn-ghost" style="flex:1;">Abbrechen</button>
</div>
</div>
</div>
<div id="df-toast" style="display:none;position:fixed;bottom:1.5rem;right:1.5rem;padding:0.75rem 1.25rem;border-radius:8px;z-index:99999;font-size:0.88rem;font-weight:600;color:#fff;"></div>
<!-- Edit Modal -->
<div id="df-edit-modal" style="display:none;position:fixed;inset:0;background:#000000bb;z-index:99998;align-items:center;justify-content:center;">
<div class="df-modal-box" style="max-width:500px;max-height:85vh;overflow-y:auto;">
<h3 style="margin:0 0 0.75rem;font-size:1rem;">✏️ Eintrag bearbeiten</h3>
<div id="df-edit-form"></div>
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
<button class="df-btn-primary" style="flex:1;" onclick="dfSaveEdit()">💾 Speichern</button>
<button class="df-btn-ghost" style="flex:1;" onclick="document.getElementById('df-edit-modal').style.display='none'">Abbrechen</button>
</div>
</div>
</div>
<script>
(function(){
var sec = document.currentScript ? document.currentScript.closest('section.vn-dataform') : document.querySelector('section.vn-dataform');
if (!sec) return;
var inBuilder = (function(){
if (document.querySelector('html').classList.contains('is-builder')) return true;
if (window.location.port === '409' || window.location.protocol === 'file:') return true;
var all = document.querySelectorAll('link[href],script[src]');
for (var i=0;i<all.length;i++){
var h=all[i].getAttribute('href')||all[i].getAttribute('src')||'';
if (h.indexOf('file:')===0||h.indexOf('AppData')!==-1||h.indexOf('Mobirise')!==-1) return true;
}
return false;
})();
if (inBuilder) return;
// Live: Dummy verstecken
var dummy = document.getElementById('df-dummy');
if (dummy) dummy.style.display = 'none';
// Loading-Div dynamisch einfügen
var ld = document.createElement('div');
ld.id = 'df-loading';
ld.className = 'df-loading';
ld.textContent = '⟳ Lade…';
document.getElementById('df-results').before(ld);
var GID = sec.dataset.gid || 'form-1';
var VIEWMODE = sec.dataset.viewmode || 'cards';
var PERPAGE = parseInt(sec.dataset.perpage) || 10;
var SEARCH = sec.dataset.showsearch === 'true' || sec.dataset.showsearch === '1';
var IMGSIZE = parseInt(sec.dataset.imgsize) || 120;
var IMGPOS = sec.dataset.imgpos || 'top';
var isAdmin = false;
var fields = [];
var curPage = 0;
var editId = 0;
var ep = (function(){
var links = document.querySelectorAll('link[href]');
for (var i=0;i<links.length;i++){
var h=links[i].getAttribute('href')||'';
if (h.indexOf('http')===0 && h.indexOf('assets/')!==-1)
return h.split('assets/')[0]+'assets/vn-dataform/api.php';
}
return 'assets/vn-dataform/api.php';
})();
function api(payload, cb) {
fetch(ep + '?action=' + payload.action + '&gid=' + encodeURIComponent(GID), {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
}).then(function(r){ return r.json(); }).then(cb).catch(function(e){ cb({success:false,error:String(e)}); });
}
function toast(msg, err) {
var t = document.getElementById('df-toast');
t.textContent = msg; t.style.display = 'block';
t.style.background = err ? '#e74c3c' : '#1d9e75';
setTimeout(function(){ t.style.display='none'; }, 3000);
}
function esc(s){ return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
function fmtDate(val) {
if (!val) return '';
// YYYY-MM-DD → TT.MM.JJJJ
var m = val.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (m) return m[3] + '.' + m[2] + '.' + m[1];
return val;
}
// ── Felder laden ──────────────────────────────────
function loadFields(cb) {
api({action:'get_fields'}, function(d){
fields = d.fields || [];
if (cb) cb();
});
}
// ── Suchefeld ─────────────────────────────────────
function initSearch() {
var srch = document.getElementById('df-search');
if (!srch) return;
// Suchfeld immer zurücksetzen
srch.value = "";
srch.setAttribute("autocomplete", "off");
srch.setAttribute("autocorrect", "off");
srch.setAttribute("autocapitalize", "off");
srch.setAttribute("spellcheck", "false");
// Browser-Autofill zusätzlich verhindern
setTimeout(function () {
srch.value = "";
}, 50);
// Suche verstecken wenn Checkbox aus
if (!SEARCH) {
var wrap = document.getElementById('df-search-wrap');
if (wrap) wrap.style.display = 'none';
return;
}
srch.oninput = function () {
curPage = 0;
loadRecords();
};
}
// ── Datensätze laden und anzeigen ─────────────────
function loadRecords() {
var search = (document.getElementById('df-search')||{value:''}).value;
api({action:'get_records', search:search, page:parseInt(curPage), limit:parseInt(PERPAGE)}, function(d){
renderResults(d.records||[], d.total||0);
});
}
function renderResults(records, total) {
console.log("Seite:", curPage, "Records:", records.length, "Total:", total);
var ldEl = document.getElementById('df-loading');
if (ldEl) ldEl.style.display = 'none';
var div = document.getElementById('df-results');
if (!records.length) {
if (total > 0 && curPage > 0) {
// Seite existiert nicht mehr → zurück zu Seite 0
curPage = 0;
loadRecords();
return;
}
div.innerHTML = '<p class="df-hint">Keine Einträge gefunden.</p>';
document.getElementById('df-pager').innerHTML = '';
return;
}
var listFields = fields.filter(function(f){ return f.show_in_list == 1; });
var html = '';
if (VIEWMODE === 'table') {
var tableFields = fields.length ? fields : listFields;
html = '<div class="df-table-wrap"><table class="df-table"><thead><tr>';
tableFields.forEach(function(f){ html += '<th>'+esc(f.label)+'</th>'; });
if (isAdmin) html += '<th></th>';
html += '</tr></thead><tbody>';
records.forEach(function(r){
html += '<tr>';
tableFields.forEach(function(f){
var tdval = r.data[f.field_key]||'';
var tdhtml = f.type==='image' && tdval
? '<img src="'+tdval+'" style="height:'+Math.round(IMGSIZE*0.5)+'px;max-width:120px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+tdval+'">'
: (f.type==='date' ? fmtDate(tdval) : esc(tdval));
html += '<td>'+tdhtml+'</td>';
});
if (isAdmin) html += '<td><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></td>';
html += '</tr>';
});
html += '</tbody></table></div>';
} else {
html = '<div class="df-cards">';
records.forEach(function(r){
var firstField = listFields[0];
var title = firstField ? esc(r.data[firstField.field_key]||'—') : '#'+r.id;
// Bild-Felder für diese Karte
var imgField = fields.find(function(f){ return f.type==='image' && r.data[f.field_key]; });
var imgUrl = imgField ? r.data[imgField.field_key] : '';
html += '<div class="df-card">';
// Bild oben
if (imgUrl && IMGPOS==='top') {
html += '<div style="overflow:hidden;max-height:'+IMGSIZE+'px;cursor:zoom-in;" onclick="dfLightbox(this.dataset.lbsrc)" data-lbsrc="'+imgUrl+'">'
+ '<img src="'+imgUrl+'" class="df-card-img" style="width:100%;height:'+IMGSIZE+'px;object-fit:cover;display:block;">'
+ '</div>';
}
html += '<div class="df-card-head">'+title;
if (isAdmin) html += '<span style="float:right;"><button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> <button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button></span>';
html += '</div><div class="df-card-body">';
listFields.slice(1).forEach(function(f){
var val = r.data[f.field_key]||'';
if (!val) return;
var link = '';
if (f.type==='email') link='<a href="mailto:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
else if (f.type==='tel') link='<a href="tel:'+esc(val)+'" style="color:inherit;">'+esc(val)+'</a>';
else if (f.type==='url') link='<a href="'+esc(val)+'" target="_blank" style="color:inherit;">'+esc(val)+'</a>';
else if (f.type==='image') {
if (IMGPOS==='top') return; // schon oben gezeigt
link='<img src="'+val+'" style="max-width:100%;max-height:'+IMGSIZE+'px;border-radius:4px;object-fit:cover;cursor:zoom-in;" class="df-lb-img" data-lbsrc="'+val+'">';
}
else if (f.type==='date') link=fmtDate(val);
else link=esc(val);
html += '<div class="df-row"><span class="df-lbl">'+esc(f.label)+'</span><span class="df-val">'+link+'</span></div>';
});
html += '</div></div>';
});
html += '</div>';
}
// ---------------- Pagination ----------------
var pages = Math.ceil(total / PERPAGE);
if (pages > 1) {
var phtml = '<div class="df-pager"><div class="df-pager-inner">';
// Erste
phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage(0)')+'">« Erste</button>';
// Zurück
phtml += '<button class="df-page-btn'+(curPage==0?' df-page-disabled':'')+'" onclick="'+(curPage==0?'return false':'dfGoPage('+(curPage-1)+')')+'">‹ Zurück</button>';
// Seitenbereich
var start = Math.max(0, curPage-2);
var end = Math.min(pages-1, curPage+2);
if (start > 0) {
phtml += '<button class="df-page-btn" onclick="dfGoPage(0)">1</button>';
if (start > 1)
phtml += '<span class="df-page-dots">…</span>';
}
for (var i=start;i<=end;i++) {
phtml += '<button class="df-page-btn'+(i==curPage?' df-page-active':'')+'" onclick="dfGoPage('+i+')">'+(i+1)+'</button>';
}
if (end < pages-1) {
if (end < pages-2)
phtml += '<span class="df-page-dots">…</span>';
phtml += '<button class="df-page-btn" onclick="dfGoPage('+(pages-1)+')">'+pages+'</button>';
}
// Weiter
phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(curPage+1)+')')+'">Weiter ›</button>';
// Letzte
phtml += '<button class="df-page-btn'+(curPage>=pages-1?' df-page-disabled':'')+'" onclick="'+(curPage>=pages-1?'return false':'dfGoPage('+(pages-1)+')')+'">Letzte »</button>';
phtml += '</div></div>';
html += phtml;
}
console.log(html);
div.innerHTML = html;
// Lightbox-Delegation
div.querySelectorAll('img.df-lb-img').forEach(function(img){
img.style.cursor = 'zoom-in';
img.onclick = function(){ dfLightbox(this.dataset.lbsrc); };
});
}
window.dfGoPage = function(p){
if (p < 0) p = 0;
curPage = p;
loadRecords();
var el = document.getElementById("df-results");
if(el){
el.scrollIntoView({
behavior:"smooth",
block:"start"
});
}
};
// ── Admin: Datensatz bearbeiten ───────────────────
window.dfEditRecord = function(id) {
editId = id;
api({action:'get_records', page:0, limit:9999}, function(d){
var rec = (d.records||[]).find(function(r){ return r.id===id; });
if (!rec) return;
var html = '';
fields.forEach(function(f){
var v = esc(rec.data[f.field_key]||'');
var einp;
if (f.type==='textarea') {
einp = '<textarea class="df-inp" id="dfe_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;">'+v+'</textarea>';
} else if (f.type==='image') {
einp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
+ (v ? '<img src="'+v+'" style="max-height:60px;border-radius:4px;object-fit:cover;margin-bottom:4px;">' : '')
+ '<input type="file" accept="image/*" class="df-inp" id="dfe_file_'+f.field_key+'" style="padding:4px;">'
+ '<input type="hidden" id="dfe_'+f.field_key+'" value="'+v+'">'
+ '</div>';
} else {
einp = '<input type="'+f.type+'" class="df-inp" id="dfe_'+f.field_key+'" value="'+v+'">';
}
html += '<label class="df-field-lbl">'+esc(f.label)+einp+'</label>';
});
document.getElementById('df-edit-form').innerHTML = html;
document.getElementById('df-edit-modal').style.display = 'flex';
});
};
window.dfSaveEdit = function() {
uploadImages('dfe_', function(){
var data = {};
fields.forEach(function(f){
var el = document.getElementById('dfe_'+f.field_key);
if (el) data[f.field_key] = el.value;
});
api({action:'save_record', id:editId, data:data}, function(d){
if (d.success) {
toast(d.message||'✅ Gespeichert!');
document.getElementById('df-edit-modal').style.display='none';
loadRecords();
renderAdminList();
} else toast(d.error||'Fehler', true);
});
});
};
window.dfDeleteRecord = function(id) {
if (!confirm('Eintrag löschen?')) return;
api({action:'delete_record', id:id}, function(d){
if (d.success) { toast(d.message||'Gelöscht!'); loadRecords(); renderAdminList(); }
else toast(d.error||'Fehler', true);
});
};
// ── Admin: Eintrags-Liste ─────────────────────────
function renderAdminList() {
api({action:'get_records', page:0, limit:999}, function(d){
var records = d.records||[];
var div = document.getElementById('df-admin-list');
if (!records.length) { div.innerHTML='<p class="df-hint">Noch keine Einträge.</p>'; return; }
var html = '<table class="df-table"><thead><tr>';
fields.slice(0,3).forEach(function(f){ html+='<th>'+esc(f.label)+'</th>'; });
html += '<th></th></tr></thead><tbody>';
records.forEach(function(r){
html += '<tr data-id="'+r.id+'">';
fields.slice(0,3).forEach(function(f){ html+='<td>'+esc(r.data[f.field_key]||'')+'</td>'; });
html += '<td style="white-space:nowrap;">'
+'<button class="df-btn-sm" onclick="dfEditRecord('+r.id+')">✏️</button> '
+'<button class="df-btn-sm df-btn-del" onclick="dfDeleteRecord('+r.id+')">🗑</button>'
+'</td></tr>';
});
html += '</tbody></table>';
div.innerHTML = html;
// Drag & Drop Zeilen
var dragSrcRow = null;
div.querySelectorAll('tbody tr').forEach(function(tr){
tr.draggable = true;
tr.style.cursor = 'grab';
tr.addEventListener('dragstart',function(){ dragSrcRow=tr; setTimeout(function(){tr.style.opacity='0.4';},0); });
tr.addEventListener('dragend',function(){ tr.style.opacity='1'; div.querySelectorAll('tbody tr').forEach(function(r){r.style.outline='';}); });
tr.addEventListener('dragover',function(e){ e.preventDefault(); tr.style.outline='2px solid #1d9e75'; });
tr.addEventListener('dragleave',function(){ tr.style.outline=''; });
tr.addEventListener('drop',function(e){
e.preventDefault(); tr.style.outline='';
if (!dragSrcRow || dragSrcRow===tr) return;
var tbody = tr.closest('tbody');
var rows = Array.from(tbody.querySelectorAll('tr'));
var si = rows.indexOf(dragSrcRow), ti = rows.indexOf(tr);
if (si < ti) tbody.insertBefore(dragSrcRow, tr.nextSibling);
else tbody.insertBefore(dragSrcRow, tr);
dragSrcRow = null;
// Neue Reihenfolge speichern
var newOrder = Array.from(tbody.querySelectorAll('tr')).map(function(r){
return parseInt(r.dataset.id);
}).filter(function(id){ return !isNaN(id); });
if (newOrder.length) {
api({action:'reorder', ids:newOrder}, function(d){
if (d.success) toast('✅ Reihenfolge gespeichert!');
});
}
});
});
});
}
// ── Admin: Felder ─────────────────────────────────
window.dfTab = function(tab) {
['records','fields','new'].forEach(function(t){
document.getElementById('df-tab-'+t).style.display = t===tab ? 'block' : 'none';
var btn = document.querySelector('[data-tab="'+t+'"]');
if (btn) btn.classList.toggle('df-tab-active', t===tab);
});
if (tab==='records') renderAdminList();
if (tab==='fields') renderFieldEditor();
if (tab==='new') renderNewForm();
};
function renderFieldEditor() {
var html = '';
fields.forEach(function(f,i){
html += '<div class="df-field-row" data-idx="'+i+'">'
+ '<span class="df-drag-handle">⠿</span>'
+ '<input type="text" class="df-inp df-fl-name" value="'+esc(f.label)+'" placeholder="Feldname">'
+ '<select class="df-inp df-fl-type">'
+ ['text','email','tel','url','number','date','textarea','image'].map(function(t){
return '<option value="'+t+'"'+(f.type===t?' selected':'')+'>'+t+'</option>';
}).join('')
+ '</select>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
+ '<input type="checkbox" class="df-fl-req"'+(f.required==1?' checked':'')+'> Pflicht</label>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;">'
+ '<input type="checkbox" class="df-fl-show"'+(f.show_in_list==1?' checked':'')+'> Anzeigen</label>'
+ '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>'
+ '</div>';
});
document.getElementById('df-field-list').innerHTML = html;
}
window.dfAddField = function() {
var li = document.getElementById('df-field-list');
var div = document.createElement('div');
div.className = 'df-field-row';
div.innerHTML = '<span class="df-drag-handle">⠿</span>'
+ '<input type="text" class="df-inp df-fl-name" placeholder="Feldname">'
+ '<select class="df-inp df-fl-type">'
+ ['text','email','tel','url','number','date','textarea','image'].map(function(t){
return '<option value="'+t+'">'+t+'</option>';
}).join('')
+ '</select>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-req"> Pflicht</label>'
+ '<label style="color:#888;font-size:0.78rem;white-space:nowrap;"><input type="checkbox" class="df-fl-show" checked> Anzeigen</label>'
+ '<button onclick="this.closest(\'.df-field-row\').remove()" style="background:#e74c3c;color:#fff;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">✕</button>';
li.appendChild(div);
};
window.dfSaveFields = function() {
var rows = document.querySelectorAll('#df-field-list .df-field-row');
var newFields = [];
rows.forEach(function(row){
var name = row.querySelector('.df-fl-name').value.trim();
if (!name) return;
newFields.push({
label: name,
type: row.querySelector('.df-fl-type').value,
required: row.querySelector('.df-fl-req').checked ? 1 : 0,
show_in_list:row.querySelector('.df-fl-show').checked ? 1 : 0,
});
});
api({action:'save_fields', fields:newFields}, function(d){
if (d.success) {
toast(d.message||'✅ Felder gespeichert!');
loadFields(function(){ loadRecords(); renderNewForm(); });
} else toast(d.error||'Fehler', true);
});
};
// ── Admin: Neuer Eintrag ──────────────────────────
function renderNewForm() {
editId = 0;
var html = '';
fields.forEach(function(f){
var inp;
if (f.type==='textarea') {
inp = '<textarea class="df-inp" id="dfn_'+f.field_key+'" rows="4" style="min-height:80px;resize:vertical;"></textarea>';
} else if (f.type==='image') {
inp = '<div style="display:flex;flex-direction:column;gap:0.4rem;">'
+ '<input type="file" accept="image/*" class="df-inp" id="dfn_file_'+f.field_key+'" style="padding:4px;">'
+ '<input type="hidden" id="dfn_'+f.field_key+'">'
+ '<img id="dfn_prev_'+f.field_key+'" src style="display:none;max-height:80px;border-radius:4px;margin-top:4px;object-fit:cover;">'
+ '</div>';
} else {
inp = '<input type="'+f.type+'" class="df-inp" id="dfn_'+f.field_key+'">';
}
html += '<label class="df-field-lbl">'+esc(f.label)+(f.required==1?' <span style="color:#e74c3c;">*</span>':'')+inp+'</label>';
});
if (!html) html = '<p class="df-hint">Zuerst Felder unter 🔧 Felder anlegen!</p>';
document.getElementById('df-new-form').innerHTML = html;
}
window.dfSaveRecord = function() {
uploadImages('dfn_', function(){
var data = {};
fields.forEach(function(f){
var el = document.getElementById('dfn_'+f.field_key);
if (el) data[f.field_key] = el.value;
});
api({action:'save_record', id:0, data:data}, function(d){
if (d.success) {
toast(d.message||'✅ Gespeichert!');
renderNewForm();
loadRecords();
} else toast(d.error||'Fehler', true);
});
});
};
// Alle Bild-Felder vor dem Speichern hochladen
function uploadImages(prefix, cb) {
var imgFields = fields.filter(function(f){ return f.type==='image'; });
var pending = 0;
if (!imgFields.length) { cb(); return; }
imgFields.forEach(function(f){
var fileInp = document.getElementById(prefix+'file_'+f.field_key);
if (!fileInp || !fileInp.files || !fileInp.files[0]) return;
pending++;
var fd = new FormData();
fd.append('img', fileInp.files[0]);
fetch(ep + '?action=upload_image&gid=' + encodeURIComponent(GID), {method:'POST', body:fd})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success) {
var hidden = document.getElementById(prefix+f.field_key);
if (hidden) hidden.value = d.url;
}
pending--;
if (pending===0) cb();
})
.catch(function(){ pending--; if(pending===0) cb(); });
});
if (pending===0) cb();
}
window.dfLightbox = function(url) {
var lb = document.getElementById('df-lightbox');
var img = document.getElementById('df-lightbox-img');
if (!lb || !img) return;
img.src = url;
lb.style.display = 'flex';
};
document.addEventListener('keydown', function(e){
if (e.key==='Escape') {
var lb = document.getElementById('df-lightbox');
if (lb) lb.style.display = 'none';
}
});
window.dfExport = function(e) {
e.preventDefault();
var exportUrl = ep
+ '?action=export_csv'
+ '&gid=' + encodeURIComponent(GID)
+ '&_=' + Date.now();
window.location.href = exportUrl;
};
window.dfChangePw = function() {
var old = document.getElementById('df-old-pw').value;
var nw = document.getElementById('df-new-pw').value;
var nw2 = document.getElementById('df-new-pw2').value;
var err = document.getElementById('df-pw-err');
if (nw !== nw2) { err.textContent='Passwörter stimmen nicht überein.'; err.style.display='block'; return; }
api({action:'change_password', old_password:old, new_password:nw}, function(d){
if (d.success) {
toast(d.message||'✅ Passwort geändert!');
document.getElementById('df-pw-modal').style.display='none';
['df-old-pw','df-new-pw','df-new-pw2'].forEach(function(id){
var el=document.getElementById(id); if(el) el.value='';
});
} else {
err.textContent = d.error||'Fehler';
err.style.display = 'block';
}
});
};
window.dfLogout = function() {
api({action:'logout'}, function(){
isAdmin=false;
document.getElementById('df-admin').style.display='none';
loadRecords();
});
};
// ── Login ─────────────────────────────────────────
function openLogin() {
api({action:'check'}, function(d){
if (d.loggedIn) { isAdmin=true; showAdmin(); return; }
var modal = document.getElementById('df-modal');
var title = document.getElementById('df-modal-title');
var err = document.getElementById('df-err');
var pw = document.getElementById('df-pw');
if (!modal) return;
if (title) title.textContent = d.firstRun ? '⚙️ Erst-Einrichtung' : '🔐 Admin-Login';
if (err) err.style.display = 'none';
if (pw) pw.value = '';
modal.style.display = 'flex';
setTimeout(function(){ if(pw) pw.focus(); }, 100);
});
}
/* Login auch über externen Textlink aufrufbar machen */
window.dfOpenLogin = openLogin;
function showAdmin() {
document.getElementById('df-admin').style.display = 'block';
dfTab('records');
}
document.getElementById('df-login-btn').onclick = function(){
var pw = document.getElementById('df-pw');
var err = document.getElementById('df-err');
api({action:'login', password: pw ? pw.value : ''}, function(d){
if (d.success) {
isAdmin = true;
document.getElementById('df-modal').style.display = 'none';
showAdmin();
} else {
if (err) { err.textContent = d.error||'Falsches Passwort.'; err.style.display='block'; }
}
});
};
var pwEl = document.getElementById('df-pw');
if (pwEl) pwEl.addEventListener('keypress', function(e){ if(e.key==='Enter') document.getElementById('df-login-btn').click(); });
document.addEventListener('dblclick', function(e){ if(e.ctrlKey){ e.preventDefault(); openLogin(); }});
document.addEventListener('keydown', function(e){ if(e.ctrlKey&&e.shiftKey&&(e.key==='A'||e.key==='a')){ e.preventDefault(); openLogin(); }});
// Handy-Trigger
var mt = sec.dataset.mobiletrigger || 'shake';
var isTouch = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
if (mt==='button' && isTouch) {
var fab = document.createElement('button');
fab.innerHTML='⚙️';
fab.style.cssText='position:fixed;bottom:1.5rem;left:1rem;width:44px;height:44px;border-radius:50%;background:#1d9e75;border:none;color:#fff;font-size:1.2rem;cursor:pointer;z-index:9998;';
fab.onclick=openLogin;
document.body.appendChild(fab);
} else if (mt==='tap4') {
var _tc=0,_tt=null;
sec.addEventListener('touchend',function(){
_tc++;clearTimeout(_tt);
_tt=setTimeout(function(){_tc=0;},1500);
if(_tc>=4){_tc=0;openLogin();}
});
} else if (mt==='shake') {
var _sx=0,_sy=0,_sz=0,_st=0;
if (window.DeviceMotionEvent) {
window.addEventListener('devicemotion',function(e){
var a=e.accelerationIncludingGravity;if(!a)return;
var dv=Math.abs((a.x||0)-_sx)+Math.abs((a.y||0)-_sy)+Math.abs((a.z||0)-_sz);
_sx=a.x||0;_sy=a.y||0;_sz=a.z||0;
if(dv>25){var n=Date.now();if(n-_st>1500){_st=n;openLogin();}}
});
}
}
// Start
loadFields(function(){
initSearch();
loadRecords();
});
})();
</script>
<!-- Lightbox -->
<div id="df-lightbox" style="display:none;position:fixed;inset:0;background:#000000ee;z-index:99999;align-items:center;justify-content:center;cursor:zoom-out;" onclick="document.getElementById('df-lightbox').style.display='none'">
<img id="df-lightbox-img" src style="max-width:92%;max-height:92%;border-radius:10px;box-shadow:0 0 40px #000;object-fit:contain;">
</div>
</section>Mitglieder in diesem Forum: Amazon [Bot] und 3 Gäste