refactor: clean up dao-tao-detail.html styling and implement course-info-widget fragment

This commit is contained in:
2026-07-27 20:49:53 +07:00
parent 1a8726285c
commit bc27a9ea9c
12 changed files with 902 additions and 987 deletions
@@ -34,6 +34,8 @@
--spacing-md: 20px;
--radius-md: 4px;
--radius-lg: 8px;
--spacing: 0.25rem;
}
/* ==========================================================================
@@ -1083,7 +1085,6 @@ i, .btn-outline-primary {
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 2rem;
width: 100%;
}
@@ -1095,6 +1096,7 @@ i, .btn-outline-primary {
gap: 4px; /* gap-x-1 */
width: 100%;
max-width: 860px;
margin-bottom:calc(var(--spacing) * 8);
}
@media (max-width: 768px) {
@@ -1125,8 +1127,88 @@ i, .btn-outline-primary {
background-color: transparent;
}
.custom-tab-btn.active {
/* .custom-tab-btn.active {
background-color: white !important;
color: var(--color-old-brick) !important; /* text-primary-600 */
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
} */
@media (min-width: 1280px) {
.xl\:mb-8 {
margin-bottom: calc(var(--spacing) * 8);
}
}
@media (min-width: 768px) {
.md\:mb-6 {
margin-bottom: calc(var(--spacing) * 6);
}
}
.mb-4 {
margin-bottom: calc(var(--spacing) * 4);
}
/* Editor.js Table Styling */
figure.table {
width: 100%;
overflow-x: auto;
margin: 2rem 0;
}
figure.table table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
background-color: #ffffff;
border: 1px solid #e0e0e0;
}
figure.table table th,
figure.table table td {
padding: 12px 15px;
border: 1px solid #e0e0e0;
vertical-align: middle;
}
/* Biến dòng đầu tiên thành Header luôn cho đẹp nếu người dùng quên bật Header trong Editor.js */
figure.table table tr:first-child td,
figure.table table th {
background-color: var(--color-old-brick, #881c1c) !important;
color: #ffffff !important;
font-weight: 700;
text-align: center;
}
figure.table table tr {
border-bottom: 1px solid #e0e0e0;
}
figure.table table tr:nth-of-type(even) {
background-color: #fcfcfc;
}
figure.table table tr:hover {
background-color: #f5f5f5;
}
.training-layout table tr:first-child td,
.training-layout table th {
background-color: var(--color-old-brick, #881c1c) !important;
color: #ffffff !important;
font-weight: 700;
text-align: center;
}
.training-layout table td,
.training-layout table th {
border: 1px solid #dee2e6;
padding: 8px 12px;
}
.training-sidebar {
a {
border: white 1px solid;
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<!-- Widget hiển thị thông tin động của khóa học (Ngày khai giảng, địa điểm, hình thức...) -->
<div th:fragment="course-info" class="bg-primary-600 rounded-lg p-4 flex flex-col justify-center mb-4">
<div class="xl:space-y-8 md:space-y-6 space-y-4">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-white rounded-full flex items-center justify-center shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar size-3.5 text-primary-600"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>
</div>
<div>
<p class="text-white/80 text-sm">Ngày khai giảng</p>
<p class="text-white font-semibold" th:text="${post.eventTime != null ? #temporals.format(post.eventTime, 'dd/MM/yyyy') : 'Đang cập nhật'}">Đang cập nhật</p>
</div>
</div>
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-white rounded-full flex items-center justify-center shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin size-3.5 text-primary-600"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path><circle cx="12" cy="10" r="3"></circle></svg>
</div>
<div>
<p class="text-white/80 text-sm">Địa điểm</p>
<p class="text-white font-semibold" th:text="${post.location != null ? post.location : 'SIS Cần Thơ'}">SIS Cần Thơ</p>
</div>
</div>
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-white rounded-full flex items-center justify-center shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users size-3.5 text-primary-600"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
</div>
<div>
<p class="text-white/80 text-sm">Hình thức</p>
<p class="text-white font-semibold">Trực tiếp / Tập trung</p>
</div>
</div>
<div class="pt-4 mt-4 border-t border-white/20">
<a href="#" class="block w-full py-3 px-4 bg-white text-primary-600 text-center font-bold rounded-lg hover:bg-gray-100 transition-colors">
Đăng ký ngay
</a>
</div>
</div>
</div>
</body>
</html>
@@ -5,67 +5,17 @@
<head>
<title th:text="${isNew} ? 'Add New Post' : 'Edit Post'">Post Form</title>
<style>
/* Editor.js container styling */
#editorjs {
border: 1px solid #d1d3e2;
/* TinyMCE Editor styling */
.tox-tinymce {
border-radius: 0.35rem;
padding: 16px 12px;
min-height: 350px;
background: #fff;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
border: 1px solid #d1d3e2;
}
#editorjs:focus-within {
.tox:focus-within {
border-color: #bac8f3;
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
}
/* Block tool styling overrides */
.ce-block__content {
max-width: 100% !important;
}
/* Auto scale media inside snippets to fit editor */
.ce-snippet-wrapper img,
.ce-snippet-wrapper video,
.ce-snippet-wrapper iframe {
max-width: 100% !important;
height: auto !important;
}
.ce-toolbar__content {
max-width: 100% !important;
}
.codex-editor__redactor {
padding-bottom: 80px !important;
}
.editor-plugin-info {
font-size: 0.75rem;
color: #858796;
}
.editor-plugin-info .badge {
font-weight: 400;
font-size: 0.7rem;
}
/* Full Screen Editor Mode */
.editor-fullscreen {
position: fixed !important;
top: 0;
left: 0;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
margin: 0 !important;
border-radius: 0 !important;
border: none !important;
overflow-y: auto !important;
padding: 40px !important;
}
/* Tag input styling */
.tag-input-container {
position: relative;
@@ -166,7 +116,10 @@
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-link"></i> Permalink:
<code>/post/<span id="slugPreview" th:text="${post.slug ?: 'auto-generated'}"></span></code>
<a th:href="@{'/post/' + ${post.slug}}" target="_blank" style="text-decoration:none" title="Mở trang bài viết trong tab mới">
<code>/post/<span id="slugPreview" th:text="${post.slug ?: 'auto-generated'}"></span></code>
<i class="fas fa-external-link-alt" style="font-size:11px;margin-left:4px"></i>
</a>
</small>
<input type="hidden" id="postSlug" th:field="*{slug}">
</div>
@@ -184,27 +137,11 @@
</button>
</div>
<div class="card-body">
<div class="editor-plugin-info mb-2">
Active blocks:
<span class="badge badge-light">Heading</span>
<span class="badge badge-light">List</span>
<span class="badge badge-light">Quote</span>
<span class="badge badge-light">Table</span>
<span class="badge badge-light">Code</span>
<span class="badge badge-light">Delimiter</span>
<span class="badge badge-light">Warning</span>
<span id="pluginBadges"></span>
</div>
<!-- The Editor.js container -->
<div id="editorjs"></div>
<!-- Hidden input to store the JSON content for form submission -->
<input type="hidden" id="editorContent" name="content" th:value="*{content}">
<!-- TinyMCE Editor Container -->
<textarea id="editorContent" name="content" th:text="*{content}" style="height: 600px; width: 100%;"></textarea>
<small class="form-text text-muted mt-2">
<i class="fas fa-info-circle"></i>
Click the <strong>+</strong> button or press <kbd>Tab</kbd> to add new blocks.
Use the toolbar to insert tables, images, headings, and more.
</small>
</div>
</div>
@@ -366,92 +303,252 @@
</form>
</div>
<!-- Editor.js Scripts (injected via layout:fragment="scripts") -->
<!-- TinyMCE Script (injected via layout:fragment="scripts") -->
<section layout:fragment="scripts">
<!-- Editor.js Core -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.30.8/dist/editorjs.umd.js"></script>
<!-- TinyMCE Core (via CDN) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.3/tinymce.min.js" referrerpolicy="origin"></script>
<!-- Built-in Block Tools -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.8.8/dist/header.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/nested-list@1.4.3/dist/nested-list.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.7.4/dist/quote.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@1.4.2/dist/delimiter.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@2.4.2/dist/table.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@2.9.3/dist/code.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@1.4.1/dist/warning.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.4.0/dist/marker.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@1.5.1/dist/inline-code.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/underline@1.1.0/dist/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.9.0/dist/image.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/attaches@1.3.0/dist/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/raw@2.4.0/dist/bundle.js"></script>
<!-- Custom SIS Editor Plugins -->
<script th:src="@{/js/manage/editor-plugins/html-snippet.js}"></script>
<script th:src="@{/js/manage/editor-plugins/tab-split.js}"></script>
<script th:src="@{/js/manage/editor-plugins/training-course.js}"></script>
<script th:src="@{/js/manage/editor-plugins/tab-start.js}"></script>
<script th:src="@{/js/manage/editor-plugins/tab-end.js}"></script>
<!-- Init Editor -->
<script th:src="@{/js/manage/editor-config.js}"></script>
<!-- Initialize the editor and page-specific scripts -->
<!-- Initialize TinyMCE -->
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
// Register Plugins
window.SISEditorPlugins = window.SISEditorPlugins || {};
if (typeof RawTool !== 'undefined') {
window.SISEditorPlugins['raw'] = { class: RawTool };
}
if (typeof TabSplitTool !== 'undefined') {
window.SISEditorPlugins['tabSplit'] = { class: TabSplitTool };
}
if (typeof TrainingCourseInfoTool !== 'undefined') {
}
if (typeof TabStartTool !== 'undefined') {
window.SISEditorPlugins['tabStart'] = { class: TabStartTool };
}
if (typeof TabEndTool !== 'undefined') {
window.SISEditorPlugins['tabEnd'] = { class: TabEndTool };
}
tinymce.init({
selector: '#editorContent',
height: 600,
menubar: true,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'help', 'wordcount'
],
toolbar: 'code | undo redo | blocks | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'table tablemergecells tablesplitcells | ' +
'insertTabStart insertTabEnd editClassId | previewTabs | removeformat | help',
toolbar_mode: 'wrap',
content_style: `
body { font-family:Helvetica,Arial,sans-serif; font-size:16px; }
.dynamic-tab-start {
display: inline-block;
background: #881c1c;
color: #fff !important;
padding: 8px 20px;
border-radius: 6px 6px 0 0;
margin: 24px 0 0 0;
font-size: 15px;
font-weight: 600;
border: 2px solid #881c1c;
border-bottom: none;
position: relative;
}
.dynamic-tab-start::before {
content: '📑 TAB: ';
font-size: 11px;
opacity: 0.8;
}
.dynamic-tab-start + p, .dynamic-tab-start + div, .dynamic-tab-start + table, .dynamic-tab-start + figure {
border-left: 3px solid #881c1c;
padding-left: 12px;
margin-left: 4px;
}
hr.dynamic-tab-end {
border: none;
border-top: 3px dashed #881c1c;
margin: 24px 0;
position: relative;
}
hr.dynamic-tab-end::after {
content: '— KẾT THÚC NHÓM TAB —';
display: block;
text-align: center;
color: #881c1c;
font-size: 11px;
font-weight: 600;
margin-top: 4px;
}
`,
// Convert URLs to absolute if needed, or keep relative
convert_urls: false,
// Table specific settings to make tables responsive and nice
table_default_attributes: {
border: '1'
},
table_default_styles: {
'border-collapse': 'collapse',
'width': '100%'
},
table_responsive_width: true,
// --- Editor.js Initialization ---
var existingContent = document.getElementById('editorContent').value;
var initialData = null;
if (existingContent && existingContent.trim() !== '') {
try {
initialData = JSON.parse(existingContent);
} catch (e) {
console.warn('[SIS Editor] Existing content is not valid JSON, treating as Raw HTML.');
initialData = {
time: Date.now(),
blocks: [
{
type: "raw",
data: {
html: existingContent
}
setup: function (editor) {
editor.on('change', function () {
tinymce.triggerSave();
});
editor.ui.registry.addButton('insertTabStart', {
text: 'Thêm Tab',
tooltip: 'Tạo một Tab ngang mới (Dành cho Chi tiết khoá học)',
onAction: function (_) {
var tabName = prompt('Nhập tiêu đề Tab (ví dụ: Tổng quan):', 'Tiêu đề Tab');
if (tabName) {
editor.insertContent('<h3 class="dynamic-tab-start">' + tabName + '</h3><p>Nhập nội dung của Tab vào đây...</p>');
}
]
};
}
});
editor.ui.registry.addButton('insertTabEnd', {
text: 'Kết thúc Tab',
tooltip: 'Đóng/Kết thúc nhóm Tab hiện tại',
onAction: function (_) {
editor.insertContent('<p>&nbsp;</p><hr class="dynamic-tab-end"/><p>&nbsp;</p>');
}
});
editor.ui.registry.addButton('previewTabs', {
text: '👁 Xem trước',
tooltip: 'Xem trước bài viết với Tab hoạt động giống giao diện thực',
onAction: function (_) {
var content = editor.getContent();
// Parse tab markers into real tab UI
var parser = document.createElement('div');
parser.innerHTML = content;
var tabStarts = parser.querySelectorAll('h3.dynamic-tab-start');
if (tabStarts.length === 0) {
// No tabs, just show raw content
editor.windowManager.open({
title: 'Xem trước bài viết',
size: 'large',
body: {
type: 'panel',
items: [{
type: 'htmlpanel',
html: '<div style="padding:16px;font-family:Helvetica,Arial,sans-serif;max-height:500px;overflow-y:auto">' + content + '</div>'
}]
},
buttons: [{ type: 'cancel', text: 'Đóng' }]
});
return;
}
// Build tab UI
var tabs = [];
tabStarts.forEach(function(startEl) {
var tabTitle = startEl.textContent;
var contentParts = [];
var currentEl = startEl.nextElementSibling;
while (currentEl && !currentEl.classList.contains('dynamic-tab-start') && !currentEl.classList.contains('dynamic-tab-end')) {
contentParts.push(currentEl.outerHTML);
currentEl = currentEl.nextElementSibling;
}
tabs.push({ title: tabTitle, html: contentParts.join('') });
});
// Build preview HTML
var previewHtml = '<div style="font-family:Helvetica,Arial,sans-serif;max-height:520px;overflow-y:auto">';
previewHtml += '<div style="display:flex;border:1px solid #881c1c;border-radius:8px;overflow:hidden;margin-bottom:16px">';
tabs.forEach(function(tab, i) {
var bg = (i === 0) ? 'background:#881c1c;color:#fff;' : 'background:#fff;color:#881c1c;';
previewHtml += '<button onclick="(function(btn){var wrap=btn.closest(\'.tce-preview-wrap\');wrap.querySelectorAll(\'.tce-tab-btn\').forEach(function(b){b.style.background=\'#fff\';b.style.color=\'#881c1c\';});btn.style.background=\'#881c1c\';btn.style.color=\'#fff\';wrap.querySelectorAll(\'.tce-tab-panel\').forEach(function(p){p.style.display=\'none\';});wrap.querySelector(\'.tce-tab-panel[data-idx=\\\'' + i + '\\\']\').style.display=\'block\';})(this)" class="tce-tab-btn" style="' + bg + 'padding:10px 20px;border:none;cursor:pointer;font-weight:600;font-size:14px;flex:1">' + tab.title + '</button>';
});
previewHtml += '</div>';
tabs.forEach(function(tab, i) {
previewHtml += '<div class="tce-tab-panel" data-idx="' + i + '" style="display:' + (i === 0 ? 'block' : 'none') + ';padding:12px">' + tab.html + '</div>';
});
previewHtml += '</div>';
editor.windowManager.open({
title: 'Xem trước Tab',
size: 'large',
body: {
type: 'panel',
items: [{
type: 'htmlpanel',
html: '<div class="tce-preview-wrap">' + previewHtml + '</div>'
}]
},
buttons: [{ type: 'cancel', text: 'Đóng' }]
});
}
});
editor.ui.registry.addButton('editClassId', {
text: '{ } Thuộc tính',
tooltip: 'Gắn ID và Class cho phần tử đang chọn',
onAction: function (_) {
var node = editor.selection.getNode();
if (!node || node.nodeName === 'BODY') {
alert('Vui lòng chọn hoặc bôi đen một đối tượng (đoạn văn, hình ảnh, bảng,...) trước khi gắn ID/Class!');
return;
}
var currentId = node.id || '';
var currentClass = node.className || '';
editor.windowManager.open({
title: 'Khai báo ID & Class',
body: {
type: 'panel',
items: [
{
type: 'htmlpanel',
html: '<p style="color:#666">Phần tử đang chọn: <b>&lt;' + node.nodeName.toLowerCase() + '&gt;</b></p>'
},
{
type: 'input',
name: 'id',
label: 'Thuộc tính ID (viết liền, không dấu)'
},
{
type: 'input',
name: 'className',
label: 'CSS Class (các class cách nhau bằng khoảng trắng)'
}
]
},
buttons: [
{
type: 'cancel',
text: 'Hủy'
},
{
type: 'submit',
text: 'Lưu',
primary: true
}
],
initialData: {
id: currentId,
className: currentClass
},
onSubmit: function (api) {
var data = api.getData();
if (data.id.trim() !== '') {
node.id = data.id.trim();
} else {
node.removeAttribute('id');
}
if (data.className.trim() !== '') {
node.className = data.className.trim();
} else {
node.removeAttribute('class');
}
// Update content manually just in case
editor.setDirty(true);
api.close();
}
});
}
});
}
}
});
window.sisEditor = initSISEditor('editorjs', 'editorContent', initialData);
// Show badges for any injected plugins
var pluginBadges = document.getElementById('pluginBadges');
var pluginKeys = Object.keys(window.SISEditorPlugins || {});
if (pluginBadges && pluginKeys.length > 0) {
pluginKeys.forEach(function (key) {
var badge = document.createElement('span');
badge.className = 'badge badge-info ml-1';
badge.textContent = key + ' (plugin)';
pluginBadges.appendChild(badge);
});
// Remove the initial JSON warning for TinyMCE
var existingContent = document.getElementById('editorContent').value;
if (existingContent && existingContent.trim().startsWith('{') && existingContent.includes('"blocks"')) {
console.warn('[SIS Editor] Found raw JSON in editor area. This post might not have been migrated correctly!');
}
// --- Unsaved Changes Warning ---
@@ -468,25 +565,11 @@
});
// --- Full Screen Mode Toggle ---
var editorContainer = document.getElementById('editorjs');
// Fullscreen is handled natively by TinyMCE via the toolbar (fullscreen plugin)
var fsBtn = document.getElementById('toggleFullscreenBtn');
var fsIcon = fsBtn.querySelector('i');
fsBtn.addEventListener('click', function () {
editorContainer.classList.toggle('editor-fullscreen');
if (editorContainer.classList.contains('editor-fullscreen')) {
fsIcon.classList.remove('fa-expand');
fsIcon.classList.add('fa-compress');
fsBtn.style.position = 'fixed';
fsBtn.style.top = '10px';
fsBtn.style.right = '20px';
fsBtn.style.zIndex = '10000';
} else {
fsIcon.classList.remove('fa-compress');
fsIcon.classList.add('fa-expand');
fsBtn.style.position = 'static';
}
});
if (fsBtn) {
fsBtn.style.display = 'none';
}
// --- Featured Image Preview ---
var imgInput = document.getElementById('postFeaturedImage');
@@ -39,6 +39,7 @@
<select class="form-control" id="type" name="type">
<option value="TEXT">Text</option>
<option value="HTML">Custom HTML</option>
<option value="COURSE_INFO">Thông tin Khóa học (Tự động)</option>
</select>
</div>
<div class="form-group">
@@ -101,163 +101,82 @@
</div>
</div>
<!-- Include Editor.js parser to render the content on the frontend -->
<!-- Content rendering and dynamic tabs parsing -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
// Content is now HTML
parsedContainer.innerHTML = rawData;
// Parse dynamic tabs from HTML (migrated from Editor.js)
var tabStarts = Array.from(parsedContainer.querySelectorAll('h3.dynamic-tab-start'));
if (tabStarts.length > 0) {
var tabsGroup = [];
var wrapper = document.createElement('div');
wrapper.className = 'custom-tabs-wrapper';
var navContainer = document.createElement('div');
navContainer.className = 'custom-tabs-nav-container';
var tabsContainer = document.createElement('div');
tabsContainer.className = 'tabs-container';
navContainer.appendChild(tabsContainer);
wrapper.appendChild(navContainer);
var firstTabStart = tabStarts[0];
var parentNode = firstTabStart.parentNode;
// Collect all elements into tabs
tabStarts.forEach(function(startEl, index) {
var tabTitle = startEl.textContent;
var btn = document.createElement('button');
btn.className = 'dao-tao-tab-btn custom-tab-btn' + (index === 0 ? ' active' : '');
btn.setAttribute('data-tab-idx', index);
btn.textContent = tabTitle;
tabsContainer.appendChild(btn);
var inTabs = false;
var tabsGroup = null;
function renderSingleBlock(block) {
var html = "";
switch(block.type) {
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "list":
var tag = block.data.style === "ordered" ? "ol" : "ul";
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "image":
html += "<figure><img src='" + block.data.file.url + "' alt='" + (block.data.caption || "") + "'><figcaption>" + (block.data.caption || "") + "</figcaption></figure>";
break;
case "tabStart":
case "tabEnd":
break;
default:
console.log("Unknown block type", block.type);
}
return html;
}
var blockList = data && data.blocks ? data.blocks : (typeof blocks !== 'undefined' ? blocks : []);
var finalHtml = "";
blockList.forEach(function(block) {
if (block.type === 'tabStart') {
if (!inTabs) {
inTabs = true;
tabsGroup = { tabs: [] };
}
tabsGroup.tabs.push({
title: block.data.title || 'Tab',
contentHtml: ''
});
return;
}
if (block.type === 'tabEnd') {
if (inTabs && tabsGroup) {
inTabs = false;
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
var panel = document.createElement('div');
panel.className = 'dao-tao-tab-panel custom-tab-panel';
panel.setAttribute('data-tab-idx', index);
panel.style.display = (index === 0) ? 'block' : 'none';
var currentEl = startEl.nextElementSibling;
while (currentEl && !currentEl.classList.contains('dynamic-tab-start') && !currentEl.classList.contains('dynamic-tab-end')) {
var nextEl = currentEl.nextElementSibling;
panel.appendChild(currentEl);
currentEl = nextEl;
}
// Remove the dynamic-tab-end if present
if (currentEl && currentEl.classList.contains('dynamic-tab-end')) {
currentEl.parentNode.removeChild(currentEl);
}
wrapper.appendChild(panel);
startEl.parentNode.removeChild(startEl);
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
parentNode.appendChild(wrapper);
// Tab click listener
wrapper.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var idx = btn.getAttribute('data-tab-idx');
wrapper.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
wrapper.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = wrapper.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
groupHtml += '</div>';
finalHtml += groupHtml;
tabsGroup = null;
}
return;
}
var blockHtml = renderSingleBlock(block);
if (inTabs && tabsGroup && tabsGroup.tabs.length > 0) {
tabsGroup.tabs[tabsGroup.tabs.length - 1].contentHtml += blockHtml;
} else {
finalHtml += blockHtml;
}
});
if (inTabs && tabsGroup) {
// Close unclosed tabs at the end
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
}
html = finalHtml;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
// Assign back to whatever variable the original script used.
html += "</" + tag + ">";
break;
case "image":
html += "<figure><img src='" + block.data.file.url + "' alt='" + (block.data.caption || "") + "'><figcaption>" + (block.data.caption || "") + "</figcaption></figure>";
break;
default:
break;
}
});
parsedContainer.innerHTML = html;
} else {
parsedContainer.innerHTML = rawData.replace(/\n/g, "<br>");
}
} catch(e) {
// Fallback to raw text if it is not JSON
parsedContainer.innerHTML = rawData.replace(/\n/g, "<br>");
}
});
</script>
@@ -167,175 +167,83 @@
</div>
</div>
<!-- Include Editor.js parser to render the content on the frontend -->
<!-- Because Editor.js saves as JSON, we need to convert it to HTML. -->
<!-- Content rendering and dynamic tabs parsing -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var rawData = document.getElementById("postContentRaw").textContent;
var parsedContainer = document.getElementById("postContentParsed");
try {
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
// Content is now HTML
parsedContainer.innerHTML = rawData;
// Parse dynamic tabs from HTML (migrated from Editor.js)
var tabStarts = Array.from(parsedContainer.querySelectorAll('h3.dynamic-tab-start'));
if (tabStarts.length > 0) {
var tabsGroup = [];
var wrapper = document.createElement('div');
wrapper.className = 'custom-tabs-wrapper';
var navContainer = document.createElement('div');
navContainer.className = 'custom-tabs-nav-container';
var tabsContainer = document.createElement('div');
tabsContainer.className = 'tabs-container';
navContainer.appendChild(tabsContainer);
wrapper.appendChild(navContainer);
var firstTabStart = tabStarts[0];
var parentNode = firstTabStart.parentNode;
// Collect all elements into tabs
tabStarts.forEach(function(startEl, index) {
var tabTitle = startEl.textContent;
var btn = document.createElement('button');
btn.className = 'dao-tao-tab-btn custom-tab-btn' + (index === 0 ? ' active' : '');
btn.setAttribute('data-tab-idx', index);
btn.textContent = tabTitle;
tabsContainer.appendChild(btn);
var inTabs = false;
var tabsGroup = null;
function renderSingleBlock(block) {
var html = "";
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
case "image":
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
break;
case "list":
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
html += "<" + tag + ">";
block.data.items.forEach(function(item) {
html += "<li>" + item + "</li>";
});
html += "</" + tag + ">";
break;
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
case "raw":
html += block.data.html;
break;
case "tabStart":
case "tabEnd":
break;
default:
console.log("Unknown block type", block.type);
}
return html;
}
var blockList = data && data.blocks ? data.blocks : (typeof blocks !== 'undefined' ? blocks : []);
var finalHtml = "";
blockList.forEach(function(block) {
if (block.type === 'tabStart') {
if (!inTabs) {
inTabs = true;
tabsGroup = { tabs: [] };
}
tabsGroup.tabs.push({
title: block.data.title || 'Tab',
contentHtml: ''
});
return;
}
if (block.type === 'tabEnd') {
if (inTabs && tabsGroup) {
inTabs = false;
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
var panel = document.createElement('div');
panel.className = 'dao-tao-tab-panel custom-tab-panel';
panel.setAttribute('data-tab-idx', index);
panel.style.display = (index === 0) ? 'block' : 'none';
var currentEl = startEl.nextElementSibling;
while (currentEl && !currentEl.classList.contains('dynamic-tab-start') && !currentEl.classList.contains('dynamic-tab-end')) {
var nextEl = currentEl.nextElementSibling;
panel.appendChild(currentEl);
currentEl = nextEl;
}
// Remove the dynamic-tab-end if present
if (currentEl && currentEl.classList.contains('dynamic-tab-end')) {
currentEl.parentNode.removeChild(currentEl);
}
wrapper.appendChild(panel);
startEl.parentNode.removeChild(startEl);
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
parentNode.appendChild(wrapper);
// Tab click listener
wrapper.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var idx = btn.getAttribute('data-tab-idx');
wrapper.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
wrapper.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = wrapper.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
groupHtml += '</div>';
finalHtml += groupHtml;
tabsGroup = null;
}
return;
}
var blockHtml = renderSingleBlock(block);
if (inTabs && tabsGroup && tabsGroup.tabs.length > 0) {
tabsGroup.tabs[tabsGroup.tabs.length - 1].contentHtml += blockHtml;
} else {
finalHtml += blockHtml;
}
});
if (inTabs && tabsGroup) {
// Close unclosed tabs at the end
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
}
html = finalHtml;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
// Assign back to whatever variable the original script used.
}
parsedContainer.innerHTML = html;
} catch(e) {
// If it's not JSON, it might just be HTML
parsedContainer.innerHTML = rawData;
}
// Fix for long paragraphs mistakenly formatted as headings with bold tags
var headings = parsedContainer.querySelectorAll('h1, h2, h3, h4, h5, h6');
headings.forEach(function(h) {
if (h.textContent.length > 100) {
var p = document.createElement('p');
p.innerHTML = h.innerHTML;
var strongs = p.querySelectorAll('strong, b');
strongs.forEach(function(s) {
var frag = document.createDocumentFragment();
while(s.firstChild) frag.appendChild(s.firstChild);
s.parentNode.replaceChild(frag, s);
});
h.parentNode.replaceChild(p, h);
}
});
});
</script>
</div>