Implement touch handling for modals in Certifications, Education, and Skills components

- Introduced touch event listeners to enhance mobile interactivity for certification, education, and skills cards.
- Refactored modal display logic to improve user experience by allowing smooth scrolling and restoring scroll position after modal closure.
- Updated event handling to prevent default actions and ensure modals function correctly across devices.
This commit is contained in:
2025-06-15 19:23:38 +02:00
parent 8f2c5c5df4
commit d9a1b04f89
3 changed files with 397 additions and 160 deletions

View File

@@ -226,7 +226,55 @@ const getImageSrc = (imagePath: string) => {
</style> </style>
<script is:inline> <script is:inline>
function showCertModal(element) { // Certifications widget touch handling - namespaced to avoid conflicts
(function() {
let certTouchStart = { x: 0, y: 0, time: 0 };
let certIsScrolling = false;
let certOriginalScrollPosition = 0;
let certClickedElement = null;
function certHandleTouchStart(e) {
const touch = e.touches[0];
certTouchStart = {
x: touch.clientX,
y: touch.clientY,
time: Date.now()
};
certIsScrolling = false;
}
function certHandleTouchMove(e) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - certTouchStart.x);
const deltaY = Math.abs(touch.clientY - certTouchStart.y);
if (deltaX > 10 || deltaY > 10) {
certIsScrolling = true;
}
}
function certHandleTouchEnd(e, element) {
const touch = e.changedTouches[0];
const touchEndTime = Date.now();
if (certIsScrolling) return;
const touchDuration = touchEndTime - certTouchStart.time;
if (touchDuration > 300) return;
const deltaX = Math.abs(touch.clientX - certTouchStart.x);
const deltaY = Math.abs(touch.clientY - certTouchStart.y);
if (deltaX > 15 || deltaY > 15) return;
e.preventDefault();
showCertModal(element);
}
window.showCertModal = function(element) {
// Store the clicked element and current scroll position
certClickedElement = element;
certOriginalScrollPosition = window.pageYOffset || document.documentElement.scrollTop;
const title = element.dataset.certTitle; const title = element.dataset.certTitle;
const date = element.dataset.certDate; const date = element.dataset.certDate;
const description = element.dataset.certDescription; const description = element.dataset.certDescription;
@@ -239,33 +287,65 @@ const getImageSrc = (imagePath: string) => {
const modal = document.getElementById('certModal'); const modal = document.getElementById('certModal');
modal.classList.add('active'); modal.classList.add('active');
// Don't disable body scroll - let modal content be scrollable
}
function closeCertModal() { setTimeout(() => {
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
modalContent.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
};
window.closeCertModal = function() {
const modal = document.getElementById('certModal'); const modal = document.getElementById('certModal');
modal.classList.remove('active'); modal.classList.remove('active');
// No need to restore body scroll since we didn't disable it
// Restore scroll position to the original card
if (certClickedElement) {
setTimeout(() => {
// First, scroll to the original position
window.scrollTo({
top: certOriginalScrollPosition,
behavior: 'smooth'
});
// Then, ensure the clicked card is visible with a slight offset
setTimeout(() => {
const elementRect = certClickedElement.getBoundingClientRect();
const isElementVisible = elementRect.top >= 0 && elementRect.bottom <= window.innerHeight;
if (!isElementVisible) {
certClickedElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, 300);
}, 100);
} }
// Initialize when DOM is loaded // Clear the reference
certClickedElement = null;
};
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Add click listeners to cert cards
const certCards = document.querySelectorAll('.cert-card'); const certCards = document.querySelectorAll('.cert-card');
certCards.forEach(card => { certCards.forEach(card => {
// Add both click and touchend for mobile compatibility
card.addEventListener('click', function(e) { card.addEventListener('click', function(e) {
if (!('ontouchstart' in window)) {
e.preventDefault(); e.preventDefault();
showCertModal(this); showCertModal(this);
}
}); });
card.addEventListener('touchstart', certHandleTouchStart, { passive: true });
card.addEventListener('touchmove', certHandleTouchMove, { passive: true });
card.addEventListener('touchend', function(e) { card.addEventListener('touchend', function(e) {
e.preventDefault(); certHandleTouchEnd(e, this);
showCertModal(this); }, { passive: false });
});
}); });
// Close modal when clicking outside
const certModal = document.getElementById('certModal'); const certModal = document.getElementById('certModal');
if (certModal) { if (certModal) {
certModal.addEventListener('click', function(e) { certModal.addEventListener('click', function(e) {
@@ -275,11 +355,11 @@ const getImageSrc = (imagePath: string) => {
}); });
} }
// Close modal with Escape key
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
closeCertModal(); closeCertModal();
} }
}); });
}); });
})();
</script> </script>

View File

