feat: implement custom CSS generator, improve code editor support, and add media picker and HTML tag management tools

This commit is contained in:
2026-08-01 18:34:51 +07:00
parent 2a8c6bfc39
commit 1ee67e6a46
29 changed files with 1807 additions and 4083 deletions
@@ -25,13 +25,13 @@
}
/*[[${themeModCustomCss}]]*/
</style>
<!-- Dedicated Page Custom CSS -->
<style th:if="${page != null and page.customCss != null and !page.customCss.empty}" th:utext="${page.customCss}"></style>
</head>
<body>
<!-- Render Editor.js Blocks -->
<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}">
<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' : ''}"
@@ -387,8 +387,38 @@
</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 -->
<th:block th:fragment="renderCell(type, i, blockData)">
@@ -479,6 +509,37 @@
</section>
</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) -->
<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}"
@@ -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>
<!-- Dedicated Page Custom JS -->