Add tabs feature with two options

This commit is contained in:
2025-08-13 15:45:07 +02:00
commit 696f1a9a82
8 changed files with 513 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

0
README.md Normal file
View File

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?><iai:component><iai:componentsdata><cdata-start/>
<div class="tabs">
<div class="tabs__container">
<!--Opis-->
<iaixsl:if test="page/projector/product/vlongdescription and not(page/projector/product/vlongdescription = '')">
<div class="tabs__item idm-active" data-tab="projector_longdescription">
<iai:variable vid="Opis produktu" />
</div>
</iaixsl:if>
<!--Parametry-->
<iaixsl:if
test="/shop/page/projector/product/price/@srp or (count(/shop/page/projector/product/dictionary/items) &gt; 0) or ($product_producer_label and not(/shop/page/projector/product/firm/@name = '')) or ($product_code_label and /shop/page/projector/product/@code) or ($product_series_label and /shop/page/projector/product/series) or ($product_producer_code_label and count(/shop/page/projector/product/sizes/size[@code_producer and not(@code_producer = '')]) &gt; 0) or (count(/shop/page/projector/product/responsible_entity/producer | /shop/page/projector/product/responsible_entity/persons/person) &gt; 0)"
>
<div class="tabs__item" data-tab="projector_dictionary">
<iai:variable vid="Parametry techniczne" />
</div>
</iaixsl:if>
<!--Do pobrania-->
<iaixsl:if
test="(/shop/page/projector/product/enclosures/documents/item) or (/shop/page/projector/product/enclosures/audio/item) or (/shop/page/projector/product/enclosures/other) or (/shop/page/projector/product/enclosures/images_attachments/item) or (/shop/page/projector/product/enclosures/video/item)"
>
<div class="tabs__item" data-tab="projector_enclosures">
<iai:variable vid="Do pobrania" />
</div>
</iaixsl:if>
<!--Gwarancja-->
<iaixsl:if test="page/projector/product/warranty and not(page/projector/product/warranty= '')">
<div class="tabs__item" data-tab="projector_warranty">
<iai:variable vid="Gwarancja" />
</div>
</iaixsl:if>
<!--Pytania-->
<iaixsl:if test="page/projector/product/warranty and not(page/projector/product/warranty= '')">
<div class="tabs__item" data-tab="product_questions_list">
<iai:variable vid="Pytania" />
</div>
</iaixsl:if>
<!--Opinie-->
<div class="tabs__item" data-tab="opinions_section">
<iai:variable vid="Opinie" />
</div>
<!--Blog-->
<iaixsl:if test="count(/shop/page/projector/blog_entries/item) &gt; 0">
<div class="tabs__item" data-tab="projector_blog">
<iai:variable vid="Blog" />
</div>
</iaixsl:if>
<!--Rekomendacje 1-->
<iaixsl:if test="page/projector/products_associated_zone1">
<div class="tabs__item" data-tab="products_associated_zone1">
<iai:variable vid="Rekomendacje 1" />
</div>
</iaixsl:if>
</div>
</div>
<cdata-end/></iai:componentsdata></iai:component>

View File

@@ -0,0 +1,26 @@
const tabButtons = document.querySelectorAll('.tabs__item');
// Leave only necessary sections id
const sectionIds = [
'projector_longdescription',
'projector_dictionary',
'projector_enclosures',
'projector_warranty',
'product_questions_list',
'opinions_section',
'projector_blog',
'products_associated_zone1'
];
const tabContents = sectionIds.map(id => document.getElementById(id));
tabButtons.forEach(btn => {
btn.addEventListener('click', () => {
tabButtons.forEach(b => b?.classList.remove('idm-active'));
btn?.classList.add('idm-active');
tabContents.forEach(content => content?.classList.remove('idm-active'));
const tabId = btn?.dataset.tab;
document.getElementById(tabId)?.classList.add('idm-active');
});
});

View File

