@meonode/ui
Version:
A structured approach to component composition with built-in theming, prop separation, and dynamic children handling.
169 lines • 18.4 kB
JavaScript
"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)}