Commit 1679dd43 by Vladislav Lagunov

Первый коммит

parents
import * as React from 'react';
import { memoize } from 'lodash';
import { FieldProps } from '~/fields/context';
import { filterMap } from '~/utils';
import * as F from '~/fields';
// Props
export interface Props<A = any> {
id?: string;
source: A[];
}
// Component
export default class ArrayIDS<A = any> extends React.Component<Props<A>> {
projectContext = (input: FieldProps) => ({
...input,
value: this.getValue(input.value),
onValueChange: this.handleValueChange(input.onValueChange),
});
getValue = memoize(valueIn => {
const { source } = this.props;
const id = this.props.id || 'id';
return filterMap(valueIn, x => source.find(s => s[id] === x) || null);
});
handleValueChange = memoize((onValueChange) => (valueOut: A[]) => {
const id = this.props.id || 'id';
onValueChange && onValueChange(valueOut ? valueOut.map(x => x[id]) : []);
});
render() {
return <F.Modifier proj={this.projectContext}>{this.props.children}</F.Modifier>
}
}
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Suggestions, { Props as SuggestionProps } from '~/fields/Suggestions';
import { AuthCtx as Ctx } from '~/context';
import { FieldProps } from '~/fields';
import TextField from '~/fields/TextField';
import { Props as TextFieldProps } from '~/fields/TextField';
import { StandardProps } from '@material-ui/core';
import Icon from '~/components/Icon';
import withStyles, { StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import { Source, State as AutoCompleteState } from '~/fields/AutoComplete/state-machine';
import * as ST from '~/fields/AutoComplete/state-machine';
import * as Rx from 'rxjs';
import { memoize } from '~/utils';
import { pick } from 'lodash';
// Props
export type Props<A> = StandardProps<React.HTMLProps<HTMLDivElement>, string> & FieldProps & {
ctx?: Ctx;
source: Source<A>;
debounce?: number;
renderItem?: SuggestionProps['renderSuggestion'];
suggestionProps?: Partial<SuggestionProps>;
printItem?(value: A): string;
keepOpenAfterSelect?: boolean;
textFieldProps?: Partial<TextFieldProps>;
openOnFocus?: boolean; // true по умолчанию
openOnClick?: boolean; // true по умолчанию
anchorEl?: HTMLElement;
nonNull?: boolean;
fullWidth?: boolean;
placeholder?: string;
null?: A;
observable?: Rx.Observable<ST.Action<A>>;
children?: React.ReactElement<React.HTMLProps<HTMLInputElement>>;
}
// State
export type State<A> = AutoCompleteState<A> & {
value: string|null;
}
// Component
// @ts-ignore Хак для работы дженерик-параметров
@withStyles(styles)
export default class AutoComplete<A=string> extends React.Component<Props<A>, State<A>> {
static defaultProps = {
openOnFocus: true,
openOnClick: true,
fullWidth: true,
printItem: x => x ? String(x) : '',
};
state: State<A> = { ...ST.init(), value: null };
anchorEl: HTMLElement|null; // Ссылка на <input/>
rectEl: HTMLElement|null; // Ссылка на элемент для выравнивания, может быть тем же что и `inputEl`
debounceTimer: number|null = null;
subscription: Rx.Subscription|null = null;
// Выполнение действий из ST
dispatch = (action: ST.Action<A>) => {
const { ctx, openOnFocus, source } = this.props;
const acCtx = { ...ctx, options: { openOnFocus, source } };
this.setState(prev => {
const [next, command] = ST.update(acCtx, action, prev);
command.subscribe(ethr => ethr.tag === 'Right' && this.dispatch(ethr.value));
return next;
});
};
handleValueChange = (value: string) => {
const debounce = this.props.debounce || 500;
this.setState({ value });
if (this.debounceTimer) clearTimeout(this.debounceTimer);
if (!value) {
!this.props.nonNull && this.handleSuggestionSelect('null' in this.props ? this.props.null : null);
}
this.debounceTimer = setTimeout(() => (this.debounceTimer = null, this.dispatch({ tag: 'Search', value })), debounce) as any;
};
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.handleValueChange(e.target.value);
};
handleFocus = (e?: React.SyntheticEvent) => {
this.dispatch({ tag: 'Focus' })
this.props.onFocus && this.props.onFocus(e);
};
handleBlur = (e?: React.SyntheticEvent) => {
this.dispatch({ tag: 'Blur' });
this.setState({ value: null });
this.props.onBlur && this.props.onBlur(e);
};
handleClick = (e?: React.SyntheticEvent) => {
const { openOnClick } = this.props;
const { open } = this.state;
if (openOnClick && !open) this.dispatch({ tag: 'Open' });
};
handleToggleVisibility = () => {
this.state.open ? this.dispatch({ tag: 'Close' }) : this.dispatch({ tag: 'Open' });
};
handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown': {
!this.state.open && this.dispatch({ tag: 'Open' });
break;
}
}
};
handleSuggestionSelect = (value: any) => {
const { keepOpenAfterSelect, onValueChange, disabled } = this.props;
this.setState({ value: null });
!keepOpenAfterSelect && this.dispatch({ tag: 'Blur' });
!disabled && onValueChange && onValueChange(value);
};
handleVisibilityChnage = (open: boolean) => {
if (!open) this.dispatch({ tag: 'Close' });
else this.dispatch({ tag: 'Open' });
};
getValue = () => {
const { printItem } = this.props;
if (this.state.value !== null) return this.state.value;
const { value } = this.props;
return (printItem || String)(value) || '';
};
componentDidMount() {
this.anchorEl = ReactDOM.findDOMNode(this) as HTMLElement;;
}
componentWillReceiveProps(nextProps: Props<A>) {
if (nextProps.observable !== this.props.observable) this.listen(nextProps);
}
componentWillUnmount() {
this.unlisten();
this.debounceTimer && (clearTimeout(this.debounceTimer), this.debounceTimer = null);
}
listen(props: Props<A>) {
this.subscription && (this.subscription.unsubscribe(), this.subscription = null);
if (!props.observable) return;
this.subscription = props.observable.subscribe(this.dispatch);
}
unlisten() {
this.subscription && (this.subscription.unsubscribe(), this.subscription = null);
}
endAdornment = memoize((nonNull: boolean, disabled: boolean) => <React.Fragment>
<Icon onClick={disabled ? undefined : this.handleToggleVisibility}>arrow_drop_down</Icon>
{!nonNull && <Icon onClick={disabled ? undefined : (() => this.handleValueChange(''))}>close</Icon>}
</React.Fragment>);
childrenProps = () => {
const { nonNull, disabled } = this.props;
return {
...pick(this.props, 'disabled', 'error', 'fullWidth', 'placeholder'),
value: this.getValue(),
onValueChange: disabled ? undefined : this.handleValueChange,
onChange: disabled ? undefined : this.handleChange,
onFocus: disabled ? undefined : this.handleFocus,
onBlur: disabled ? undefined : this.handleBlur,
onKeyDown: disabled ? undefined : this.handleKeyDown,
onClick: disabled ? undefined : this.handleClick,
endAdornment: this.endAdornment(!!nonNull, !!disabled),
};
};
render() {
const { children, ctx, source, debounce, renderItem, printItem, openOnFocus, openOnClick, suggestionProps, keepOpenAfterSelect, textFieldProps, classes, anchorEl, observable, ...rest } = this.props;
const { suggestions, open } = this.state;
return <div {...rest} className={classes!.wrapper}>
{!observable && (children ? React.cloneElement(children, this.childrenProps()) : <TextField {...this.childrenProps() as any} {...textFieldProps}/>)}
<Suggestions
{...suggestionProps}
ctx={ctx}
anchorEl={anchorEl || this.anchorEl || undefined}
open={open}
suggestions={suggestions}
renderSuggestion={renderItem || printItem}
onSelect={this.handleSuggestionSelect}
onVisibilityChange={this.handleVisibilityChnage}
/>
</div>;
}
}
// Props
export type CreateObservableProps = {
children: React.ReactElement<TextFieldProps>;
debounce?: number;
};
// Component
export class CreateObservable extends React.Component<CreateObservableProps> {
observable = new Rx.Subject<ST.Action<any>>();
debounceTimer: number|null = null;
handleValueChange = (value: string) => {
const { onValueChange } = this.props.children.props;
const debounce = this.props.debounce || 500;
if (this.debounceTimer) clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => (this.debounceTimer = null, this.observable.next({ tag: 'Search', value })), debounce) as any;
onValueChange && onValueChange(value);
};
handleFocus: TextFieldProps['onFocus'] = (e) => {
const { onFocus } = this.props.children.props;
this.observable.next({ tag: 'Focus' });
onFocus && onFocus(e);
};
handleBlur: TextFieldProps['onBlur'] = (e) => {
const { onBlur } = this.props.children.props;
this.observable.next({ tag: 'Blur' });
onBlur && onBlur(e);
};
handleKeyDown: TextFieldProps['onKeyDown'] = (e) => {
const { onKeyDown } = this.props.children.props;
switch (e.key) {
case 'ArrowDown': {
this.observable.next({ tag: 'Open' });
break;
}
}
onKeyDown && onKeyDown(e);
};
componentWillUnmount() {
this.debounceTimer && (clearTimeout(this.debounceTimer), this.debounceTimer = null);
}
render() {
return React.cloneElement(this.props.children, {
onValueChange: this.handleValueChange,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
onKeyDown: this.handleKeyDown,
});
}
}
// Style
export function styles(theme: Theme): StyleRules {
// const { unit } = theme.spacing;
return {
wrapper: {
'& > div': {
position: 'relative',
}
},
};
}
import { Err } from "~/context";
import { Cmd, noop, Eff, cmd } from "@bitmaster/core";
import { PendingAction, pending, AuthCtx } from "~/utils";
// Контекст
export type Ctx<A> = (AuthCtx | {}) & {
options: Options<A>;
};
// Options
export type Options<A> = {
source: Source<A>;
printItem?(x: A): string;
limit?: number;
openOnFocus?: boolean;
};
// Параметры для `pageCollection`
export interface AutoCompleteQuery {
offset: number;
limit: number;
search: string
}
export const defaultQuery = { offset: 0, search: '', limit: 10 };
// Источник для автодополнений
export type Source<A> =
| Array<A>
| AsyncSource<A>
| AsyncSource2<A>
// Асинхронные автодополенния
export type AsyncSource<A> = {
pageCollection(ctx: AuthCtx, query: AutoCompleteQuery): Eff<Err, [A[], number]>;
}
// Асинхронные автодополенния
export type AsyncSource2<A> = {
getCollection(ctx: AuthCtx, query: AutoCompleteQuery): Eff<Err, A[]>;
}
// Actions
export type Action<A> =
| PendingAction
| { tag: 'Error', error: Err }
| { tag: 'Search', value: string }
| { tag: 'Offset', value: number }
| { tag: 'Query', query: AutoCompleteQuery }
| { tag: 'Query/success', suggestions: A[], total: number, query: AutoCompleteQuery }
| { tag: 'Focus' }
| { tag: 'Blur' }
| { tag: 'Close' }
| { tag: 'Open' }
// State
export type State<A> = {
open: boolean;
suggestions: A[];
pending: boolean;
total: number;
query: AutoCompleteQuery;
};
// Init
export function init<A>(): State<A> {
return { open: false, suggestions: [], pending: false, total: 0, query: defaultQuery };
}
// Update
export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [State<A>, Cmd<Action<A>>] {
const onFailure = (error: Err) => ({ tag: 'Error', error } as Action<A>);
switch(action.tag) {
case 'Pending': {
return [{ ...state, pending: action.pending }, noop];
}
case 'Error': {
return [state, noop];
}
case 'Query': {
const { query } = action;
const { options: { source } } = ctx;
const printItem = ctx.options.printItem || String;
if (Array.isArray(source)) {
// Поиск подходящих записей в массиве
const suggestions: A[] = [];
let skipped = 0;
for (let i = 0; i < source.length && suggestions.length < query.limit; i++) {
if (!query.search || fuzzyFilter(query.search, printItem(source[i]))) {
if (query.offset > skipped) {
skipped++;
continue;
}
suggestions.push(source[i]);
}
}
return [{ ...state, suggestions, query, open: true }, noop];
} else if ('pageCollection' in source) {
if (!('auth' in ctx) || !ctx.auth) return [state, noop];
// Запрос на поиск записей
const onSuccess = ([suggestions, total]) => ({ tag: 'Query/success', suggestions, total, query } as Action<A>);
const command = source.pageCollection(ctx, query).perform(onFailure, onSuccess);
return [{ ...state, open: true }, pending(command)];
} else if ('getCollection' in source) {
if (!('auth' in ctx) || !ctx.auth) return [state, noop];
// Запрос на поиск записей
const onSuccess = (suggestions) => ({ tag: 'Query/success', suggestions, total: 0, query } as Action<A>);
const command = source.getCollection(ctx, query).perform(onFailure, onSuccess);
return [{ ...state, open: true }, pending(command)];
} else {
throw new Error('Unknown source');
}
}
case 'Query/success': {
const { suggestions, total, query } = action;
return [{ ...state, suggestions, total, query }, noop];
}
case 'Offset': {
return [state, cmd.of<Action<A>>({ tag: 'Query', query: { ...state.query, offset: action.value } })];
}
case 'Search': {
return [state, cmd.of<Action<A>>({ tag: 'Query', query: { ...state.query, search: action.value } })];
}
case 'Focus': {
if (ctx.options.openOnFocus) {
return [state, cmd.of<Action<A>>({ tag: 'Query', query: state.query })];
}
return [state, noop];
}
case 'Blur': {
return [{ ...state, open: false }, noop];
}
case 'Close': {
return [{ ...state, open: false }, noop];
}
case 'Open': {
if (state.open) return [state, noop];
return [state, cmd.of<Action<A>>({ tag: 'Query', query: defaultQuery })];
}
}
}
// https://github.com/callemall/material-ui/blob/a96ddc1b45b0fa325f0ec2d0cbd26ed565c9b40c/src/AutoComplete/AutoComplete.js#L599
export function fuzzyFilter(searchText: string, x: string): boolean {
const compareString = x.toLowerCase();
searchText = searchText.toLowerCase();
let searchTextIndex = 0;
for (let index = 0; index < x.length; index++) {
if (compareString[index] === searchText[searchTextIndex]) {
searchTextIndex += 1;
}
}
return searchTextIndex === searchText.length;
}
import * as React from 'react';
import Switch, { SwitchProps } from '@material-ui/core/Switch';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { FieldProps } from './';
import * as classNames from 'classnames';
import { StandardProps } from '@material-ui/core';
// Props
export type Props = StandardProps<React.HTMLProps<HTMLDivElement>, ClassKey, 'value'> & FieldProps<boolean> & WithStyles<ClassKey> & {
alignLeft?: boolean;
}
// Component
class BooleanField extends React.Component<Props> {
handleChange: SwitchProps['onChange'] = (e, v) => {
const { onValueChange } = this.props;
onValueChange && onValueChange(v);
};
render() {
const { ctx, value, classes, alignLeft, disabled, error, ...rest } = this.props as Props & WithStyles<ClassKey>;
const rootClass = classNames(classes.root, {
[classes.alignLeft]: alignLeft,
});
return <div {...rest}>
<Switch
className={rootClass}
checked={value}
onChange={this.handleChange}
disabled={disabled}
/>
</div>;
}
}
export default withStyles(styles)(BooleanField);
// CSS классы
export type ClassKey = 'root'|'alignLeft';
// Styles
export function styles(theme: Theme): StyleRules<ClassKey> {
const { unit } = theme.spacing;
return {
root: {
'& > span:first-child': {
height: unit * 4.5,
}
},
alignLeft: {
marginLeft: -13,
},
};
}
import * as React from 'react';
import { isEqual } from 'lodash';
import { FieldProps, Modifier } from '~/fields';
// Props
export type Props = {
delay?: number,
}
// Component
export class Debounce extends React.Component<Props> {
value: any;
prevContext: FieldProps;
timer: number|null = null;
callback: Function|null = null;
projectContext = (context: FieldProps) => {
if (!this.prevContext || !isEqual(context.value, this.prevContext.value)) {
this.value = context.value;
}
this.prevContext = context;
return {
...context,
value: this.value,
onValueChange: this.handleChange,
onBlur: this.handleBlur,
};
};
handleChange = (value) => {
if (this.timer) clearTimeout(this.timer);
this.value = value;
this.forceUpdate();
this.callback = () => {
if (!this.prevContext) return;
this.timer = null;
this.callback = null;
const { onValueChange } = this.prevContext;
onValueChange && onValueChange(value);
};
this.timer = setTimeout(this.callback, this.props.delay || 500);
};
handleBlur = (e: React.ChangeEvent<HTMLInputElement>) => {
this.callback && this.callback();
};
render() {
return <Modifier proj={this.projectContext}>{this.props.children}</Modifier>;
}
}
export default Debounce;;
import * as React from 'react';
// Props
export type Props<A> = {
delay?: number,
children: React.ReactElement<{ value: A, onValueChange?(x: A): void, onBlur?(e: React.FocusEvent): void; }>;
}
// State
export type State<A> = {
value?: A;
}
// Component
export class Debounce<A> extends React.Component<Props<A>, State<A>> {
static defaultProps = {
delay: 500,
};
state: State<A> = {};
timer: number|null = null;
callback: Function|null = null;
componentWillReceiveProps(nextProps: Props<A>) {
if ('value' in this.state) this.setState(prev => ({}));
}
handleChange = (value) => {
if (this.timer) clearTimeout(this.timer);
this.setState({ value })
this.forceUpdate();
this.callback = () => {
this.timer = null;
this.callback = null;
const { onValueChange } = this.props.children.props;
onValueChange && onValueChange(value);
};
this.timer = setTimeout(this.callback, this.props.delay);
};
handleBlur = (e: React.FocusEvent) => {
this.callback && this.callback();
const { onBlur } = this.props.children.props;
onBlur && onBlur(e);
};
render() {
return React.cloneElement(this.props.children, {
value: 'value' in this.state ? this.state.value : this.props.children.props.value,
onValueChange: this.handleChange,
onBlur: this.handleBlur,
});
}
}
export default Debounce;
import * as React from 'react';
import { FieldProps, Provider } from '~/fields';
// Props
export type Props<A> = FieldProps<A>;
// Component
export default class In<A> extends React.Component<Props<A>> {
render() {
const { children, ...rest } = this.props;
return <Provider value={rest}>{children}</Provider>;
}
}
import * as React from 'react';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { StandardProps } from '@material-ui/core';
import * as classNames from 'classnames';
// props
export type Props = StandardProps<React.HTMLProps<HTMLLabelElement>, ClassKey> & WithStyles<ClassKey> & {
focused?: boolean;
error?: boolean;
required?: boolean;
}
// component
class InputLabel extends React.Component<Props> {
render() {
const { classes, focused, error, required, ...rest } = this.props;
const rootClass = classNames(classes.root, {
[classes.focused]: focused,
[classes.error]: error,
[classes.required]: required,
})
return <label {...rest} className={rootClass} role="label"/>;
}
}
export default withStyles(styles)(InputLabel);
// CSS классы
export type ClassKey = 'root'|'focused'|'error'|'required';
// styles
function styles(theme: Theme): StyleRules<ClassKey> {
const { unit } = theme.spacing;
return {
root: {
color: theme.palette.text.secondary,
fontSize: theme.typography.pxToRem(13),
fontWeight: 500,
lineHeight: 1,
padding: 0,
},
focused: {
color: theme.palette.primary.main,
},
error: {
color: theme.palette.error.main,
},
required: {
'&:after': {
content: '"*"',
position: 'relative',
top: 2,
transform: 'scale(1.5)',
marginLeft: unit - 2,
display: 'inline-block',
},
},
};
}
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { FieldProps } from './';
import * as moment from 'moment';
import TextField, { Props as TextFieldProps } from './TextField';
import { StandardProps, Popover } from '@material-ui/core';
import Icon from '~/components/Icon';
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import DayPicker from 'react-dates/lib/components/DayPicker';
import * as F from '~/fields';
// Props
export type Props = StandardProps<React.HTMLProps<HTMLDivElement>, ClassKey, 'value'> & FieldProps<moment.Moment|null> & WithStyles<ClassKey> & {
InputProps?: Partial<TextFieldProps>;
format?: string;
}
// Model
export interface State {
textFieldValue: string|null;
open: boolean;
value: null|string;
}
// Component
class MomentField extends React.Component<Props, State> {
rootEl: HTMLElement|null;
pickerEl: HTMLElement|null;
state: State = { open: false, textFieldValue: null, value: null };
unlisten: Function|null;
projectContext = (input: FieldProps) => {
const { classes } = this.props as Props & WithStyles;
const self = this;
const endAdornment = <React.Fragment>
<Icon data-ignore-focus-in className={classes.iconDate} onClick={this.handleVisibilityToggle}>date_range</Icon>
<Icon data-ignore-focus-in className={classes.iconClose} onClick={this.handleClear}>close</Icon>
</React.Fragment>;
return {
...input,
endAdornment,
value: inputValue(this.state, input),
onValueChange: this.handleChange,
onBlur: this.handleBlur,
onKeyDown: this.handleKeyDown,
};
function inputValue(state: State, input: FieldProps) {
return state.value !== null ? state.value : input.value ? input.value.format(self.format(self.props)) : '';
}
};
componentDidMount() {
document.addEventListener('focusin', this.handleFocusIn);
document.addEventListener('click', this.handleFocusIn);
this.unlisten = () => (document.removeEventListener('focusin', this.handleFocusIn), document.removeEventListener('click', this.handleFocusIn), this.unlisten = null);
}
componentWillUnmount() {
this.unlisten && this.unlisten();
}
handleRootRef: React.Ref<HTMLDivElement> = c => {
if (!c) { this.rootEl = null; return; }
const rootEl = ReactDOM.findDOMNode(c); if (!rootEl) return;
this.rootEl = rootEl as HTMLElement;
};
handlePickerRef: React.Ref<HTMLDivElement> = c => {
if (!c) { this.pickerEl = null; return; }
const rootEl = ReactDOM.findDOMNode(c); if (!rootEl) return;
this.pickerEl = rootEl as HTMLElement;
}
handleChange: TextFieldProps['onValueChange'] = value => {
this.setState({ value });
};
handleBlur = () => {
const { onValueChange } = this.props;
const { value } = this.state;
if (!value) return;
const nextValue = moment(value, this.format(this.props)); if (!nextValue.isValid()) return;
this.setState({ value: null })
const prevValueOf = this.props.value ? this.props.value.valueOf() : 0;
const nextValueOf = nextValue.valueOf();
prevValueOf !== nextValueOf && onValueChange && onValueChange(nextValue);
};
handleKeyDown: TextFieldProps['onKeyDown'] = e => {
switch (e.key) {
case 'Enter':
this.handleBlur();
break;
}
};
handleFocusIn = (e: FocusEvent) => {
const { rootEl, pickerEl } = this;
const { disabled } = this.props; if (disabled) return;
const targetElement = e.target as Element;
if (targetElement.hasAttribute('data-ignore-focus-in')) return;
const open = isElementInside(targetElement);
this.setState({ open });
function isElementInside(node: Node): boolean {
if (node === rootEl || node === pickerEl) return true;
return node.parentNode ? isElementInside(node.parentNode) : false;
}
};
handleClear = (e) => {
e.preventDefault(); e.stopPropagation();
const { onValueChange } = this.props;
onValueChange && onValueChange(null);
};
handleVisibilityToggle = (e: React.MouseEvent<HTMLSpanElement>) => {
const { open } = this.state;
this.setState({ open: !open });
};
handleClose = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (this.state.open) this.setState({ open: false });
};
handleDatesChange = (date: moment.Moment) => {
const { value, onValueChange } = this.props;
const prev = value || moment().startOf('minute');
const next = date.clone();
next.hours(prev.hours());
next.minutes(prev.minutes());
onValueChange && onValueChange(next);
};
format(props: Props): string {
return props.format || 'DD/MM/YYYY';
}
render() {
const { classes, /*dirty, */ error, InputProps, onBlur, onChange, onFocus, value, format: formapProps, disabled, ...rest } = this.props;
const { rootEl } = this;
const { open } = this.state;
const rootClass = classes.root;
return <F.Modifier proj={this.projectContext}>
<div {...rest} className={rootClass} ref={this.handleRootRef}>
<TextField
{...InputProps as any}
disabled={disabled}
error={error}
className={classes.input}
/>
{rootEl && <Popover className={classes.modal} anchorEl={rootEl} open={open} onClose={this.handleClose} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} disableAutoFocus disableEnforceFocus disableRestoreFocus hideBackdrop>
<DayPicker
ref={this.handlePickerRef}
date={value}
onDayClick={this.handleDatesChange}
numberOfMonths={1}
/>
</Popover>}
</div>
</F.Modifier>;
}
}
export default withStyles(styles)(MomentField);
// CSS классы
export type ClassKey = 'root'|'modal'|'input'|'iconClose'|'iconDate';
// Styles
export function styles(theme: Theme): StyleRules<ClassKey> {
const { unit } = theme.spacing;
return {
root: {
},
iconClose: {
position: 'absolute',
right: unit,
},
iconDate: {
position: 'absolute',
right: unit * 4,
},
modal: {
/* width: 0,
* height: 0, */
/* top: unit,
* left: -unit,*/
},
input: {
width: 180,
paddingRight: `${unit * 8}px !important`,
position: 'relative',
},
};
}
import * as React from 'react';
import { isEqual } from 'lodash';
import memoize from 'memoize-one';
import { Chip, Checkbox, MenuItem, ListItemText, StandardProps } from '@material-ui/core';
import * as classNames from 'classnames';
import withStyles, { StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import AutoComplete from './AutoComplete';
import { Props as TextFieldProps } from '~/fields/TextField';
import { Source } from '~/fields/AutoComplete/state-machine';
import { FieldProps } from '~/fields';
// Props
export type Props<A> = StandardProps<React.HTMLProps<HTMLDivElement>, string, 'value'> & FieldProps & {
source: Source<A>;
textFieldProps?: Partial<TextFieldProps>;
renderSuggestion?(a: A): React.ReactNode;
placeholder?: string;
printItem(a: A): string;
id?: string; // Используется для установки `key` пропсов
isEqual?(a: A, b: A): boolean;
}
// @ts-ignore Component
@withStyles(styles)
export default class MultiSelect<A> extends React.Component<Props<A>> {
handleDelete = memoize((idx: number) => (e?: React.MouseEvent<HTMLDivElement>) => {
const { value, onValueChange, disabled } = this.props; if (disabled) return;
const nextValue = value!.slice();
nextValue.splice(idx, 1);
if (e) {
const nextSibling = e.currentTarget.parentNode!.childNodes[idx + 1] as HTMLElement|null;
nextSibling && nextSibling.focus();
}
onValueChange && onValueChange(nextValue);
});
isEqual(a: A, b: A) {
return (this.props.isEqual || isEqual)(a, b);
}
handleValueChange = (item: A) => {
const { value, onValueChange } = this.props;;
const nextValue = value!.slice();
if (!item) {
onValueChange && onValueChange([]);
return;
}
for (let i = 0; i < nextValue.length; i++) {
if (this.isEqual(nextValue[i], item)) {
nextValue.splice(i, 1);
onValueChange && onValueChange(nextValue);
return;
}
}
nextValue.push(item);
onValueChange && onValueChange(nextValue);
};
renderChips = memoize((value: Props<A>['value']) => {
const { classes, printItem, disabled } = this.props;
const id = this.props.id || 'id';
const chipClassName = classNames({ [classes!.disabled!]: disabled });
return !value.length ? null : <div><span className={classes!.chips}>
{value!.map((ch, i) => (
<Chip key={ch[id]} className={chipClassName} label={printItem(ch)} onClick={this.handleDelete(i)} onDelete={this.handleDelete(i)}/>
))}
</span></div>;
});
renderSuggestion = (item: A) => {
const { renderSuggestion, printItem, value } = this.props;
const selected = !!value!.find(x => this.isEqual(x, item));
const id = this.props.id || 'id';
return <MenuItem key={item[id]} disableRipple>
<Checkbox disableRipple checked={selected}/>
<ListItemText primary={(renderSuggestion || printItem || String)(item)}/>
</MenuItem>;
};
render() {
const { classes, className, source, renderSuggestion, printItem, id, textFieldProps, value, error, disabled, onValueChange, placeholder, ...rest } = this.props;
const rootClass = classNames(className, classes!.root, {
[classes!.disabled!]: disabled,
});
return <div {...rest} className={rootClass}>
{this.renderChips(value)}
<AutoComplete<A>
value={null}
printItem={printItem}
source={source}
renderItem={this.renderSuggestion}
onValueChange={this.handleValueChange}
textFieldProps={textFieldProps}
placeholder={placeholder}
error={error}
disabled={disabled}
keepOpenAfterSelect
openOnClick
openOnFocus
nonNull
/>
</div>;
}
}
// Style
export function styles(theme: Theme): StyleRules {
const { unit } = theme.spacing;
return {
root: {
},
chips: {
marginLeft: -unit,
marginBottom: unit,
display: 'inline-block',
'& > *': {
display: 'inline-flex',
marginLeft: unit,
marginTop: unit,
},
},
disabled: {
cursor: 'not-allowed',
},
};
}
import * as React from 'react';
import { CrudResource } from '~/utils/jsonapi';
import { AuthCtx as Ctx } from '~/utils';
import { Eff } from '@bitmaster/core';
import * as Rx from 'rxjs';
import { Err, notifyError } from '~/context';
import { FieldProps } from '~/fields';
import * as F from '~/fields';
import { Resource } from '@bitmaster/utils/jsonapi';
// Props
export type Props<A = any> = {
children: React.ReactElement<{ resource: CrudResource<A, never> }>;
source: PrimarySource<A>;
id?: string;
}
// Props
export type State<A extends Resource<any>= any> = {
value: A|null; // Ресурс полученный из id
pending: boolean;
}
// Источник для получения ресурса по ид
export type PrimarySource<A> = {
getPrimary(ctx: Ctx, id: string): Eff<Err, A>;
}
// Component
export class ResourceID extends React.Component<Props, State> {
subscription: Rx.Subscription|null; // Подписка на запрос
state: State = { pending: false, value: null };
inputContext: FieldProps|null = null;
prevValue: any;
projectContext = (input: FieldProps) => {
this.inputContext = input;
const { value } = this.state;
if (this.prevValue !== input.value) {
if (input.value) this.resolveResource(input);
else this.setState({ value: null });
this.prevValue = input.value;
}
return { ...input, value, onValueChange: this.handleValueChange };
};
handleValueChange = (value: State['value'], at=[]) => {
const id = this.props.id || 'id';
const nextValue = value ? value[id] : null;
this.setState({ value, pending: false });
if (!this.inputContext) return;
const { onValueChange } = this.inputContext;
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
onValueChange && onValueChange(nextValue, []);
};
componentDidMount() {
this.resolveResource(this.props);
}
resolveResource(inputContext: FieldProps) {
if (this.subscription) return;
const { source } = this.props; if (!source) return;
const { ctx, value } = inputContext; if (!ctx) return; if (typeof(value) !== 'string') return;
this.setState({ pending: true });
this.subscription = source.getPrimary(ctx, value).subscribe(ethr => {
this.subscription = null;
if (ethr.tag === 'Left') {
this.setState({ pending: false });
notifyError(ethr.value);
} else {
this.setState({ pending: false, value: ethr.value });
}
});
}
render() {
return <F.Modifier proj={this.projectContext}>
{this.props.children}
</F.Modifier>;
}
}
export default ResourceID;
import * as React from 'react';
import { isEqual } from 'lodash';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import * as classNames from 'classnames';
import { FieldProps } from './';
import { MenuItem, StandardProps } from '@material-ui/core';
import Select, { SelectProps } from '@material-ui/core/Select';
import { AuthCtx as Ctx } from '~/context';
import { Err, notifyError } from '~/context';
import * as Rx from 'rxjs';
import { Eff } from '@bitmaster/core';
// Источник для опций
export type Source<A> =
| Array<A>
| AsyncSource<A>
// Асинхронные автодополенния
export type AsyncSource<A> = {
getCollection(ctx: Ctx): Eff<Err, A[]>;
}
// Props
export type Props<A = any> = StandardProps<React.HTMLProps<HTMLDivElement>, ClassKey> & FieldProps<A> & {
selectProps?: Partial<SelectProps>;
source: Source<A>;
renderItem?(item: A): string;
prepareItem?(item: A): any;
isEqual?(a: A, b: A): boolean;
nullCase?: string;
}
// State
export interface State<A> {
open: boolean;
options: null|'pending'|A[];
}
// Тип для запроса коллекции
export interface SelectFieldQuery {
offset: number;
limit: number;
}
// Component
// @ts-ignore
@withStyles(styles)
export default class SelectField<A = any> extends React.Component<Props<A>, State<A>> {
state: State<A> = { open: false, options: null };
subscription: Rx.Subscription|null = null;
componentDidMount() {
const { source, ctx } = this.props;
if (!Array.isArray(source) && ctx) {
this.fetchOptions(ctx, source, false);
}
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
handleChange: SelectProps['onChange'] = e => {
const { onValueChange, prepareItem } = this.props;
const options = this.getOptions();
const idx = Number(e.target.value); if (isNaN(idx) || options.length <= idx) return;
if (idx < 0) {
onValueChange && onValueChange(null as any);
return;
}
const prepare = prepareItem || (x => x);
onValueChange && onValueChange(prepare(options[idx]));
};
handleOpen = () => {
const { source, ctx } = this.props;
if (Array.isArray(source)) {
this.setState({ open: true });
}
else if (ctx) {
this.fetchOptions(ctx, source);
}
};
handleClose = () => {
this.setState({ open: false });
};
getOptions() {
return Array.isArray(this.state.options) ? this.state.options : Array.isArray(this.props.source) ? this.props.source : [];
}
fetchOptions(ctx: Ctx, source: AsyncSource<A>, open=true) {
if (this.subscription) return;
this.setState({ options: 'pending' });
this.subscription = source.getCollection(ctx).subscribe(ethr => {
this.subscription = null;
if (ethr.tag === 'Left') {
notifyError(ethr.value);
this.setState({ options: null });
} else {
this.setState({ options: ethr.value, open });
}
});
}
render() {
const { classes, error, nullCase, /*dirty, selectProps, options: optionsProps, */ disabled, renderItem, /*onFocus, */value, className, isEqual: isEqualProp /*...rest*/ } = this.props as Props<A> & WithStyles<ClassKey>;
const { open } = this.state;
const rootClass = classNames(className, classes.root, {
[classes.error]: error,
});
const options = this.getOptions();
const predicate = isEqualProp || isEqual;
const valueIdx = options.findIndex(x => predicate(x, value!));
return (
<Select value={valueIdx} onChange={this.handleChange} className={rootClass} disabled={disabled} open={open} onOpen={this.handleOpen} onClose={this.handleClose}>
{nullCase && <MenuItem key="@null" value={-1}>{nullCase}</MenuItem>}
{options.map((item, idx) => <MenuItem key={idx} value={idx}>{(renderItem || String)(item)}</MenuItem>)}
</Select>
);
}
}
// CSS классы
export type ClassKey = 'root'|'error';
// Styles
function styles(theme: Theme): StyleRules<ClassKey> {
const { unit } = theme.spacing;
return {
root: {
'&:before': {
display: 'none',
},
'& > div > div': {
paddingLeft: unit,
background: `rgba(0,0,0,0.04)`,
borderRadius: 2,
},
},
error: {
background: `rgba(255,0,0,0.08)`,
},
};
}
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { memoize } from 'lodash';
import MenuList from '@material-ui/core/MenuList';
import MenuItem, { MenuItemProps } from '@material-ui/core/MenuItem';
import Modal from '@material-ui/core/Modal';
import Paper from '@material-ui/core/Paper';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { StandardProps } from '@material-ui/core';
import PendingOverlay from '@bitmaster/components/PendingOverlay';
import { Ctx, pgettext } from '@bitmaster/utils/gettext';
// props
export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string> & WithStyles<string> & {
ctx?: Ctx;
suggestions: A[];
renderSuggestion?(a: A): React.ReactElement<MenuItemProps>|string;
anchorEl?: HTMLElement;
open?: boolean;
marginThreshold?: number;
pending?: boolean;
dontMove?: boolean;
// callbacks
onSelect?(a: A): void;
onVisibilityChange?(open: boolean): void;
}
// state
export interface State {
selected: number;
}
// component
class Suggestions<A> extends React.Component<Props<A>, State> {
state = { selected: 0 };
paperEl: HTMLElement|null = null;
unlisten: Function|null = null;
componentDidMount() {
if (this.props.open) this.listen();
}
componentWillUnmount() {
this.unlisten && this.unlisten();
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.open && !this.props.open) { // show component
this.listen();
}
if (!nextProps.open && this.props.open) { // hide component
this.setHovered(0);
this.unlisten && this.unlisten();
}
if (nextProps.suggestions !== this.props.suggestions) {
this.setHovered(0);
}
}
componentDidUpdate(prevProps: Props) {
const dontMove = typeof(this.props.dontMove) === 'boolean' ? this.props.dontMove : true;
if (prevProps.suggestions !== this.props.suggestions && !dontMove && this.paperEl) {
this.handlePaperRef(this.paperEl as any);
}
}
listen() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('focusin', this.handleFocusClick);
document.addEventListener('click', this.handleFocusClick);
this.unlisten = () => {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('focusin', this.handleFocusClick);
document.removeEventListener('click', this.handleFocusClick);
this.unlisten = null;
};
}
handleFocusClick = (e: FocusEvent|MouseEvent) => {
const { paperEl, props: { anchorEl, onVisibilityChange } } = this;
onVisibilityChange && onVisibilityChange(isElementInside(e.target as Node));
function isElementInside(node: Node): boolean {
if (node === anchorEl || node === paperEl) return true;
return node.parentNode ? isElementInside(node.parentNode) : false;
}
};
setHovered(selected: number) {
const SCROLL_BOTTOM_THRESHOLD = 0.8;
const SCROLL_TOP_THRESHOLD = 0.1;
this.setState({ selected }, () => {
if (!this.paperEl) return;
//const menuItemClasses = styleManager.render(menuItem.styleSheet);
const selected = this.paperEl.querySelector('[data-is-selected=true]') as HTMLElement | null; if (!selected) return;
const menu = selected.parentNode as HTMLElement;
const menuRect = menu.getBoundingClientRect();
if (selected.offsetTop - menu.scrollTop >= SCROLL_BOTTOM_THRESHOLD * menuRect.height) {
// reached the point where we need to scroll down
menu.scrollTop = selected.offsetTop - SCROLL_BOTTOM_THRESHOLD * menuRect.height;
}
else if (selected.offsetTop - menu.scrollTop < SCROLL_TOP_THRESHOLD * menuRect.height) {
// need to scroll up
menu.scrollTop = selected.offsetTop - SCROLL_TOP_THRESHOLD * menuRect.height;
}
});
}
handleKeyDown = (e: KeyboardEvent) => {
const { suggestions, onSelect } = this.props;
switch (e.key) {
case 'ArrowDown':
if (this.state.selected < suggestions.length - 1)
this.setHovered(this.state.selected + 1);
break;
case 'ArrowUp':
if (this.state.selected > 0)
this.setHovered(this.state.selected - 1);
break;
case 'Enter': {
e.stopPropagation();
e.preventDefault();
if (this.state.selected >= suggestions.length) break;
onSelect && onSelect(suggestions[this.state.selected]);
break;
}
}
};
handleSuggestionClick = memoize((idx: number) => (e: any) => {
const { suggestions, onSelect } = this.props;
e.stopPropagation();
e.preventDefault();
if (idx >= suggestions.length) return;
onSelect && onSelect(suggestions[idx]);
});
handleClose = () => {
const { onVisibilityChange } = this.props;
onVisibilityChange && onVisibilityChange(false);
};
handlePaperRef = (c: React.Component|null) => {
const { anchorEl } = this.props;
if (!c || !anchorEl) return;
this.paperEl = ReactDOM.findDOMNode(c) as HTMLElement;
const marginThreshold = this.props.marginThreshold || 8;
const heightThreshold = window.innerHeight - marginThreshold;
const paperRect = this.paperEl.getBoundingClientRect();
const anchorRect = anchorEl.getBoundingClientRect();
const rectHeight = paperRect.height;
let top = anchorRect.bottom + 4;
const bottom = top + rectHeight
if (bottom > heightThreshold && anchorRect.top - window.scrollY - rectHeight - 8 > marginThreshold) {
top = anchorRect.top - 8 - rectHeight;
}
this.paperEl.style.top = top + 'px';
this.paperEl.style.left = anchorRect.left + 'px';
this.paperEl.style.minWidth = anchorRect.width + 'px';
};
// Render suggestion list
renderSuggestions(suggestions: Props['suggestions']) {
const { renderSuggestion } = this.props;
const { selected } = this.state;
return suggestions.map((x, idx) => {
const element = renderSuggestion ? renderSuggestion(x) : String(x);
return React.isValidElement(element)
? React.cloneElement(element, { key: idx, onMouseDown: e => { this.handleSuggestionClick(idx)(e); element.props.onClick && element.props.onClick(e); } , selected: selected === idx, 'data-is-selected': selected === idx } as MenuItemProps)
: <MenuItem key={idx} selected={selected === idx} onMouseDown={this.handleSuggestionClick(idx)} data-is-selected={selected === idx}>{element}</MenuItem>;
});
}
render() {
const { classes, open, pending, suggestions, ctx } = this.props;
const PaperProps = { ref: this.handlePaperRef, className: classes.paper };
const __ = k => pgettext(ctx || null, 'autocomplete', k);
return <Modal className={classes.modal} open={!!open} onClose={this.handleClose} disableAutoFocus disableEnforceFocus disableRestoreFocus hideBackdrop>
<Paper {...PaperProps}>
{!!suggestions.length && <MenuList className={classes.menu}>{this.renderSuggestions(suggestions)}</MenuList>}
{!suggestions.length && <div className={classes.empty}><span>{__('Nothing found…')}</span></div>}
<PendingOverlay pending={pending || false}/>
</Paper>
</Modal>;
}
}
export default withStyles(styles)(Suggestions);
// Styles
export function styles(theme): StyleRules {
const { unit } = theme.spacing;
return {
paper: {
position: 'absolute',
zIndex: 9000,
},
menu: {
maxHeight: 300,
overflowY: 'auto',
},
modal: {
width: 0,
height: 0,
},
empty: {
color: theme.palette.text.secondary,
padding: [unit, unit * 2],
textAlign: 'center',
fontStyle: 'italic',
width: '100%',
},
};
}
import * as React from 'react';
import { pgettext } from '@bitmaster/utils/gettext';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { StyleRules, WithStyles } from '@material-ui/core/styles/withStyles';
import * as classNames from 'classnames';
import { FieldProps } from './';
import { StandardProps } from '@material-ui/core';
const eye = require('./eye.svg');
const eyeSlash = require('./eye-slash.svg');
// Props
export type Props = StandardProps<React.HTMLProps<HTMLDivElement>, ClassKey, 'ref'|'onKeyDown'|'disabled'> & FieldProps<string> & WithStyles<ClassKey> & {
type?: React.HTMLProps<HTMLInputElement>['type'];
filter?(s: string): boolean;
regexFilter?: RegExp;
inputProps?: React.HTMLProps<HTMLInputElement>;
beginAdornment?: React.ReactNode;
endAdornment?: React.ReactNode;
Input?: React.ComponentType|string;
placeholder?: string;
fullWidth?: boolean;
};
// State
export type State = {
type: string|null;
};
// Component
export class TextField extends React.Component<Props, State> {
state: State = { type: null };
static getDerivedStateFromProps(props: Props, state: State) {
if (props.type === 'password-switch' && state.type === null) return { type: 'password' };
if (props.type !== 'password-switch' && state.type !== null) return { type: null };
return null;
}
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { onValueChange, filter, regexFilter } = this.props;
if (filter && !filter(e.target.value)) return;
if (regexFilter && !regexFilter.test(e.target.value)) return;
onValueChange && onValueChange(e.target.value);
};
handleSwitchPassword = () => {
const { type } = this.state;
this.setState({ type: type === 'password' ? 'text' : 'password' })
};
render() {
const {
ctx,
classes,
// dirty,
fullWidth,
error,
inputProps,
onBlur,
onChange,
onFocus,
value,
beginAdornment,
endAdornment: _endAdornment,
className,
disabled,
onKeyDown,
type,
Input: InputProp,
placeholder,
...rest
} = this.props;
const rootClass = classNames(className, classes.root, {
[classes.disabled]: disabled,
[classes.error]: error,
[classes.textarea]: InputProp === 'textarea',
[classes.fullWidth]: fullWidth,
});
const inputClass = classNames(classes.input, {
[classes.error]: error,
});
const __ = k => ctx ? pgettext(ctx, 'fields', k) : k;
const Input: any = InputProp || 'input';
const endAdornment = type !== 'password-switch' ? _endAdornment : <React.Fragment>
{_endAdornment}
<span className={classes.eye} onClick={this.handleSwitchPassword} dangerouslySetInnerHTML={{ __html: this.state.type === 'password' ? eye : eyeSlash}} data-rh={this.state.type === 'password' ? __('Show password') : __('Hide password')}/>
</React.Fragment>;
return <div {...rest} className={rootClass}>
{beginAdornment}
<Input
placeholder={placeholder}
{...inputProps}
type={type === 'password-switch' ? this.state.type : type}
className={inputClass}
onChange={this.handleChange}
value={value || ''}
disabled={disabled}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
{endAdornment}
</div>;
}
}
export default withStyles(styles)(TextField);
// CSS классы
export type ClassKey = 'root'|'error'|'input'|'textarea'|'eye'|'fullWidth'|'disabled';
// Styles
function styles(theme: Theme): StyleRules<ClassKey> {
const { unit } = theme.spacing;
return {
root: {
height: unit * 4.5,
boxSizing: 'border-box',
display: 'inline-flex',
alignItems: 'center',
background: `rgba(0,0,0,0.04)`,
borderRadius: 3,
position: 'relative',
'&$error': {
background: `rgba(255,0,0,0.08)`,
},
},
disabled: {
cursor: 'not-allowed',
},
input: {
padding: [unit, unit] as any,
outline: 'none',
border: 'none',
background: 'none',
flex: '10 10 auto',
height: '100%',
width: '100%',
boxSizing: 'border-box',
fontSize: 18,
color: theme.palette.text.primary,
'&[disabled]': {
color: theme.palette.text.disabled,
cursor: 'not-allowed',
},
'&:after': {
content: '""',
position: 'absolute',
display: 'block',
width: '100%',
height: 0,
},
'&:focus:after': {
background: theme.palette.primary.main,
height: 2,
},
'&$error:after': {
background: theme.palette.error.main,
},
'& + *': {
marginLeft: -unit,
},
'&::placeholder': {
fontStyle: 'italic',
},
},
textarea: {
height: 'auto',
width: '100%',
padding: 0,
'& textarea': { padding: unit },
},
eye: {
width: 22,
},
error: {
},
fullWidth: {
width: '100%',
},
};
}
import * as React from 'react';
import { FieldProps } from './';
import * as F from './';
import { StandardProps } from '@material-ui/core';
import { memoize, ObjectKey, ObjectPath } from '~/utils';
// Props
export type Props = StandardProps<React.HTMLProps<HTMLDivElement>, string> & {
in: Array<string|number>|string|number;
onlyValue?: boolean;
dontWrap?: boolean;
}
const zoomOnChange = memoize((path: ObjectKey[], onChange: ((x: any, at: ObjectPath) => void)|undefined) => {
return (next, at=[]) => {
onChange && onChange(next, [...at, ...path]);
};
})
const zoomAny = memoize((path: ObjectKey[], input: any) => {
let iter = input as any;
for (const key of path) {
if (typeof(iter) !== 'object' || iter === null) return iter;
iter = iter[key];
}
return iter;
})
// Component
export default class Zoom extends React.Component<Props> {
projectContext = (input: FieldProps) => {
const { onlyValue } = this.props;
const path = Array.isArray(this.props.in) ? this.props.in : [this.props.in];
if (onlyValue) return {
...input,
value: zoomAny(path, input.value),
onValueChange: zoomOnChange(path, input.onValueChange),
};
else return {
...input,
value: zoomAny(path, input.value),
error: zoomAny(path, input.error),
disabled: zoomAny(path, input.disabled),
onValueChange: zoomOnChange(path, input.onValueChange),
};
}
render() {
const { dontWrap, children, in: in_, onlyValue, ...rest } = this.props;
const dataIn = Array.isArray(in_) ? in_ : [in_];
return <F.Modifier proj={this.projectContext}>
{dontWrap ? children : <div {...rest} role="zoom" data-in={dataIn.join('-')}>{children}</div>}
</F.Modifier>;
}
}
import * as React from 'react';
import { AuthCtx as Ctx } from '~/context';
import { I18nString } from '@bitmaster/utils/gettext';
import { ObjectPath } from '~/utils';
const hoistNonReactStatics = require('hoist-non-react-statics');
export const { Provider, Consumer } = React.createContext<FieldProps>(void 0 as any);
// Контекст поля
export interface FieldProps<Value=any> {
ctx?: Ctx;
value?: Value;
disabled?: Disabled;
error?: Error;
FieldWrapper?: React.ComponentType;
onValueChange?(value: Value, at?: ObjectPath): void;
onFocus?(e?: React.SyntheticEvent): void;
onBlur?(e?: React.SyntheticEvent): void;
onClick?(e?: React.SyntheticEvent): void;
onKeyDown?(e: React.KeyboardEvent): void;
beginAdornments?: React.ReactNode;
endAdornments?: React.ReactNode;
}
// Обертка для полей используемых внутри
export function withFieldContext<P extends FieldProps<any>, C extends React.ComponentType<P>>(Component: C): C {
class WithFieldContext extends React.Component<P> {
render() {
return <Consumer>{this.renderer}</Consumer>;
};
renderer = (context: FieldProps) => {
// @ts-ignore
return React.createElement(Component, Object.assign({}, this.props, context));
};
}
hoistNonReactStatics(WithFieldContext, Component);
return WithFieldContext as any;
}
export interface ModifierProps {
proj(input: FieldProps): FieldProps;
children?: React.ReactNode;
}
// Модификация контекста
export class Modifier extends React.Component<ModifierProps> {
render() {
return <Consumer>{this.renderer}</Consumer>;
}
renderer = context => <Provider value={this.props.proj(context)}>{this.props.children}</Provider>
}
// Флаги активности
export interface DisabledRecord {
[K: string]: Disabled;
}
export type Disabled = boolean|DisabledRecord;
// Ошибки в форме
export interface ErrorRecord {
[K: string]: Error;
}
export type Error = boolean|I18nString|string|ErrorRecord;
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M272.702 359.139c-80.483-9.011-136.212-86.886-116.93-167.042l116.93 167.042zM288 392c-102.556 0-192.092-54.701-240-136 21.755-36.917 52.1-68.342 88.344-91.658l-27.541-39.343C67.001 152.234 31.921 188.741 6.646 231.631a47.999 47.999 0 0 0 0 48.739C63.004 376.006 168.14 440 288 440a332.89 332.89 0 0 0 39.648-2.367l-32.021-45.744A284.16 284.16 0 0 1 288 392zm281.354-111.631c-33.232 56.394-83.421 101.742-143.554 129.492l48.116 68.74c3.801 5.429 2.48 12.912-2.949 16.712L450.23 509.83c-5.429 3.801-12.912 2.48-16.712-2.949L102.084 33.399c-3.801-5.429-2.48-12.912 2.949-16.712L125.77 2.17c5.429-3.801 12.912-2.48 16.712 2.949l55.526 79.325C226.612 76.343 256.808 72 288 72c119.86 0 224.996 63.994 281.354 159.631a48.002 48.002 0 0 1 0 48.738zM528 256c-44.157-74.933-123.677-127.27-216.162-135.007C302.042 131.078 296 144.83 296 160c0 30.928 25.072 56 56 56s56-25.072 56-56l-.001-.042c30.632 57.277 16.739 130.26-36.928 171.719l26.695 38.135C452.626 346.551 498.308 306.386 528 256z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M569.354 231.631C512.97 135.949 407.81 72 288 72 168.14 72 63.004 135.994 6.646 231.631a47.999 47.999 0 0 0 0 48.739C63.031 376.051 168.19 440 288 440c119.86 0 224.996-63.994 281.354-159.631a47.997 47.997 0 0 0 0-48.738zM288 392c-102.556 0-192.091-54.701-240-136 44.157-74.933 123.677-127.27 216.162-135.007C273.958 131.078 280 144.83 280 160c0 30.928-25.072 56-56 56s-56-25.072-56-56l.001-.042C157.794 179.043 152 200.844 152 224c0 75.111 60.889 136 136 136s136-60.889 136-136c0-31.031-10.4-59.629-27.895-82.515C451.704 164.638 498.009 205.106 528 256c-47.908 81.299-137.444 136-240 136z"/></svg>
\ No newline at end of file
import { withFieldContext } from '~/fields/context';
export * from './context';
export { default as In } from './In';
export { default as Zoom } from './Zoom';
export { default as Label } from './Label';
export { default as ResourceID } from './ResourceID';
export { default as ArrayIDS } from './ArrayIDS';
export { default as Debounce } from './Debounce';
import BooleanField_ from './BooleanField';
import MomentField_ from './MomentField';
import SelectField_ from './SelectField';
import TextField_ from './TextField';
import AutoComplete_ from './AutoComplete';
import MultiSelect_ from './MultiSelect';
import DebounceUN from './DebounceUN';
export const BooleanField = withFieldContext(BooleanField_) as typeof BooleanField_;
export const MomentField = withFieldContext(MomentField_) as typeof MomentField_;
export const SelectField = withFieldContext(SelectField_) as typeof SelectField_;
export const TextField = withFieldContext(TextField_) as typeof TextField_;
export const AutoComplete = withFieldContext(AutoComplete_) as typeof AutoComplete_;
export const MultiSelect = withFieldContext(MultiSelect_) as typeof MultiSelect_;
export const undecorated = {
TextField: TextField_,
SelectField: SelectField_,
MomentField: MomentField_,
BooleanField: BooleanField_,
AutoComplete: AutoComplete_,
MultiSelect: MultiSelect_,
Debounce: DebounceUN,
};
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