@@ -0,0 +1,112 @@
@keyframes slide-in {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
@keyframes slide-in-opacity {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tabs {
border-bottom: 3px solid #efefef;
@media(max-width: 978px) {
border: none;
}
&__container {
display: flex;
align-items: center;
gap: 1rem;
&::-webkit-scrollbar {
height: 3px;
}
&::-webkit-scrollbar-track {
background: #efefef;
border-radius: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: black;
min-width: 30%;
border-radius: 8px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #2e8b57;
}
@media(max-width: 978px) {
overflow-y: hidden;
overflow-x: scroll;
}
}
&__item {
white-space: nowrap;
position: relative;
padding: 8px 12px;
cursor: pointer;
transition: color .3s ease;
color: #888;
font-weight: 600;
&:hover {
color: #000;
}
&.idm-active {
color: #000;
&::after {
display: block;
}
}
&::after {
content: "";
display: none;
position: absolute;
bottom: -3px;
left: 0;
width: 100%;
height: 2.5px;
background-color: black;
border-radius: 2px;
animation: slide-in .3s ease;
@media(max-width: 978px) {
display: none;
}
}
}
}
// Leave only necessary sections id
#projector_longdescription,
#projector_dictionary,
#projector_enclosures,
#projector_warranty,
#product_questions_list,
#opinions_section,
#projector_blog,
#products_associated_zone1 {
display: none;
animation: slide-in-opacity .3s ease;
padding-top: 5rem;
&.idm-active {
display: block;
}
}

66
mobile-tabs /index.xslt Normal file
View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?><iai:component><iai:componentsdata><cdata-start/>
<div class="tabs">
<div class="tabs__container">
<!--Opis-->
<iaixsl:if test="page/projector/product/vlongdescription and not(page/projector/product/vlongdescription = '')">
<div class="tabs__item idm-active" data-tab="projector_longdescription">
<iai:variable vid="Opis produktu" />
</div>
</iaixsl:if>
<!--Parametry-->
<iaixsl:if
test="/shop/page/projector/product/price/@srp or (count(/shop/page/projector/product/dictionary/items) &gt; 0) or ($product_producer_label and not(/shop/page/projector/product/firm/@name = '')) or ($product_code_label and /shop/page/projector/product/@code) or ($product_series_label and /shop/page/projector/product/series) or ($product_producer_code_label and count(/shop/page/projector/product/sizes/size[@code_producer and not(@code_producer = '')]) &gt; 0) or (count(/shop/page/projector/product/responsible_entity/producer | /shop/page/projector/product/responsible_entity/persons/person) &gt; 0)"
>
<div class="tabs__item" data-tab="projector_dictionary">
<iai:variable vid="Parametry techniczne" />
</div>
</iaixsl:if>
<!--Do pobrania-->
<iaixsl:if
test="(/shop/page/projector/product/enclosures/documents/item) or (/shop/page/projector/product/enclosures/audio/item) or (/shop/page/projector/product/enclosures/other) or (/shop/page/projector/product/enclosures/images_attachments/item) or (/shop/page/projector/product/enclosures/video/item)"
>
<div class="tabs__item" data-tab="projector_enclosures">
<iai:variable vid="Do pobrania" />
</div>
</iaixsl:if>
<!--Gwarancja-->
<iaixsl:if test="page/projector/product/warranty and not(page/projector/product/warranty= '')">
<div class="tabs__item" data-tab="projector_warranty">
<iai:variable vid="Gwarancja" />
</div>
</iaixsl:if>
<!--Pytania-->
<iaixsl:if test="page/projector/product/warranty and not(page/projector/product/warranty= '')">
<div class="tabs__item" data-tab="product_questions_list">
<iai:variable vid="Pytania" />
</div>
</iaixsl:if>
<!--Opinie-->
<div class="tabs__item" data-tab="opinions_section">
<iai:variable vid="Opinie" />
</div>
<!--Blog-->
<iaixsl:if test="count(/shop/page/projector/blog_entries/item) &gt; 0">
<div class="tabs__item" data-tab="projector_blog">
<iai:variable vid="Blog" />
</div>
</iaixsl:if>
<!--Rekomendacje 1-->
<iaixsl:if test="page/projector/products_associated_zone1">
<div class="tabs__item" data-tab="products_associated_zone1">
<iai:variable vid="Rekomendacje 1" />
</div>
</iaixsl:if>
</div>
</div>
<cdata-end/></iai:componentsdata></iai:component>

106
mobile-tabs /src/main.js Normal file
View File

@@ -0,0 +1,106 @@
// Leave only necessary sections id
const sectionIds = [
'projector_longdescription',
'projector_dictionary',
'projector_enclosures',
'projector_warranty',
'product_questions_list',
'opinions_section',
'projector_blog',
'products_associated_zone1'
];
const tabContents = sectionIds.map(id => document.getElementById(id));
const tabButtons = document.querySelectorAll('.tabs__item');
let isMobileView = false;
// Engine
(() => {
let resizeTimeout
let prevView = app_shop.vars.view
const container = document.querySelector('#container')
const VIEW_PHONE = [1, 2]
const VIEW_TABLET_DESKTOP = [3, 4]
const handleResize = () => {
if (!container?.classList.contains('projector_page')) return
clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(() => {
const currentView = app_shop.vars.view
if (VIEW_PHONE.includes(currentView)) {
isMobileView = true
placeContentsInBetween()
} else if (VIEW_TABLET_DESKTOP.includes(currentView)) {
isMobileView = false
}
if (VIEW_TABLET_DESKTOP.includes(prevView) && VIEW_PHONE.includes(currentView)) {
placeContentsInBetween()
} else if (VIEW_PHONE.includes(prevView) && VIEW_TABLET_DESKTOP.includes(currentView)) {
placeContentsDefault()
}
prevView = currentView
initTabs()
}, 200)
}
window.addEventListener('DOMContentLoaded', () => {
handleResize()
})
window.addEventListener('resize', handleResize)
})()
// Utils
function handleTabClick(e) {
const btn = e.currentTarget
if (isMobileView) {
tabButtons.forEach(b => {
if (b !== btn) b?.classList.remove('idm-active')
})
btn?.classList.toggle('idm-active')
} else {
tabButtons.forEach(b => b?.classList.remove('idm-active'))
btn?.classList.add('idm-active')
}
const tabId = btn?.dataset.tab
if (isMobileView) {
tabContents.forEach(content => {
if(content.id !== tabId) content?.classList.remove('idm-active')
})
document.getElementById(tabId)?.classList.toggle('idm-active')
} else {
tabContents.forEach(content => content?.classList.remove('idm-active'))
document.getElementById(tabId)?.classList.add('idm-active')
}
}
function initTabs() {
tabButtons.forEach(btn => {
btn.removeEventListener('click', handleTabClick)
btn.addEventListener('click', handleTabClick)
})
}
function placeContentsInBetween() {
tabButtons.forEach(btn => {
const tabId = btn?.dataset.tab
const relevantTab = document.getElementById(tabId)
btn.insertAdjacentElement('afterend', relevantTab)
})
}
function placeContentsDefault() {
const tabs = document.querySelector('.tabs')
tabButtons.forEach(btn => {
const tabId = btn?.dataset.tab
const relevantTab = document.getElementById(tabId)
tabs.insertAdjacentElement('afterend', relevantTab)
})
}

113
mobile-tabs /src/style.less Normal file
View File

@@ -0,0 +1,113 @@
@keyframes slide-in-opacity {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tabs {
border-bottom: 3px solid #efefef;
@media (max-width: 978px) {
border: none;
}
&__container {
display: flex;
align-items: center;
gap: 1rem;
@media (max-width: 978px) {
gap: 2rem;
flex-direction: column;
align-items: flex-start;
}
}
&__item {
white-space: nowrap;
position: relative;
padding: 8px 12px;
cursor: pointer;
transition: color .3s ease;
color: #888;
font-weight: 600;
&:hover {
color: #000;
}
&.idm-active {
color: #000;
&::after {
opacity: 1;
}
@media (max-width: 978px) {
&::before {
opacity: 0;
transform: translateY(50%);
}
}
}
@media (max-width: 978px) {
width: 100%;
}
&::after, &::before {
content: "";
display: block;
opacity: 0;
position: absolute;
bottom: -3px;
left: 0;
width: 100%;
height: 2.5px;
background-color: black;
border-radius: 2px;
transition: all 0.3s ease;
}
// Mobile plus&minus
@media (max-width: 978px) {
padding: 0;
&::after, &::before {
opacity: 1;
width: 2rem;
left: calc(100% - 2rem);
bottom: 50%;
transform: translateY(50%);
}
&::before {
opacity: 1;
transform: rotate(90deg) translateY(0) translateX(5%);
}
}
}
}
// Leave only necessary sections id
#projector_longdescription,
#projector_dictionary,
#projector_enclosures,
#projector_warranty,
#product_questions_list,
#opinions_section,
#projector_blog,
#products_associated_zone1 {
display: none;
animation: slide-in-opacity .3s ease;
padding-top: 5rem;
&.idm-active {
display: block;
}
}