Started adding frontend notifications, fixing firefox file upload bug

This commit is contained in:
2018-01-22 19:03:06 -05:00
parent f14e96c490
commit 5856052f82
1536 changed files with 109746 additions and 2658 deletions

View File

@@ -0,0 +1,36 @@
/* eslint react/require-default-props: 0 */
import React from 'react';
import PropTypes from 'prop-types';
import { css } from 'glamor';
const rule = isDefault =>
css({
color: isDefault ? '#000' : '#fff',
fontWeight: 'bold',
fontSize: '14px',
background: 'transparent',
outline: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
opacity: isDefault ? '0.3' : '0.7',
transition: '.3s ease',
alignSelf: 'flex-start',
':hover, :focus': {
opacity: 1
}
});
function DefaultCloseButton({ closeToast, type }) {
return (
<button {...rule(type === 'default')} type="button" onClick={closeToast}>
</button>
);
}
DefaultCloseButton.propTypes = {
closeToast: PropTypes.func
};
export default DefaultCloseButton;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import Transition from 'react-transition-group/Transition';
import { css } from 'glamor';
import getAnimation from './animation';
const animate = {
animationDuration: '0.75s',
animationFillMode: 'both'
};
const animation = pos => {
const { enter, exit } = getAnimation(pos);
const enterAnimation = css.keyframes('enter', {
'from, 60%, 75%, 90%, to': {
animationTimingFunction: 'cubic-bezier(0.215, 0.610, 0.355, 1.000)'
},
...enter
});
const exitAnimation = css.keyframes('exit', exit);
return {
enter: css({ ...animate, animationName: enterAnimation }),
exit: css({ ...animate, animationName: exitAnimation })
};
};
function DefaultTransition({ children, position, ...props }) {
const { enter, exit } = animation(position);
return (
<Transition
{...props}
timeout={750}
onEnter={node => node.classList.add(enter)}
onEntered={node => node.classList.remove(enter)}
onExit={node => node.classList.add(exit)}
>
{children}
</Transition>
);
}
export default DefaultTransition;

View File

@@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import { css } from 'glamor';
import { TYPE } from './constant';
import style from './style';
const trackProgress = css.keyframes('track-progress', {
'0%': { width: '100%' },
'100%': { width: 0 }
});
const progress = (type, isRunning, hide, delay) =>
css({
position: 'absolute',
bottom: 0,
left: 0,
width: 0,
height: '5px',
zIndex: style.zIndex,
opacity: hide ? 0 : 0.7,
animation: `${trackProgress} linear 1`,
animationPlayState: isRunning ? 'running' : 'paused',
animationDuration: `${delay}ms`,
backgroundColor: 'rgba(255,255,255,.7)',
...(type === 'default' ? { background: style.colorProgressDefault } : {})
});
function ProgressBar({ delay, isRunning, closeToast, type, hide, className }) {
return (
<div
{...(typeof className !== 'string'
? css(progress(type, isRunning, hide, delay), className)
: progress(type, isRunning, hide, delay))}
{...typeof className === 'string' && { className }}
onAnimationEnd={closeToast}
/>
);
}
ProgressBar.propTypes = {
/**
* The animation delay which determine when to close the toast
*/
delay: PropTypes.number.isRequired,
/**
* Whether or not the animation is running or paused
*/
isRunning: PropTypes.bool.isRequired,
/**
* Func to close the current toast
*/
closeToast: PropTypes.func.isRequired,
/**
* Optional type : info, success ...
*/
type: PropTypes.string,
/**
* Hide or not the progress bar
*/
hide: PropTypes.bool,
/**
* Optionnal className
*/
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
};
ProgressBar.defaultProps = {
type: TYPE.DEFAULT,
hide: false,
className: ''
};
export default ProgressBar;

184
goTorrentWebUI/node_modules/react-toastify/src/Toast.js generated vendored Normal file
View File

