Rewriting scrolling logic [SCI-9584]

This commit is contained in:
Gregor Lasnibat 2023-10-30 13:05:35 +01:00
parent 256bc0f9e1
commit 46199340aa
2 changed files with 126 additions and 173 deletions

View file

@ -1,22 +1,17 @@
<template>
<div ref="wrapper"
id="repository-item-sidebar-wrapper"
class='items-sidebar-wrapper h-full bg-white gap-2.5 self-stretch rounded-tl-4 rounded-bl-4 shadow-lg
transition-all duration-500 ease-sharp'
:class="{ 'translate-x-0 w-[565px]': isShowing, 'translate-x-full w-0': !isShowing }">
<div ref="wrapper" id="repository-item-sidebar-wrapper"
class='items-sidebar-wrapper bg-white gap-2.5 self-stretch rounded-tl-4 rounded-bl-4 transition-transform ease-in-out transform shadow-lg'
:class="{ 'translate-x-0 w-[565px] h-full': isShowing, 'transition-transform ease-in-out duration-400 transform translate-x-full w-0': !isShowing }">
<div id="repository-item-sidebar" class="w-full h-full pl-6 bg-white flex flex-col">
<div id="sticky-header-wrapper" class="sticky top-0 right-0 bg-white flex z-50 flex-col h-[78px] pt-6">
<div ref="stickyHeaderRef" id="sticky-header-wrapper" class="sticky top-0 right-0 bg-white flex z-50 flex-col h-[78px] pt-6">
<div class="header flex w-full h-[30px] pr-6">
<repository-item-sidebar-title v-if="defaultColumns"
:editable="permissions?.can_manage && !defaultColumns?.archived"
:name="defaultColumns.name"
@update="update"
></repository-item-sidebar-title>
<i id="close-icon"
@click="toggleShowHideSidebar(currentItemUrl)"
class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0"></i>
:editable="permissions?.can_manage && !defaultColumns?.archived" :name="defaultColumns.name"
@update="update"></repository-item-sidebar-title>
<i id="close-icon" @click="toggleShowHideSidebar(currentItemUrl)"
class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0"></i>
</div>
<div id="divider" class="w-500 bg-sn-light-grey flex items-center self-stretch h-px mt-6 mr-6"></div>
</div>
@ -119,14 +114,9 @@
class="font-inter text-lg font-semibold leading-7 pb-4 transition-colors duration-300">
{{ i18n.t('repositories.item_card.custom_columns_label') }}
</div>
<CustomColumns :customColumns="customColumns"
:repositoryRowId="repositoryRowId"
:repositoryId="repository?.id"
:inArchivedRepositoryRow="defaultColumns?.archived"
:permissions="permissions"
:updatePath="updatePath"
:actions="actions"
@update="update" />
<CustomColumns :customColumns="customColumns" :repositoryRowId="repositoryRowId"
:repositoryId="repository?.id" :inArchivedRepositoryRow="defaultColumns?.archived"
:permissions="permissions" :updatePath="updatePath" :actions="actions" @update="update" />
</section>
<div id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px"></div>
@ -192,7 +182,7 @@
</div>
<!-- NAVIGATION -->
<div ref="navigationRef" id="navigation"
<div v-if="isShowing" ref="navigationRef" id="navigation"
class="flex item-end gap-x-4 min-w-[130px] min-h-[130px] h-fit sticky top-0 right-[24px] ">
<scroll-spy :itemsToCreate="[
{ id: 'highlight-item-1', textId: 'text-item-1', labelAlias: 'information_label', label: 'information-label', sectionId: 'information-section' },
@ -278,8 +268,8 @@ export default {
// Check if the clicked element is not within the sidebar and it's not another item link or belogs to modals
const selectors = ['a', '.modal', '.label-printing-progress-modal'];
if (!$(event.target).parents('#repository-item-sidebar-wrapper').length &&
!selectors.some(selector => event.target.closest(selector))) {
if (!$(event.target).parents('#repository-item-sidebar-wrapper').length &&
!selectors.some(selector => event.target.closest(selector))) {
this.toggleShowHideSidebar(null);
}
},

View file

@ -1,21 +1,17 @@
<template>
<div class="flex gap-3">
<div id="navigation-text">
<div class="flex flex-col py-2 px-0 gap-3 self-stretch w-[130px] h-[130px] justify-center items-center">
<div v-for="(itemObj, index) in itemsToCreate" :key="index"
class="flex flex-col w-[130px] h-[130px] justify-between text-right">
<div @click="handleSideNavClick" :id="itemObj?.textId" class="hover:cursor-pointer text-sn-grey"
:class="{ 'text-sn-science-blue': selectedNavText === itemObj?.textId }">{{
i18n.t(`repositories.highlight_component.${itemObj?.labelAlias}`) }}
</div>
</div>
<div id="navigation-text"
class="flex flex-col py-2 px-0 gap-3 self-stretch w-[130px] h-[130px] justify-center items-center">
<div v-for="navigationItem in itemsToCreate" :key="navigationItem.textId" @click="navigateToSection(navigationItem)"
class="nav-text-item flex flex-col w-[130px] h-[130px] justify-between text-right hover:cursor-pointer"
:class="{ 'text-sn-science-blue': selectedNavText === navigationItem.textId, 'text-sn-grey': selectedNavText !== navigationItem.textId }">
{{ i18n.t(`repositories.highlight_component.${navigationItem.labelAlias}`) }}
</div>
</div>
<div id="highlight-container" class="w-[1px] h-[130px] flex flex-col justify-evenly bg-sn-light-grey">
<div v-for="(itemObj, index) in itemsToCreate" :key="index">
<div :id="itemObj?.id" class="w-[5px] h-[28px] rounded-[11px]"
:class="{ 'bg-sn-science-blue relative left-[-2px]': itemObj?.id === selectedNavIndicator }"></div>
</div>
<div v-for="navigationItem in itemsToCreate" :key="navigationItem.id" class="w-[5px] h-[28px] rounded-[11px]"
:class="{ 'bg-sn-science-blue relative left-[-2px]': selectedNavIndicator === navigationItem.id }"></div>
</div>
</div>
</template>
@ -35,176 +31,143 @@ export default {
selectedNavText: null,
selectedNavIndicator: null,
sections: [],
prevSection: null,
scrollTimer: null,
shouldRecalculateWhenStopped: false
activeSection: null,
proximityThreshold: 60,
header: null,
previousScrollTop: 0,
};
},
computed: {
headerHeight() {
return this.header.getBoundingClientRect().height;
}
},
mounted() {
this.bodyContainerEl = this.$parent.$refs.bodyWrapper
this.sections = this.$parent.$refs.scrollSpyContent.querySelectorAll('section[id]');
this.bodyContainerEl?.addEventListener('scroll', this.handleScroll)
this.highlightActiveSectionOnScroll()
this.initializeComponent();
},
beforeDestroy() {
this.removeScrollListener();
},
watch: {
activeSection: 'highlightSection'
},
methods: {
// If the user scrolls too fast to register movement, then we need to do something when the scrolling has stopped.
// When the scrolling has stopped and if we have permission to recalculate
// then we find the closest dom node relative to the target area and highlight it
scrollStopped() {
if (!this.shouldRecalculateWhenStopped) return
const bodyWrapperTargetAreaRectTop = this.bodyContainerEl.getBoundingClientRect().top;
const sectionRects = Array.from(this.sections).map((s) => {
const rect = s.getBoundingClientRect();
return {
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
width: rect.width,
height: rect.height,
id: s.getAttribute('id'),
};
});
const closestDomNodeToHighlight = this.findClosestDomNode(sectionRects, bodyWrapperTargetAreaRectTop)
// If user clicked on the navigation and not actually scrolled the scroll event still happened.
// However, in those cases top/bot values will be zero and we should not compute closestDomNode highlighting
if (closestDomNodeToHighlight.top !== 0 && closestDomNodeToHighlight.bottom !== 0) {
const id = closestDomNodeToHighlight.id
const foundMatchToHighlight = this.itemsToCreate.find((e) => e.sectionId === id)
this.selectedNavText = foundMatchToHighlight.textId
this.selectedNavIndicator = foundMatchToHighlight.id
}
initializeComponent() {
this.header = this.$parent.$refs.stickyHeaderRef;
this.bodyContainerEl = this.$parent.$refs.bodyWrapper;
this.sections = Array.from(this.$parent.$refs.scrollSpyContent.querySelectorAll('section[id]'));
this.proximityThreshold = this.headerHeight;
this.highlightSection(this.sections[0]);
this.addScrollListener();
},
// For finding the closest dom node (to highlight)
findClosestDomNode(arr, referenceValue) {
if (arr.length === 0) {
return null;
}
let closestObject = arr[0];
let minDifference = Math.abs(arr[0].top - referenceValue);
for (let i = 1; i < arr.length; i++) {
const difference = Math.abs(arr[i].top - referenceValue);
if (difference < minDifference) {
minDifference = difference;
closestObject = arr[i];
}
}
return closestObject;
addScrollListener() {
this.bodyContainerEl?.addEventListener('scroll', this.handleScroll);
},
removeScrollListener() {
this.bodyContainerEl?.removeEventListener('scroll', this.handleScroll);
},
// Handling scroll events
handleScroll() {
this.shouldRecalculateWhenStopped = true
this.highlightActiveSectionOnScroll()
if (this.scrollTimer) {
clearTimeout(this.scrollTimer);
}
this.scrollTimer = setTimeout(this.scrollStopped, 200);
},
const currentScrollTop = this.bodyContainerEl.scrollTop;
// Highlighting active sections while scrolling
highlightActiveSectionOnScroll() {
if (!this.bodyContainerEl) return;
const bodyWrapperTargetAreaRect = this.bodyContainerEl.getBoundingClientRect();
const margin = this.targetAreaMargin;
// Far top position
if (this.bodyContainerEl.scrollTop === 0) {
this.shouldRecalculateWhenStopped = false
this.handleTopOrBotScrollPosition(this.sections[0]);
if (currentScrollTop === 0) {
this.setActiveSection(this.sections[0]);
return;
}
// Far bottom position
if (this.bodyContainerEl.scrollTop + this.bodyContainerEl.clientHeight === this.bodyContainerEl.scrollHeight) {
this.shouldRecalculateWhenStopped = false
this.handleTopOrBotScrollPosition(this.sections[this.sections.length - 1])
return
// Determine the scroll direction (down or up)
if (currentScrollTop > this.previousScrollTop) {
this.handleScrollDown();
} else {
this.handleScrollUp();
}
// Checks when a section enters targetArea's boundary and highlights it
for (const section of this.sections) {
const sectionRect = section.getBoundingClientRect();
if (sectionRect === this.prevSection) continue;
this.previousScrollTop = currentScrollTop;
},
if (this.isSectionInBounds(sectionRect, bodyWrapperTargetAreaRect, margin)) {
this.handleSectionHighlight(section);
}
// scrolling from up -> down
// highlighting based on proximity to the header
handleScrollDown() {
const headerTop = this.getDistanceToTop(this.header);
const nearestSection = this.sections.reduce((acc, section) => {
const distance = Math.abs(headerTop - this.getDistanceToTop(section));
return distance < this.proximityThreshold ? section : acc;
}, null);
if (nearestSection) this.setActiveSection(nearestSection);
},
// scrolling from down -> up
// highlighting based on passing out of view
handleScrollUp() {
if (!this.activeSection) return;
const activeSectionRect = this.activeSection.getBoundingClientRect();
const containerRect = this.bodyContainerEl.getBoundingClientRect();
if (activeSectionRect.bottom < containerRect.top || activeSectionRect.top > containerRect.bottom) {
const previousSection = this.getPreviousSection(this.activeSection);
if (previousSection) this.setActiveSection(previousSection);
}
},
// For handling top/bottom most positions
handleTopOrBotScrollPosition(section) {
const sectionId = section.getAttribute('id');
const foundObj = this.itemsToCreate.find((obj) => obj?.sectionId === sectionId);
getPreviousSection(currentSection) {
const index = this.sections.indexOf(currentSection);
return index > 0 ? this.sections[index - 1] : null;
},
getDistanceToTop(el) {
return el.getBoundingClientRect().top;
},
setActiveSection(section) {
if (section !== this.activeSection) {
this.activeSection = section;
this.highlightSection(section);
}
},
highlightSection(section) {
const foundObj = this.itemsToCreate.find(obj => obj.sectionId === section.id);
if (foundObj) {
this.selectedNavText = foundObj.textId;
this.selectedNavIndicator = foundObj.id;
}
},
// For checking if a section is within targetArea's boundaries
isSectionInBounds(sectionRect, targetAreaRect, margin) {
const upperBound = targetAreaRect.top - margin;
const lowerBound = targetAreaRect.top + margin;
return sectionRect.top >= upperBound && sectionRect.top <= lowerBound;
},
navigateToSection(navigationItem) {
if (!this.bodyContainerEl) return;
// For highlighting a section during scrolling
handleSectionHighlight(section) {
const sectionId = section.getAttribute('id');
const foundObj = this.itemsToCreate.find((obj) => obj?.sectionId === sectionId);
this.removeScrollListener();
this.activeSection = document.getElementById(navigationItem.sectionId);
this.selectedNavText = navigationItem.textId;
this.selectedNavIndicator = navigationItem.id;
if (foundObj) {
this.selectedNavText = foundObj.textId;
this.selectedNavIndicator = foundObj.id;
this.prevSection = section.getBoundingClientRect();
}
},
const domElToScrollTo = this.$parent.$refs[navigationItem.label];
const top = domElToScrollTo.offsetTop - (this.stickyHeaderHeightPx + this.cardTopPaddingPx);
// For handling clicks on the side navigation
handleSideNavClick(e) {
if (!this.bodyContainerEl) return
this.bodyContainerEl?.removeEventListener('scroll', this.handleScroll)
let refToScrollTo
const targetId = e.target.id
const foundObj = this.itemsToCreate.find((obj) => obj?.textId === targetId)
if (!foundObj) return
// Highlighting
refToScrollTo = foundObj.label
this.selectedNavText = foundObj.textId
this.selectedNavIndicator = foundObj.id
const sectionLabels = this.itemsToCreate.map((obj) => obj.label)
const labelsToUnhighlight = sectionLabels.filter((i) => i !== refToScrollTo)
// Scrolling to desired section
const domElToScrollTo = this.$parent.$refs[refToScrollTo]
const top = domElToScrollTo.offsetTop - this?.stickyHeaderHeightPx - this?.cardTopPaddingPx;
this.bodyContainerEl.scrollTo({
top: top,
top,
behavior: "auto"
})
});
// flashing the title color to blue and back over 300ms
domElToScrollTo?.classList.add('text-sn-science-blue')
labelsToUnhighlight.forEach(id => document.getElementById(id)?.classList.remove('text-sn-science-blue'))
setTimeout(() => {
domElToScrollTo?.classList.remove('text-sn-science-blue')
}, 300)
this.flashTitleColor(domElToScrollTo);
setTimeout(this.addScrollListener, 100);
},
setTimeout(() => {
this.bodyContainerEl?.addEventListener('scroll', this.handleScroll)
}, 100)
flashTitleColor(domEl) {
if (!domEl) return
}
},
domEl.classList.add('text-sn-science-blue');
setTimeout(() => domEl.classList.remove('text-sn-science-blue'), 300);
},
}
}
</script>