152 lines
4.4 KiB
JavaScript
152 lines
4.4 KiB
JavaScript
"use client";
|
|
|
|
import React, { useEffect, useRef } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
|
|
const DEFAULT_REMOTE_URL = "https://uikit.d.aiengines.ir/AppSidebar.js";
|
|
const REACT_REMOTE_DEPS_KEY = "__UIKIT_REACT_REMOTE_DEPS__";
|
|
const APP_SIDEBAR_SLOT = Symbol.for("uikit.remote.app-sidebar.slot");
|
|
const remotePromises = new Map();
|
|
|
|
function ensureReactRemoteDeps() {
|
|
if (typeof window === "undefined") return;
|
|
const previous = window[REACT_REMOTE_DEPS_KEY] || {};
|
|
window[REACT_REMOTE_DEPS_KEY] = {
|
|
...previous,
|
|
React: previous.React || React,
|
|
createRoot: previous.createRoot || createRoot,
|
|
};
|
|
}
|
|
|
|
function loadRemote(componentName, remoteFileUrl) {
|
|
const cacheKey = `${componentName}:${remoteFileUrl}`;
|
|
if (!remotePromises.has(cacheKey)) {
|
|
remotePromises.set(
|
|
cacheKey,
|
|
import(
|
|
/* webpackIgnore: true */
|
|
/* @vite-ignore */
|
|
remoteFileUrl
|
|
).catch((error) => {
|
|
remotePromises.delete(cacheKey);
|
|
throw error;
|
|
}),
|
|
);
|
|
}
|
|
return remotePromises.get(cacheKey);
|
|
}
|
|
|
|
function createReactRemoteElementComponent({
|
|
componentName,
|
|
tagName,
|
|
defaultRemoteUrl,
|
|
normalizeProps,
|
|
}) {
|
|
return function ReactRemoteElementComponent(props) {
|
|
const containerRef = useRef(null);
|
|
const elementRef = useRef(null);
|
|
const latestPropsRef = useRef(null);
|
|
latestPropsRef.current = normalizeProps ? normalizeProps(props) : props;
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
ensureReactRemoteDeps();
|
|
|
|
loadRemote(componentName, defaultRemoteUrl)
|
|
.then(() => {
|
|
if (!active || !containerRef.current) return;
|
|
const element = document.createElement(tagName);
|
|
element.componentProps = latestPropsRef.current;
|
|
containerRef.current.appendChild(element);
|
|
elementRef.current = element;
|
|
})
|
|
.catch((error) => {
|
|
console.error(`[uikit/remote/ui] Failed to load ${componentName}:`, error);
|
|
});
|
|
|
|
return () => {
|
|
active = false;
|
|
if (elementRef.current) {
|
|
elementRef.current.remove();
|
|
elementRef.current = null;
|
|
}
|
|
};
|
|
}, [componentName, defaultRemoteUrl, tagName]);
|
|
|
|
useEffect(() => {
|
|
if (elementRef.current) {
|
|
elementRef.current.componentProps = latestPropsRef.current;
|
|
}
|
|
});
|
|
|
|
return React.createElement("span", {
|
|
ref: containerRef,
|
|
style: { display: "contents" },
|
|
});
|
|
};
|
|
}
|
|
|
|
function createAppSidebarSlot(name) {
|
|
function AppSidebarSlot() {
|
|
return null;
|
|
}
|
|
AppSidebarSlot[APP_SIDEBAR_SLOT] = name;
|
|
return AppSidebarSlot;
|
|
}
|
|
|
|
const Exit = createAppSidebarSlot("exit");
|
|
|
|
function toPlainText(value) {
|
|
if (value === null || value === undefined || typeof value === "boolean") return undefined;
|
|
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
if (Array.isArray(value)) return value.map(toPlainText).filter(Boolean).join("");
|
|
if (React.isValidElement(value)) return toPlainText(value.props?.children);
|
|
return undefined;
|
|
}
|
|
|
|
function extractExit(children) {
|
|
const result = { showExit: false };
|
|
React.Children.forEach(children, (child) => {
|
|
if (!React.isValidElement(child)) return;
|
|
if (child.type?.[APP_SIDEBAR_SLOT] !== "exit") return;
|
|
|
|
result.showExit = true;
|
|
result.exitLabel = child.props.label || toPlainText(child.props.children) || "خروج";
|
|
result.onExit = child.props.onClick;
|
|
result.exitDisabled = Boolean(child.props.disabled);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
|
|
function normalizeIcon(icon) {
|
|
if (!icon || React.isValidElement(icon)) return icon;
|
|
if (typeof icon === "function") return React.createElement(icon);
|
|
if (typeof icon === "object" && icon?.$$typeof) {
|
|
try { return React.createElement(icon); } catch (_) { return icon; }
|
|
}
|
|
return icon;
|
|
}
|
|
|
|
function normalizeItems(items) {
|
|
if (!Array.isArray(items)) return [];
|
|
return items.map((item) => ({ ...item, icon: normalizeIcon(item?.icon) }));
|
|
}
|
|
|
|
const RemoteAppSidebarElement = createReactRemoteElementComponent({
|
|
componentName: "AppSidebar",
|
|
tagName: "uikit-app-sidebar",
|
|
defaultRemoteUrl: DEFAULT_REMOTE_URL,
|
|
normalizeProps: ({ children, items, ...props }) => ({
|
|
...props,
|
|
items: normalizeItems(items),
|
|
...(children !== undefined && children !== null ? extractExit(children) : {}),
|
|
}),
|
|
});
|
|
|
|
function AppSidebarRoot(props) {
|
|
return React.createElement(RemoteAppSidebarElement, props);
|
|
}
|
|
|
|
export const AppSidebar = Object.assign(AppSidebarRoot, { Exit });
|