Files
uldp-degree-mangement-system/views/admin/terms/index.ejs
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

1210 lines
54 KiB
Plaintext

<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-2" style="color: var(--primary-dark);">
Terms & Conditions Editor
</h1>
<p class="text-muted mb-0">
<a href="/admin/info" class="text-decoration-none">
<i class="fas fa-arrow-left me-1"></i>Back to Information Pages
</a>
</p>
</div>
<div class="d-flex gap-2">
<button type="submit" form="termsForm" class="btn btn-primary" id="saveBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</div>
<form id="termsForm" action="/admin/terms/update" method="POST" class="needs-validation" novalidate>
<!-- Hidden inputs -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="page" id="pageJson">
<input type="hidden" name="content" id="contentJson">
<div class="row">
<!-- Main Content Column -->
<div class="col-lg-8">
<!-- Page Information Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Page Title <span class="text-danger">*</span></label>
<textarea class="form-control" id="pageTitle" rows="2" required><%= data.page?.title || 'Terms & Conditions Go and Grow Camp e.K.' %></textarea>
<div class="invalid-feedback">Please enter a page title</div>
</div>
</div>
</div>
<!-- Hero Section Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Hero Section</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-5">
<div class="mb-3">
<label class="form-label">Background Image</label>
<div class="input-group">
<input type="text" class="form-control" id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '/uploads/terms/faqimage.jpg' %>" readonly>
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-image-type="terms">
<i class="fas fa-upload"></i>
</button>
</div>
<small class="form-text text-muted">Recommended size: 1920x1080px</small>
</div>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px; width: 100%;">
<% if (data.hero?.backgroundImage) { %>
<%
let heroImgSrc = data.hero.backgroundImage;
if (heroImgSrc && !heroImgSrc.startsWith('http://') && !heroImgSrc.startsWith('https://')) {
heroImgSrc = heroImgSrc.startsWith('/') ? heroImgSrc : '/' + heroImgSrc;
}
%>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
No image selected
</div>
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label">Hero Title</label>
<input type="text" class="form-control" id="heroTitle"
value="<%= data.hero?.title || 'Go and Grow Camp' %>">
</div>
</div>
</div>
</div>
<!-- Content Editor Section -->
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Editor</h5>
<p class="text-muted mb-0 small">Write your Terms & Conditions content just like a blog post</p>
</div>
<div class="card-body">
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
</div>
</div>
</div>
<!-- Sidebar Column -->
<div class="col-lg-4">
<!-- SEO Settings Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">SEO Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" class="form-control" id="metadataTitle"
value="<%= data.page?.metadata?.title || '' %>">
<small class="text-muted">Title for search engines (max 60 characters)</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea class="form-control" id="metadataDescription" rows="3"
placeholder="Meta description for SEO"><%= data.page?.metadata?.description || '' %></textarea>
<small class="text-muted">Description for search engines (max 160 characters)</small>
</div>
</div>
</div>
<!-- Page Settings Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Last Updated Date</label>
<input type="date" class="form-control" id="footerNoteLastUpdated"
value="<%= data.footerNote?.lastUpdated || '' %>">
</div>
<div class="mb-3">
<label class="form-label">Footer Text</label>
<input type="text" class="form-control" id="footerNoteText"
value="<%= data.footerNote?.text || '' %>"
placeholder="e.g., Last updated on...">
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enableScrollspy"
<%= data.layout?.enableScrollspy ? 'checked' : '' %>>
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
</div>
<small class="text-muted">Creates table of contents from headers</small>
</div>
</div>
</div>
<!-- Content Tips Card -->
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Tips</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<h6><i class="fas fa-lightbulb me-2"></i>Tips for Terms & Conditions:</h6>
<ul class="mb-0 small">
<li>Use <strong>Header 2</strong> for main sections</li>
<li>Use <strong>Header 3</strong> for subsections</li>
<li>Use <strong>Lists</strong> for terms items</li>
<li>Use <strong>Conclusion</strong> tool for important notes</li>
<li>Use <strong>Quote</strong> for legal references</li>
</ul>
<hr class="my-2">
<h6><i class="fas fa-keyboard me-2"></i>Keyboard Shortcuts:</h6>
<ul class="mb-0 small">
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd>: Convert list item to header</li>
<li><kbd>Tab</kbd> in list: Indent item</li>
<li><kbd>Backspace</kbd> at start: Exit list</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Terms & Conditions Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<iframe id="previewFrame" style="width: 100%; height: 600px; border: none;"></iframe>
</div>
</div>
</div>
</div>
<!-- Hidden inputs for file upload -->
<input type="file" id="directImageUpload" style="display: none;">
<input type="hidden" id="currentImageType">
<input type="hidden" id="currentTargetInput">
<!-- Editor.js Dependencies -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
<script>
// Terms Content Manager - Simple version
class TermsContentManager {
// Convert Editor.js data to Terms content structure
convertEditorToTerms(editorData) {
const contentItems = [];
let currentSection = null;
editorData.blocks.forEach(block => {
switch (block.type) {
case 'paragraph':
contentItems.push({
type: 'paragraph',
text: block.data.text
});
break;
case 'header':
// Xử lý các header levels khác nhau
const text = block.data.text;
const level = block.data.level || 2;
const isMainSection = /^\d+\./.test(text);
if (isMainSection) {
// Section chính (bắt đầu bằng số như "1.", "2.")
contentItems.push({
type: 'section',
title: text,
content: '', // Will be filled by following paragraphs
subsections: []
});
} else {
// Lưu header với level cụ thể (h2, h3, h4)
contentItems.push({
type: 'header',
level: level,
text: text
});
}
break;
case 'list':
const lastItem = contentItems[contentItems.length - 1];
// Check if this is a cancellation table
if (lastItem?.type === 'section' && block.data.items.some(item =>
item.toLowerCase().includes('cancellation') ||
item.toLowerCase().includes('fee') ||
item.toLowerCase().includes('standard')
)) {
// This might be a cancellation table
const isCancellationTable = block.data.items.some(item =>
item.toLowerCase().includes('days before')
);
if (isCancellationTable) {
lastItem.subsections = lastItem.subsections || [];
lastItem.subsections.push({
type: 'cancellation_table',
title: 'Standard Cancellation Fees',
items: block.data.items
});
} else if (block.data.items.some(item => item.toLowerCase().includes('school group'))) {
lastItem.subsections = lastItem.subsections || [];
lastItem.subsections.push({
type: 'cancellation_section',
title: 'Cancellation policy for school groups:',
items: block.data.items
});
} else {
// Regular list - store as list type with items array
contentItems.push({
type: 'list',
style: block.data.style || 'unordered',
items: block.data.items
});
}
} else {
// Regular list - store as list type with items array
contentItems.push({
type: 'list',
style: block.data.style || 'unordered',
items: block.data.items
});
}
break;
case 'quote':
contentItems.push({
type: 'note',
text: block.data.text
});
break;
case 'embed':
// store embed as separate item (YouTube support)
const source = block.data.source || block.data.embed || '';
const videoId = (source.match(/(?:youtu\.be\/|youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/) || [])[1]
|| (block.data.embed && (block.data.embed.match(/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/) || [])[1])
|| '';
contentItems.push({
type: 'embed',
url: source,
videoId: videoId,
title: block.data.title || '',
caption: block.data.caption || ''
});
break;
case 'image':
// Tạo URL đầy đủ nếu đường dẫn là tương đối
let imageUrl = block.data.file.url;
if (imageUrl && !imageUrl.startsWith('http')) {
// Thêm base URL nếu cần, tùy thuộc cấu hình của bạn
// imageUrl = '/uploads/' + imageUrl; // Ví dụ
}
// Tạo HTML cho ảnh với caption
let imgHtml = `<img src="${imageUrl}" alt="${block.data.caption || ''}" style="max-width: 100%; height: auto; border-radius: 8px;" />`;
if (block.data.caption) {
imgHtml += `<p style="text-align: center; font-size: 0.9em; color: #666; margin-top: 8px;">${block.data.caption}</p>`;
}
// Thêm ảnh vào content
if (currentSection) {
currentSection.content += (currentSection.content ? '<br/>' : '') + imgHtml;
} else {
contentItems.push({
type: 'image', // 👈 Tạo type mới cho ảnh
url: imageUrl,
caption: block.data.caption || '',
html: imgHtml
});
}
break;
}
});
// Combine section content from following paragraphs
const processedItems = [];
currentSection = null;
contentItems.forEach(item => {
if (item.type === 'section') {
if (currentSection) processedItems.push(currentSection);
currentSection = item;
} else if (currentSection && item.type === 'paragraph') {
// Add paragraph to section content
currentSection.content += (currentSection.content ? '<br/>' : '') + item.text;
} else {
if (currentSection) {
processedItems.push(currentSection);
currentSection = null;
}
processedItems.push(item);
}
});
if (currentSection) processedItems.push(currentSection);
return processedItems;
}
// Convert Terms content to Editor.js data
convertTermsToEditor(termsContent) {
const blocks = [];
termsContent.forEach(item => {
switch (item.type) {
case 'paragraph':
// Check if paragraph contains HTML list and convert back
if (item.text && item.text.includes('<ul>') || item.text && item.text.includes('<ol>')) {
// Extract list items from HTML
const listMatch = item.text.match(/<(ul|ol)>(.*?)<\/\1>/s);
if (listMatch) {
const listType = listMatch[1];
const itemsHtml = listMatch[2];
const items = [];
const liRegex = /<li>(.*?)<\/li>/g;
let match;
while ((match = liRegex.exec(itemsHtml)) !== null) {
items.push(match[1]);
}
if (items.length > 0) {
blocks.push({
type: 'list',
data: {
style: listType === 'ul' ? 'unordered' : 'ordered',
items: items
}
});
break;
}
}
}
blocks.push({
type: 'paragraph',
data: {
text: item.text
}
});
break;
case 'list':
// Directly convert list items
blocks.push({
type: 'list',
data: {
style: item.style || 'unordered',
items: item.items || []
}
});
break;
case 'header':
// Header với level cụ thể (h2, h3, h4)
blocks.push({
type: 'header',
data: {
text: item.text,
level: item.level || 2
}
});
break;
case 'section':
blocks.push({
type: 'header',
data: {
text: item.title,
level: 2
}
});
if (item.content) {
// Split content by <br/> and create separate paragraphs
const contentParts = item.content.split(/<br\/?>/);
contentParts.forEach(part => {
if (part.trim()) {
blocks.push({
type: 'paragraph',
data: {
text: part.trim()
}
});
}
});
}
if (item.subsections) {
item.subsections.forEach(subsection => {
if (subsection.type === 'cancellation_table' || subsection.type === 'cancellation_section') {
blocks.push({
type: 'header',
data: {
text: subsection.title,
level: 3
}
});
if (subsection.items) {
blocks.push({
type: 'list',
data: {
style: 'unordered',
items: subsection.items
}
});
}
} else if (subsection.type === 'note') {
blocks.push({
type: 'quote',
data: {
text: subsection.text,
caption: '',
alignment: 'left'
}
});
}
});
}
break;
case 'note':
blocks.push({
type: 'quote',
data: {
text: item.text,
caption: '',
alignment: 'left'
}
});
break;
case 'embed':
blocks.push({
type: 'embed',
data: {
service: 'youtube',
source: item.url,
embed: item.videoId ? `https://www.youtube.com/embed/${item.videoId}` : item.url,
width: 580,
height: 320,
caption: item.caption || '',
title: item.title || ''
}
});
break;
case 'image':
blocks.push({
type: 'image',
data: {
file: {
url: item.url
},
caption: item.caption || '',
withBorder: false,
withBackground: false,
stretched: false
}
});
break;
}
});
return {
time: Date.now(),
blocks: blocks,
version: "2.28.2"
};
}
}
// Initialize Editor
document.addEventListener('DOMContentLoaded', async () => {
// Initialize content manager
window.termsContentManager = new TermsContentManager();
// Load existing content if available
let initialEditorData = {
time: Date.now(),
blocks: [],
version: "2.28.2"
};
if (window.TERMS_DATA?.content?.content) {
try {
const termsContent = window.TERMS_DATA.content.content;
initialEditorData = window.termsContentManager.convertTermsToEditor(termsContent);
} catch (error) {
console.error('Error loading terms content:', error);
}
}
// SmartList class với khả năng phát hiện dòng được chọn
class SmartList extends window.List {
constructor({data, config, api, readOnly}) {
super({data, config, api, readOnly});
this._element = null;
this.selectedText = ''; // Lưu text của dòng được chọn
this.selectedIndex = -1;
this.selectedFullText = '';
}
render() {
this._element = super.render();
// Thêm sự kiện để phát hiện dòng được chọn
this._element.addEventListener('mouseup', () => {
setTimeout(() => this.detectSelectedItem(), 100);
});
this._element.addEventListener('keyup', () => {
setTimeout(() => this.detectSelectedItem(), 100);
});
return this._element;
}
detectSelectedItem() {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
let listItem = range.commonAncestorContainer;
// Tìm phần tử li gần nhất
while (listItem && listItem.nodeName !== 'LI') {
listItem = listItem.parentNode;
}
if (listItem && this._element.contains(listItem)) {
// Lấy index của item được chọn
const items = this._element.querySelectorAll('li');
const index = Array.from(items).indexOf(listItem);
if (index >= 0) {
// Lưu text được chọn (có thể chỉ một phần)
this.selectedText = selection.toString().trim() || listItem.textContent.trim();
this.selectedIndex = index;
this.selectedFullText = listItem.textContent.trim();
// Đánh dấu visual
this.clearHighlights();
listItem.style.backgroundColor = 'rgba(0, 123, 255, 0.1)';
}
}
}
clearHighlights() {
if (!this._element) return;
this._element.querySelectorAll('li').forEach(li => {
li.style.backgroundColor = '';
});
}
save(blockContent) {
const savedData = super.save(blockContent);
// Lưu thông tin dòng được chọn
if (this.selectedIndex !== undefined && this.selectedIndex >= 0) {
savedData.selectedIndex = this.selectedIndex;
savedData.selectedText = this.selectedText;
savedData.selectedFullText = this.selectedFullText;
savedData.hasPartialSelection = this.selectedText !== this.selectedFullText;
}
return savedData;
}
static get conversionConfig() {
return {
export: (data) => {
// Export text đã chọn
return data.selectedText || data.items[0] || '';
},
import: (content) => {
return {
items: [content],
style: 'unordered'
};
}
};
}
}
// Helper method để convert toàn bộ item
function convertEntireListItem(editor, blockIndex, allItems, listStyle, itemIndex) {
const itemToConvert = allItems[itemIndex];
const remainingItems = allItems.filter((item, idx) => idx !== itemIndex);
// Xóa list gốc
editor.blocks.delete(blockIndex);
// Insert header
editor.blocks.insert('header', {
text: itemToConvert,
level: 3
}, {}, blockIndex, true);
// Insert remaining list nếu còn
if (remainingItems.length > 0) {
setTimeout(() => {
editor.blocks.insert('list', {
style: listStyle,
items: remainingItems
}, {}, blockIndex + 1, false);
}, 50);
}
return Promise.resolve();
}
// Initialize Editor
let blogEditorInstance = null;
try {
if (window.BlogEditor) {
const initialCustom = window.TERMS_DATA?.content?.content ? { blocks: window.TERMS_DATA.content.content } : { blocks: [] };
blogEditorInstance = new window.BlogEditor('editorjs', initialCustom);
window.blogEditorInstance = blogEditorInstance;
console.log('BlogEditor instance created');
} else {
console.warn('BlogEditor not available as global. Falling back to EditorJS init.');
// Try to dynamically import the Conclusion tool
let ConclusionClass = null;
try {
const mod = await import('/js/tools/conclusion.js');
ConclusionClass = mod.default || mod.Conclusion || null;
} catch (e) {
console.warn('Could not load Conclusion tool module:', e);
}
// image upload target for this editor
const uploadTarget = '/admin/upload/image?imageType=terms';
// build tools object and include Conclusion only when available
const toolsObj = {
header: {
class: window.Header,
config: {
placeholder: 'Enter section title',
levels: [2,3,4],
defaultLevel: 2
}
},
paragraph: {
class: window.Paragraph,
inlineToolbar: true
},
list: {
class: SmartList,
inlineToolbar: true,
config: {
defaultStyle: 'unordered'
}
},
image: {
class: window.ImageTool,
config: {
endpoints: { byFile: uploadTarget },
field: 'image',
types: 'image/*',
additionalRequestData: { imageType: 'terms' },
uploader: {
uploadByFile: async (file) => {
try {
const formData = new FormData();
formData.append('image', file);
const resp = await fetch(uploadTarget, {
method: 'POST',
body: formData
});
const result = await resp.json();
if (result && result.success) {
return {
success: 1,
file: {
url: result.path
}
};
}
return {
success: 0,
error: result?.error || 'Upload failed'
};
} catch (err) {
console.error('Image upload error:', err);
return {
success: 0,
error: 'Upload failed'
};
}
}
}
}
},
quote: {
class: window.Quote,
inlineToolbar: true,
config: {
quotePlaceholder: 'Enter a quote',
captionPlaceholder: "Quote's author"
}
},
marker: {
class: window.Marker,
shortcut: 'CMD+SHIFT+M'
},
embed: {
class: window.Embed,
config: {
services: { youtube: true },
inlineToolbar: true
}
}
};
if (ConclusionClass) {
toolsObj.conclusion = {
class: ConclusionClass,
inlineToolbar: true
};
}
try {
window.termsEditor = new window.EditorJS({
holder: 'editorjs',
placeholder: 'Write your Terms & Conditions content here...',
data: initialEditorData,
tools: toolsObj
});
// Override blocks.convert để xử lý tách list
window.termsEditor.isReady.then(() => {
console.log('Terms Editor ready with smart list conversion');
// Lưu reference đến convert gốc
const originalConvert = window.termsEditor.blocks.convert;
// Override convert method
window.termsEditor.blocks.convert = async function(blockIndex, targetType) {
try {
const block = await window.termsEditor.blocks.getBlockByIndex(blockIndex);
if (block && block.name === 'list' && (targetType === 'header' || targetType === 'heading')) {
console.log('=== List to Header Conversion (Single Line) ===');
// Lấy data đầy đủ của list
const savedData = await window.termsEditor.saver.save();
const listBlock = savedData.blocks[blockIndex];
if (!listBlock || !listBlock.data || !listBlock.data.items) {
console.error('Invalid list block data');
return originalConvert.call(this, blockIndex, targetType);
}
const allItems = [...listBlock.data.items];
const listStyle = listBlock.data.style || 'unordered';
// Kiểm tra xem có dòng được chọn không
const selectedIndex = listBlock.data.selectedIndex;
const selectedText = listBlock.data.selectedText || listBlock.data.selectedFullText;
if (selectedIndex === undefined || !selectedText) {
// Không có dòng nào được chọn, convert toàn bộ item đầu tiên
console.warn('No selected item, converting first item');
return convertEntireListItem(window.termsEditor, blockIndex, allItems, listStyle, 0);
}
console.log('Selected index:', selectedIndex);
console.log('Selected text:', selectedText);
console.log('All items:', allItems);
// Xóa list gốc
await window.termsEditor.blocks.delete(blockIndex);
// Đợi DOM update
await new Promise(resolve => setTimeout(resolve, 10));
// Tạo mảng items mới
const newItems = [];
let headerInserted = false;
let insertPosition = blockIndex;
// Duyệt qua tất cả items và xây dựng lại list
for (let i = 0; i < allItems.length; i++) {
if (i === selectedIndex) {
// Item được chọn -> chuyển thành header
console.log('Inserting header for item:', selectedText);
await window.termsEditor.blocks.insert('header', {
text: selectedText,
level: 3
}, {}, insertPosition, true);
headerInserted = true;
insertPosition++; // Tăng vị trí insert cho block tiếp theo
// Kiểm tra xem có phải chỉ chọn một phần text không
const hasPartialSelection = listBlock.data.hasPartialSelection;
const originalText = listBlock.data.selectedFullText || allItems[i];
if (hasPartialSelection && selectedText !== originalText) {
// Nếu chỉ chọn một phần, giữ lại phần còn lại trong list
const remainingText = originalText.replace(selectedText, '').trim();
if (remainingText) {
newItems.push(remainingText);
}
}
} else {
// Giữ nguyên các items khác
newItems.push(allItems[i]);
}
}
// Nếu vẫn còn items trong list, insert list mới
if (newItems.length > 0) {
console.log('Inserting remaining list with items:', newItems);
await new Promise(resolve => setTimeout(resolve, 50));
await window.termsEditor.blocks.insert('list', {
style: listStyle,
items: newItems
}, {}, insertPosition, false);
}
console.log('=== Conversion completed successfully ===');
return Promise.resolve(block);
}
} catch (error) {
console.error('!!! Error in convert override:', error);
}
// Gọi convert gốc cho các trường hợp khác
return originalConvert.call(this, blockIndex, targetType);
};
});
} catch (err) {
console.error('Fallback EditorJS init failed:', err);
window.termsEditor = new window.EditorJS({
holder: 'editorjs',
data: initialEditorData
});
}
}
} catch (err) {
console.error('Error initializing editor module:', err);
// attempt fallback
window.termsEditor = new window.EditorJS({
holder: 'editorjs',
data: initialEditorData
});
}
// Form submission handler
const form = document.getElementById('termsForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
// Save editor content (use BlogEditor instance if available to leverage uploader/autosave)
let editorData = null;
if (window.blogEditorInstance?.editor) {
editorData = await window.blogEditorInstance.editor.save();
} else if (window.termsEditor) {
editorData = await window.termsEditor.save();
} else {
throw new Error('Editor instance not available');
}
// Convert to Terms content structure
const termsContent = window.termsContentManager.convertEditorToTerms(editorData);
// Prepare JSON data
const heroData = {
title: document.getElementById('heroTitle').value.trim(),
backgroundImage: document.getElementById('heroBackgroundImage').value.trim(),
sectionClass: 'uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative',
backgroundClasses: 'uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge',
overlayStyle: {
backgroundColor: 'rgba(0, 0, 0, 0)'
},
titleClass: 'text-white text-[5vw] uk-text-center',
enableScrollspy: true
};
const pageData = {
title: document.getElementById('pageTitle').value.trim(),
divider: true,
sectionClass: 'uk-section-default uk-section-overlap uk-section',
titleClass: 'text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center',
dividerClass: 'uk-divider-small uk-text-left@m uk-text-center'
};
const contentData = {
sectionClass: 'uk-section-muted uk-section-overlap uk-section',
textClass: 'uk-panel uk-margin text-[1vw]',
content: termsContent
};
// Set hidden inputs
document.getElementById('heroJson').value = JSON.stringify(heroData);
document.getElementById('pageJson').value = JSON.stringify(pageData);
document.getElementById('contentJson').value = JSON.stringify(contentData);
// Submit form
form.submit();
} catch (error) {
console.error('Error saving terms content:', error);
alert('Error saving content. Please check console for details.');
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Preview functionality
const previewBtn = document.querySelector('.preview-btn');
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
previewBtn.addEventListener('click', async function () {
try {
// Save editor content first
let editorData = null;
if (window.blogEditorInstance?.editor) {
editorData = await window.blogEditorInstance.editor.save();
} else if (window.termsEditor) {
editorData = await window.termsEditor.save();
} else {
throw new Error('Editor instance not available');
}
const termsContent = window.termsContentManager.convertEditorToTerms(editorData);
// Prepare data for preview
const formData = new FormData();
formData.append('content', JSON.stringify(termsContent));
formData.append('heroTitle', document.getElementById('heroTitle').value);
formData.append('heroBackgroundImage', document.getElementById('heroBackgroundImage').value);
formData.append('pageTitle', document.getElementById('pageTitle').value);
const response = await fetch('/admin/terms/preview', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Preview failed');
const html = await response.text();
const previewFrame = document.getElementById('previewFrame');
const blob = new Blob([html], { type: 'text/html' });
previewFrame.src = URL.createObjectURL(blob);
previewModal.show();
} catch (error) {
console.error('Preview error:', error);
alert('Error generating preview. Please check console for details.');
}
});
// Image upload handling
document.querySelectorAll('.btn-upload-image').forEach(btn => {
btn.addEventListener('click', function () {
const targetInput = this.getAttribute('data-target-input');
const imageType = this.getAttribute('data-image-type');
document.getElementById('currentImageType').value = imageType;
document.getElementById('currentTargetInput').value = targetInput;
document.getElementById('directImageUpload').click();
});
});
// Handle file selection
document.getElementById('directImageUpload').addEventListener('change', async function (e) {
if (!this.files || !this.files[0]) return;
const formData = new FormData();
formData.append('image', this.files[0]);
const imageType = document.getElementById('currentImageType').value;
const targetInput = document.getElementById('currentTargetInput').value;
try {
const uploadResponse = await fetch(`/admin/upload/image?imageType=${imageType}`, {
method: 'POST',
body: formData
});
const uploadResult = await uploadResponse.json();
if (uploadResult.success && uploadResult.path) {
const inputElement = document.getElementById(targetInput);
if (inputElement) {
inputElement.value = uploadResult.path;
// Update preview
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(uploadResult.path);
}
// Show success message
showToast('Image uploaded successfully', 'success');
}
} else {
showToast(uploadResult.error || 'Error uploading image', 'error');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error uploading image', 'error');
}
this.value = '';
});
function updateHeroImagePreview(imageUrl) {
const previewDiv = document.getElementById('heroImagePreview');
let img = previewDiv.querySelector('img');
let fallback = previewDiv.querySelector('.border');
if (!img) {
img = document.createElement('img');
img.className = 'img-thumbnail';
img.style.height = '200px';
img.style.width = '100%';
img.style.objectFit = 'cover';
img.alt = 'Background image preview';
img.onerror = function() {
this.style.display = 'none';
if (fallback) fallback.style.display = 'flex';
};
previewDiv.insertBefore(img, fallback);
}
img.src = imageUrl;
img.style.display = 'block';
if (fallback) fallback.style.display = 'none';
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
document.body.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
// Custom handler để tách list item thành header với Ctrl+Shift+H
window.addEventListener('keydown', async (e) => {
// Ctrl/Cmd + Shift + H = Convert list item sang header
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'H') {
e.preventDefault();
const editor = window.blogEditorInstance?.editor || window.termsEditor;
if (!editor) return;
try {
const currentBlockIndex = editor.blocks.getCurrentBlockIndex();
const block = await editor.blocks.getBlockByIndex(currentBlockIndex);
if (block && block.name === 'list') {
// Trigger conversion to header
await editor.blocks.convert(currentBlockIndex, 'header');
showToast('Converted selected list item to header', 'success');
} else {
showToast('Please select text in a list item first', 'info');
}
} catch (error) {
console.error('Error converting list item:', error);
showToast('Error converting to header', 'error');
}
}
});
// Thêm CSS để hiển thị dòng được chọn trong list
const style = document.createElement('style');
style.textContent = `
.ce-list__item--selected {
background-color: rgba(0, 123, 255, 0.1) !important;
border-left: 3px solid #007bff;
padding-left: 8px !important;
}
.ce-list__item--partial-selection {
background-color: rgba(255, 193, 7, 0.1) !important;
border-left: 3px solid #ffc107;
}
/* Tooltip for list items */
.ce-block--selected .ce-list {
position: relative;
}
.ce-block--selected .ce-list::after {
content: 'Select text and press Ctrl+Shift+H to convert to header';
position: absolute;
top: -30px;
left: 0;
background: #333;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
display: none;
}
.ce-block--selected .ce-list:hover::after {
display: block;
}
`;
document.head.appendChild(style);
});
// Store terms data in global variable
window.TERMS_DATA = <%- JSON.stringify(data) %>;
</script>