@helioviewer/event-tree
Version:
A react component for grouping and listing and selecting solar events.
248 lines (202 loc) • 6.43 kB
JSX
import React from "react";
import { useRef, useState, useEffect } from "react";
import Cache from "./Core/Cache.jsx";
import EventTree from "./Core/EventTree.jsx";
import API from "./Core/API.jsx";
import Checkbox from "./Checkbox.jsx";
import Node from "./Node.jsx";
import SourceHeader from "./SourceHeader.jsx";
import { LoadingIcon } from "./Icons.jsx";
const useIsMount = () => {
const isMountRef = useRef(false);
useEffect(() => {
isMountRef.current = true;
}, []);
return isMountRef.current;
};
function HelioviewerEventTree({
source,
eventsDate,
onEventsUpdate,
onHoveredEventsUpdate,
onSelectionsUpdate = null,
visibility = true,
labelVisibility = true,
onToggleVisibility = null,
onToggleLabelVisibility = null,
forcedSelections = null,
apiURL = "https://api.helioviewer.org",
onLoad = null,
onError = (err) => {
console.error(err);
}
}) {
const isMount = useIsMount();
const cache = Cache.make(source);
const [selections, setSelections] = useState(forcedSelections ?? cache.getSelections());
const [loading, setLoading] = useState(false);
const [showEmptyBranches, setShowEmptyBranches] = useState(cache.getShowEmptyBranches());
const [eventTree, setEventTree] = useState(null);
const [events, setEvents] = useState([]);
// Run initially first
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
async function fetchEvents() {
setLoading(true);
try {
const api = new API(apiURL);
const apiEvents = await api.getEvents(eventsDate, source, { signal });
const eventTree = EventTree.make(apiEvents, source);
const adjustedSelections = eventTree.adjustSelectionsWithPath(selections);
const selectedTree = eventTree.applySelections(adjustedSelections);
Cache.make(source).saveSelections(adjustedSelections);
if (onSelectionsUpdate != null) {
onSelectionsUpdate(adjustedSelections, selectedTree.selectedEvents());
}
onEventsUpdate(selectedTree.selectedEvents());
setEventTree(selectedTree);
} catch (error) {
if (error.name != "AbortError") {
onError(error);
}
} finally {
setLoading(false);
}
}
fetchEvents();
return () => {
controller.abort(); // Abort on cleanup
};
}, [eventsDate.getTime()]);
useEffect(() => {
// This is only for after mount
if (!isMount) {
return;
}
Cache.make(source).saveSelections(selections);
const selectedTree = eventTree.removeSelections().applySelections(selections);
if (onSelectionsUpdate != null) {
onSelectionsUpdate(selections, selectedTree.selectedEvents());
}
onEventsUpdate(selectedTree.selectedEvents());
setEventTree(selectedTree);
}, [selections]);
useEffect(() => {
// Only update if it is updated from outside
if (!isMount || forcedSelections == null) {
return;
}
setSelections(forcedSelections);
}, [forcedSelections]);
useEffect(() => {
if (onLoad) {
onLoad();
}
});
const toggleCheckbox = function (id) {
const newSelections = eventTree.toggleCheckbox(id).extractSelections();
setSelections(newSelections);
};
const toggleExpand = function (id) {
const newTree = eventTree.toggleExpand(id);
setEventTree(newTree);
};
const onHover = function (hoverId) {
const hoveredEvents = eventTree.getEventsOfNode(hoverId).map((e) => {
return e.id;
});
onHoveredEventsUpdate(hoveredEvents);
};
const offHover = function () {
onHoveredEventsUpdate([]);
};
const handleShowEmptyBranches = function (newShowEmptyBranchesValue) {
Cache.make(source).saveShowEmptyBranches(newShowEmptyBranchesValue);
setShowEmptyBranches(newShowEmptyBranchesValue);
};
if (eventTree == null) {
return;
}
let nodeContainerStyle = {
margin: 0,
padding: ".55em .125em .1em 1em",
backgroundImage: "linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0))",
MozBackgroundImage: "-moz-linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0))",
OBackgroundImage: "-o-linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0))",
WebkitBackgroundImage: "-webkit-linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0))"
};
let hasEvents = eventTree.getEventCount(source) > 0;
let hasCategories = eventTree[source].children.length > 0;
if (!hasEvents) {
nodeContainerStyle = {
opacity: 0.5,
...nodeContainerStyle
};
}
const nodeChildrensStyle = {
paddingLeft: 3,
paddingTop: 10,
paddingBottom: 10,
fontSize: 13
};
if (!eventTree[source].expand) {
nodeChildrensStyle["display"] = "none";
}
let eventTreeRender = <span style={{ color: "red" }}>No events</span>;
if (hasCategories) {
eventTreeRender = eventTree[source].children.map((cid) => {
return (
<Node
key={cid}
id={cid}
toggleCheckbox={toggleCheckbox}
toggleExpand={toggleExpand}
onHover={onHover}
offHover={offHover}
eventTree={eventTree}
showEmptyBranches={showEmptyBranches}
/>
);
});
}
return (
<>
<div style={nodeContainerStyle}>
<SourceHeader
hasEvents={hasEvents}
hasCategories={hasCategories}
checked={eventTree[source].state}
onCheckedUpdate={() => toggleCheckbox(source)}
onHeaderHover={() => {
onHover(source);
}}
offHeaderHover={() => {
offHover();
}}
source={source}
eventsDate={eventsDate}
expand={eventTree[source].expand}
toggleExpand={() => toggleExpand(source)}
showEmptyBranches={showEmptyBranches}
handleShowEmptyBranches={handleShowEmptyBranches}
visibility={visibility}
handleVisibility={onToggleVisibility}
labelVisibility={labelVisibility}
handleLabelVisibility={onToggleLabelVisibility}
/>
<div style={nodeChildrensStyle}>
{loading ? (
<div className="event-tree-container-loader">
<LoadingIcon />
<span> Loading</span>
</div>
) : (
eventTreeRender
)}
</div>
</div>
</>
);
}
export default HelioviewerEventTree;