UNPKG

@lemonadejs/calendar

Version:

LemonadeJS reactive JavaScript calendar plugin

131 lines (117 loc) 4.97 kB
// @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;