From cc228e5c5208335bbfdaa9d39def7312f71bd7cd Mon Sep 17 00:00:00 2001 From: Victor Phan Date: Sat, 1 Aug 2026 19:10:46 +0700 Subject: [PATCH] feat: add support for global and element-specific custom HTML attributes across editor plugins and page templates --- .../src/main/resources/static/css/custom.css | 18 +++++-- .../static/js/manage/editor-plugins/flex.js | 29 +++++++++-- .../static/js/manage/editor-plugins/grid.js | 27 ++++++++-- .../js/manage/editor-plugins/hover-card.js | 37 ++++++++++++-- .../js/manage/editor-plugins/html-snippet.js | 20 ++++++-- .../js/manage/editor-plugins/timeline.js | 17 +++++-- .../src/main/resources/templates/page.html | 51 +++++++++++++++---- 7 files changed, 168 insertions(+), 31 deletions(-) diff --git a/sisvietnamvn_main/src/main/resources/static/css/custom.css b/sisvietnamvn_main/src/main/resources/static/css/custom.css index 18645f15..1f017ed5 100644 --- a/sisvietnamvn_main/src/main/resources/static/css/custom.css +++ b/sisvietnamvn_main/src/main/resources/static/css/custom.css @@ -569,6 +569,18 @@ figure.table table tr:hover { text-align: center; } +.bg-white { + background-color: var(--color-background); +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.overview-wrapper-shadow { + box-shadow: 0 0 30px #0000001a; +} + /* ========================================================================== 5. Responsive Breakpoints — 3 Groups Only ========================================================================== */ @@ -1649,10 +1661,10 @@ Large Desktop (@media (min-width: 1366px)): Lines 1421 - 1481. */ /* 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; + background: linear-gradient(135deg, var(--color-old-brick) 0%, #5a1212 100%) !important; + border-color: var(--color-old-brick) !important; transform: translateY(-8px) scale(1.02) !important; - box-shadow: 0 16px 36px rgba(13, 113, 186, 0.45) !important; + box-shadow: 0 16px 36px rgba(136, 28, 28, 0.45) !important; } .sis-hover-card-v2:hover .sis-hover-card__title, diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/flex.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/flex.js index 0bac8ba1..03e6075b 100644 --- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/flex.js +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/flex.js @@ -21,7 +21,8 @@ class FlexTool { align: data.align || 'stretch', gap: data.gap || '3', // standard Bootstrap gap level globalClass: data.globalClass || '', - globalId: data.globalId || '' + globalId: data.globalId || '', + globalAttributes: data.globalAttributes || data.attributes || '' }; this.activeInstances = {}; @@ -35,6 +36,7 @@ class FlexTool { this.data[`accContent${i}`] = data[`accContent${i}`] || ''; this.data[`class${i}`] = data[`class${i}`] || ''; this.data[`id${i}`] = data[`id${i}`] || ''; + this.data[`attr${i}`] = data[`attr${i}`] || data[`attributes${i}`] || ''; this.data[`width${i}`] = parseInt(data[`width${i}`]) || 0; // 0 means auto } this.wrapper = undefined; @@ -179,12 +181,12 @@ class FlexTool { // 2. Global CSS Class const classDiv = document.createElement('div'); - classDiv.className = 'col-md-5 mb-2'; + classDiv.className = 'col-md-3 mb-2'; classDiv.innerHTML = ''; const classInput = document.createElement('input'); classInput.type = 'text'; classInput.className = 'form-control form-control-sm'; - classInput.placeholder = 'e.g. my-custom-flex'; + classInput.placeholder = 'e.g. my-flex-row'; classInput.value = this.data.globalClass || ''; if (this.readOnly) classInput.disabled = true; classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim()); @@ -194,7 +196,7 @@ class FlexTool { // 3. Global HTML ID const idDiv = document.createElement('div'); - idDiv.className = 'col-md-5 mb-2'; + idDiv.className = 'col-md-3 mb-2'; idDiv.innerHTML = ''; const idInput = document.createElement('input'); idInput.type = 'text'; @@ -207,6 +209,21 @@ class FlexTool { idDiv.appendChild(idInput); bgSettingsRow.appendChild(idDiv); + // 4. Global Custom Attributes + const attrDiv = document.createElement('div'); + attrDiv.className = 'col-md-4 mb-2'; + attrDiv.innerHTML = ''; + 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:#fff"'; + attrInput.value = this.data.globalAttributes || ''; + if (this.readOnly) attrInput.disabled = true; + attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim()); + this.globalAttrInput = attrInput; + attrDiv.appendChild(attrInput); + bgSettingsRow.appendChild(attrDiv); + this.wrapper.appendChild(bgSettingsRow); // Columns inputs container @@ -715,7 +732,8 @@ class FlexTool { align: this.data.align, gap: this.data.gap, globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''), - globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || '') + globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''), + globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || '') }; for (let i = 1; i <= this.data.cols; i++) { const type = this.data[`type${i}`] || 'html'; @@ -740,6 +758,7 @@ class FlexTool { savedData[`accContent${i}`] = this.data[`accContent${i}`] || ''; savedData[`class${i}`] = this.data[`class${i}`] || ''; savedData[`id${i}`] = this.data[`id${i}`] || ''; + savedData[`attr${i}`] = this.data[`attr${i}`] || ''; savedData[`width${i}`] = this.data[`width${i}`] || 0; } return savedData; diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/grid.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/grid.js index c8ef8d27..8374ec12 100644 --- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/grid.js +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/grid.js @@ -17,7 +17,8 @@ class GridTool { this.data = { cols: parseInt(data.cols) || 2, globalClass: data.globalClass || '', - globalId: data.globalId || '' + globalId: data.globalId || '', + globalAttributes: data.globalAttributes || data.attributes || '' }; this.activeInstances = {}; @@ -31,6 +32,7 @@ class GridTool { this.data[`accContent${i}`] = data[`accContent${i}`] || ''; this.data[`class${i}`] = data[`class${i}`] || ''; this.data[`id${i}`] = data[`id${i}`] || ''; + this.data[`attr${i}`] = data[`attr${i}`] || data[`attributes${i}`] || ''; this.data[`width${i}`] = parseInt(data[`width${i}`]) || 0; // 0 means auto } this.wrapper = undefined; @@ -73,7 +75,7 @@ class GridTool { // 2. Global CSS Class const classDiv = document.createElement('div'); - classDiv.className = 'col-md-5 mb-2'; + classDiv.className = 'col-md-3 mb-2'; classDiv.innerHTML = ''; const classInput = document.createElement('input'); classInput.type = 'text'; @@ -88,7 +90,7 @@ class GridTool { // 3. Global HTML ID const idDiv = document.createElement('div'); - idDiv.className = 'col-md-5 mb-2'; + idDiv.className = 'col-md-3 mb-2'; idDiv.innerHTML = ''; const idInput = document.createElement('input'); idInput.type = 'text'; @@ -101,6 +103,21 @@ class GridTool { idDiv.appendChild(idInput); settingsRow.appendChild(idDiv); + // 4. Global Custom Attributes + const attrDiv = document.createElement('div'); + attrDiv.className = 'col-md-4 mb-2'; + attrDiv.innerHTML = ''; + 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:#fff"'; + attrInput.value = this.data.globalAttributes || ''; + if (this.readOnly) attrInput.disabled = true; + attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim()); + this.globalAttrInput = attrInput; + attrDiv.appendChild(attrInput); + settingsRow.appendChild(attrDiv); + this.wrapper.appendChild(settingsRow); // Columns inputs container @@ -607,7 +624,8 @@ class GridTool { const savedData = { cols: this.data.cols, globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''), - globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || '') + globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''), + globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || '') }; for (let i = 1; i <= this.data.cols; i++) { const type = this.data[`type${i}`] || 'html'; @@ -632,6 +650,7 @@ class GridTool { savedData[`accContent${i}`] = this.data[`accContent${i}`] || ''; savedData[`class${i}`] = this.data[`class${i}`] || ''; savedData[`id${i}`] = this.data[`id${i}`] || ''; + savedData[`attr${i}`] = this.data[`attr${i}`] || ''; savedData[`width${i}`] = this.data[`width${i}`] || 0; } return savedData; diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/hover-card.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/hover-card.js index 9d346592..8ac40761 100644 --- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/hover-card.js +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/hover-card.js @@ -19,6 +19,7 @@ class SISHoverCardTool { styleVariant: (data && data.styleVariant) || 'v1', globalClass: (data && data.globalClass) || 'sis-hover-card-section', globalId: (data && data.globalId) || '', + globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '', items: (data && data.items && Array.isArray(data.items)) ? data.items : [] }; @@ -98,7 +99,7 @@ class SISHoverCardTool { // 2. Global CSS Class const classDiv = document.createElement('div'); - classDiv.className = 'col-md-5 mb-2'; + classDiv.className = 'col-md-3 mb-2'; classDiv.innerHTML = ''; const classInput = document.createElement('input'); classInput.type = 'text'; @@ -113,7 +114,7 @@ class SISHoverCardTool { // 3. Global HTML ID const idDiv = document.createElement('div'); - idDiv.className = 'col-md-4 mb-2'; + idDiv.className = 'col-md-3 mb-2'; idDiv.innerHTML = ''; const idInput = document.createElement('input'); idInput.type = 'text'; @@ -126,6 +127,21 @@ class SISHoverCardTool { idDiv.appendChild(idInput); settingsRow.appendChild(idDiv); + // 4. Global Custom Attributes + const attrDiv = document.createElement('div'); + attrDiv.className = 'col-md-3 mb-2'; + attrDiv.innerHTML = ''; + const attrInput = document.createElement('input'); + attrInput.type = 'text'; + attrInput.className = 'form-control form-control-sm'; + attrInput.placeholder = 'e.g. data-aos="fade-up" style="..."'; + attrInput.value = this.data.globalAttributes || ''; + if (this.readOnly) attrInput.disabled = true; + attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim()); + this.globalAttrInput = attrInput; + attrDiv.appendChild(attrInput); + settingsRow.appendChild(attrDiv); + this.wrapper.appendChild(settingsRow); // Cards items container @@ -380,7 +396,21 @@ class SISHoverCardTool { hoverImgGroup.appendChild(hoverInputGroup); hoverImgGroup.appendChild(hoverPreviewContainer); - itemBox.appendChild(hoverImgGroup); + // Custom Attributes field for individual card + const cardAttrDiv = document.createElement('div'); + cardAttrDiv.className = 'form-group mb-2'; + cardAttrDiv.innerHTML = ''; + const cardAttrInput = document.createElement('input'); + cardAttrInput.type = 'text'; + cardAttrInput.className = 'form-control form-control-sm'; + cardAttrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"'; + cardAttrInput.value = item.attributes || ''; + if (this.readOnly) cardAttrInput.disabled = true; + cardAttrInput.addEventListener('input', (e) => { + item.attributes = e.target.value.trim(); + }); + cardAttrDiv.appendChild(cardAttrInput); + itemBox.appendChild(cardAttrDiv); // Default Active Radio (Only 1 option active at a time) const activeRadioDiv = document.createElement('div'); @@ -418,6 +448,7 @@ class SISHoverCardTool { 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 || ''), + globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || ''), items: this.data.items || [] }; } diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/html-snippet.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/html-snippet.js index edc9923f..7fb75843 100644 --- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/html-snippet.js +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/html-snippet.js @@ -12,7 +12,8 @@ class HtmlSnippetTool { constructor({ data, api }) { this.data = { - id: data.id || '' + id: data.id || '', + globalAttributes: data.globalAttributes || data.attributes || '' }; this.api = api; this.wrapper = undefined; @@ -39,7 +40,7 @@ class HtmlSnippetTool { title.style.marginTop = '0'; title.style.marginBottom = '10px'; title.style.padding = '15px 15px 0 15px'; - title.innerText = 'HTML Snippet ID:'; + title.innerText = 'HTML Snippet ID & Custom Attributes:'; title.style.fontSize = '14px'; title.style.color = '#333'; @@ -52,12 +53,24 @@ class HtmlSnippetTool { input.classList.add('ce-input'); input.placeholder = 'Enter Snippet ID (e.g., test_banner)'; input.value = this.data.id; + input.style.flex = '1'; input.addEventListener('input', (e) => { this.data.id = e.target.value.trim(); }); + const attrInput = document.createElement('input'); + attrInput.classList.add('ce-input'); + attrInput.placeholder = 'Custom Attributes (e.g. data-aos="fade-up" style="...")'; + attrInput.value = this.data.globalAttributes || ''; + attrInput.style.flex = '1'; + + attrInput.addEventListener('input', (e) => { + this.data.globalAttributes = e.target.value.trim(); + }); + inputContainer.appendChild(input); + inputContainer.appendChild(attrInput); this.wrapper.appendChild(title); this.wrapper.appendChild(inputContainer); @@ -65,7 +78,8 @@ class HtmlSnippetTool { save(blockContent) { return { - id: this.data.id + id: this.data.id, + globalAttributes: this.data.globalAttributes || '' }; } } diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/timeline.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/timeline.js index d31cf72f..9b5178a4 100644 --- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/timeline.js +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/timeline.js @@ -10,6 +10,7 @@ class SISTimelineTool { this.api = api; this.readOnly = readOnly; this.data = { + globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '', items: (data && data.items && Array.isArray(data.items)) ? data.items : [] }; // Add an initial empty item if list is empty @@ -29,10 +30,13 @@ class SISTimelineTool { this.wrapper.style.marginBottom = '12px'; const headerLabel = document.createElement('label'); - headerLabel.className = 'font-weight-bold text-primary small mb-3 d-block'; + headerLabel.className = 'font-weight-bold text-primary small mb-2 d-block'; headerLabel.innerHTML = ' Timeline Group Settings'; this.wrapper.appendChild(headerLabel); + const globalAttrGroup = this._createInput('e.g. data-aos="fade-up" style="..."', this.data.globalAttributes, 'global-attr', ' Global Custom Attributes'); + this.wrapper.appendChild(globalAttrGroup); + this.itemsContainer = document.createElement('div'); this.wrapper.appendChild(this.itemsContainer); @@ -196,9 +200,12 @@ class SISTimelineTool { imageGroup.appendChild(inputRow); imageGroup.appendChild(imagePreview); + const attrGroup = this._createInput('e.g. data-aos="fade-right" style="..."', item.attributes, 'attr', ' Step Custom Attributes'); + itemBox.appendChild(dateGroup); itemBox.appendChild(descGroup); itemBox.appendChild(imageGroup); + itemBox.appendChild(attrGroup); this.itemsContainer.appendChild(itemBox); } @@ -254,11 +261,15 @@ class SISTimelineTool { const date = box.querySelector('.ce-timeline-input-date').value.trim(); const desc = box.querySelector('.ce-timeline-input-desc').value.trim(); const img = box.querySelector('.ce-timeline-input-image').value.trim(); - if (date || desc || img) { - items.push({ date: date, description: desc, imageUrl: img }); + const attrEl = box.querySelector('.ce-timeline-input-attr'); + const attr = attrEl ? attrEl.value.trim() : ''; + if (date || desc || img || attr) { + items.push({ date: date, description: desc, imageUrl: img, attributes: attr }); } }); + const globalAttrEl = this.wrapper.querySelector('.ce-timeline-input-global-attr'); return { + globalAttributes: globalAttrEl ? globalAttrEl.value.trim() : (this.data.globalAttributes || ''), items: items }; } diff --git a/sisvietnamvn_main/src/main/resources/templates/page.html b/sisvietnamvn_main/src/main/resources/templates/page.html index 863d035b..524e039c 100644 --- a/sisvietnamvn_main/src/main/resources/templates/page.html +++ b/sisvietnamvn_main/src/main/resources/templates/page.html @@ -349,7 +349,8 @@
+ th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}" + th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}">
@@ -357,6 +358,7 @@ th:id="${block.data['id' + i] != null and !#strings.isEmpty(block.data['id' + i]) ? block.data['id' + i] : null}" th:class="${(block.data['width' + i] != null and block.data['width' + i] gt 0) ? ('col-lg-' + block.data['width' + i] + ' col-md-6 col-12 mb-4') : (block.data['cols'] == 1 ? 'col-12 mb-4' : (block.data['cols'] == 2 ? 'col-md-6 col-12 mb-4' : (block.data['cols'] == 3 ? 'col-lg-4 col-md-6 col-12 mb-4' : (block.data['cols'] == 4 ? 'col-lg-3 col-md-6 col-12 mb-4' : 'col-lg col-md-6 col-12 mb-4'))))}" th:classappend="${block.data['class' + i] != null and !#strings.isEmpty(block.data['class' + i]) ? block.data['class' + i] : ''}" + th:data-custom-attrs="${block.data['attr' + i] != null and !#strings.isEmpty(block.data['attr' + i]) ? block.data['attr' + i] : null}" >
@@ -369,7 +371,8 @@
+ th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}" + th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}">
@@ -392,12 +396,14 @@
+ 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'] : ''))}" + th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}">
-
+
@@ -692,17 +698,42 @@ } }); - // 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'); + // Interactive Hover Cards — Mouseover event delegation to apply active class dynamically + document.addEventListener('mouseover', function (e) { + var card = e.target.closest('.sis-hover-card, .sis-hover-card-v2'); + if (card && !card.classList.contains('active')) { + var parentRow = card.closest('.row') || card.parentElement; if (parentRow) { - parentRow.querySelectorAll('.sis-hover-card').forEach(function (c) { + parentRow.querySelectorAll('.sis-hover-card, .sis-hover-card-v2').forEach(function (c) { c.classList.remove('active'); }); } card.classList.add('active'); - }); + } + }); + + // Apply Custom Attributes (e.g. data-aos="fade-up" style="background:#fff") dynamically + document.querySelectorAll('[data-custom-attrs]').forEach(function (el) { + var attrStr = el.getAttribute('data-custom-attrs'); + if (attrStr && attrStr.trim() !== '') { + var tempDiv = document.createElement('div'); + tempDiv.innerHTML = '
'; + var tempEl = tempDiv.firstElementChild; + if (tempEl) { + Array.from(tempEl.attributes).forEach(function (attr) { + if (attr.name === 'class') { + attr.value.split(/\s+/).filter(Boolean).forEach(function (c) { + el.classList.add(c); + }); + } else if (attr.name === 'style') { + el.style.cssText += ';' + attr.value; + } else { + el.setAttribute(attr.name, attr.value); + } + }); + } + el.removeAttribute('data-custom-attrs'); + } }); });