@@ -0,0 +1,184 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { css } from 'glamor';
import ProgressBar from './ProgressBar';
import { POSITION, TYPE } from './constant';
import style from './style';
import {
falseOrElement,
falseOrDelay,
objectValues
} from './util/propValidator';
const toast = type =>
css({
position: 'relative',
minHeight: '48px',
marginBottom: '1rem',
padding: '8px',
borderRadius: '1px',
boxShadow:
'0 1px 10px 0 rgba(0, 0, 0, .1), 0 2px 15px 0 rgba(0, 0, 0, .05)',
display: 'flex',
justifyContent: 'space-between',
maxHeight: '800px',
overflow: 'hidden',
fontFamily: style.fontFamily,
cursor: 'pointer',
background: style[`color${type.charAt(0).toUpperCase()}${type.slice(1)}`],
...(type === 'default' ? { color: '#aaa' } : {}),
[`@media ${style.mobile}`]: {
marginBottom: 0
}
});
const body = css({
margin: 'auto 0',
flex: 1
});
class Toast extends Component {
static propTypes = {
closeButton: falseOrElement.isRequired,
autoClose: falseOrDelay.isRequired,
children: PropTypes.node.isRequired,
closeToast: PropTypes.func.isRequired,
position: PropTypes.oneOf(objectValues(POSITION)).isRequired,
pauseOnHover: PropTypes.bool.isRequired,
closeOnClick: PropTypes.bool.isRequired,
transition: PropTypes.func.isRequired,
isDocumentHidden: PropTypes.bool.isRequired,
in: PropTypes.bool,
onExited: PropTypes.func,
hideProgressBar: PropTypes.bool,
onOpen: PropTypes.func,
onClose: PropTypes.func,
type: PropTypes.oneOf(objectValues(TYPE)),
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
bodyClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
progressClassName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
updateId: PropTypes.number
};
static defaultProps = {
type: TYPE.DEFAULT,
in: true,
hideProgressBar: false,
onOpen: null,
onClose: null,
className: '',
bodyClassName: '',
progressClassName: '',
updateId: null
};
state = {
isRunning: true
};
componentDidMount() {
this.props.onOpen !== null && this.props.onOpen(this.getChildrenProps());
}
componentWillReceiveProps(nextProps) {
if (this.props.isDocumentHidden !== nextProps.isDocumentHidden) {
this.setState({
isRunning: !nextProps.isDocumentHidden
});
}
}
componentWillUnmount() {
this.props.onClose !== null && this.props.onClose(this.getChildrenProps());
}
getChildrenProps() {
return this.props.children.props;
}
getToastProps() {
const toastProps = {};
if (this.props.autoClose !== false && this.props.pauseOnHover === true) {
toastProps.onMouseEnter = this.pauseToast;
toastProps.onMouseLeave = this.playToast;
}
typeof this.props.className === 'string' &&
(toastProps.className = this.props.className);
this.props.closeOnClick && (toastProps.onClick = this.props.closeToast);
return toastProps;
}
pauseToast = () => {
this.setState({ isRunning: false });
};
playToast = () => {
this.setState({ isRunning: true });
};
render() {
const {
closeButton,
children,
autoClose,
type,
hideProgressBar,
closeToast,
transition: Transition,
position,
onExited,
className,
bodyClassName,
progressClassName,
updateId
} = this.props;
return (
<Transition
in={this.props.in}
appear
unmountOnExit
onExited={onExited}
position={position}
>
<div
{...(typeof className !== 'string'
? css(toast(type), className)
: toast(type))}
{...this.getToastProps()}
>
<div
{...(typeof bodyClassName !== 'string'
? css(body, bodyClassName)
: body)}
{...typeof bodyClassName === 'string' && {
className: bodyClassName
}}
>
{children}
</div>
{closeButton !== false && closeButton}
{autoClose !== false && (
<ProgressBar
key={`pb-${updateId}`}
delay={autoClose}
isRunning={this.state.isRunning}
closeToast={closeToast}
hide={hideProgressBar}
type={type}
className={progressClassName}
/>
)}
</div>
</Transition>
);
}
}
export default Toast;

View File

