229 lines
6.7 KiB
JavaScript
229 lines
6.7 KiB
JavaScript
|
|
/**
|
||
|
|
* Dynamic Page Content Loader
|
||
|
|
* Loads page content from the API and renders it with proper formatting
|
||
|
|
*/
|
||
|
|
|
||
|
|
// Convert Quill Delta to HTML - accurate conversion matching backend format
|
||
|
|
function convertDeltaToHtml(delta) {
|
||
|
|
if (!delta || !delta.ops) return "";
|
||
|
|
|
||
|
|
let html = "";
|
||
|
|
let currentLine = "";
|
||
|
|
let inListType = null; // 'bullet' or 'ordered'
|
||
|
|
|
||
|
|
const ops = delta.ops;
|
||
|
|
|
||
|
|
for (let i = 0; i < ops.length; i++) {
|
||
|
|
const op = ops[i];
|
||
|
|
const nextOp = ops[i + 1];
|
||
|
|
|
||
|
|
if (typeof op.insert === "string") {
|
||
|
|
const text = op.insert;
|
||
|
|
const inlineAttrs = op.attributes || {};
|
||
|
|
|
||
|
|
// Check if this is a standalone newline with block attributes
|
||
|
|
if (text === "\n") {
|
||
|
|
const blockAttrs = inlineAttrs;
|
||
|
|
|
||
|
|
// Handle list transitions
|
||
|
|
if (blockAttrs.list) {
|
||
|
|
const newListType = blockAttrs.list;
|
||
|
|
if (inListType !== newListType) {
|
||
|
|
if (inListType) {
|
||
|
|
html += inListType === "ordered" ? "</ol>" : "</ul>";
|
||
|
|
}
|
||
|
|
html += newListType === "ordered" ? "<ol>" : "<ul>";
|
||
|
|
inListType = newListType;
|
||
|
|
}
|
||
|
|
html += `<li>${currentLine}</li>`;
|
||
|
|
} else {
|
||
|
|
// Close any open list
|
||
|
|
if (inListType) {
|
||
|
|
html += inListType === "ordered" ? "</ol>" : "</ul>";
|
||
|
|
inListType = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply block formatting
|
||
|
|
if (blockAttrs.header) {
|
||
|
|
html += `<h${blockAttrs.header}>${currentLine}</h${blockAttrs.header}>`;
|
||
|
|
} else if (blockAttrs.blockquote) {
|
||
|
|
html += `<blockquote>${currentLine}</blockquote>`;
|
||
|
|
} else if (blockAttrs["code-block"]) {
|
||
|
|
html += `<pre><code>${currentLine}</code></pre>`;
|
||
|
|
} else if (currentLine) {
|
||
|
|
html += `<p>${currentLine}</p>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
currentLine = "";
|
||
|
|
} else {
|
||
|
|
// Regular text - may contain embedded newlines
|
||
|
|
const parts = text.split("\n");
|
||
|
|
|
||
|
|
for (let j = 0; j < parts.length; j++) {
|
||
|
|
const part = parts[j];
|
||
|
|
|
||
|
|
// Format the text part
|
||
|
|
if (part.length > 0) {
|
||
|
|
let formatted = escapeHtml(part);
|
||
|
|
|
||
|
|
// Apply inline formatting
|
||
|
|
if (inlineAttrs.bold) formatted = `<strong>${formatted}</strong>`;
|
||
|
|
if (inlineAttrs.italic) formatted = `<em>${formatted}</em>`;
|
||
|
|
if (inlineAttrs.underline) formatted = `<u>${formatted}</u>`;
|
||
|
|
if (inlineAttrs.strike) formatted = `<s>${formatted}</s>`;
|
||
|
|
if (inlineAttrs.code) formatted = `<code>${formatted}</code>`;
|
||
|
|
if (inlineAttrs.link)
|
||
|
|
formatted = `<a href="${inlineAttrs.link}" target="_blank" rel="noopener">${formatted}</a>`;
|
||
|
|
|
||
|
|
currentLine += formatted;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle embedded newlines (not the last part)
|
||
|
|
if (j < parts.length - 1) {
|
||
|
|
// Close any open list for embedded newlines
|
||
|
|
if (inListType) {
|
||
|
|
html += inListType === "ordered" ? "</ol>" : "</ul>";
|
||
|
|
inListType = null;
|
||
|
|
}
|
||
|
|
if (currentLine) {
|
||
|
|
html += `<p>${currentLine}</p>`;
|
||
|
|
}
|
||
|
|
currentLine = "";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (op.insert && op.insert.image) {
|
||
|
|
// Flush pending content
|
||
|
|
if (currentLine) {
|
||
|
|
if (inListType) {
|
||
|
|
html += `<li>${currentLine}</li>`;
|
||
|
|
html += inListType === "ordered" ? "</ol>" : "</ul>";
|
||
|
|
inListType = null;
|
||
|
|
} else {
|
||
|
|
html += `<p>${currentLine}</p>`;
|
||
|
|
}
|
||
|
|
currentLine = "";
|
||
|
|
}
|
||
|
|
html += `<img src="${op.insert.image}" class="content-image" alt="Content image">`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Flush remaining content
|
||
|
|
if (inListType) {
|
||
|
|
if (currentLine) html += `<li>${currentLine}</li>`;
|
||
|
|
html += inListType === "ordered" ? "</ol>" : "</ul>";
|
||
|
|
} else if (currentLine) {
|
||
|
|
html += `<p>${currentLine}</p>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return html;
|
||
|
|
}
|
||
|
|
|
||
|
|
function escapeHtml(text) {
|
||
|
|
return text
|
||
|
|
.replace(/&/g, "&")
|
||
|
|
.replace(/</g, "<")
|
||
|
|
.replace(/>/g, ">")
|
||
|
|
.replace(/"/g, """);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse and render page content (handles both Delta JSON and raw HTML)
|
||
|
|
function parsePageContent(content) {
|
||
|
|
if (!content) return "<p>Content coming soon...</p>";
|
||
|
|
|
||
|
|
try {
|
||
|
|
const delta = JSON.parse(content);
|
||
|
|
if (delta.ops) {
|
||
|
|
return convertDeltaToHtml(delta);
|
||
|
|
}
|
||
|
|
return content;
|
||
|
|
} catch (e) {
|
||
|
|
// Not JSON, return as-is (probably HTML)
|
||
|
|
return content;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load page content from API
|
||
|
|
async function loadPageContent(slug, options = {}) {
|
||
|
|
const {
|
||
|
|
titleSelector = "#pageTitle",
|
||
|
|
contentSelector = "#dynamicContent",
|
||
|
|
staticSelector = "#staticContent",
|
||
|
|
showLoading = true,
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
const dynamicContent = document.querySelector(contentSelector);
|
||
|
|
const staticContent = document.querySelector(staticSelector);
|
||
|
|
const titleElement = document.querySelector(titleSelector);
|
||
|
|
|
||
|
|
if (!dynamicContent) {
|
||
|
|
console.warn("Dynamic content container not found:", contentSelector);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (showLoading) {
|
||
|
|
dynamicContent.innerHTML = `
|
||
|
|
<div style="text-align: center; padding: 40px;">
|
||
|
|
<div class="loading-spinner" style="width: 40px; height: 40px; border: 3px solid rgba(252,177,216,0.2); border-top-color: #FCB1D8; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
|
||
|
|
<p style="margin-top: 16px; color: #888;">Loading content...</p>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/pages/${slug}`);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success && data.page) {
|
||
|
|
const page = data.page;
|
||
|
|
|
||
|
|
// Update page title if provided
|
||
|
|
if (page.title && titleElement) {
|
||
|
|
titleElement.textContent = page.title;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse and render content
|
||
|
|
const htmlContent = parsePageContent(page.content);
|
||
|
|
dynamicContent.innerHTML = htmlContent;
|
||
|
|
|
||
|
|
// Hide static fallback if exists
|
||
|
|
if (staticContent) {
|
||
|
|
staticContent.style.display = "none";
|
||
|
|
}
|
||
|
|
|
||
|
|
return page;
|
||
|
|
} else {
|
||
|
|
// Show static fallback
|
||
|
|
dynamicContent.style.display = "none";
|
||
|
|
if (staticContent) {
|
||
|
|
staticContent.style.display = "block";
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load page content:", error);
|
||
|
|
dynamicContent.style.display = "none";
|
||
|
|
if (staticContent) {
|
||
|
|
staticContent.style.display = "block";
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-initialize on DOMContentLoaded if data-page-slug is set
|
||
|
|
document.addEventListener("DOMContentLoaded", () => {
|
||
|
|
const pageContainer = document.querySelector("[data-page-slug]");
|
||
|
|
if (pageContainer) {
|
||
|
|
const slug = pageContainer.dataset.pageSlug;
|
||
|
|
loadPageContent(slug);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Export for use in other scripts
|
||
|
|
window.DynamicPage = {
|
||
|
|
loadPageContent,
|
||
|
|
parsePageContent,
|
||
|
|
convertDeltaToHtml,
|
||
|
|
};
|