refactor: clean up dao-tao-detail.html styling and implement course-info-widget fragment
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
script_replacement = """ <!-- Content rendering and dynamic tabs parsing -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
var rawData = document.getElementById("postContentRaw").textContent;
|
||||||
|
var parsedContainer = document.getElementById("postContentParsed");
|
||||||
|
|
||||||
|
// 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 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>"""
|
||||||
|
|
||||||
|
files = [
|
||||||
|
'src/main/resources/templates/posts/full-width.html',
|
||||||
|
'src/main/resources/templates/posts/livestream.html',
|
||||||
|
'src/main/resources/templates/posts/sidebar.html'
|
||||||
|
]
|
||||||
|
|
||||||
|
for file_path in files:
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Regex to match the old Editor.js script block
|
||||||
|
pattern = re.compile(r'<!-- Include Editor\.js parser.*?<\/script>', re.DOTALL)
|
||||||
|
|
||||||
|
new_content = pattern.sub(script_replacement, content)
|
||||||
|
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
print(f"Updated {file_path}")
|
||||||
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
script_replacement = """ <!-- Content rendering and dynamic tabs parsing -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
var rawData = document.getElementById("postContentRaw").textContent;
|
||||||
|
var parsedContainer = document.getElementById("postContentParsed");
|
||||||
|
|
||||||
|
// 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 training-course-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 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>"""
|
||||||
|
|
||||||
|
file_path = 'src/main/resources/templates/dao-tao-detail.html'
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Pattern matches from <!-- Tab Switching + Editor.js Parser Script --> to the end of the <script> tag before </div>
|
||||||
|
pattern = re.compile(r'<!-- Tab Switching \+ Editor\.js Parser Script -->.*?<\/script>', re.DOTALL)
|
||||||
|
new_content = pattern.sub(script_replacement, content)
|
||||||
|
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
print(f"Updated {file_path}")
|
||||||
@@ -1,191 +1 @@
|
|||||||
<style>
|
<table class='table table-bordered'><tbody><tr><td>TT</td><td>Tên bài</td><td>Mục tiêu bài học</td><td>Số tiết học</td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td>Tổn<br>g số</td><td>Lý<br>thuyế<br>t</td><td>Thực hành</td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td>Lab</td><td>Buồn<br>g<br>bệnh</td></tr><tr><td>01</td><td>Bức xạ tia x</td><td>- Trình bày được đơn vị đo bức xạ<br>tia X, thực hiện đúng quy định kỹ<br>thuật an toàn bức xạ tia X.<br>- Mô tả chính xác phương pháp đo<br>bức xạ tia X.</td><td>12</td><td>02</td><td>10</td><td></td></tr><tr><td>02</td><td>Nguyên lý tạo hình<br>ảnh trên dsa và an toàn<br>bức xạ tia x</td><td>- Mô tả được nguyên lý tạo hình<br>ảnh trên DSA.<br>- Vận dụng được các nguyên tắc<br>an toàn bức xạ tia X.</td><td>22</td><td>02</td><td>20</td><td></td></tr><tr><td>03</td><td>Thuốc cản quang trong<br>can thiệp nội mạch</td><td>- Phân biệt được các nhóm thuốc<br>cản quang hiện nay.<br>- Áp dụng đúng liều lượng thuốc<br>cản quang trong can thiệp nội<br>mạch, các phản ứng bất lợi của<br>thuốc cản quang và xử trí.</td><td>22</td><td>02</td><td>20</td><td></td></tr><tr><td>04</td><td>Bệnh thận do thuốc<br>cản quang</td><td>- Trình bày tiêu chuẩn chẩn<br>đoán của bệnh thận do thuốc cản<br>quang (CIN)<br>- Giải thích chính xác cơ chế<br>bệnh sinh của CIN<br>Đánh giá được nguy cơ và xây dựng<br>cách phòng ngừa CIN.</td><td>42</td><td>02</td><td>20</td><td>20</td></tr><tr><td>05</td><td>Hình ảnh Giải phẫu<br>học của gan</td><td>- Trình bày được giải phẩu các<br>phân thùy gan, các cấu trúc cố<br>định gan và liên quan của gan với<br>các tạng xung quanh.</td><td>24</td><td>04</td><td>20</td><td></td></tr></tbody></table>
|
||||||
.education-slider-wrapper {
|
|
||||||
position: relative;
|
|
||||||
padding: 0px;
|
|
||||||
/* Space for buttons */
|
|
||||||
}
|
|
||||||
|
|
||||||
.education-slider-container {
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.education-slider-wrapper .news-grid,
|
|
||||||
.education-slider-wrapper .row,
|
|
||||||
.education-slider-track {
|
|
||||||
display: flex !important;
|
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
gap: 20px;
|
|
||||||
margin: 0 !important;
|
|
||||||
transition: transform 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force children to be fixed width cards */
|
|
||||||
.education-slider-wrapper .news-grid>*,
|
|
||||||
.education-slider-wrapper .row>*,
|
|
||||||
.education-slider-track>* {
|
|
||||||
flex: 0 0 calc(33.333% - 14px) !important;
|
|
||||||
/* show 3 cards at a time */
|
|
||||||
max-width: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
|
|
||||||
.education-slider-wrapper .news-grid>*,
|
|
||||||
.education-slider-wrapper .row>*,
|
|
||||||
.education-slider-track>* {
|
|
||||||
flex: 0 0 calc(50% - 10px) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
|
|
||||||
.education-slider-wrapper .news-grid>*,
|
|
||||||
.education-slider-wrapper .row>*,
|
|
||||||
.education-slider-track>* {
|
|
||||||
flex: 0 0 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.edu-slider-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(200, 200, 200, 0.5);
|
|
||||||
/* mờ / transparent */
|
|
||||||
color: #333;
|
|
||||||
border: none;
|
|
||||||
font-size: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 10;
|
|
||||||
transition: background-color 0.2s, color 0.2s;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edu-slider-btn:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edu-slider-btn.prev-btn {
|
|
||||||
left: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edu-slider-btn.next-btn {
|
|
||||||
right: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edu-slider-btn:disabled {
|
|
||||||
opacity: 0.2;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="cc--component-container cc--news-events">
|
|
||||||
<div class="c--component c--news-events">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="news-events-row">
|
|
||||||
<div class="education-column news-column" style="width: 100%;">
|
|
||||||
<div
|
|
||||||
style="display: flex; justify-content: space-between; align-items: baseline; border-bottom: 2px solid var(--color-brand); margin-bottom: 16px;">
|
|
||||||
<!-- Thêm flex-grow: 1 vào style của h2 -->
|
|
||||||
<h2 class="section-title--news"
|
|
||||||
style="font-size: 2rem; margin: 0; padding-bottom: 4px; border-bottom: unset;">ĐÀO TẠO</h2>
|
|
||||||
<a href="/news/education" class="news-events-section__cta news-events-section__cta--desktop"
|
|
||||||
style="color: var(--color-brand); font-weight: 800; font-size: 16px; text-transform: uppercase; text-decoration: none; border-bottom: unset;">XEM
|
|
||||||
THÊM</a>
|
|
||||||
</div>
|
|
||||||
<div class="education-slider-wrapper" style="padding-right: 0;">
|
|
||||||
<button class="edu-slider-btn prev-btn" aria-label="Previous">‹</button>
|
|
||||||
<div class="education-slider-container">
|
|
||||||
<div class="education-slider-track">
|
|
||||||
[component:news-grid data-source="posts:tag=education,limit=5,collection=news_posts"]
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="edu-slider-btn next-btn" aria-label="Next">›</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div><!-- Mobile Footer CTA -->
|
|
||||||
<div class="news-events-section__footer"><a href="/news/education"
|
|
||||||
class="news-events-section__cta news-events-section__cta--mobile">Xem Thêm</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const wrappers = document.querySelectorAll('.education-slider-wrapper');
|
|
||||||
wrappers.forEach(wrapper => {
|
|
||||||
const prevBtn = wrapper.querySelector('.prev-btn');
|
|
||||||
const nextBtn = wrapper.querySelector('.next-btn');
|
|
||||||
|
|
||||||
let track = wrapper.querySelector('.education-slider-track');
|
|
||||||
if (track && track.children.length === 1 && (track.children[0].classList.contains('news-grid') || track.children[0].classList.contains('row'))) {
|
|
||||||
track = track.children[0];
|
|
||||||
track.classList.add('education-slider-track');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!track) return;
|
|
||||||
|
|
||||||
let currentIndex = 0;
|
|
||||||
|
|
||||||
function updateSlider() {
|
|
||||||
const items = track.children;
|
|
||||||
if(items.length === 0) return;
|
|
||||||
const itemWidth = items[0].getBoundingClientRect().width;
|
|
||||||
|
|
||||||
const gap = parseFloat(window.getComputedStyle(track).gap) || 20;
|
|
||||||
track.style.transform = `translateX(-${currentIndex * (itemWidth + gap)}px)`;
|
|
||||||
|
|
||||||
const containerWidth = wrapper.querySelector('.education-slider-container').getBoundingClientRect().width;
|
|
||||||
const visibleCount = Math.max(1, Math.floor((containerWidth + gap) / (itemWidth + gap)));
|
|
||||||
|
|
||||||
prevBtn.disabled = currentIndex === 0;
|
|
||||||
nextBtn.disabled = currentIndex >= items.length - visibleCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevBtn.addEventListener('click', () => {
|
|
||||||
if (currentIndex > 0) {
|
|
||||||
currentIndex--;
|
|
||||||
updateSlider();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nextBtn.addEventListener('click', () => {
|
|
||||||
const items = track.children;
|
|
||||||
const itemWidth = items[0].getBoundingClientRect().width;
|
|
||||||
const gap = parseFloat(window.getComputedStyle(track).gap) || 20;
|
|
||||||
const containerWidth = wrapper.querySelector('.education-slider-container').getBoundingClientRect().width;
|
|
||||||
const visibleCount = Math.max(1, Math.floor((containerWidth + gap) / (itemWidth + gap)));
|
|
||||||
|
|
||||||
if (currentIndex < items.length - visibleCount) {
|
|
||||||
currentIndex++;
|
|
||||||
updateSlider();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
const items = track.children;
|
|
||||||
if(items.length === 0) return;
|
|
||||||
const itemWidth = items[0].getBoundingClientRect().width;
|
|
||||||
const gap = parseFloat(window.getComputedStyle(track).gap) || 20;
|
|
||||||
const containerWidth = wrapper.querySelector('.education-slider-container').getBoundingClientRect().width;
|
|
||||||
const visibleCount = Math.max(1, Math.floor((containerWidth + gap) / (itemWidth + gap)));
|
|
||||||
|
|
||||||
if (currentIndex > items.length - visibleCount) {
|
|
||||||
currentIndex = Math.max(0, items.length - visibleCount);
|
|
||||||
}
|
|
||||||
updateSlider();
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(updateSlider, 200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
Binary file not shown.
@@ -1,16 +1,12 @@
|
|||||||
package com.sisvietnamvn.web.config;
|
package com.sisvietnamvn.web.config;
|
||||||
|
|
||||||
import com.sisvietnamvn.web.domain.Post;
|
import com.sisvietnamvn.web.domain.Post;
|
||||||
import com.sisvietnamvn.web.domain.PageStatus;
|
|
||||||
import com.sisvietnamvn.web.repository.PostRepository;
|
import com.sisvietnamvn.web.repository.PostRepository;
|
||||||
import org.springframework.boot.CommandLineRunner;
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import java.util.Optional;
|
import java.util.List;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class PostMigrator implements CommandLineRunner {
|
public class PostMigrator implements CommandLineRunner {
|
||||||
@@ -23,57 +19,110 @@ public class PostMigrator implements CommandLineRunner {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(String... args) throws Exception {
|
public void run(String... args) throws Exception {
|
||||||
String[] slugs = {
|
System.out.println("=== STARTING JSON TO HTML MIGRATION ===");
|
||||||
"can-thiep-mach-mau-than-kinh-nang-cao",
|
|
||||||
"can-thiep-mach-mau-cac-tang-va-ngoai-bien-co-ban",
|
|
||||||
"chuan-bi-dung-cu-va-cham-soc-benh-nhan-trong-phong-chup-mach",
|
|
||||||
"tim-mach-can-thiep-co-ban"
|
|
||||||
};
|
|
||||||
|
|
||||||
String[] titles = {
|
List<Post> posts = postRepository.findAll();
|
||||||
"Can thiệp mạch máu thần kinh nâng cao",
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
"Can thiệp mạch máu các tạng và mạch máu ngoại biên cơ bản",
|
int count = 0;
|
||||||
"Chuẩn bị dụng cụ và chăm sóc bệnh nhân trong phòng chụp mạch",
|
|
||||||
"Tim mạch can thiệp cơ bản"
|
|
||||||
};
|
|
||||||
|
|
||||||
System.out.println("=== STARTING MIGRATION FOR 4 COURSES ===");
|
for (Post post : posts) {
|
||||||
|
String content = post.getContent();
|
||||||
for (int i = 0; i < slugs.length; i++) {
|
if (content != null && content.trim().startsWith("{") && content.contains("\"blocks\"")) {
|
||||||
String slug = slugs[i];
|
System.out.println("Migrating post: " + post.getTitle());
|
||||||
String title = titles[i];
|
String html = convertEditorJsToHtml(content, mapper);
|
||||||
String path = "pdf_json_output/" + slug + ".json";
|
post.setContent(html);
|
||||||
|
postRepository.save(post);
|
||||||
if (!Files.exists(Paths.get(path))) {
|
count++;
|
||||||
System.out.println("File not found: " + path);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String json = new String(Files.readAllBytes(Paths.get(path)));
|
|
||||||
|
|
||||||
Optional<Post> opt = postRepository.findBySlug(slug);
|
|
||||||
Post post;
|
|
||||||
if (opt.isPresent()) {
|
|
||||||
post = opt.get();
|
|
||||||
System.out.println("Updating post: " + post.getTitle());
|
|
||||||
} else {
|
|
||||||
System.out.println("Creating new post: " + title);
|
|
||||||
post = new Post();
|
|
||||||
post.setSlug(slug);
|
|
||||||
post.setTitle(title);
|
|
||||||
post.setStatus(PageStatus.PUBLISHED);
|
|
||||||
post.setCreatedDate(Instant.now());
|
|
||||||
post.setLastModifiedDate(Instant.now());
|
|
||||||
post.setCreatedBy("admin");
|
|
||||||
post.setLastModifiedBy("admin");
|
|
||||||
post.setExcerpt("Khóa học " + title + " tại Bệnh viện Đa khoa Quốc tế S.I.S Cần Thơ.");
|
|
||||||
post.setEventTime(Instant.now().plus(30 + i * 10, ChronoUnit.DAYS)); // dummy start date
|
|
||||||
}
|
|
||||||
|
|
||||||
post.setContent(json);
|
|
||||||
postRepository.save(post);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("=== SUCCESSFULLY MIGRATED 4 COURSES ===");
|
System.out.println("=== SUCCESSFULLY MIGRATED " + count + " POSTS TO HTML ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String convertEditorJsToHtml(String json, ObjectMapper mapper) {
|
||||||
|
try {
|
||||||
|
JsonNode root = mapper.readTree(json);
|
||||||
|
JsonNode blocks = root.get("blocks");
|
||||||
|
if (blocks == null || !blocks.isArray()) return json;
|
||||||
|
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
|
||||||
|
for (JsonNode block : blocks) {
|
||||||
|
String type = block.get("type").asText();
|
||||||
|
JsonNode data = block.get("data");
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case "header":
|
||||||
|
int level = data.has("level") ? data.get("level").asInt() : 2;
|
||||||
|
String text = data.has("text") ? data.get("text").asText() : "";
|
||||||
|
html.append("<h").append(level).append(">").append(text).append("</h").append(level).append(">\n");
|
||||||
|
break;
|
||||||
|
case "paragraph":
|
||||||
|
String ptext = data.has("text") ? data.get("text").asText() : "";
|
||||||
|
html.append("<p>").append(ptext).append("</p>\n");
|
||||||
|
break;
|
||||||
|
case "image":
|
||||||
|
String url = data.has("file") && data.get("file").has("url") ? data.get("file").get("url").asText() :
|
||||||
|
(data.has("url") ? data.get("url").asText() : "");
|
||||||
|
String caption = data.has("caption") ? data.get("caption").asText() : "";
|
||||||
|
html.append("<figure><img src='").append(url).append("' alt='").append(caption).append("'><figcaption>").append(caption).append("</figcaption></figure>\n");
|
||||||
|
break;
|
||||||
|
case "list":
|
||||||
|
String style = data.has("style") ? data.get("style").asText() : "unordered";
|
||||||
|
String tag = style.equals("ordered") ? "ol" : "ul";
|
||||||
|
html.append("<").append(tag).append(">\n");
|
||||||
|
JsonNode items = data.get("items");
|
||||||
|
if (items != null && items.isArray()) {
|
||||||
|
for (JsonNode item : items) {
|
||||||
|
html.append("<li>").append(item.asText()).append("</li>\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html.append("</").append(tag).append(">\n");
|
||||||
|
break;
|
||||||
|
case "quote":
|
||||||
|
String qtext = data.has("text") ? data.get("text").asText() : "";
|
||||||
|
String qcaption = data.has("caption") ? data.get("caption").asText() : "";
|
||||||
|
html.append("<blockquote>").append(qtext).append(" <cite>").append(qcaption).append("</cite></blockquote>\n");
|
||||||
|
break;
|
||||||
|
case "table":
|
||||||
|
html.append("<figure class='table' style='width:100%'><table>\n");
|
||||||
|
JsonNode content = data.get("content");
|
||||||
|
boolean withHeadings = data.has("withHeadings") && data.get("withHeadings").asBoolean();
|
||||||
|
if (content != null && content.isArray()) {
|
||||||
|
for (int i = 0; i < content.size(); i++) {
|
||||||
|
JsonNode row = content.get(i);
|
||||||
|
html.append("<tr>");
|
||||||
|
if (row.isArray()) {
|
||||||
|
for (JsonNode cell : row) {
|
||||||
|
String cellTag = (withHeadings && i == 0) ? "th" : "td";
|
||||||
|
html.append("<").append(cellTag).append(">").append(cell.asText()).append("</").append(cellTag).append(">");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html.append("</tr>\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html.append("</table></figure>\n");
|
||||||
|
break;
|
||||||
|
case "raw":
|
||||||
|
String rtext = data.has("html") ? data.get("html").asText() : "";
|
||||||
|
html.append(rtext).append("\n");
|
||||||
|
break;
|
||||||
|
case "tabStart":
|
||||||
|
String ttitle = data.has("title") ? data.get("title").asText() : "Tab";
|
||||||
|
// Use a special class so frontend can parse it back into tabs if needed
|
||||||
|
html.append("<h3 class='dynamic-tab-start'>").append(ttitle).append("</h3>\n");
|
||||||
|
break;
|
||||||
|
case "tabEnd":
|
||||||
|
html.append("<hr class='dynamic-tab-end'/>\n");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return json; // Fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
--spacing-md: 20px;
|
--spacing-md: 20px;
|
||||||
--radius-md: 4px;
|
--radius-md: 4px;
|
||||||
--radius-lg: 8px;
|
--radius-lg: 8px;
|
||||||
|
|
||||||
|
--spacing: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
@@ -1083,7 +1085,6 @@ i, .btn-outline-primary {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 2rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1095,6 +1096,7 @@ i, .btn-outline-primary {
|
|||||||
gap: 4px; /* gap-x-1 */
|
gap: 4px; /* gap-x-1 */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 860px;
|
max-width: 860px;
|
||||||
|
margin-bottom:calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -1125,8 +1127,88 @@ i, .btn-outline-primary {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-tab-btn.active {
|
/* .custom-tab-btn.active {
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
color: var(--color-old-brick) !important; /* text-primary-600 */
|
color: var(--color-old-brick) !important; /* text-primary-600 */
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
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>
|
<head>
|
||||||
<title th:text="${isNew} ? 'Add New Post' : 'Edit Post'">Post Form</title>
|
<title th:text="${isNew} ? 'Add New Post' : 'Edit Post'">Post Form</title>
|
||||||
<style>
|
<style>
|
||||||
/* Editor.js container styling */
|
/* TinyMCE Editor styling */
|
||||||
#editorjs {
|
.tox-tinymce {
|
||||||
border: 1px solid #d1d3e2;
|
|
||||||
border-radius: 0.35rem;
|
border-radius: 0.35rem;
|
||||||
padding: 16px 12px;
|
border: 1px solid #d1d3e2;
|
||||||
min-height: 350px;
|
|
||||||
background: #fff;
|
|
||||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#editorjs:focus-within {
|
.tox:focus-within {
|
||||||
border-color: #bac8f3;
|
border-color: #bac8f3;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
|
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 styling */
|
||||||
.tag-input-container {
|
.tag-input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -166,7 +116,10 @@
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="fas fa-link"></i> Permalink:
|
<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>
|
</small>
|
||||||
<input type="hidden" id="postSlug" th:field="*{slug}">
|
<input type="hidden" id="postSlug" th:field="*{slug}">
|
||||||
</div>
|
</div>
|
||||||
@@ -184,27 +137,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="editor-plugin-info mb-2">
|
<!-- TinyMCE Editor Container -->
|
||||||
Active blocks:
|
<textarea id="editorContent" name="content" th:text="*{content}" style="height: 600px; width: 100%;"></textarea>
|
||||||
<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}">
|
|
||||||
|
|
||||||
<small class="form-text text-muted mt-2">
|
<small class="form-text text-muted mt-2">
|
||||||
<i class="fas fa-info-circle"></i>
|
<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>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -366,92 +303,252 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Editor.js Scripts (injected via layout:fragment="scripts") -->
|
<!-- TinyMCE Script (injected via layout:fragment="scripts") -->
|
||||||
<section layout:fragment="scripts">
|
<section layout:fragment="scripts">
|
||||||
<!-- Editor.js Core -->
|
<!-- TinyMCE Core (via CDN) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.30.8/dist/editorjs.umd.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.3/tinymce.min.js" referrerpolicy="origin"></script>
|
||||||
|
|
||||||
<!-- Built-in Block Tools -->
|
<!-- Initialize TinyMCE -->
|
||||||
<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 -->
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Register Plugins
|
tinymce.init({
|
||||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
selector: '#editorContent',
|
||||||
if (typeof RawTool !== 'undefined') {
|
height: 600,
|
||||||
window.SISEditorPlugins['raw'] = { class: RawTool };
|
menubar: true,
|
||||||
}
|
plugins: [
|
||||||
if (typeof TabSplitTool !== 'undefined') {
|
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||||
window.SISEditorPlugins['tabSplit'] = { class: TabSplitTool };
|
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||||
}
|
'insertdatetime', 'media', 'table', 'help', 'wordcount'
|
||||||
|
],
|
||||||
if (typeof TrainingCourseInfoTool !== 'undefined') {
|
toolbar: 'code | undo redo | blocks | ' +
|
||||||
}
|
'bold italic backcolor | alignleft aligncenter ' +
|
||||||
if (typeof TabStartTool !== 'undefined') {
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
window.SISEditorPlugins['tabStart'] = { class: TabStartTool };
|
'table tablemergecells tablesplitcells | ' +
|
||||||
}
|
'insertTabStart insertTabEnd editClassId | previewTabs | removeformat | help',
|
||||||
if (typeof TabEndTool !== 'undefined') {
|
toolbar_mode: 'wrap',
|
||||||
window.SISEditorPlugins['tabEnd'] = { class: TabEndTool };
|
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 ---
|
setup: function (editor) {
|
||||||
var existingContent = document.getElementById('editorContent').value;
|
editor.on('change', function () {
|
||||||
var initialData = null;
|
tinymce.triggerSave();
|
||||||
|
});
|
||||||
if (existingContent && existingContent.trim() !== '') {
|
|
||||||
try {
|
editor.ui.registry.addButton('insertTabStart', {
|
||||||
initialData = JSON.parse(existingContent);
|
text: 'Thêm Tab',
|
||||||
} catch (e) {
|
tooltip: 'Tạo một Tab ngang mới (Dành cho Chi tiết khoá học)',
|
||||||
console.warn('[SIS Editor] Existing content is not valid JSON, treating as Raw HTML.');
|
onAction: function (_) {
|
||||||
initialData = {
|
var tabName = prompt('Nhập tiêu đề Tab (ví dụ: Tổng quan):', 'Tiêu đề Tab');
|
||||||
time: Date.now(),
|
if (tabName) {
|
||||||
blocks: [
|
editor.insertContent('<h3 class="dynamic-tab-start">' + tabName + '</h3><p>Nhập nội dung của Tab vào đây...</p>');
|
||||||
{
|
|
||||||
type: "raw",
|
|
||||||
data: {
|
|
||||||
html: existingContent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
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> </p><hr class="dynamic-tab-end"/><p> </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><' + node.nodeName.toLowerCase() + '></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);
|
// Remove the initial JSON warning for TinyMCE
|
||||||
|
var existingContent = document.getElementById('editorContent').value;
|
||||||
// Show badges for any injected plugins
|
if (existingContent && existingContent.trim().startsWith('{') && existingContent.includes('"blocks"')) {
|
||||||
var pluginBadges = document.getElementById('pluginBadges');
|
console.warn('[SIS Editor] Found raw JSON in editor area. This post might not have been migrated correctly!');
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Unsaved Changes Warning ---
|
// --- Unsaved Changes Warning ---
|
||||||
@@ -468,25 +565,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Full Screen Mode Toggle ---
|
// --- 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 fsBtn = document.getElementById('toggleFullscreenBtn');
|
||||||
var fsIcon = fsBtn.querySelector('i');
|
if (fsBtn) {
|
||||||
|
fsBtn.style.display = 'none';
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Featured Image Preview ---
|
// --- Featured Image Preview ---
|
||||||
var imgInput = document.getElementById('postFeaturedImage');
|
var imgInput = document.getElementById('postFeaturedImage');
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<select class="form-control" id="type" name="type">
|
<select class="form-control" id="type" name="type">
|
||||||
<option value="TEXT">Text</option>
|
<option value="TEXT">Text</option>
|
||||||
<option value="HTML">Custom HTML</option>
|
<option value="HTML">Custom HTML</option>
|
||||||
|
<option value="COURSE_INFO">Thông tin Khóa học (Tự động)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -101,163 +101,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Include Editor.js parser to render the content on the frontend -->
|
<!-- Content rendering and dynamic tabs parsing -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
var rawData = document.getElementById("postContentRaw").textContent;
|
var rawData = document.getElementById("postContentRaw").textContent;
|
||||||
var parsedContainer = document.getElementById("postContentParsed");
|
var parsedContainer = document.getElementById("postContentParsed");
|
||||||
|
|
||||||
try {
|
// Content is now HTML
|
||||||
var data = JSON.parse(rawData);
|
parsedContainer.innerHTML = rawData;
|
||||||
var html = "";
|
|
||||||
if (data.blocks) {
|
// 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 panel = document.createElement('div');
|
||||||
var tabsGroup = null;
|
panel.className = 'dao-tao-tab-panel custom-tab-panel';
|
||||||
|
panel.setAttribute('data-tab-idx', index);
|
||||||
function renderSingleBlock(block) {
|
panel.style.display = (index === 0) ? 'block' : 'none';
|
||||||
var html = "";
|
|
||||||
switch(block.type) {
|
var currentEl = startEl.nextElementSibling;
|
||||||
|
while (currentEl && !currentEl.classList.contains('dynamic-tab-start') && !currentEl.classList.contains('dynamic-tab-end')) {
|
||||||
case "paragraph":
|
var nextEl = currentEl.nextElementSibling;
|
||||||
html += "<p>" + block.data.text + "</p>";
|
panel.appendChild(currentEl);
|
||||||
break;
|
currentEl = nextEl;
|
||||||
case "header":
|
}
|
||||||
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
|
|
||||||
break;
|
// Remove the dynamic-tab-end if present
|
||||||
case "list":
|
if (currentEl && currentEl.classList.contains('dynamic-tab-end')) {
|
||||||
var tag = block.data.style === "ordered" ? "ol" : "ul";
|
currentEl.parentNode.removeChild(currentEl);
|
||||||
html += "<" + tag + ">";
|
}
|
||||||
block.data.items.forEach(function(item) {
|
|
||||||
html += "<li>" + item + "</li>";
|
wrapper.appendChild(panel);
|
||||||
});
|
startEl.parentNode.removeChild(startEl);
|
||||||
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>';
|
|
||||||
});
|
});
|
||||||
groupHtml += '</div>';
|
|
||||||
|
parentNode.appendChild(wrapper);
|
||||||
tabsGroup.tabs.forEach(function(tab, index) {
|
|
||||||
var displayStyle = (index === 0) ? 'block' : 'none';
|
// Tab click listener
|
||||||
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
|
wrapper.addEventListener('click', function(e) {
|
||||||
groupHtml += tab.contentHtml;
|
if (e.target.classList.contains('custom-tab-btn')) {
|
||||||
groupHtml += '</div>';
|
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>
|
</script>
|
||||||
|
|||||||
@@ -167,175 +167,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Include Editor.js parser to render the content on the frontend -->
|
<!-- Content rendering and dynamic tabs parsing -->
|
||||||
<!-- Because Editor.js saves as JSON, we need to convert it to HTML. -->
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
var rawData = document.getElementById("postContentRaw").textContent;
|
var rawData = document.getElementById("postContentRaw").textContent;
|
||||||
var parsedContainer = document.getElementById("postContentParsed");
|
var parsedContainer = document.getElementById("postContentParsed");
|
||||||
try {
|
|
||||||
var data = JSON.parse(rawData);
|
// Content is now HTML
|
||||||
var html = "";
|
parsedContainer.innerHTML = rawData;
|
||||||
if (data.blocks) {
|
|
||||||
|
// 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 panel = document.createElement('div');
|
||||||
var tabsGroup = null;
|
panel.className = 'dao-tao-tab-panel custom-tab-panel';
|
||||||
|
panel.setAttribute('data-tab-idx', index);
|
||||||
function renderSingleBlock(block) {
|
panel.style.display = (index === 0) ? 'block' : 'none';
|
||||||
var html = "";
|
|
||||||
switch(block.type) {
|
var currentEl = startEl.nextElementSibling;
|
||||||
|
while (currentEl && !currentEl.classList.contains('dynamic-tab-start') && !currentEl.classList.contains('dynamic-tab-end')) {
|
||||||
case "header":
|
var nextEl = currentEl.nextElementSibling;
|
||||||
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
|
panel.appendChild(currentEl);
|
||||||
break;
|
currentEl = nextEl;
|
||||||
case "paragraph":
|
}
|
||||||
html += "<p>" + block.data.text + "</p>";
|
|
||||||
break;
|
// Remove the dynamic-tab-end if present
|
||||||
case "image":
|
if (currentEl && currentEl.classList.contains('dynamic-tab-end')) {
|
||||||
html += "<figure><img src='" + block.data.url + "' alt='" + (block.data.caption || '') + "'><figcaption>" + (block.data.caption || '') + "</figcaption></figure>";
|
currentEl.parentNode.removeChild(currentEl);
|
||||||
break;
|
}
|
||||||
case "list":
|
|
||||||
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
|
wrapper.appendChild(panel);
|
||||||
html += "<" + tag + ">";
|
startEl.parentNode.removeChild(startEl);
|
||||||
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>';
|
|
||||||
});
|
});
|
||||||
groupHtml += '</div>';
|
|
||||||
|
parentNode.appendChild(wrapper);
|
||||||
tabsGroup.tabs.forEach(function(tab, index) {
|
|
||||||
var displayStyle = (index === 0) ? 'block' : 'none';
|
// Tab click listener
|
||||||
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
|
wrapper.addEventListener('click', function(e) {
|
||||||
groupHtml += tab.contentHtml;
|
if (e.target.classList.contains('custom-tab-btn')) {
|
||||||
groupHtml += '</div>';
|
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>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user