utils_dynamic_render.js

/**
 * @type {VNode}
 * @access private
 */
let renderPtr = null;
let renderAncestor = [];

/**
 * @type {number}
 * @access private
 */
let hookIdx = 0;

/**
 * @function render
 * @param {VNode}        vNode
 * @param {nexacro.Form} form
 * @deprecated $react를 사용하세요.
 * @memberof $v
 * @example
 * const { v, render, vDiv } = $v;
 * const vApp = function (props) {
 *     return v(vDiv, {
 *         left: 0,
 *         top: 0,
 *         right: 0,
 *         bottom: 0,
 *         children: [v("static", { text: "Hello World!!!" })],
 *     });
 * };
 *
 */
export function render(vNode, form) {
    let destroyTargetForm = form;
    if (destroyTargetForm instanceof nexacro.Div) {
        destroyTargetForm = destroyTargetForm.form;
    }
    Array.from(destroyTargetForm.all).forEach((child) => {
        destroyComponent(child);
    });

    renderComponent(vNode, form, vNode, form);
}

/**
 * @param {VNode}        vNode      가상 노드
 * @param {nexacro.Form} form       대상 Form.
 * @param {VNode}        rootVNode  최상위 가상 노드
 * @param {nexacro.Form} rootForm   최상위 Form.
 * @access private
 */
