/**
* @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;
}