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:
@@ -17,8 +17,29 @@ if (!isset($_SESSION["user"])) {
|
|||||||
<input type="text" id="title" name="title"
|
<input type="text" id="title" name="title"
|
||||||
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
|
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
|
||||||
placeholder="Titel hier eingeben" required>
|
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>
|
</main>
|
||||||
|
|
||||||
<!-- Seitenleiste -->
|
<!-- Seitenleiste -->
|
||||||
|
|||||||
@@ -63,6 +63,11 @@
|
|||||||
Dein Beitrag wurde erfolgreich veröffentlicht!
|
Dein Beitrag wurde erfolgreich veröffentlicht!
|
||||||
</p>
|
</p>
|
||||||
<?php endif; ?>
|
<?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
|
<?php
|
||||||
unset($_SESSION["message"]);
|
unset($_SESSION["message"]);
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ if ($pfad === "deleteAccount") {
|
|||||||
<meta name="author" content="Niklas Ortmann">
|
<meta name="author" content="Niklas Ortmann">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/main.css">
|
<link rel="stylesheet" href="css/main.css">
|
||||||
<link rel="stylesheet" href="css/navbar.css">
|
<link rel="stylesheet" href="css/navbar.css">
|
||||||
<link rel="stylesheet" href="css/footer.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/profile.css">
|
||||||
<link rel="stylesheet" href="css/showArticle.css">
|
<link rel="stylesheet" href="css/showArticle.css">
|
||||||
<link rel="stylesheet" href="css/message.css">
|
<link rel="stylesheet" href="css/message.css">
|
||||||
|
|
||||||
|
<script src="js/editor.js" async></script>
|
||||||
<title>EduForge</title>
|
<title>EduForge</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
+119
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -64,10 +64,54 @@ require_once '../validator/article-validator.php';
|
|||||||
$cleanedTags = array_unique($cleanedTags);
|
$cleanedTags = array_unique($cleanedTags);
|
||||||
$cleanedTags = implode(',', $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: ---------------------------
|
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
||||||
try {
|
try {
|
||||||
$articleManager = ArticleManager::getInstance();
|
$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
|
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen
|
||||||
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
||||||
@@ -84,7 +128,4 @@ require_once '../validator/article-validator.php';
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
@@ -19,7 +19,7 @@ class ArticleManager
|
|||||||
public static function getInstance()
|
public static function getInstance()
|
||||||
{
|
{
|
||||||
$articleManager = DatabaseArticleManager::getInstance(); // Hier kann zwischen dem lokalen und datenbankbasiertem ArticleManager gewechselt werden.
|
$articleManager = DatabaseArticleManager::getInstance(); // Hier kann zwischen dem lokalen und datenbankbasiertem ArticleManager gewechselt werden.
|
||||||
|
/*
|
||||||
// 100 fiktionale Fachbeiträge:
|
// 100 fiktionale Fachbeiträge:
|
||||||
$dummyArticles = [
|
$dummyArticles = [
|
||||||
// --- INFORMATIK & MATHE (1-20) ---
|
// --- INFORMATIK & MATHE (1-20) ---
|
||||||
@@ -154,7 +154,7 @@ class ArticleManager
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return $articleManager;
|
return $articleManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,19 +33,18 @@ function articleTitleValidator($title)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft, ob der Contenttext 10-7000 Zeichen enthält.
|
* Prüft, ob der übergebene Content ein formal valider JSON-String ist.
|
||||||
* @param $content
|
* @param mixed $content Der zu prüfende Inhalt
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
function articleContentValidator($content)
|
function articleContentValidator($content)
|
||||||
{
|
{
|
||||||
$content = trim($content);
|
if (!is_string($content)) {
|
||||||
$zeichenAnzahl = mb_strlen($content);
|
|
||||||
if ($zeichenAnzahl <= 7000 && $zeichenAnzahl >= 10) {
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüft direkt, ob der String valides JSON enthält
|
||||||
|
return json_validate($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user