diff --git a/dist/remote-ui.d.ts b/dist/remote-ui.d.ts index 1b22ad1..dc44fd1 100644 --- a/dist/remote-ui.d.ts +++ b/dist/remote-ui.d.ts @@ -1,23 +1,54 @@ -import type { ComponentType, ReactNode } from "react"; +import type { BadgeProps, StackProps } from "@chakra-ui/react"; +import type { ReactNode } from "react"; -export type UikitRemoteDepsOptions = { - React?: unknown; - createRoot?: unknown; - ChakraProvider?: unknown; - Badge?: unknown; - theme?: unknown; - chakraProviderProps?: Record; +export type UikitRemoteLevel = "full" | "compact" | "simple"; + +export interface UikitRemoteSetupOptions { + React?: any; + createRoot?: any; + ChakraProvider?: any; + Badge?: any; + Button?: any; + HStack?: any; + IconButton?: any; + Input?: any; + Text?: any; + theme?: any; + chakraProviderProps?: Record; + + /** Backward compatible alias for GBadgeUrl. */ remoteUrl?: string; -}; + GBadgeUrl?: string; + PaginationUrl?: string; + remoteUrls?: { + GBadge?: string; + Pagination?: string; + }; +} -export declare function setupUikitRemoteDeps(options?: UikitRemoteDepsOptions): void; +export declare function setupUikitRemoteDeps( + options?: UikitRemoteSetupOptions, +): void; -export type GBadgeProps = { +export interface GBadgeProps extends BadgeProps { children?: ReactNode; remoteUrl?: string; - theme?: unknown; - chakraProviderProps?: Record; - [key: string]: unknown; -}; + theme?: any; + chakraProviderProps?: Record; +} -export declare const GBadge: ComponentType; +export declare const GBadge: (props: GBadgeProps) => JSX.Element; + +export interface PaginationProps extends Omit { + currentPage?: number; + totalCount?: number; + totalPages?: number; + pageSize?: number; + onPageChange?: (page: number) => void; + level?: UikitRemoteLevel; + remoteUrl?: string; + theme?: any; + chakraProviderProps?: Record; +} + +export declare const Pagination: (props: PaginationProps) => JSX.Element; diff --git a/dist/remote-ui.js b/dist/remote-ui.js index 4ae70b7..5f8b339 100644 --- a/dist/remote-ui.js +++ b/dist/remote-ui.js @@ -1,93 +1,138 @@ -import { jsx as g } from "react/jsx-runtime"; -import h, { useRef as u, useEffect as m } from "react"; -import { createRoot as _ } from "react-dom/client"; -import { Badge as E, ChakraProvider as P } from "@chakra-ui/react"; -const R = "https://uikit.d.aiengines.ir/GBadge.js", d = "__UIKIT_REMOTE_DEPS__", f = "__UIKIT_REMOTE_CONFIG__", i = /* @__PURE__ */ new Map(); -function w(e = {}) { +import { jsx as E } from "react/jsx-runtime"; +import p, { useRef as s, useEffect as R } from "react"; +import { createRoot as k } from "react-dom/client"; +import { Text as U, Input as _, IconButton as I, HStack as v, Button as G, Badge as C, ChakraProvider as T } from "@chakra-ui/react"; +const h = { + GBadge: "https://uikit.d.aiengines.ir/GBadge.js", + Pagination: "https://uikit.d.aiengines.ir/Pagination.js" +}, l = "__UIKIT_REMOTE_DEPS__", g = "__UIKIT_REMOTE_CONFIG__", u = /* @__PURE__ */ new Map(), y = { + GBadge: ["React", "createRoot", "ChakraProvider", "Badge"], + Pagination: [ + "React", + "createRoot", + "ChakraProvider", + "Button", + "HStack", + "IconButton", + "Input", + "Text" + ] +}; +function D(e = {}) { if (typeof window > "u") return; - const r = window[d] || {}, n = window[f] || {}; - window[d] = { - ...r, - React: e.React || r.React || h, - createRoot: e.createRoot || r.createRoot || _, - ChakraProvider: e.ChakraProvider || r.ChakraProvider || P, - Badge: e.Badge || r.Badge || E, - theme: Object.prototype.hasOwnProperty.call(e, "theme") ? e.theme : r.theme, + const t = window[l] || {}, r = window[g] || {}; + window[l] = { + ...t, + React: e.React || t.React || p, + createRoot: e.createRoot || t.createRoot || k, + ChakraProvider: e.ChakraProvider || t.ChakraProvider || T, + Badge: e.Badge || t.Badge || C, + Button: e.Button || t.Button || G, + HStack: e.HStack || t.HStack || v, + IconButton: e.IconButton || t.IconButton || I, + Input: e.Input || t.Input || _, + Text: e.Text || t.Text || U, + theme: Object.prototype.hasOwnProperty.call(e, "theme") ? e.theme : t.theme, chakraProviderProps: { - ...r.chakraProviderProps || {}, + ...t.chakraProviderProps || {}, ...e.chakraProviderProps || {} } - }, window[f] = { - ...n, - ...e.remoteUrl ? { GBadgeUrl: e.remoteUrl } : {} + }; + const n = e.remoteUrls || {}; + window[g] = { + ...r, + ...e.remoteUrl ? { GBadgeUrl: e.remoteUrl } : {}, + ...e.GBadgeUrl ? { GBadgeUrl: e.GBadgeUrl } : {}, + ...e.PaginationUrl ? { PaginationUrl: e.PaginationUrl } : {}, + ...n.GBadge ? { GBadgeUrl: n.GBadge } : {}, + ...n.Pagination ? { PaginationUrl: n.Pagination } : {} }; } -function v() { +function O(e) { if (typeof window > "u") return; - const e = window[d]; - (!(e != null && e.React) || !(e != null && e.createRoot) || !(e != null && e.ChakraProvider) || !(e != null && e.Badge)) && w(); + const t = window[l]; + (y[e] || []).some((o) => !(t != null && t[o])) && D(); } -function k() { - var e; - return typeof window > "u" ? R : ((e = window[f]) == null ? void 0 : e.GBadgeUrl) || R; +function S(e, t) { + var r; + return t || (typeof window > "u" ? h[e] : ((r = window[g]) == null ? void 0 : r[`${e}Url`]) || h[e]); } -function y() { - const e = k(); - return i.has(e) || i.set( - e, +function M(e, t) { + const r = S(e, t), n = `${e}:${r}`; + return u.has(n) || u.set( + n, import( /* webpackIgnore: true */ /* @vite-ignore */ - e - ).catch((r) => { - throw i.delete(e), r; + r + ).catch((o) => { + throw u.delete(n), o; }) - ), i.get(e); + ), u.get(n); } -function B(e) { +function x(e) { if (typeof e == "string" || typeof e == "number") return e; e != null && console.warn( "[uikit/remote/ui] GBadge only accepts text/number children across the remote boundary." ); } -const T = ({ - children: e, - remoteUrl: r, - theme: n, - chakraProviderProps: s, - ...p -}) => { - const a = u(null), t = u(null), c = u(null); - return c.current = { - ...p, - children: B(e), - ...n !== void 0 ? { theme: n } : {}, - ...s ? { chakraProviderProps: s } : {} - }, m(() => { - let l = !0; - return v(), r && w({ remoteUrl: r }), y().then(() => { - if (!l || !a.current) return; - const o = document.createElement("uikit-g-badge"); - o.componentProps = c.current, a.current.appendChild(o), t.current = o; - }).catch((o) => { - console.error("[uikit/remote/ui] Failed to load GBadge:", o); - }), () => { - l = !1, t.current && (t.current.remove(), t.current = null); - }; - }, [r]), m(() => { - t.current && (t.current.componentProps = c.current); - }), /* @__PURE__ */ g( - "span", - { - ref: a, - style: { - display: "contents" +function w({ + componentName: e, + tagName: t, + normalizeProps: r +}) { + return function({ + remoteUrl: o, + theme: f, + chakraProviderProps: m, + ...B + }) { + const c = s(null), a = s(null), d = s(null); + return d.current = { + ...r ? r(B) : B, + ...f !== void 0 ? { theme: f } : {}, + ...m ? { chakraProviderProps: m } : {} + }, R(() => { + let P = !0; + return O(e), M(e, o).then(() => { + if (!P || !c.current) return; + const i = document.createElement(t); + i.componentProps = d.current, c.current.appendChild(i), a.current = i; + }).catch((i) => { + console.error( + `[uikit/remote/ui] Failed to load ${e}:`, + i + ); + }), () => { + P = !1, a.current && (a.current.remove(), a.current = null); + }; + }, [e, o, t]), R(() => { + a.current && (a.current.componentProps = d.current); + }), /* @__PURE__ */ E( + "span", + { + ref: c, + style: { + display: "contents" + } } - } - ); -}; + ); + }; +} +const F = w({ + componentName: "GBadge", + tagName: "uikit-g-badge", + normalizeProps: ({ children: e, ...t }) => ({ + ...t, + children: x(e) + }) +}), $ = w({ + componentName: "Pagination", + tagName: "uikit-pagination" +}); export { - T as GBadge, - w as setupUikitRemoteDeps + F as GBadge, + $ as Pagination, + D as setupUikitRemoteDeps }; diff --git a/dist/remote/Pagination.js b/dist/remote/Pagination.js new file mode 100644 index 0000000..85cec3e --- /dev/null +++ b/dist/remote/Pagination.js @@ -0,0 +1,335 @@ +const H = { + full: { + showPageSummary: !0, + showTotalCount: !0, + showInput: !0, + inputWidth: "100px", + justifyContent: "space-around", + pageButtonPx: void 0, + getPageNumbers: ({ currentPage: t, totalPages: e }) => [ + 1, + t, + t + 1, + e + ] + }, + compact: { + showPageSummary: !1, + showTotalCount: !0, + showInput: !0, + inputWidth: "56px", + justifyContent: "space-around", + pageButtonPx: 2, + getPageNumbers: ({ currentPage: t, totalPages: e }) => [ + 1, + t, + e + ] + }, + simple: { + showPageSummary: !1, + showTotalCount: !1, + showInput: !1, + inputWidth: "48px", + justifyContent: "center", + pageButtonPx: void 0, + getPageNumbers: () => [] + } +}, V = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"], ne = /[۰-۹]/g, oe = /[٠-٩]/g; +function k() { + const t = window.__UIKIT_REMOTE_DEPS__, e = []; + if (t?.React || e.push("React"), t?.createRoot || e.push("createRoot"), t?.ChakraProvider || e.push("ChakraProvider"), t?.Button || e.push("Button"), t?.HStack || e.push("HStack"), t?.IconButton || e.push("IconButton"), t?.Input || e.push("Input"), t?.Text || e.push("Text"), e.length) + throw new Error( + `[uikit-pagination] Missing remote dependencies: ${e.join(", ")}. Set window.__UIKIT_REMOTE_DEPS__ before loading the remote component.` + ); + return t; +} +function a(t) { + return t == null ? "" : String(t).replace(/\d/g, (e) => V[Number(e)]); +} +function G(t) { + return t == null ? "" : String(t).replace( + ne, + (e) => String(V.indexOf(e)) + ).replace(oe, (e) => String(e.charCodeAt(0) - 1632)); +} +function re(t) { + return H[t] ? t : "full"; +} +function W(t, e) { + const m = Number(t), g = Number(e); + return !g || g <= 0 || !m || Number.isNaN(m) ? 1 : Math.min(Math.max(m, 1), g); +} +function ie(t) { + const { React: e } = k(); + return e.createElement( + "svg", + { + width: "20", + height: "20", + viewBox: "0 0 24 24", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + "aria-hidden": "true", + ...t + }, + e.createElement("path", { + d: "M15 18L9 12L15 6", + stroke: "currentColor", + strokeWidth: "2", + strokeLinecap: "round", + strokeLinejoin: "round" + }) + ); +} +function se(t) { + const { React: e } = k(); + return e.createElement( + "svg", + { + width: "20", + height: "20", + viewBox: "0 0 24 24", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + "aria-hidden": "true", + ...t + }, + e.createElement("path", { + d: "M9 18L15 12L9 6", + stroke: "currentColor", + strokeWidth: "2", + strokeLinecap: "round", + strokeLinejoin: "round" + }) + ); +} +function ue({ + currentPage: t = 1, + totalCount: e = 0, + totalPages: m, + pageSize: g = 10, + onPageChange: h, + level: v = "full", + ...B +}) { + const { React: n, Button: K, HStack: S, IconButton: j, Input: O, Text: P } = k(), { useCallback: I, useEffect: N, useMemo: U, useRef: X, useState: R } = n, F = re(v), f = H[F], r = F === "simple", y = Number(g), $ = Number.isFinite(y) && y > 0 ? y : 10, L = Number(e), D = Number.isFinite(L) && L > 0 ? L : 0, A = Number(m), u = Number.isFinite(A) && A > 0 ? Math.ceil(A) : Math.ceil(D / $), i = W(t, u), [d, c] = R( + r ? "" : a(i) + ), [E, b] = R(!1), [p, l] = R(null), C = X(null), w = p ?? i, q = D > 0, J = U(() => r || u <= 0 ? [] : Array.from( + new Set( + f.getPageNumbers({ + currentPage: i, + totalPages: u + }) + ) + ).filter((o) => o >= 1 && o <= u).sort((o, s) => o - s), [f, r, i, u]), _ = I(() => { + c(r ? "" : a(i)), b(!1), l(null); + }, [r, i]), M = I( + (o) => { + const s = W(o, u); + b(!1), c(r ? "" : a(i)), l(null), s !== i && h?.(s); + }, + [r, h, i, u] + ), x = I(() => { + if (!d) { + _(), C.current?.blur(); + return; + } + const o = parseInt(G(d), 10); + if (Number.isNaN(o)) { + _(), C.current?.blur(); + return; + } + const s = W(o, u); + if (r) { + b(!1), c(""), s !== i ? (l(s), h?.(s)) : l(null), C.current?.blur(); + return; + } + s !== i ? (l(s), c(a(s)), h?.(s)) : (c(a(i)), b(!1), l(null)), C.current?.blur(); + }, [ + d, + r, + h, + _, + i, + u + ]), Q = I(() => { + l(null), c(""), b(!0); + }, []), Y = I((o) => { + const s = o.target.value, te = G(s).replace(/\D/g, ""); + c(a(te)); + }, []); + if (N(() => { + if (r) { + p !== null && i === p && l(null); + return; + } + if (p !== null) { + i === p && (l(null), b(!1), c(a(i))); + return; + } + E || c(a(i)); + }, [E, r, p, i]), N(() => { + r && E && C.current?.focus(); + }, [E, r]), N(() => { + if (!E || !d || !r && p !== null) return; + const o = setTimeout(() => { + x(); + }, 1e3); + return () => clearTimeout(o); + }, [x, d, E, r, p]), u <= 0) return null; + const z = () => n.createElement(O, { + ref: C, + type: "text", + inputMode: "numeric", + placeholder: "صفحه", + w: f.inputWidth, + value: d, + onFocus: () => { + r || (b(!0), l(null), c("")); + }, + onChange: Y, + onBlur: () => { + if (d) { + r && x(); + return; + } + _(); + }, + onKeyDown: (o) => { + o.key === "Enter" && x(), o.key === "Escape" && _(); + }, + textAlign: "center", + color: "#1D2939", + px: 1, + minW: 0, + dir: "ltr" + }), T = (o, s = !1) => n.createElement( + K, + { + key: o, + size: "md", + px: f.pageButtonPx, + bg: s ? "#4B49AC" : "#e6e6f1", + color: s ? "#e6e6f1" : "#4B49AC", + _hover: { bg: s ? "#4B49AC" : "#cbcbe8" }, + onClick: r && s ? Q : () => M(o), + variant: "soft" + }, + a(o) + ), Z = r && u > 1 && w !== 1, ee = r && u > 1 && w !== u; + return n.createElement( + S, + { + justifyContent: f.justifyContent, + backgroundColor: "white", + py: "10px", + px: "16px", + rounded: "lg", + border: "1px", + borderColor: "blue.200", + w: "100%", + ...B + }, + f.showPageSummary && n.createElement( + S, + { spacing: 2, whiteSpace: "nowrap" }, + n.createElement(P, { fontWeight: "bold" }, "صفحه"), + n.createElement(P, null, a(i)), + n.createElement(P, { fontWeight: "bold" }, "از"), + n.createElement(P, null, a(u)) + ), + n.createElement( + S, + { spacing: 2, alignItems: "center", justifyContent: "center" }, + n.createElement(j, { + size: "md", + bg: "#e6e6f1", + color: "#4B49AC", + _hover: { bg: "#cbcbe8" }, + onClick: () => M(w - 1), + isDisabled: w === 1, + icon: n.createElement(ie), + "aria-label": "صفحه قبلی" + }), + r ? [ + Z ? T(1) : null, + E ? n.createElement( + n.Fragment, + { key: "input" }, + z() + ) : T(w, !0), + ee ? T(u) : null + ] : [ + ...J.map( + (o) => T(o, i === o) + ), + f.showInput ? n.createElement( + n.Fragment, + { key: "input" }, + z() + ) : null + ], + n.createElement(j, { + size: "md", + bg: "#e6e6f1", + color: "#4B49AC", + _hover: { bg: "#cbcbe8" }, + onClick: () => M(w + 1), + isDisabled: w === u, + icon: n.createElement(se), + "aria-label": "صفحه بعدی" + }) + ), + f.showTotalCount && q && n.createElement( + S, + { spacing: 2, whiteSpace: "nowrap" }, + n.createElement( + P, + { w: "auto", fontWeight: "bold" }, + "تعداد کل:" + ), + n.createElement(P, null, a(D)) + ) + ); +} +class ae extends HTMLElement { + constructor() { + super(), this._props = {}, this._root = null; + } + connectedCallback() { + const { createRoot: e } = k(); + this._root || (this._root = e(this)), this.renderComponent(); + } + disconnectedCallback() { + this._root && (this._root.unmount(), this._root = null); + } + set componentProps(e) { + this._props = e ?? {}, this.isConnected && this.renderComponent(); + } + get componentProps() { + return this._props; + } + renderComponent() { + if (!this._root) return; + const { + React: e, + ChakraProvider: m, + theme: g, + chakraProviderProps: h + } = k(), { theme: v, chakraProviderProps: B, ...n } = this._props || {}; + this._root.render( + e.createElement( + m, + { + theme: v || g, + resetCSS: !1, + ...h || {}, + ...B || {} + }, + e.createElement(ue, n) + ) + ); + } +} +customElements.get("uikit-pagination") || customElements.define("uikit-pagination", ae); diff --git a/src/remote/ui/Pagination.remote.jsx b/src/remote/ui/Pagination.remote.jsx new file mode 100644 index 0000000..679db76 --- /dev/null +++ b/src/remote/ui/Pagination.remote.jsx @@ -0,0 +1,560 @@ +// Remote Web Component for Pagination. +// No imports. React, createRoot and Chakra components are provided by the host via window.__UIKIT_REMOTE_DEPS__. + +const LEVELS = { + full: { + showPageSummary: true, + showTotalCount: true, + showInput: true, + inputWidth: "100px", + justifyContent: "space-around", + pageButtonPx: undefined, + getPageNumbers: ({ currentPage, totalPages }) => [ + 1, + currentPage, + currentPage + 1, + totalPages, + ], + }, + + compact: { + showPageSummary: false, + showTotalCount: true, + showInput: true, + inputWidth: "56px", + justifyContent: "space-around", + pageButtonPx: 2, + getPageNumbers: ({ currentPage, totalPages }) => [ + 1, + currentPage, + totalPages, + ], + }, + + simple: { + showPageSummary: false, + showTotalCount: false, + showInput: false, + inputWidth: "48px", + justifyContent: "center", + pageButtonPx: undefined, + getPageNumbers: () => [], + }, +}; + +const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]; +const PERSIAN_DIGIT_REGEX = /[۰-۹]/g; +const ARABIC_DIGIT_REGEX = /[٠-٩]/g; + +function getRemoteDeps() { + const deps = window.__UIKIT_REMOTE_DEPS__; + const missing = []; + + if (!deps?.React) missing.push("React"); + if (!deps?.createRoot) missing.push("createRoot"); + if (!deps?.ChakraProvider) missing.push("ChakraProvider"); + if (!deps?.Button) missing.push("Button"); + if (!deps?.HStack) missing.push("HStack"); + if (!deps?.IconButton) missing.push("IconButton"); + if (!deps?.Input) missing.push("Input"); + if (!deps?.Text) missing.push("Text"); + + if (missing.length) { + throw new Error( + `[uikit-pagination] Missing remote dependencies: ${missing.join(", ")}. ` + + "Set window.__UIKIT_REMOTE_DEPS__ before loading the remote component.", + ); + } + + return deps; +} + +function toFaDigits(value) { + if (value === undefined || value === null) return ""; + + return String(value).replace(/\d/g, (digit) => PERSIAN_DIGITS[Number(digit)]); +} + +function toEnDigits(value) { + if (value === undefined || value === null) return ""; + + return String(value) + .replace(PERSIAN_DIGIT_REGEX, (digit) => + String(PERSIAN_DIGITS.indexOf(digit)), + ) + .replace(ARABIC_DIGIT_REGEX, (digit) => String(digit.charCodeAt(0) - 1632)); +} + +function normalizeLevel(level) { + if (LEVELS[level]) return level; + + return "full"; +} + +function getSafePage(page, totalPages) { + const numericPage = Number(page); + const numericTotalPages = Number(totalPages); + + if (!numericTotalPages || numericTotalPages <= 0) return 1; + if (!numericPage || Number.isNaN(numericPage)) return 1; + + return Math.min(Math.max(numericPage, 1), numericTotalPages); +} + +function ChevronRightIcon(props) { + const { React } = getRemoteDeps(); + + return React.createElement( + "svg", + { + width: "20", + height: "20", + viewBox: "0 0 24 24", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + "aria-hidden": "true", + ...props, + }, + React.createElement("path", { + d: "M15 18L9 12L15 6", + stroke: "currentColor", + strokeWidth: "2", + strokeLinecap: "round", + strokeLinejoin: "round", + }), + ); +} + +function ChevronLeftIcon(props) { + const { React } = getRemoteDeps(); + + return React.createElement( + "svg", + { + width: "20", + height: "20", + viewBox: "0 0 24 24", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + "aria-hidden": "true", + ...props, + }, + React.createElement("path", { + d: "M9 18L15 12L9 6", + stroke: "currentColor", + strokeWidth: "2", + strokeLinecap: "round", + strokeLinejoin: "round", + }), + ); +} + +function RemotePagination({ + currentPage = 1, + totalCount = 0, + totalPages, + pageSize = 10, + onPageChange, + level = "full", + ...containerProps +}) { + const { React, Button, HStack, IconButton, Input, Text } = getRemoteDeps(); + + const { useCallback, useEffect, useMemo, useRef, useState } = React; + + const resolvedLevel = normalizeLevel(level); + const config = LEVELS[resolvedLevel]; + const isSimple = resolvedLevel === "simple"; + + const numericPageSize = Number(pageSize); + const safePageSize = + Number.isFinite(numericPageSize) && numericPageSize > 0 + ? numericPageSize + : 10; + + const numericTotalCount = Number(totalCount); + const safeTotalCount = + Number.isFinite(numericTotalCount) && numericTotalCount > 0 + ? numericTotalCount + : 0; + + const numericTotalPages = Number(totalPages); + const safeTotalPages = + Number.isFinite(numericTotalPages) && numericTotalPages > 0 + ? Math.ceil(numericTotalPages) + : Math.ceil(safeTotalCount / safePageSize); + + const safeCurrentPage = getSafePage(currentPage, safeTotalPages); + + const [inputValue, setInputValue] = useState( + isSimple ? "" : toFaDigits(safeCurrentPage), + ); + const [isEditing, setIsEditing] = useState(false); + const [pendingPage, setPendingPage] = useState(null); + const inputRef = useRef(null); + + const visibleCurrentPage = pendingPage ?? safeCurrentPage; + const hasTotalCount = safeTotalCount > 0; + + const pageNumbers = useMemo(() => { + if (isSimple || safeTotalPages <= 0) return []; + + return Array.from( + new Set( + config.getPageNumbers({ + currentPage: safeCurrentPage, + totalPages: safeTotalPages, + }), + ), + ) + .filter((page) => page >= 1 && page <= safeTotalPages) + .sort((a, b) => a - b); + }, [config, isSimple, safeCurrentPage, safeTotalPages]); + + const resetInput = useCallback(() => { + setInputValue(isSimple ? "" : toFaDigits(safeCurrentPage)); + setIsEditing(false); + setPendingPage(null); + }, [isSimple, safeCurrentPage]); + + const goToPage = useCallback( + (page) => { + const safePage = getSafePage(page, safeTotalPages); + + setIsEditing(false); + setInputValue(isSimple ? "" : toFaDigits(safeCurrentPage)); + setPendingPage(null); + + if (safePage !== safeCurrentPage) { + onPageChange?.(safePage); + } + }, + [isSimple, onPageChange, safeCurrentPage, safeTotalPages], + ); + + const commitInput = useCallback(() => { + if (!inputValue) { + resetInput(); + inputRef.current?.blur(); + return; + } + + const page = parseInt(toEnDigits(inputValue), 10); + + if (Number.isNaN(page)) { + resetInput(); + inputRef.current?.blur(); + return; + } + + const safePage = getSafePage(page, safeTotalPages); + + if (isSimple) { + setIsEditing(false); + setInputValue(""); + + if (safePage !== safeCurrentPage) { + setPendingPage(safePage); + onPageChange?.(safePage); + } else { + setPendingPage(null); + } + + inputRef.current?.blur(); + return; + } + + if (safePage !== safeCurrentPage) { + setPendingPage(safePage); + setInputValue(toFaDigits(safePage)); + onPageChange?.(safePage); + } else { + setInputValue(toFaDigits(safeCurrentPage)); + setIsEditing(false); + setPendingPage(null); + } + + inputRef.current?.blur(); + }, [ + inputValue, + isSimple, + onPageChange, + resetInput, + safeCurrentPage, + safeTotalPages, + ]); + + const startEditing = useCallback(() => { + setPendingPage(null); + setInputValue(""); + setIsEditing(true); + }, []); + + const handleInputChange = useCallback((event) => { + const rawValue = event.target.value; + const onlyDigits = toEnDigits(rawValue).replace(/\D/g, ""); + + setInputValue(toFaDigits(onlyDigits)); + }, []); + + useEffect(() => { + if (isSimple) { + if (pendingPage !== null && safeCurrentPage === pendingPage) { + setPendingPage(null); + } + + return; + } + + if (pendingPage !== null) { + if (safeCurrentPage === pendingPage) { + setPendingPage(null); + setIsEditing(false); + setInputValue(toFaDigits(safeCurrentPage)); + } + + return; + } + + if (!isEditing) { + setInputValue(toFaDigits(safeCurrentPage)); + } + }, [isEditing, isSimple, pendingPage, safeCurrentPage]); + + useEffect(() => { + if (isSimple && isEditing) { + inputRef.current?.focus(); + } + }, [isEditing, isSimple]); + + useEffect(() => { + if (!isEditing) return; + if (!inputValue) return; + if (!isSimple && pendingPage !== null) return; + + const timeout = setTimeout(() => { + commitInput(); + }, 1000); + + return () => clearTimeout(timeout); + }, [commitInput, inputValue, isEditing, isSimple, pendingPage]); + + if (safeTotalPages <= 0) return null; + + const renderInput = () => + React.createElement(Input, { + ref: inputRef, + type: "text", + inputMode: "numeric", + placeholder: "صفحه", + w: config.inputWidth, + value: inputValue, + onFocus: () => { + if (!isSimple) { + setIsEditing(true); + setPendingPage(null); + setInputValue(""); + } + }, + onChange: handleInputChange, + onBlur: () => { + if (inputValue) { + if (isSimple) { + commitInput(); + } + + return; + } + + resetInput(); + }, + onKeyDown: (event) => { + if (event.key === "Enter") { + commitInput(); + } + + if (event.key === "Escape") { + resetInput(); + } + }, + textAlign: "center", + color: "#1D2939", + px: 1, + minW: 0, + dir: "ltr", + }); + + const renderPageButton = (number, isCurrent = false) => + React.createElement( + Button, + { + key: number, + size: "md", + px: config.pageButtonPx, + bg: isCurrent ? "#4B49AC" : "#e6e6f1", + color: isCurrent ? "#e6e6f1" : "#4B49AC", + _hover: { bg: isCurrent ? "#4B49AC" : "#cbcbe8" }, + onClick: isSimple && isCurrent ? startEditing : () => goToPage(number), + variant: "soft", + }, + toFaDigits(number), + ); + + const showFirstButton = + isSimple && safeTotalPages > 1 && visibleCurrentPage !== 1; + const showLastButton = + isSimple && safeTotalPages > 1 && visibleCurrentPage !== safeTotalPages; + + return React.createElement( + HStack, + { + justifyContent: config.justifyContent, + backgroundColor: "white", + py: "10px", + px: "16px", + rounded: "lg", + border: "1px", + borderColor: "blue.200", + w: "100%", + ...containerProps, + }, + config.showPageSummary && + React.createElement( + HStack, + { spacing: 2, whiteSpace: "nowrap" }, + React.createElement(Text, { fontWeight: "bold" }, "صفحه"), + React.createElement(Text, null, toFaDigits(safeCurrentPage)), + React.createElement(Text, { fontWeight: "bold" }, "از"), + React.createElement(Text, null, toFaDigits(safeTotalPages)), + ), + React.createElement( + HStack, + { spacing: 2, alignItems: "center", justifyContent: "center" }, + React.createElement(IconButton, { + size: "md", + bg: "#e6e6f1", + color: "#4B49AC", + _hover: { bg: "#cbcbe8" }, + onClick: () => goToPage(visibleCurrentPage - 1), + isDisabled: visibleCurrentPage === 1, + icon: React.createElement(ChevronRightIcon), + "aria-label": "صفحه قبلی", + }), + isSimple + ? [ + showFirstButton ? renderPageButton(1) : null, + isEditing + ? React.createElement( + React.Fragment, + { key: "input" }, + renderInput(), + ) + : renderPageButton(visibleCurrentPage, true), + showLastButton ? renderPageButton(safeTotalPages) : null, + ] + : [ + ...pageNumbers.map((number) => + renderPageButton(number, safeCurrentPage === number), + ), + config.showInput + ? React.createElement( + React.Fragment, + { key: "input" }, + renderInput(), + ) + : null, + ], + React.createElement(IconButton, { + size: "md", + bg: "#e6e6f1", + color: "#4B49AC", + _hover: { bg: "#cbcbe8" }, + onClick: () => goToPage(visibleCurrentPage + 1), + isDisabled: visibleCurrentPage === safeTotalPages, + icon: React.createElement(ChevronLeftIcon), + "aria-label": "صفحه بعدی", + }), + ), + config.showTotalCount && + hasTotalCount && + React.createElement( + HStack, + { spacing: 2, whiteSpace: "nowrap" }, + React.createElement( + Text, + { w: "auto", fontWeight: "bold" }, + "تعداد کل:", + ), + React.createElement(Text, null, toFaDigits(safeTotalCount)), + ), + ); +} + +class PaginationElement extends HTMLElement { + constructor() { + super(); + + this._props = {}; + this._root = null; + } + + connectedCallback() { + const { createRoot } = getRemoteDeps(); + + if (!this._root) { + this._root = createRoot(this); + } + + this.renderComponent(); + } + + disconnectedCallback() { + if (this._root) { + this._root.unmount(); + this._root = null; + } + } + + set componentProps(value) { + this._props = value ?? {}; + + if (this.isConnected) { + this.renderComponent(); + } + } + + get componentProps() { + return this._props; + } + + renderComponent() { + if (!this._root) return; + + const { + React, + ChakraProvider, + theme: defaultTheme, + chakraProviderProps: defaultProviderProps, + } = getRemoteDeps(); + + const { theme, chakraProviderProps, ...paginationProps } = + this._props || {}; + + this._root.render( + React.createElement( + ChakraProvider, + { + theme: theme || defaultTheme, + resetCSS: false, + ...(defaultProviderProps || {}), + ...(chakraProviderProps || {}), + }, + React.createElement(RemotePagination, paginationProps), + ), + ); + } +} + +if (!customElements.get("uikit-pagination")) { + customElements.define("uikit-pagination", PaginationElement); +} diff --git a/src/remote/ui/index.d.ts b/src/remote/ui/index.d.ts index 1b22ad1..dc44fd1 100644 --- a/src/remote/ui/index.d.ts +++ b/src/remote/ui/index.d.ts @@ -1,23 +1,54 @@ -import type { ComponentType, ReactNode } from "react"; +import type { BadgeProps, StackProps } from "@chakra-ui/react"; +import type { ReactNode } from "react"; -export type UikitRemoteDepsOptions = { - React?: unknown; - createRoot?: unknown; - ChakraProvider?: unknown; - Badge?: unknown; - theme?: unknown; - chakraProviderProps?: Record; +export type UikitRemoteLevel = "full" | "compact" | "simple"; + +export interface UikitRemoteSetupOptions { + React?: any; + createRoot?: any; + ChakraProvider?: any; + Badge?: any; + Button?: any; + HStack?: any; + IconButton?: any; + Input?: any; + Text?: any; + theme?: any; + chakraProviderProps?: Record; + + /** Backward compatible alias for GBadgeUrl. */ remoteUrl?: string; -}; + GBadgeUrl?: string; + PaginationUrl?: string; + remoteUrls?: { + GBadge?: string; + Pagination?: string; + }; +} -export declare function setupUikitRemoteDeps(options?: UikitRemoteDepsOptions): void; +export declare function setupUikitRemoteDeps( + options?: UikitRemoteSetupOptions, +): void; -export type GBadgeProps = { +export interface GBadgeProps extends BadgeProps { children?: ReactNode; remoteUrl?: string; - theme?: unknown; - chakraProviderProps?: Record; - [key: string]: unknown; -}; + theme?: any; + chakraProviderProps?: Record; +} -export declare const GBadge: ComponentType; +export declare const GBadge: (props: GBadgeProps) => JSX.Element; + +export interface PaginationProps extends Omit { + currentPage?: number; + totalCount?: number; + totalPages?: number; + pageSize?: number; + onPageChange?: (page: number) => void; + level?: UikitRemoteLevel; + remoteUrl?: string; + theme?: any; + chakraProviderProps?: Record; +} + +export declare const Pagination: (props: PaginationProps) => JSX.Element; diff --git a/src/remote/ui/index.jsx b/src/remote/ui/index.jsx index dd7a2ac..313e432 100644 --- a/src/remote/ui/index.jsx +++ b/src/remote/ui/index.jsx @@ -2,14 +2,40 @@ import React, { useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; -import { Badge, ChakraProvider } from "@chakra-ui/react"; +import { + Badge, + Button, + ChakraProvider, + HStack, + IconButton, + Input, + Text, +} from "@chakra-ui/react"; + +const DEFAULT_REMOTE_URLS = { + GBadge: "https://uikit.d.aiengines.ir/GBadge.js", + Pagination: "https://uikit.d.aiengines.ir/Pagination.js", +}; -const DEFAULT_REMOTE_URL = "https://uikit.d.aiengines.ir/GBadge.js"; const REMOTE_DEPS_KEY = "__UIKIT_REMOTE_DEPS__"; const REMOTE_CONFIG_KEY = "__UIKIT_REMOTE_CONFIG__"; const remotePromises = new Map(); +const REQUIRED_DEPS = { + GBadge: ["React", "createRoot", "ChakraProvider", "Badge"], + Pagination: [ + "React", + "createRoot", + "ChakraProvider", + "Button", + "HStack", + "IconButton", + "Input", + "Text", + ], +}; + export function setupUikitRemoteDeps(options = {}) { if (typeof window === "undefined") return; @@ -23,6 +49,11 @@ export function setupUikitRemoteDeps(options = {}) { ChakraProvider: options.ChakraProvider || previousDeps.ChakraProvider || ChakraProvider, Badge: options.Badge || previousDeps.Badge || Badge, + Button: options.Button || previousDeps.Button || Button, + HStack: options.HStack || previousDeps.HStack || HStack, + IconButton: options.IconButton || previousDeps.IconButton || IconButton, + Input: options.Input || previousDeps.Input || Input, + Text: options.Text || previousDeps.Text || Text, theme: Object.prototype.hasOwnProperty.call(options, "theme") ? options.theme : previousDeps.theme, @@ -32,54 +63,65 @@ export function setupUikitRemoteDeps(options = {}) { }, }; + const remoteUrls = options.remoteUrls || {}; + window[REMOTE_CONFIG_KEY] = { ...previousConfig, ...(options.remoteUrl ? { GBadgeUrl: options.remoteUrl } : {}), + ...(options.GBadgeUrl ? { GBadgeUrl: options.GBadgeUrl } : {}), + ...(options.PaginationUrl ? { PaginationUrl: options.PaginationUrl } : {}), + ...(remoteUrls.GBadge ? { GBadgeUrl: remoteUrls.GBadge } : {}), + ...(remoteUrls.Pagination ? { PaginationUrl: remoteUrls.Pagination } : {}), }; } -function ensureRemoteDeps() { +function ensureRemoteDeps(componentName) { if (typeof window === "undefined") return; const deps = window[REMOTE_DEPS_KEY]; + const requiredDeps = REQUIRED_DEPS[componentName] || []; + const hasMissingDeps = requiredDeps.some((key) => !deps?.[key]); - if ( - !deps?.React || - !deps?.createRoot || - !deps?.ChakraProvider || - !deps?.Badge - ) { + if (hasMissingDeps) { setupUikitRemoteDeps(); } } -function getRemoteUrl() { - if (typeof window === "undefined") return DEFAULT_REMOTE_URL; +function getRemoteUrl(componentName, explicitRemoteUrl) { + if (explicitRemoteUrl) return explicitRemoteUrl; - return window[REMOTE_CONFIG_KEY]?.GBadgeUrl || DEFAULT_REMOTE_URL; + if (typeof window === "undefined") { + return DEFAULT_REMOTE_URLS[componentName]; + } + + return ( + window[REMOTE_CONFIG_KEY]?.[`${componentName}Url`] || + DEFAULT_REMOTE_URLS[componentName] + ); } -function loadRemote() { - const remoteUrl = getRemoteUrl(); +function loadRemote(componentName, explicitRemoteUrl) { + const remoteUrl = getRemoteUrl(componentName, explicitRemoteUrl); + const cacheKey = `${componentName}:${remoteUrl}`; - if (!remotePromises.has(remoteUrl)) { + if (!remotePromises.has(cacheKey)) { remotePromises.set( - remoteUrl, + cacheKey, import( /* webpackIgnore: true */ /* @vite-ignore */ remoteUrl ).catch((error) => { - remotePromises.delete(remoteUrl); + remotePromises.delete(cacheKey); throw error; }), ); } - return remotePromises.get(remoteUrl); + return remotePromises.get(cacheKey); } -function normalizeChildren(children) { +function normalizeBadgeChildren(children) { if (typeof children === "string" || typeof children === "number") { return children; } @@ -95,72 +137,89 @@ function normalizeChildren(children) { return undefined; } -export const GBadge = ({ - children, - remoteUrl, - theme, - chakraProviderProps, - ...props -}) => { - const containerRef = useRef(null); - const elementRef = useRef(null); - const latestPropsRef = useRef(null); +function createRemoteElementComponent({ + componentName, + tagName, + normalizeProps, +}) { + return function RemoteElementComponent({ + remoteUrl, + theme, + chakraProviderProps, + ...props + }) { + const containerRef = useRef(null); + const elementRef = useRef(null); + const latestPropsRef = useRef(null); - latestPropsRef.current = { - ...props, - children: normalizeChildren(children), - ...(theme !== undefined ? { theme } : {}), - ...(chakraProviderProps ? { chakraProviderProps } : {}), - }; - - useEffect(() => { - let active = true; - - ensureRemoteDeps(); - - if (remoteUrl) { - setupUikitRemoteDeps({ remoteUrl }); - } - - loadRemote() - .then(() => { - if (!active) return; - if (!containerRef.current) return; - - const element = document.createElement("uikit-g-badge"); - - element.componentProps = latestPropsRef.current; - - containerRef.current.appendChild(element); - - elementRef.current = element; - }) - .catch((error) => { - console.error("[uikit/remote/ui] Failed to load GBadge:", error); - }); - - return () => { - active = false; - - if (elementRef.current) { - elementRef.current.remove(); - elementRef.current = null; - } + latestPropsRef.current = { + ...(normalizeProps ? normalizeProps(props) : props), + ...(theme !== undefined ? { theme } : {}), + ...(chakraProviderProps ? { chakraProviderProps } : {}), }; - }, [remoteUrl]); - useEffect(() => { - if (!elementRef.current) return; + useEffect(() => { + let active = true; - elementRef.current.componentProps = latestPropsRef.current; - }); + ensureRemoteDeps(componentName); - return ( - - ); -}; + loadRemote(componentName, remoteUrl) + .then(() => { + if (!active) return; + if (!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, remoteUrl, tagName]); + + useEffect(() => { + if (!elementRef.current) return; + + elementRef.current.componentProps = latestPropsRef.current; + }); + + return ( + + ); + }; +} + +export const GBadge = createRemoteElementComponent({ + componentName: "GBadge", + tagName: "uikit-g-badge", + normalizeProps: ({ children, ...props }) => ({ + ...props, + children: normalizeBadgeChildren(children), + }), +}); + +export const Pagination = createRemoteElementComponent({ + componentName: "Pagination", + tagName: "uikit-pagination", +}); diff --git a/src/ui/Pagination.jsx b/src/ui/Pagination.jsx new file mode 100644 index 0000000..390b03d --- /dev/null +++ b/src/ui/Pagination.jsx @@ -0,0 +1,449 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Button, HStack, IconButton, Input, Text } from "@chakra-ui/react"; + +const LEVELS = { + full: { + showPageSummary: true, + showTotalCount: true, + showInput: true, + inputWidth: "100px", + justifyContent: "space-around", + pageButtonPx: undefined, + getPageNumbers: ({ currentPage, totalPages }) => [ + 1, + currentPage, + currentPage + 1, + totalPages, + ], + }, + + compact: { + showPageSummary: false, + showTotalCount: true, + showInput: true, + inputWidth: "56px", + justifyContent: "space-around", + pageButtonPx: 2, + getPageNumbers: ({ currentPage, totalPages }) => [ + 1, + currentPage, + totalPages, + ], + }, + + simple: { + showPageSummary: false, + showTotalCount: false, + showInput: false, + inputWidth: "48px", + justifyContent: "center", + pageButtonPx: undefined, + getPageNumbers: () => [], + }, +}; + +const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]; +const PERSIAN_DIGIT_REGEX = /[۰-۹]/g; +const ARABIC_DIGIT_REGEX = /[٠-٩]/g; + +const toFaDigits = (value) => { + if (value === undefined || value === null) return ""; + + return String(value).replace(/\d/g, (digit) => PERSIAN_DIGITS[Number(digit)]); +}; + +const toEnDigits = (value) => { + if (value === undefined || value === null) return ""; + + return String(value) + .replace(PERSIAN_DIGIT_REGEX, (digit) => + String(PERSIAN_DIGITS.indexOf(digit)), + ) + .replace(ARABIC_DIGIT_REGEX, (digit) => String(digit.charCodeAt(0) - 1632)); +}; + +const normalizeLevel = (level) => { + if (LEVELS[level]) return level; + + return "full"; +}; + +const getSafePage = (page, totalPages) => { + const numericPage = Number(page); + const numericTotalPages = Number(totalPages); + + if (!numericTotalPages || numericTotalPages <= 0) return 1; + if (!numericPage || Number.isNaN(numericPage)) return 1; + + return Math.min(Math.max(numericPage, 1), numericTotalPages); +}; + +const ChevronRightIcon = (props) => ( + +); + +const ChevronLeftIcon = (props) => ( + +); + +export function Pagination({ + currentPage = 1, + totalCount = 0, + totalPages, + pageSize = 10, + onPageChange, + level = "full", + ...containerProps +}) { + const resolvedLevel = normalizeLevel(level); + const config = LEVELS[resolvedLevel]; + const isSimple = resolvedLevel === "simple"; + + const numericPageSize = Number(pageSize); + const safePageSize = + Number.isFinite(numericPageSize) && numericPageSize > 0 + ? numericPageSize + : 10; + + const numericTotalCount = Number(totalCount); + const safeTotalCount = + Number.isFinite(numericTotalCount) && numericTotalCount > 0 + ? numericTotalCount + : 0; + + const numericTotalPages = Number(totalPages); + const safeTotalPages = + Number.isFinite(numericTotalPages) && numericTotalPages > 0 + ? Math.ceil(numericTotalPages) + : Math.ceil(safeTotalCount / safePageSize); + const safeCurrentPage = getSafePage(currentPage, safeTotalPages); + + const [inputValue, setInputValue] = useState( + isSimple ? "" : toFaDigits(safeCurrentPage), + ); + const [isEditing, setIsEditing] = useState(false); + const [pendingPage, setPendingPage] = useState(null); + const inputRef = useRef(null); + + const visibleCurrentPage = pendingPage ?? safeCurrentPage; + const hasTotalCount = safeTotalCount > 0; + + const pageNumbers = useMemo(() => { + if (isSimple || safeTotalPages <= 0) return []; + + return Array.from( + new Set( + config.getPageNumbers({ + currentPage: safeCurrentPage, + totalPages: safeTotalPages, + }), + ), + ) + .filter((page) => page >= 1 && page <= safeTotalPages) + .sort((a, b) => a - b); + }, [config, isSimple, safeCurrentPage, safeTotalPages]); + + const resetInput = useCallback(() => { + setInputValue(isSimple ? "" : toFaDigits(safeCurrentPage)); + setIsEditing(false); + setPendingPage(null); + }, [isSimple, safeCurrentPage]); + + const goToPage = useCallback( + (page) => { + const safePage = getSafePage(page, safeTotalPages); + + setIsEditing(false); + setInputValue(isSimple ? "" : toFaDigits(safeCurrentPage)); + setPendingPage(null); + + if (safePage !== safeCurrentPage) { + onPageChange?.(safePage); + } + }, + [isSimple, onPageChange, safeCurrentPage, safeTotalPages], + ); + + const commitInput = useCallback(() => { + if (!inputValue) { + resetInput(); + inputRef.current?.blur(); + return; + } + + const page = parseInt(toEnDigits(inputValue), 10); + + if (Number.isNaN(page)) { + resetInput(); + inputRef.current?.blur(); + return; + } + + const safePage = getSafePage(page, safeTotalPages); + + if (isSimple) { + setIsEditing(false); + setInputValue(""); + + if (safePage !== safeCurrentPage) { + setPendingPage(safePage); + onPageChange?.(safePage); + } else { + setPendingPage(null); + } + + inputRef.current?.blur(); + return; + } + + if (safePage !== safeCurrentPage) { + setPendingPage(safePage); + setInputValue(toFaDigits(safePage)); + onPageChange?.(safePage); + } else { + setInputValue(toFaDigits(safeCurrentPage)); + setIsEditing(false); + setPendingPage(null); + } + + inputRef.current?.blur(); + }, [ + inputValue, + isSimple, + onPageChange, + resetInput, + safeCurrentPage, + safeTotalPages, + ]); + + const startEditing = useCallback(() => { + setPendingPage(null); + setInputValue(""); + setIsEditing(true); + }, []); + + const handleInputChange = useCallback((event) => { + const rawValue = event.target.value; + const onlyDigits = toEnDigits(rawValue).replace(/\D/g, ""); + + setInputValue(toFaDigits(onlyDigits)); + }, []); + + useEffect(() => { + if (isSimple) { + if (pendingPage !== null && safeCurrentPage === pendingPage) { + setPendingPage(null); + } + + return; + } + + if (pendingPage !== null) { + if (safeCurrentPage === pendingPage) { + setPendingPage(null); + setIsEditing(false); + setInputValue(toFaDigits(safeCurrentPage)); + } + + return; + } + + if (!isEditing) { + setInputValue(toFaDigits(safeCurrentPage)); + } + }, [isEditing, isSimple, pendingPage, safeCurrentPage]); + + useEffect(() => { + if (isSimple && isEditing) { + inputRef.current?.focus(); + } + }, [isEditing, isSimple]); + + useEffect(() => { + if (!isEditing) return; + if (!inputValue) return; + if (!isSimple && pendingPage !== null) return; + + const timeout = setTimeout(() => { + commitInput(); + }, 1000); + + return () => clearTimeout(timeout); + }, [commitInput, inputValue, isEditing, isSimple, pendingPage]); + + if (safeTotalPages <= 0) return null; + + const renderInput = () => ( + { + if (!isSimple) { + setIsEditing(true); + setPendingPage(null); + setInputValue(""); + } + }} + onChange={handleInputChange} + onBlur={() => { + if (inputValue) { + if (isSimple) { + commitInput(); + } + + return; + } + + resetInput(); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + commitInput(); + } + + if (event.key === "Escape") { + resetInput(); + } + }} + textAlign="center" + color="#1D2939" + px={1} + minW={0} + dir="ltr" + /> + ); + + const renderPageButton = (number, isCurrent = false) => ( + + ); + + const showFirstButton = + isSimple && safeTotalPages > 1 && visibleCurrentPage !== 1; + const showLastButton = + isSimple && safeTotalPages > 1 && visibleCurrentPage !== safeTotalPages; + + return ( + + {config.showPageSummary && ( + + صفحه + {toFaDigits(safeCurrentPage)} + از + {toFaDigits(safeTotalPages)} + + )} + + + goToPage(visibleCurrentPage - 1)} + isDisabled={visibleCurrentPage === 1} + icon={} + aria-label="صفحه قبلی" + /> + + {isSimple ? ( + <> + {showFirstButton && renderPageButton(1)} + + {isEditing + ? renderInput() + : renderPageButton(visibleCurrentPage, true)} + + {showLastButton && renderPageButton(safeTotalPages)} + + ) : ( + <> + {pageNumbers.map((number) => + renderPageButton(number, safeCurrentPage === number), + )} + + {config.showInput && renderInput()} + + )} + + goToPage(visibleCurrentPage + 1)} + isDisabled={visibleCurrentPage === safeTotalPages} + icon={} + aria-label="صفحه بعدی" + /> + + + {config.showTotalCount && hasTotalCount && ( + + + تعداد کل: + + {toFaDigits(safeTotalCount)} + + )} + + ); +} + +export default Pagination; diff --git a/vite.config.js b/vite.config.js index e99466d..eef959b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -43,8 +43,21 @@ export default defineConfig(({ mode }) => { target: "es2020", + minify: "esbuild", + + sourcemap: false, + lib: { - entry: path.resolve(process.cwd(), "src/remote/ui/GBadge.remote.jsx"), + entry: { + GBadge: path.resolve( + process.cwd(), + "src/remote/ui/GBadge.remote.jsx", + ), + Pagination: path.resolve( + process.cwd(), + "src/remote/ui/Pagination.remote.jsx", + ), + }, formats: ["es"], }, @@ -53,9 +66,11 @@ export default defineConfig(({ mode }) => { treeshake: true, output: { - entryFileNames: "remote/GBadge.js", + entryFileNames: "remote/[name].js", - inlineDynamicImports: true, + chunkFileNames: "remote/chunks/[name]-[hash].js", + + assetFileNames: "remote/assets/[name]-[hash][extname]", }, }, }, @@ -94,6 +109,7 @@ export default defineConfig(({ mode }) => { external: (id) => { return ( externalPackages.includes(id) || + id.startsWith("react-dom/") || id.startsWith("react-icons/") || id.startsWith("@emotion/") || id.startsWith("react-select/")