Commit 77a24c9f by Vladislav Lagunov

Удалены нестабильные модули

parent ebb4b956
import * as React from 'react';
import * as classNames from 'classnames';
import { lighten } from '@material-ui/core/styles/colorManipulator';
// @ts-ignore
import withStyles, { WithStyles, StyleRules, StyledComponentProps } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
/** aliass */
export type Duration = number;
// props
export interface Props {
className?: string;
duration: Duration;
startAnimation?: boolean;
onPassRewinder: Function;
}
// Component
class CircleTimer extends React.Component<Props & WithStyles<ClassKey>> {
spinner: HTMLDivElement | null = null;
filler: HTMLDivElement | null = null;
mask: HTMLDivElement | null = null;
public static getStyles(duration: Duration) {
const spinnerAnimation = `rota ${duration}ms linear 1`;
const fillerAnimation = `opa ${duration}ms steps(1,end) 1 reverse`;
const maskAnimation = `opa ${duration}ms steps(1,end) 1`;
return {
spinner: {
MozAnimation: spinnerAnimation,
WebkitAnimation: spinnerAnimation,
OebkitAnimation: spinnerAnimation,
animation: spinnerAnimation,
},
filler: {
MozAnimation: fillerAnimation,
WebkitAnimation: fillerAnimation,
OAnimation: fillerAnimation,
animation: fillerAnimation,
},
mask: {
MozAnimation: maskAnimation,
WebkitAnimation: maskAnimation,
OAnimation: maskAnimation,
animation: maskAnimation,
},
};
}
componentDidMount() {
this.props.onPassRewinder && this.props.onPassRewinder(this.rewind);
}
componentWillUnmount() {
this.props.onPassRewinder && this.props.onPassRewinder(null);
}
rewind = () => { // TODO: нужно править!!!
const { spinner, filler, mask } = this;
if (spinner === null || filler === null || mask === null) return;
const elements = [ spinner, filler, mask ];
const currentAnimations = elements.map(e => e.style.animation);
elements.forEach(e => { e.style['MozAnimation'] = e.style['WebkitAnimation'] = e.style.animation = 'none'});
elements.forEach(e => e.offsetWidth); // хак https://css-tricks.com/restart-css-animation/
elements.forEach((e, i) => { e.style['MozAnimation'] = e.style['WebkitAnimation'] = e.style.animation = currentAnimations[i]});
};
render() {
const { duration, classes, className, ...other } = this.props;
const rootClass = classNames(classes.root, className);
const styles = CircleTimer.getStyles(this.props.duration || 1000);
return <div {...other} className={rootClass}>
<div className={classes.spinner} style={styles.spinner} ref={e => this.spinner = e}></div>
<div className={classes.filler} style={styles.filler} ref={e => this.filler = e}></div>
<div className={classes.mask} style={styles.mask} ref={e => this.mask = e}></div>
</div>;
}
}
export default withStyles(styles)(CircleTimer);
// CSS классы
export type ClassKey = 'root'|'spinner'|'filler'|'mask'|'rotaClass'|'opaClass'|'@keyframes rota'|'@keyframes opa';
// Styles
export function styles(theme: Theme): StyleRules<ClassKey> {
// const duration = '1s';
const size = '1.5em';
const pie = {
width: '50%',
height: '100%',
transformOrigin: '100% 50%',
position: 'absolute' as 'absolute',
background: lighten('#000', 0.7),
};
return {
root: {
position: 'relative',
display: 'inline-block',
background: 'white',
width: size,
height: size,
boxSizing: 'border-box',
'& *': {
boxSizing: 'border-box',
},
},
spinner: {
...pie,
borderRadius: '100% 0 0 100% / 50% 0 0 50%',
zIndex: 200,
borderRight: 'none',
},
filler: {
...pie,
borderRadius: '0 100% 100% 0 / 0 50% 50% 0',
left: '50%',
opacity: 0,
zIndex: 100,
borderLeft: 'none',
},
mask: {
width: '50%',
height: '100%',
position: 'absolute',
background: 'inherit',
opacity: 1,
zIndex: 300,
},
rotaClass: {
animation: 'rota 100ms linear infinite',
},
opaClass: {
animation: 'opa 100ms linear infinite',
},
'@keyframes rota': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
'@keyframes opa': {
'0%': { opacity: 1 },
'50%': { opacity: 0 },
'100%': { opacity: 0 },
},
};
}
export * from './CircleTimer';
export { default } from './CircleTimer';
import * as React from 'react';
import * as classNames from 'classnames';
import Icon from '@material-ui/core/Icon';
import { fade } from '@material-ui/core/styles/colorManipulator';
import withStyles, { StyleRules, WithStyles } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import { sprintf, dcnpgettext, pgettext, Translations } from '../utils/gettext';
/** react props */
export interface Props {
ctx: { translations: Translations };
className?: string;
limit: number;
offset: number;
total: number;
listenKeyboard?: boolean;
reactHint?: boolean;
// коллбеки
onGotoNext?(): void; /// deprecated use `onOffsetChange`
onGotoPrev?(): void; /// deprecated use `onOffsetChange`
onOffsetChange?(offset: number): void;
}
class Pagination extends React.Component<Props & WithStyles> {
unlisten: Function|null = null;
componentDidMount() {
if (this.props.listenKeyboard === true) this.listenKeyboard();
}
componentWillUnmount() {
this.unlisten && this.unlisten();
}
componentWillReceiveProps(nextProps: Props) {
if (this.props.listenKeyboard && !nextProps.listenKeyboard) this.unlisten && this.unlisten();
if (!this.props.listenKeyboard && nextProps.listenKeyboard) this.listenKeyboard();
}
listenKeyboard() {
window.addEventListener('keydown', this.handleKeyDown);
this.unlisten = () => window.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowRight' && e.ctrlKey) {
this.handleRight();
}
if (e.key === 'ArrowLeft' && e.ctrlKey) {
this.handleLeft();
}
if (e.key === 'ArrowUp' && e.ctrlKey) {
this.handleUp();
}
if (e.key === 'ArrowDown' && e.ctrlKey) {
this.handleDown();
}
};
handleLeft = () => {
const { offset, limit, onOffsetChange, onGotoPrev } = this.props;
if (offset <= 0) return;
onGotoPrev && onGotoPrev();
const nextOffset = Math.max(offset - limit, 0);
offset !== nextOffset && onOffsetChange && onOffsetChange(nextOffset);
}
handleRight = () => {
const { offset, limit, onOffsetChange, onGotoNext, total } = this.props;
const nextOffset = offset + limit;
if (nextOffset >= total) return;
onGotoNext && onGotoNext();
offset !== nextOffset && onOffsetChange && onOffsetChange(nextOffset);
};
handleUp = () => {
const { offset, limit, total, onOffsetChange } = this.props;
const nextOffset = total % limit === 0 ? total - limit : Math.floor(total / limit) * limit;
offset !== nextOffset && onOffsetChange && onOffsetChange(nextOffset);
};
handleDown = () => {
const { offset, onOffsetChange } = this.props;
offset !== 0 && onOffsetChange && onOffsetChange(0);
};
render() {
const { className: classNameProps, limit, offset, total, reactHint, listenKeyboard, classes, ctx } = this.props;
const __ = k => pgettext(ctx, 'pagination', k);
const nextDisabled = offset + limit >= total;
const prevDisabled = offset <= 0;
const to = offset + limit > total ? total : offset + limit;
const className = classNames(classes.root, classNameProps);
const prevClassName = classNames(classes.button, prevDisabled ? classes.disabled : undefined);
const nextClassName = classNames(classes.button, nextDisabled ? classes.disabled : undefined);
const itemsClaim = sprintf(
dcnpgettext(
ctx.translations,
'messages',
'pagination',
'<b>%d</b>&ndash;<b>%d</b> of <b>%d</b>',
'<b>%d</b>&ndash;<b>%d</b> of <b>%d</b>',
total,
),
offset + 1,
to,
total
);
const prevHint = reactHint && listenKeyboard ? __('Go back (CTRL+LEFT)') : undefined;
const nextHint = reactHint && listenKeyboard ? __('Go forward (CTRL+RIGHT)') : undefined;
return (
<div className={className}>
<span dangerouslySetInnerHTML={{ __html: itemsClaim }} className={classes.claim}/>
<div className={classes.navigationGroup}>
<span className={prevClassName} onClick={this.handleLeft} data-rh={prevHint}><Icon>navigate_before</Icon></span>
<span className={nextClassName} onClick={this.handleRight} data-rh={nextHint}><Icon>navigate_next</Icon></span>
</div>
</div>
);
}
}
export default withStyles(styles)(Pagination);
// Styles
export function styles(theme: Theme): StyleRules {
const { unit } = theme.spacing;
const borderRadius = 2;
const buttonHeight = 36;
const buttonWidth = buttonHeight * 1.0;
return {
root: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
navigationGroup: {
boxShadow: '0 1px 6px rgba(0,0,0,.12),0 1px 4px rgba(0,0,0,.12)',
background: 'white',
borderRadius: borderRadius,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
tratatata: '',
marginLeft: unit * 2,
'& > * + *': {
borderLeft: `solid 1px ${theme.palette.divider}`,
},
'& > *:first-child': {
borderLeft: 'none',
borderTopLeftRadius: borderRadius,
borderBottomLeftRadius: borderRadius,
},
'& > *:last-child': {
borderTopRightRadius: borderRadius,
borderBottomRightRadius: borderRadius,
},
},
claim: {
fontSize: 14,
color: theme.palette.text.primary,
'& b': {
fontWeight: 500,
},
},
button: {
height: buttonHeight,
width: buttonWidth,
textAlign: 'center',
background: 'white',
color: theme.palette.text.secondary,
userSelect: 'none',
cursor: 'pointer',
'&:hover': {
background: fade(theme.palette.text.primary, 0.1),
color: theme.palette.text.primary,
},
'& > *': {
display: 'inline-block',
verticalAlign: 'middle',
},
'&:after': {
content: '""',
display: 'inline-block',
verticalAlign: 'middle',
width: 0.1,
height: '100%',
}
},
disabled: {
background: 'white !important',
color: `${theme.palette.text.secondary} !important`,
cursor: 'not-allowed !important',
}
};
};
export * from './Pagination';
export { default } from './Pagination';
import * as React from 'react';
import * as classNames from 'classnames';
import { fade } from '@material-ui/core/styles/colorManipulator';
import CircularProgress from '@material-ui/core/CircularProgress';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { StandardProps } from '@material-ui/core';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
// Props
export type Props = StandardProps<React.HTMLProps<HTMLDivElement>, ClassKey> & {
pending: boolean;
}
// Component
function PendingOverlay(props: Props & WithStyles<ClassKey>) {
const { classes, className, ...rest } = props;
const rootClass = classNames(classes.root, className, { [classes.pending]: props.pending });
// @ts-ignore
return <div {...rest} className={rootClass}>{props.pending && <CircularProgress/>}</div>;
}
export default withStyles(styles)(PendingOverlay);
// CSS классы
export type ClassKey = 'root'|'pending';
// Styles
export function styles(theme: Theme): StyleRules<ClassKey> {
return {
root: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: fade(theme.palette.background.paper, 0.57),
zIndex: 1000,
visibility: 'hidden',
'&$pending': {
visibility: 'visible',
},
},
pending: {},
};
}
export * from './PendingOverlay';
export { default } from './PendingOverlay';
import * as React from 'react';
import * as classNames from 'classnames';
import { CircularProgress } from '@material-ui/core';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
// Props
export interface Props {
className?: string;
}
// Component
export function Spinner(props: Props & WithStyles<ClassKey>) {
const { classes, className } = props;
const rootClass = classNames(classes.root, className);
return <div className={rootClass}>
<div className={classes.wrapper}>
<CircularProgress size={32} color="secondary"/>
<h2 className={classes.title}>Please, wait…</h2>
</div>
</div>;
}
export default withStyles(styles)(Spinner);
// CSS классы
export type ClassKey = 'root'|'title'|'wrapper';
// Styles
export function styles(theme: Theme): StyleRules {
return {
root: {
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
},
title: {
...theme.typography.title,
},
wrapper: {
width: '100%',
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
textAlign: 'center',
},
};
}
export * from './Spinner';
export { default } from './Spinner';
[Dolphin]
SortOrder=1
Timestamp=2018,10,31,13,49,36
Version=3
ViewMode=1
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 "Confirm deletion"
msgstr "Подтвердите удаление"
msgid "Are you sure you want to delete this item?"
msgstr "Вы действительно хотите удалить этот объект?"
msgid "Cancel"
msgstr "Отмена"
msgid "Confirm"
msgstr "Подтвердить"
import * as React from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core';
import { DialogProps } from '@material-ui/core/Dialog';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import * as classNames from 'classnames';
import Button from '@material-ui/core/Button';
import { LocaleCtx, Gettext, withGettext } from '../gettext';
// Props
export interface Props {
__: Gettext;
ctx: LocaleCtx;
className?: string;
children: React.ReactNode;
Confirmation: React.ReactType<ConfirmationProps>;
}
// State
export interface State {
showConfirmation: boolean;
args: any[];
}
// Component
export class WithConfirmation extends React.Component<Props & WithStyles, State> {
state: State = { showConfirmation: false, args: [] };
handleClick = (...args) => {
this.setState({ args, showConfirmation: true });
}
handleConfirm = () => {
const { children } = this.props;
const { args } = this.state;
if (children && children.hasOwnProperty('props') && typeof(children['props'].onClick) === 'function') children['props'].onClick.apply(undefined, args);
this.setState({ showConfirmation: false, args: [] });
};
handleCancel = () => {
this.setState({ showConfirmation: false, args: [] });
};
render() {
const { __, ctx, classes, className, Confirmation, children: childrenProp } = this.props;
const { showConfirmation } = this.state;
const rootClass = classNames(classes.root, className);
const children = childrenProp && childrenProp.hasOwnProperty('type') ? React.cloneElement(childrenProp as any, { onClick: this.handleClick }) : childrenProp;
return <div className={rootClass}>
{children}
<Confirmation __={__}ctx={ctx} open={showConfirmation} onClose={this.handleCancel} onConfirm={this.handleConfirm}/>
</div>;
}
}
export default withStyles(styles)(withGettext(require('./i18n'))(WithConfirmation));
// Props
export type ConfirmationProps = DialogProps & {
__: Gettext;
onConfirm?();
}
// Component
export const DeleteConfirmation = withStyles(styles)((props: ConfirmationProps & WithStyles) => {
const { __, classes, onConfirm, ...other } = props;
return (
<Dialog {...other}>
<DialogTitle>{__('Confirm deletion')}</DialogTitle>
<DialogContent>
{__('Are you sure you want to delete this item?')}
</DialogContent>
<DialogActions>
<Button onClick={other.onClose}>{__('Cancel')}</Button>
<Button onClick={onConfirm} color="primary">{__('Confirm')}</Button>
</DialogActions>
</Dialog>
);
});
// Styles
export function styles(theme: Theme): StyleRules {
return {
};
}
import * as React from 'react';
import memoize from '../functions/memoize';
import { ObjectKey, ObjectPath } from '../functions/get-at';
import { isEqual } from 'lodash';
import { AuthCtx as Ctx } from '~/context';
import { I18nString } from '../gettext';
// Контекст поля
export interface FieldProps<T = any> {
ctx?: Ctx;
value: T;
disabled?: Disabled;
// Здесь должен передавться результат `validate` те `Validation<A>`
error?: Error;
onValueChange?(value: T): void;
onValueChange?(value: any, at?: ObjectPath): void;
}
// Поддерживаемые действия с формой
export type Model<T> ={
initial: T;
modified: T|null;
};
// Поддерживаемые действия с формой
export type FormAction =
| { tag: 'Reset' }
| { tag: 'Change', value, at }
// Флаги активности
export type Disabled = boolean|object;
// Ошибки в поле
export type Error = boolean|string|object|I18nString;
// Валидация формы
export type Validation<A> = Partial<Record<keyof A, Error>>;
// Props
export type CardProps<T> = {
ctx?: any,
model: {
initial: T;
modified: T|null;
};
dispatch(action: FormAction): void;
}
// Опции для `createForm`
export type CreateFormOptions<T> = {
validate?(value: T, instance): any;
disabled?(value: T, instance): any;
};
export default function createForm<O extends CreateFormOptions<any>>(options: O) {
type T = O extends CreateFormOptions<infer T> ? T : never;
function createZoom<T2 = T>(self: React.Component<CardProps<T>>) {
function zoom(): FieldProps<T2>;
function zoom<K1 extends keyof T2>(...keys: [K1]): FieldProps<T2[K1]>;
function zoom<K1 extends keyof T2, K2 extends keyof T2[K1]>(...keys: [K1, K2]): FieldProps<T2[K1][K2]>;
function zoom<K1 extends keyof T2, K2 extends keyof T2[K1], K3 extends keyof T2[K1][K2]>(...keys: [K1, K2, K3]): FieldProps<T2[K1][K2][K3]>;
function zoom(...keys) {
const { dispatch, ctx } = self.props;
const value = self.props.model.modified || self.props.model.initial;
const error = options.validate ? options.validate(value, self) : {};
const disabled = options.disabled ? options.disabled(value, self) : {};
const onValueChange = (value, at: ObjectPath=[]) => dispatch({ tag: 'Change', value, at });
const zoomedError = zoomAny(keys, error);
return {
ctx,
value: zoomAny(keys, value),
error: typeof(zoomedError) === 'function' && ctx ? zoomedError(ctx) : zoomedError,
disabled: zoomAny(keys, disabled),
onValueChange: zoomOnChange(keys, onValueChange),
};
}
return zoom;
}
function set<M extends CardProps<T>['model']>(model: M, form: T): M {
// @ts-ignore
return isEqual(model.initial, form) ? { ...model, modified: null } : { ...model, modified: form };
}
function get<M extends CardProps<T>['model']>(model: M): T {
// @ts-ignore
return model.modified || model.initial;
}
function modify<M extends CardProps<T>['model']>(model: M, f: (form: T) => T): M {
return set(model, f(get(model)));
}
// @ts-ignore
return { ...options, createZoom, set, get, modify } as { createZoom: typeof createZoom, set: typeof set, get: typeof get, modify: typeof modify } & O;
}
const zoomOnChange = memoize((path: ObjectKey[], onChange: ((x: any, at: ObjectPath) => void)|undefined) => {
return (next, at=[]) => {
onChange && onChange(next, [...path, ...at]);
};
});
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;
});
// Хелперы
export type Any<T=any> = React.ReactType<FieldProps<T>>;
export type GetProps<T extends Any> = T extends React.ReactType<infer Props> ? Props : never;
import memoize from '../functions/memoize';
import { ObjectKey, ObjectPath } from '../functions/get-at';
import { FieldProps } from '../create-form';
// Опции для `createForm`
export type CreateZoomOptions<T> = {
validate?(value: T): any;
disabled?(value: T): any;
getValue: () => T;
onValueChange(value: T): void;
onValueChange(value: unknown, at: ObjectPath): void;
};
// Функция-хелпер для подключения полей ввода
export type Zoom<T> = {
(): FieldProps<T>;
<K1 extends keyof T>(...keys: [K1]): FieldProps<T[K1]>;
<K1 extends keyof T, K2 extends keyof T[K1]>(...keys: [K1, K2]): FieldProps<T[K1][K2]>;
<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(...keys: [K1, K2, K3]): FieldProps<T[K1][K2][K3]>;
<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3]>(...keys: [K1, K2, K3, K4]): FieldProps<T[K1][K2][K3][K4]>;
};
// Функция-хелпер для подключения полей ввода
export default function createZoom<T>(options: CreateZoomOptions<T>): Zoom<T> {
const { onValueChange, getValue } = options;
return (...keys) => {
const value = getValue();
const error = options.validate ? options.validate(value) : {};
const disabled = options.disabled ? options.disabled(value) : {};
const zoomedError = zoomAny(keys, error);
return {
value: zoomAny(keys, value),
error: zoomedError,
disabled: zoomAny(keys, disabled),
onValueChange: zoomOnChange(keys, onValueChange),
};
};
}
const zoomOnChange = memoize((path: ObjectKey[], onChange: ((x: any, at: ObjectPath) => void)|undefined) => {
return (next, at=[]) => {
onChange && onChange(next, [...path, ...at]);
};
});
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;
});
import { Err, AuthCtx } from "~/context";
import { Cmd, noop, Eff, cmd } from "../../core";
/**
* Экшен для управления флагом загрузки
*/
export type PendingAction =
| { tag: '@Pending', pending: boolean }
// Pending
export function pending<A>(command: Cmd<A>): Cmd<A> {
return cmd.concat(
cmd.of({ tag: '@Pending', pending: true } as never),
command,
cmd.of({ tag: '@Pending', pending: false } as never),
);
}
// Контекст
export type Ctx<A> = (AuthCtx | {}) & {
options: Options<A>;
};
// Options
export type Options<A> = {
source: Source<A>;
printItem?(x: A): string;
limit?: number;
openOnFocus?: boolean;
openOnClick?: boolean;
};
// Параметры для `pageCollection`
export interface AutoCompleteQuery {
offset: number;
limit: number;
search: string
}
export const defaultQuery = { offset: 0, search: '', limit: 20 };
// Источник для автодополнений
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: 'More' }
| { tag: 'Query', query: AutoCompleteQuery }
| { tag: 'Query/success', suggestions: A[], total: number, query: AutoCompleteQuery }
| { tag: 'Focus' }
| { tag: 'Blur' }
| { tag: 'Close' }
| { tag: 'Open' }
| { tag: 'Click' }
// 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 || (x => JSON.stringify(x));
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, total: source.length }, 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: { ...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': {
if (ctx.options.openOnFocus && !ctx.options.openOnClick) {
// Не открываем попап на фокусе если указан
// `ctx.options.openOnClick`, тк он откроется на клике
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 })];
}
case 'Click': {
if (ctx.options.openOnClick) {
if (state.open) return [{ ...state, open: false }, noop];
return [state, cmd.of<Action<A>>({ tag: 'Query', query: defaultQuery })];
}
return [state, noop];
}
}
}
// 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 '../create-form';
import * as classNames from 'classnames';
import { StandardProps } from '@material-ui/core';
// Props
export type Props = StandardProps<SwitchProps, ClassKey, 'value'|'disabled'> & FieldProps<boolean> & {
alignLeft?: boolean;
}
// Component
// @ts-ignore
@withStyles(styles)
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 (
<Switch
{...rest}
className={rootClass}
checked={value}
onChange={this.handleChange}
disabled={Boolean(disabled)}
data-rh-at="right"
data-rh={typeof(error) === 'string' || typeof(error) === 'function' ? typeof(error) === 'string' ? error : error(ctx) : undefined}
/>
);
}
}
export default 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';
// 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 => ({ value: undefined }));
}
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: this.state.value !== undefined ? this.state.value : this.props.children.props.value,
onValueChange: this.handleChange,
onBlur: this.handleBlur,
});
}
}
export default Debounce;
import * as React from 'react';
import Icon, { IconProps } from '@material-ui/core/Icon';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import * as classNames from 'classnames';
// Component
class IconComponent extends React.Component<IconProps & WithStyles<string>> {
render() {
const { classes, className } = this.props;
const rootClass = classNames(className, {
[classes.button]: 'onClick' in this.props
});
// @ts-ignore
return React.createElement(Icon, { ...this.props, className: rootClass });
}
}
export default withStyles(styles)(IconComponent);
// Styles
export function styles(theme: Theme): StyleRules {
// const { unit } = theme.spacing;
return {
button: {
cursor: 'pointer',
color: theme.palette.text.secondary,
'&:hover': {
color: theme.palette.text.primary,
}
},
};
}
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, { StyleRules } from '@material-ui/core/styles/withStyles';
import { FieldProps } from '../create-form';
import * as moment from 'moment';
import TextField, { Props as TextFieldProps } from './TextField';
import { StandardProps, Popover } from '@material-ui/core';
import Icon from './Icon';
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import DayPickerSingleDateController from 'react-dates/lib/components/DayPickerSingleDateController';
// Props
export type Props = StandardProps<React.HTMLProps<HTMLDivElement>, ClassKey, 'value'|'disabled'> & FieldProps<moment.Moment|null> & {
InputProps?: Partial<TextFieldProps>;
format?: string;
nonNull?: boolean;
dayPickerProps?: Partial<Record<string, any>>;
}
// Model
export interface State {
textFieldValue: string|null;
open: boolean;
value: null|string;
}
// Component
// @ts-ignore
@withStyles(styles)
class MomentField extends React.Component<Props, State> {
static defaultProps = {
nonNull: false,
} as any;
rootEl: HTMLElement|null;
pickerEl: HTMLElement|null;
state: State = { open: false, textFieldValue: null, value: null };
unlisten: Function|null;
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, ctx, error, InputProps, onBlur, onChange, onFocus, value, format: formapProps, disabled, nonNull, dayPickerProps, ...rest } = this.props;
const { rootEl } = this;
const { open } = this.state;
const self = this;
const endAdornment = <React.Fragment>
<Icon data-ignore-focus-in onClick={this.handleVisibilityToggle}>date_range</Icon>
{!nonNull && <Icon data-ignore-focus-in onClick={this.handleClear}>close</Icon>}
</React.Fragment>;
return <div {...rest} ref={this.handleRootRef}>
<TextField
{...InputProps as any}
disabled={disabled}
error={error}
className={classes!.input}
onValueChange={this.handleChange}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
endAdornment={endAdornment}
value={inputValue(this.state, this.props)}
/>
{rootEl && <Popover anchorEl={rootEl} open={open} onClose={this.handleClose} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} disableAutoFocus disableEnforceFocus disableRestoreFocus hideBackdrop>
<DayPickerSingleDateController
ref={this.handlePickerRef}
date={value}
onDateChange={this.handleDatesChange}
numberOfMonths={1}
{...dayPickerProps}
/>
</Popover>}
</div>;
function inputValue(state: State, input: FieldProps) {
return state.value !== null ? state.value : input.value ? input.value.format(self.format(self.props)) : '';
}
}
}
export default MomentField;
// CSS классы
export type ClassKey = 'input';
// Styles
export function styles(theme: Theme): StyleRules<ClassKey> {
// const { unit } = theme.spacing;
return {
input: {
width: 180,
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 '../create-form';
// Props
export type Props<A extends any[]> = StandardProps<React.HTMLProps<HTMLDivElement>, string, 'value'|'disabled'> & FieldProps<A> & {
source: Source<A[number]>;
fullWidth?: boolean;
textFieldProps?: Partial<TextFieldProps>;
renderItem?(a: A[number], selected: boolean): React.ReactNode;
placeholder?: string;
printItem?(a: A[number]): string;
id?: string; // Используется для установки `key` пропсов
isEqual?(a: A[number], b: A[number]): boolean;
}
// @ts-ignore Component
@withStyles(styles)
export default class MultiSelect<A extends any[]> extends React.Component<Props<A>> {
static defaultProps = {
printItem: String,
} as any;
handleDelete = memoize((idx: number) => (e?: React.MouseEvent<HTMLDivElement>) => {
const { value, onValueChange, disabled } = this.props; if (disabled) return;
// @ts-ignore
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;;
// @ts-ignore
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 });
// @ts-ignore
return !value.length ? null : <div><span className={classes!.chips}>
{(value as any).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 { renderItem, printItem, value } = this.props;
// @ts-ignore
const selected = !!value!.find(x => this.isEqual(x, item));
const id = this.props.id || 'id';
if (renderItem) return renderItem(item, selected);
return <MenuItem key={item[id]} disableRipple>
<Checkbox disableRipple checked={selected}/>
<ListItemText primary={(printItem || String)(item)}/>
</MenuItem>;
};
render() {
const { ctx, classes, fullWidth, className, source, renderItem, printItem, id, textFieldProps, value, error, disabled, onValueChange, placeholder, ...rest } = this.props;
const rootClass = classNames(className, classes!.root, {
[classes!.disabled!]: disabled,
[classes!.fullWidth!]: fullWidth,
});
return <div {...rest} className={rootClass}>
{this.renderChips(value)}
<AutoComplete<A|null>
ctx={ctx}
value={null}
printItem={printItem}
source={source}
// @ts-ignore
renderItem={this.renderSuggestion}
onValueChange={this.handleValueChange}
textFieldProps={textFieldProps}
placeholder={placeholder}
error={error}
disabled={disabled}
fullWidth={fullWidth}
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',
},
fullWidth: {
width: '100%',
},
};
}
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 '../create-form';
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 '../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<SelectProps, ClassKey, 'disabled'> & FieldProps<A> & {
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 { ctx, source, classes, error, nullCase, disabled, renderItem, 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 {...rest} value={valueIdx} onChange={this.handleChange} className={rootClass} disabled={Boolean(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 '../PendingOverlay';
import { LocaleCtx, Gettext, withGettext } from '../gettext';
// Props
export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string> & WithStyles<string> & {
__: Gettext;
ctx: LocaleCtx;
suggestions: A[];
renderSuggestion?(a: A): React.ReactElement<MenuItemProps>|string;
anchorEl?: HTMLElement;
open?: boolean;
marginThreshold?: number;
pending?: boolean;
dontMove?: boolean;
before?: React.ReactNode;
after?: React.ReactNode;
// 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) {
this.paperEl.style.bottom = - (anchorRect.top - 8) + 'px';
} else {
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, before, after } = this.props;
const PaperProps = { ref: this.handlePaperRef, className: classes.paper };
return <Modal className={classes.modal} open={!!open} onClose={this.handleClose} disableAutoFocus disableEnforceFocus disableRestoreFocus hideBackdrop>
<Paper {...PaperProps}>
{!!suggestions.length && <MenuList className={classes.menu}>{before}{this.renderSuggestions(suggestions)}{after}</MenuList>}
{!suggestions.length && <div className={classes.empty}><span>{__('Nothing found…')}</span></div>}
<PendingOverlay pending={pending || false}/>
</Paper>
</Modal>;
}
}
export default withStyles(styles)(withGettext(require('./i18n'))(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] as any,
textAlign: 'center',
fontStyle: 'italic',
width: '100%',
},
};
}
import * as React from 'react';
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 '../create-form';
import { StandardProps } from '@material-ui/core';
import { withGettext, Gettext, LocaleCtx } from '../gettext';
const eye = require('./eye.svg');
const eyeSlash = require('./eye-slash.svg');
// Props
export type Props = StandardProps<React.HTMLProps<HTMLInputElement>, ClassKey, 'ref'|'disabled'|'value'> & FieldProps<string> & {
__: Gettext;
ctx?: LocaleCtx;
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
// @ts-ignore
@withStyles(styles)
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 as Props & WithStyles<ClassKey>;
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 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}
data-rh-at="right"
data-rh={typeof(error) === 'string' || typeof(error) === 'function' ? typeof(error) === 'string' ? error : error(ctx) : undefined}
/>
{endAdornment}
</div>;
}
}
export default withGettext(require('./i18n'))(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,
boxSizing: 'border-box',
display: 'inline-flex',
alignItems: 'center',
background: `rgba(0,0,0,0.04)`,
borderRadius: 3,
position: 'relative',
paddingRight: unit * 0.5,
'&$error': {
background: `rgba(255,0,0,0.08)`,
'& .material-icons': {
color: `rgba(255,0,0,0.54)`,
},
},
'& .material-icons': {
fontSize: 18,
},
},
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: 14,
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',
fontSize: 14,
},
'&$error::placeholder': {
color: `rgba(255,0,0,0.54)`,
},
},
textarea: {
height: 'auto',
width: '100%',
padding: 0,
'& textarea': { padding: unit },
},
eye: {
width: 22,
},
error: {
},
fullWidth: {
width: '100%',
},
};
}
<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
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 "Загрузить еще"
export { default as Label } from './Label';
export { default as Debounce } from './Debounce';
export { default as BooleanField } from './BooleanField';
export { default as MomentField } from './MomentField';
export { default as SelectField } from './SelectField';
export { default as TextField } from './TextField';
export { default as AutoComplete } from './AutoComplete';
export { default as MultiSelect } from './MultiSelect';
## Пример использования
```ts
import { Transaction, withGettext, I18nString } from '~/gettext';
const translations = Transaction.fromJed(require('./i18n'));
// Отложенный перевод строк
function printStatus(status: 'buzy'|'idle'): I18nString {
const { __ } = translations;
switch (status) {
case 'buzy': return __('Buzy');
case 'idle': return __('Idle');
}
}
// Тип I18nString
// type I18nString = (locale?: string|LocaleCtx|null) => string;
// те функция, которая вернет строку если ей передать нужную локаль
console.log(printStatus('buzy')('ru-RU')); // => 'Занят'
// Эти поля должны присутствовать в `Props` для использования `withGettext`
export type Props = {
__: Gettext;
ctx: LocaleCtx;
};
class Widget extends React.Component<Props> {
render() {
const { __ } = this.props;
return <div>
{__('Hello, World!')}
</div>;
}
}
export default withGettext(translations)(Widget);
// Или так
// export default withGettext(require('./i18n'))(Widget);
```
import * as React from 'react';
import { Omit } from '@material-ui/core';
import memoize from '../functions/memoize';
// Делимитер контекста в jed-формате
const CONTEXT_DELIMITER = String.fromCharCode(4);
// Translations
export type TranslationsData = Record<string, Record<string, string[]|string>>;
export type POData = Record<string, string[]|string>;
export type ES6Module<T> = { default: T, __esModule };
// Хелпер для перевода
export type Gettext = (singular_key: string, plural_key?: string, context?: string, val?: number) => string;
export type I18nString = (locale?: string|LocaleCtx|null) => string;
export class Translations {
constructor(
readonly data: TranslationsData,
) {}
static fromPO(...data: POData[]): Translations;
static fromPO(...data: ES6Module<POData>[]): Translations;
static fromPO(...data: POData[][]): Translations;
static fromPO(...data: ES6Module<POData>[][]): Translations;
static fromPO(...webpackContexts) {
const podata: POData[] = flattenModules(webpackContexts);
return new Translations(assignData({}, ...podata));
function flattenModules(xs: any[]) {
return Array.prototype.concat.apply([], xs.map(x => {
if ('__esModule' in x) x = x.default;
return Array.isArray(x) ? flattenModules(x) : [x]
}));
}
}
__: (singular_key: string, plural_key?: string, context?: string, n?: number) => I18nString = (singular_key, plural_key?, context?, n?) => {
return localeOrCtx => {
let locale = !localeOrCtx ? 'en-US' : typeof(localeOrCtx) === 'string' ? localeOrCtx : localeOrCtx.locale;
locale = locale.replace(/_/, '-');
if (!(locale in this.data)) {
// Не найдена локаль в `this.data`
return n === 1 ? singular_key : (plural_key || singular_key);
}
const key = context ? context + CONTEXT_DELIMITER + singular_key : singular_key;
const dict = this.data[locale];
const pluralIdx = getPluralForm(locale)(n || 1);
const pluralList = dict[key] || [singular_key, plural_key];
return nth(pluralList, pluralIdx)!;
};
function nth<A>(xs: A|A[], idx: number): A {
if (!Array.isArray(xs)) return xs;
return idx >= xs.length ? xs[xs.length - 1] : xs[idx];
}
}
bind(localeOrCtx?: string|LocaleCtx|null): Gettext {
return (...args) => {
return this.__(...args)(localeOrCtx);
};
}
}
// Использование локали из `ctx`
export type LocaleCtx = { locale: string };
/**
* HOC для добавления переводов (для разделения переводов по фичам)
* ```ts
* export default withGettext(require('./i18n'))();
* ```
*/
export function withGettext(...webpackContexts) {
const translations: Translations = webpackContexts[0] instanceof Translations ? webpackContexts[0] : Translations.fromPO(...webpackContexts);
return <P extends { __: Gettext, ctx?: { locale: string } }>(Component: React.ComponentType<P>) => {
class WithTranslations extends React.Component<Omit<P, '__'>> {
makeGettext: (locale?: string|LocaleCtx) => Gettext = memoize((locale) => translations.bind(locale))
render() {
const { ctx } = this.props as any;
// @ts-ignore
const locale = ctx ? (ctx.locale || 'en-US') : 'en-US';
return React.createElement(Component as any, { ...this.props as any, __: this.makeGettext(locale) });
}
}
// @ts-ignore
WithTranslations.displayName = 'WithTranslations(' + Component.name + ')';
require('hoist-non-react-statics')(WithTranslations, Component);
return WithTranslations;
};
}
/**
* Добавление переводов из `srcs` в `dst`
*/
function assignData(dst: TranslationsData, ...srcs: POData[]): TranslationsData {
srcs.forEach(data => {
let is_jed_1_format = false;
if (data['locale_data']) {
is_jed_1_format = true;
data = data['locale_data']['messages'];
}
// @ts-ignore
const locale_ = data[''].language || data[''].lang || data[''].locale; if (!locale_ || typeof(locale_) !== 'string') return;
const locale = locale_.replace(/_/g, '-');
for (const k of Object.keys(data)) {
if (k === '') continue;
dst[locale] = dst[locale] || {};
if (Array.isArray(data[k])) {
dst[locale][k] = is_jed_1_format ? data[k] : data[k].slice(1);
if (dst[locale][k].length === 1) dst[locale][k] = dst[locale][k][0];
} else {
dst[locale][k] = data[k];
}
}
});
return dst;
}
/**
* Минималистичный `sprintf`. Только простая замена %s на позиционные аргументы
*/
export function sprintf(format: string, ...args: any[]): string {
var i = 0;
return format.replace(/%(s|d)/g, function() {
return args[i++];
});
}
export namespace deferred {
/**
* Минималистичный `sprintf`. Только простая замена %s на позиционные аргументы
*/
export function sprintf(format: I18nString, ...args: any[]): I18nString {
return locale => {
var i = 0;
return format(locale).replace(/%(s|d)/g, function() {
return args[i++];
});
}
}
}
/**
* Вычисление множественной формы для указанного языка. Если язык не
* распознан применяются правила для
* https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#Plural_rule_1_(2_forms)
* http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
*/
export function getPluralForm(locale: string): (n: number) => number {
const [lang] = locale.split(/[_\-]/);
// Славянские языки, 3 формы
if (lang === 'ru' || lang === 'be' || lang === 'bs' || lang === 'hr' || lang === 'sh' || lang === 'sr_ME' || lang === 'uk') {
return n => (n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2);
}
// Литовский
if (lang === 'lt') {
return n => (n % 10 == 1 && (n % 100 < 11 || n % 100 > 19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? 1 : 2);
}
// Арабский 6 форм
if (lang === 'ar' || lang === 'ars') {
return n => (n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n % 100 >= 3 && n % 100 <= 10) ? 3 : ((n % 100 >= 11 && n % 100 <= 99) ? 4 : 5))));
}
// Бретонский 5 форм
if (lang === 'br') {
return n => (n % 10 == 1 && n % 100 != 11 && n % 100 != 71 && n % 100 != 91) ? 0 : ((n % 10 == 2 && n % 100 != 12 && n % 100 != 72 && n % 100 != 92) ? 1 : ((((n % 10 == 3 || n % 10 == 4) || n % 10 == 9) && (n % 100 < 10 || n % 100 > 19) && (n % 100 < 70 || n % 100 > 79) && (n % 100 < 90 || n % 100 > 99)) ? 2 : ((n != 0 && n % 1000000 == 0) ? 3 : 4)));
}
// Чешский и словакский, 3 формы
if (lang === 'cz' || lang === 'sk') {
return n => (n == 1) ? 0 : ((n >= 2 && n <= 4) ? 1 : 2);
}
// Уельский, 6 форм
if (lang === 'cy') {
return n => (n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));
}
// Словенский, лужицкие сербы, 4 формы
if (lang === 'sl' || lang === 'dsb' || lang === 'hsb') {
return n => (n % 100 == 1) ? 0 : ((n % 100 == 2) ? 1 : ((n % 100 == 3 || n % 100 == 4) ? 2 : 3));
}
// TODO http://mlocati.github.io/cldr-to-gettext-plural-rules/
return n => n == 1 ? 0 : 1;
}
import { Update } from './';
import Monad from 'burrido';
// Do notation
export const { Do } = Monad({
pure: Update.of,
bind: (m, proj) => m.chain(proj),
});
Update.Do = Do;
declare module "./" {
interface UpdateStatic {
Do<State, Result>(iter: () => IterableIterator<Update<Error, any, any>>): Update<Error, State, Result>;
}
interface BoundStatics<Error, State> {
Do<Result>(iter: () => IterableIterator<Update<Error, any, any>>): Update<Error, State, Result>;
}
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
</head>
<body>
<div id="app"/>
</body>
<script type="text/javascript" src="entry.bundle.js"></script>
</html>
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Update } from '../';
import '../burrido';
import * as http from '../../http';
import { InputHTMLAttributes } from 'react';
type Err = http.HttpError;
type Props = {
};
type State = {
pending: boolean;
search: string;
response?: http.Response;
};
const U = Update.bind<Err, State>();
class Widget extends Update.Component<Err, Props, State> {
state: State = { search: '', pending: false };
handleNameChange: InputHTMLAttributes<HTMLInputElement>['onChange'] = (e) => this.setState({ search: e.target.value });
handleSubmit = (e: React.FormEvent) => this.setState(U.Do<void>(function*() {
const { search: q }: State = yield U.get;
const response = yield U.effect(http.get(http.join('https://api.github.com/search/repositories', { q }))).pending();
yield U.patch({ response });
}));
render() {
const { search, response, pending } = this.state;
return <div>
<form onSubmit={e => (e.preventDefault(), !pending && this.handleSubmit(e))}>
<input value={search} onChange={this.handleNameChange} placeholder="Github search"/>
<button type="submit" disabled={pending}>{pending ? 'Please, wait…' : 'Go'}</button>
</form>
<textarea value={JSON.stringify(response)} readOnly/>
</div>;
}
}
ReactDOM.render(<Widget/>, document.getElementById('app'));
{
"name": "example",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"react": "^16.6.3",
"react-dom": "^16.6.3",
"ts-loader": "3",
"typescript": "^3.1.6",
"webpack": "3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "2"
}
}
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
const tsConfig = require('./tsconfig.json');
module.exports = function(env={}) {
const entry = env.entry || './index.tsx';
const output = path.resolve(__dirname);
return {
context: path.resolve(__dirname),
entry: { entry: [entry], },
output: {
path: output,
filename: '[name].bundle.js',
},
module: {
loaders: [{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
compilerOptions: tsConfig.compilerOptions,
transpileOnly: env.hasOwnProperty('transpileOnly') ? env.transpileOnly : true,
},
}],
},
resolve: {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
alias: {
react: path.resolve(__dirname, 'node_modules/react'),
},
},
};
};
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "update",
"version": "1.0.0",
"main": "index.ts",
"license": "MIT",
"peerDependencies": {
"react": "^16.6.3"
},
"dependencies": {
"@types/react": "^16.7.6"
},
"optionalDependencies": {
"burrido": "^1.0.8"
}
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/prop-types@*":
version "15.5.6"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.6.tgz#9c03d3fed70a8d517c191b7734da2879b50ca26c"
"@types/react@^16.7.6":
version "16.7.6"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
burrido@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/burrido/-/burrido-1.0.8.tgz#b29684a5486ab3301149147983c006933847f324"
dependencies:
immutagen "^1.0.0"
csstype@^2.2.0:
version "2.5.7"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.7.tgz#bf9235d5872141eccfb2d16d82993c6b149179ff"
immutagen@^1.0.0:
version "1.0.8"
resolved "https://registry.yarnpkg.com/immutagen/-/immutagen-1.0.8.tgz#efc32eccab30a833496de43a4ea7aa4353e1097a"
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