mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-06 21:24:23 +08:00
Rewriting scrolling logic [SCI-9584]
This commit is contained in:
parent
256bc0f9e1
commit
46199340aa
2 changed files with 126 additions and 173 deletions
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue