mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-10 16:08:52 +01:00
New experiment: Boosts Carousel™️
This commit is contained in:
parent
62e88e4b78
commit
e2139399ee
7 changed files with 361 additions and 91 deletions
116
src/app.css
116
src/app.css
|
@ -75,7 +75,7 @@ a.mention span {
|
|||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.deck header {
|
||||
.deck > header {
|
||||
min-height: 3em;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
@ -93,25 +93,25 @@ a.mention span {
|
|||
transition: transform 0.5s ease-in-out;
|
||||
user-select: none;
|
||||
}
|
||||
.deck header[hidden] {
|
||||
.deck > header[hidden] {
|
||||
transform: translateY(-100%);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.deck header > .header-side:last-of-type {
|
||||
.deck > header > .header-side:last-of-type {
|
||||
text-align: right;
|
||||
grid-column: 3;
|
||||
}
|
||||
.deck header :is(button, .button).plain {
|
||||
.deck > header :is(button, .button).plain {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.deck header h1 {
|
||||
.deck > header h1 {
|
||||
margin: 0 8px;
|
||||
padding: 0;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
.deck header h1:first-child {
|
||||
.deck > header h1:first-child {
|
||||
text-align: left;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
@ -368,11 +368,109 @@ a.mention span {
|
|||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.boost-carousel {
|
||||
background: linear-gradient(
|
||||
to bottom right,
|
||||
var(--reblog-faded-color),
|
||||
transparent 60%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
.boost-carousel:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image: radial-gradient(
|
||||
ellipse 50% 32px at bottom center,
|
||||
var(--reblog-faded-color),
|
||||
transparent
|
||||
),
|
||||
linear-gradient(to top, var(--bg-color), transparent 64px);
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
}
|
||||
.boost-carousel .status-reblog {
|
||||
background-image: none;
|
||||
}
|
||||
.boost-carousel header {
|
||||
padding: 8px 16px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.boost-carousel h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
color: var(--reblog-color);
|
||||
text-shadow: 0 1px var(--bg-color);
|
||||
}
|
||||
.boost-carousel ul {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
counter-reset: index;
|
||||
}
|
||||
.boost-carousel ul > li {
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-stop: always;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: min(320px, calc(100% - 16px));
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 70vh;
|
||||
max-height: 70dvh;
|
||||
counter-increment: index;
|
||||
position: relative;
|
||||
}
|
||||
.boost-carousel ul > li:before {
|
||||
content: counter(index);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
font-size: 10px;
|
||||
color: var(--reblog-color);
|
||||
padding: 8px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ui-state {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-boost-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-decoration-line: none;
|
||||
color: inherit;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease-out;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
animation: appear 0.2s ease-out;
|
||||
border: 1px solid var(--outline-color);
|
||||
background-color: var(--bg-blur-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px var(--bg-color);
|
||||
}
|
||||
.status-boost-link:is(:hover, :focus) {
|
||||
background-color: var(--link-bg-hover-color);
|
||||
}
|
||||
.status-boost-link:active:not(:has(:is(.media, button):active)) {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.deck-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -867,7 +965,7 @@ meter.donut:is(.danger, .explode):after {
|
|||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.timeline-deck header {
|
||||
.timeline-deck > header {
|
||||
min-height: 6em;
|
||||
border-bottom: 0;
|
||||
background-color: var(--bg-faded-blur-color);
|
||||
|
@ -884,10 +982,10 @@ meter.donut:is(.danger, .explode):after {
|
|||
transparent
|
||||
);
|
||||
}
|
||||
.deck header h1 {
|
||||
.deck > header h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.timeline-deck .timeline:not(.flat) li {
|
||||
.timeline-deck .timeline:not(.flat) > li {
|
||||
border: 1px solid var(--divider-color);
|
||||
margin: 16px 0;
|
||||
background-color: var(--bg-color);
|
||||
|
|
|
@ -43,6 +43,7 @@ const ICONS = {
|
|||
popin: ['mingcute:external-link-line', '180deg'],
|
||||
plus: 'mingcute:add-circle-line',
|
||||
'chevron-left': 'mingcute:left-line',
|
||||
'chevron-right': 'mingcute:right-line',
|
||||
reply: ['mingcute:share-forward-line', '180deg', 'horizontal'],
|
||||
thread: 'mingcute:route-line',
|
||||
group: 'mingcute:group-line',
|
||||
|
|
|
@ -47,6 +47,55 @@ function Home({ hidden }) {
|
|||
reply: !!status.inReplyToAccountId,
|
||||
};
|
||||
});
|
||||
|
||||
{
|
||||
// BOOSTS CAROUSEL
|
||||
let specialHome = [];
|
||||
let boostStash = [];
|
||||
for (let i = 0; i < homeValues.length; i++) {
|
||||
const status = homeValues[i];
|
||||
if (status.reblog) {
|
||||
boostStash.push(status);
|
||||
} else {
|
||||
specialHome.push(status);
|
||||
}
|
||||
}
|
||||
// if boostStash is more than quarter of homeValues
|
||||
if (boostStash.length > homeValues.length / 4) {
|
||||
// if boostStash is more than 3 quarter of homeValues
|
||||
const boostStashID = boostStash.map((status) => status.id);
|
||||
if (boostStash.length > (homeValues.length * 3) / 4) {
|
||||
// insert boost array at the end of specialHome list
|
||||
specialHome = [
|
||||
...specialHome,
|
||||
{ id: boostStashID, boosts: boostStash },
|
||||
];
|
||||
} else {
|
||||
// insert boosts array in the middle of specialHome list
|
||||
const half = Math.floor(specialHome.length / 2);
|
||||
specialHome = [
|
||||
...specialHome.slice(0, half),
|
||||
{
|
||||
id: boostStashID,
|
||||
boosts: boostStash,
|
||||
},
|
||||
...specialHome.slice(half),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Untouched, this is fine
|
||||
specialHome = homeValues;
|
||||
}
|
||||
console.log({
|
||||
specialHome,
|
||||
});
|
||||
if (firstLoad) {
|
||||
states.specialHome = specialHome;
|
||||
} else {
|
||||
states.specialHome.push(...specialHome);
|
||||
}
|
||||
}
|
||||
|
||||
if (firstLoad) {
|
||||
states.home = homeValues;
|
||||
} else {
|
||||
|
@ -84,36 +133,35 @@ function Home({ hidden }) {
|
|||
useHotkeys('j', () => {
|
||||
// focus on next status after active status
|
||||
// Traverses .timeline li .status-link, focus on .status-link
|
||||
const activeStatus = document.activeElement.closest('.status-link');
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll(
|
||||
'.status-link, .status-boost-link',
|
||||
),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const nextStatus = activeStatus.parentElement.nextElementSibling;
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
const nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||
if (nextStatus) {
|
||||
const statusLink = nextStatus.querySelector('.status-link');
|
||||
if (statusLink) {
|
||||
statusLink.focus();
|
||||
}
|
||||
nextStatus.focus();
|
||||
nextStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const statusLinks = document.querySelectorAll(
|
||||
'.timeline li .status-link',
|
||||
);
|
||||
let topmostStatusLink;
|
||||
for (const statusLink of statusLinks) {
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
if (statusLinkRect.top >= 44) {
|
||||
// 44 is the magic number for header height, not real
|
||||
topmostStatusLink = statusLink;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -121,67 +169,68 @@ function Home({ hidden }) {
|
|||
useHotkeys('k', () => {
|
||||
// focus on previous status after active status
|
||||
// Traverses .timeline li .status-link, focus on .status-link
|
||||
const activeStatus = document.activeElement.closest('.status-link');
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll(
|
||||
'.status-link, .status-boost-link',
|
||||
),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const prevStatus = activeStatus.parentElement.previousElementSibling;
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
const prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||
if (prevStatus) {
|
||||
const statusLink = prevStatus.querySelector('.status-link');
|
||||
if (statusLink) {
|
||||
statusLink.focus();
|
||||
}
|
||||
prevStatus.focus();
|
||||
prevStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const statusLinks = document.querySelectorAll(
|
||||
'.timeline li .status-link',
|
||||
);
|
||||
let topmostStatusLink;
|
||||
for (const statusLink of statusLinks) {
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
if (statusLinkRect.top >= 44) {
|
||||
// 44 is the magic number for header height, not real
|
||||
topmostStatusLink = statusLink;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys(['enter', 'o'], () => {
|
||||
// open active status
|
||||
const activeStatus = document.activeElement.closest('.status-link');
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
);
|
||||
if (activeStatus) {
|
||||
activeStatus.click();
|
||||
}
|
||||
});
|
||||
|
||||
const { scrollDirection, reachTop, nearReachTop, nearReachBottom } =
|
||||
const { scrollDirection, reachStart, nearReachStart, nearReachEnd } =
|
||||
useScroll({
|
||||
scrollableElement: scrollableRef.current,
|
||||
distanceFromTop: 0.1,
|
||||
distanceFromBottom: 0.15,
|
||||
scrollThresholdUp: 44,
|
||||
distanceFromStart: 0.1,
|
||||
distanceFromEnd: 0.15,
|
||||
scrollThresholdStart: 44,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (nearReachBottom && showMore) {
|
||||
if (nearReachEnd && showMore) {
|
||||
loadStatuses();
|
||||
}
|
||||
}, [nearReachBottom]);
|
||||
}, [nearReachEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reachTop) {
|
||||
if (reachStart) {
|
||||
loadStatuses(true);
|
||||
}
|
||||
}, [reachTop]);
|
||||
}, [reachStart]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
@ -196,6 +245,10 @@ function Home({ hidden }) {
|
|||
})();
|
||||
}, []);
|
||||
|
||||
const snapHome = snapStates.settings.boostsCarousel
|
||||
? snapStates.specialHome
|
||||
: snapStates.home;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="home-page"
|
||||
|
@ -205,7 +258,7 @@ function Home({ hidden }) {
|
|||
tabIndex="-1"
|
||||
>
|
||||
<button
|
||||
hidden={scrollDirection === 'down' && !nearReachTop}
|
||||
hidden={scrollDirection === 'down' && !nearReachStart}
|
||||
type="button"
|
||||
id="compose-button"
|
||||
onClick={(e) => {
|
||||
|
@ -224,7 +277,7 @@ function Home({ hidden }) {
|
|||
</button>
|
||||
<div class="timeline-deck deck">
|
||||
<header
|
||||
hidden={scrollDirection === 'down' && !nearReachTop}
|
||||
hidden={scrollDirection === 'down' && !nearReachStart}
|
||||
onClick={() => {
|
||||
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
|
@ -263,8 +316,8 @@ function Home({ hidden }) {
|
|||
</header>
|
||||
{snapStates.homeNew.length > 0 &&
|
||||
scrollDirection === 'up' &&
|
||||
!nearReachTop &&
|
||||
!nearReachBottom && (
|
||||
!nearReachStart &&
|
||||
!nearReachEnd && (
|
||||
<button
|
||||
class="updates-button"
|
||||
type="button"
|
||||
|
@ -285,11 +338,18 @@ function Home({ hidden }) {
|
|||
<Icon icon="arrow-up" /> New posts
|
||||
</button>
|
||||
)}
|
||||
{snapStates.home.length ? (
|
||||
{snapHome.length ? (
|
||||
<>
|
||||
<ul class="timeline">
|
||||
{snapStates.home.map(({ id: statusID, reblog }) => {
|
||||
{snapHome.map(({ id: statusID, reblog, boosts }) => {
|
||||
const actualStatusID = reblog || statusID;
|
||||
if (boosts) {
|
||||
return (
|
||||
<li key={statusID}>
|
||||
<BoostsCarousel boosts={boosts} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li key={statusID}>
|
||||
<Link
|
||||
|
@ -367,4 +427,61 @@ function Home({ hidden }) {
|
|||
);
|
||||
}
|
||||
|
||||
function BoostsCarousel({ boosts }) {
|
||||
const carouselRef = useRef();
|
||||
const { reachStart, reachEnd } = useScroll({
|
||||
scrollableElement: carouselRef.current,
|
||||
direction: 'horizontal',
|
||||
});
|
||||
console.log({ reachStart, reachEnd });
|
||||
return (
|
||||
<div class="boost-carousel">
|
||||
<header>
|
||||
<h3>{boosts.length} Boosts</h3>
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
class="small plain2"
|
||||
disabled={reachStart}
|
||||
onClick={() => {
|
||||
carouselRef.current?.scrollBy({
|
||||
left: -Math.min(320, carouselRef.current?.offsetWidth),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="chevron-left" />
|
||||
</button>{' '}
|
||||
<button
|
||||
type="button"
|
||||
class="small plain2"
|
||||
disabled={reachEnd}
|
||||
onClick={() => {
|
||||
carouselRef.current?.scrollBy({
|
||||
left: Math.min(320, carouselRef.current?.offsetWidth),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="chevron-right" />
|
||||
</button>
|
||||
</span>
|
||||
</header>
|
||||
<ul ref={carouselRef}>
|
||||
{boosts.map((boost) => {
|
||||
const { id: statusID, reblog } = boost;
|
||||
const actualStatusID = reblog || statusID;
|
||||
return (
|
||||
<li>
|
||||
<a class="status-boost-link" href={`#/s/${actualStatusID}`}>
|
||||
<Status statusID={statusID} size="s" />
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Home);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import './settings.css';
|
||||
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Avatar from '../components/avatar';
|
||||
import Icon from '../components/icon';
|
||||
|
@ -16,6 +17,7 @@ import store from '../utils/store';
|
|||
*/
|
||||
|
||||
function Settings({ onClose }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
// Accounts
|
||||
const accounts = store.local.getJSON('accounts');
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
|
@ -184,6 +186,17 @@ function Settings({ onClose }) {
|
|||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<h2>Settings</h2>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={snapStates.settings.boostsCarousel}
|
||||
onChange={(e) => {
|
||||
states.settings.boostsCarousel = e.target.checked;
|
||||
}}
|
||||
/>{' '}
|
||||
Boosts carousel (experimental)
|
||||
</label>
|
||||
<h2>Hidden features</h2>
|
||||
<p>
|
||||
<button
|
||||
|
|
|
@ -295,9 +295,9 @@ function StatusPage({ id }) {
|
|||
location.hash = closeLink;
|
||||
});
|
||||
|
||||
const { nearReachTop } = useScroll({
|
||||
const { nearReachStart } = useScroll({
|
||||
scrollableElement: scrollableRef.current,
|
||||
distanceFromTop: 0.1,
|
||||
distanceFromStart: 0.1,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -367,7 +367,7 @@ function StatusPage({ id }) {
|
|||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
hidden={!ancestors.length || nearReachTop}
|
||||
hidden={!ancestors.length || nearReachStart}
|
||||
>
|
||||
<Icon icon="arrow-up" />
|
||||
<Icon icon="comment" />{' '}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { proxy } from 'valtio';
|
||||
import { proxy, subscribe } from 'valtio';
|
||||
|
||||
import store from './store';
|
||||
|
||||
const states = proxy({
|
||||
history: [],
|
||||
statuses: {},
|
||||
statusThreadNumber: {},
|
||||
home: [],
|
||||
specialHome: [],
|
||||
homeNew: [],
|
||||
homeLastFetchTime: null,
|
||||
notifications: [],
|
||||
|
@ -20,9 +23,19 @@ const states = proxy({
|
|||
showAccount: false,
|
||||
showDrafts: false,
|
||||
composeCharacterCount: 0,
|
||||
settings: {
|
||||
boostsCarousel: store.local.get('settings:boostsCarousel') === '1' || true,
|
||||
},
|
||||
});
|
||||
export default states;
|
||||
|
||||
subscribe(states.settings, () => {
|
||||
store.local.set(
|
||||
'settings:boostsCarousel',
|
||||
states.settings.boostsCarousel ? '1' : '0',
|
||||
);
|
||||
});
|
||||
|
||||
export function saveStatus(status, opts) {
|
||||
const { override, skipThreading } = Object.assign(
|
||||
{ override: true, skipThreading: false },
|
||||
|
|
|
@ -1,55 +1,83 @@
|
|||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export default function useScroll({
|
||||
scrollableElement = window,
|
||||
distanceFromTop = 0,
|
||||
distanceFromBottom = 0,
|
||||
scrollThresholdUp = 10,
|
||||
scrollThresholdDown = 10,
|
||||
scrollableElement,
|
||||
distanceFromStart = 0,
|
||||
distanceFromEnd = 0,
|
||||
scrollThresholdStart = 10,
|
||||
scrollThresholdEnd = 10,
|
||||
direction = 'vertical',
|
||||
} = {}) {
|
||||
const [scrollDirection, setScrollDirection] = useState(null);
|
||||
const [reachTop, setReachTop] = useState(false);
|
||||
const [nearReachTop, setNearReachTop] = useState(false);
|
||||
const [nearReachBottom, setNearReachBottom] = useState(false);
|
||||
const [reachStart, setReachStart] = useState(false);
|
||||
const [reachEnd, setReachEnd] = useState(false);
|
||||
const [nearReachStart, setNearReachStart] = useState(false);
|
||||
const [nearReachEnd, setNearReachEnd] = useState(false);
|
||||
const isVertical = direction === 'vertical';
|
||||
|
||||
if (!scrollableElement) {
|
||||
console.warn('Scrollable element is not defined');
|
||||
scrollableElement = window;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let previousScrollTop = scrollableElement.scrollTop;
|
||||
let previousScrollStart = isVertical
|
||||
? scrollableElement.scrollTop
|
||||
: scrollableElement.scrollLeft;
|
||||
|
||||
function onScroll() {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
|
||||
const scrollDistance = Math.abs(scrollTop - previousScrollTop);
|
||||
const distanceFromTopPx =
|
||||
scrollHeight * Math.min(1, Math.max(0, distanceFromTop));
|
||||
const distanceFromBottomPx =
|
||||
scrollHeight * Math.min(1, Math.max(0, distanceFromBottom));
|
||||
const {
|
||||
scrollTop,
|
||||
scrollLeft,
|
||||
scrollHeight,
|
||||
scrollWidth,
|
||||
clientHeight,
|
||||
clientWidth,
|
||||
} = scrollableElement;
|
||||
const scrollStart = isVertical ? scrollTop : scrollLeft;
|
||||
const scrollDimension = isVertical ? scrollHeight : scrollWidth;
|
||||
const clientDimension = isVertical ? clientHeight : clientWidth;
|
||||
const scrollDistance = Math.abs(scrollStart - previousScrollStart);
|
||||
const distanceFromStartPx =
|
||||
scrollDimension * Math.min(1, Math.max(0, distanceFromStart));
|
||||
const distanceFromEndPx =
|
||||
scrollDimension * Math.min(1, Math.max(0, distanceFromEnd));
|
||||
|
||||
if (
|
||||
scrollDistance >=
|
||||
(previousScrollTop < scrollTop
|
||||
? scrollThresholdDown
|
||||
: scrollThresholdUp)
|
||||
(previousScrollStart < scrollStart
|
||||
? scrollThresholdEnd
|
||||
: scrollThresholdStart)
|
||||
) {
|
||||
setScrollDirection(previousScrollTop < scrollTop ? 'down' : 'up');
|
||||
previousScrollTop = scrollTop;
|
||||
setScrollDirection(previousScrollStart < scrollStart ? 'end' : 'start');
|
||||
previousScrollStart = scrollStart;
|
||||
}
|
||||
|
||||
setReachTop(scrollTop === 0);
|
||||
setNearReachTop(scrollTop <= distanceFromTopPx);
|
||||
setNearReachBottom(
|
||||
scrollTop + clientHeight >= scrollHeight - distanceFromBottomPx,
|
||||
setReachStart(scrollStart === 0);
|
||||
setReachEnd(scrollStart + clientDimension >= scrollDimension);
|
||||
setNearReachStart(scrollStart <= distanceFromStartPx);
|
||||
setNearReachEnd(
|
||||
scrollStart + clientDimension >= scrollDimension - distanceFromEndPx,
|
||||
);
|
||||
}
|
||||
|
||||
scrollableElement.addEventListener('scroll', onScroll, { passive: true });
|
||||
scrollableElement.dispatchEvent(new Event('scroll'));
|
||||
|
||||
return () => scrollableElement.removeEventListener('scroll', onScroll);
|
||||
}, [
|
||||
scrollableElement,
|
||||
distanceFromTop,
|
||||
distanceFromBottom,
|
||||
scrollThresholdUp,
|
||||
scrollThresholdDown,
|
||||
distanceFromStart,
|
||||
distanceFromEnd,
|
||||
scrollThresholdStart,
|
||||
scrollThresholdEnd,
|
||||
]);
|
||||
|
||||
return { scrollDirection, reachTop, nearReachTop, nearReachBottom };
|
||||
return {
|
||||
scrollDirection,
|
||||
reachStart,
|
||||
reachEnd,
|
||||
nearReachStart,
|
||||
nearReachEnd,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue