Skip to content

Instantly share code, notes, and snippets.

@kristersd
Created October 28, 2022 09:49
Show Gist options
  • Save kristersd/7a6eb5422b2c1b1b37a9dd7638bcc403 to your computer and use it in GitHub Desktop.
Save kristersd/7a6eb5422b2c1b1b37a9dd7638bcc403 to your computer and use it in GitHub Desktop.
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {H_IN_MS} from "../../../../../../../common/constants/Commons";
import TimeFormat from "../../../../../../../common/components/TimeFormat/TimeFormat";
import {useSelector} from "react-redux";
import customSelector from "./customSelector";
import {Badge} from "reactstrap";
import TimeService from "../../../../../../../common/services/time/timeService";
const MARKER_HEIGHT = 16;
const CCWDragDropSelected = ({weekViewStoreRef, scrollableContainerRef, weekData, totalDayHeight, hourPositions}) => {
const [marker, setMarker] = useState(null);
const timeZone = useSelector((state) => TimeService.getViewTimeZone(state));
const timeAccurateFormatValue = useSelector((state) => state.config.Global.options.timeAccurateFormat.value);
const isDragging = useSelector(customSelector);
const sidebarHeightRef = useRef(0);
const reversedHourPositions = useMemo(() => {
return hourPositions.slice().reverse();
}, [hourPositions]);
/**
* Calculates marker time based on offset.
* Used for case, when there is no event as target, for example, the ul element.
* @type {function(offsetY: number): number}
*/
const calculateMarkerTime = useCallback((offsetY) => {
let prev = reversedHourPositions[0];
let dataHour;
for (let i = 1; i < reversedHourPositions.length; i++) {
const curr = reversedHourPositions[i];
if (prev.top <= offsetY && offsetY < curr.top) {
dataHour = prev;
break;
}
prev = curr;
dataHour = curr;
}
const relativeHeight = 1 - (dataHour.top + dataHour.height - offsetY) / dataHour.height;
return dataHour.h + Math.round(relativeHeight * H_IN_MS);
}, [reversedHourPositions]);
/**
* Calculates marker time based on event.
* @type {function(number, {style: {top: number, height: number}, event: {onAirTime: number, duration: number}}): number}
*/
const calculateMarkerTimeByEvent = useCallback((offsetY, {style: {top, height}, event: {onAirTime, duration}}) => {
const relativeHeight = 1 - (top + height - offsetY) / height;
return TimeService.getUTCWithOffset(onAirTime + Math.round(relativeHeight * duration), timeZone);
}, [timeZone]);
/**
* Finds event id from DOM elements recursively
* @type {function(element: HTMLElement): {eventId?: number, dayIndex?: number}}
*/
const findEventFromElement = useCallback((element) => {
if (!element) {
return {};
}
const eventId = +element.getAttribute("data-channel-event");
if (eventId) {
return {
eventId,
dayIndex: +element.parentElement.getAttribute("data-day-index"),
};
}
return findEventFromElement(element.parentElement);
}, []);
useEffect(() => {
setTimeout(() => {
const element = document.querySelector(".timeline > thead");
const {height} = element.getBoundingClientRect();
sidebarHeightRef.current = height;
});
}, []);
useEffect(() => {
const element = scrollableContainerRef.current;
if (!element) {
return;
}
let initialTop = element.getBoundingClientRect().top;
let initialLeft = element.getBoundingClientRect().left;
let prevMarkerPosition = {x: 0, y: 0};
let mousePosition = {clientX: 0, clientY: 0};
let scrollPosition = {scrollLeft: 0, scrollTop: 0};
const debouncedUpdateElementPos = _.debounce(() => {
const clientRect = element.getBoundingClientRect();
initialTop = clientRect.top;
initialLeft = clientRect.left;
}, 100);
const handleEvent = (e) => {
if (!e) {
return;
}
let eventElement = e.target;
let xPosition, yPosition;
debouncedUpdateElementPos();
if (e.type === "scroll") {
const {scrollLeft, scrollTop} = scrollPosition;
prevMarkerPosition.x += ((scrollLeft - e.target.scrollLeft) * -1);
prevMarkerPosition.y += ((scrollTop - e.target.scrollTop) * -1);
scrollPosition = {
scrollLeft: e.target.scrollLeft,
scrollTop: e.target.scrollTop,
};
xPosition = prevMarkerPosition.x;
yPosition = prevMarkerPosition.y;
eventElement = document.elementFromPoint(mousePosition.clientX, mousePosition.clientY);
}
else {
xPosition = e.clientX - initialLeft + element.scrollLeft;
yPosition = e.clientY - initialTop + element.scrollTop - sidebarHeightRef.current;
yPosition = yPosition <= 0 ? 0 : yPosition;
}
const {eventId, dayIndex} = findEventFromElement(eventElement);
const currentDay = weekData.days[dayIndex];
let markerTime;
if (eventId) {
const {events} = currentDay;
const {event, style} = events.find(({event}) => (event.id | 0) === eventId);
markerTime = calculateMarkerTimeByEvent(yPosition, {style, event});
}
else {
markerTime = calculateMarkerTime(yPosition);
}
if (e.type !== "scroll") {
mousePosition.clientX = e.clientX;
mousePosition.clientY = e.clientY;
}
weekViewStoreRef.current = {
eventMarkerTime: markerTime,
};
prevMarkerPosition.x = xPosition;
prevMarkerPosition.y = yPosition;
setMarker({
top: Math.min(yPosition, totalDayHeight - MARKER_HEIGHT),
time: markerTime,
});
};
const eventTypes = ["mousemove", "scroll"];
for (const eventType of eventTypes) {
element.addEventListener(eventType, handleEvent);
}
return () => {
for (const eventType of eventTypes) {
element.removeEventListener(eventType, handleEvent);
}
};
}, [calculateMarkerTime, calculateMarkerTimeByEvent, findEventFromElement, scrollableContainerRef, totalDayHeight, weekData.days, weekViewStoreRef]);
if (!marker) {
return null;
}
return (
<td>
<span className="position-absolute"
style={{
left: 0,
top: marker.top,
height: MARKER_HEIGHT,
zIndex: 100,
}}>
<Badge pill color="primary">
<TimeFormat time={marker.time}
format={timeAccurateFormatValue} />
</Badge>
</span>
</td>
);
};
export default React.memo(CCWDragDropSelected);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment