feat: implement custom CSS generator, improve code editor support, and add media picker and HTML tag management tools
@@ -0,0 +1,80 @@
|
|||||||
|
.sis-hover-card-grid {
|
||||||
|
padding-top: 30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"] {
|
||||||
|
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border: 1px solid #e6f0fa !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 37, 84, 0.08) !important;
|
||||||
|
padding: 30px 20px !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
min-height: 260px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"] img {
|
||||||
|
height: 80px !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
transition:
|
||||||
|
filter 0.4s ease,
|
||||||
|
transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"] h3 {
|
||||||
|
color: #0d71ba !important;
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
transition: color 0.4s ease !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"] p {
|
||||||
|
color: rgba(255, 255, 255, 0) !important;
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
max-height 0.4s ease,
|
||||||
|
opacity 0.4s ease,
|
||||||
|
color 0.4s ease,
|
||||||
|
margin-top 0.4s ease !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"]:hover,
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"].active {
|
||||||
|
background-color: #0d71ba !important;
|
||||||
|
border-color: #0d71ba !important;
|
||||||
|
transform: translateY(-8px) !important;
|
||||||
|
box-shadow: 0 12px 25px rgba(13, 113, 186, 0.3) !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"]:hover h3,
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"].active h3 {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"]:hover p,
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"].active p {
|
||||||
|
color: #ffffff !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
max-height: 150px;
|
||||||
|
margin-top: 12px !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"]:hover img,
|
||||||
|
.sis-hover-card-grid .row > [class*=\"col-\"].active img {
|
||||||
|
/* filter: brightness(0) invert(1) !important; */
|
||||||
|
transform: scale(1.05) !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var cards = document.querySelectorAll('.sis-hover-card-grid .row > [class*=\"col-\"]');
|
||||||
|
if (cards.length > 0) {
|
||||||
|
cards.forEach(function (card) {
|
||||||
|
card.addEventListener('click', function () {
|
||||||
|
cards.forEach(function (c) { c.classList.remove('active'); });
|
||||||
|
card.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<section class="sis-hover-card-v2-section py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="text-center display-4 text-primary-600 xl:mb-6 md:mb-4 mb-3 max-md:px-4 uppercase font-weight-bold">GIÁ TRỊ CỐT LÕI</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -565,6 +565,10 @@ figure.table table tr:hover {
|
|||||||
text-decoration-line: none;
|
text-decoration-line: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
5. Responsive Breakpoints — 3 Groups Only
|
5. Responsive Breakpoints — 3 Groups Only
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
@@ -1370,7 +1374,7 @@ Large Desktop (@media (min-width: 1366px)): Lines 1421 - 1481. */
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sis-timeline__mobile-img {
|
.sis-timeline__mobile-img {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1385,9 +1389,9 @@ Large Desktop (@media (min-width: 1366px)): Lines 1421 - 1481. */
|
|||||||
top: 174px;
|
top: 174px;
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sis-timeline__img-slot-empty {
|
.sis-timeline__img-slot-empty {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sis-timeline__img-wrapper {
|
.sis-timeline__img-wrapper {
|
||||||
@@ -1455,3 +1459,270 @@ Large Desktop (@media (min-width: 1366px)): Lines 1421 - 1481. */
|
|||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Dedicated Hover Cards Plugin Styles
|
||||||
|
========================================================================== */
|
||||||
|
.sis-hover-card-section {
|
||||||
|
padding-top: 30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
.sis-hover-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e6f0fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 37, 84, 0.06);
|
||||||
|
padding: 25px 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sis-hover-card__icon {
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: auto !important;
|
||||||
|
margin-bottom: 18px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
height: 80px !important;
|
||||||
|
width: 80px !important;
|
||||||
|
position: relative !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card__icon img {
|
||||||
|
height: 80px !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card__img-main {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 50% !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
display: block !important;
|
||||||
|
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card__img-hover {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 50% !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card__title {
|
||||||
|
color: var(--color-old-brick) !important;
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
transition: color 0.35s ease !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card__desc {
|
||||||
|
color: rgba(255, 255, 255, 0) !important;
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
line-height: 1.55 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
max-height 0.4s ease,
|
||||||
|
opacity 0.35s ease,
|
||||||
|
color 0.35s ease,
|
||||||
|
margin-top 0.35s ease !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card:hover,
|
||||||
|
.sis-hover-card.active {
|
||||||
|
background-color: #0d71ba !important;
|
||||||
|
border-color: #0d71ba !important;
|
||||||
|
transform: translateY(-8px) !important;
|
||||||
|
box-shadow: 0 12px 28px rgba(13, 113, 186, 0.28) !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card:hover .sis-hover-card__title,
|
||||||
|
.sis-hover-card.active .sis-hover-card__title {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card:hover .sis-hover-card__desc,
|
||||||
|
.sis-hover-card.active .sis-hover-card__desc {
|
||||||
|
color: #ffffff !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
max-height: 160px;
|
||||||
|
margin-top: 14px !important;
|
||||||
|
}
|
||||||
|
/* If hover image is present, swap images */
|
||||||
|
.sis-hover-card:hover .sis-hover-card__img-main:has(~ .sis-hover-card__img-hover),
|
||||||
|
.sis-hover-card.active .sis-hover-card__img-main:has(~ .sis-hover-card__img-hover) {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card:hover .sis-hover-card__img-hover,
|
||||||
|
.sis-hover-card.active .sis-hover-card__img-hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: translate(-50%, -50%) scale(1.3) !important;
|
||||||
|
}
|
||||||
|
/* Fallback image filter if hover image is NOT present */
|
||||||
|
.sis-hover-card:hover .sis-hover-card__icon img:not(.sis-hover-card__img-hover):not(:has(~ .sis-hover-card__img-hover)),
|
||||||
|
.sis-hover-card.active .sis-hover-card__icon img:not(.sis-hover-card__img-hover):not(:has(~ .sis-hover-card__img-hover)) {
|
||||||
|
transform: translate(-50%, -50%) scale(1.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Hover Cards Variant 2 (Hover 2 — Modern Blue & Glow Style)
|
||||||
|
========================================================================== */
|
||||||
|
.sis-hover-card-section .row,
|
||||||
|
.sis-hover-card-v2-section .row {
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2-section {
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2 {
|
||||||
|
border: 1px solid rgba(53, 157, 253, 0.25) !important;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
padding: 32px 20px !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
text-align: center !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
position: relative !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__icon {
|
||||||
|
margin-bottom: 18px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
height: 85px !important;
|
||||||
|
width: 85px !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__icon img {
|
||||||
|
height: 80px !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__title {
|
||||||
|
-webkit-background-clip: text !important;
|
||||||
|
font-size: 1.3rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
transition: all 0.35s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__desc {
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
font-size: 0.92rem !important;
|
||||||
|
line-height: 1.6 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
max-height 0.4s ease,
|
||||||
|
opacity 0.35s ease,
|
||||||
|
margin-top 0.35s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover 2 Active / Hover State */
|
||||||
|
.sis-hover-card-v2:hover,
|
||||||
|
.sis-hover-card-v2.active {
|
||||||
|
background: linear-gradient(135deg, #0d71ba 0%, #003c7e 100%) !important;
|
||||||
|
border-color: #359dfd !important;
|
||||||
|
transform: translateY(-8px) scale(1.02) !important;
|
||||||
|
box-shadow: 0 16px 36px rgba(13, 113, 186, 0.45) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2:hover .sis-hover-card__title,
|
||||||
|
.sis-hover-card-v2.active .sis-hover-card__title {
|
||||||
|
-webkit-background-clip: unset !important;
|
||||||
|
-webkit-text-fill-color: #ffffff !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2:hover .sis-hover-card__desc,
|
||||||
|
.sis-hover-card-v2.active .sis-hover-card__desc {
|
||||||
|
opacity: 1 !important;
|
||||||
|
max-height: 180px !important;
|
||||||
|
margin-top: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover 2 Image Swapping */
|
||||||
|
.sis-hover-card-v2:hover .sis-hover-card__img-main:has(~ .sis-hover-card__img-hover),
|
||||||
|
.sis-hover-card-v2.active .sis-hover-card__img-main:has(~ .sis-hover-card__img-hover) {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2:hover .sis-hover-card__img-hover,
|
||||||
|
.sis-hover-card-v2.active .sis-hover-card__img-hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: translate(-50%, -50%) scale(1.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sis-hover-card-v2:hover .sis-hover-card__icon img:not(.sis-hover-card__img-hover):not(:has(~ .sis-hover-card__img-hover)),
|
||||||
|
.sis-hover-card-v2.active .sis-hover-card__icon img:not(.sis-hover-card__img-hover):not(:has(~ .sis-hover-card__img-hover)) {
|
||||||
|
filter: brightness(0) invert(1) !important;
|
||||||
|
transform: translate(-50%, -50%) scale(1.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Layout for Tablet & Mobile */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.sis-hover-card-v2-section {
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
}
|
||||||
|
.sis-hover-card-v2 {
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 24px 16px !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__title {
|
||||||
|
font-size: 1.15rem !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__desc {
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
.sis-hover-card-v2-section {
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
.sis-hover-card-v2 {
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 18px 10px !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__icon {
|
||||||
|
height: 65px !important;
|
||||||
|
width: 65px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__icon img {
|
||||||
|
height: 60px !important;
|
||||||
|
}
|
||||||
|
.sis-hover-card-v2 .sis-hover-card__title {
|
||||||
|
font-size: 1.05rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,53 @@
|
|||||||
// Global plugin registry — external plugins register here
|
// Global plugin registry — external plugins register here
|
||||||
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global Media Library Modal Handler
|
||||||
|
* Callable from any Editor.js plugin via window.openSISMediaModal(callback)
|
||||||
|
*/
|
||||||
|
window.openSISMediaModal = function (callback) {
|
||||||
|
if (window.SISMediaPicker && typeof window.SISMediaPicker.open === 'function') {
|
||||||
|
window.SISMediaPicker.open(callback);
|
||||||
|
} else {
|
||||||
|
window.sisMediaSelectCallback = callback;
|
||||||
|
var modalEl = document.getElementById('sisMediaLibraryModal');
|
||||||
|
if (modalEl && typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||||||
|
$(modalEl).modal('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic Monaco Editor Script Loader Helper
|
||||||
|
*/
|
||||||
|
window.loadMonacoEditor = function () {
|
||||||
|
if (window.monaco) return Promise.resolve(window.monaco);
|
||||||
|
if (window._monacoLoadingPromise) return window._monacoLoadingPromise;
|
||||||
|
|
||||||
|
window._monacoLoadingPromise = new Promise((resolve, reject) => {
|
||||||
|
if (typeof require !== 'undefined' && require.config) {
|
||||||
|
require(['vs/editor/editor.main'], function () {
|
||||||
|
resolve(window.monaco);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js';
|
||||||
|
script.onload = () => {
|
||||||
|
window.require.config({
|
||||||
|
paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }
|
||||||
|
});
|
||||||
|
window.require(['vs/editor/editor.main'], () => {
|
||||||
|
resolve(window.monaco);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
script.onerror = (err) => reject(err);
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
return window._monacoLoadingPromise;
|
||||||
|
};
|
||||||
|
|
||||||
class SISRawHtmlTool {
|
class SISRawHtmlTool {
|
||||||
static get toolbox() {
|
static get toolbox() {
|
||||||
return {
|
return {
|
||||||
@@ -47,11 +94,13 @@ class SISRawHtmlTool {
|
|||||||
stretched: !!(data && data.stretched)
|
stretched: !!(data && data.stretched)
|
||||||
};
|
};
|
||||||
this.textarea = null;
|
this.textarea = null;
|
||||||
|
this.editorInstance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.style.width = '100%';
|
container.style.width = '100%';
|
||||||
|
container.className = 'ce-raw-html-wrapper mb-3';
|
||||||
|
|
||||||
const stretchWrapper = document.createElement('div');
|
const stretchWrapper = document.createElement('div');
|
||||||
stretchWrapper.className = 'custom-control custom-switch mb-2';
|
stretchWrapper.className = 'custom-control custom-switch mb-2';
|
||||||
@@ -72,32 +121,73 @@ class SISRawHtmlTool {
|
|||||||
|
|
||||||
stretchWrapper.appendChild(this.stretchCheck);
|
stretchWrapper.appendChild(this.stretchCheck);
|
||||||
stretchWrapper.appendChild(stretchLabel);
|
stretchWrapper.appendChild(stretchLabel);
|
||||||
|
container.appendChild(stretchWrapper);
|
||||||
|
|
||||||
|
// Header status bar
|
||||||
|
const headerBar = document.createElement('div');
|
||||||
|
headerBar.className = 'bg-dark text-white p-2 rounded-top d-flex justify-content-between align-items-center small font-weight-bold';
|
||||||
|
headerBar.innerHTML = '<span><i class="fab fa-html5 text-danger"></i> Raw HTML Code (Monaco IDE Editor)</span><span class="badge badge-info font-weight-normal">HTML5</span>';
|
||||||
|
container.appendChild(headerBar);
|
||||||
|
|
||||||
|
// Monaco Container
|
||||||
|
const editorContainer = document.createElement('div');
|
||||||
|
editorContainer.className = 'monaco-editor-box border border-dark rounded-bottom';
|
||||||
|
editorContainer.style.height = '350px';
|
||||||
|
editorContainer.style.width = '100%';
|
||||||
|
container.appendChild(editorContainer);
|
||||||
|
|
||||||
|
// Textarea fallback (hidden when Monaco is active)
|
||||||
this.textarea = document.createElement('textarea');
|
this.textarea = document.createElement('textarea');
|
||||||
this.textarea.className = 'form-control';
|
this.textarea.className = 'form-control d-none';
|
||||||
this.textarea.style.fontFamily = 'monospace';
|
|
||||||
this.textarea.style.fontSize = '13px';
|
|
||||||
this.textarea.style.minHeight = '250px';
|
|
||||||
this.textarea.style.whiteSpace = 'pre-wrap';
|
|
||||||
this.textarea.value = this.data.html || '';
|
this.textarea.value = this.data.html || '';
|
||||||
|
container.appendChild(this.textarea);
|
||||||
|
|
||||||
if (this.readOnly) {
|
if (this.readOnly) {
|
||||||
this.textarea.disabled = true;
|
|
||||||
this.stretchCheck.disabled = true;
|
this.stretchCheck.disabled = true;
|
||||||
|
this.textarea.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.textarea.addEventListener('input', () => {
|
// Initialize Monaco Editor
|
||||||
this.data.html = this.textarea.value;
|
window.loadMonacoEditor().then((monaco) => {
|
||||||
|
if (!editorContainer || !document.body.contains(editorContainer)) return;
|
||||||
|
this.editorInstance = monaco.editor.create(editorContainer, {
|
||||||
|
value: this.data.html || '',
|
||||||
|
language: 'html',
|
||||||
|
theme: 'vs-dark',
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
tabSize: 2,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
readOnly: !!this.readOnly
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editorInstance.onDidChangeModelContent(() => {
|
||||||
|
this.data.html = this.editorInstance.getValue();
|
||||||
|
if (this.textarea) this.textarea.value = this.data.html;
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn('[SIS Raw HTML Tool] Monaco Editor CDN fallback to Textarea:', err);
|
||||||
|
this.textarea.classList.remove('d-none');
|
||||||
|
this.textarea.style.minHeight = '300px';
|
||||||
|
this.textarea.style.backgroundColor = '#1e1e24';
|
||||||
|
this.textarea.style.color = '#66d9ef';
|
||||||
|
this.textarea.style.fontFamily = 'monospace';
|
||||||
|
editorContainer.style.display = 'none';
|
||||||
|
|
||||||
|
this.textarea.addEventListener('input', () => {
|
||||||
|
this.data.html = this.textarea.value;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
container.appendChild(stretchWrapper);
|
|
||||||
container.appendChild(this.textarea);
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(container) {
|
save() {
|
||||||
|
const htmlVal = this.editorInstance ? this.editorInstance.getValue() : (this.textarea ? this.textarea.value : (this.data.html || ''));
|
||||||
return {
|
return {
|
||||||
html: this.textarea ? this.textarea.value : this.data.html,
|
html: htmlVal,
|
||||||
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
|
stretched: this.stretchCheck ? this.stretchCheck.checked : !!(this.data && this.data.stretched)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ class FlexTool {
|
|||||||
direction: data.direction || 'row',
|
direction: data.direction || 'row',
|
||||||
justify: data.justify || 'start',
|
justify: data.justify || 'start',
|
||||||
align: data.align || 'stretch',
|
align: data.align || 'stretch',
|
||||||
gap: data.gap || '3' // standard Bootstrap gap level
|
gap: data.gap || '3', // standard Bootstrap gap level
|
||||||
|
globalClass: data.globalClass || '',
|
||||||
|
globalId: data.globalId || ''
|
||||||
};
|
};
|
||||||
this.activeInstances = {};
|
this.activeInstances = {};
|
||||||
|
|
||||||
@@ -186,6 +188,7 @@ class FlexTool {
|
|||||||
classInput.value = this.data.globalClass || '';
|
classInput.value = this.data.globalClass || '';
|
||||||
if (this.readOnly) classInput.disabled = true;
|
if (this.readOnly) classInput.disabled = true;
|
||||||
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||||
|
this.globalClassInput = classInput;
|
||||||
classDiv.appendChild(classInput);
|
classDiv.appendChild(classInput);
|
||||||
bgSettingsRow.appendChild(classDiv);
|
bgSettingsRow.appendChild(classDiv);
|
||||||
|
|
||||||
@@ -200,6 +203,7 @@ class FlexTool {
|
|||||||
idInput.value = this.data.globalId || '';
|
idInput.value = this.data.globalId || '';
|
||||||
if (this.readOnly) idInput.disabled = true;
|
if (this.readOnly) idInput.disabled = true;
|
||||||
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||||
|
this.globalIdInput = idInput;
|
||||||
idDiv.appendChild(idInput);
|
idDiv.appendChild(idInput);
|
||||||
bgSettingsRow.appendChild(idDiv);
|
bgSettingsRow.appendChild(idDiv);
|
||||||
|
|
||||||
@@ -710,8 +714,8 @@ class FlexTool {
|
|||||||
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 || '',
|
globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''),
|
||||||
globalId: this.data.globalId || ''
|
globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (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';
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ class GridTool {
|
|||||||
this.api = api;
|
this.api = api;
|
||||||
this.readOnly = readOnly;
|
this.readOnly = readOnly;
|
||||||
this.data = {
|
this.data = {
|
||||||
cols: parseInt(data.cols) || 2
|
cols: parseInt(data.cols) || 2,
|
||||||
|
globalClass: data.globalClass || '',
|
||||||
|
globalId: data.globalId || ''
|
||||||
};
|
};
|
||||||
this.activeInstances = {};
|
this.activeInstances = {};
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ class GridTool {
|
|||||||
classInput.value = this.data.globalClass || '';
|
classInput.value = this.data.globalClass || '';
|
||||||
if (this.readOnly) classInput.disabled = true;
|
if (this.readOnly) classInput.disabled = true;
|
||||||
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||||
|
this.globalClassInput = classInput;
|
||||||
classDiv.appendChild(classInput);
|
classDiv.appendChild(classInput);
|
||||||
settingsRow.appendChild(classDiv);
|
settingsRow.appendChild(classDiv);
|
||||||
|
|
||||||
@@ -94,6 +97,7 @@ class GridTool {
|
|||||||
idInput.value = this.data.globalId || '';
|
idInput.value = this.data.globalId || '';
|
||||||
if (this.readOnly) idInput.disabled = true;
|
if (this.readOnly) idInput.disabled = true;
|
||||||
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||||
|
this.globalIdInput = idInput;
|
||||||
idDiv.appendChild(idInput);
|
idDiv.appendChild(idInput);
|
||||||
settingsRow.appendChild(idDiv);
|
settingsRow.appendChild(idDiv);
|
||||||
|
|
||||||
@@ -602,8 +606,8 @@ class GridTool {
|
|||||||
save(blockContent) {
|
save(blockContent) {
|
||||||
const savedData = {
|
const savedData = {
|
||||||
cols: this.data.cols,
|
cols: this.data.cols,
|
||||||
globalClass: this.data.globalClass || '',
|
globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''),
|
||||||
globalId: this.data.globalId || ''
|
globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (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';
|
||||||
|
|||||||
@@ -0,0 +1,428 @@
|
|||||||
|
/**
|
||||||
|
* SISHoverCardTool — Dedicated Hover Cards Block Tool for Editor.js
|
||||||
|
* Allows content managers to freely add, remove, reorder, and configure interactive hover cards.
|
||||||
|
*/
|
||||||
|
class SISHoverCardTool {
|
||||||
|
static get toolbox() {
|
||||||
|
return {
|
||||||
|
title: 'Hover Cards',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect></svg>'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor({ data, api, readOnly }) {
|
||||||
|
this.api = api;
|
||||||
|
this.readOnly = readOnly;
|
||||||
|
this.instanceId = 'hc_' + Math.random().toString(36).substring(7);
|
||||||
|
this.data = {
|
||||||
|
cols: parseInt(data && data.cols) || 4,
|
||||||
|
styleVariant: (data && data.styleVariant) || 'v1',
|
||||||
|
globalClass: (data && data.globalClass) || 'sis-hover-card-section',
|
||||||
|
globalId: (data && data.globalId) || '',
|
||||||
|
items: (data && data.items && Array.isArray(data.items)) ? data.items : []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add initial default item if list is empty
|
||||||
|
if (this.data.items.length === 0) {
|
||||||
|
this.data.items.push({
|
||||||
|
title: 'Card Title',
|
||||||
|
description: 'Card description text...',
|
||||||
|
imageUrl: '',
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.wrapper = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.wrapper = document.createElement('div');
|
||||||
|
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-hovercard-tool-wrapper';
|
||||||
|
this.wrapper.style.fontFamily = 'inherit';
|
||||||
|
|
||||||
|
const titleLabel = document.createElement('div');
|
||||||
|
titleLabel.className = 'font-weight-bold text-primary small mb-3';
|
||||||
|
titleLabel.innerHTML = '<i class="fas fa-th-large"></i> Dedicated Hover Cards Settings';
|
||||||
|
this.wrapper.appendChild(titleLabel);
|
||||||
|
|
||||||
|
// Global Settings Row
|
||||||
|
const settingsRow = document.createElement('div');
|
||||||
|
settingsRow.className = 'form-row mb-3 pb-2 border-bottom';
|
||||||
|
|
||||||
|
// 1. Cols selector
|
||||||
|
const colDiv = document.createElement('div');
|
||||||
|
colDiv.className = 'col-md-3 mb-2';
|
||||||
|
colDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Columns per Row</label>';
|
||||||
|
const colSelect = document.createElement('select');
|
||||||
|
colSelect.className = 'form-control form-control-sm';
|
||||||
|
[1, 2, 3, 4, 6].forEach(num => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = num;
|
||||||
|
opt.textContent = `${num} Column${num > 1 ? 's' : ''}`;
|
||||||
|
if (num === this.data.cols) opt.selected = true;
|
||||||
|
colSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (this.readOnly) colSelect.disabled = true;
|
||||||
|
colSelect.addEventListener('change', (e) => {
|
||||||
|
this.data.cols = parseInt(e.target.value) || 4;
|
||||||
|
});
|
||||||
|
this.colsSelect = colSelect;
|
||||||
|
colDiv.appendChild(colSelect);
|
||||||
|
settingsRow.appendChild(colDiv);
|
||||||
|
|
||||||
|
// 2. Style Variant Selector (Hover 1 vs Hover 2)
|
||||||
|
const variantDiv = document.createElement('div');
|
||||||
|
variantDiv.className = 'col-md-3 mb-2';
|
||||||
|
variantDiv.innerHTML = '<label class="small font-weight-bold text-primary mb-1"><i class="fas fa-palette"></i> Card Style Variant</label>';
|
||||||
|
const variantSelect = document.createElement('select');
|
||||||
|
variantSelect.className = 'form-control form-control-sm';
|
||||||
|
|
||||||
|
const optV1 = document.createElement('option');
|
||||||
|
optV1.value = 'v1';
|
||||||
|
optV1.textContent = 'Hover 1 (Standard Light)';
|
||||||
|
if (this.data.styleVariant === 'v1') optV1.selected = true;
|
||||||
|
variantSelect.appendChild(optV1);
|
||||||
|
|
||||||
|
const optV2 = document.createElement('option');
|
||||||
|
optV2.value = 'v2';
|
||||||
|
optV2.textContent = 'Hover 2 (Modern Blue & Glow)';
|
||||||
|
if (this.data.styleVariant === 'v2') optV2.selected = true;
|
||||||
|
variantSelect.appendChild(optV2);
|
||||||
|
|
||||||
|
if (this.readOnly) variantSelect.disabled = true;
|
||||||
|
variantSelect.addEventListener('change', (e) => {
|
||||||
|
this.data.styleVariant = e.target.value;
|
||||||
|
});
|
||||||
|
this.variantSelect = variantSelect;
|
||||||
|
variantDiv.appendChild(variantSelect);
|
||||||
|
settingsRow.appendChild(variantDiv);
|
||||||
|
|
||||||
|
// 2. Global CSS Class
|
||||||
|
const classDiv = document.createElement('div');
|
||||||
|
classDiv.className = 'col-md-5 mb-2';
|
||||||
|
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Global CSS Class(es)</label>';
|
||||||
|
const classInput = document.createElement('input');
|
||||||
|
classInput.type = 'text';
|
||||||
|
classInput.className = 'form-control form-control-sm';
|
||||||
|
classInput.placeholder = 'e.g. sis-hover-card-section';
|
||||||
|
classInput.value = this.data.globalClass || '';
|
||||||
|
if (this.readOnly) classInput.disabled = true;
|
||||||
|
classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim());
|
||||||
|
this.globalClassInput = classInput;
|
||||||
|
classDiv.appendChild(classInput);
|
||||||
|
settingsRow.appendChild(classDiv);
|
||||||
|
|
||||||
|
// 3. Global HTML ID
|
||||||
|
const idDiv = document.createElement('div');
|
||||||
|
idDiv.className = 'col-md-4 mb-2';
|
||||||
|
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Global HTML ID</label>';
|
||||||
|
const idInput = document.createElement('input');
|
||||||
|
idInput.type = 'text';
|
||||||
|
idInput.className = 'form-control form-control-sm';
|
||||||
|
idInput.placeholder = 'e.g. hover-cards-section';
|
||||||
|
idInput.value = this.data.globalId || '';
|
||||||
|
if (this.readOnly) idInput.disabled = true;
|
||||||
|
idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim());
|
||||||
|
this.globalIdInput = idInput;
|
||||||
|
idDiv.appendChild(idInput);
|
||||||
|
settingsRow.appendChild(idDiv);
|
||||||
|
|
||||||
|
this.wrapper.appendChild(settingsRow);
|
||||||
|
|
||||||
|
// Cards items container
|
||||||
|
this.itemsContainer = document.createElement('div');
|
||||||
|
this.wrapper.appendChild(this.itemsContainer);
|
||||||
|
|
||||||
|
this._renderAllItems();
|
||||||
|
|
||||||
|
if (!this.readOnly) {
|
||||||
|
const addButton = document.createElement('button');
|
||||||
|
addButton.type = 'button';
|
||||||
|
addButton.className = 'btn btn-sm btn-success mt-2';
|
||||||
|
addButton.innerHTML = '<i class="fas fa-plus"></i> Add Hover Card';
|
||||||
|
addButton.addEventListener('click', () => {
|
||||||
|
this.data.items.push({
|
||||||
|
title: 'New Card Title',
|
||||||
|
description: 'New card description text...',
|
||||||
|
imageUrl: '',
|
||||||
|
active: false
|
||||||
|
});
|
||||||
|
this._renderAllItems();
|
||||||
|
});
|
||||||
|
this.wrapper.appendChild(addButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderAllItems() {
|
||||||
|
this.itemsContainer.innerHTML = '';
|
||||||
|
this.data.items.forEach((item, index) => {
|
||||||
|
this._renderItem(item, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderItem(item, index) {
|
||||||
|
const itemBox = document.createElement('div');
|
||||||
|
itemBox.className = 'mb-3 p-3 bg-white border rounded shadow-sm';
|
||||||
|
itemBox.style.position = 'relative';
|
||||||
|
|
||||||
|
// Top Control Action buttons (Move Up, Move Down, Delete)
|
||||||
|
if (!this.readOnly) {
|
||||||
|
const actionGroup = document.createElement('div');
|
||||||
|
actionGroup.style.position = 'absolute';
|
||||||
|
actionGroup.style.top = '10px';
|
||||||
|
actionGroup.style.right = '10px';
|
||||||
|
actionGroup.className = 'btn-group btn-group-sm';
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
const upBtn = document.createElement('button');
|
||||||
|
upBtn.type = 'button';
|
||||||
|
upBtn.className = 'btn btn-outline-secondary';
|
||||||
|
upBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
|
||||||
|
upBtn.title = 'Move Up';
|
||||||
|
upBtn.addEventListener('click', () => {
|
||||||
|
const temp = this.data.items[index];
|
||||||
|
this.data.items[index] = this.data.items[index - 1];
|
||||||
|
this.data.items[index - 1] = temp;
|
||||||
|
this._renderAllItems();
|
||||||
|
});
|
||||||
|
actionGroup.appendChild(upBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < this.data.items.length - 1) {
|
||||||
|
const downBtn = document.createElement('button');
|
||||||
|
downBtn.type = 'button';
|
||||||
|
downBtn.className = 'btn btn-outline-secondary';
|
||||||
|
downBtn.innerHTML = '<i class="fas fa-arrow-down"></i>';
|
||||||
|
downBtn.title = 'Move Down';
|
||||||
|
downBtn.addEventListener('click', () => {
|
||||||
|
const temp = this.data.items[index];
|
||||||
|
this.data.items[index] = this.data.items[index + 1];
|
||||||
|
this.data.items[index + 1] = temp;
|
||||||
|
this._renderAllItems();
|
||||||
|
});
|
||||||
|
actionGroup.appendChild(downBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.className = 'btn btn-danger';
|
||||||
|
removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
||||||
|
removeBtn.title = 'Remove Card';
|
||||||
|
removeBtn.addEventListener('click', () => {
|
||||||
|
this.data.items.splice(index, 1);
|
||||||
|
this._renderAllItems();
|
||||||
|
});
|
||||||
|
actionGroup.appendChild(removeBtn);
|
||||||
|
|
||||||
|
itemBox.appendChild(actionGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header Label
|
||||||
|
const itemHeader = document.createElement('h6');
|
||||||
|
itemHeader.className = 'font-weight-bold text-dark mb-3';
|
||||||
|
itemHeader.innerHTML = `<i class="fas fa-id-card text-info mr-1"></i> Card #${index + 1}`;
|
||||||
|
itemBox.appendChild(itemHeader);
|
||||||
|
|
||||||
|
// 1. Card Title
|
||||||
|
const titleGroup = document.createElement('div');
|
||||||
|
titleGroup.className = 'form-group mb-2';
|
||||||
|
titleGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Card Title</label>';
|
||||||
|
const titleInput = document.createElement('input');
|
||||||
|
titleInput.type = 'text';
|
||||||
|
titleInput.className = 'form-control form-control-sm';
|
||||||
|
titleInput.placeholder = 'e.g. Tiên phong';
|
||||||
|
titleInput.value = item.title || '';
|
||||||
|
if (this.readOnly) titleInput.disabled = true;
|
||||||
|
titleInput.addEventListener('input', (e) => { item.title = e.target.value; });
|
||||||
|
titleGroup.appendChild(titleInput);
|
||||||
|
itemBox.appendChild(titleGroup);
|
||||||
|
|
||||||
|
// 2. Card Description
|
||||||
|
const descGroup = document.createElement('div');
|
||||||
|
descGroup.className = 'form-group mb-2';
|
||||||
|
descGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Card Description</label>';
|
||||||
|
const descInput = document.createElement('textarea');
|
||||||
|
descInput.className = 'form-control form-control-sm';
|
||||||
|
descInput.rows = 2;
|
||||||
|
descInput.placeholder = 'e.g. Phát triển và ứng dụng kỹ thuật tiên tiến...';
|
||||||
|
descInput.value = item.description || '';
|
||||||
|
if (this.readOnly) descInput.disabled = true;
|
||||||
|
descInput.addEventListener('input', (e) => { item.description = e.target.value; });
|
||||||
|
descGroup.appendChild(descInput);
|
||||||
|
itemBox.appendChild(descGroup);
|
||||||
|
|
||||||
|
// 3. Image URL + Media Library Selector
|
||||||
|
const imgGroup = document.createElement('div');
|
||||||
|
imgGroup.className = 'form-group mb-2';
|
||||||
|
imgGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Card Icon / Image URL</label>';
|
||||||
|
|
||||||
|
const inputGroup = document.createElement('div');
|
||||||
|
inputGroup.className = 'input-group input-group-sm mb-2';
|
||||||
|
|
||||||
|
const imgInput = document.createElement('input');
|
||||||
|
imgInput.type = 'text';
|
||||||
|
imgInput.className = 'form-control';
|
||||||
|
imgInput.placeholder = '/uploads/2026/07/icon.jpg';
|
||||||
|
imgInput.value = item.imageUrl || '';
|
||||||
|
if (this.readOnly) imgInput.disabled = true;
|
||||||
|
|
||||||
|
const previewContainer = document.createElement('div');
|
||||||
|
previewContainer.className = 'mt-1';
|
||||||
|
const updatePreview = (url) => {
|
||||||
|
previewContainer.innerHTML = '';
|
||||||
|
if (url) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.style.maxHeight = '60px';
|
||||||
|
img.style.maxWidth = '100px';
|
||||||
|
img.style.objectFit = 'contain';
|
||||||
|
img.className = 'border rounded p-1 bg-light';
|
||||||
|
previewContainer.appendChild(img);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updatePreview(item.imageUrl);
|
||||||
|
|
||||||
|
imgInput.addEventListener('input', (e) => {
|
||||||
|
item.imageUrl = e.target.value.trim();
|
||||||
|
updatePreview(item.imageUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
inputGroup.appendChild(imgInput);
|
||||||
|
|
||||||
|
if (!this.readOnly) {
|
||||||
|
const appendDiv = document.createElement('div');
|
||||||
|
appendDiv.className = 'input-group-append';
|
||||||
|
|
||||||
|
const mediaBtn = document.createElement('button');
|
||||||
|
mediaBtn.type = 'button';
|
||||||
|
mediaBtn.className = 'btn btn-outline-info';
|
||||||
|
mediaBtn.innerHTML = '<i class="fas fa-images"></i> Media Library';
|
||||||
|
mediaBtn.addEventListener('click', () => {
|
||||||
|
if (typeof window.openSISMediaModal === 'function') {
|
||||||
|
window.openSISMediaModal((selectedUrl) => {
|
||||||
|
imgInput.value = selectedUrl;
|
||||||
|
item.imageUrl = selectedUrl;
|
||||||
|
updatePreview(selectedUrl);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Media modal function openSISMediaModal is not available.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
appendDiv.appendChild(mediaBtn);
|
||||||
|
inputGroup.appendChild(appendDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
imgGroup.appendChild(inputGroup);
|
||||||
|
imgGroup.appendChild(previewContainer);
|
||||||
|
itemBox.appendChild(imgGroup);
|
||||||
|
|
||||||
|
// 4. Hover Icon / Image URL (Swapped on hover)
|
||||||
|
const hoverImgGroup = document.createElement('div');
|
||||||
|
hoverImgGroup.className = 'form-group mb-2';
|
||||||
|
hoverImgGroup.innerHTML = '<label class="small font-weight-bold text-info mb-1"><i class="fas fa-sync-alt"></i> Hover Icon / Image URL (Optional - Swaps on Hover)</label>';
|
||||||
|
|
||||||
|
const hoverInputGroup = document.createElement('div');
|
||||||
|
hoverInputGroup.className = 'input-group input-group-sm mb-2';
|
||||||
|
|
||||||
|
const hoverImgInput = document.createElement('input');
|
||||||
|
hoverImgInput.type = 'text';
|
||||||
|
hoverImgInput.className = 'form-control';
|
||||||
|
hoverImgInput.placeholder = '/uploads/2026/07/hover-icon.jpg (optional)';
|
||||||
|
hoverImgInput.value = item.hoverImageUrl || '';
|
||||||
|
if (this.readOnly) hoverImgInput.disabled = true;
|
||||||
|
|
||||||
|
const hoverPreviewContainer = document.createElement('div');
|
||||||
|
hoverPreviewContainer.className = 'mt-1';
|
||||||
|
const updateHoverPreview = (url) => {
|
||||||
|
hoverPreviewContainer.innerHTML = '';
|
||||||
|
if (url) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.style.maxHeight = '60px';
|
||||||
|
img.style.maxWidth = '100px';
|
||||||
|
img.style.objectFit = 'contain';
|
||||||
|
img.className = 'border rounded p-1 bg-info text-white';
|
||||||
|
hoverPreviewContainer.appendChild(img);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateHoverPreview(item.hoverImageUrl);
|
||||||
|
|
||||||
|
hoverImgInput.addEventListener('input', (e) => {
|
||||||
|
item.hoverImageUrl = e.target.value.trim();
|
||||||
|
updateHoverPreview(item.hoverImageUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
hoverInputGroup.appendChild(hoverImgInput);
|
||||||
|
|
||||||
|
if (!this.readOnly) {
|
||||||
|
const appendDiv = document.createElement('div');
|
||||||
|
appendDiv.className = 'input-group-append';
|
||||||
|
|
||||||
|
const mediaBtn = document.createElement('button');
|
||||||
|
mediaBtn.type = 'button';
|
||||||
|
mediaBtn.className = 'btn btn-outline-info';
|
||||||
|
mediaBtn.innerHTML = '<i class="fas fa-images"></i> Media Library';
|
||||||
|
mediaBtn.addEventListener('click', () => {
|
||||||
|
if (typeof window.openSISMediaModal === 'function') {
|
||||||
|
window.openSISMediaModal((selectedUrl) => {
|
||||||
|
hoverImgInput.value = selectedUrl;
|
||||||
|
item.hoverImageUrl = selectedUrl;
|
||||||
|
updateHoverPreview(selectedUrl);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Media modal function openSISMediaModal is not available.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
appendDiv.appendChild(mediaBtn);
|
||||||
|
hoverInputGroup.appendChild(appendDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverImgGroup.appendChild(hoverInputGroup);
|
||||||
|
hoverImgGroup.appendChild(hoverPreviewContainer);
|
||||||
|
itemBox.appendChild(hoverImgGroup);
|
||||||
|
|
||||||
|
// Default Active Radio (Only 1 option active at a time)
|
||||||
|
const activeRadioDiv = document.createElement('div');
|
||||||
|
activeRadioDiv.className = 'custom-control custom-radio mt-2';
|
||||||
|
const activeRadio = document.createElement('input');
|
||||||
|
activeRadio.type = 'radio';
|
||||||
|
activeRadio.name = 'hovercard_active_' + this.instanceId;
|
||||||
|
activeRadio.className = 'custom-control-input';
|
||||||
|
const radioId = 'hovercard_active_' + index + '_' + Math.random().toString(36).substring(7);
|
||||||
|
activeRadio.id = radioId;
|
||||||
|
activeRadio.checked = !!item.active;
|
||||||
|
if (this.readOnly) activeRadio.disabled = true;
|
||||||
|
|
||||||
|
activeRadio.addEventListener('change', () => {
|
||||||
|
this.data.items.forEach((it, i) => {
|
||||||
|
it.active = (i === index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeLabel = document.createElement('label');
|
||||||
|
activeLabel.className = 'custom-control-label small font-weight-bold text-secondary';
|
||||||
|
activeLabel.htmlFor = radioId;
|
||||||
|
activeLabel.innerHTML = 'Default Active Card (Only 1 active at a time)';
|
||||||
|
|
||||||
|
activeRadioDiv.appendChild(activeRadio);
|
||||||
|
activeRadioDiv.appendChild(activeLabel);
|
||||||
|
itemBox.appendChild(activeRadioDiv);
|
||||||
|
|
||||||
|
this.itemsContainer.appendChild(itemBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
return {
|
||||||
|
cols: this.colsSelect ? parseInt(this.colsSelect.value) : (this.data.cols || 4),
|
||||||
|
styleVariant: this.variantSelect ? this.variantSelect.value : (this.data.styleVariant || 'v1'),
|
||||||
|
globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || 'sis-hover-card-section'),
|
||||||
|
globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''),
|
||||||
|
items: this.data.items || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the plugin globally
|
||||||
|
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||||
|
window.SISEditorPlugins['hovercard'] = { class: SISHoverCardTool };
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* SISTagTool — Custom HTML Tag Opening, Closing & Wrapper Block Tool for Editor.js
|
||||||
|
* Allows content managers to define custom HTML tags (div, section, article, header, footer, etc.)
|
||||||
|
* with custom Class(es), ID, and Attributes (data-*, style, aria-*, etc.).
|
||||||
|
*/
|
||||||
|
class SISTagTool {
|
||||||
|
static get toolbox() {
|
||||||
|
return {
|
||||||
|
title: 'HTML Tag (Open/Close)',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 18l6-6-6-6M8 6l-6 6 6 6"></path></svg>'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor({ data, api, readOnly }) {
|
||||||
|
this.api = api;
|
||||||
|
this.readOnly = readOnly;
|
||||||
|
this.data = {
|
||||||
|
mode: data && data.mode ? data.mode : 'open', // 'open' or 'close'
|
||||||
|
tagName: data && data.tagName ? data.tagName : 'div',
|
||||||
|
elementId: data && data.elementId ? data.elementId : '',
|
||||||
|
elementClass: data && data.elementClass ? data.elementClass : '',
|
||||||
|
attributes: data && data.attributes ? data.attributes : ''
|
||||||
|
};
|
||||||
|
this.wrapper = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.wrapper = document.createElement('div');
|
||||||
|
this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-tag-tool-wrapper';
|
||||||
|
this.wrapper.style.fontFamily = 'inherit';
|
||||||
|
this.wrapper.style.borderLeft = '4px solid #007bff';
|
||||||
|
|
||||||
|
const headerDiv = document.createElement('div');
|
||||||
|
headerDiv.className = 'font-weight-bold text-primary small mb-3 d-flex justify-content-between align-items-center';
|
||||||
|
headerDiv.innerHTML = '<span><i class="fas fa-code"></i> HTML Custom Tag Settings</span><span class="badge badge-primary tag-preview-badge" style="font-family: monospace;"><div></span>';
|
||||||
|
this.wrapper.appendChild(headerDiv);
|
||||||
|
|
||||||
|
const formRow = document.createElement('div');
|
||||||
|
formRow.className = 'form-row';
|
||||||
|
|
||||||
|
// 1. Tag Mode (Opening Tag / Closing Tag)
|
||||||
|
const modeDiv = document.createElement('div');
|
||||||
|
modeDiv.className = 'col-md-3 mb-2';
|
||||||
|
modeDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Tag Action / Mode</label>';
|
||||||
|
const modeSelect = document.createElement('select');
|
||||||
|
modeSelect.className = 'form-control form-control-sm';
|
||||||
|
|
||||||
|
const optOpen = document.createElement('option');
|
||||||
|
optOpen.value = 'open';
|
||||||
|
optOpen.textContent = 'Opening Tag (<tag>)';
|
||||||
|
if (this.data.mode === 'open') optOpen.selected = true;
|
||||||
|
modeSelect.appendChild(optOpen);
|
||||||
|
|
||||||
|
const optClose = document.createElement('option');
|
||||||
|
optClose.value = 'close';
|
||||||
|
optClose.textContent = 'Closing Tag (</tag>)';
|
||||||
|
if (this.data.mode === 'close') optClose.selected = true;
|
||||||
|
modeSelect.appendChild(optClose);
|
||||||
|
|
||||||
|
if (this.readOnly) modeSelect.disabled = true;
|
||||||
|
this.modeSelect = modeSelect;
|
||||||
|
modeDiv.appendChild(modeSelect);
|
||||||
|
formRow.appendChild(modeDiv);
|
||||||
|
|
||||||
|
// 2. Tag Name (div, section, article, header, footer, etc.)
|
||||||
|
const nameDiv = document.createElement('div');
|
||||||
|
nameDiv.className = 'col-md-3 mb-2';
|
||||||
|
nameDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Tag Name</label>';
|
||||||
|
const nameInput = document.createElement('input');
|
||||||
|
nameInput.type = 'text';
|
||||||
|
nameInput.className = 'form-control form-control-sm';
|
||||||
|
nameInput.placeholder = 'e.g. div, section, article';
|
||||||
|
nameInput.value = this.data.tagName || 'div';
|
||||||
|
if (this.readOnly) nameInput.disabled = true;
|
||||||
|
this.nameInput = nameInput;
|
||||||
|
nameDiv.appendChild(nameInput);
|
||||||
|
formRow.appendChild(nameDiv);
|
||||||
|
|
||||||
|
// 3. Element ID
|
||||||
|
const idDiv = document.createElement('div');
|
||||||
|
idDiv.className = 'col-md-3 mb-2';
|
||||||
|
idDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">ID (id="..." )</label>';
|
||||||
|
const idInput = document.createElement('input');
|
||||||
|
idInput.type = 'text';
|
||||||
|
idInput.className = 'form-control form-control-sm';
|
||||||
|
idInput.placeholder = 'e.g. heroSection';
|
||||||
|
idInput.value = this.data.elementId || '';
|
||||||
|
if (this.readOnly) idInput.disabled = true;
|
||||||
|
this.idInput = idInput;
|
||||||
|
idDiv.appendChild(idInput);
|
||||||
|
formRow.appendChild(idDiv);
|
||||||
|
|
||||||
|
// 4. Element CSS Class(es)
|
||||||
|
const classDiv = document.createElement('div');
|
||||||
|
classDiv.className = 'col-md-3 mb-2';
|
||||||
|
classDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Class(es) (class="..." )</label>';
|
||||||
|
const classInput = document.createElement('input');
|
||||||
|
classInput.type = 'text';
|
||||||
|
classInput.className = 'form-control form-control-sm';
|
||||||
|
classInput.placeholder = 'e.g. container py-5 my-section';
|
||||||
|
classInput.value = this.data.elementClass || '';
|
||||||
|
if (this.readOnly) classInput.disabled = true;
|
||||||
|
this.classInput = classInput;
|
||||||
|
classDiv.appendChild(classInput);
|
||||||
|
formRow.appendChild(classDiv);
|
||||||
|
|
||||||
|
// 5. Custom Attributes Row
|
||||||
|
const attrRow = document.createElement('div');
|
||||||
|
attrRow.className = 'form-row mt-2 attr-row';
|
||||||
|
const attrDiv = document.createElement('div');
|
||||||
|
attrDiv.className = 'col-12';
|
||||||
|
attrDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes (e.g. data-aos="fade-up" style="background:#fff")</label>';
|
||||||
|
const attrInput = document.createElement('input');
|
||||||
|
attrInput.type = 'text';
|
||||||
|
attrInput.className = 'form-control form-control-sm';
|
||||||
|
attrInput.placeholder = 'e.g. data-aos="fade-up" style="background: #f8f9fa;" aria-label="Section"';
|
||||||
|
attrInput.value = this.data.attributes || '';
|
||||||
|
if (this.readOnly) attrInput.disabled = true;
|
||||||
|
this.attrInput = attrInput;
|
||||||
|
attrDiv.appendChild(attrInput);
|
||||||
|
attrRow.appendChild(attrDiv);
|
||||||
|
|
||||||
|
// Live HTML Code Preview Box
|
||||||
|
const previewBox = document.createElement('div');
|
||||||
|
previewBox.className = 'mt-3 p-2 bg-dark text-success rounded small font-weight-bold text-monospace live-tag-preview';
|
||||||
|
previewBox.style.fontSize = '12px';
|
||||||
|
|
||||||
|
const updatePreview = () => {
|
||||||
|
const mode = this.modeSelect.value;
|
||||||
|
const tag = (this.nameInput.value.trim() || 'div').toLowerCase();
|
||||||
|
const idVal = this.idInput.value.trim();
|
||||||
|
const classVal = this.classInput.value.trim();
|
||||||
|
const attrVal = this.attrInput.value.trim();
|
||||||
|
|
||||||
|
if (mode === 'close') {
|
||||||
|
previewBox.textContent = `</${tag}>`;
|
||||||
|
attrRow.style.display = 'none';
|
||||||
|
idDiv.style.display = 'none';
|
||||||
|
classDiv.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
attrRow.style.display = 'flex';
|
||||||
|
idDiv.style.display = 'block';
|
||||||
|
classDiv.style.display = 'block';
|
||||||
|
let tagStr = `<${tag}`;
|
||||||
|
if (idVal) tagStr += ` id="${idVal}"`;
|
||||||
|
if (classVal) tagStr += ` class="${classVal}"`;
|
||||||
|
if (attrVal) tagStr += ` ${attrVal}`;
|
||||||
|
tagStr += `>`;
|
||||||
|
previewBox.textContent = tagStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewBadge = this.wrapper.querySelector('.tag-preview-badge');
|
||||||
|
if (previewBadge) previewBadge.textContent = previewBox.textContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.modeSelect.addEventListener('change', updatePreview);
|
||||||
|
this.nameInput.addEventListener('input', updatePreview);
|
||||||
|
this.idInput.addEventListener('input', updatePreview);
|
||||||
|
this.classInput.addEventListener('input', updatePreview);
|
||||||
|
this.attrInput.addEventListener('input', updatePreview);
|
||||||
|
|
||||||
|
this.wrapper.appendChild(formRow);
|
||||||
|
this.wrapper.appendChild(attrRow);
|
||||||
|
this.wrapper.appendChild(previewBox);
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
return this.wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const mode = this.modeSelect ? this.modeSelect.value : (this.data.mode || 'open');
|
||||||
|
const tagName = this.nameInput ? (this.nameInput.value.trim().toLowerCase() || 'div') : (this.data.tagName || 'div');
|
||||||
|
const elementId = this.idInput ? this.idInput.value.trim() : (this.data.elementId || '');
|
||||||
|
const elementClass = this.classInput ? this.classInput.value.trim() : (this.data.elementClass || '');
|
||||||
|
const attributes = this.attrInput ? this.attrInput.value.trim() : (this.data.attributes || '');
|
||||||
|
|
||||||
|
let rawHtmlTag = '';
|
||||||
|
if (mode === 'close') {
|
||||||
|
rawHtmlTag = `</${tagName}>`;
|
||||||
|
} else {
|
||||||
|
rawHtmlTag = `<${tagName}`;
|
||||||
|
if (elementId) rawHtmlTag += ` id="${elementId}"`;
|
||||||
|
if (elementClass) rawHtmlTag += ` class="${elementClass}"`;
|
||||||
|
if (attributes) rawHtmlTag += ` ${attributes}`;
|
||||||
|
rawHtmlTag += `>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: mode,
|
||||||
|
tagName: tagName,
|
||||||
|
elementId: elementId,
|
||||||
|
elementClass: elementClass,
|
||||||
|
attributes: attributes,
|
||||||
|
rawHtmlTag: rawHtmlTag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the plugin globally
|
||||||
|
window.SISEditorPlugins = window.SISEditorPlugins || {};
|
||||||
|
window.SISEditorPlugins['tag'] = { class: SISTagTool };
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* SIS Media Picker & Library Modal Utility
|
||||||
|
* Standalone reusable module to select and upload media files anywhere in the application.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* window.openSISMediaModal(function(selectedUrl, selectedMediaObj) {
|
||||||
|
* console.log("Selected Image URL:", selectedUrl);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* Or via object API:
|
||||||
|
* SISMediaPicker.open(function(selectedUrl, selectedMediaObj) { ... });
|
||||||
|
*/
|
||||||
|
(function (window, document) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var SISMediaPicker = {
|
||||||
|
callback: null,
|
||||||
|
mediaData: [],
|
||||||
|
modalId: 'sisMediaLibraryModal',
|
||||||
|
|
||||||
|
initModal: function () {
|
||||||
|
var modalEl = document.getElementById(this.modalId);
|
||||||
|
if (modalEl) return modalEl;
|
||||||
|
|
||||||
|
modalEl = document.createElement('div');
|
||||||
|
modalEl.id = this.modalId;
|
||||||
|
modalEl.className = 'modal fade';
|
||||||
|
modalEl.setAttribute('tabindex', '-1');
|
||||||
|
modalEl.setAttribute('role', 'dialog');
|
||||||
|
modalEl.setAttribute('aria-hidden', 'true');
|
||||||
|
modalEl.innerHTML =
|
||||||
|
'<div class="modal-dialog modal-xl" role="document">' +
|
||||||
|
'<div class="modal-content border-0 shadow-lg">' +
|
||||||
|
'<div class="modal-header bg-primary text-white p-3">' +
|
||||||
|
'<h5 class="modal-title font-weight-bold" id="' + this.modalId + 'Label">' +
|
||||||
|
'<i class="fas fa-images mr-2"></i> Thư viện Media / Chọn Hình ảnh' +
|
||||||
|
'</h5>' +
|
||||||
|
'<button type="button" class="close text-white opacity-100" data-dismiss="modal" aria-label="Close">' +
|
||||||
|
'<span aria-hidden="true">×</span>' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="modal-body bg-light p-3">' +
|
||||||
|
'<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 bg-white p-2 border rounded shadow-sm">' +
|
||||||
|
'<div class="input-group input-group-sm mb-2 mb-md-0" style="max-width: 380px;">' +
|
||||||
|
'<div class="input-group-prepend"><span class="input-group-text bg-light"><i class="fas fa-search text-muted"></i></span></div>' +
|
||||||
|
'<input type="text" class="form-control" id="sisMediaSearchInput" placeholder="Tìm kiếm theo tên file..." />' +
|
||||||
|
'</div>' +
|
||||||
|
'<div>' +
|
||||||
|
'<label class="btn btn-sm btn-success mb-0 cursor-pointer shadow-sm" style="cursor: pointer;">' +
|
||||||
|
'<i class="fas fa-cloud-upload-alt mr-1"></i> Tải ảnh mới lên' +
|
||||||
|
'<input type="file" id="sisMediaUploadInput" accept="image/*" style="display: none;" />' +
|
||||||
|
'</label>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="sisMediaGrid" class="row" style="max-height: 480px; overflow-y: auto; align-content: start;">' +
|
||||||
|
'<div class="col-12 text-center text-muted py-5"><i class="fas fa-spinner fa-spin fa-2x"></i><br/><span class="mt-2 d-block">Đang tải danh sách hình ảnh...</span></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="modal-footer bg-white p-2">' +
|
||||||
|
'<button type="button" class="btn btn-sm btn-secondary px-3" data-dismiss="modal">Đóng</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
document.body.appendChild(modalEl);
|
||||||
|
|
||||||
|
// Bind Search Input Handler
|
||||||
|
var self = this;
|
||||||
|
document.addEventListener('input', function (e) {
|
||||||
|
if (e.target && e.target.id === 'sisMediaSearchInput') {
|
||||||
|
var term = e.target.value.toLowerCase().trim();
|
||||||
|
var filtered = self.mediaData.filter(function (item) {
|
||||||
|
var name = (item.originalFilename || item.name || '').toLowerCase();
|
||||||
|
return name.includes(term);
|
||||||
|
});
|
||||||
|
self.renderGrid(filtered);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind Upload Input Handler
|
||||||
|
document.addEventListener('change', function (e) {
|
||||||
|
if (e.target && e.target.id === 'sisMediaUploadInput') {
|
||||||
|
var file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
var grid = document.getElementById('sisMediaGrid');
|
||||||
|
if (grid) {
|
||||||
|
grid.innerHTML = '<div class="col-12 text-center text-info py-5"><i class="fas fa-spinner fa-spin fa-2x"></i><br/><span class="mt-2 d-block">Đang tải tệp lên server...</span></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/manage/media/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (data && data.success === 1 && data.file && data.file.url) {
|
||||||
|
self.selectItem(data.file.url, data.file);
|
||||||
|
} else {
|
||||||
|
alert('Tải tệp lên thất bại. Vui lòng thử lại!');
|
||||||
|
self.fetchMediaList();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
alert('Lỗi tải tệp: ' + err.message);
|
||||||
|
self.fetchMediaList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return modalEl;
|
||||||
|
},
|
||||||
|
|
||||||
|
open: function (callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
var modalEl = this.initModal();
|
||||||
|
|
||||||
|
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||||||
|
$(modalEl).modal('show');
|
||||||
|
} else {
|
||||||
|
modalEl.style.display = 'block';
|
||||||
|
modalEl.classList.add('show');
|
||||||
|
}
|
||||||
|
this.fetchMediaList();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchMediaList: function () {
|
||||||
|
var grid = document.getElementById('sisMediaGrid');
|
||||||
|
if (grid) {
|
||||||
|
grid.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="fas fa-spinner fa-spin fa-2x text-primary"></i><br/><span class="mt-2 d-block small">Đang nạp thư viện...</span></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
fetch('/api/manage/media/list')
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
self.mediaData = data || [];
|
||||||
|
self.renderGrid(self.mediaData);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
if (grid) {
|
||||||
|
grid.innerHTML = '<div class="col-12 text-center text-danger py-4"><i class="fas fa-exclamation-triangle fa-2x mb-2"></i><br/>Không thể tải danh sách tệp. Bạn có thể dán đường dẫn URL trực tiếp.</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderGrid: function (list) {
|
||||||
|
var grid = document.getElementById('sisMediaGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="far fa-folder-open fa-2x mb-2"></i><br/>Chưa có tệp hình ảnh nào. Bấm nút "Tải ảnh mới lên" để thêm.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
grid.innerHTML = list.map(function (item) {
|
||||||
|
var url = item.fileUrl || item.url || '';
|
||||||
|
var name = item.originalFilename || item.name || 'Image';
|
||||||
|
return '<div class="col-lg-2 col-md-3 col-sm-4 col-6 mb-3">' +
|
||||||
|
'<div class="card h-100 border shadow-sm media-select-card" style="cursor: pointer; transition: all 0.2s;" data-url="' + url + '">' +
|
||||||
|
'<div style="height: 110px; background: #f8f9fc; display: flex; align-items: center; justify-content: center; overflow: hidden;" class="p-1">' +
|
||||||
|
'<img src="' + url + '" style="max-height: 100%; max-width: 100%; object-fit: contain;" alt="' + name + '" />' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="card-body p-2 text-center bg-white border-top">' +
|
||||||
|
'<p class="card-text small text-truncate m-0 font-weight-bold text-dark" title="' + name + '">' + name + '</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Add click listeners to items
|
||||||
|
grid.querySelectorAll('.media-select-card').forEach(function (card) {
|
||||||
|
card.addEventListener('click', function () {
|
||||||
|
var selectedUrl = card.getAttribute('data-url');
|
||||||
|
self.selectItem(selectedUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
selectItem: function (url, mediaObj) {
|
||||||
|
if (typeof this.callback === 'function') {
|
||||||
|
this.callback(url, mediaObj);
|
||||||
|
}
|
||||||
|
var modalEl = document.getElementById(this.modalId);
|
||||||
|
if (modalEl) {
|
||||||
|
if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
|
||||||
|
$(modalEl).modal('hide');
|
||||||
|
} else {
|
||||||
|
modalEl.style.display = 'none';
|
||||||
|
modalEl.classList.remove('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose standalone module globally
|
||||||
|
window.SISMediaPicker = SISMediaPicker;
|
||||||
|
|
||||||
|
// Backward-compatible global helper function
|
||||||
|
window.openSISMediaModal = function (callback) {
|
||||||
|
SISMediaPicker.open(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
})(window, document);
|
||||||
@@ -225,7 +225,12 @@
|
|||||||
<label for="customCss" class="font-weight-bold text-primary mb-0">
|
<label for="customCss" class="font-weight-bold text-primary mb-0">
|
||||||
<i class="fab fa-css3-alt"></i> Custom CSS (Page-Specific Styles)
|
<i class="fab fa-css3-alt"></i> Custom CSS (Page-Specific Styles)
|
||||||
</label>
|
</label>
|
||||||
<span class="badge badge-info px-2 py-1" style="font-family: monospace;">CSS3</span>
|
<div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm px-2 py-0 mr-1" id="btnGenMediaQueryPage" title="Auto-generate Media Queries for Mobile, Tablet & Desktop" style="font-size: 11px;">
|
||||||
|
<i class="fas fa-mobile-alt"></i> Auto-Generate Media Queries
|
||||||
|
</button>
|
||||||
|
<span class="badge badge-info px-2 py-1" style="font-family: monospace;">CSS3</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
class="form-control text-white"
|
class="form-control text-white"
|
||||||
@@ -434,6 +439,11 @@
|
|||||||
<script th:src="@{/js/manage/editor-plugins/timeline.js}"></script>
|
<script th:src="@{/js/manage/editor-plugins/timeline.js}"></script>
|
||||||
<script th:src="@{/js/manage/editor-plugins/grid.js}"></script>
|
<script th:src="@{/js/manage/editor-plugins/grid.js}"></script>
|
||||||
<script th:src="@{/js/manage/editor-plugins/flex.js}"></script>
|
<script th:src="@{/js/manage/editor-plugins/flex.js}"></script>
|
||||||
|
<script th:src="@{/js/manage/editor-plugins/hover-card.js}"></script>
|
||||||
|
<script th:src="@{/js/manage/editor-plugins/tag.js}"></script>
|
||||||
|
|
||||||
|
<!-- SIS Standalone Media Picker Library -->
|
||||||
|
<script th:src="@{/js/manage/sis-media-picker.js}"></script>
|
||||||
|
|
||||||
<!-- Init Editor -->
|
<!-- Init Editor -->
|
||||||
<script th:src="@{/js/manage/editor-config.js}"></script>
|
<script th:src="@{/js/manage/editor-config.js}"></script>
|
||||||
@@ -518,6 +528,12 @@
|
|||||||
exportBtn.addEventListener('click', function () {
|
exportBtn.addEventListener('click', function () {
|
||||||
if (window.sisEditor && typeof window.sisEditor.save === 'function') {
|
if (window.sisEditor && typeof window.sisEditor.save === 'function') {
|
||||||
window.sisEditor.save().then(function (outputData) {
|
window.sisEditor.save().then(function (outputData) {
|
||||||
|
// Attach dedicated CSS and JS if they exist
|
||||||
|
var cssElement = document.getElementById('customCss');
|
||||||
|
var jsElement = document.getElementById('customJs');
|
||||||
|
if (cssElement) outputData.customCss = cssElement.value;
|
||||||
|
if (jsElement) outputData.customJs = jsElement.value;
|
||||||
|
|
||||||
var jsonString = JSON.stringify(outputData, null, 2);
|
var jsonString = JSON.stringify(outputData, null, 2);
|
||||||
var blob = new Blob([jsonString], { type: 'application/json' });
|
var blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
@@ -594,6 +610,16 @@
|
|||||||
throw new Error('Invalid Editor.js format: Missing "blocks" array.');
|
throw new Error('Invalid Editor.js format: Missing "blocks" array.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If dedicated CSS and JS exist in the backup, load them into textareas
|
||||||
|
if (parsed.hasOwnProperty('customCss')) {
|
||||||
|
var cssElement = document.getElementById('customCss');
|
||||||
|
if (cssElement) cssElement.value = parsed.customCss;
|
||||||
|
}
|
||||||
|
if (parsed.hasOwnProperty('customJs')) {
|
||||||
|
var jsElement = document.getElementById('customJs');
|
||||||
|
if (jsElement) jsElement.value = parsed.customJs;
|
||||||
|
}
|
||||||
|
|
||||||
if (window.sisEditor && typeof window.sisEditor.render === 'function') {
|
if (window.sisEditor && typeof window.sisEditor.render === 'function') {
|
||||||
window.sisEditor.render(parsed).then(function () {
|
window.sisEditor.render(parsed).then(function () {
|
||||||
importModal.modal('hide');
|
importModal.modal('hide');
|
||||||
@@ -610,6 +636,68 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Dedicated Custom CSS Auto Generator (Desktop, Tablet & Mobile Media Queries) ---
|
||||||
|
function insertResponsiveMediaQueries(textarea) {
|
||||||
|
if (!textarea) return;
|
||||||
|
var template = `/* ==========================================================================
|
||||||
|
DEFAULT / DESKTOP STYLES (≥ 992px)
|
||||||
|
========================================================================== */
|
||||||
|
.custom-section {
|
||||||
|
padding: 40px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
TABLET RESPONSIVE STYLES (768px – 991px)
|
||||||
|
========================================================================== */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.custom-section {
|
||||||
|
padding: 25px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
MOBILE PHONE RESPONSIVE STYLES (≤ 767px)
|
||||||
|
========================================================================== */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.custom-section {
|
||||||
|
padding: 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
if (textarea.value.trim() === '') {
|
||||||
|
textarea.value = template;
|
||||||
|
} else {
|
||||||
|
textarea.value += '\n\n' + template;
|
||||||
|
}
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
var btnGenCss = document.getElementById('btnGenMediaQueryPage');
|
||||||
|
if (btnGenCss) {
|
||||||
|
btnGenCss.addEventListener('click', function () {
|
||||||
|
insertResponsiveMediaQueries(document.getElementById('customCss'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab indent support for Code Textareas
|
||||||
|
function enableTabIndentation(textarea) {
|
||||||
|
if (!textarea) return;
|
||||||
|
textarea.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = this.selectionStart;
|
||||||
|
var end = this.selectionEnd;
|
||||||
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||||
|
this.selectionStart = this.selectionEnd = start + 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
enableTabIndentation(document.getElementById('customCss'));
|
||||||
|
enableTabIndentation(document.getElementById('customJs'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -213,7 +213,12 @@
|
|||||||
<label for="postCustomCss" class="font-weight-bold text-primary mb-0">
|
<label for="postCustomCss" class="font-weight-bold text-primary mb-0">
|
||||||
<i class="fab fa-css3-alt"></i> Custom CSS (Post-Specific Styles)
|
<i class="fab fa-css3-alt"></i> Custom CSS (Post-Specific Styles)
|
||||||
</label>
|
</label>
|
||||||
<span class="badge badge-info px-2 py-1" style="font-family: monospace;">CSS3</span>
|
<div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm px-2 py-0 mr-1" id="btnGenMediaQueryPost" title="Auto-generate Media Queries for Mobile, Tablet & Desktop" style="font-size: 11px;">
|
||||||
|
<i class="fas fa-mobile-alt"></i> Auto-Generate Media Queries
|
||||||
|
</button>
|
||||||
|
<span class="badge badge-info px-2 py-1" style="font-family: monospace;">CSS3</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
class="form-control text-white"
|
class="form-control text-white"
|
||||||
@@ -746,6 +751,68 @@
|
|||||||
}
|
}
|
||||||
layoutSelect.addEventListener('change', toggleEventTime);
|
layoutSelect.addEventListener('change', toggleEventTime);
|
||||||
toggleEventTime(); // Initial check
|
toggleEventTime(); // Initial check
|
||||||
|
|
||||||
|
// --- Dedicated Custom CSS Auto Generator (Desktop, Tablet & Mobile Media Queries) ---
|
||||||
|
function insertResponsiveMediaQueries(textarea) {
|
||||||
|
if (!textarea) return;
|
||||||
|
var template = `/* ==========================================================================
|
||||||
|
DEFAULT / DESKTOP STYLES (≥ 992px)
|
||||||
|
========================================================================== */
|
||||||
|
.custom-post-block {
|
||||||
|
padding: 40px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
TABLET RESPONSIVE STYLES (768px – 991px)
|
||||||
|
========================================================================== */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.custom-post-block {
|
||||||
|
padding: 25px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
MOBILE PHONE RESPONSIVE STYLES (≤ 767px)
|
||||||
|
========================================================================== */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.custom-post-block {
|
||||||
|
padding: 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
if (textarea.value.trim() === '') {
|
||||||
|
textarea.value = template;
|
||||||
|
} else {
|
||||||
|
textarea.value += '\n\n' + template;
|
||||||
|
}
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
var btnGenCssPost = document.getElementById('btnGenMediaQueryPost');
|
||||||
|
if (btnGenCssPost) {
|
||||||
|
btnGenCssPost.addEventListener('click', function () {
|
||||||
|
insertResponsiveMediaQueries(document.getElementById('postCustomCss'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab indent support for Code Textareas
|
||||||
|
function enableTabIndentation(textarea) {
|
||||||
|
if (!textarea) return;
|
||||||
|
textarea.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = this.selectionStart;
|
||||||
|
var end = this.selectionEnd;
|
||||||
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||||
|
this.selectionStart = this.selectionEnd = start + 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
enableTabIndentation(document.getElementById('postCustomCss'));
|
||||||
|
enableTabIndentation(document.getElementById('postCustomJs'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Tag click-to-add helper ---
|
// --- Tag click-to-add helper ---
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
}
|
}
|
||||||
/*[[${themeModCustomCss}]]*/
|
/*[[${themeModCustomCss}]]*/
|
||||||
</style>
|
</style>
|
||||||
<!-- Dedicated Page Custom CSS -->
|
|
||||||
<style th:if="${page != null and page.customCss != null and !page.customCss.empty}" th:utext="${page.customCss}"></style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Render Editor.js Blocks -->
|
<!-- Render Editor.js Blocks -->
|
||||||
<div layout:fragment="content">
|
<div layout:fragment="content">
|
||||||
|
<!-- Dedicated Page Custom CSS -->
|
||||||
|
<style th:if="${page != null and page.customCss != null and !page.customCss.empty}" th:utext="${page.customCss}"></style>
|
||||||
<th:block th:each="block : ${blocks}">
|
<th:block th:each="block : ${blocks}">
|
||||||
<div
|
<div
|
||||||
th:class="${(block.data != null and block.data['stretched'] == true) or (block.data != null and block.data['stretched'] == null and block['stretched'] == true) ? 'sis-fullwidth-breakout' : ''}"
|
th:class="${(block.data != null and block.data['stretched'] == true) or (block.data != null and block.data['stretched'] == null and block['stretched'] == true) ? 'sis-fullwidth-breakout' : ''}"
|
||||||
@@ -387,8 +387,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</th:block>
|
||||||
</th:block>
|
|
||||||
|
<!-- 17. Dedicated Hover Cards Block -->
|
||||||
|
<th:block th:if="${block['type'] == 'hovercard' or block['type'] == 'hover-card' or block['type'] == 'hoverCard'}">
|
||||||
|
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
|
||||||
|
th:class="${(block.data['styleVariant'] == 'v2' or (block.data['globalClass'] != null and block.data['globalClass'].contains('v2'))) ? ('sis-hover-card-v2-section ' + (block.data['globalClass'] != null ? block.data['globalClass'] : '')) : ('sis-hover-card-section ' + (block.data['globalClass'] != null ? block.data['globalClass'] : ''))}">
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<th:block th:each="item, iterStat : ${block.data['items']}">
|
||||||
|
<div th:class="${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'] == 6 ? 'col-lg-2 col-md-4 col-6 mb-4' : 'col-lg-3 col-md-6 col-12 mb-4')))}">
|
||||||
|
<div th:class="${(block.data['styleVariant'] == 'v2' or (block.data['globalClass'] != null and block.data['globalClass'].contains('v2'))) ? ('sis-hover-card sis-hover-card-v2 ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : '')) : ('sis-hover-card ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : ''))}">
|
||||||
|
<div class="w-full flex flex-col items-center justify-center transition-all duration-500">
|
||||||
|
<div class="sis-hover-card__icon" th:if="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}">
|
||||||
|
<img th:src="${item['imageUrl']}" class="sis-hover-card__img-main" th:alt="${item['title']}" />
|
||||||
|
<img th:if="${item['hoverImageUrl'] != null and !#strings.isEmpty(item['hoverImageUrl'])}" th:src="${item['hoverImageUrl']}" class="sis-hover-card__img-hover" th:alt="${item['title']}" />
|
||||||
|
</div>
|
||||||
|
<h3 class="sis-hover-card__title" th:utext="${item['title']}">Title</h3>
|
||||||
|
</div>
|
||||||
|
<p class="sis-hover-card__desc" th:utext="${item['description']}">Description</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<!-- 18. Custom HTML Tag Block (Opening or Closing Tag) -->
|
||||||
|
<th:block th:if="${block['type'] == 'tag'}">
|
||||||
|
<th:block th:if="${block.data['rawHtmlTag'] != null and !#strings.isEmpty(block.data['rawHtmlTag'])}"
|
||||||
|
th:utext="${block.data['rawHtmlTag']}"></th:block>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
<!-- Shared Cell Content Renderer Fragment -->
|
<!-- Shared Cell Content Renderer Fragment -->
|
||||||
<th:block th:fragment="renderCell(type, i, blockData)">
|
<th:block th:fragment="renderCell(type, i, blockData)">
|
||||||
@@ -479,6 +509,37 @@
|
|||||||
</section>
|
</section>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
|
<!-- Nested Plugin: hovercard -->
|
||||||
|
<th:block th:if="${(type == 'hovercard' or type == 'hover-card' or type == 'hoverCard') and blockData != null and blockData['col' + i] != null and blockData['col' + i] != '' and blockData['col' + i]['items'] != 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]['styleVariant'] == 'v2' or (blockData['col' + i]['globalClass'] != null and blockData['col' + i]['globalClass'].contains('v2'))) ? ('sis-hover-card-v2-section ' + (blockData['col' + i]['globalClass'] != null ? blockData['col' + i]['globalClass'] : '')) : ('sis-hover-card-section ' + (blockData['col' + i]['globalClass'] != null ? blockData['col' + i]['globalClass'] : ''))}">
|
||||||
|
<div class="container-fluid p-0">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<th:block th:each="item, iterStat : ${blockData['col' + i]['items']}">
|
||||||
|
<div th:class="${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'] == 6 ? 'col-lg-2 col-md-4 col-6 mb-4' : 'col-lg-3 col-md-6 col-12 mb-4')))}">
|
||||||
|
<div th:class="${(blockData['col' + i]['styleVariant'] == 'v2' or (blockData['col' + i]['globalClass'] != null and blockData['col' + i]['globalClass'].contains('v2'))) ? ('sis-hover-card sis-hover-card-v2 ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : '')) : ('sis-hover-card ' + ((item['active'] == true or (item['active'] == null and iterStat.first)) ? 'active' : ''))}">
|
||||||
|
<div class="w-full flex flex-col items-center justify-center transition-all duration-500">
|
||||||
|
<div class="sis-hover-card__icon" th:if="${item['imageUrl'] != null and !#strings.isEmpty(item['imageUrl'])}">
|
||||||
|
<img th:src="${item['imageUrl']}" class="sis-hover-card__img-main" th:alt="${item['title']}" />
|
||||||
|
<img th:if="${item['hoverImageUrl'] != null and !#strings.isEmpty(item['hoverImageUrl'])}" th:src="${item['hoverImageUrl']}" class="sis-hover-card__img-hover" th:alt="${item['title']}" />
|
||||||
|
</div>
|
||||||
|
<h3 class="sis-hover-card__title" th:utext="${item['title']}">Title</h3>
|
||||||
|
</div>
|
||||||
|
<p class="sis-hover-card__desc" th:utext="${item['description']}">Description</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<!-- Nested Plugin: tag -->
|
||||||
|
<th:block th:if="${type == 'tag' and blockData != null and blockData['col' + i] != null}">
|
||||||
|
<th:block th:if="${blockData['col' + i]['rawHtmlTag'] != null and !#strings.isEmpty(blockData['col' + i]['rawHtmlTag'])}"
|
||||||
|
th:utext="${blockData['col' + i]['rawHtmlTag']}"></th:block>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
<!-- Nested Plugin: grid (Recursive) -->
|
<!-- 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}">
|
<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}"
|
<div th:id="${blockData['col' + i]['globalId'] != null and !#strings.isEmpty(blockData['col' + i]['globalId']) ? blockData['col' + i]['globalId'] : null}"
|
||||||
@@ -630,6 +691,19 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Interactive Hover Cards — Mouseenter transfers active class to hovered card
|
||||||
|
document.querySelectorAll('.sis-hover-card').forEach(function (card) {
|
||||||
|
card.addEventListener('mouseenter', function () {
|
||||||
|
var parentRow = card.closest('.row');
|
||||||
|
if (parentRow) {
|
||||||
|
parentRow.querySelectorAll('.sis-hover-card').forEach(function (c) {
|
||||||
|
c.classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
card.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<!-- Dedicated Page Custom JS -->
|
<!-- Dedicated Page Custom JS -->
|
||||||
|
|||||||
@@ -5,13 +5,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
||||||
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
||||||
<!-- Dedicated Post Custom CSS & JS -->
|
|
||||||
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
|
||||||
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div layout:fragment="content">
|
<div layout:fragment="content">
|
||||||
|
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
||||||
|
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
||||||
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
|
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
|
||||||
<div class="t--template t--article">
|
<div class="t--template t--article">
|
||||||
|
|
||||||
|
|||||||
@@ -5,51 +5,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
||||||
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
||||||
<!-- Dedicated Post Custom CSS & JS -->
|
|
||||||
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
|
||||||
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
|
||||||
<style>
|
|
||||||
.livestream-video-wrapper {
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 56.25%; /* 16:9 Aspect Ratio */
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
.livestream-video-wrapper iframe {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.livestream-meta {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.livestream-title {
|
|
||||||
font-size: 2.25rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #111;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
.livestream-date {
|
|
||||||
color: #881C1C;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div layout:fragment="content">
|
<div layout:fragment="content">
|
||||||
|
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
||||||
|
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
||||||
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
|
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
|
||||||
<div class="t--template t--article">
|
<div class="t--template t--article">
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
||||||
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
||||||
<!-- Dedicated Post Custom CSS & JS -->
|
|
||||||
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
|
||||||
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div layout:fragment="content">
|
<div layout:fragment="content">
|
||||||
|
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
||||||
|
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
||||||
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
|
<div id="block-umass-base-content" class="block block-system block-system-main-block tc--template-container tc--article">
|
||||||
<div class="t--template t--article">
|
<div class="t--template t--article">
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
<title th:text="${post.title} + ' | UMass Amherst'">Post Title | UMass Amherst</title>
|
||||||
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
<meta name="description" th:content="${post.metaDescription ?: post.excerpt}">
|
||||||
<!-- Dedicated Post Custom CSS & JS -->
|
|
||||||
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
|
||||||
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div layout:fragment="content-top">
|
<div layout:fragment="content-top">
|
||||||
|
<style th:if="${post != null and post.customCss != null and !post.customCss.empty}" th:utext="${post.customCss}"></style>
|
||||||
|
<script th:if="${post != null and post.customJs != null and !post.customJs.empty}" th:utext="${post.customJs}"></script>
|
||||||
<!-- Content Top (Banner 3 lớp kết hợp Title bài Post) -->
|
<!-- Content Top (Banner 3 lớp kết hợp Title bài Post) -->
|
||||||
<div class="content-top">
|
<div class="content-top">
|
||||||
<div class="lc--layout-container lc--full">
|
<div class="lc--layout-container lc--full">
|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"time": 1700000000000,
|
"time": 1785560196216,
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{
|
{
|
||||||
|
"id": "HEo3OKYI07",
|
||||||
"type": "stickyNav",
|
"type": "stickyNav",
|
||||||
"data": {
|
"data": {
|
||||||
"title": "Tầm nhìn - Sứ mệnh",
|
"title": "Tầm nhìn - Sứ mệnh",
|
||||||
@@ -31,146 +32,124 @@
|
|||||||
"label": "Thành tựu và giải thưởng",
|
"label": "Thành tựu và giải thưởng",
|
||||||
"link": "/page/thanh-tuu-va-giai-thuong"
|
"link": "/page/thanh-tuu-va-giai-thuong"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"sticky": true,
|
||||||
|
"stretched": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "_9-7doWIJA",
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"data": {
|
"data": {
|
||||||
"cols": 2,
|
"cols": 2,
|
||||||
|
"globalClass": "",
|
||||||
|
"globalId": "",
|
||||||
"type1": "html",
|
"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",
|
"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,
|
"imgUrl1": "",
|
||||||
|
"ytUrl1": "",
|
||||||
|
"accTitle1": "",
|
||||||
|
"accContent1": "",
|
||||||
"class1": "relative z-10",
|
"class1": "relative z-10",
|
||||||
|
"id1": "",
|
||||||
|
"width1": 6,
|
||||||
"type2": "image",
|
"type2": "image",
|
||||||
"col2": {
|
"col2": {
|
||||||
"file": {
|
"file": {
|
||||||
"url": "/images/umass/mission.jpeg"
|
"extension": "webp",
|
||||||
|
"size": 98106,
|
||||||
|
"name": "dotquy203.webp",
|
||||||
|
"url": "/uploads/2026/08/dotquy203.webp"
|
||||||
},
|
},
|
||||||
"caption": "",
|
"caption": "",
|
||||||
"withBorder": false,
|
"withBorder": false,
|
||||||
"withBackground": false,
|
"stretched": false,
|
||||||
"stretched": false
|
"withBackground": false
|
||||||
},
|
},
|
||||||
"width2": 6,
|
"imgUrl2": "",
|
||||||
"class2": "shadow-lg rounded"
|
"ytUrl2": "",
|
||||||
}
|
"accTitle2": "",
|
||||||
},
|
"accContent2": "",
|
||||||
{
|
"class2": "shadow-lg rounded",
|
||||||
"type": "grid",
|
"id2": "",
|
||||||
"data": {
|
|
||||||
"cols": 2,
|
|
||||||
"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
|
"width2": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "SL3DLP54-m",
|
||||||
"type": "grid",
|
"type": "grid",
|
||||||
"data": {
|
"data": {
|
||||||
"cols": 4,
|
"cols": 2,
|
||||||
"type1": "flex",
|
"globalClass": "",
|
||||||
|
"globalId": "",
|
||||||
|
"type1": "image",
|
||||||
"col1": {
|
"col1": {
|
||||||
"cols": 2,
|
"file": {
|
||||||
"direction": "column",
|
"url": "/uploads/2026/07/dotquy201.webp"
|
||||||
"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",
|
"caption": "",
|
||||||
"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"
|
"withBorder": false,
|
||||||
|
"stretched": false,
|
||||||
|
"withBackground": false
|
||||||
},
|
},
|
||||||
"width1": 3,
|
"imgUrl1": "",
|
||||||
"class1": "rounded-lg shadow bg-primary-600 text-white p-4 h-full",
|
"ytUrl1": "",
|
||||||
"type2": "flex",
|
"accTitle1": "",
|
||||||
"col2": {
|
"accContent1": "",
|
||||||
"cols": 2,
|
"class1": "shadow-lg rounded",
|
||||||
"direction": "column",
|
"id1": "",
|
||||||
"justify": "center",
|
"width1": 6,
|
||||||
"align": "center",
|
"type2": "html",
|
||||||
"gap": "2",
|
"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",
|
||||||
"type1": "image",
|
"imgUrl2": "",
|
||||||
"col1": {
|
"ytUrl2": "",
|
||||||
"file": {
|
"accTitle2": "",
|
||||||
"url": "/images/career/core-2.webp"
|
"accContent2": "",
|
||||||
},
|
"class2": "",
|
||||||
"caption": "",
|
"id2": "",
|
||||||
"withBorder": false,
|
"width2": 6
|
||||||
"withBackground": false,
|
}
|
||||||
"stretched": false
|
},
|
||||||
|
{
|
||||||
|
"id": "bJ3ycvyeZT",
|
||||||
|
"type": "hovercard",
|
||||||
|
"data": {
|
||||||
|
"cols": 4,
|
||||||
|
"styleVariant": "v2",
|
||||||
|
"globalClass": "sis-hover-card-v2-section",
|
||||||
|
"globalId": "",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Tiên phong",
|
||||||
|
"description": "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",
|
||||||
|
"imageUrl": "/images/career/core-1.webp",
|
||||||
|
"hoverImageUrl": "/images/career/core-hover-1.webp",
|
||||||
|
"active": true
|
||||||
},
|
},
|
||||||
"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"
|
"title": "Thấu hiểu",
|
||||||
},
|
"description": "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",
|
||||||
"width2": 3,
|
"imageUrl": "/images/career/core-2.webp",
|
||||||
"class2": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100",
|
"hoverImageUrl": "/images/career/core-hover-2.webp",
|
||||||
"type3": "flex",
|
"active": false
|
||||||
"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"
|
"title": "Chuẩn mực",
|
||||||
},
|
"description": "Về y đức và học thuật của người thầy giáo – thầy thuốc",
|
||||||
"width3": 3,
|
"imageUrl": "/images/career/core-3.webp",
|
||||||
"class3": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100",
|
"hoverImageUrl": "/images/career/core-hover-3.webp",
|
||||||
"type4": "flex",
|
"active": false
|
||||||
"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"
|
"title": "An toàn",
|
||||||
},
|
"description": "Cho người bệnh, nhân viên y tế và cộng đồng",
|
||||||
"width4": 3,
|
"imageUrl": "/images/career/core-4.webp",
|
||||||
"class4": "rounded-lg shadow bg-white text-primary-600 p-4 h-full border border-primary-100"
|
"hoverImageUrl": "/images/career/core-hover-4.webp",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": "2.30.8"
|
"version": "2.31.0-rc.7"
|
||||||
}
|
}
|
||||||