feat: add Raw HTML, Accordion, and YouTube plugin support to the editor configuration
This commit is contained in:
+148
-97
@@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import oracledb
|
import oracledb
|
||||||
|
|
||||||
# Construct the Editor.js JSON structure using raw HTML blocks matching ckeditor5-layout schema
|
# Construct the Editor.js JSON structure using our new structured 'grid' blocks
|
||||||
json_data = {
|
json_data = {
|
||||||
"time": 1700000000000,
|
"time": 1700000000000,
|
||||||
"blocks": [
|
"blocks": [
|
||||||
@@ -21,16 +21,12 @@ json_data = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "raw",
|
"type": "grid",
|
||||||
"data": {
|
"data": {
|
||||||
"html": """
|
"cols": 2,
|
||||||
<section class="relative overflow-hidden xl:py-16 md:py-12 py-8">
|
"type1": "html",
|
||||||
<div class="absolute top-0 right-0 w-1/2 h-full bg-primary-100 -skew-x-12 translate-x-1/4 z-0 opacity-50 hidden lg:block"></div>
|
"col1": """
|
||||||
<div class="container relative z-10">
|
<div class="relative pt-4">
|
||||||
<div class="ck-layout-columns" columns="2">
|
|
||||||
<!-- Column 1 (Left): Text -->
|
|
||||||
<div>
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute md:-left-4 -left-3 md:-top-5 -top-2 md:size-12 size-8 bg-primary-100 rounded-full"></div>
|
<div class="absolute md:-left-4 -left-3 md:-top-5 -top-2 md:size-12 size-8 bg-primary-100 rounded-full"></div>
|
||||||
<h2 class="display-3 text-primary-500 uppercase mb-4 block z-[1] relative">TẦM NHÌN</h2>
|
<h2 class="display-3 text-primary-500 uppercase mb-4 block z-[1] relative">TẦM NHÌN</h2>
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
@@ -38,110 +34,165 @@ json_data = {
|
|||||||
<p class="body-1 text-gray-800 text-justify !font-medium">Trở thành bệnh viện đại học dẫn đầu Việt Nam đạt chuẩn quốc tế</p>
|
<p class="body-1 text-gray-800 text-justify !font-medium">Trở thành bệnh viện đại học dẫn đầu Việt Nam đạt chuẩn quốc tế</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- Column 2 (Right): Image -->
|
|
||||||
<div>
|
|
||||||
<div class="relative rounded-lg overflow-hidden aspect-[3/2] shadow-lg">
|
|
||||||
<img alt="vision" src="/images/umass/mission.jpeg" class="w-full h-full object-cover transition-transform duration-700 hover:scale-110" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
""",
|
""",
|
||||||
"stretched": True
|
"width1": 6,
|
||||||
|
"class1": "relative z-10",
|
||||||
|
"type2": "image",
|
||||||
|
"col2": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/umass/mission.jpeg"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": False,
|
||||||
|
"withBackground": False,
|
||||||
|
"stretched": False
|
||||||
|
},
|
||||||
|
"width2": 6,
|
||||||
|
"class2": "shadow-lg rounded"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "raw",
|
"type": "grid",
|
||||||
"data": {
|
"data": {
|
||||||
"html": """
|
"cols": 2,
|
||||||
<section class="xl:py-16 md:py-12 py-8 bg-primary-500 text-white overflow-hidden">
|
"type1": "image",
|
||||||
<div class="container">
|
"col1": {
|
||||||
<div class="ck-layout-columns" columns="2">
|
"file": {
|
||||||
<!-- Column 1 (Left): Image -->
|
"url": "/images/umass/vision.jpeg"
|
||||||
<div>
|
},
|
||||||
<div class="relative h-full">
|
"caption": "",
|
||||||
<div class="relative rounded-lg overflow-hidden aspect-[3/2] shadow-lg">
|
"withBorder": False,
|
||||||
<img alt="mission" src="/images/umass/vision.jpeg" class="w-full h-full object-cover" />
|
"withBackground": False,
|
||||||
</div>
|
"stretched": False
|
||||||
</div>
|
},
|
||||||
</div>
|
"width1": 6,
|
||||||
<!-- Column 2 (Right): Text -->
|
"class1": "shadow-lg rounded",
|
||||||
<div>
|
"type2": "html",
|
||||||
<div class="relative">
|
"col2": """
|
||||||
<div class="absolute md:-left-5 -left-4 md:-top-5 -top-2 md:size-12 size-8 bg-white/50 rounded-full"></div>
|
<div class="relative pt-4">
|
||||||
<h2 class="display-3 text-white mb-4 relative z-[1]">SỨ MỆNH</h2>
|
<div class="absolute md:-left-5 -left-4 md:-top-5 -top-2 md:size-12 size-8 bg-primary-100 rounded-full"></div>
|
||||||
</div>
|
<h2 class="display-3 text-primary-500 mb-4 relative z-[1]">SỨ MỆNH</h2>
|
||||||
<div class="bg-white p-4 md:p-6 rounded-lg overview-wrapper-shadow">
|
<div class="bg-white p-4 md:p-6 rounded-lg overview-wrapper-shadow">
|
||||||
<p class="body-1 text-primary-500 text-justify !font-medium">Mang đến giải pháp chăm sóc sức khỏe tối ưu từ sự tích hợp giữa điều trị, nghiên cứu và đào tạo</p>
|
<p class="body-1 text-primary-500 text-justify !font-medium">Mang đến giải pháp chăm sóc sức khỏe tối ưu từ sự tích hợp giữa điều trị, nghiên cứu và đào tạo</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
""",
|
""",
|
||||||
"stretched": True
|
"width2": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "raw",
|
"type": "grid",
|
||||||
"data": {
|
"data": {
|
||||||
"html": """
|
"cols": 4,
|
||||||
<section class="bg-primary-75 py-12 overflow-hidden">
|
"type1": "flex",
|
||||||
<div class="container">
|
"col1": {
|
||||||
<h2 class="text-center display-4 text-primary-600 xl:mb-6 md:mb-4 mb-3 uppercase">Giá trị cốt lõi</h2>
|
"cols": 2,
|
||||||
|
"direction": "column",
|
||||||
<div class="ck-layout-columns" columns="4">
|
"justify": "center",
|
||||||
<!-- Column 1: Tiên phong -->
|
"align": "center",
|
||||||
<div class="p-3">
|
"gap": "2",
|
||||||
<div class="rounded-lg shadow bg-primary-600 text-white p-4 h-full flex flex-col items-center text-center">
|
"type1": "image",
|
||||||
<div class="mb-3">
|
"col1": {
|
||||||
<img alt="Tiên phong" src="/images/career/core-hover-1.webp" style="width: 80px; height: 80px; object-fit: contain;" />
|
"file": {
|
||||||
</div>
|
"url": "/images/career/core-hover-1.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": False,
|
||||||
|
"withBackground": False,
|
||||||
|
"stretched": False
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": """
|
||||||
|
<div class="text-center">
|
||||||
<h3 class="heading-1 font-weight-bold mb-2">Tiên phong</h3>
|
<h3 class="heading-1 font-weight-bold mb-2">Tiên phong</h3>
|
||||||
<p class="title-1">Phát triển và ứng dụng kỹ thuật tiên tiến vào điều trị, giảng dạy, nghiên cứu</p>
|
<p class="title-1">Phát triển và ứng dụng kỹ thuật tiên tiến vào điều trị, giảng dạy, nghiên cứu</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
"""
|
||||||
|
},
|
||||||
<!-- Column 2: Thấu hiểu -->
|
"width1": 3,
|
||||||
<div class="p-3">
|
"class1": "rounded-lg shadow bg-primary-600 text-white p-4 h-full",
|
||||||
<div class="rounded-lg shadow bg-white text-primary-600 p-4 h-full flex flex-col items-center text-center border border-primary-100">
|
"type2": "flex",
|
||||||
<div class="mb-3">
|
"col2": {
|
||||||
<img alt="Thấu hiểu" src="/images/career/core-2.webp" style="width: 80px; height: 80px; object-fit: contain;" />
|
"cols": 2,
|
||||||
</div>
|
"direction": "column",
|
||||||
|
"justify": "center",
|
||||||
|
"align": "center",
|
||||||
|
"gap": "2",
|
||||||
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/career/core-2.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": False,
|
||||||
|
"withBackground": False,
|
||||||
|
"stretched": False
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": """
|
||||||
|
<div class="text-center">
|
||||||
<h3 class="heading-1 font-weight-bold mb-2">Thấu hiểu</h3>
|
<h3 class="heading-1 font-weight-bold mb-2">Thấu hiểu</h3>
|
||||||
<p class="title-1 text-gray-700">Nỗi đau thể xác và tinh thần của người bệnh để đưa ra giải pháp điều trị tối ưu</p>
|
<p class="title-1 text-gray-700">Nỗi đau thể xác và tinh thần của người bệnh để đưa ra giải pháp điều trị tối ưu</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
"""
|
||||||
|
},
|
||||||
<!-- Column 3: Chuẩn mực -->
|
"width2": 3,
|
||||||
<div class="p-3">
|
"class2": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100",
|
||||||
<div class="rounded-lg shadow bg-white text-primary-600 p-4 h-full flex flex-col items-center text-center border border-primary-100">
|
"type3": "flex",
|
||||||
<div class="mb-3">
|
"col3": {
|
||||||
<img alt="Chuẩn mực" src="/images/career/core-3.webp" style="width: 80px; height: 80px; object-fit: contain;" />
|
"cols": 2,
|
||||||
</div>
|
"direction": "column",
|
||||||
|
"justify": "center",
|
||||||
|
"align": "center",
|
||||||
|
"gap": "2",
|
||||||
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/career/core-3.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": False,
|
||||||
|
"withBackground": False,
|
||||||
|
"stretched": False
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": """
|
||||||
|
<div class="text-center">
|
||||||
<h3 class="heading-1 font-weight-bold mb-2">Chuẩn mực</h3>
|
<h3 class="heading-1 font-weight-bold mb-2">Chuẩn mực</h3>
|
||||||
<p class="title-1 text-gray-700">Về y đức và học thuật của người thầy giáo – thầy thuốc</p>
|
<p class="title-1 text-gray-700">Về y đức và học thuật của người thầy giáo – thầy thuốc</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
"""
|
||||||
|
},
|
||||||
<!-- Column 4: An toàn -->
|
"width3": 3,
|
||||||
<div class="p-3">
|
"class3": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100",
|
||||||
<div class="rounded-lg shadow bg-white text-primary-600 p-4 h-full flex flex-col items-center text-center border border-primary-100">
|
"type4": "flex",
|
||||||
<div class="mb-3">
|
"col4": {
|
||||||
<img alt="An toàn" src="/images/career/core-4.webp" style="width: 80px; height: 80px; object-fit: contain;" />
|
"cols": 2,
|
||||||
</div>
|
"direction": "column",
|
||||||
|
"justify": "center",
|
||||||
|
"align": "center",
|
||||||
|
"gap": "2",
|
||||||
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/career/core-4.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": False,
|
||||||
|
"withBackground": False,
|
||||||
|
"stretched": False
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": """
|
||||||
|
<div class="text-center">
|
||||||
<h3 class="heading-1 font-weight-bold mb-2">An toàn</h3>
|
<h3 class="heading-1 font-weight-bold mb-2">An toàn</h3>
|
||||||
<p class="title-1 text-gray-700">Cho người bệnh, nhân viên y tế và cộng đồng</p>
|
<p class="title-1 text-gray-700">Cho người bệnh, nhân viên y tế và cộng đồng</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
"""
|
||||||
</div>
|
},
|
||||||
</div>
|
"width4": 3,
|
||||||
</section>
|
"class4": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100"
|
||||||
""",
|
|
||||||
"stretched": True
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -153,15 +204,15 @@ json_string = json.dumps(json_data, ensure_ascii=False, indent=2)
|
|||||||
with open("tam_nhin_su_menh_import.json", "w", encoding="utf-8") as f:
|
with open("tam_nhin_su_menh_import.json", "w", encoding="utf-8") as f:
|
||||||
f.write(json_string)
|
f.write(json_string)
|
||||||
|
|
||||||
print("Generated tam_nhin_su_menh_import.json successfully!")
|
print("Generated structured tam_nhin_su_menh_import.json successfully using Grid block configurations!")
|
||||||
|
|
||||||
# Automatically attempt to update local Oracle Database
|
# Automatically update Oracle Database
|
||||||
try:
|
try:
|
||||||
conn = oracledb.connect(user='sisvietnam', password='sisvietnam', dsn='localhost:1521/sisvietnam')
|
conn = oracledb.connect(user='sisvietnam', password='sisvietnam', dsn='localhost:1521/sisvietnam')
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("UPDATE sis_page SET content = :content WHERE slug = 'tam-nhin-su-menh-gia-tri-cot-loi'", content=json_string)
|
cursor.execute("UPDATE sis_page SET content = :content WHERE slug = 'tam-nhin-su-menh-gia-tri-cot-loi'", content=json_string)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("Updated database table 'sis_page' for slug 'tam-nhin-su-menh-gia-tri-cot-loi' with layout JSON blocks!")
|
print("Updated database table 'sis_page' for slug 'tam-nhin-su-menh-gia-tri-cot-loi' with structured Grid block layouts!")
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("DB Update bypassed or failed:", e)
|
print("DB Update failed:", e)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -922,11 +922,23 @@ function initSISEditor(holderId, hiddenInputId, initialData, skipSubmitHandler =
|
|||||||
builtInTools.image = {
|
builtInTools.image = {
|
||||||
class: ImageTool,
|
class: ImageTool,
|
||||||
config: {
|
config: {
|
||||||
endpoints: { byFile: '/api/manage/media/upload' },
|
endpoints: {
|
||||||
|
byFile: '/api/manage/media/upload',
|
||||||
|
byUrl: '/api/manage/media/fetchUrl'
|
||||||
|
},
|
||||||
|
uploader: {
|
||||||
|
uploadByUrl(url) {
|
||||||
|
return Promise.resolve({
|
||||||
|
success: 1,
|
||||||
|
file: { url: url }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
field: 'file',
|
field: 'file',
|
||||||
types: 'image/*'
|
types: 'image/*'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
window.SISEditorPlugins.image = builtInTools.image;
|
||||||
}
|
}
|
||||||
if (typeof AttachesTool !== 'undefined') {
|
if (typeof AttachesTool !== 'undefined') {
|
||||||
builtInTools.attaches = {
|
builtInTools.attaches = {
|
||||||
|
|||||||
@@ -150,20 +150,21 @@ class FlexTool {
|
|||||||
|
|
||||||
this.wrapper.appendChild(settingsRow);
|
this.wrapper.appendChild(settingsRow);
|
||||||
|
|
||||||
// Layout Columns Number Input
|
// Background Settings Row
|
||||||
const countGroup = document.createElement('div');
|
const bgSettingsRow = document.createElement('div');
|
||||||
countGroup.className = 'form-group mb-3';
|
bgSettingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||||
countGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1 d-block">Number of Flex Items (1 - 12)</label>';
|
|
||||||
|
|
||||||
|
// 1. Flex Items (Cols)
|
||||||
|
const colDiv = document.createElement('div');
|
||||||
|
colDiv.className = 'col-md-2 mb-2';
|
||||||
|
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex Items</label>';
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'number';
|
input.type = 'number';
|
||||||
input.className = 'form-control form-control-sm';
|
input.className = 'form-control form-control-sm';
|
||||||
input.style.width = '100px';
|
|
||||||
input.min = '1';
|
input.min = '1';
|
||||||
input.max = '12';
|
input.max = '12';
|
||||||
input.value = this.data.cols;
|
input.value = this.data.cols;
|
||||||
if (this.readOnly) input.disabled = true;
|
if (this.readOnly) input.disabled = true;
|
||||||
|
|
||||||
input.addEventListener('input', (e) => {
|
input.addEventListener('input', (e) => {
|
||||||
let val = parseInt(e.target.value) || 2;
|
let val = parseInt(e.target.value) || 2;
|
||||||
if (val < 1) val = 1;
|
if (val < 1) val = 1;
|
||||||
@@ -171,9 +172,38 @@ class FlexTool {
|
|||||||
this.data.cols = val;
|
this.data.cols = val;
|
||||||
this._renderColumnInputs();
|
this._renderColumnInputs();
|
||||||
});
|
});
|
||||||
|
colDiv.appendChild(input);
|
||||||
|
bgSettingsRow.appendChild(colDiv);
|
||||||
|
|
||||||
countGroup.appendChild(input);
|
// 2. Global CSS Class
|
||||||
this.wrapper.appendChild(countGroup);
|
const classDiv = document.createElement('div');
|
||||||
|
classDiv.className = 'col-md-5 mb-2';
|
||||||
|
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex CSS Class(es)</label>';
|
||||||
|
const classInput = document.createElement('input');
|
||||||
|
classInput.type = 'text';
|
||||||
|
classInput.className = 'form-control form-control-sm';
|
||||||
|
classInput.placeholder = 'e.g. my-custom-flex';
|
||||||
|
classInput.value = this.data.globalClass || '';
|
||||||
|
if (this.readOnly) classInput.disabled = true;
|
||||||
|
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||||
|
classDiv.appendChild(classInput);
|
||||||
|
bgSettingsRow.appendChild(classDiv);
|
||||||
|
|
||||||
|
// 3. Global HTML ID
|
||||||
|
const idDiv = document.createElement('div');
|
||||||
|
idDiv.className = 'col-md-5 mb-2';
|
||||||
|
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Flex HTML ID</label>';
|
||||||
|
const idInput = document.createElement('input');
|
||||||
|
idInput.type = 'text';
|
||||||
|
idInput.className = 'form-control form-control-sm';
|
||||||
|
idInput.placeholder = 'e.g. flex-section-1';
|
||||||
|
idInput.value = this.data.globalId || '';
|
||||||
|
if (this.readOnly) idInput.disabled = true;
|
||||||
|
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||||
|
idDiv.appendChild(idInput);
|
||||||
|
bgSettingsRow.appendChild(idDiv);
|
||||||
|
|
||||||
|
this.wrapper.appendChild(bgSettingsRow);
|
||||||
|
|
||||||
// Columns inputs container
|
// Columns inputs container
|
||||||
this.inputsContainer = document.createElement('div');
|
this.inputsContainer = document.createElement('div');
|
||||||
@@ -260,8 +290,8 @@ class FlexTool {
|
|||||||
|
|
||||||
if (window.SISEditorPlugins) {
|
if (window.SISEditorPlugins) {
|
||||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||||
// Avoid self-nesting flex to prevent recursion issues
|
// Allow nesting any registered plugins (no exclusions)
|
||||||
if (key !== 'flex' && !types.some(t => t.value === key)) {
|
if (!types.some(t => t.value === key)) {
|
||||||
types.push({ value: key, label: `[Plugin] ${key}` });
|
types.push({ value: key, label: `[Plugin] ${key}` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -311,7 +341,9 @@ class FlexTool {
|
|||||||
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
||||||
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
||||||
try {
|
try {
|
||||||
const pluginClass = window.SISEditorPlugins[type].class;
|
const pluginEntry = window.SISEditorPlugins[type];
|
||||||
|
const pluginClass = pluginEntry.class;
|
||||||
|
const pluginConfig = pluginEntry.config || {};
|
||||||
let parsedData = {};
|
let parsedData = {};
|
||||||
if (this.data[`col${i}`]) {
|
if (this.data[`col${i}`]) {
|
||||||
try {
|
try {
|
||||||
@@ -323,12 +355,136 @@ class FlexTool {
|
|||||||
const instance = new pluginClass({
|
const instance = new pluginClass({
|
||||||
data: parsedData,
|
data: parsedData,
|
||||||
api: this.api,
|
api: this.api,
|
||||||
readOnly: this.readOnly
|
readOnly: this.readOnly,
|
||||||
|
config: pluginConfig,
|
||||||
|
block: {
|
||||||
|
id: 'nested-flex-' + i + '-' + Math.random().toString(36).substring(7),
|
||||||
|
name: type,
|
||||||
|
holder: typeContainer,
|
||||||
|
isEmpty: false,
|
||||||
|
selected: false,
|
||||||
|
stretched: false,
|
||||||
|
tunes: {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.activeInstances[i] = instance;
|
this.activeInstances[i] = instance;
|
||||||
|
|
||||||
const element = instance.render();
|
const element = instance.render();
|
||||||
typeContainer.appendChild(element);
|
typeContainer.appendChild(element);
|
||||||
|
|
||||||
|
// Fix for Editor.js Image plugin 404 errors (spinner hangs forever)
|
||||||
|
if (type === 'image') {
|
||||||
|
// Custom UI override for nested images
|
||||||
|
const injectCustomButtons = () => {
|
||||||
|
if (element.querySelector('.sis-custom-image-controls')) return;
|
||||||
|
|
||||||
|
const fileBtn = element.querySelector('.cdx-button');
|
||||||
|
if (fileBtn) {
|
||||||
|
fileBtn.style.display = 'none'; // Hide native button
|
||||||
|
}
|
||||||
|
|
||||||
|
const controls = document.createElement('div');
|
||||||
|
controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light';
|
||||||
|
controls.style.display = 'flex';
|
||||||
|
controls.style.flexDirection = 'column';
|
||||||
|
controls.style.gap = '8px';
|
||||||
|
|
||||||
|
controls.innerHTML = `
|
||||||
|
<div class="text-muted small text-center mb-1"><i class="fas fa-cog"></i> Image Controls</div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm w-100" id="btn-upload-img-${i}"><i class="fas fa-upload"></i> Upload Image</button>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm w-100" id="btn-fetch-url-${i}"><i class="fas fa-link"></i> Fetch URL</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm w-100" id="btn-media-lib-${i}"><i class="fas fa-images"></i> Media Library</button>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm w-100 mt-2" id="btn-clear-img-${i}"><i class="fas fa-trash"></i> Remove Image</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const uploadBtn = controls.querySelector(`#btn-upload-img-${i}`);
|
||||||
|
if (uploadBtn && fileBtn) {
|
||||||
|
uploadBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileBtn.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlBtn = controls.querySelector(`#btn-fetch-url-${i}`);
|
||||||
|
if (urlBtn) {
|
||||||
|
urlBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const url = prompt('Enter Image URL:');
|
||||||
|
if (url && url.trim() !== '') {
|
||||||
|
this.data[`col${i}`] = {
|
||||||
|
file: { url: url.trim() },
|
||||||
|
caption: '',
|
||||||
|
withBorder: false,
|
||||||
|
withBackground: false,
|
||||||
|
stretched: false
|
||||||
|
};
|
||||||
|
this._renderTypeSpecificInput(i, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaBtn = controls.querySelector(`#btn-media-lib-${i}`);
|
||||||
|
if (mediaBtn) {
|
||||||
|
mediaBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (typeof openMediaPickerModal === 'function') {
|
||||||
|
openMediaPickerModal((url) => {
|
||||||
|
if (url) {
|
||||||
|
this.data[`col${i}`] = {
|
||||||
|
file: { url: url },
|
||||||
|
caption: '',
|
||||||
|
withBorder: false,
|
||||||
|
withBackground: false,
|
||||||
|
stretched: false
|
||||||
|
};
|
||||||
|
this._renderTypeSpecificInput(i, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Media Library is not yet configured on this system.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearBtn = controls.querySelector(`#btn-clear-img-${i}`);
|
||||||
|
if (clearBtn) {
|
||||||
|
clearBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm('Are you sure you want to remove this image?')) {
|
||||||
|
this.data[`col${i}`] = '';
|
||||||
|
this._renderTypeSpecificInput(i, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
element.appendChild(controls);
|
||||||
|
};
|
||||||
|
injectCustomButtons();
|
||||||
|
|
||||||
|
const handleImageError = () => {
|
||||||
|
element.classList.remove('image-tool--loading');
|
||||||
|
element.classList.add('image-tool--empty');
|
||||||
|
const preloader = element.querySelector('.image-tool__image-preloader');
|
||||||
|
if (preloader) preloader.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (node.tagName === 'IMG') {
|
||||||
|
node.addEventListener('error', handleImageError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(element, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
const existingImg = element.querySelector('img');
|
||||||
|
if (existingImg) existingImg.addEventListener('error', handleImageError);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to render plugin ${type} inside FlexTool:`, err);
|
console.error(`Failed to render plugin ${type} inside FlexTool:`, err);
|
||||||
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
||||||
@@ -553,7 +709,9 @@ class FlexTool {
|
|||||||
direction: this.data.direction,
|
direction: this.data.direction,
|
||||||
justify: this.data.justify,
|
justify: this.data.justify,
|
||||||
align: this.data.align,
|
align: this.data.align,
|
||||||
gap: this.data.gap
|
gap: this.data.gap,
|
||||||
|
globalClass: this.data.globalClass || '',
|
||||||
|
globalId: this.data.globalId || ''
|
||||||
};
|
};
|
||||||
for (let i = 1; i <= this.data.cols; i++) {
|
for (let i = 1; i <= this.data.cols; i++) {
|
||||||
const type = this.data[`type${i}`] || 'html';
|
const type = this.data[`type${i}`] || 'html';
|
||||||
|
|||||||
@@ -44,34 +44,60 @@ class GridTool {
|
|||||||
title.innerHTML = '<i class="fas fa-columns"></i> Multi-Column Grid Layout';
|
title.innerHTML = '<i class="fas fa-columns"></i> Multi-Column Grid Layout';
|
||||||
this.wrapper.appendChild(title);
|
this.wrapper.appendChild(title);
|
||||||
|
|
||||||
// Layout Columns Number Input
|
// Layout Settings Row
|
||||||
const selectorGroup = document.createElement('div');
|
const settingsRow = document.createElement('div');
|
||||||
selectorGroup.className = 'form-group mb-3';
|
settingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||||
|
|
||||||
const label = document.createElement('label');
|
// 1. Columns
|
||||||
label.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
const colDiv = document.createElement('div');
|
||||||
label.innerText = 'Number of Columns (1 - 12)';
|
colDiv.className = 'col-md-2 mb-2';
|
||||||
selectorGroup.appendChild(label);
|
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Columns</label>';
|
||||||
|
const colInput = document.createElement('input');
|
||||||
const input = document.createElement('input');
|
colInput.type = 'number';
|
||||||
input.type = 'number';
|
colInput.className = 'form-control form-control-sm';
|
||||||
input.className = 'form-control form-control-sm';
|
colInput.min = '1';
|
||||||
input.style.width = '100px';
|
colInput.max = '12';
|
||||||
input.min = '1';
|
colInput.value = this.data.cols;
|
||||||
input.max = '12';
|
if (this.readOnly) colInput.disabled = true;
|
||||||
input.value = this.data.cols;
|
colInput.addEventListener('input', (e) => {
|
||||||
if (this.readOnly) input.disabled = true;
|
|
||||||
|
|
||||||
input.addEventListener('input', (e) => {
|
|
||||||
let val = parseInt(e.target.value) || 2;
|
let val = parseInt(e.target.value) || 2;
|
||||||
if (val < 1) val = 1;
|
if (val < 1) val = 1;
|
||||||
if (val > 12) val = 12;
|
if (val > 12) val = 12;
|
||||||
this.data.cols = val;
|
this.data.cols = val;
|
||||||
this._renderColumnInputs();
|
this._renderColumnInputs();
|
||||||
});
|
});
|
||||||
|
colDiv.appendChild(colInput);
|
||||||
|
settingsRow.appendChild(colDiv);
|
||||||
|
|
||||||
selectorGroup.appendChild(input);
|
// 2. Global CSS Class
|
||||||
this.wrapper.appendChild(selectorGroup);
|
const classDiv = document.createElement('div');
|
||||||
|
classDiv.className = 'col-md-5 mb-2';
|
||||||
|
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Grid CSS Class(es)</label>';
|
||||||
|
const classInput = document.createElement('input');
|
||||||
|
classInput.type = 'text';
|
||||||
|
classInput.className = 'form-control form-control-sm';
|
||||||
|
classInput.placeholder = 'e.g. my-custom-grid';
|
||||||
|
classInput.value = this.data.globalClass || '';
|
||||||
|
if (this.readOnly) classInput.disabled = true;
|
||||||
|
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||||
|
classDiv.appendChild(classInput);
|
||||||
|
settingsRow.appendChild(classDiv);
|
||||||
|
|
||||||
|
// 3. Global HTML ID
|
||||||
|
const idDiv = document.createElement('div');
|
||||||
|
idDiv.className = 'col-md-5 mb-2';
|
||||||
|
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Grid HTML ID</label>';
|
||||||
|
const idInput = document.createElement('input');
|
||||||
|
idInput.type = 'text';
|
||||||
|
idInput.className = 'form-control form-control-sm';
|
||||||
|
idInput.placeholder = 'e.g. grid-section-1';
|
||||||
|
idInput.value = this.data.globalId || '';
|
||||||
|
if (this.readOnly) idInput.disabled = true;
|
||||||
|
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||||
|
idDiv.appendChild(idInput);
|
||||||
|
settingsRow.appendChild(idDiv);
|
||||||
|
|
||||||
|
this.wrapper.appendChild(settingsRow);
|
||||||
|
|
||||||
// Columns inputs container
|
// Columns inputs container
|
||||||
this.inputsContainer = document.createElement('div');
|
this.inputsContainer = document.createElement('div');
|
||||||
@@ -160,8 +186,8 @@ class GridTool {
|
|||||||
// Dynamically add other registered plugins from window.SISEditorPlugins
|
// Dynamically add other registered plugins from window.SISEditorPlugins
|
||||||
if (window.SISEditorPlugins) {
|
if (window.SISEditorPlugins) {
|
||||||
Object.keys(window.SISEditorPlugins).forEach(key => {
|
Object.keys(window.SISEditorPlugins).forEach(key => {
|
||||||
// Avoid self-nesting grid to prevent recursion issues
|
// Allow nesting any registered plugins (no exclusions)
|
||||||
if (key !== 'grid' && !types.some(t => t.value === key)) {
|
if (!types.some(t => t.value === key)) {
|
||||||
types.push({ value: key, label: `[Plugin] ${key}` });
|
types.push({ value: key, label: `[Plugin] ${key}` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -211,7 +237,9 @@ class GridTool {
|
|||||||
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
// Check if type is a dynamic editor plugin registered in window.SISEditorPlugins
|
||||||
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
if (window.SISEditorPlugins && window.SISEditorPlugins[type]) {
|
||||||
try {
|
try {
|
||||||
const pluginClass = window.SISEditorPlugins[type].class;
|
const pluginEntry = window.SISEditorPlugins[type];
|
||||||
|
const pluginClass = pluginEntry.class;
|
||||||
|
const pluginConfig = pluginEntry.config || {};
|
||||||
let parsedData = {};
|
let parsedData = {};
|
||||||
if (this.data[`col${i}`]) {
|
if (this.data[`col${i}`]) {
|
||||||
try {
|
try {
|
||||||
@@ -223,12 +251,136 @@ class GridTool {
|
|||||||
const instance = new pluginClass({
|
const instance = new pluginClass({
|
||||||
data: parsedData,
|
data: parsedData,
|
||||||
api: this.api,
|
api: this.api,
|
||||||
readOnly: this.readOnly
|
readOnly: this.readOnly,
|
||||||
|
config: pluginConfig,
|
||||||
|
block: {
|
||||||
|
id: 'nested-grid-' + i + '-' + Math.random().toString(36).substring(7),
|
||||||
|
name: type,
|
||||||
|
holder: typeContainer,
|
||||||
|
isEmpty: false,
|
||||||
|
selected: false,
|
||||||
|
stretched: false,
|
||||||
|
tunes: {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.activeInstances[i] = instance;
|
this.activeInstances[i] = instance;
|
||||||
|
|
||||||
const element = instance.render();
|
const element = instance.render();
|
||||||
typeContainer.appendChild(element);
|
typeContainer.appendChild(element);
|
||||||
|
|
||||||
|
// Fix for Editor.js Image plugin 404 errors (spinner hangs forever)
|
||||||
|
if (type === 'image') {
|
||||||
|
// Custom UI override for nested images
|
||||||
|
const injectCustomButtons = () => {
|
||||||
|
if (element.querySelector('.sis-custom-image-controls')) return;
|
||||||
|
|
||||||
|
const fileBtn = element.querySelector('.cdx-button');
|
||||||
|
if (fileBtn) {
|
||||||
|
fileBtn.style.display = 'none'; // Hide native button
|
||||||
|
}
|
||||||
|
|
||||||
|
const controls = document.createElement('div');
|
||||||
|
controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light';
|
||||||
|
controls.style.display = 'flex';
|
||||||
|
controls.style.flexDirection = 'column';
|
||||||
|
controls.style.gap = '8px';
|
||||||
|
|
||||||
|
controls.innerHTML = `
|
||||||
|
<div class="text-muted small text-center mb-1"><i class="fas fa-cog"></i> Image Controls</div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm w-100" id="btn-upload-img-${i}"><i class="fas fa-upload"></i> Upload Image</button>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm w-100" id="btn-fetch-url-${i}"><i class="fas fa-link"></i> Fetch URL</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm w-100" id="btn-media-lib-${i}"><i class="fas fa-images"></i> Media Library</button>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm w-100 mt-2" id="btn-clear-img-${i}"><i class="fas fa-trash"></i> Remove Image</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const uploadBtn = controls.querySelector(`#btn-upload-img-${i}`);
|
||||||
|
if (uploadBtn && fileBtn) {
|
||||||
|
uploadBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileBtn.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlBtn = controls.querySelector(`#btn-fetch-url-${i}`);
|
||||||
|
if (urlBtn) {
|
||||||
|
urlBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const url = prompt('Enter Image URL:');
|
||||||
|
if (url && url.trim() !== '') {
|
||||||
|
this.data[`col${i}`] = {
|
||||||
|
file: { url: url.trim() },
|
||||||
|
caption: '',
|
||||||
|
withBorder: false,
|
||||||
|
withBackground: false,
|
||||||
|
stretched: false
|
||||||
|
};
|
||||||
|
this._renderTypeSpecificInput(i, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaBtn = controls.querySelector(`#btn-media-lib-${i}`);
|
||||||
|
if (mediaBtn) {
|
||||||
|
mediaBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (typeof openMediaPickerModal === 'function') {
|
||||||
|
openMediaPickerModal((url) => {
|
||||||
|
if (url) {
|
||||||
|
this.data[`col${i}`] = {
|
||||||
|
file: { url: url },
|
||||||
|
caption: '',
|
||||||
|
withBorder: false,
|
||||||
|
withBackground: false,
|
||||||
|
stretched: false
|
||||||
|
};
|
||||||
|
this._renderTypeSpecificInput(i, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Media Library is not yet configured on this system.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearBtn = controls.querySelector(`#btn-clear-img-${i}`);
|
||||||
|
if (clearBtn) {
|
||||||
|
clearBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm('Are you sure you want to remove this image?')) {
|
||||||
|
this.data[`col${i}`] = '';
|
||||||
|
this._renderTypeSpecificInput(i, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
element.appendChild(controls);
|
||||||
|
};
|
||||||
|
injectCustomButtons();
|
||||||
|
|
||||||
|
const handleImageError = () => {
|
||||||
|
element.classList.remove('image-tool--loading');
|
||||||
|
element.classList.add('image-tool--empty');
|
||||||
|
const preloader = element.querySelector('.image-tool__image-preloader');
|
||||||
|
if (preloader) preloader.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (node.tagName === 'IMG') {
|
||||||
|
node.addEventListener('error', handleImageError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(element, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
const existingImg = element.querySelector('img');
|
||||||
|
if (existingImg) existingImg.addEventListener('error', handleImageError);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to render plugin ${type} inside GridTool:`, err);
|
console.error(`Failed to render plugin ${type} inside GridTool:`, err);
|
||||||
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
typeContainer.innerHTML = `<div class="alert alert-danger small">Error loading plugin ${type}</div>`;
|
||||||
@@ -449,7 +601,9 @@ class GridTool {
|
|||||||
|
|
||||||
save(blockContent) {
|
save(blockContent) {
|
||||||
const savedData = {
|
const savedData = {
|
||||||
cols: this.data.cols
|
cols: this.data.cols,
|
||||||
|
globalClass: this.data.globalClass || '',
|
||||||
|
globalId: this.data.globalId || ''
|
||||||
};
|
};
|
||||||
for (let i = 1; i <= this.data.cols; i++) {
|
for (let i = 1; i <= this.data.cols; i++) {
|
||||||
const type = this.data[`type${i}`] || 'html';
|
const type = this.data[`type${i}`] || 'html';
|
||||||
|
|||||||
@@ -317,6 +317,9 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<label class="font-weight-bold mb-0"> <i class="fas fa-cubes"></i> Page Content (Block Editor) </label>
|
<label class="font-weight-bold mb-0"> <i class="fas fa-cubes"></i> Page Content (Block Editor) </label>
|
||||||
<div>
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success mr-1" id="exportJsonBtn">
|
||||||
|
<i class="fas fa-file-export"></i> Export JSON
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-info mr-1" id="importJsonBtn">
|
<button type="button" class="btn btn-sm btn-outline-info mr-1" id="importJsonBtn">
|
||||||
<i class="fas fa-file-import"></i> Import JSON
|
<i class="fas fa-file-import"></i> Import JSON
|
||||||
</button>
|
</button>
|
||||||
@@ -509,6 +512,43 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Export JSON Logic
|
||||||
|
var exportBtn = document.getElementById('exportJsonBtn');
|
||||||
|
if (exportBtn) {
|
||||||
|
exportBtn.addEventListener('click', function () {
|
||||||
|
if (window.sisEditor && typeof window.sisEditor.save === 'function') {
|
||||||
|
window.sisEditor.save().then(function (outputData) {
|
||||||
|
var jsonString = JSON.stringify(outputData, null, 2);
|
||||||
|
var blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
|
||||||
|
// Get page title for filename if possible, default to editor_backup
|
||||||
|
var titleInput = document.getElementById('title') || document.querySelector('input[name="title"]');
|
||||||
|
var filename = 'editor_backup.json';
|
||||||
|
if (titleInput && titleInput.value.trim()) {
|
||||||
|
filename = titleInput.value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_') + '_backup.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}, 0);
|
||||||
|
}).catch(function (error) {
|
||||||
|
console.error('[SIS Editor] Export failed:', error);
|
||||||
|
alert('Failed to export editor data: ' + error.message);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Editor instance is not initialized or ready.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Import JSON Modal Logic
|
// Import JSON Modal Logic
|
||||||
var importBtn = document.getElementById('importJsonBtn');
|
var importBtn = document.getElementById('importJsonBtn');
|
||||||
var importModal = $('#importJsonModal');
|
var importModal = $('#importJsonModal');
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
<!-- Add Meta Description for SEO -->
|
<!-- Add Meta Description for SEO -->
|
||||||
<meta name="description" th:if="${page.metaDescription != null}" th:content="${page.metaDescription}" />
|
<meta name="description" th:if="${page.metaDescription != null}" th:content="${page.metaDescription}" />
|
||||||
|
|
||||||
|
<!-- Load lightweight Bootstrap Grid CSS to support grid/flex block layouts without overriding theme styles -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap-grid.min.css" />
|
||||||
|
|
||||||
<!-- Customizer CSS overrides -->
|
<!-- Customizer CSS overrides -->
|
||||||
<style th:inline="css">
|
<style th:inline="css">
|
||||||
body {
|
body {
|
||||||
@@ -345,50 +348,64 @@
|
|||||||
|
|
||||||
<!-- 15. Grid / Columns Block -->
|
<!-- 15. Grid / Columns Block -->
|
||||||
<th:block th:if="${block['type'] == 'grid'}">
|
<th:block th:if="${block['type'] == 'grid'}">
|
||||||
<div class="container my-4">
|
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
|
||||||
|
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}">
|
||||||
|
<div class="container py-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
|
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
|
||||||
<div
|
<div
|
||||||
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i])} ? ${block.data['id' + i]} : null"
|
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i]) ? block.data['id' + i] : null}"
|
||||||
th:class="${block.data['width' + i] != null and block.data['width' + i] > 0} ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (${block.data['cols'] == 1} ? 'col-12 mb-4' : (${block.data['cols'] == 2} ? 'col-md-6 col-12 mb-4' : (${block.data['cols'] == 3} ? 'col-lg-4 col-md-6 col-12 mb-4' : (${block.data['cols'] == 4} ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))"
|
th:class="${(block.data['width' + i] != null and block.data['width' + i] gt 0) ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||||
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i])} ? ${block.data['class' + i]} : ''"
|
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i]) ? block.data['class' + i] : ''}"
|
||||||
>
|
>
|
||||||
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, data=${block.data})"></div>
|
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<!-- 16. Flex Layout Block -->
|
<!-- 16. Flex Layout Block -->
|
||||||
<th:block th:if="${block['type'] == 'flex'}">
|
<th:block th:if="${block['type'] == 'flex'}">
|
||||||
<div class="container my-4">
|
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
|
||||||
|
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}">
|
||||||
|
<div class="container py-4">
|
||||||
<div
|
<div
|
||||||
style="display: flex; flex-wrap: wrap;"
|
style="display: flex; flex-wrap: wrap;"
|
||||||
th:styleappend="'flex-direction: ' + ${block.data['direction']} + '; justify-content: ' + (${block.data['justify'] == 'start' ? 'flex-start' : (block.data['justify'] == 'end' ? 'flex-end' : (block.data['justify'] == 'between' ? 'space-between' : (block.data['justify'] == 'around' ? 'space-around' : (block.data['justify'] == 'evenly' ? 'space-evenly' : 'center'))))}) + '; align-items: ' + (${block.data['align'] == 'start' ? 'flex-start' : (block.data['align'] == 'end' ? 'flex-end' : block.data['align'])} ) + '; gap: ' + (${block.data['gap'] == '0' ? '0' : (block.data['gap'] == '1' ? '0.25rem' : (block.data['gap'] == '2' ? '0.5rem' : (block.data['gap'] == '3' ? '1rem' : (block.data['gap'] == '4' ? '1.5rem' : '2rem'))))}) + ';'"
|
th:styleappend="${'flex-direction: ' + (block.data['direction'] != null ? block.data['direction'] : 'row') + '; justify-content: ' + (block.data['justify'] == 'start' ? 'flex-start' : (block.data['justify'] == 'end' ? 'flex-end' : (block.data['justify'] == 'between' ? 'space-between' : (block.data['justify'] == 'around' ? 'space-around' : (block.data['justify'] == 'evenly' ? 'space-evenly' : 'center'))))) + '; align-items: ' + (block.data['align'] == 'start' ? 'flex-start' : (block.data['align'] == 'end' ? 'flex-end' : (block.data['align'] != null ? block.data['align'] : 'stretch'))) + '; gap: ' + (block.data['gap'] == '0' ? '0' : (block.data['gap'] == '1' ? '0.25rem' : (block.data['gap'] == '2' ? '0.5rem' : (block.data['gap'] == '3' ? '1rem' : (block.data['gap'] == '4' ? '1.5rem' : (block.data['gap'] == '5' ? '2rem' : '1rem')))))) + ';'}"
|
||||||
>
|
>
|
||||||
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
|
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
|
||||||
<div
|
<div
|
||||||
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i])} ? ${block.data['id' + i]} : null"
|
th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i]) ? block.data['id' + i] : null}"
|
||||||
th:class="${block.data['width' + i] != null and block.data['width' + i] > 0} ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (${block.data['cols'] == 1} ? 'col-12 mb-4' : (${block.data['cols'] == 2} ? 'col-md-6 col-12 mb-4' : (${block.data['cols'] == 3} ? 'col-lg-4 col-md-6 col-12 mb-4' : (${block.data['cols'] == 4} ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))"
|
th:class="${(block.data['width' + i] != null and block.data['width' + i] gt 0) ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||||
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i])} ? ${block.data['class' + i]} : ''"
|
th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i]) ? block.data['class' + i] : ''}"
|
||||||
>
|
>
|
||||||
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, data=${block.data})"></div>
|
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<!-- Shared Cell Content Renderer Fragment -->
|
<!-- Shared Cell Content Renderer Fragment -->
|
||||||
<th:block th:fragment="renderCell(type, i, data)">
|
<th:block th:fragment="renderCell(type, i, blockData)">
|
||||||
<!-- Type 1: HTML / Text -->
|
<!-- Type 1: HTML / Text -->
|
||||||
<th:block th:if="${type == null or type == 'html'}" th:utext="${data['col' + i]}"></th:block>
|
<th:block th:if="${type == null or type == 'html'}" th:utext="${blockData != null ? blockData['col' + i] : ''}"></th:block>
|
||||||
|
|
||||||
<!-- Type 2: Image -->
|
<!-- Type 2: Image -->
|
||||||
<th:block th:if="${type == 'image'}">
|
<th:block th:if="${type == 'image'}">
|
||||||
<div class="sis-grid-image-wrapper text-center">
|
<div class="sis-grid-image-wrapper text-center">
|
||||||
<img th:if="${data['imgUrl' + i] != null and !#strings.isEmpty(data['imgUrl' + i])}" th:src="${data['imgUrl' + i]}" class="img-fluid rounded shadow-sm" alt="Grid Image" />
|
<!-- Native Image Plugin structure -->
|
||||||
|
<th:block th:if="${blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['file'] != null}">
|
||||||
|
<img th:src="${blockData['col' + i]['file']['url']}" class="img-fluid rounded shadow-sm" th:alt="${blockData['col' + i]['caption']}" />
|
||||||
|
</th:block>
|
||||||
|
<!-- Custom imgUrl fallback -->
|
||||||
|
<th:block th:unless="${blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['file'] != null}">
|
||||||
|
<img th:if="${blockData != null and blockData['imgUrl' + i] != null and !#strings.isEmpty(blockData['imgUrl' + i])}" th:src="${blockData['imgUrl' + i]}" class="img-fluid rounded shadow-sm" alt="Grid Image" />
|
||||||
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
@@ -397,7 +414,7 @@
|
|||||||
<div class="sis-youtube-container text-center">
|
<div class="sis-youtube-container text-center">
|
||||||
<div
|
<div
|
||||||
class="sis-yt-thumb-wrapper"
|
class="sis-yt-thumb-wrapper"
|
||||||
th:data-embed-url="${data['ytUrl' + i]}"
|
th:data-embed-url="${blockData != null ? blockData['ytUrl' + i] : ''}"
|
||||||
style="
|
style="
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 56.25%;
|
padding-bottom: 56.25%;
|
||||||
@@ -412,7 +429,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
"
|
"
|
||||||
th:styleappend="${(data['ytUrl' + i] != null and #strings.contains(data['ytUrl' + i], 'youtu.be/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringAfter(data['ytUrl' + i], 'youtu.be/') + '/hqdefault.jpg' + ');' : ((data['ytUrl' + i] != null and #strings.contains(data['ytUrl' + i], 'v=')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(data['ytUrl' + i], 'v='), '&') + '/hqdefault.jpg' + ');' : ((data['ytUrl' + i] != null and #strings.contains(data['ytUrl' + i], 'embed/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(data['ytUrl' + i], 'embed/'), '?') + '/hqdefault.jpg' + ');' : ''))}"
|
th:styleappend="${blockData != null and blockData['ytUrl' + i] != null ? ((#strings.contains(blockData['ytUrl' + i], 'youtu.be/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringAfter(blockData['ytUrl' + i], 'youtu.be/') + '/hqdefault.jpg' + ');' : ((#strings.contains(blockData['ytUrl' + i], 'v=')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(blockData['ytUrl' + i], 'v='), '&') + '/hqdefault.jpg' + ');' : ((#strings.contains(blockData['ytUrl' + i], 'embed/')) ? 'background-image: url(' + 'https://img.youtube.com/vi/' + #strings.substringBefore(#strings.substringAfter(blockData['ytUrl' + i], 'embed/'), '?') + '/hqdefault.jpg' + ');' : ''))) : ''}"
|
||||||
>
|
>
|
||||||
<div class="sis-yt-play-btn" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: transform 0.2s ease">
|
<div class="sis-yt-play-btn" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: transform 0.2s ease">
|
||||||
<svg width="68" height="48" viewBox="0 0 68 48">
|
<svg width="68" height="48" viewBox="0 0 68 48">
|
||||||
@@ -428,24 +445,24 @@
|
|||||||
<th:block th:if="${type == 'accordion'}">
|
<th:block th:if="${type == 'accordion'}">
|
||||||
<details class="sis-accordion-item mb-3">
|
<details class="sis-accordion-item mb-3">
|
||||||
<summary class="sis-accordion-title font-weight-bold p-3 bg-light border rounded" style="cursor: pointer; user-select: none">
|
<summary class="sis-accordion-title font-weight-bold p-3 bg-light border rounded" style="cursor: pointer; user-select: none">
|
||||||
<span th:utext="${data['accTitle' + i]}">Accordion Title</span>
|
<span th:utext="${blockData != null ? blockData['accTitle' + i] : ''}">Accordion Title</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="sis-accordion-body p-3 border border-top-0 rounded-bottom" th:utext="${data['accContent' + i]}"></div>
|
<div class="sis-accordion-body p-3 border border-top-0 rounded-bottom" th:utext="${blockData != null ? blockData['accContent' + i] : ''}"></div>
|
||||||
</details>
|
</details>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<!-- Nested Plugin: html-snippet -->
|
<!-- Nested Plugin: html-snippet -->
|
||||||
<th:block th:if="${type == 'html-snippet' and data['col' + i] != null and data['col' + i]['id'] != null}">
|
<th:block th:if="${type == 'html-snippet' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['id'] != null}">
|
||||||
<div th:replace="~{fragments/snippets :: snippet(id=${data['col' + i]['id']})}"></div>
|
<div th:replace="~{fragments/snippets :: snippet(id=${blockData['col' + i]['id']})}"></div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<!-- Nested Plugin: timeline -->
|
<!-- Nested Plugin: timeline -->
|
||||||
<th:block th:if="${type == 'timeline' and data['col' + i] != null and data['col' + i]['items'] != null}">
|
<th:block th:if="${type == 'timeline' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['items'] != null}">
|
||||||
<section class="sis-timeline">
|
<section class="sis-timeline">
|
||||||
<div class="sis-timeline__grid">
|
<div class="sis-timeline__grid">
|
||||||
<div class="sis-timeline__text-col" style="width: 100%;">
|
<div class="sis-timeline__text-col" style="width: 100%;">
|
||||||
<div class="sis-timeline__line"></div>
|
<div class="sis-timeline__line"></div>
|
||||||
<th:block th:each="item : ${data['col' + i]['items']}">
|
<th:block th:each="item : ${blockData['col' + i]['items']}">
|
||||||
<div class="sis-timeline__entry">
|
<div class="sis-timeline__entry">
|
||||||
<div class="sis-timeline__dot"></div>
|
<div class="sis-timeline__dot"></div>
|
||||||
<div class="sis-timeline__entry-content">
|
<div class="sis-timeline__entry-content">
|
||||||
@@ -461,6 +478,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
<!-- Nested Plugin: grid (Recursive) -->
|
||||||
|
<th:block th:if="${type == 'grid' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['cols'] != null}">
|
||||||
|
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
|
||||||
|
th:class="${blockData['col' + i]['globalClass'] != null and !#strings.isEmpty(blockData['col' + i]['globalClass']) ? blockData['col' + i]['globalClass'] : null}">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<div class="row">
|
||||||
|
<th:block th:each="nestedI : ${#numbers.sequence(1, blockData['col' + i]['cols'])}">
|
||||||
|
<div
|
||||||
|
th:id="${blockData['col' + i]['id' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['id' + nestedI]) ? blockData['col' + i]['id' + nestedI] : null}"
|
||||||
|
th:class="${(blockData['col' + i]['width' + nestedI] != null and blockData['col' + i]['width' + nestedI] gt 0) ? ('col-lg-' + blockData['col' + i]['width' + nestedI] + ' col-md-6 col-12 mb-4') : (blockData['col' + i]['cols'] == 1 ? 'col-12 mb-4' : (blockData['col' + i]['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||||
|
th:classappend="${blockData['col' + i]['class' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['class' + nestedI]) ? blockData['col' + i]['class' + nestedI] : ''}"
|
||||||
|
>
|
||||||
|
<div th:replace=":: renderCell(type=${blockData['col' + i]['type' + nestedI]}, i=${nestedI}, blockData=${blockData['col' + i]})"></div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<!-- Nested Plugin: flex (Recursive) -->
|
||||||
|
<th:block th:if="${type == 'flex' and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['cols'] != null}">
|
||||||
|
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
|
||||||
|
th:class="${blockData['col' + i]['globalClass'] != null and !#strings.isEmpty(blockData['col' + i]['globalClass']) ? blockData['col' + i]['globalClass'] : null}">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<div
|
||||||
|
style="display: flex; flex-wrap: wrap;"
|
||||||
|
th:styleappend="${'flex-direction: ' + (blockData['col' + i]['direction'] != null ? blockData['col' + i]['direction'] : 'row') + '; justify-content: ' + (blockData['col' + i]['justify'] == 'start' ? 'flex-start' : (blockData['col' + i]['justify'] == 'end' ? 'flex-end' : (blockData['col' + i]['justify'] == 'between' ? 'space-between' : (blockData['col' + i]['justify'] == 'around' ? 'space-around' : (blockData['col' + i]['justify'] == 'evenly' ? 'space-evenly' : 'center'))))) + '; align-items: ' + (blockData['col' + i]['align'] == 'start' ? 'flex-start' : (blockData['col' + i]['align'] == 'end' ? 'flex-end' : (blockData['col' + i]['align'] != null ? blockData['col' + i]['align'] : 'stretch'))) + '; gap: ' + (blockData['col' + i]['gap'] == '0' ? '0' : (blockData['col' + i]['gap'] == '1' ? '0.25rem' : (blockData['col' + i]['gap'] == '2' ? '0.5rem' : (blockData['col' + i]['gap'] == '3' ? '1rem' : (blockData['col' + i]['gap'] == '4' ? '1.5rem' : (blockData['col' + i]['gap'] == '5' ? '2rem' : '1rem')))))) + ';'}"
|
||||||
|
>
|
||||||
|
<th:block th:each="nestedI : ${#numbers.sequence(1, blockData['col' + i]['cols'])}">
|
||||||
|
<div
|
||||||
|
th:id="${blockData['col' + i]['id' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['id' + nestedI]) ? blockData['col' + i]['id' + nestedI] : null}"
|
||||||
|
th:class="${(blockData['col' + i]['width' + nestedI] != null and blockData['col' + i]['width' + nestedI] gt 0) ? ('col-lg-' + blockData['col' + i]['width' + nestedI] + ' col-md-6 col-12 mb-4') : (blockData['col' + i]['cols'] == 1 ? 'col-12 mb-4' : (blockData['col' + i]['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (blockData['col' + i]['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}"
|
||||||
|
th:classappend="${blockData['col' + i]['class' + nestedI] != null and !#strings.isEmpty(blockData['col' + i]['class' + nestedI]) ? blockData['col' + i]['class' + nestedI] : ''}"
|
||||||
|
>
|
||||||
|
<div th:replace=":: renderCell(type=${blockData['col' + i]['type' + nestedI]}, i=${nestedI}, blockData=${blockData['col' + i]})"></div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
</th:block>
|
</th:block>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
@@ -2,24 +2,173 @@
|
|||||||
"time": 1700000000000,
|
"time": 1700000000000,
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{
|
{
|
||||||
"type": "raw",
|
"type": "stickyNav",
|
||||||
"data": {
|
"data": {
|
||||||
"html": "\n<section class=\"relative overflow-hidden xl:py-16 md:py-12 py-8\">\n <div class=\"absolute top-0 right-0 w-1/2 h-full bg-primary-100 -skew-x-12 translate-x-1/4 z-0 opacity-50 hidden lg:block\"></div>\n <div class=\"container relative z-10\">\n <div class=\"ck-layout-columns\" columns=\"2\">\n <!-- Column 1 (Left): Text -->\n <div>\n <div class=\"relative\">\n <div class=\"absolute md:-left-4 -left-3 md:-top-5 -top-2 md:size-12 size-8 bg-primary-100 rounded-full\"></div>\n <h2 class=\"display-3 text-primary-500 uppercase mb-4 block z-[1] relative\">TẦM NHÌN</h2>\n <div class=\"flex flex-col space-y-4\">\n <div class=\"bg-white p-4 md:p-6 rounded-lg overview-wrapper-shadow\">\n <p class=\"body-1 text-gray-800 text-justify !font-medium\">Trở thành bệnh viện đại học dẫn đầu Việt Nam đạt chuẩn quốc tế</p>\n </div>\n </div>\n </div>\n </div>\n <!-- Column 2 (Right): Image -->\n <div>\n <div class=\"relative rounded-lg overflow-hidden aspect-[3/2] shadow-lg\">\n <img alt=\"vision\" src=\"/images/umass/mission.jpeg\" class=\"w-full h-full object-cover transition-transform duration-700 hover:scale-110\" />\n </div>\n </div>\n </div>\n </div>\n</section>\n",
|
"title": "Tầm nhìn - Sứ mệnh",
|
||||||
"stretched": true
|
"bgColor": "#ffffff",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"label": "Giới thiệu tổng quan",
|
||||||
|
"link": "/page/gioi-thieu-tong-quan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Lịch sử hình thành",
|
||||||
|
"link": "/page/lich-su-hinh-thanh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Tầm nhìn - Sứ mệnh - Giá trị cốt lõi",
|
||||||
|
"link": "/page/tam-nhin-su-menh-gia-tri-cot-loi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ban lãnh đạo",
|
||||||
|
"link": "/page/doi-ngu-lanh-dao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Các đơn vị",
|
||||||
|
"link": "/page/cac-don-vi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Thành tựu và giải thưởng",
|
||||||
|
"link": "/page/thanh-tuu-va-giai-thuong"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "raw",
|
"type": "grid",
|
||||||
"data": {
|
"data": {
|
||||||
"html": "\n<section class=\"xl:py-16 md:py-12 py-8 bg-primary-500 text-white overflow-hidden\">\n <div class=\"container\">\n <div class=\"ck-layout-columns\" columns=\"2\">\n <!-- Column 1 (Left): Image -->\n <div>\n <div class=\"relative h-full\">\n <div class=\"relative rounded-lg overflow-hidden aspect-[3/2] shadow-lg\">\n <img alt=\"mission\" src=\"/images/umass/vision.jpeg\" class=\"w-full h-full object-cover\" />\n </div>\n </div>\n </div>\n <!-- Column 2 (Right): Text -->\n <div>\n <div class=\"relative\">\n <div class=\"absolute md:-left-5 -left-4 md:-top-5 -top-2 md:size-12 size-8 bg-white/50 rounded-full\"></div>\n <h2 class=\"display-3 text-white mb-4 relative z-[1]\">SỨ MỆNH</h2>\n </div>\n <div class=\"bg-white p-4 md:p-6 rounded-lg overview-wrapper-shadow\">\n <p class=\"body-1 text-primary-500 text-justify !font-medium\">Mang đến giải pháp chăm sóc sức khỏe tối ưu từ sự tích hợp giữa điều trị, nghiên cứu và đào tạo</p>\n </div>\n </div>\n </div>\n </div>\n</section>\n",
|
"cols": 2,
|
||||||
"stretched": true
|
"type1": "html",
|
||||||
|
"col1": "\n<div class=\"relative pt-4\">\n <div class=\"absolute md:-left-4 -left-3 md:-top-5 -top-2 md:size-12 size-8 bg-primary-100 rounded-full\"></div>\n <h2 class=\"display-3 text-primary-500 uppercase mb-4 block z-[1] relative\">TẦM NHÌN</h2>\n <div class=\"flex flex-col space-y-4\">\n <div class=\"bg-white p-4 md:p-6 rounded-lg overview-wrapper-shadow\">\n <p class=\"body-1 text-gray-800 text-justify !font-medium\">Trở thành bệnh viện đại học dẫn đầu Việt Nam đạt chuẩn quốc tế</p>\n </div>\n </div>\n</div>\n",
|
||||||
|
"width1": 6,
|
||||||
|
"class1": "relative z-10",
|
||||||
|
"type2": "image",
|
||||||
|
"col2": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/umass/mission.jpeg"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": false,
|
||||||
|
"withBackground": false,
|
||||||
|
"stretched": false
|
||||||
|
},
|
||||||
|
"width2": 6,
|
||||||
|
"class2": "shadow-lg rounded"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "raw",
|
"type": "grid",
|
||||||
"data": {
|
"data": {
|
||||||
"html": "\n<section class=\"bg-primary-75 py-12 overflow-hidden\">\n <div class=\"container\">\n <h2 class=\"text-center display-4 text-primary-600 xl:mb-6 md:mb-4 mb-3 uppercase\">Giá trị cốt lõi</h2>\n \n <div class=\"ck-layout-columns\" columns=\"4\">\n <!-- Column 1: Tiên phong -->\n <div class=\"p-3\">\n <div class=\"rounded-lg shadow bg-primary-600 text-white p-4 h-full flex flex-col items-center text-center\">\n <div class=\"mb-3\">\n <img alt=\"Tiên phong\" src=\"/images/career/core-hover-1.webp\" style=\"width: 80px; height: 80px; object-fit: contain;\" />\n </div>\n <h3 class=\"heading-1 font-weight-bold mb-2\">Tiên phong</h3>\n <p class=\"title-1\">Phát triển và ứng dụng kỹ thuật tiên tiến vào điều trị, giảng dạy, nghiên cứu</p>\n </div>\n </div>\n \n <!-- Column 2: Thấu hiểu -->\n <div class=\"p-3\">\n <div class=\"rounded-lg shadow bg-white text-primary-600 p-4 h-full flex flex-col items-center text-center border border-primary-100\">\n <div class=\"mb-3\">\n <img alt=\"Thấu hiểu\" src=\"/images/career/core-2.webp\" style=\"width: 80px; height: 80px; object-fit: contain;\" />\n </div>\n <h3 class=\"heading-1 font-weight-bold mb-2\">Thấu hiểu</h3>\n <p class=\"title-1 text-gray-700\">Nỗi đau thể xác và tinh thần của người bệnh để đưa ra giải pháp điều trị tối ưu</p>\n </div>\n </div>\n \n <!-- Column 3: Chuẩn mực -->\n <div class=\"p-3\">\n <div class=\"rounded-lg shadow bg-white text-primary-600 p-4 h-full flex flex-col items-center text-center border border-primary-100\">\n <div class=\"mb-3\">\n <img alt=\"Chuẩn mực\" src=\"/images/career/core-3.webp\" style=\"width: 80px; height: 80px; object-fit: contain;\" />\n </div>\n <h3 class=\"heading-1 font-weight-bold mb-2\">Chuẩn mực</h3>\n <p class=\"title-1 text-gray-700\">Về y đức và học thuật của người thầy giáo – thầy thuốc</p>\n </div>\n </div>\n \n <!-- Column 4: An toàn -->\n <div class=\"p-3\">\n <div class=\"rounded-lg shadow bg-white text-primary-600 p-4 h-full flex flex-col items-center text-center border border-primary-100\">\n <div class=\"mb-3\">\n <img alt=\"An toàn\" src=\"/images/career/core-4.webp\" style=\"width: 80px; height: 80px; object-fit: contain;\" />\n </div>\n <h3 class=\"heading-1 font-weight-bold mb-2\">An toàn</h3>\n <p class=\"title-1 text-gray-700\">Cho người bệnh, nhân viên y tế và cộng đồng</p>\n </div>\n </div>\n </div>\n </div>\n</section>\n",
|
"cols": 2,
|
||||||
"stretched": true
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/umass/vision.jpeg"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": false,
|
||||||
|
"withBackground": false,
|
||||||
|
"stretched": false
|
||||||
|
},
|
||||||
|
"width1": 6,
|
||||||
|
"class1": "shadow-lg rounded",
|
||||||
|
"type2": "html",
|
||||||
|
"col2": "\n<div class=\"relative pt-4\">\n <div class=\"absolute md:-left-5 -left-4 md:-top-5 -top-2 md:size-12 size-8 bg-primary-100 rounded-full\"></div>\n <h2 class=\"display-3 text-primary-500 mb-4 relative z-[1]\">SỨ MỆNH</h2>\n <div class=\"bg-white p-4 md:p-6 rounded-lg overview-wrapper-shadow\">\n <p class=\"body-1 text-primary-500 text-justify !font-medium\">Mang đến giải pháp chăm sóc sức khỏe tối ưu từ sự tích hợp giữa điều trị, nghiên cứu và đào tạo</p>\n </div>\n</div>\n",
|
||||||
|
"width2": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"data": {
|
||||||
|
"cols": 4,
|
||||||
|
"type1": "flex",
|
||||||
|
"col1": {
|
||||||
|
"cols": 2,
|
||||||
|
"direction": "column",
|
||||||
|
"justify": "center",
|
||||||
|
"align": "center",
|
||||||
|
"gap": "2",
|
||||||
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/career/core-hover-1.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": false,
|
||||||
|
"withBackground": false,
|
||||||
|
"stretched": false
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": "\n<div class=\"text-center\">\n <h3 class=\"heading-1 font-weight-bold mb-2\">Tiên phong</h3>\n <p class=\"title-1\">Phát triển và ứng dụng kỹ thuật tiên tiến vào điều trị, giảng dạy, nghiên cứu</p>\n</div>\n"
|
||||||
|
},
|
||||||
|
"width1": 3,
|
||||||
|
"class1": "rounded-lg shadow bg-primary-600 text-white p-4 h-full",
|
||||||
|
"type2": "flex",
|
||||||
|
"col2": {
|
||||||
|
"cols": 2,
|
||||||
|
"direction": "column",
|
||||||
|
"justify": "center",
|
||||||
|
"align": "center",
|
||||||
|
"gap": "2",
|
||||||
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/career/core-2.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": false,
|
||||||
|
"withBackground": false,
|
||||||
|
"stretched": false
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": "\n<div class=\"text-center\">\n <h3 class=\"heading-1 font-weight-bold mb-2\">Thấu hiểu</h3>\n <p class=\"title-1 text-gray-700\">Nỗi đau thể xác và tinh thần của người bệnh để đưa ra giải pháp điều trị tối ưu</p>\n</div>\n"
|
||||||
|
},
|
||||||
|
"width2": 3,
|
||||||
|
"class2": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100",
|
||||||
|
"type3": "flex",
|
||||||
|
"col3": {
|
||||||
|
"cols": 2,
|
||||||
|
"direction": "column",
|
||||||
|
"justify": "center",
|
||||||
|
"align": "center",
|
||||||
|
"gap": "2",
|
||||||
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/career/core-3.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": false,
|
||||||
|
"withBackground": false,
|
||||||
|
"stretched": false
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": "\n<div class=\"text-center\">\n <h3 class=\"heading-1 font-weight-bold mb-2\">Chuẩn mực</h3>\n <p class=\"title-1 text-gray-700\">Về y đức và học thuật của người thầy giáo – thầy thuốc</p>\n</div>\n"
|
||||||
|
},
|
||||||
|
"width3": 3,
|
||||||
|
"class3": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100",
|
||||||
|
"type4": "flex",
|
||||||
|
"col4": {
|
||||||
|
"cols": 2,
|
||||||
|
"direction": "column",
|
||||||
|
"justify": "center",
|
||||||
|
"align": "center",
|
||||||
|
"gap": "2",
|
||||||
|
"type1": "image",
|
||||||
|
"col1": {
|
||||||
|
"file": {
|
||||||
|
"url": "/images/career/core-4.webp"
|
||||||
|
},
|
||||||
|
"caption": "",
|
||||||
|
"withBorder": false,
|
||||||
|
"withBackground": false,
|
||||||
|
"stretched": false
|
||||||
|
},
|
||||||
|
"type2": "html",
|
||||||
|
"col2": "\n<div class=\"text-center\">\n <h3 class=\"heading-1 font-weight-bold mb-2\">An toàn</h3>\n <p class=\"title-1 text-gray-700\">Cho người bệnh, nhân viên y tế và cộng đồng</p>\n</div>\n"
|
||||||
|
},
|
||||||
|
"width4": 3,
|
||||||
|
"class4": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user