function renderComponent(vNode, form, rootVNode, rootForm) {
    let comp;

    // 렌더링 대상 컴포넌트 저장
    vNode.base = form;
    vNode.rootVNode = rootVNode;
    vNode.rootForm = rootForm;

    if (vNode.type === "div") {
        const {
            id,
            children,
            ref,
            onHScroll,
            onVScroll,
            scrollX,
            scrollY,
            ...props
        } = vNode.props;

        comp = new nexacro.Div(
            id ?? vNode.id,
            props.left,
            props.top,
            props.width,
            props.height,
            props.right,
            props.bottom,
        );

        // 이벤트 핸들러 등록
        if (onHScroll) {
            comp.addEventHandler("onhscroll", onHScroll);
        }
        if (onVScroll) {
            comp.addEventHandler("onvscroll", onVScroll);
        }

        if (ref && ref.current) {
            ref.current = comp;
        }

        // 스크롤
        if (scrollX || scrollY) {
            comp.form.addEventHandler("onload", () => {
                comp.scrollTo(scrollX ?? 0, scrollY ?? 0);
            });
        }

        // 기본 넥사크로 프로퍼티 덮기
        Object.keys(props).forEach((prop) => {
            comp[prop] = vNode.props[prop];
        });

        // 자식 컴포넌트 렌더링
        vNode.flatChildren?.forEach((child, i) => {
            renderComponent(child, comp, rootVNode, rootForm);
        });
    } else if (vNode.type === "static") {
        const { id, children, ...props } = vNode.props;

        comp = new nexacro.Static(
            id ?? vNode.id,
            props.left,
            props.top,
            props.width,
            props.height,
            props.right,
            props.bottom,
        );

        // 기본 넥사크로 프로퍼티 덮기
        Object.keys(props).forEach((prop) => {
            comp[prop] = vNode.props[prop];
        });
    } else if (vNode.type === "checkBox") {
        const { id, children, ref, canChange, onChanged, onMounted, ...props } =
            vNode.props;

        comp = new nexacro.CheckBox(
            id ?? vNode.id,
            props.left,
            props.top,
            props.width,
            props.height,
            props.right,
            props.bottom,
        );

        if (ref && ref.hasOwnProperty("current")) {
            ref.current = comp;
        }

        // 이벤트 핸들러 등록
        if (canChange) {
            comp.addEventHandler("canchange", canChange);
        }
        if (onChanged) {
            comp.addEventHandler("onchanged", onChanged);
        }
        if (onMounted) {
            onMounted();
        }

        // 기본 넥사크로 프로퍼티 덮기
        Object.keys(props).forEach((prop) => {
            comp[prop] = props[prop];
        });
    } else if (vNode.type === "edit") {
        const { id, children, ref, onMounted, onChanged, ...props } =
            vNode.props;

        comp = new nexacro.Edit(
            id ?? vNode.id,
            props.left,
            props.top,
            props.width,
            props.height,
            props.right,
            props.bottom,
        );

        if (ref && ref.current) {
            ref.current = comp;
        }

        if (onMounted) {
            onMounted();
        }

        if (onChanged) {
            comp.addEventHandler("onchanged", onChanged);
        }

        // 기본 넥사크로 프로퍼티 덮기
        Object.keys(props).forEach((prop) => {
            comp[prop] = vNode.props[prop];
        });
    } else if (vNode.type === "textArea") {
        const { id, children, ref, canChange, onChanged, ...props } =
            vNode.props;
        comp = new nexacro.TextArea(
            id ?? vNode.id,
            props.left,
            props.top,
            props.width,
            props.height,
            props.right,
            props.bottom,
        );

        // 이벤트 핸들러 등록
        if (canChange) {
            comp.addEventHandler("canchange", canChange);
        }
        if (onChanged) {
            comp.addEventHandler("onchanged", onChanged);
        }

        if (ref && ref.current) {
            ref.current = comp;
        }

        // 바인딩 이후 이벤트
        ref?.(comp);

        // 기본 넥사크로 프로퍼티 덮기
        Object.keys(props).forEach((prop) => {
            comp[prop] = vNode.props[prop];
        });
    } else if (vNode.type === "panel") {
        const {
            id,
            children,
            ref,
            onHScroll,
            onVScroll,
            scrollX,
            scrollY,
            ...props
        } = vNode.props;

        comp = new nexacro.Panel(
            id ?? vNode.id,
            props.left,
            props.top,
            props.width,
            props.height,
            props.right,
            props.bottom,
        );

        // 이벤트 핸들러 등록
        if (onHScroll) {
            comp.addEventHandler("onhscroll", onHScroll);
        }
        if (onVScroll) {
            comp.addEventHandler("onvscroll", onVScroll);
        }

        if (ref && ref.current) {
            ref.current = comp;
        }

        // 스크롤
        if (scrollX || scrollY) {
            comp.form.addEventHandler("onload", () => {
                comp.scrollTo(scrollX ?? 0, scrollY ?? 0);
            });
        }

        // 기본 넥사크로 프로퍼티 덮기
        Object.keys(props).forEach((prop) => {
            comp[prop] = vNode.props[prop];
        });

        // 자식 컴포넌트 렌더링
        vNode.flatChildren?.forEach((child, i) => {
            renderComponent(child, form, rootVNode, rootForm);
            comp.addItem(child.nativeId, child.nativeId);
        });
    } else if (vNode.type === "button") {
        const { id, children, ref, onClick, ...props } = vNode.props;
        comp = new nexacro.Button(
            id ?? vNode.id,
            props.left,
            props.top,
            props.width,
            props.height,
            props.right,
            props.bottom,
        );

        // 이벤트 핸들러 등록
        if (onClick) {
            comp.addEventHandler("onclick", onClick);
        }

        if (ref && ref.current) {
            ref.current = comp;
        }

        // 기본 넥사크로 프로퍼티 덮기
        Object.keys(props).forEach((prop) => {
            comp[prop] = vNode.props[prop];
        });
    } else {
        renderPtr = vNode;
        renderAncestor.push(vNode);
        hookIdx = 0;
        const node = vNode.type({
            ...vNode.props,
            children: vNode.flatChildren,
        });
        renderComponent(node, form, rootVNode, rootForm);
        renderPtr = null;
        renderAncestor = [];
        return;
    }

    // 컴포넌트 자식 추가
    const compId = getVNodeId(vNode);
    form.addChild(compId, comp);
    if (renderPtr) {
        renderPtr.nativeId = compId;
    }
    if (renderAncestor.length > 0) {
        renderAncestor.forEach((ancestor) => {
            ancestor.nativeId = compId;
        });
    }
    comp?.show();
    comp?.form?.resetScroll();
}

/**
 * 노드 아이디를 반환한다.
 *
 * @param vNode
 * @returns {*}
 * @access private
 */
function getVNodeId(vNode) {
    return vNode.props.id ?? vNode.id;
}

/**
 * 컴포넌트를 재귀하며 삭제한다.
 *
 * @param comp
 * @access private
 */
