/* eslint-disable no-redeclare */ import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; import { token2CSSVar, useCSSVarRegister, useStyleRegister } from '../../_util/cssinjs'; import warning from '../../_util/warning'; import { genCommonStyle, genLinkStyle } from '../../style'; import type { AliasToken, ComponentTokenMap, GlobalToken, OverrideToken, UseComponentStyleResult, } from '../interface'; import useToken, { ignore, unitless } from '../useToken'; import genCalc from './calc'; import type AbstractCalculator from './calc/calculator'; import genMaxMin from './maxmin'; import statisticToken, { merge as mergeToken } from './statistic'; import useResetIconStyle from './useResetIconStyle'; import type { Ref } from 'vue'; import { defineComponent, computed, createVNode, Fragment } from 'vue'; import { useConfigContextInject } from '../../config-provider/context'; import type { VueNode } from 'ant-design-vue/es/_util/type'; import { objectType } from 'ant-design-vue/es/_util/type'; export type OverrideTokenWithoutDerivative = ComponentTokenMap; export type OverrideComponent = keyof OverrideTokenWithoutDerivative; export type GlobalTokenWithComponent = GlobalToken & ComponentTokenMap[C]; type ComponentToken = Exclude; type ComponentTokenKey = keyof ComponentToken; export interface StyleInfo { hashId: string; prefixCls: string; rootPrefixCls: string; iconPrefixCls: string; } export type CSSUtil = { calc: (number: any) => AbstractCalculator; max: (...values: (number | string)[]) => number | string; min: (...values: (number | string)[]) => number | string; }; export type TokenWithCommonCls = T & { /** Wrap component class with `.` prefix */ componentCls: string; /** Origin prefix which do not have `.` prefix */ prefixCls: string; /** Wrap icon class with `.` prefix */ iconCls: string; /** Wrap ant prefixCls class with `.` prefix */ antCls: string; } & CSSUtil; export type FullToken = TokenWithCommonCls< GlobalTokenWithComponent >; export type GenStyleFn = ( token: FullToken, info: StyleInfo, ) => CSSInterpolation; export type GetDefaultToken = | null | OverrideTokenWithoutDerivative[C] | (( token: AliasToken & Partial, ) => OverrideTokenWithoutDerivative[C]); const getDefaultComponentToken = ( component: C, token: Ref, getDefaultToken: GetDefaultToken, ) => { if (typeof getDefaultToken === 'function') { return getDefaultToken(mergeToken(token.value, token.value[component] ?? {})); } return getDefaultToken ?? {}; }; const getComponentToken = ( component: C, token: Ref, defaultToken: OverrideTokenWithoutDerivative[C], options?: { deprecatedTokens?: [ComponentTokenKey, ComponentTokenKey][]; }, ) => { const customToken = { ...(token.value[component] as ComponentToken) }; if (options?.deprecatedTokens) { const { deprecatedTokens } = options; deprecatedTokens.forEach(([oldTokenKey, newTokenKey]) => { if (process.env.NODE_ENV !== 'production') { warning( !customToken?.[oldTokenKey], `Component Token \`${String( oldTokenKey, )}\` of ${component} is deprecated. Please use \`${String(newTokenKey)}\` instead.`, ); } // Should wrap with `if` clause, or there will be `undefined` in object. if (customToken?.[oldTokenKey] || customToken?.[newTokenKey]) { customToken[newTokenKey] ??= customToken?.[oldTokenKey]; } }); } const mergedToken: any = { ...defaultToken, ...customToken }; // Remove same value as global token to minimize size Object.keys(mergedToken).forEach(key => { if (mergedToken[key] === token[key as keyof GlobalToken]) { delete mergedToken[key]; } }); return mergedToken; }; const getCompVarPrefix = (component: string, prefix?: string) => `${[ prefix, component.replace(/([A-Z]+)([A-Z][a-z]+)/g, '$1-$2').replace(/([a-z])([A-Z])/g, '$1-$2'), ] .filter(Boolean) .join('-')}`; export default function genComponentStyleHook( componentName: C | [C, string], styleFn: GenStyleFn, getDefaultToken?: GetDefaultToken, options: { resetStyle?: boolean; // Deprecated token key map [["oldTokenKey", "newTokenKey"], ["oldTokenKey", "newTokenKey"]] deprecatedTokens?: [ComponentTokenKey, ComponentTokenKey][]; /** * Only use component style in client side. Ignore in SSR. */ clientOnly?: boolean; /** * Set order of component style. Default is -999. */ order?: number; injectStyle?: boolean; } = {}, ) { const cells = (Array.isArray(componentName) ? componentName : [componentName, componentName]) as [ C, string, ]; const [component] = cells; const concatComponent = cells.join('-'); return (_prefixCls?: Ref): UseComponentStyleResult => { const prefixCls = computed(() => _prefixCls?.value); const [theme, realToken, hashId, token, cssVar] = useToken(); const { getPrefixCls, iconPrefixCls, csp } = useConfigContextInject(); const rootPrefixCls = computed(() => getPrefixCls()); const type = computed(() => (cssVar.value ? 'css' : 'js')); const calc = computed(() => genCalc(type.value)); const maxMin = computed(() => genMaxMin(type.value)); // Shared config // const sharedConfig: Omit[0], 'path'> = { // theme: theme.value, // token: token.value, // hashId: hashId.value, // nonce: () => csp.value?.nonce!, // clientOnly: options.clientOnly, // // antd is always at top of styles // order: options.order || -999, // }; const sharedConfig = computed(() => { return { theme: theme.value, token: token.value, hashId: hashId.value, nonce: () => csp.value && csp.value.nonce, clientOnly: options.clientOnly, // antd is always at top of styles order: options.order || -999, }; }); // Generate style for all a tags in antd component. useStyleRegister( computed(() => ({ ...sharedConfig.value, clientOnly: false, path: ['Shared', rootPrefixCls.value], })), () => [ { // Link '&': genLinkStyle(token.value), }, ] as CSSObject[], ); // Generate style for icons useResetIconStyle(iconPrefixCls, csp); const wrapSSR = useStyleRegister( computed(() => ({ ...sharedConfig.value, path: [concatComponent, prefixCls.value, iconPrefixCls.value], })), () => { if (options.injectStyle === false) { return []; } const { token: proxyToken, flush } = statisticToken(token.value); const defaultComponentToken = getDefaultComponentToken( component, realToken, getDefaultToken, ); const componentCls = `.${prefixCls.value}`; const componentToken = getComponentToken(component, realToken, defaultComponentToken, { deprecatedTokens: options.deprecatedTokens, }); if (cssVar.value) { Object.keys(defaultComponentToken).forEach(key => { defaultComponentToken[key] = `var(${token2CSSVar( key, getCompVarPrefix(component, cssVar.value?.prefix), )})`; }); } const mergedToken = mergeToken< TokenWithCommonCls> >( proxyToken, { componentCls, prefixCls: prefixCls.value, iconCls: `.${iconPrefixCls.value}`, antCls: `.${rootPrefixCls.value}`, calc: calc.value, max: maxMin.value.max, min: maxMin.value.min, }, cssVar.value ? defaultComponentToken : componentToken, ); const styleInterpolation = styleFn(mergedToken as unknown as FullToken, { hashId: hashId.value, prefixCls: prefixCls.value, rootPrefixCls: rootPrefixCls.value, iconPrefixCls: iconPrefixCls.value, }); flush(component, componentToken); return [ options.resetStyle === false ? null : genCommonStyle(mergedToken, prefixCls.value), styleInterpolation, ] as CSSObject[]; }, ); return [wrapSSR, hashId]; }; } export interface SubStyleComponentProps { prefixCls: Ref; } // Get from second argument type RestParameters = T extends [any, ...infer Rest] ? Rest : never; export const genSubStyleComponent: ( componentName: [C, string], ...args: RestParameters>> ) => any = (componentName, styleFn, getDefaultToken, options) => { const useStyle = genComponentStyleHook(componentName, styleFn, getDefaultToken, { resetStyle: false, // Sub Style should default after root one order: -998, ...options, }); const StyledComponent = defineComponent({ props: { prefixCls: String, }, setup(props) { const prefixCls = computed(() => props.prefixCls); useStyle(prefixCls); return () => { return null; }; }, }); return StyledComponent; }; export type CSSVarRegisterProps = { rootCls: string; component: string; cssVar: { prefix?: string; key?: string; }; }; const genCSSVarRegister = ( component: C, getDefaultToken?: GetDefaultToken, options?: { unitless?: { [key in ComponentTokenKey]: boolean; }; deprecatedTokens?: [ComponentTokenKey, ComponentTokenKey][]; injectStyle?: boolean; }, ) => { function prefixToken(key: string) { return `${component}${key.slice(0, 1).toUpperCase()}${key.slice(1)}`; } const { unitless: originUnitless = {}, injectStyle = true } = options ?? {}; const compUnitless: any = { [prefixToken('zIndexPopup')]: true, }; Object.keys(originUnitless).forEach((key: keyof ComponentTokenKey) => { compUnitless[prefixToken(key)] = originUnitless[key]; }); const CSSVarRegister = defineComponent({ props: { rootCls: String, component: String, cssVar: objectType<{ prefix: string; key: string; }>(), }, setup(props) { const [, realToken] = useToken(); useCSSVarRegister( computed(() => { return { path: [props.component], prefix: props.cssVar.prefix, key: props.cssVar.key!, unitless: { ...unitless, ...compUnitless, }, ignore, token: realToken.value, scope: props.rootCls, }; }), () => { const defaultToken = getDefaultComponentToken(component, realToken, getDefaultToken); const componentToken = getComponentToken(component, realToken, defaultToken, { deprecatedTokens: options?.deprecatedTokens, }); Object.keys(defaultToken).forEach(key => { componentToken[prefixToken(key)] = componentToken[key]; delete componentToken[key]; }); return componentToken; }, ); return () => { return null; }; }, }); const useCSSVar = (rootCls: Ref) => { const [, , , , cssVar] = useToken(); return [ (node: VueNode): VueNode => injectStyle && cssVar.value ? createVNode(Fragment, null, [ createVNode(CSSVarRegister, { rootCls: rootCls.value, cssVar: cssVar.value, component, }), node, ]) : node, computed(() => cssVar.value?.key), ] as const; }; return useCSSVar; }; export const genStyleHooks = ( component: C | [C, string], styleFn: GenStyleFn, getDefaultToken?: GetDefaultToken, options?: { resetStyle?: boolean; deprecatedTokens?: [ComponentTokenKey, ComponentTokenKey][]; /** * Component tokens that do not need unit. */ unitless?: { [key in ComponentTokenKey]: boolean; }; /** * Only use component style in client side. Ignore in SSR. */ clientOnly?: boolean; /** * Set order of component style. * @default -999 */ order?: number; /** * Whether generate styles * @default true */ injectStyle?: boolean; }, ) => { const useStyle = genComponentStyleHook(component, styleFn, getDefaultToken, options); const useCSSVar = genCSSVarRegister( Array.isArray(component) ? component[0] : component, getDefaultToken, options, ); return (prefixCls: Ref, rootCls: Ref = prefixCls) => { const [, hashId] = useStyle(prefixCls); const [wrapCSSVar, cssVarCls] = useCSSVar(rootCls); return [wrapCSSVar, hashId, cssVarCls] as const; }; };