@@ -0,0 +1,368 @@
import React, { Component, isValidElement, cloneElement } from 'react';
import PropTypes from 'prop-types';
import { css } from 'glamor';
import TransitionGroup from 'react-transition-group/TransitionGroup';
import Toast from './Toast';
import DefaultCloseButton from './DefaultCloseButton';
import DefaultTransition from './DefaultTransition';
import { POSITION, ACTION } from './constant';
import style from './style';
import EventManager from './util/EventManager';
import {
falseOrDelay,
falseOrElement,
isValidDelay,
objectValues
} from './util/propValidator';
const toastPosition = pos => {
const positionKey = pos.toUpperCase().replace('-', '_');
const positionRule =
typeof POSITION[positionKey] !== 'undefined'
? style[positionKey]
: style.TOP_RIGHT;
/** define margin for center toast based on toast witdh */
if (
positionKey.indexOf('CENTER') !== -1 &&
typeof positionRule.marginLeft === 'undefined'
) {
positionRule.marginLeft = `-${parseInt(style.width, 10) / 2}px`;
}
return css(
positionRule,
css({
[`@media ${style.mobile}`]: {
left: 0,
margin: 0,
position: 'fixed',
...(pos.substring(0, 3) === 'top' ? { top: 0 } : { bottom: 0 })
}
})
);
};
const container = (disablePointer, position) =>
css(
{
zIndex: style.zIndex,
position: 'fixed',
padding: '4px',
width: style.width,
boxSizing: 'border-box',
color: '#fff',
...(disablePointer ? { pointerEvents: 'none' } : {}),
[`@media ${style.mobile}`]: {
width: '100vw',
padding: 0
}
},
toastPosition(position)
);
class ToastContainer extends Component {
static propTypes = {
/**
* Set toast position
*/
position: PropTypes.oneOf(objectValues(POSITION)),
/**
* Disable or set autoClose delay
*/
autoClose: falseOrDelay,
/**
* Disable or set a custom react element for the close button
*/
closeButton: falseOrElement,
/**
* Hide or not progress bar when autoClose is enabled
*/
hideProgressBar: PropTypes.bool,
/**
* Pause toast duration on hover
*/
pauseOnHover: PropTypes.bool,
/**
* Dismiss toast on click
*/
closeOnClick: PropTypes.bool,
/**
* Newest on top
*/
newestOnTop: PropTypes.bool,
/**
* An optional className
*/
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* An optional style
*/
style: PropTypes.object,
/**
* An optional className for the toast
*/
toastClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* An optional className for the toast body
*/
bodyClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
/**
* An optional className for the toast progress bar
*/
progressClassName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
/**
* Define enter and exit transition using react-transition-group
*/
transition: PropTypes.func
};
static defaultProps = {
position: POSITION.TOP_RIGHT,
transition: DefaultTransition,
autoClose: 5000,
hideProgressBar: false,
closeButton: <DefaultCloseButton />,
pauseOnHover: true,
closeOnClick: true,
newestOnTop: false,
className: null,
style: null,
toastClassName: '',
bodyClassName: '',
progressClassName: ''
};
/**
* Hold toast ids
*/
state = {
toast: [],
isDocumentHidden: false
};
/**
* Hold toast's informations:
* - what to render
* - position
* - raw content
* - options
*/
collection = {};
componentDidMount() {
const { SHOW, CLEAR, MOUNTED } = ACTION;
EventManager.on(SHOW, (content, options) => this.show(content, options))
.on(CLEAR, id => (id !== null ? this.removeToast(id) : this.clear()))
.emit(MOUNTED, this);
document.addEventListener('visibilitychange', this.isDocumentHidden);
}
componentWillUnmount() {
EventManager.off(ACTION.SHOW);
EventManager.off(ACTION.CLEAR);
document.removeEventListener('visibilitychange', this.isDocumentHidden);
}
isDocumentHidden = () => this.setState({ isDocumentHidden: document.hidden });
isToastActive = id => this.state.toast.indexOf(parseInt(id, 10)) !== -1;
removeToast(id) {
this.setState({
toast: this.state.toast.filter(v => v !== parseInt(id, 10))
});
}
makeCloseButton(toastClose, toastId, type) {
let closeButton = this.props.closeButton;
if (isValidElement(toastClose) || toastClose === false) {
closeButton = toastClose;
}
return closeButton === false
? false
: cloneElement(closeButton, {
closeToast: () => this.removeToast(toastId),
type: type
});
}
getAutoCloseDelay(toastAutoClose) {
return toastAutoClose === false || isValidDelay(toastAutoClose)
? toastAutoClose
: this.props.autoClose;
}
isFunction(object) {
return !!(object && object.constructor && object.call && object.apply);
}
canBeRendered(content) {
return (
isValidElement(content) ||
typeof content === 'string' ||
typeof content === 'number' ||
this.isFunction(content)
);
}
show(content, options) {
if (!this.canBeRendered(content)) {
throw new Error(
`The element you provided cannot be rendered. You provided an element of type ${typeof content}`
);
}
const toastId = options.toastId;
const closeToast = () => this.removeToast(toastId);
const toastOptions = {
id: toastId,
type: options.type,
closeToast: closeToast,
updateId: options.updateId,
position: options.position || this.props.position,
transition: options.transition || this.props.transition,
className: options.className || this.props.toastClassName,
bodyClassName: options.bodyClassName || this.props.bodyClassName,
closeButton: this.makeCloseButton(
options.closeButton,
toastId,
options.type
),
pauseOnHover:
options.pauseOnHover !== null
? options.pauseOnHover
: this.props.pauseOnHover,
closeOnClick:
options.closeOnClick !== null
? options.closeOnClick
: this.props.closeOnClick,
progressClassName:
options.progressClassName || this.props.progressClassName,
autoClose: this.getAutoCloseDelay(
options.autoClose !== false
? parseInt(options.autoClose, 10)
: options.autoClose
),
hideProgressBar:
typeof options.hideProgressBar === 'boolean'
? options.hideProgressBar
: this.props.hideProgressBar
};
this.isFunction(options.onOpen) && (toastOptions.onOpen = options.onOpen);
this.isFunction(options.onClose) &&
(toastOptions.onClose = options.onClose);
/**
* add closeToast function to react component only
*/
if (
isValidElement(content) &&
typeof content.type !== 'string' &&
typeof content.type !== 'number'
) {
content = cloneElement(content, {
closeToast
});
} else if (this.isFunction(content)) {
content = content({ closeToast });
}
this.collection = Object.assign({}, this.collection, {
[toastId]: {
position: toastOptions.position,
options: toastOptions,
content: content
}
});
this.setState({
toast:
toastOptions.updateId !== null
? [...this.state.toast]
: [...this.state.toast, toastId]
});
}
makeToast(content, options) {
return (
<Toast
{...options}
isDocumentHidden={this.state.isDocumentHidden}
key={`toast-${options.id}`}
>
{content}
</Toast>
);
}
clear() {
this.setState({ toast: [] });
}
renderToast() {
const toastToRender = {};
const { className, style, newestOnTop } = this.props;
const collection = newestOnTop
? Object.keys(this.collection).reverse()
: Object.keys(this.collection);
collection.forEach(toastId => {
const item = this.collection[toastId];
toastToRender[item.position] || (toastToRender[item.position] = []);
if (this.state.toast.indexOf(parseInt(toastId, 10)) !== -1) {
toastToRender[item.position].push(
this.makeToast(item.content, item.options)
);
} else {
toastToRender[item.position].push(null);
delete this.collection[toastId];
}
});
return Object.keys(toastToRender).map(position => {
const disablePointer =
toastToRender[position].length === 1 &&
toastToRender[position][0] === null;
return (
<TransitionGroup
{...(typeof className !== 'string'
? css(container(disablePointer, position), className)
: container(disablePointer, position))}
{...typeof className === 'string' && { className }}
{...style !== null && { style }}
key={`container-${position}`}
>
{toastToRender[position]}
</TransitionGroup>
);
});
}
render() {
return <div>{this.renderToast()}</div>;
}
}
export default ToastContainer;

