UNPKG

@meonode/ui

Version:

A structured approach to component composition with built-in theming, prop separation, and dynamic children handling.

169 lines 18.4 kB
"use strict";var _excluded=["ref","children","nodetheme","theme","props"],_excluded2=["flex"],_excluded3=["style"],_excluded4=["children","key"];function _typeof(a){"@babel/helpers - typeof";return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},_typeof(a)}function _objectWithoutProperties(a,b){if(null==a)return{};var c,d,e=_objectWithoutPropertiesLoose(a,b);if(Object.getOwnPropertySymbols){var f=Object.getOwnPropertySymbols(a);for(d=0;d<f.length;d++)c=f[d],-1===b.indexOf(c)&&{}.propertyIsEnumerable.call(a,c)&&(e[c]=a[c])}return e}function _objectWithoutPropertiesLoose(a,b){if(null==a)return{};var c={};for(var d in a)if({}.hasOwnProperty.call(a,d)){if(-1!==b.indexOf(d))continue;c[d]=a[d]}return c}function ownKeys(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);b&&(d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable})),c.push.apply(c,d)}return c}function _objectSpread(a){for(var b,c=1;c<arguments.length;c++)b=null==arguments[c]?{}:arguments[c],c%2?ownKeys(Object(b),!0).forEach(function(c){_defineProperty(a,c,b[c])}):Object.getOwnPropertyDescriptors?Object.defineProperties(a,Object.getOwnPropertyDescriptors(b)):ownKeys(Object(b)).forEach(function(c){Object.defineProperty(a,c,Object.getOwnPropertyDescriptor(b,c))});return a}function _defineProperty(a,b,c){return(b=_toPropertyKey(b))in a?Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0}):a[b]=c,a}function _toPropertyKey(a){var b=_toPrimitive(a,"string");return"symbol"==_typeof(b)?b:b+""}function _toPrimitive(a,b){if("object"!=_typeof(a)||!a)return a;var c=a[Symbol.toPrimitive];if(void 0!==c){var d=c.call(a,b||"default");if("object"!=_typeof(d))return d;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===b?String:Number)(a)}import React,{createElement,isValidElement}from"react";import{getComponentType,getCSSProps,getDOMProps,getElementTypeName,getValueByPath,isNodeInstance}from"./node.helper.js";import{isForwardRef,isMemo,isReactClassComponent,isValidElementType}from"./react-is.helper.js";import{createRoot}from"react-dom/client";/** * Represents a node in a React component tree with theme and styling capabilities. * This class wraps React elements and handles: * - Props processing and normalization * - Theme inheritance and resolution * - Child node processing and management * - Style processing with theme variables * @template E The type of React element or component this node represents */export class BaseNode{/** * Creates a new BaseNode instance that wraps a React element. * Processes raw props by: * - Extracting and resolving theme-aware styles * - Processing DOM-related props * - Normalizing children with theme inheritance * @param element The React element/component to wrap * @param rawProps Initial props including theme, styles, and children */constructor(a){var b=this,c=1<arguments.length&&arguments[1]!==void 0?arguments[1]:{};_defineProperty(this,"rawProps",{}),_defineProperty(this,"isBaseNode",!0),_defineProperty(this,"_portalDOMElement",null),_defineProperty(this,"_portalReactRoot",null),_defineProperty(this,"_normalizeChild",function(a){var c,d;if(!a)return a;var e=(null===(c=b.rawProps)||void 0===c?void 0:c.nodetheme)||(null===(d=b.rawProps)||void 0===d?void 0:d.theme)||b.props.nodetheme||b.props.theme;// For BaseNode instances, apply current theme if child has no theme if(a instanceof BaseNode){var f;return null!==(f=a.rawProps)&&void 0!==f&&f.nodetheme||void 0===e?a.render():new BaseNode(a.element,_objectSpread(_objectSpread({},a.rawProps),{},{nodetheme:e})).render()}// For React.Component instances, wrap in BaseNode with theme if needed if(a instanceof React.Component)return a.props.nodetheme||void 0===e?a.render():new BaseNode(a.render(),_objectSpread(_objectSpread({},a.props),{},{nodetheme:e})).render();// Validate element type before returning if(!isValidElementType(a)){var g=getComponentType(a);throw new Error("Invalid element type: ".concat(g," provided!"))}// Return valid React elements as-is return a}),this.element=a,this.rawProps=c;// Destructure raw props into relevant parts var d=c.ref,e=c.children,f=c.nodetheme,g=c.theme,h=c.props,i=_objectWithoutProperties(c,_excluded),j=g||f,k=this._resolveObjWithTheme(i,j),l=getCSSProps(k),m=this._resolveDefaultStyle(l),n=_objectSpread(_objectSpread({},m),k.style),o=getDOMProps(k),p=void 0;// Resolve any theme variables in the remaining props // Extract CSS-related properties from the resolved theme-aware props // Resolve default styles based on the processed style properties // Merge resolved default styles and processed style properties into final style props // Extract remaining props that are valid DOM attributes // Process children while maintaining theme inheritance // Combine processed props into final normalized form e&&(Array.isArray(e)?p=e.map(function(a,c){return b._processRawNode(a,j,c)}):p=this._processRawNode(e,j)),this.props=_objectSpread(_objectSpread(_objectSpread({},o),h),{},{style:n,ref:d,nodetheme:j,theme:g,children:p})}/** * Resolves default styles for a given CSSProperties object. * This method ensures that certain default styles, such as `minHeight`, `minWidth`, * and `flexShrink`, are applied based on the provided style properties. * * - If the element is a flex container: * - Sets `flexShrink` to 0 for specific scenarios: * - Column-based layout without wrapping. * - Row-based layout without wrapping (a default direction is assumed to be 'row'). * - If the element is not a flex container: * - Defaults `flexShrink` to 0. * @param style The CSSProperties object containing style definitions. * @returns An object with resolved default styles. */_resolveDefaultStyle(a){var b,c=a.flex,d=_objectWithoutProperties(a,_excluded2),e="flex"===d.display,f=!!(d.overflow||d.overflowY||d.overflowX),g=(null===(b=d.flexFlow)||void 0===b?void 0:b.includes("wrap"))||"wrap"===d.flexWrap,h=void 0;if(!e)// If it's not a flex container, default flex-shrink to 0 h=0;else if(!f){var i="column"===d.flexDirection||"column-reverse"===d.flexDirection,j="row"===d.flexDirection||"row-reverse"===d.flexDirection||!d.flexDirection;// Scenario 1: Column-based layout i&&!g?h=0:j&&!g&&(h=0)}return _objectSpread({flex:c,flexShrink:h,minHeight:0,minWidth:0},d)}/** * Resolves theme variable references in an object's values recursively. * Handles nested objects and prevents circular references. * Theme variables are referenced using the format "theme.path.to.value". * @param obj The object whose values should be resolved against the theme * @param theme Optional theme object containing variable definitions * @returns A new object with all theme variables resolved to their values */_resolveObjWithTheme(a,b){var c;// Early return if no theme or empty object if(!b||0===Object.keys(a).length)return a;// Merge parent theme with current theme var d=_objectSpread(_objectSpread({},null===(c=this.rawProps)||void 0===c?void 0:c.nodetheme),b),e=function resolveRecursively(a,b){// Prevent processing same object multiple times if(b.has(a))return a;// Track this object to detect circular references b.add(a);var c={};for(var f in a){var g=a[f];// Skip complex objects/functions unless they are specifically targeted for theme resolution, // This prevents inadvertently flattening or stripping methods from instances like dayjs, React components, etc. // You might need to refine this condition based on what specific types you want to *never* resolve/transform if("function"==typeof g||g instanceof HTMLElement||g instanceof Date||g&&"object"===_typeof(g)&&!Array.isArray(g)&&Object.getPrototypeOf(g)!==Object.prototype){c[f]=g;continue}// Resolve theme variables in string values if("string"==typeof g&&g.includes("theme.")){var h=g;h=h.replace(/theme\.([a-zA-Z0-9_.-]+)/g,function(a,b){var c=getValueByPath(d,b);// Only convert string/number theme values return void 0!==c&&null!==c?"object"===_typeof(c)&&!Array.isArray(c)&&"default"in c?c["default"]:c:a;// Keep original if no valid theme value found }),c[f]=h}// Recursively process nested objects else c[f]=g&&"object"===_typeof(g)&&!Array.isArray(g)?e(g,b):g}return c};/** * Recursively resolves theme variables in an object, tracking visited objects * to prevent infinite recursion with circular references. */return e(a,new Set)}/** * Renders a processed NodeElement into a ReactNode, applying theme and key if needed. * * Handles the following cases: * 1. If the element is a BaseNode instance, it re-wraps it to apply the key and theme if needed. * 2. If the element is a React class component type, it wraps it in a BaseNode. * 3. If the element is a NodeInstance object, it calls its render method. * 4. If the element is a React.Component instance, it calls its render method. * 5. If the element is a functional component, it creates a React element with the provided key. * 6. For all other valid ReactNode types, it returns the element as-is. * @param processedElement The processed node element to render. * @param passedTheme The theme to apply, if any. * @param passedKey The key to assign, if any. * @returns The rendered ReactNode. */_renderProcessedNode(a,b,c){var d={};// 1. BaseNode instance: re-wrap to apply key/theme if needed if(void 0!==c&&(d.key=c),a instanceof BaseNode){var e,f,g=(null===(e=a.rawProps)||void 0===e?void 0:e.theme)||(null===(f=a.rawProps)||void 0===f?void 0:f.nodetheme)||b;return new BaseNode(a.element,_objectSpread(_objectSpread(_objectSpread({},a.rawProps),d),{},{nodetheme:g})).render()}// 2. React class component type: wrap in BaseNode return isReactClassComponent(a)?new BaseNode(a,d).render():isNodeInstance(a)?a.render():a instanceof React.Component?a.render():"function"==typeof a?createElement(a,{key:c}):a;// 3. NodeInstance object: call its render // 4. React.Component instance: call its render // 5. Functional component: create element with key // 6. Other valid ReactNode types }/** * Renders the result of a function child, supporting theme propagation. * * Used for children that are functions (`() => Children`). If the returned value is a `BaseNode` * without an explicit theme, the parent theme is injected. Otherwise, the result is rendered as-is. * @template E - The type of ReactNode or NodeInstance. * @param props Renderer props. * @param props.render Function to invoke for rendering the child. * @param props.passedTheme Theme to provide to the child, if applicable. * @param props.passedKey Key to assign to the rendered node. * @param props.processRawNode Function to process raw nodes. * @returns The rendered ReactNode, with theme applied if necessary. */_functionRenderer(a){var b=a.render,c=a.passedTheme,d=a.passedKey,e=a.processRawNode,f=b();// Invoke the render function to get the child node. // Handle React.Component instance if(f instanceof React.Component){var g=f.render(),h=e(g,c);return this._renderProcessedNode(h,c,d)}// Handle BaseNode instance if(f instanceof BaseNode){var i,j=f;return void 0===(null===(i=j.rawProps)||void 0===i?void 0:i.nodetheme)&&void 0!==c?new BaseNode(j.element,_objectSpread(_objectSpread({key:d},j.rawProps),{},{nodetheme:c})).render():j.render()}// Process other result types var k=e(f,c);return k?this._renderProcessedNode(k,c,d):isNodeInstance(f)?f.render():f;// Fallback: return result directly (e.g., JSX, string, etc.) // Non-BaseNode results will not automatically receive the theme. }/** * Processes a single raw child element, converting it into a ProcessedChild. * If the child is part of an array and lacks an explicit key, a stable indexed key * (`elementName_child_index`) is generated for new BaseNode instances. * @param rawNode The raw child element to process. * @param parentTheme The theme inherited from the parent node. * @param childIndex Optional index of the child if it's part of an array. * @returns The processed child. */_processRawNode(a,b,c// Index for generating stable keys for array children ){var d=getComponentType(a),e=function generateIndexedKeyIfNeeded(a,b){if(b!==void 0&&null!==b)return b;if(void 0!==c){var d=getElementTypeName(a);// Get element type name for key generation return"".concat(d,"-").concat(c)}// No explicit key, and not an array child, so BaseNode constructor will handle. };// Determine the type of the raw node // Helper to generate an indexed key if no explicit key is present and an index is available. // Case 1: Child is already a BaseNode instance if(a instanceof BaseNode){var f=a,g=f.rawProps||{},h=g.theme||g.nodetheme||b,i=e(f.element,g.key);// Get initial raw props of the child // Prefer child's own theme return new BaseNode(f.element,_objectSpread(_objectSpread({},g),{},{nodetheme:h,// Use the determined theme for the new node key:i}));// Create a new BaseNode with merged props and theme }// Case 2: Child is a primitive (string, number, boolean, null, undefined) if("string"===d||"number"===d||"boolean"===d||null===a||void 0===a)return a;// Case 3: Child is a function that needs to be called during render (FunctionRenderer). if("function"===d&&!isReactClassComponent(a)&&!isMemo(a)&&!isForwardRef(a)){// The key is for the BaseNode that wraps the _functionRenderer component. // Functions themselves don't have a .key prop that we can access here. var j=e(this._functionRenderer,void 0);// Generate key for function renderer return new BaseNode(this._functionRenderer,{processRawNode:this._processRawNode.bind(this),render:a,passedTheme:b,key:j// Assign the generated key })}// Case 4: Child is a React Element (JSX element like <div> or <MyComponent>) if(isValidElement(a)){var k=a.props,l=k.style,m=_objectWithoutProperties(k,_excluded3),n=_objectSpread(_objectSpread({},m),l||{}),o=n.theme||n.nodetheme||b,p=e(a.type,a.key);// Combine top-level props from the element with its flattened style object properties return new BaseNode(a.type,_objectSpread(_objectSpread({},n),{},{// Pass the combined props nodetheme:o,key:p}))}// Case 5: Child is an ElementType (string tag, class component, Memo/ForwardRef) if(isReactClassComponent(a)||"object"===d&&(isMemo(a)||isForwardRef(a))){// ElementTypes don't have an intrinsic key from the rawNode itself. var q=e(a,void 0);return new BaseNode(a,{nodetheme:b,// Apply parent theme key:q})}// Case 6: Handle instances of React.Component if(a instanceof React.Component){var r=a.render();// Recursively process the rendered element with a parent theme and index if available return this._processRawNode(r,b,c)}// Case 7: Fallback for other ReactNode types (e.g., Fragments, Portals if not caught by isValidElement) // These are returned as-is. If they are elements within an array, React expects them to have keys. // This logic primarily adds keys to BaseNode instances we create, other ReactNodes are returned as-is. return a}/** * Normalizes a child node into a renderable ReactNode. * Processes different types of child nodes to ensure they can be properly rendered * while maintaining theme inheritance. * * Handles: * - BaseNode instances (applies theme if needed) * - React.Component instances (applies theme if needed) * - Other valid React element types (returned as-is) * - null/undefined values (returned as-is) * @param child The child node to normalize into a renderable form * @returns The normalized ReactNode that can be rendered by React * @throws Error if child is an invalid element type *//** * Converts this BaseNode instance into a renderable React node. * Recursively processes child nodes and uses `React.createElement` to construct the final React element. * @returns A ReactNode representing the rendered element. */render(){var a=this;if(!isValidElementType(this.element)){var b=getComponentType(this.element);throw new Error("Invalid element type: ".concat(b," provided!"))}var c=this.props,d=c.children,e=c.key,f=_objectWithoutProperties(c,_excluded4),g=void 0;// Extract children and key // More accurate type if(d!==void 0&&null!==d)if(!Array.isArray(d))// Single child g=this._normalizeChild(d);else if(0<d.length){var h=d.map(function(b){return a._normalizeChild(b)});// Normalize each child in the array // Check if all children are null/undefined (e.g., conditional rendering resulted in nothing) g=h.every(function(a){return null===a||void 0===a})?void 0:h}else g=void 0;// Prepare props for React.createElement var i=_objectSpread(_objectSpread({},f),{},{key:e});return createElement(this.element,i,g)}_ensurePortalInfrastructure(){if("undefined"==typeof window)return!1;if(this._portalDOMElement&&this._portalReactRoot)return!0;if(this._portalDOMElement&&!this._portalDOMElement.isConnected&&(this._portalDOMElement=null,this._portalDOMElement=null),this._portalDOMElement||(this._portalDOMElement=document.createElement("div"),document.body.appendChild(this._portalDOMElement)),!this._portalReactRoot){if(!this._portalDOMElement)return!1;this._portalReactRoot=createRoot(this._portalDOMElement)}return!0}toPortal(){var a=this;if(!this._ensurePortalInfrastructure()||!this._portalReactRoot)return null;var b=this.render();return this._portalReactRoot.render(b),_objectSpread(_objectSpread({},this._portalReactRoot),{},{unmount:function unmount(){a._portalReactRoot&&(a._portalReactRoot.unmount(),a._portalReactRoot=null),a._portalDOMElement&&(a._portalDOMElement.parentNode&&a._portalDOMElement.parentNode.removeChild(a._portalDOMElement),a._portalDOMElement=null)}})}}/** * Factory function to create a BaseNode instance. * @param element The React element type. * @param props The props for the node. * @returns NodeInstance<E> - A new BaseNode instance. */export function Node(a){var b=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},c=_objectSpread({},b);// Ensure we are working with a mutable copy // 'theme' prop itself is not directly used by BaseNode after this, nodetheme is used. // We can keep `theme` in rawProps if needed for cloning or inspection. return c.theme&&!c.nodetheme&&(c.nodetheme=c.theme),new BaseNode(a,c)}