import {ContextType, createContext, Component, useContext} from 'react' import {isUndefined} from 'lodash-es' import {Json, safeInvoke} from '@befe/brick-utils' import {ComponentLocale, ComponentLocaleDict, getI18nSingleton, I18n, LocaleDict, LocaleTextReplacement} from '../i18n' import {ThemeConfig, ThemeConfigWithDefaults, getThemeConfigSingleton} from '../theme' // @todo 这个应该来自 sack 中规范化的 backend-frontend API type interface RespJson { status?: 'ok' | 'fail' message?: string } export interface ConfigContextValue { /** * 主题相关基础配置 */ theme?: ThemeConfig /** * 获取本地化文本 */ getLocaleText?: I18n['getLocaleText'] /** * request 响应成功的判断函数 */ isResponseSuccess?: (respJson: Json | undefined, respText: string) => boolean /** * request 响应的消息提取器 */ getResponseMessage?: (respJson: Json | undefined, respText: string) => string /** * 字数计数函数 */ textCounter?: (text: string) => number /** * @private * 是否为根 context * - 默认值 true,经过 ConfigProvider 之后恒为 false,亦即只有第一个 ConfigProvider 的 context.isRoot 为 true * - 只有根 context 可以更新 themeSingleton */ isRoot?: boolean /** * 包裹 wrap 的 类名,用于 "添加 scope 层次以增强特指度" 的样式隔离方案 * * 相对于不提供的,如果提供了 wrapClassName e.g. wrapClassName="wrap-name" * - children 将被 wrap 到一个 div.wrap-name 中 * - mount 到 body 的 popup portal 的 children 会被到一个 div.wrap-name 中 * * 需要搭配利用 `@include meta.load-css()` 将样式 wrap 到相应的 .wrap-class {} 中进行 sass 编译后的样式 */ wrapClassName?: string | null } /** * 多 root 场景下,需要另行传递的无法在 root 间自行取值的, config context * * 如应用样式隔离方案时, confirm(), alert(), toast(), message(), notification() 等所需要的 wrapClassName */ export type ConfigContextValueOffBoard = Pick type ConfigContextValueWithDefaults = Required & { theme: ThemeConfigWithDefaults } function parseHTMLMessage(respText: string) { const domParser = new DOMParser() const doc = domParser.parseFromString(respText, 'text/html') try { return doc.body.innerText.trim() } catch { // textContent for test // - https://github.com/jsdom/jsdom/issues/1245 // - https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent return doc.body.textContent || '' } } const i18n = getI18nSingleton() const theme = getThemeConfigSingleton() export const defaultConfig: ConfigContextValueWithDefaults = { theme: theme.config, getLocaleText: (...args) => i18n.getLocaleText(...args), isResponseSuccess: (respJson) => { return !!(respJson && (respJson as RespJson).status === 'ok') }, getResponseMessage: (respJson, respText) => { return (respJson ? (respJson as RespJson).message : parseHTMLMessage(respText)) || '' }, textCounter: (text = '') => (text || '').length, isRoot: true, wrapClassName: null, } export const ConfigContext = createContext(defaultConfig) export const ConfigConsumer = ConfigContext.Consumer export function getValueFromContextTheme( context: ContextType, themePropName: TN ) { const value = (context.theme as ThemeConfigWithDefaults)?.[themePropName] return isUndefined(value) ? defaultConfig.theme[themePropName] : value } export function getIconSetFromContextTheme(context: ContextType) { return { info: getValueFromContextTheme(context, 'alertIconInfo'), success: getValueFromContextTheme(context, 'alertIconSuccess'), warning: getValueFromContextTheme(context, 'alertIconWarning'), error: getValueFromContextTheme(context, 'alertIconError'), } } export function getDefaultValueUsingContextTheme< Props, N extends keyof Props, TN extends keyof ThemeConfigWithDefaults >( componentInstance: Component, propName: N, themePropName: TN ) { const propValue = componentInstance.props[propName] if (!isUndefined(propValue)) { return propValue as Required[N] } return getValueFromContextTheme(componentInstance.context as ContextType, themePropName) } export function useLocale(localeDict: ComponentLocale) { const configContext = useContext(ConfigContext) return { getLocaleText: (key: string, ...args: LocaleTextReplacement[]) => { return safeInvoke(configContext.getLocaleText, localeDict, key, ...args); }, } }