UNPKG

@lemonadejs/timeline

Version:

LemonadeJS timeline component

120 lines (105 loc) 4.22 kB
// @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;