/** * React widget helper */ import React, { ReactElement } from 'react'; import ReactDOM, { flushSync } from 'react-dom'; import { DomHelper, StringHelper, Widget } from '@bryntum/gantt'; declare global { interface Window { bryntum: { isTestEnv?: boolean react?: { isReactElement?: (element: any) => boolean handleReactElement?: (widget: Widget, element: any) => void handleReactHeaderElement?: (column: { grid: any; id: string }, headerElement: HTMLElement, html: any) => void } } } } /** * Extends the standard React.ReactPortal type to include an optional `taskGeneration` property. * This property can be used to track or associate a generation number with the portal, * which may be useful for managing updates or synchronization tasks in complex React applications. * * @property {number} [taskGeneration] Optional generation number for task tracking or synchronization. */ type ReactPortalWithTaskGeneration = React.ReactPortal & { taskGeneration?: number }; /** * Represents the state for a React component that manages portals and their generation. * * @property {Map} portals A map associating unique string keys with `ReactPortalWithTaskGeneration` instances. * @property {number} generation A numeric value indicating the current generation or version of the component state. */ type ReactComponentState = { portals : Map generation : number }; /** * Extends the standard HTMLElement to include additional optional properties * used for managing element-specific data and state within the application. * * @property {any} [elementData] Arbitrary data associated with the element. * @property {string|null} [lastPortalId] Identifier for the last portal rendered into this element, or null if none. * @property {boolean} [didSetTextContent] Indicates whether the text content of the element has been set. */ type HTMLElementWithData = HTMLElement & { elementData?: any lastPortalId?: string | null jsxContainer?: HTMLElement | null didSetTextContent?: boolean }; /** * Development warning. Shown when environment is set to 'development' * @param {string} clsName react component instance * @param {string} msg console message */ function devWarning(clsName: string, msg: string): void { // @ts-ignore if (window.bryntum?.isTestEnv || process.env.NODE_ENV === 'development') { console.warn( `Bryntum${clsName}Component development warning!\n${msg}\n` + 'Please check React integration guide: https://bryntum.com/products/gantt/docs/guide/Gantt/integration/react/guide' ); } } function devWarningContainer(clsName: string, containerParam: string): void { devWarning( clsName, `Using "${containerParam}" parameter for configuration is not recommended.\n` + 'Widget is placed automatically inside its container element.\n' + `Solution: remove "${containerParam}" parameter from configuration.` ); } function devWarningConfigProp(clsName: string, prop: string): void { devWarning( clsName, `Using "${prop}" parameter for configuration is not recommended.\n` + `Solution: Use separate parameter for each "${prop}" value to enable reactive updates of the API instance` ); } function devWarningProjectProp(clsName: string): void { devWarning( clsName, 'Using the "project" prop with inner store configurations is not recommended.\n' + `Solution: Use an instance of the 'ProjectModel' class or the '' component` ); } /** * Returns `true` if the provided element is an instance of React Element. * All React elements require an additional $$typeof: Symbol.for('react.element') field declared on the object for security reasons. * The object, which React.createElement() returns has $$typeof property equal to Symbol.for('react.element') * * Sources: * https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html * https://github.com/facebook/react/pull/4832 * * @param {*} element Element to check * @returns {boolean} True if element is a React element * @internal */ function isReactElement(element: any): boolean { return Boolean(element?.$$typeof === Symbol.for('react.element') || element?.$$typeof === Symbol.for('react.transitional.element')); } /** * Creates bryntum component config from react component * @param {object} reactInstance react component instance * @returns {object} config object */ function createConfig(reactInstance: any): object { const { element, props, constructor } = reactInstance, { instanceClass, instanceName, isView } = constructor, filter = (arr: any[]) => arr.filter(prop => props[prop] !== undefined), configNames = filter(constructor.configNames || []), propertyConfigNames = filter(constructor.propertyConfigNames || []), propertyNames = filter(constructor.propertyNames || []), featureNames = filter(constructor.featureNames || []), bryntumConfig = { adopt : undefined, appendTo : undefined, href : undefined, reactComponent : reactInstance, listeners : {}, features : {}, hasFrameworkRenderer : isView ? hasFrameworkRenderer : undefined, processCellContent : isView ? processCellContent : undefined, processCellEditor : isView ? processCellEditor : undefined, processEventContent : isView ? processEventContent : undefined, processTaskContent : isView ? processTaskContent : undefined, processTaskItemContent : isView ? processTaskItemContent : undefined, processResourceHeader : isView ? processResourceHeader : undefined } as any; // Data store configs support reactive behavior const isDataStoreConfig = (prop: any) => { if (reactInstance.dataStores) { const dataStoreNames = Object.values(reactInstance.dataStores); return dataStoreNames.includes(prop) || dataStoreNames.includes(`${prop}Data`); } }; // Assign configs. Skip properties configNames .concat(propertyConfigNames) .concat(featureNames) .forEach(prop => { applyPropValue(bryntumConfig, prop, props[prop]); if (['features', 'config'].includes(prop) && !isDataStoreConfig(prop)) { devWarningConfigProp(instanceClass.$name, prop); } // Warn when using project prop with inner stores configs if (prop === 'project' && reactInstance.projectStores) { // @ts-ignore if (Object.values(reactInstance.dataStores).some(store => props[prop][store])) { devWarningProjectProp(instanceClass.$name); } } }); // Prepare watch arrays reactInstance.configNames = configNames; reactInstance.propertyNames = configNames .concat(propertyNames) .concat(propertyConfigNames) .concat(featureNames); // Handle inline data for stores if (reactInstance.dataStores) { Object.values(reactInstance.dataStores).forEach((dataName: string) => { if (props[dataName]) { bryntumConfig[dataName] = props[dataName]; } }); } // Cleanup unused instance arrays if (reactInstance.propertyConfigNames) { delete reactInstance.propertyConfigNames; } if (reactInstance.featureNames) { delete reactInstance.featureNames; } // If component has no container specified in config then use adopt to Wrapper's element const containerParam = ['adopt', 'appendTo', 'insertAfter', 'insertBefore'].find( prop => bryntumConfig[prop] ); if (!containerParam) { if (instanceName === 'Button') { // Button should always be or