@lemonadejs/calendar
Version:
LemonadeJS reactive JavaScript calendar plugin
131 lines (117 loc) • 4.97 kB
JavaScript
// @ts-nocheck
import React, {
useRef,
useEffect,
useImperativeHandle,
useState,
memo,
} from 'react';
import Component from './index';
// 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.
//
// `onInputChange` is calendar-specific: the lemonadejs plugin uses `onChange`
// (capital C) as a hook for the bound input element's native DOM `change`
// event. Renaming it to `onInputChange` frees `onChange` to mean "date
// selection callback" — matching what React users expect.
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 Calendar = memo(React.forwardRef((props, mainReference) => {
const containerRef = useRef(null);
const instanceRef = useRef(null);
const propsRef = useRef(props);
const [instance, setInstance] = useState(null);
// Keep a ref to the latest props so callback trampolines always invoke
// the user's most recent function (avoids stale-closure bugs on parent
// rerenders).
propsRef.current = props;
// Single-mount effect. Guard via `instanceRef` (a ref, not the
// `instance` state) — under React StrictMode the mount effect runs
// twice and closures capture stale state, so checking `instance` here
// would mount the component twice into the same container. Refs are
// mutable and survive the re-run.
useEffect(() => {
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
// React-style: drop the leading `self` argument
? (...args) => {
const fn = propsRef.current[key];
if (typeof fn === 'function') return fn(...args.slice(1));
}
// Legacy lowercase callback: pass through unchanged
: (...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);
}, []);
// Prop sync: write non-function prop changes back to the live instance.
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]);
// Expose the lemonadejs instance through the forwarded ref.
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 Calendar;