View File

@@ -0,0 +1,143 @@
import { POSITION } from './constant';
export default function getAnimation(pos) {
switch (pos) {
case POSITION.TOP_RIGHT:
case POSITION.BOTTOM_RIGHT:
default:
return {
enter: {
from: {
opacity: 0,
transform: 'translate3d(3000px, 0, 0)'
},
'60%': {
opacity: 1,
transform: 'translate3d(-25px, 0, 0)'
},
'75%': {
transform: 'translate3d(10px, 0, 0)'
},
'90%': {
transform: 'translate3d(-5px, 0, 0)'
},
to: {
transform: 'none'
}
},
exit: {
'20%': {
opacity: 1,
transform: 'translate3d(-20px, 0, 0)'
},
to: {
opacity: 0,
transform: 'translate3d(2000px, 0, 0)'
}
}
};
case POSITION.TOP_LEFT:
case POSITION.BOTTOM_LEFT:
return {
enter: {
'0%': {
opacity: 0,
transform: 'translate3d(-3000px, 0, 0)'
},
'60%': {
opacity: 1,
transform: 'translate3d(25px, 0, 0)'
},
'75%': {
transform: 'translate3d(-10px, 0, 0)'
},
'90%': {
transform: 'translate3d(5px, 0, 0)'
},
to: {
transform: 'none'
}
},
exit: {
'20%': {
opacity: 1,
transform: 'translate3d(20px, 0, 0)'
},
to: {
opacity: 0,
transform: 'translate3d(-2000px, 0, 0)'
}
}
};
case POSITION.BOTTOM_CENTER:
return {
enter: {
from: {
opacity: 0,
transform: 'translate3d(0, 3000px, 0)'
},
'60%': {
opacity: 1,
transform: 'translate3d(0, -20px, 0)'
},
'75%': {
transform: 'translate3d(0, 10px, 0)'
},
'90%': {
transform: 'translate3d(0, -5px, 0)'
},
to: {
transform: 'translate3d(0, 0, 0)'
}
},
exit: {
'20%': {
transform: 'translate3d(0, 10px, 0)'
},
'40%, 45%': {
opacity: 1,
transform: 'translate3d(0, -20px, 0)'
},
to: {
opacity: 0,
transform: 'translate3d(0, 2000px, 0)'
}
}
};
case POSITION.TOP_CENTER:
return {
enter: {
'0%': {
opacity: 0,
transform: 'translate3d(0, -3000px, 0)'
},
'60%': {
opacity: 1,
transform: 'translate3d(0, 25px, 0)'
},
'75%': {
transform: 'translate3d(0, -10px, 0)'
},
'90%': {
transform: 'translate3d(0, 5px, 0)'
},
to: {
transform: 'none'
}
},
exit: {
'20%': {
transform: 'translate3d(0, -10px, 0)'
},
'40%, 45%': {
opacity: 1,
transform: 'translate3d(0, 20px, 0)'
},
to: {
opacity: 0,
transform: 'translate3d(0, -2000px, 0)'
}
}
};
}
}