@@ -188,16 +188,62 @@ const getEducationStyle = (title: string) => {
</style> </style>
<script is:inline> <script is:inline>
function showEducationModal(element) { // Education widget touch handling - namespaced to avoid conflicts
(function() {
let eduTouchStart = { x: 0, y: 0, time: 0 };
let eduIsScrolling = false;
let eduOriginalScrollPosition = 0;
let eduClickedElement = null;
function eduHandleTouchStart(e) {
const touch = e.touches[0];
eduTouchStart = {
x: touch.clientX,
y: touch.clientY,
time: Date.now()
};
eduIsScrolling = false;
}
function eduHandleTouchMove(e) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - eduTouchStart.x);
const deltaY = Math.abs(touch.clientY - eduTouchStart.y);
if (deltaX > 10 || deltaY > 10) {
eduIsScrolling = true;
}
}
function eduHandleTouchEnd(e, element) {
const touch = e.changedTouches[0];
const touchEndTime = Date.now();
if (eduIsScrolling) return;
const touchDuration = touchEndTime - eduTouchStart.time;
if (touchDuration > 300) return;
const deltaX = Math.abs(touch.clientX - eduTouchStart.x);
const deltaY = Math.abs(touch.clientY - eduTouchStart.y);
if (deltaX > 15 || deltaY > 15) return;
e.preventDefault();
showEducationModal(element);
}
window.showEducationModal = function(element) {
// Store the clicked element and current scroll position
eduClickedElement = element;
eduOriginalScrollPosition = window.pageYOffset || document.documentElement.scrollTop;
const title = element.dataset.educationTitle; const title = element.dataset.educationTitle;
const description = element.dataset.educationDescription; const description = element.dataset.educationDescription;
// Parse the title to extract clean degree name
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = title; tempDiv.innerHTML = title;
const cleanTitle = tempDiv.textContent || tempDiv.innerText || title; const cleanTitle = tempDiv.textContent || tempDiv.innerText || title;
// Extract degree type for styling
const titleLower = cleanTitle.toLowerCase(); const titleLower = cleanTitle.toLowerCase();
let statusText = ''; let statusText = '';
let statusClass = ''; let statusClass = '';
@@ -213,7 +259,6 @@ const getEducationStyle = (title: string) => {
statusClass = 'text-purple-600 dark:text-purple-400'; statusClass = 'text-purple-600 dark:text-purple-400';
} }
// Update modal content
document.getElementById('educationModalTitle').innerHTML = title; document.getElementById('educationModalTitle').innerHTML = title;
document.getElementById('educationModalDescription').textContent = description || 'No additional information available.'; document.getElementById('educationModalDescription').textContent = description || 'No additional information available.';
document.getElementById('educationModalStatus').textContent = statusText; document.getElementById('educationModalStatus').textContent = statusText;
@@ -221,33 +266,65 @@ const getEducationStyle = (title: string) => {
const modal = document.getElementById('educationModal'); const modal = document.getElementById('educationModal');
modal.classList.add('active'); modal.classList.add('active');
// Don't disable body scroll - let modal content be scrollable
}
function closeEducationModal() { setTimeout(() => {
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
modalContent.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
};
window.closeEducationModal = function() {
const modal = document.getElementById('educationModal'); const modal = document.getElementById('educationModal');
modal.classList.remove('active'); modal.classList.remove('active');
// No need to restore body scroll since we didn't disable it
// Restore scroll position to the original card
if (eduClickedElement) {
setTimeout(() => {
// First, scroll to the original position
window.scrollTo({
top: eduOriginalScrollPosition,
behavior: 'smooth'
});
// Then, ensure the clicked card is visible with a slight offset
setTimeout(() => {
const elementRect = eduClickedElement.getBoundingClientRect();
const isElementVisible = elementRect.top >= 0 && elementRect.bottom <= window.innerHeight;
if (!isElementVisible) {
eduClickedElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, 300);
}, 100);
} }
// Initialize when DOM is loaded // Clear the reference
eduClickedElement = null;
};
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Add click listeners to education cards
const educationCards = document.querySelectorAll('.education-card'); const educationCards = document.querySelectorAll('.education-card');
educationCards.forEach(card => { educationCards.forEach(card => {
// Add both click and touchend for mobile compatibility
card.addEventListener('click', function(e) { card.addEventListener('click', function(e) {
if (!('ontouchstart' in window)) {
e.preventDefault(); e.preventDefault();
showEducationModal(this); showEducationModal(this);
}
}); });
card.addEventListener('touchstart', eduHandleTouchStart, { passive: true });
card.addEventListener('touchmove', eduHandleTouchMove, { passive: true });
card.addEventListener('touchend', function(e) { card.addEventListener('touchend', function(e) {
e.preventDefault(); eduHandleTouchEnd(e, this);
showEducationModal(this); }, { passive: false });
});
}); });
// Close modal when clicking outside
const educationModal = document.getElementById('educationModal'); const educationModal = document.getElementById('educationModal');
if (educationModal) { if (educationModal) {
educationModal.addEventListener('click', function(e) { educationModal.addEventListener('click', function(e) {
@@ -257,11 +334,11 @@ const getEducationStyle = (title: string) => {
}); });
} }
// Close modal with Escape key
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
closeEducationModal(); closeEducationModal();
} }
}); });
}); });
})();
</script> </script>

