@josmangarsal/pragmatic-scheduler
Version:
React resource scheduler
245 lines (244 loc) • 11.3 kB
JavaScript
;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useScrollLoading = void 0;
var jsx_runtime_1 = require("@emotion/react/jsx-runtime");
var react_1 = require("react");
var Scheduler_1 = require("../components/Scheduler");
var date_fns_1 = require("date-fns");
var DEFAULT_CONFIG = {
gestureDuration: 600,
minWheelDelta: 10,
resetDelay: 300, // Delay before resetting the gesture
};
var useScrollLoading = function (_a, config) {
var scrollRef = _a.scrollRef;
var _b = (0, react_1.useContext)(Scheduler_1.SchedulerContext), enabled = _b.extendWithScroll, changeDates = _b.changeDates, _c = _b.calendarBounds, start = _c.start, end = _c.end;
var _d = __read((0, react_1.useState)({
readyToLoadLeft: false,
readyToLoadRight: false,
progress: 0,
direction: null,
}), 2), state = _d[0], setState = _d[1];
var settings = (0, react_1.useMemo)(function () { return (__assign(__assign({}, DEFAULT_CONFIG), config)); }, [config]);
// Use refs for mutable state that doesn't cause re-renders
var gestureRef = (0, react_1.useRef)({
startTime: 0,
accumulatedDelta: 0,
resetTimeout: null,
hasTriggered: false,
lastDirection: null,
});
// Reset gesture state
var resetGesture = (0, react_1.useCallback)(function () {
if (gestureRef.current.resetTimeout) {
clearTimeout(gestureRef.current.resetTimeout);
gestureRef.current.resetTimeout = null;
}
gestureRef.current.startTime = 0;
gestureRef.current.accumulatedDelta = 0;
gestureRef.current.hasTriggered = false;
gestureRef.current.lastDirection = null;
setState(function (prev) { return (__assign(__assign({}, prev), { progress: 0, direction: null })); });
}, []);
// Handle date change
var handleDateChange = (0, react_1.useCallback)(function (direction) {
if (gestureRef.current.hasTriggered || !changeDates)
return;
gestureRef.current.hasTriggered = true;
var daysToAdd = direction === 'right' ? 1 : -1;
changeDates((0, date_fns_1.addDays)(start, daysToAdd), (0, date_fns_1.addDays)(end, daysToAdd));
// Reset after a small delay to show completion
setTimeout(function () {
resetGesture();
}, 100);
}, [changeDates, start, end, resetGesture]);
// Check if scroll is at edges
var checkScrollEdges = (0, react_1.useCallback)(function () {
if (!scrollRef.current)
return { atLeft: false, atRight: false };
var element = scrollRef.current;
var scrollLeft = element.scrollLeft;
var maxScroll = element.scrollWidth - element.clientWidth;
return {
atLeft: scrollLeft <= 1,
atRight: scrollLeft >= maxScroll - 1,
};
}, [scrollRef]);
// Update edge states when scrolling
(0, react_1.useEffect)(function () {
if (!enabled || !scrollRef.current)
return;
var element = scrollRef.current;
var handleScroll = function () {
var _a = checkScrollEdges(), atLeft = _a.atLeft, atRight = _a.atRight;
setState(function (prev) {
// Only update if values actually changed
if (prev.readyToLoadLeft !== atLeft || prev.readyToLoadRight !== atRight) {
return __assign(__assign({}, prev), { readyToLoadLeft: atLeft, readyToLoadRight: atRight });
}
return prev;
});
// Reset gesture if scrolling away from edges
if (!atLeft && !atRight && gestureRef.current.startTime > 0) {
resetGesture();
}
};
element.addEventListener('scroll', handleScroll);
// Initial check
handleScroll();
return function () {
element.removeEventListener('scroll', handleScroll);
};
}, [enabled, scrollRef, checkScrollEdges, resetGesture]);
// Handle wheel events
(0, react_1.useEffect)(function () {
if (!enabled || !scrollRef.current || !changeDates)
return;
var element = scrollRef.current;
var handleWheel = function (e) {
// Skip if already triggered
if (gestureRef.current.hasTriggered)
return;
// Check if we're at an edge
var _a = checkScrollEdges(), atLeft = _a.atLeft, atRight = _a.atRight;
var wheelDirection = e.deltaY > 0 ? 'right' : 'left';
// Only process if at the correct edge
var atCorrectEdge = (wheelDirection === 'left' && atLeft) || (wheelDirection === 'right' && atRight);
if (!atCorrectEdge) {
// Reset if not at edge
if (gestureRef.current.startTime > 0) {
resetGesture();
}
return;
}
// Prevent default scroll behavior when at edge
e.preventDefault();
// Ignore small wheel deltas
var absDelta = Math.abs(e.deltaY);
if (absDelta < settings.minWheelDelta)
return;
var now = Date.now();
// Start new gesture or check direction change
if (gestureRef.current.startTime === 0) {
gestureRef.current.startTime = now;
gestureRef.current.lastDirection = wheelDirection;
setState(function (prev) { return (__assign(__assign({}, prev), { direction: wheelDirection })); });
}
else if (gestureRef.current.lastDirection !== wheelDirection) {
// Direction changed, reset
resetGesture();
return;
}
// Accumulate delta
gestureRef.current.accumulatedDelta += absDelta;
// Calculate progress (0 to 1)
var timeElapsed = now - gestureRef.current.startTime;
var timeProgress = Math.min(timeElapsed / settings.gestureDuration, 1);
var deltaProgress = Math.min(gestureRef.current.accumulatedDelta / 300, 1);
// Use whichever progress is greater
var progress = Math.max(timeProgress, deltaProgress);
// Update progress
setState(function (prev) { return (__assign(__assign({}, prev), { progress: progress })); });
// Trigger action at 100% progress
if (progress >= 1 && !gestureRef.current.hasTriggered) {
handleDateChange(wheelDirection);
return;
}
// Clear existing timeout
if (gestureRef.current.resetTimeout) {
clearTimeout(gestureRef.current.resetTimeout);
}
// Set new timeout to reset if no more wheel events
gestureRef.current.resetTimeout = setTimeout(function () {
if (!gestureRef.current.hasTriggered) {
resetGesture();
}
}, settings.resetDelay);
};
element.addEventListener('wheel', handleWheel, { passive: false });
return function () {
element.removeEventListener('wheel', handleWheel);
if (gestureRef.current.resetTimeout) {
// eslint-disable-next-line react-hooks/exhaustive-deps
clearTimeout(gestureRef.current.resetTimeout);
}
};
}, [enabled, scrollRef, changeDates, checkScrollEdges, settings, handleDateChange, resetGesture]);
// Create the ScrollReboundLoader component
var ScrollReboundLoader = (0, react_1.useMemo)(function () {
var Loader = function (_a) {
var _b;
var direction = _a.direction;
var shouldShow = (direction === 'left' && state.readyToLoadLeft) || (direction === 'right' && state.readyToLoadRight);
var isActive = state.direction === direction && state.progress > 0;
if (!shouldShow)
return null;
return ((0, jsx_runtime_1.jsx)("div", __assign({ style: (_b = {
position: 'absolute',
top: 0,
bottom: 0,
width: '40px'
},
_b[direction] = 0,
_b.pointerEvents = 'none',
_b.zIndex = 100,
_b.overflow = 'hidden',
_b.background = "linear-gradient(".concat(direction === 'left' ? '90deg' : '270deg', ", \n rgba(79, 172, 254, ").concat(isActive ? 0.1 : 0, ") 0%, \n transparent 100%)"),
_b.transition = 'background 0.3s ease',
_b) }, { children: (0, jsx_runtime_1.jsx)("div", { style: {
position: 'absolute',
top: 0,
bottom: 0,
width: '100%',
transform: "scaleX(".concat(isActive ? state.progress : 0, ")"),
transformOrigin: "".concat(direction, " center"),
background: state.progress >= 0.9
? 'linear-gradient(90deg, #4ade80, #22c55e)'
: 'linear-gradient(90deg, #4facfe, #00f2fe)',
opacity: isActive ? 0.8 : 0,
transition: isActive
? 'transform 0.1s linear, opacity 0.2s ease, background 0.3s ease'
: 'transform 0.3s ease, opacity 0.3s ease',
boxShadow: isActive ? '0 0 20px rgba(79, 172, 254, 0.5)' : 'none',
borderRadius: direction === 'left' ? '0 4px 4px 0' : '4px 0 0 4px',
} }) })));
};
Loader.displayName = 'ScrollReboundLoader';
return Loader;
}, [state]);
return {
ScrollReboundLoader: ScrollReboundLoader,
isLoadingLeft: state.readyToLoadLeft,
isLoadingRight: state.readyToLoadRight,
progress: state.progress,
isActive: state.direction !== null,
};
};
exports.useScrollLoading = useScrollLoading;