function destroyComponent(comp) {
    if (comp instanceof nexacro.Div) {
        Array.from(comp.form.all).forEach((child) => {
            destroyComponent(child);
        });
    }

    comp.destroy();
}

/**
 * @typedef useStateSetStateOptions
 * @property {boolean} render
 */

/**
 * 상태를 저장합니다.
 *
 * 아직 사용이 불안정하며 값이 변경 시 전체 재 렌더링이 발생합니다.
 *
 * @function useState
 * @param initialValue
 * @returns {any[]}
 * @deprecated $react를 사용하세요.
 * @memberof $v
 */
export function useState(initialValue) {
    const ptr = renderPtr;
    const compId = getVNodeId(ptr);
    hookIdx++;

    // 저장소 초기화
    if (!ptr.rootForm._v_stateStore) {
        ptr.rootForm._v_stateStore = new Map();
    }
    const stateStore = ptr.rootForm._v_stateStore;

    if (!compId) {
        throw new Error("useState는 컴포넌트 내에서만 사용 가능 합니다.");
    }

    const stateKey = `${compId}-${hookIdx}`;
    let value, setValue;

    if (!stateStore.has(stateKey)) {
        value = initialValue;
        setValue = (value, opt) => {
            const currentEntry = stateStore.get(stateKey);
            const oldValue = currentEntry.value;

            if (value !== oldValue) {
                currentEntry.value = value;
                stateStore.set(stateKey, currentEntry);

                if (ptr?.rootForm?.form?.all && opt?.render !== false) {
                    Array.from(ptr.rootForm.form.all).forEach((child) => {
                        ptr.rootForm.removeChild(child.id);
                        child.destroy();
                    });

                    const vScrollPos = ptr.rootForm.form.getVScrollPos();
                    const hScrollPos = ptr.rootForm.form.getHScrollPos();

                    render(ptr.rootVNode, ptr.rootForm);

                    ptr.rootForm.form.scrollTo(hScrollPos, vScrollPos);
                }
            }
        };
        stateStore.set(stateKey, { value, setValue });
    }

    const stateEntry = stateStore.get(stateKey);

    return [stateEntry.value, stateEntry.setValue];
}

/**
 * 넥사크로 실제 객체를 반환하는 훅입니다.
 *
 * @function useRef
 * @param initialValue
 * @returns {any[]}
 * @deprecated $react를 사용하세요.
 * @memberof $v
 */
export function useRef(initialValue) {
    return {
        current: initialValue,
    };
}

/**
 * 디바운스 훅
 *
 * @function useDebounce
 * @param {Function} callback  - 실행할 콜백 함수
 * @param {number}   delay     - 지연 시간 (밀리초)
 * @returns {Function} 디바운스된 함수
 * @memberof $v
 * @example
 * const debouncedSearch = useDebounce((value) => {
 *     // API 호출 등의 무거운 작업
 *     searchAPI(value);
 * }, 500);
 *
 */
export function useDebounce(callback, delay) {
    const ptr = renderPtr;
    const compId = getVNodeId(ptr);
    hookIdx++;

    // 저장소 초기화
    if (!ptr.rootForm._v_debounceStore) {
        ptr.rootForm._v_debounceStore = new Map();
    }
    const debounceStore = ptr.rootForm._v_debounceStore;

    if (!compId) {
        throw new Error("useDebounce는 컴포넌트 내에서만 사용 가능합니다.");
    }

    const debounceKey = `${compId}-${hookIdx}`;

    if (!debounceStore.has(debounceKey)) {
        let timeoutId = null;

        const debouncedFn = (...args) => {
            // 이전 타이머가 있다면 제거
            if (timeoutId !== null) {
                clearTimeout(timeoutId);
            }

            // 새로운 타이머 설정
            timeoutId = setTimeout(() => {
                callback.apply(null, args);
            }, delay);
        };

        // 정리 함수 (cleanup)
        const cleanup = () => {
            if (timeoutId !== null) {
                clearTimeout(timeoutId);
                timeoutId = null;
            }
        };

        debounceStore.set(debounceKey, {
            fn: debouncedFn,
            cleanup: cleanup,
        });
    }

    const debounceEntry = debounceStore.get(debounceKey);
    return debounceEntry.fn;
}