View File

@@ -0,0 +1,21 @@
export const POSITION = {
TOP_LEFT: 'top-left',
TOP_RIGHT: 'top-right',
TOP_CENTER: 'top-center',
BOTTOM_LEFT: 'bottom-left',
BOTTOM_RIGHT: 'bottom-right',
BOTTOM_CENTER: 'bottom-center'
};
export const TYPE = {
INFO: 'info',
SUCCESS: 'success',
WARNING: 'warning',
ERROR: 'error',
DEFAULT: 'default'
};
export const ACTION = {
SHOW: 'SHOW_TOAST',
CLEAR: 'CLEAR_TOAST',
MOUNTED: 'CONTAINER_MOUNTED'
};

View File

@@ -0,0 +1,5 @@
import ToastContainer from './ToastContainer';
import toaster from './toaster';
import { defineStyle } from './style';
export { ToastContainer, toaster as toast, defineStyle as style };

View File

@@ -0,0 +1,45 @@
const style = {
width: '320px',
colorDefault: '#fff',
colorInfo: '#3498db',
colorSuccess: '#07bc0c',
colorWarning: '#f1c40f',
colorError: '#e74c3c',
colorProgressDefault:
'linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55)',
mobile: 'only screen and (max-width : 480px)',
fontFamily: 'sans-serif',
zIndex: 9999,
TOP_LEFT: {
top: '1em',
left: '1em'
},
TOP_CENTER: {
top: '1em',
left: '50%'
},
TOP_RIGHT: {
top: '1em',
right: '1em'
},
BOTTOM_LEFT: {
bottom: '1em',
left: '1em'
},
BOTTOM_CENTER: {
bottom: '1em',
left: '50%'
},
BOTTOM_RIGHT: {
bottom: '1em',
right: '1em'
}
};
export function defineStyle(props) {
for (let prop in props) {
style[prop] = props[prop];
}
}
export default style;

View File

