Commit 15b21b11 by Vladislav Lagunov

Merge branch 'master' of ssh://git.bitmaster.ru:34022/npm/common

parents 3daa759d 695dd1fe
...@@ -45,6 +45,7 @@ export type Validation<A> = Partial<Record<keyof A, Error>>; ...@@ -45,6 +45,7 @@ export type Validation<A> = Partial<Record<keyof A, Error>>;
// Props // Props
export type CardProps<T> = { export type CardProps<T> = {
ctx?: any,
model: { model: {
initial: T; initial: T;
modified: T|null; modified: T|null;
...@@ -55,38 +56,35 @@ export type CardProps<T> = { ...@@ -55,38 +56,35 @@ export type CardProps<T> = {
// Опции для `createForm` // Опции для `createForm`
export type CreateFormOptions<T> = { export type CreateFormOptions<T> = {
validate?(value: T): any; validate?(value: T, instance): any;
disabled?(value: T): any; disabled?(value: T, instance): any;
}; };
export default function createForm<O extends CreateFormOptions<any>>(options: O) { export default function createForm<O extends CreateFormOptions<any>>(options: O) {
type T = O extends CreateFormOptions<infer T> ? T : never; type T = O extends CreateFormOptions<infer T> ? T : never;
function bindZoom(self: React.Component<CardProps<T>>) { function createZoom(self: React.Component<CardProps<T>>) {
function Zoom<K1 extends keyof T, C extends Any<T[K1]>>(...keys: [K1]): (Component: C, props?: GetProps<C>) => JSX.Element; function zoom(): FieldProps<T>;
function Zoom<K1 extends keyof T, K2 extends keyof T[K1], C extends Any<T[K1][K2]>>(...keys: [K1, K2]): (Component: C, props?: GetProps<C>) => JSX.Element; function zoom<K1 extends keyof T>(...keys: [K1]): FieldProps<T[K1]>;
function Zoom<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], C extends Any<T[K1][K2][K3]>>(...keys: [K1, K2, K3]): (Component: C, props?: GetProps<C>) => JSX.Element; function zoom<K1 extends keyof T, K2 extends keyof T[K1]>(...keys: [K1, K2]): FieldProps<T[K1][K2]>;
function Zoom(...keys) { function zoom<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(...keys: [K1, K2, K3]): FieldProps<T[K1][K2][K3]>;
return (Component, props={}) => { function zoom(...keys) {
const { dispatch } = self.props; const { dispatch, ctx } = self.props;
const value = self.props.model.modified || self.props.model.initial; const value = self.props.model.modified || self.props.model.initial;
const error = options.validate ? options.validate(value) : {}; const error = options.validate ? options.validate(value, self) : {};
const disabled = options.disabled ? options.disabled(value) : {}; const disabled = options.disabled ? options.disabled(value, self) : {};
const onValueChange = (value, at: ObjectPath=[]) => dispatch({ tag: 'Change', value, at }); const onValueChange = (value, at: ObjectPath=[]) => dispatch({ tag: 'Change', value, at });
const props_ = { return {
...props, ctx,
value: zoomAny(keys, value), value: zoomAny(keys, value),
error: zoomAny(keys, error), error: zoomAny(keys, error),
disabled: zoomAny(keys, disabled), disabled: zoomAny(keys, disabled),
onValueChange: zoomOnChange(keys, onValueChange), onValueChange: zoomOnChange(keys, onValueChange),
}; };
return React.createElement(Component, props_);
};
} }
return Zoom; return zoom;
} }
function set<M extends CardProps<T>['model']>(model: M, form: T): M { function set<M extends CardProps<T>['model']>(model: M, form: T): M {
...@@ -104,7 +102,7 @@ export default function createForm<O extends CreateFormOptions<any>>(options: O) ...@@ -104,7 +102,7 @@ export default function createForm<O extends CreateFormOptions<any>>(options: O)
} }
// @ts-ignore // @ts-ignore
return { ...options, bindZoom, set, get, modify } as { bindZoom: typeof bindZoom, set: typeof set, get: typeof get, modify: typeof modify } & O; return { ...options, createZoom, set, get, modify } as { createZoom: typeof createZoom, set: typeof set, get: typeof get, modify: typeof modify } & O;
} }
......
...@@ -5,7 +5,7 @@ import { AuthCtx as Ctx } from '~/context'; ...@@ -5,7 +5,7 @@ import { AuthCtx as Ctx } from '~/context';
import { FieldProps } from '~/create-form'; import { FieldProps } from '~/create-form';
import TextField from '~/fields/TextField'; import TextField from '~/fields/TextField';
import { Props as TextFieldProps } from '~/fields/TextField'; import { Props as TextFieldProps } from '~/fields/TextField';
import { StandardProps } from '@material-ui/core'; import { StandardProps, ListItem } from '@material-ui/core';
import Icon from './Icon'; import Icon from './Icon';
import withStyles, { StyleRules } from '@material-ui/core/styles/withStyles'; import withStyles, { StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme'; import { Theme } from '@material-ui/core/styles/createMuiTheme';
...@@ -15,10 +15,12 @@ import * as Rx from 'rxjs'; ...@@ -15,10 +15,12 @@ import * as Rx from 'rxjs';
import memoize from '~/functions/memoize'; import memoize from '~/functions/memoize';
import { pick } from 'lodash'; import { pick } from 'lodash';
import classNames = require('classnames'); import classNames = require('classnames');
import { withGettext, Gettext } from '~/gettext';
import { fade } from '@material-ui/core/styles/colorManipulator';
// Props // Props
export type Props<A> = StandardProps<React.HTMLProps<HTMLDivElement>, string, 'value'> & FieldProps<A> & { export type Props<A> = StandardProps<React.HTMLProps<HTMLDivElement>, string, 'value'|'disabled'> & FieldProps<A> & {
ctx?: Ctx; ctx?: Ctx;
source: Source<A>; source: Source<A>;
debounce?: number; debounce?: number;
...@@ -48,19 +50,23 @@ export type State<A> = AutoCompleteState<A> & { ...@@ -48,19 +50,23 @@ export type State<A> = AutoCompleteState<A> & {
// Component // Component
// @ts-ignore Хак для работы дженерик-параметров // @ts-ignore Хак для работы дженерик-параметров
@withStyles(styles) @withStyles(styles)
export default class AutoComplete<A=string> extends React.Component<Props<A>, State<A>> { // @ts-ignore
@withGettext(require('./i18n'))
export default class AutoComplete<A=string> extends React.Component<Props<A> & { __: Gettext }, State<A>> {
static defaultProps = { static defaultProps = {
openOnFocus: true, openOnFocus: true,
openOnClick: true, openOnClick: true,
fullWidth: true, fullWidth: true,
printItem: x => x ? String(x) : '', printItem: x => x ? String(x) : '',
} as any; } as any;
state: State<A> = { ...ST.init(), value: null }; state: State<A> = { ...ST.init(), value: null };
anchorEl: HTMLElement|null; // Ссылка на <input/> anchorEl: HTMLElement|null; // Ссылка на <input/>
rectEl: HTMLElement|null; // Ссылка на элемент для выравнивания, может быть тем же что и `inputEl` rectEl: HTMLElement|null; // Ссылка на элемент для выравнивания, может быть тем же что и `inputEl`
debounceTimer: number|null = null; debounceTimer: number|null = null;
subscription: Rx.Subscription|null = null; subscription: Rx.Subscription|null = null;
paperPrevScroll: number|null = null;
paperEl: HTMLDivElement|null = null;
// Выполнение действий из ST // Выполнение действий из ST
dispatch = (action: ST.Action<A>) => { dispatch = (action: ST.Action<A>) => {
...@@ -144,6 +150,14 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St ...@@ -144,6 +150,14 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St
this.anchorEl = ReactDOM.findDOMNode(this) as HTMLElement;; this.anchorEl = ReactDOM.findDOMNode(this) as HTMLElement;;
} }
componentDidUpdate() {
if (this.paperEl && this.paperPrevScroll && this.paperEl.scrollTop !== this.paperPrevScroll) {
this.paperEl.scrollTo(0, this.paperPrevScroll);
this.paperEl = null;
this.paperPrevScroll = null;
}
}
componentWillReceiveProps(nextProps: Props<A>) { componentWillReceiveProps(nextProps: Props<A>) {
if (nextProps.observable !== this.props.observable) this.listen(nextProps); if (nextProps.observable !== this.props.observable) this.listen(nextProps);
} }
...@@ -163,13 +177,13 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St ...@@ -163,13 +177,13 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St
this.subscription && (this.subscription.unsubscribe(), this.subscription = null); this.subscription && (this.subscription.unsubscribe(), this.subscription = null);
} }
endAdornment = memoize((nonNull: boolean, disabled: boolean) => <React.Fragment> endAdornment = memoize((nonNull: boolean, disabled: boolean, __: Gettext) => <React.Fragment>
<Icon onClick={disabled ? undefined : this.handleToggleVisibility}>arrow_drop_down</Icon> <Icon onClick={disabled ? undefined : this.handleToggleVisibility}>arrow_drop_down</Icon>
{!nonNull && <Icon onClick={disabled ? undefined : this.handleCloseClick}>close</Icon>} {!nonNull && <Icon onClick={disabled ? undefined : this.handleCloseClick} data-rh={__('Clear')}>close</Icon>}
</React.Fragment>); </React.Fragment>);
childrenProps = () => { childrenProps = () => {
const { nonNull, disabled } = this.props; const { nonNull, disabled, __ } = this.props;
return { return {
...pick(this.props, 'disabled', 'error', 'fullWidth', 'placeholder', 'ctx'), ...pick(this.props, 'disabled', 'error', 'fullWidth', 'placeholder', 'ctx'),
...@@ -180,13 +194,31 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St ...@@ -180,13 +194,31 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St
onBlur: disabled ? undefined : this.handleBlur, onBlur: disabled ? undefined : this.handleBlur,
onKeyDown: disabled ? undefined : this.handleKeyDown, onKeyDown: disabled ? undefined : this.handleKeyDown,
onClick: disabled ? undefined : this.handleClick, onClick: disabled ? undefined : this.handleClick,
endAdornment: this.endAdornment(!!nonNull, !!disabled), endAdornment: this.endAdornment(!!nonNull, !!disabled, __),
}; };
}; };
handleSeeMore = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
e.stopPropagation();
// const { query: { offset, limit } } = this.state;
this.paperEl = e.currentTarget.parentElement!.parentElement! as HTMLDivElement;
this.paperPrevScroll = this.paperEl.scrollTop;
this.dispatch({ tag: 'More' });
};
renderSeeMoreLink() {
const { __, classes } = this.props;
const { total, query: { limit, offset } } = this.state;
if (total !== 0 && limit + offset >= total) return null;
return <ListItem className={classes!.seeMore}>
<a href="javascript://void 0" onMouseDown={this.handleSeeMore}>{__('See more')}</a>
</ListItem>;
}
render() { render() {
const { className, fullWidth, children, ctx, source, debounce, renderItem, printItem, openOnFocus, openOnClick, suggestionProps, keepOpenAfterSelect, textFieldProps, classes, anchorEl, observable, ...rest } = this.props; const { className, fullWidth, children, ctx, source, debounce, renderItem, printItem, openOnFocus, openOnClick, suggestionProps, keepOpenAfterSelect, textFieldProps, classes, anchorEl, observable, ...rest } = this.props;
const { suggestions, open } = this.state; const { suggestions, open, pending } = this.state;
const rootClass = classNames(classes!.root, className, { const rootClass = classNames(classes!.root, className, {
[classes!.fullWidth!]: fullWidth [classes!.fullWidth!]: fullWidth
}); });
...@@ -195,6 +227,7 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St ...@@ -195,6 +227,7 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St
{!observable && (children ? React.cloneElement(children, this.childrenProps()) : <TextField {...this.childrenProps() as any} {...textFieldProps}/>)} {!observable && (children ? React.cloneElement(children, this.childrenProps()) : <TextField {...this.childrenProps() as any} {...textFieldProps}/>)}
<Suggestions <Suggestions
{...suggestionProps} {...suggestionProps}
pending={pending}
ctx={ctx!} ctx={ctx!}
anchorEl={anchorEl || this.anchorEl || undefined} anchorEl={anchorEl || this.anchorEl || undefined}
open={open} open={open}
...@@ -202,6 +235,7 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St ...@@ -202,6 +235,7 @@ export default class AutoComplete<A=string> extends React.Component<Props<A>, St
renderSuggestion={renderItem || printItem} renderSuggestion={renderItem || printItem}
onSelect={this.handleSuggestionSelect} onSelect={this.handleSuggestionSelect}
onVisibilityChange={this.handleVisibilityChnage} onVisibilityChange={this.handleVisibilityChnage}
after={this.renderSeeMoreLink()}
/> />
</div>; </div>;
} }
...@@ -268,7 +302,17 @@ export class CreateObservable extends React.Component<CreateObservableProps> { ...@@ -268,7 +302,17 @@ export class CreateObservable extends React.Component<CreateObservableProps> {
// Style // Style
export function styles(theme: Theme): StyleRules { export function styles(theme: Theme): StyleRules {
// const { unit } = theme.spacing; const { unit } = theme.spacing;
const linkStyles = {
color: theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
color: fade(theme.palette.primary.main, 0.75),
},
'&:active': {
color: theme.palette.error.main,
},
};
return { return {
root: { root: {
...@@ -280,5 +324,16 @@ export function styles(theme: Theme): StyleRules { ...@@ -280,5 +324,16 @@ export function styles(theme: Theme): StyleRules {
fullWidth: { fullWidth: {
width: '100%', width: '100%',
}, },
seeMore: {
marginBottom: -unit,
'& a': {
display: 'block',
width: '100%',
height: '100%',
textAlign: 'center',
...linkStyles,
},
},
}; };
} }
...@@ -41,7 +41,7 @@ export interface AutoCompleteQuery { ...@@ -41,7 +41,7 @@ export interface AutoCompleteQuery {
limit: number; limit: number;
search: string search: string
} }
export const defaultQuery = { offset: 0, search: '', limit: 10 }; export const defaultQuery = { offset: 0, search: '', limit: 20 };
// Источник для автодополнений // Источник для автодополнений
...@@ -68,6 +68,7 @@ export type Action<A> = ...@@ -68,6 +68,7 @@ export type Action<A> =
| { tag: 'Error', error: Err } | { tag: 'Error', error: Err }
| { tag: 'Search', value: string } | { tag: 'Search', value: string }
| { tag: 'Offset', value: number } | { tag: 'Offset', value: number }
| { tag: 'More' }
| { tag: 'Query', query: AutoCompleteQuery } | { tag: 'Query', query: AutoCompleteQuery }
| { tag: 'Query/success', suggestions: A[], total: number, query: AutoCompleteQuery } | { tag: 'Query/success', suggestions: A[], total: number, query: AutoCompleteQuery }
| { tag: 'Focus' } | { tag: 'Focus' }
...@@ -106,7 +107,7 @@ export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [Sta ...@@ -106,7 +107,7 @@ export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [Sta
case 'Query': { case 'Query': {
const { query } = action; const { query } = action;
const { options: { source } } = ctx; const { options: { source } } = ctx;
const printItem = ctx.options.printItem || String; const printItem = ctx.options.printItem || (x => JSON.stringify(x));
if (Array.isArray(source)) { if (Array.isArray(source)) {
// Поиск подходящих записей в массиве // Поиск подходящих записей в массиве
...@@ -121,7 +122,7 @@ export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [Sta ...@@ -121,7 +122,7 @@ export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [Sta
suggestions.push(source[i]); suggestions.push(source[i]);
} }
} }
return [{ ...state, suggestions, query, open: true }, noop]; return [{ ...state, suggestions, query, open: true, total: source.length }, noop];
} else if ('pageCollection' in source) { } else if ('pageCollection' in source) {
if (!('auth' in ctx) || !ctx.auth) return [state, noop]; if (!('auth' in ctx) || !ctx.auth) return [state, noop];
// Запрос на поиск записей // Запрос на поиск записей
...@@ -146,7 +147,11 @@ export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [Sta ...@@ -146,7 +147,11 @@ export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [Sta
return [state, cmd.of<Action<A>>({ tag: 'Query', query: { ...state.query, offset: action.value } })]; return [state, cmd.of<Action<A>>({ tag: 'Query', query: { ...state.query, offset: action.value } })];
} }
case 'Search': { case 'Search': {
return [state, cmd.of<Action<A>>({ tag: 'Query', query: { ...state.query, search: action.value } })]; return [state, cmd.of<Action<A>>({ tag: 'Query', query: { ...defaultQuery, search: action.value } })];
}
case 'More': {
const { query: { limit } } = state;
return [state, cmd.of<Action<A>>({ tag: 'Query', query: { ...state.query, limit: limit + 20 } })];
} }
case 'Focus': { case 'Focus': {
if (ctx.options.openOnFocus && !ctx.options.openOnClick) { if (ctx.options.openOnFocus && !ctx.options.openOnClick) {
......
...@@ -12,21 +12,21 @@ import { FieldProps } from '~/create-form'; ...@@ -12,21 +12,21 @@ import { FieldProps } from '~/create-form';
// Props // Props
export type Props<A> = StandardProps<React.HTMLProps<HTMLDivElement>, string, 'value'> & FieldProps<A> & { export type Props<A extends any[]> = StandardProps<React.HTMLProps<HTMLDivElement>, string, 'value'|'disabled'> & FieldProps<A> & {
source: Source<A>; source: Source<A[number]>;
fullWidth?: boolean; fullWidth?: boolean;
textFieldProps?: Partial<TextFieldProps>; textFieldProps?: Partial<TextFieldProps>;
renderItem?(a: A, selected: boolean): React.ReactNode; renderItem?(a: A[number], selected: boolean): React.ReactNode;
placeholder?: string; placeholder?: string;
printItem?(a: A): string; printItem?(a: A[number]): string;
id?: string; // Используется для установки `key` пропсов id?: string; // Используется для установки `key` пропсов
isEqual?(a: A, b: A): boolean; isEqual?(a: A[number], b: A[number]): boolean;
} }
// @ts-ignore Component // @ts-ignore Component
@withStyles(styles) @withStyles(styles)
export default class MultiSelect<A> extends React.Component<Props<A>> { export default class MultiSelect<A extends any[]> extends React.Component<Props<A>> {
static defaultProps = { static defaultProps = {
printItem: String, printItem: String,
} as any; } as any;
......
...@@ -7,7 +7,7 @@ import Modal from '@material-ui/core/Modal'; ...@@ -7,7 +7,7 @@ import Modal from '@material-ui/core/Modal';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles'; import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { StandardProps } from '@material-ui/core'; import { StandardProps } from '@material-ui/core';
import PendingOverlay from '@bitmaster/components/PendingOverlay'; import PendingOverlay from '~/PendingOverlay';
import { LocaleCtx, Gettext, withGettext } from '~/gettext'; import { LocaleCtx, Gettext, withGettext } from '~/gettext';
...@@ -22,6 +22,8 @@ export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string ...@@ -22,6 +22,8 @@ export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string
marginThreshold?: number; marginThreshold?: number;
pending?: boolean; pending?: boolean;
dontMove?: boolean; dontMove?: boolean;
before?: React.ReactNode;
after?: React.ReactNode;
// Callbacks // Callbacks
onSelect?(a: A): void; onSelect?(a: A): void;
...@@ -159,9 +161,10 @@ class Suggestions<A> extends React.Component<Props<A>, State> { ...@@ -159,9 +161,10 @@ class Suggestions<A> extends React.Component<Props<A>, State> {
let top = anchorRect.bottom + 4; let top = anchorRect.bottom + 4;
const bottom = top + rectHeight const bottom = top + rectHeight
if (bottom > heightThreshold && anchorRect.top - window.scrollY - rectHeight - 8 > marginThreshold) { if (bottom > heightThreshold && anchorRect.top - window.scrollY - rectHeight - 8 > marginThreshold) {
top = anchorRect.top - 8 - rectHeight; this.paperEl.style.bottom = - (anchorRect.top - 8) + 'px';
} } else {
this.paperEl.style.top = top + 'px'; this.paperEl.style.top = top + 'px';
}
this.paperEl.style.left = anchorRect.left + 'px'; this.paperEl.style.left = anchorRect.left + 'px';
this.paperEl.style.minWidth = anchorRect.width + 'px'; this.paperEl.style.minWidth = anchorRect.width + 'px';
}; };
...@@ -179,12 +182,12 @@ class Suggestions<A> extends React.Component<Props<A>, State> { ...@@ -179,12 +182,12 @@ class Suggestions<A> extends React.Component<Props<A>, State> {
} }
render() { render() {
const { __, classes, open, pending, suggestions } = this.props; const { __, classes, open, pending, suggestions, before, after } = this.props;
const PaperProps = { ref: this.handlePaperRef, className: classes.paper }; const PaperProps = { ref: this.handlePaperRef, className: classes.paper };
return <Modal className={classes.modal} open={!!open} onClose={this.handleClose} disableAutoFocus disableEnforceFocus disableRestoreFocus hideBackdrop> return <Modal className={classes.modal} open={!!open} onClose={this.handleClose} disableAutoFocus disableEnforceFocus disableRestoreFocus hideBackdrop>
<Paper {...PaperProps}> <Paper {...PaperProps}>
{!!suggestions.length && <MenuList className={classes.menu}>{this.renderSuggestions(suggestions)}</MenuList>} {!!suggestions.length && <MenuList className={classes.menu}>{before}{this.renderSuggestions(suggestions)}{after}</MenuList>}
{!suggestions.length && <div className={classes.empty}><span>{__('Nothing found…')}</span></div>} {!suggestions.length && <div className={classes.empty}><span>{__('Nothing found…')}</span></div>}
<PendingOverlay pending={pending || false}/> <PendingOverlay pending={pending || false}/>
</Paper> </Paper>
...@@ -192,7 +195,7 @@ class Suggestions<A> extends React.Component<Props<A>, State> { ...@@ -192,7 +195,7 @@ class Suggestions<A> extends React.Component<Props<A>, State> {
} }
} }
export default withStyles(styles)(withGettext(require('./i18n.po.yml'))(Suggestions)); export default withStyles(styles)(withGettext(require('./i18n'))(Suggestions));
// Styles // Styles
......
...@@ -10,7 +10,7 @@ const eyeSlash = require('./eye-slash.svg'); ...@@ -10,7 +10,7 @@ const eyeSlash = require('./eye-slash.svg');
// Props // Props
export type Props = StandardProps<React.HTMLProps<HTMLInputElement>, ClassKey, 'ref'|'disabled'> & FieldProps<string> & { export type Props = StandardProps<React.HTMLProps<HTMLInputElement>, ClassKey, 'ref'|'disabled'|'value'> & FieldProps<string> & {
__: Gettext; __: Gettext;
ctx?: LocaleCtx; ctx?: LocaleCtx;
type?: React.HTMLProps<HTMLInputElement>['type']; type?: React.HTMLProps<HTMLInputElement>['type'];
...@@ -115,7 +115,7 @@ export class TextField extends React.Component<Props, State> { ...@@ -115,7 +115,7 @@ export class TextField extends React.Component<Props, State> {
} }
} }
export default withGettext(require('./i18n.po.yml'))(TextField); export default withGettext(require('./i18n'))(TextField);
// CSS классы // CSS классы
...@@ -128,15 +128,22 @@ function styles(theme: Theme): StyleRules<ClassKey> { ...@@ -128,15 +128,22 @@ function styles(theme: Theme): StyleRules<ClassKey> {
return { return {
root: { root: {
height: unit * 4.5, height: unit * 4,
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
background: `rgba(0,0,0,0.04)`, background: `rgba(0,0,0,0.04)`,
borderRadius: 3, borderRadius: 3,
position: 'relative', position: 'relative',
paddingRight: unit * 0.5,
'&$error': { '&$error': {
background: `rgba(255,0,0,0.08)`, background: `rgba(255,0,0,0.08)`,
'& .material-icons': {
color: `rgba(255,0,0,0.54)`,
},
},
'& .material-icons': {
fontSize: 18,
}, },
}, },
...@@ -153,7 +160,7 @@ function styles(theme: Theme): StyleRules<ClassKey> { ...@@ -153,7 +160,7 @@ function styles(theme: Theme): StyleRules<ClassKey> {
height: '100%', height: '100%',
width: '100%', width: '100%',
boxSizing: 'border-box', boxSizing: 'border-box',
fontSize: 18, fontSize: 14,
color: theme.palette.text.primary, color: theme.palette.text.primary,
'&[disabled]': { '&[disabled]': {
color: theme.palette.text.disabled, color: theme.palette.text.disabled,
...@@ -178,6 +185,7 @@ function styles(theme: Theme): StyleRules<ClassKey> { ...@@ -178,6 +185,7 @@ function styles(theme: Theme): StyleRules<ClassKey> {
}, },
'&::placeholder': { '&::placeholder': {
fontStyle: 'italic', fontStyle: 'italic',
fontSize: 14,
}, },
'&$error::placeholder': { '&$error::placeholder': {
color: `rgba(255,0,0,0.54)`, color: `rgba(255,0,0,0.54)`,
......
# https://git.bitmaster.ru/npm/common/tree/master/gettext#%D0%A4%D0%BE%D1%80%D0%BC%D0%B0%D1%82-poyml
- "":
locale: "ru-RU"
"Fill out this field": "Заполните это поле"
"Nothing found…": "Ничего не найдено…"
export default (function (webpackContext) {
return webpackContext.keys().map(webpackContext);
})(require.context('./', false, /.*\.po$/));
msgid ""
msgstr ""
"Language: ru_RU\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Cannot be empty"
msgstr "Заполните это поле"
msgid "Nothing found…"
msgstr "Ничего не найдено…"
msgid "Clear"
msgstr "Очистить"
msgid "See more"
msgstr "Загрузить еще"
...@@ -7,4 +7,5 @@ export default function findMap<A, B>(xs: A[], f: (a: A) => B|undefined): B|unde ...@@ -7,4 +7,5 @@ export default function findMap<A, B>(xs: A[], f: (a: A) => B|undefined): B|unde
const b_or_undefined = f(x); const b_or_undefined = f(x);
if (b_or_undefined !== undefined) return b_or_undefined; if (b_or_undefined !== undefined) return b_or_undefined;
} }
return;
} }
...@@ -8,28 +8,28 @@ const CONTEXT_DELIMITER = String.fromCharCode(4); ...@@ -8,28 +8,28 @@ const CONTEXT_DELIMITER = String.fromCharCode(4);
// Translations // Translations
export type Data = Record<string, Record<string, string[]|string>>; export type TranslationsData = Record<string, Record<string, string[]|string>>;
export type JedFormat = { locale_data: { messages: Record<string, string[]> } }; export type POData = Record<string, string[]|string>;
export type ES6Module<T> = { default: T }; export type ES6Module<T> = { default: T };
// Хелпер для перевода // Хелпер для перевода
export type Gettext = (singular_key: string, context?: string, plural_key?: string, val?: number) => string; export type Gettext = (singular_key: string, plural_key?: string, context?: string, val?: number) => string;
export type I18nString = (locale?: string|LocaleCtx|null) => string; export type I18nString = (locale?: string|LocaleCtx|null) => string;
export class Translations { export class Translations {
constructor( constructor(
readonly data: Data readonly data: TranslationsData,
) {} ) {}
static fromJed(...data: JedFormat[]): Translations; static fromPO(...data: POData[]): Translations;
static fromJed(...data: ES6Module<JedFormat>[]): Translations; static fromPO(...data: ES6Module<POData>[]): Translations;
static fromJed(...data: JedFormat[][]): Translations; static fromPO(...data: POData[][]): Translations;
static fromJed(...data: ES6Module<JedFormat>[][]): Translations; static fromPO(...data: ES6Module<POData>[][]): Translations;
static fromJed(...webpackContexts) { static fromPO(...webpackContexts) {
const jedTranslations: JedFormat[] = flattenModules(webpackContexts); const podata: POData[] = flattenModules(webpackContexts);
return new Translations(assignJedData({}, ...jedTranslations)); return new Translations(assignData({}, ...podata));
function flattenModules(xs: any[]) { function flattenModules(xs: any[]) {
return Array.prototype.concat.apply([], xs.map(x => { return Array.prototype.concat.apply([], xs.map(x => {
...@@ -80,7 +80,7 @@ export type LocaleCtx = { locale: string }; ...@@ -80,7 +80,7 @@ export type LocaleCtx = { locale: string };
* ``` * ```
*/ */
export function withGettext(...webpackContexts) { export function withGettext(...webpackContexts) {
const translations: Translations = webpackContexts[0] instanceof Translations ? webpackContexts[0] : Translations.fromJed(...webpackContexts); const translations: Translations = webpackContexts[0] instanceof Translations ? webpackContexts[0] : Translations.fromPO(...webpackContexts);
return <P extends { __: Gettext, ctx?: { locale: string } }>(Component: React.ComponentType<P>) => { return <P extends { __: Gettext, ctx?: { locale: string } }>(Component: React.ComponentType<P>) => {
class WithTranslations extends React.Component<Omit<P, '__'>> { class WithTranslations extends React.Component<Omit<P, '__'>> {
...@@ -104,27 +104,21 @@ export function withGettext(...webpackContexts) { ...@@ -104,27 +104,21 @@ export function withGettext(...webpackContexts) {
/** /**
* Добавление переводов из `srcs` в `dst` * Добавление переводов из `srcs` в `dst`
*/ */
function assignJedData(dst: Data, ...srcs: JedFormat[]): Data { function assignData(dst: TranslationsData, ...srcs: POData[]): TranslationsData {
srcs.forEach(data => { srcs.forEach(data => {
// @ts-ignore
const locale_ = data[''].language || data[''].lang || data[''].locale; if (!locale_ || typeof(locale_) !== 'string') return; const locale_ = data[''].language || data[''].lang || data[''].locale; if (!locale_ || typeof(locale_) !== 'string') return;
const locale = locale_.replace(/_/g, '-'); const locale = locale_.replace(/_/g, '-');
const podata = data['%podata'];
for (const k of Object.keys(data)) { for (const k of Object.keys(data)) {
if (k === '' || k === '%podata') continue; if (k === '') continue;
dst[locale] = dst[locale] || {}; dst[locale] = dst[locale] || {};
if (Array.isArray(data[k]) && data[k][0] === null) { if (Array.isArray(data[k])) {
dst[locale][k] = data[k].slice(1); dst[locale][k] = data[k].slice(1);
if (dst[locale][k].length === 1) dst[locale][k] = dst[locale][k][0]; if (dst[locale][k].length === 1) dst[locale][k] = dst[locale][k][0];
} else { } else {
dst[locale][k] = data[k]; dst[locale][k] = data[k];
} }
} }
if (podata) for (const explicit of podata) {
const key = explicit.msgctxt ? explicit.msgctxt + CONTEXT_DELIMITER + explicit.msgid : explicit.msgid;
dst[locale] = dst[locale] || {};
dst[locale][key] = explicit.msgstr;
}
}); });
return dst; return dst;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment