@lemonadejs/timeline
Version:
LemonadeJS timeline component
120 lines (105 loc) • 4.22 kB
JavaScript
// @ts-nocheck
import React, {
useRef,
useEffect,
useImperativeHandle,
useState,
memo,
} from 'react';
import Component from './index';
import './style.css';
// Maps React-style camelCase prop names to the underlying lemonadejs
// lowercase option names. The wrapper drops the leading `self` argument
// from camelCase callbacks before invoking the user's handler — `self` is
// redundant in React because the user already has access to the instance
// through the forwarded ref.
const REACT_TO_LEMONADE = {
onChange: 'onchange',
onUpdate: 'onupdate',
onCreate: 'oncreate',
onBeforeCreate: 'onbeforecreate',
onChangeEvent: 'onchangeevent',
onBeforeChangeEvent: 'onbeforechangeevent',
onBeforeChange: 'onbeforechange',
onBeforeInsert: 'onbeforeinsert',
onDelete: 'ondelete',
onError: 'onerror',
onClose: 'onclose',
onOpen: 'onopen',
onEdition: 'onedition',
onDblClick: 'ondblclick',
onInputChange: 'onChange',
};
const RESERVED_REACT_PROPS = new Set(['children', 'className', 'style', 'key']);
// HTML pass-through props (data-*, aria-*, role) are spread onto the
// wrapper div for testing/accessibility. They are NOT forwarded to the
// lemonadejs instance as options.
const isHtmlPassThroughProp = (key) =>
key.startsWith('data-') || key.startsWith('aria-') || key === 'role';
const Timeline = memo(React.forwardRef((props, mainReference) => {
const containerRef = useRef(null);
const instanceRef = useRef(null);
const propsRef = useRef(props);
const [instance, setInstance] = useState(null);
propsRef.current = props;
useEffect(() => {
// Guard via `instanceRef` (a ref, not the `instance` state) so
// React StrictMode's double-invoke of the mount effect doesn't
// construct two instances into the same container. Closures capture
// stale state on the StrictMode re-run; refs reflect the latest mount.
if (!containerRef.current || instanceRef.current) return;
const options = {};
for (const key in props) {
if (RESERVED_REACT_PROPS.has(key)) continue;
if (isHtmlPassThroughProp(key)) continue;
const lemonadeKey = REACT_TO_LEMONADE[key] || key;
const isReactCallback = key in REACT_TO_LEMONADE;
const value = props[key];
if (typeof value === 'function') {
options[lemonadeKey] = isReactCallback
? (...args) => {
const fn = propsRef.current[key];
if (typeof fn === 'function') return fn(...args.slice(1));
}
: (...args) => {
const fn = propsRef.current[key];
if (typeof fn === 'function') return fn(...args);
};
} else {
options[lemonadeKey] = value;
}
}
const newInstance = Component(containerRef.current, options);
instanceRef.current = newInstance;
setInstance(newInstance);
}, []);
useEffect(() => {
const inst = instanceRef.current;
if (!inst) return;
for (const key in props) {
if (RESERVED_REACT_PROPS.has(key)) continue;
if (isHtmlPassThroughProp(key)) continue;
if (typeof props[key] === 'function') continue;
const lemonadeKey = REACT_TO_LEMONADE[key] || key;
if (Object.prototype.hasOwnProperty.call(inst, lemonadeKey)
&& props[key] !== inst[lemonadeKey]) {
inst[lemonadeKey] = props[key];
}
}
}, [props]);
useImperativeHandle(mainReference, () => instance, [instance]);
const htmlAttrs = {};
for (const key in props) {
if (isHtmlPassThroughProp(key)) htmlAttrs[key] = props[key];
}
return React.createElement(React.Fragment, null,
React.createElement('div', {
ref: containerRef,
className: props.className,
style: { width: '100%', height: '100%', ...props.style },
...htmlAttrs,
}),
props.children,
);
}));
export default Timeline;