@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
163 lines • 6.23 kB
JavaScript
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