View File

@@ -187,7 +187,55 @@ if (uncategorizedSkills.length > 0) {
</style> </style>
<script is:inline> <script is:inline>
function showSkillModal(element) { // Skills widget touch handling - namespaced to avoid conflicts
(function() {
let skillTouchStart = { x: 0, y: 0, time: 0 };
let skillIsScrolling = false;
let skillOriginalScrollPosition = 0;
let skillClickedElement = null;
function skillHandleTouchStart(e) {
const touch = e.touches[0];
skillTouchStart = {
x: touch.clientX,
y: touch.clientY,
time: Date.now()
};
skillIsScrolling = false;
}
function skillHandleTouchMove(e) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - skillTouchStart.x);
const deltaY = Math.abs(touch.clientY - skillTouchStart.y);
if (deltaX > 10 || deltaY > 10) {
skillIsScrolling = true;
}
}
function skillHandleTouchEnd(e, element) {
const touch = e.changedTouches[0];
const touchEndTime = Date.now();
if (skillIsScrolling) return;
const touchDuration = touchEndTime - skillTouchStart.time;
if (touchDuration > 300) return;
const deltaX = Math.abs(touch.clientX - skillTouchStart.x);
const deltaY = Math.abs(touch.clientY - skillTouchStart.y);
if (deltaX > 15 || deltaY > 15) return;
e.preventDefault();
showSkillModal(element);
}
window.showSkillModal = function(element) {
// Store the clicked element and current scroll position
skillClickedElement = element;
skillOriginalScrollPosition = window.pageYOffset || document.documentElement.scrollTop;
const title = element.dataset.skillTitle; const title = element.dataset.skillTitle;
const description = element.dataset.skillDescription; const description = element.dataset.skillDescription;
@@ -196,33 +244,65 @@ if (uncategorizedSkills.length > 0) {
const modal = document.getElementById('skillModal'); const modal = document.getElementById('skillModal');
modal.classList.add('active'); modal.classList.add('active');
// Don't disable body scroll - let modal content be scrollable
}
function closeSkillModal() { setTimeout(() => {
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
modalContent.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
};
window.closeSkillModal = function() {
const modal = document.getElementById('skillModal'); const modal = document.getElementById('skillModal');
modal.classList.remove('active'); modal.classList.remove('active');
// No need to restore body scroll since we didn't disable it
// Restore scroll position to the original skill pill
if (skillClickedElement) {
setTimeout(() => {
// First, scroll to the original position
window.scrollTo({
top: skillOriginalScrollPosition,
behavior: 'smooth'
});
// Then, ensure the clicked skill pill is visible with a slight offset
setTimeout(() => {
const elementRect = skillClickedElement.getBoundingClientRect();
const isElementVisible = elementRect.top >= 0 && elementRect.bottom <= window.innerHeight;
if (!isElementVisible) {
skillClickedElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, 300);
}, 100);
} }
// Initialize when DOM is loaded // Clear the reference
skillClickedElement = null;
};
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Add click listeners to skill pills
const skillPills = document.querySelectorAll('.skill-pill'); const skillPills = document.querySelectorAll('.skill-pill');
skillPills.forEach(pill => { skillPills.forEach(pill => {
// Add both click and touchend for mobile compatibility
pill.addEventListener('click', function(e) { pill.addEventListener('click', function(e) {
if (!('ontouchstart' in window)) {
e.preventDefault(); e.preventDefault();
showSkillModal(this); showSkillModal(this);
}
}); });
pill.addEventListener('touchstart', skillHandleTouchStart, { passive: true });
pill.addEventListener('touchmove', skillHandleTouchMove, { passive: true });
pill.addEventListener('touchend', function(e) { pill.addEventListener('touchend', function(e) {
e.preventDefault(); skillHandleTouchEnd(e, this);
showSkillModal(this); }, { passive: false });
});
}); });
// Close modal when clicking outside
const skillModal = document.getElementById('skillModal'); const skillModal = document.getElementById('skillModal');
if (skillModal) { if (skillModal) {
skillModal.addEventListener('click', function(e) { skillModal.addEventListener('click', function(e) {
@@ -232,11 +312,11 @@ if (uncategorizedSkills.length > 0) {
}); });
} }
// Close modal with Escape key
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
closeSkillModal(); closeSkillModal();
} }
}); });
}); });
})();
</script> </script>