erweiterter Beitragseditor

content ist nun im json-Format
Bilder können hochgeladen werden
Textblöcke können im Editor angehangen werden
This commit is contained in:
2026-06-14 10:44:17 +02:00
parent f9c1c67a38
commit 80f92a384e
7 changed files with 202 additions and 14 deletions
+22 -1
View File
@@ -17,8 +17,29 @@ if (!isset($_SESSION["user"])) {
<input type="text" id="title" name="title"
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
placeholder="Titel hier eingeben" required>
<textarea id="content" name="content" placeholder="Schreibe deinen Beitrag..."><?php if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){echo htmlspecialchars($_SESSION['old_content']); unset($_SESSION['old_content']);}elseif (isset($content) && !empty($content)){echo htmlspecialchars($content);}?></textarea>
<!-- Hier werden die dynamischen divs via JavaScript eingefügt -->
<div id="block-container"></div>
<!-- Plus-Button und das Pop-up-Menü -->
<div id="add-block-control" class="add-block-control">
<button type="button" id="plus-button" class="plus-button">+</button>
<div id="block-popup" class="block-popup hidden">
<button type="button" data-type="text">Textblock</button>
<button type="button" data-type="image">Bild einfügen</button>
</div>
</div>
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
<textarea id="content" name="content" style="display:none;">
<?php
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
echo htmlspecialchars($_SESSION['old_content']);
unset($_SESSION['old_content']);
} else {
echo '[]'; // Standardmäßig ein leeres JSON-Array
}?>
</textarea>
</main>
<!-- Seitenleiste -->
+5
View File
@@ -63,6 +63,11 @@
Dein Beitrag wurde erfolgreich veröffentlicht!
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "image_upload_error"): ?>
<p class="alert-message is-error">
Das Bild konnte nicht hochgeladen werden. Bitte versuche es erneut oder verwende ein anderes Bildformat.
</p>
<?php endif; ?>
<?php
unset($_SESSION["message"]);
?>
+3
View File
@@ -42,6 +42,7 @@ if ($pfad === "deleteAccount") {
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
@@ -50,6 +51,8 @@ if ($pfad === "deleteAccount") {
<link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css">
<script src="js/editor.js" async></script>
<title>EduForge</title>
</head>
+119
View File
@@ -0,0 +1,119 @@
/**
* Editor-Steuerung für dynamische Inhaltsblöcke
*/
document.addEventListener("DOMContentLoaded", function() {
const form = document.getElementById("editor-form");
// Falls das Formular auf der aktuellen Seite nicht existiert, Skript abbrechen
if (!form) {
return;
}
const container = document.getElementById("block-container");
const plusButton = document.getElementById("plus-button");
const popup = document.getElementById("block-popup");
const hiddenContentInput = document.getElementById("content");
// Pop-up umschalten bei Klick auf das Plus
plusButton.addEventListener("click", () => {
popup.classList.toggle("hidden");
});
// Klick auf eine Block-Option im Pop-up
popup.querySelectorAll("button").forEach(btn => {
btn.addEventListener("click", function() {
const type = this.getAttribute("data-type");
addBlockElement(type, "");
popup.classList.add("hidden");
});
});
// Erstellt ein visuelles HTML-Element im Editor
function addBlockElement(type, value = "") {
const blockDiv = document.createElement("div");
blockDiv.classList.add("editor-block");
blockDiv.setAttribute("data-type", type);
// Löschen-Button für den Block
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.innerHTML = "✕";
deleteBtn.classList.add("delete-block-btn");
deleteBtn.addEventListener("click", () => {
blockDiv.remove();
});
blockDiv.appendChild(deleteBtn);
if (type === "text") {
const textarea = document.createElement("textarea");
textarea.placeholder = "Schreibe deinen Textblock...";
textarea.value = value;
blockDiv.appendChild(textarea);
} else if (type === "image") {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "image/*";
const imgPreview = document.createElement("img");
imgPreview.style.maxWidth = "200px";
imgPreview.style.display = "block";
imgPreview.style.marginTop = "10px";
// Falls bereits ein Bildpfad existiert (z.B. nach einem Reload bei Validierungsfehlern)
if (value && typeof value === 'string' && value.startsWith('uploads/')) {
imgPreview.src = value;
blockDiv.setAttribute("data-value", value);
}
fileInput.addEventListener("change", function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
imgPreview.src = e.target.result;
// Temporäre Speicherung des Base64-Strings im HTML-Attribut
blockDiv.setAttribute("data-value", e.target.result);
}
reader.readAsDataURL(this.files[0]);
}
});
blockDiv.appendChild(fileInput);
blockDiv.appendChild(imgPreview);
}
container.appendChild(blockDiv);
}
// Beim Abschicken: HTML-Blöcke auslesen und als JSON-String serialisieren
form.addEventListener("submit", function(e) {
const blocks = [];
container.querySelectorAll(".editor-block").forEach(blockDiv => {
const type = blockDiv.getAttribute("data-type");
let value = "";
if (type === "text") {
value = blockDiv.querySelector("textarea").value;
} else if (type === "image") {
value = blockDiv.getAttribute("data-value") || "";
}
blocks.push({ type: type, value: value });
});
// JSON-Daten in das unsichtbare Formularfeld schreiben
hiddenContentInput.value = JSON.stringify(blocks);
});
// Existierende Blöcke laden (stellt alte Daten aus der Session wieder her)
try {
const initialBlocks = JSON.parse(hiddenContentInput.value);
if (Array.isArray(initialBlocks)) {
initialBlocks.forEach(b => addBlockElement(b.type, b.value));
}
} catch(e) {
// Fallback für alten Reintext aus Altbeständen
if (hiddenContentInput.value.trim() !== "") {
addBlockElement("text", hiddenContentInput.value);
}
}
});
+45 -4
View File
@@ -64,10 +64,54 @@ require_once '../validator/article-validator.php';
$cleanedTags = array_unique($cleanedTags);
$cleanedTags = implode(',', $cleanedTags);
}
// ----------------- Base64-Bilder verarbeiten und auf Server speichern -----------------
$blocks = json_decode($content, true);
$uploadDir = __DIR__ . '/../../uploads/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
foreach ($blocks as &$block) {
// Base64-Format-Prüfung:
if ($block['type'] === 'image' && str_starts_with($block['value'], 'data:image/')) {
// Base64-String zerlegen:
log_alert("Bild erkannt, verarbeite...");
$parts = explode(',', $block['value']);
$metadata = $parts[0];
$base64Data = $parts[1];
// Dateiendung ermitteln
preg_match('/data:image\/(?<extension>.*?);/', $metadata, $matches);
$extension = $matches['extension'] ?? 'jpg';
if ($extension === 'jpeg') { $extension = 'jpg'; }
// Eindeutigen Dateinamen generieren
$fileName = 'img_' . uniqid() . '.' . $extension;
$filePath = $uploadDir . $fileName;
// Datei im /uploads speichern:
if (file_put_contents($filePath, base64_decode($base64Data)) !== false) {
// temporären Base64-String durch den echten Pfad ersetzen
$block['value'] = 'uploads/' . $fileName;
} else {
$_SESSION["message"] = "image_upload_error";
header("location: ../../index.php?pfad=createArticle");
exit();
}
}
}
unset($block);
// Aktualisiertes Array wieder in JSON konvertieren
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE);
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
try {
$articleManager = ArticleManager::getInstance();
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
$articleManager->addArticle($title, $finalContent, $author, $category, $cleanedTags);
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
@@ -84,7 +128,4 @@ require_once '../validator/article-validator.php';
exit();
}
}
?>
+2 -2
View File
@@ -19,7 +19,7 @@ class ArticleManager
public static function getInstance()
{
$articleManager = DatabaseArticleManager::getInstance(); // Hier kann zwischen dem lokalen und datenbankbasiertem ArticleManager gewechselt werden.
/*
// 100 fiktionale Fachbeiträge:
$dummyArticles = [
// --- INFORMATIK & MATHE (1-20) ---
@@ -154,7 +154,7 @@ class ArticleManager
);
}
}
*/
return $articleManager;
}
+6 -7
View File
@@ -33,19 +33,18 @@ function articleTitleValidator($title)
}
/**
* Prüft, ob der Contenttext 10-7000 Zeichen enthält.
* @param $content
* Prüft, ob der übergebene Content ein formal valider JSON-String ist.
* @param mixed $content Der zu prüfende Inhalt
* @return bool
*/
function articleContentValidator($content)
{
$content = trim($content);
$zeichenAnzahl = mb_strlen($content);
if ($zeichenAnzahl <= 7000 && $zeichenAnzahl >= 10) {
return true;
}else{
if (!is_string($content)) {
return false;
}
// Prüft direkt, ob der String valides JSON enthält
return json_validate($content);
}
/**