amis/src/theme.tsx

166 lines
3.9 KiB
TypeScript
Raw Normal View History

2019-04-30 11:11:25 +08:00
// 主题管理
2019-09-09 13:48:08 +08:00
import cx from 'classnames';
2019-06-10 22:53:21 +08:00
import React from 'react';
2020-02-29 09:59:17 +08:00
import hoistNonReactStatic from 'hoist-non-react-statics';
2019-09-09 13:48:08 +08:00
import {ExtractProps, Omit} from './types';
2019-04-30 11:11:25 +08:00
2019-11-07 10:41:14 +08:00
export type ClassValue =
| string
| number
| ClassDictionary
| ClassArray
| undefined
| null
| boolean;
2019-04-30 11:11:25 +08:00
interface ClassDictionary {
2019-11-07 10:41:14 +08:00
[id: string]: any;
2019-04-30 11:11:25 +08:00
}
// This is the only way I found to break circular references between ClassArray and ClassValue
// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540
2019-09-09 13:48:08 +08:00
interface ClassArray extends Array<ClassValue> {} // tslint:disable-line no-empty-interface
2019-04-30 11:11:25 +08:00
export type ClassNamesFn = (...classes: ClassValue[]) => string;
interface ThemeConfig {
2019-11-07 10:41:14 +08:00
classPrefix?: string;
renderers?: {
[propName: string]: any;
};
2019-04-30 11:11:25 +08:00
2019-11-07 10:41:14 +08:00
[propsName: string]: any;
2019-09-09 13:48:08 +08:00
}
2019-04-30 11:11:25 +08:00
2019-09-09 13:48:08 +08:00
const themes: {
2019-11-07 10:41:14 +08:00
[propName: string]: ThemeConfig;
2019-04-30 11:11:25 +08:00
} = {
2019-11-07 10:41:14 +08:00
default: {}
2019-04-30 11:11:25 +08:00
};
2019-09-09 13:48:08 +08:00
export function theme(name: string, config: Partial<ThemeConfig>) {
2019-11-07 10:41:14 +08:00
themes[name] = {
...config
};
2019-04-30 11:11:25 +08:00
}
2019-09-09 13:48:08 +08:00
const fns: {
2019-11-07 10:41:14 +08:00
[propName: string]: (...classes: ClassValue[]) => string;
2019-04-30 11:11:25 +08:00
} = {};
2019-09-09 13:48:08 +08:00
export function makeClassnames(ns?: string) {
2019-11-07 10:41:14 +08:00
if (ns && fns[ns]) {
return fns[ns];
}
const fn = (...classes: ClassValue[]) => {
const str = cx(...(classes as any));
return str && ns
? str
.replace(/(^|\s)([A-Z])/g, '$1' + ns + '$2')
.replace(/(^|\s)\:/g, '$1')
: str || '';
};
ns && (fns[ns] = fn);
return fn;
2019-04-30 11:11:25 +08:00
}
export type ThemeInstance = ThemeConfig & {
2019-11-07 10:41:14 +08:00
getRendererConfig: (name?: string) => any;
classnames: ClassNamesFn;
2019-04-30 11:11:25 +08:00
};
2019-09-09 13:48:08 +08:00
export function hasTheme(theme: string): boolean {
2019-11-07 10:41:14 +08:00
return !!themes[theme];
2019-04-30 11:11:25 +08:00
}
2019-09-09 13:48:08 +08:00
export function setDefaultTheme(theme: string) {
2019-11-07 10:41:14 +08:00
if (hasTheme(theme)) {
defaultTheme = theme;
}
2019-08-22 19:54:03 +08:00
}
export function classnames(...classes: ClassValue[]) {
2019-11-07 10:41:14 +08:00
return getTheme(defaultTheme).classnames(...classes);
2019-08-22 19:54:03 +08:00
}
export function getClassPrefix() {
2019-11-07 10:41:14 +08:00
return getTheme(defaultTheme).classPrefix;
2019-08-22 19:54:03 +08:00
}
2019-09-09 13:48:08 +08:00
export function getTheme(theme: string): ThemeInstance {
2019-11-07 10:41:14 +08:00
if (!themes[theme]) {
throw new Error(`Theme with name "${theme}" does not exist!`);
}
2019-04-30 11:11:25 +08:00
2019-11-07 10:41:14 +08:00
const config = themes[theme];
2019-04-30 11:11:25 +08:00
2019-11-07 10:41:14 +08:00
if (!config.getRendererConfig) {
config.getRendererConfig = (name?: string) =>
config.renderers && name ? config.renderers[name] : null;
}
2019-04-30 11:11:25 +08:00
2019-11-07 10:41:14 +08:00
if (!config.classnames) {
const ns = config.classPrefix;
config.classnames = config.classnames || makeClassnames(ns);
}
2019-04-30 11:11:25 +08:00
2019-11-07 10:41:14 +08:00
return config as ThemeInstance;
2019-04-30 11:11:25 +08:00
}
export interface ThemeProps {
2019-11-07 10:41:14 +08:00
classPrefix: string;
classnames: ClassNamesFn;
2019-04-30 11:11:25 +08:00
}
export const ThemeContext = React.createContext('theme');
2019-09-09 13:48:08 +08:00
export let defaultTheme: string = 'default';
2019-04-30 11:11:25 +08:00
2019-11-07 10:41:14 +08:00
export function themeable<
T extends React.ComponentType<ThemeProps & ExtractProps<T>>
>(ComposedComponent: T) {
type ComposedProps = JSX.LibraryManagedAttributes<T, ExtractProps<T>>;
type Props = Omit<ComposedProps, keyof ThemeProps> & {
theme?: string;
classPrefix?: string;
classnames?: ClassNamesFn;
};
class EnhancedComponent extends React.Component<Props> {
static displayName = `Themeable(${ComposedComponent.displayName ||
ComposedComponent.name})`;
static contextType = ThemeContext;
static ComposedComponent = ComposedComponent;
render() {
const theme: string = this.props.theme || this.context || defaultTheme;
const config = hasTheme(theme) ? getTheme(theme) : getTheme(defaultTheme);
const injectedProps: {
classPrefix: string;
classnames: ClassNamesFn;
} = {
classPrefix: config.classPrefix as string,
classnames: config.classnames
};
return (
<ThemeContext.Provider value={theme}>
<ComposedComponent
{
...this.props as any /* todo, 解决这个类型问题 */
}
{...injectedProps}
/>
</ThemeContext.Provider>
);
2019-04-30 11:11:25 +08:00
}
2019-11-07 10:41:14 +08:00
}
return hoistNonReactStatic(
EnhancedComponent,
ComposedComponent
) as React.ComponentClass<Props> & {
ComposedComponent: T;
};
2019-09-09 13:48:08 +08:00
}