UNPKG

@furystack/shades

Version:

A lightweight UI framework for FuryStack with JSX support

163 lines 6.23 kB
import { isShadeComponent } from './models/shade-component.js'; import { SVG_NS, isSvgTag } from './svg.js'; import { createVNode } from './vnode.js'; // --------------------------------------------------------------------------- // Render-mode toggle // --------------------------------------------------------------------------- let renderMode = false; /** * When true, the JSX factory produces VNode descriptors instead of real DOM elements. * Set to true by `_performUpdate` before calling `render()`, then back to false after. */ export const setRenderMode = (mode) => { renderMode = mode; }; // --------------------------------------------------------------------------- // Real-DOM helpers (used outside render mode) // --------------------------------------------------------------------------- /** * Appends `children` to `el`. Strings/numbers are wrapped in text nodes; * nested arrays are flattened recursively. Used outside render mode (real * DOM); inside render mode the JSX factory builds VNodes instead. */ export const appendChild = (el, children) => { for (const child of children) { if (typeof child === 'string' || typeof child === 'number') { el.appendChild(document.createTextNode(child)); } else { if (child instanceof Element || child instanceof DocumentFragment) { el.appendChild(child); } else if (child instanceof Array) { appendChild(el, child); } } } }; export const hasStyle = (props) => { return (!!props && typeof props === 'object' && typeof props.style === 'object'); }; /** Copies `props.style` (when present) onto `el.style`. No-op for non-styled props. */ export const attachStyles = (el, props) => { if (hasStyle(props)) for (const key in props.style) { if (Object.prototype.hasOwnProperty.call(props.style, key)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error el.style[key] = props.style[key]; } } }; export const attachDataAttributes = (el, props) => { if (props) { Object.entries(props) .filter(([key]) => key.startsWith('data-') || key.startsWith('aria-')) .forEach(([key, value]) => el.setAttribute(key, value || '')); } }; /** * Assigns `props` onto `el` as element properties (not attributes). `style` * is forwarded to {@link attachStyles}; `data-*` / `aria-*` are forwarded to * {@link attachDataAttributes}. */ export const attachProps = (el, props) => { if (!props) { return; } attachStyles(el, props); if (hasStyle(props)) { const { style, ...rest } = props; Object.assign(el, rest); } else { Object.assign(el, props); } attachDataAttributes(el, props); }; /** * SVG counterpart of {@link attachProps}. SVG attributes are XML-based and * must be set via `setAttribute` rather than property assignment. Event * handlers (`on*`) and `style` are still set as properties. */ export const attachSvgProps = (el, props) => { if (!props) { return; } for (const [key, value] of Object.entries(props)) { if (key === 'style' && typeof value === 'object' && value !== null) { for (const [sk, sv] of Object.entries(value)) { ; el.style[sk] = sv; } } else if (key === 'className') { el.setAttribute('class', String(value)); } else if (key.startsWith('on') && typeof value === 'function') { ; el[key] = value; } else if (value !== null && value !== undefined && value !== false) { el.setAttribute(key, String(value)); } } }; /** * JSX factory backing both intrinsic elements (`<div>`, `<svg>`, …) and * Shade components (`<MyShade>`). Configured as `jsxFactory` in tsconfig. * Outside render mode this returns real DOM nodes; the render-mode wrapper * {@link createComponent} swaps in VNode descriptors. */ export const createComponentInner = (...[elementType, props, ...children]) => { if (typeof elementType === 'string') { const isSvg = isSvgTag(elementType); const el = isSvg ? document.createElementNS(SVG_NS, elementType) : document.createElement(elementType); if (isSvg) { attachSvgProps(el, props); } else { attachProps(el, props); } if (children) { appendChild(el, children); } return el; } else if (isShadeComponent(elementType)) { const el = elementType(props, children); attachStyles(el, props); return el; } return undefined; }; export const createFragmentInner = (...[_props, ...children]) => { const fragment = document.createDocumentFragment(); appendChild(fragment, children); return fragment; }; export const createComponent = (...args) => { // Strip __self / __source dev-mode metadata that JSX transpilers (e.g. Vite 8+) // inject into the props object. These are not real component props and would // pollute shallow-equality checks, prop forwarding, and component rendering. const rawProps = args[1]; if (rawProps && typeof rawProps === 'object' && ('__self' in rawProps || '__source' in rawProps)) { const { __self, __source, ...cleanProps } = rawProps; args[1] = cleanProps; } // In render mode, produce VNode descriptors instead of real DOM elements if (renderMode) { const [type, props, ...children] = args; // When jsxFragmentFactory === jsxFactory (both "createComponent"), the compiler // passes createComponent itself as the first arg for fragments: createComponent(createComponent, null, ...) if (type === null || type === createComponent) { return createVNode(null, null, ...children); } return createVNode(type, props, ...children); } if (args[0] === null) { return createFragmentInner(...args); } return createComponentInner(...args); }; //# sourceMappingURL=shade-component.js.map