feat: add support for global and element-specific custom HTML attributes across editor plugins and page templates

This commit is contained in:
2026-08-01 19:10:46 +07:00
parent 1ee67e6a46
commit cc228e5c52
7 changed files with 168 additions and 31 deletions
@@ -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,
@@ -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 = '<label class="small font-weight-bold text-secondary mb-1">Flex CSS Class(es)</label>';
const classInput = document.createElement('input');
classInput.type = 'text';
classInput.className = 'form-control form-control-sm';
classInput.placeholder = 'e.g. my-custom-flex';
classInput.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 = '<label class="small font-weight-bold text-secondary mb-1">Flex HTML ID</label>';
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 = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</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:#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;
@@ -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 = '<label class="small font-weight-bold text-secondary mb-1">Grid CSS Class(es)</label>';
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 = '<label class="small font-weight-bold text-secondary mb-1">Grid HTML ID</label>';
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 = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</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:#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;
@@ -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 = '<label class="small font-weight-bold text-secondary mb-1">Global CSS Class(es)</label>';
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 = '<label class="small font-weight-bold text-secondary mb-1">Global HTML ID</label>';
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 = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</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="..."';
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 = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Card Custom Attributes</label>';
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 || []
};
}
@@ -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 || ''
};
}
}
@@ -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 = '<i class="fas fa-stream"></i> Timeline Group Settings';
this.wrapper.appendChild(headerLabel);
const globalAttrGroup = this._createInput('e.g. data-aos="fade-up" style="..."', this.data.globalAttributes, 'global-attr', '<i class="fas fa-sliders-h"></i> 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', '<i class="fas fa-sliders-h"></i> 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
};
}
@@ -349,7 +349,8 @@
<!-- 15. Grid / Columns Block -->
<th:block th:if="${block['type'] == 'grid'}">
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}">
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)}">
<div class="container py-4">
<div class="row">
<th:block th:each="i : ${#numbers.sequence(1, block.data['cols'])}">
@@ -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}"
>
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
</div>
@@ -369,7 +371,8 @@
<!-- 16. Flex Layout Block -->
<th:block th:if="${block['type'] == 'flex'}">
<div th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
th:class="${block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? block.data['globalClass'] : null}">
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)}">
<div class="container py-4">
<div
style="display: flex; flex-wrap: wrap;"
@@ -380,6 +383,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}"
>
<div th:replace=":: renderCell(type=${block.data['type' + i]}, i=${i}, blockData=${block.data})"></div>
</div>
@@ -392,12 +396,14 @@
<!-- 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'] : ''))}">
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)}">
<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 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' : ''))}"
th:data-custom-attrs="${item['attributes'] != null and !#strings.isEmpty(item['attributes']) ? item['attributes'] : null}">
<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']}" />
@@ -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 = '<div ' + attrStr + '></div>';
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');
}
});
});
</script>