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>
|
||||
.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>
|
||||
<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>
|
||||
Binary file not shown.
@@ -1,16 +1,12 @@
|
||||
package com.sisvietnamvn.web.config;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Post;
|
||||
import com.sisvietnamvn.web.domain.PageStatus;
|
||||
import com.sisvietnamvn.web.repository.PostRepository;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Optional;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
@Component
|
||||
public class PostMigrator implements CommandLineRunner {
|
||||
@@ -23,57 +19,110 @@ public class PostMigrator implements CommandLineRunner {
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
String[] slugs = {
|
||||
"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"
|
||||
};
|
||||
System.out.println("=== STARTING JSON TO HTML MIGRATION ===");
|
||||
|
||||
String[] titles = {
|
||||
"Can thiệp mạch máu thần kinh nâng cao",
|
||||
"Can thiệp mạch máu các tạng và mạch máu ngoại biên cơ bản",
|
||||
"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"
|
||||
};
|
||||
List<Post> posts = postRepository.findAll();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
int count = 0;
|
||||
|
||||
System.out.println("=== STARTING MIGRATION FOR 4 COURSES ===");
|
||||
|
||||
for (int i = 0; i < slugs.length; i++) {
|
||||
String slug = slugs[i];
|
||||
String title = titles[i];
|
||||
String path = "pdf_json_output/" + slug + ".json";
|
||||
|
||||
if (!Files.exists(Paths.get(path))) {
|
||||
System.out.println("File not found: " + path);
|
||||
continue;
|
||||
for (Post post : posts) {
|
||||
String content = post.getContent();
|
||||
if (content != null && content.trim().startsWith("{") && content.contains("\"blocks\"")) {
|
||||
System.out.println("Migrating post: " + post.getTitle());
|
||||
String html = convertEditorJsToHtml(content, mapper);
|
||||
post.setContent(html);
|
||||
postRepository.save(post);
|
||||
count++;
|
||||
}
|
||||
|
||||
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;
|
||||
--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> </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);
|
||||
|
||||
// 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>
|
||||
|
||||
Reference in New Issue
Block a user