UNPKG

reblendjs

Version:

This is build using react way of handling dom but with web components

280 lines (258 loc) 10.8 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { REBLEND_PRIMITIVE_ELEMENT_NAME, ReblendTyping } from 'reblend-typing' import { capitalize, isCallable, REBLEND_COMPONENT, REBLEND_WRAPPER_FOR_REACT_COMPONENT } from '../common/utils' import { DiffUtil } from './DiffUtil' import { NodeUtil } from './NodeUtil' import { PropertyUtil } from './PropertyUtil' import { Reblend } from './Reblend' import { ReblendReactClass } from './ReblendReactClass' const componentConfig: { [key: string]: boolean } = { ReblendPlaceholder: true, defaultReblendPlaceholderStyle: true, } export const ElementUtil = class { /** * Creates an HTML, SVG, MathML, or XML element based on the provided tag name. * * @param {string} tag - The tag name of the element to create. * @returns {HTMLElement | SVGElement | Element} The created element. */ static createElementWithNamespace(tag: string): HTMLElement | SVGElement | Element { const svgTags = [ 'svg', 'path', 'circle', 'rect', 'line', 'text', 'use', 'g', 'defs', 'clipPath', 'polygon', 'polyline', 'image', 'symbol', ] const mathMLTags = ['math', 'mi', 'mn', 'mo', 'ms', 'mtext'] const xmlTags = ['xml', 'xmlns'] if (svgTags.includes(tag)) { return document.createElementNS('http://www.w3.org/2000/svg', tag) } else if (mathMLTags.includes(tag)) { return document.createElementNS('http://www.w3.org/1998/Math/MathML', tag) } else if (xmlTags.includes(tag)) { return document.createElementNS('http://www.w3.org/XML/1998/namespace', tag) } else { return document.createElement(tag) // Default to HTML namespace } } /** * Sets attributes on the given element, handling namespaces for XML, SVG, MathML, and XLink attributes. * * @param {HTMLElement | SVGElement} element - The element on which to set attributes. * @param {Record<string, string>} attributes - A record of attribute names and values to set. */ static setAttributesWithNamespace(element: HTMLElement | SVGElement, attributes: Record<string, string>): void { const svgAttributes = [ /* SVG attributes */ ] const mathMLAttributes = ['displaystyle', 'scriptlevel', 'mathvariant', 'mathsize', 'lspace', 'rspace'] for (const [key, value] of Object.entries(attributes)) { if (key.startsWith('xmlns:')) { // Handle XML namespace attributes element.setAttributeNS('http://www.w3.org/2000/xmlns/', key, value) } else if (key === 'xmlns') { // Handle default XML namespace attribute element.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', value) } else if (element instanceof SVGElement && svgAttributes.includes(key as never)) { // Handle SVG attributes element.setAttributeNS('http://www.w3.org/2000/svg', key, value) } else if (element instanceof Element && mathMLAttributes.includes(key)) { // Handle MathML attributes element.setAttributeNS('http://www.w3.org/1998/Math/MathML', key, value) } else if (key.startsWith('xlink:')) { // Handle XLink attributes element.setAttributeNS('http://www.w3.org/1999/xlink', key, value) } else { // Handle standard HTML attributes element.setAttribute(key, value) } } } /** * Creates child nodes from the given ReblendTyping.VNode children and appends them to the container array. * Supports nested arrays, Sets, and various node types such as Reblend, DOM Nodes, React Nodes, and primitive values. * * @param {ReblendTyping.VNodeChildren} children - The children to process. * @param {(ReblendTyping.Component<P, S> | HTMLElement)[]} [containerArr=[]] - The array to store the created child nodes. * @returns {(ReblendTyping.Component<P, S> | HTMLElement)[]} The array containing the created child nodes. */ static async createChildren<P, S>( children: ReblendTyping.VNodeChildren, containerArr: (ReblendTyping.Component<P, S> | HTMLElement)[] = [], ): Promise<(ReblendTyping.Component<P, S> | HTMLElement)[]> { if (!children) { return containerArr } if (!(children instanceof Set) && !Array.isArray(children)) { children = [children] } for (const child of children instanceof Set ? Array.from(children) : children) { if (isCallable(child)) { containerArr.push(child as any) } else if (Array.isArray(child)) { ElementUtil.createChildren(child as any, containerArr) } else if ( child instanceof Reblend || child instanceof Node || NodeUtil.isPrimitive(child) || NodeUtil.isReactToReblendVirtualNode(child) || NodeUtil.isReblendVirtualNode(child) || NodeUtil.isStandardVirtualNode(child) ) { const domChild = DiffUtil.deepFlat(await ElementUtil.createElement(child as any)) domChild && containerArr.push(...domChild) } else { throw new TypeError('Invalid child node in children') } } return containerArr } /** * Creates an element based on the provided virtual node (ReblendTyping.VNode) or primitive value. * The created element is returned as a `BaseComponent`. * * @param {ReblendTyping.VNode | ReblendTyping.VNode[] | ReblendTyping.ReactNode | ReblendTyping.Primitive} vNode - The virtual node or primitive to create an element from. * @returns {ReblendTyping.Component<P, S>[]} The created `BaseComponent` instances. */ static async createElement<P, S>( vNode: ReblendTyping.VNode | ReblendTyping.VNode[] | ReblendTyping.ReactNode | ReblendTyping.Primitive, ): Promise<ReblendTyping.Component<P, S>[]> { if (vNode instanceof Reblend || vNode instanceof Node) { if (!(vNode as any).displayName) { ;(vNode as any).displayName = capitalize((vNode as any as HTMLElement).tagName) NodeUtil.extendPrototype(vNode, Reblend.prototype) NodeUtil.addSymbol('ReblendNodeStandard', vNode) ;(vNode as any)._constructor() } return [vNode as any] } if (Array.isArray(vNode)) { return (await ElementUtil.createChildren(vNode)) as any } if (NodeUtil.isPrimitive(vNode)) { return [ElementUtil.newReblendPrimitive()?.setData(vNode as ReblendTyping.Primitive) as any] } const { displayName } = vNode as ReblendTyping.VNode let clazz: typeof Reblend = displayName as any as typeof Reblend const isTagStandard = typeof displayName === 'string' const isReactNode = NodeUtil.isReactNode(displayName as any) const tagName = isTagStandard ? displayName : (NodeUtil.isReactNode(clazz) ? (clazz as any as ReblendTyping.ReactNode).displayName : clazz?.ELEMENT_NAME) || `Anonymous` isTagStandard || (clazz.ELEMENT_NAME = tagName) if (isReactNode) { clazz = ReblendReactClass as any clazz.ELEMENT_NAME = capitalize(`${(displayName as unknown as ReblendTyping.ReactNode).displayName}`) } const element: ReblendTyping.Component<P, S> = ElementUtil.createElementWithNamespace( isTagStandard ? displayName : 'div', ) as ReblendTyping.Component<P, S> NodeUtil.addSymbol( isReactNode ? 'ReactToReblendNode' : isTagStandard ? 'ReblendNodeStandard' : 'ReblendNode', element, ) element.displayName = tagName if (isTagStandard || isReactNode) { NodeUtil.extendPrototype(element, Reblend.prototype) } else { NodeUtil.extendPrototype(element, clazz.prototype) if (clazz.config) { Object.entries(clazz.config).forEach(([key, value]) => { if (!componentConfig[key]) { throw new Error(`Unsupported key \`${key}\` found in component configuration.`) } else { element[key] = value } }) } } if (isReactNode) { element.ReactClass = displayName as any NodeUtil.extendPrototype(element, ReblendReactClass.prototype) } element._constructor() if (!isTagStandard) { const isProduction = process.env.REBLEND_ENVIRONEMENT_PRODUCTION if (isReactNode) { element.setAttribute(REBLEND_WRAPPER_FOR_REACT_COMPONENT, isProduction ? '' : tagName) } else { element.setAttribute(REBLEND_COMPONENT, isProduction ? '' : tagName) } } if (isTagStandard && 'ref' in (vNode as ReblendTyping.VNode).props) { if ((vNode as ReblendTyping.VNode).props.ref && !(vNode as ReblendTyping.VNode).props.ref.current) { const ref = (vNode as ReblendTyping.VNode).props.ref const descriptor = Object.getOwnPropertyDescriptor(ref, 'current') if (typeof ref === 'function') { ref(element) } else if (!descriptor || descriptor.configurable) { Object.defineProperty(ref, 'current', { value: element, configurable: false, writable: false, }) } element.ref = ref } } if (NodeUtil.isStandard(element) || isReactNode) { await PropertyUtil.setProps((vNode as ReblendTyping.VNode).props, element, true) await element.populateHtmlElements() } else { PropertyUtil.setProps((vNode as ReblendTyping.VNode).props, element, true).finally(() => { element.populateHtmlElements() }) } return [element] } /** * Creates a new Reblend primitive element. * * @returns {ReblendTyping.Primitive} The newly created Reblend primitive element. */ static newReblendPrimitive(): Text & { setData: (data: ReblendTyping.Primitive) => Text getData: () => ReblendTyping.Primitive } { const text: any = document.createTextNode('') as any as ReblendTyping.Primitive NodeUtil.extendPrototype(text, Reblend.prototype) text.displayName = REBLEND_PRIMITIVE_ELEMENT_NAME /** * Sets the data of the Reblend primitive. * * @param {ReblendTyping.Primitive} data - The data to set. * @returns {ReblendTyping.Primitive} The updated Reblend primitive. */ text.setData = function (data: ReblendTyping.Primitive): ReblendTyping.Primitive { this.reblendPrimitiveData = data if (this.reblendPrimitiveData !== undefined && this.reblendPrimitiveData !== null) { const textContent = `${this.reblendPrimitiveData}` this.nodeValue = textContent } else { this.nodeValue = '' } return this } /** * Gets the data of the Reblend primitive. * * @returns {ReblendTyping.Primitive} The data of the Reblend primitive. */ text.getData = function (): ReblendTyping.Primitive { return this.reblendPrimitiveData } return text } }