@@ -0,0 +1,114 @@
import EventManager from './util/EventManager';
import { POSITION, TYPE, ACTION } from './constant';
const defaultOptions = {
type: TYPE.DEFAULT,
autoClose: null,
closeButton: null,
hideProgressBar: null,
position: null,
pauseOnHover: null,
closeOnClick: null,
className: null,
bodyClassName: null,
progressClassName: null,
transition: null,
updateId: null
};
let container = null;
let queue = [];
let toastId = 0;
/**
* Merge provided options with the defaults settings and generate the toastId
* @param {*} options
*/
function mergeOptions(options, type) {
return Object.assign({}, defaultOptions, options, {
type: type,
toastId: ++toastId
});
}
/**
* Dispatch toast. If the container is not mounted, the toast is enqueued
* @param {*} content
* @param {*} options
*/
function emitEvent(content, options) {
if (container !== null) {
EventManager.emit(ACTION.SHOW, content, options);
} else {
queue.push({ action: ACTION.SHOW, content, options });
}
return options.toastId;
}
const toaster = Object.assign(
(content, options) =>
emitEvent(
content,
mergeOptions(options, (options && options.type) || TYPE.DEFAULT)
),
{
success: (content, options) =>
emitEvent(content, mergeOptions(options, TYPE.SUCCESS)),
info: (content, options) =>
emitEvent(content, mergeOptions(options, TYPE.INFO)),
warn: (content, options) =>
emitEvent(content, mergeOptions(options, TYPE.WARNING)),
warning: (content, options) =>
emitEvent(content, mergeOptions(options, TYPE.WARNING)),
error: (content, options) =>
emitEvent(content, mergeOptions(options, TYPE.ERROR)),
dismiss: (id = null) => container && EventManager.emit(ACTION.CLEAR, id),
isActive: () => false,
update(id, options) {
if (container && typeof container.collection[id] !== 'undefined') {
const {
options: oldOptions,
content: oldContent
} = container.collection[id];
const updateId =
oldOptions.updateId !== null ? oldOptions.updateId + 1 : 1;
const nextOptions = Object.assign({}, oldOptions, options, {
toastId: id,
updateId: updateId
});
const content =
typeof nextOptions.render !== 'undefined'
? nextOptions.render
: oldContent;
delete nextOptions.render;
return emitEvent(content, nextOptions);
}
return false;
}
},
{
POSITION,
TYPE
}
);
/**
* Wait until the ToastContainer is mounted to dispatch the toast
* and attach isActive method
*/
EventManager.on(ACTION.MOUNTED, containerInstance => {
container = containerInstance;
toaster.isActive = id => container.isToastActive(id);
queue.forEach(item => {
EventManager.emit(item.action, item.content, item.options);
});
queue = [];
});
export default toaster;

View File

@@ -0,0 +1,33 @@
const eventManager = {
eventList: new Map(),
on(event, callback) {
this.eventList.has(event) || this.eventList.set(event, []);
this.eventList.get(event).push(callback);
return this;
},
off(event = null) {
return this.eventList.delete(event);
},
emit(event, ...args) {
if (!this.eventList.has(event)) {
/* eslint no-console: 0 */
console.warn(
`<${event}> Event is not registered. Did you forgot to bind the event ?`
);
return false;
}
this.eventList
.get(event)
.forEach(callback => setTimeout(() => callback.call(this, ...args), 0));
return true;
}
};
export default eventManager;

View File

@@ -0,0 +1,45 @@
import { isValidElement } from 'react';
export function isValidDelay(val) {
return typeof val === 'number' && !isNaN(val) && val > 0;
}
export function objectValues(obj) {
return Object.keys(obj).map(key => obj[key]);
}
function withRequired(fn) {
fn.isRequired = function(props, propName, componentName) {
const prop = props[propName];
if (typeof prop === 'undefined') {
return new Error(`The prop ${propName} is marked as required in
${componentName}, but its value is undefined.`);
}
fn(props, propName, componentName);
};
return fn;
}
export const falseOrDelay = withRequired((props, propName, componentName) => {
const prop = props[propName];
if (prop !== false && !isValidDelay(prop)) {
return new Error(`${componentName} expect ${propName}
to be a valid Number > 0 or equal to false. ${prop} given.`);
}
return null;
});
export const falseOrElement = withRequired((props, propName, componentName) => {
const prop = props[propName];
if (prop !== false && !isValidElement(prop)) {
return new Error(`${componentName} expect ${propName}
to be a valid react element or equal to false. ${prop} given.`);
}
return null;
});