semantic-ui-react
Version:
The official Semantic-UI-React integration.
329 lines (283 loc) • 10.9 kB
JavaScript
import _ from 'lodash/fp'
import PropTypes from 'prop-types'
import leven from './leven'
const typeOf = (...args) => Object.prototype.toString.call(...args)
/**
* Ensure a component can render as a give prop value.
*/
export const as = (...args) => PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
])(...args)
/* eslint-disable max-nested-callbacks */
const findBestSuggestions = _.memoize((propValueWords, suggestions) => _.flow(
_.map((suggestion) => {
const suggestionWords = suggestion.split(' ')
const propValueScore = _.flow(
_.map(x => _.map(y => leven(x, y), suggestionWords)),
_.map(_.min),
_.sum,
)(propValueWords)
const suggestionScore = _.flow(
_.map(x => _.map(y => leven(x, y), propValueWords)),
_.map(_.min),
_.sum,
)(suggestionWords)
return { suggestion, score: propValueScore + suggestionScore }
}),
_.sortBy(['score', 'suggestion']),
_.take(3),
)(suggestions))
/* eslint-enable max-nested-callbacks */
/**
* Similar to PropTypes.oneOf but shows closest matches.
* Word order is ignored allowing `left chevron` to match `chevron left`.
* Useful for very large lists of options (e.g. Icon name, Flag name, etc.)
* @param {string[]} suggestions An array of allowed values.
*/
export const suggest = suggestions => (props, propName, componentName) => {
if (!Array.isArray(suggestions)) {
throw new Error([
'Invalid argument supplied to suggest, expected an instance of array.',
` See \`${propName}\` prop in \`${componentName}\`.`,
].join(''))
}
const propValue = props[propName]
// skip if prop is undefined or is included in the suggestions
if (_.isNil(propValue) || propValue === false || _.includes(propValue, suggestions)) return
// find best suggestions
const propValueWords = propValue.split(' ')
const bestMatches = findBestSuggestions(propValueWords, suggestions)
// skip if a match scored 0
// since we're matching on words (classNames) this allows any word order to pass validation
// e.g. `left chevron` vs `chevron left`
if (bestMatches.some(x => x.score === 0)) return
return new Error([
`Invalid prop \`${propName}\` of value \`${propValue}\` supplied to \`${componentName}\`.`,
`\n\nInstead of \`${propValue}\`, did you mean:`,
bestMatches.map(x => `\n - ${x.suggestion}`).join(''),
'\n',
].join(''))
}
/**
* Disallow other props from being defined with this prop.
* @param {string[]} disallowedProps An array of props that cannot be used with this prop.
*/
export const disallow = disallowedProps => (props, propName, componentName) => {
if (!Array.isArray(disallowedProps)) {
throw new Error([
'Invalid argument supplied to disallow, expected an instance of array.',
` See \`${propName}\` prop in \`${componentName}\`.`,
].join(''))
}
// skip if prop is undefined
if (_.isNil(props[propName]) || props[propName] === false) return
// find disallowed props with values
const disallowed = disallowedProps.reduce((acc, disallowedProp) => {
if (!_.isNil(props[disallowedProp]) && props[disallowedProp] !== false) {
return [...acc, disallowedProp]
}
return acc
}, [])
if (disallowed.length > 0) {
return new Error([
`Prop \`${propName}\` in \`${componentName}\` conflicts with props: \`${disallowed.join('`, `')}\`.`,
'They cannot be defined together, choose one or the other.',
].join(' '))
}
}
/**
* Ensure a prop adherers to multiple prop type validators.
* @param {function[]} validators An array of propType functions.
*/
export const every = validators => (props, propName, componentName, ...rest) => {
if (!Array.isArray(validators)) {
throw new Error([
'Invalid argument supplied to every, expected an instance of array.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}
const errors = _.flow(
_.map((validator) => {
if (typeof validator !== 'function') {
throw new Error(`every() argument "validators" should contain functions, found: ${typeOf(validator)}.`)
}
return validator(props, propName, componentName, ...rest)
}),
_.compact,
)(validators)
// we can only return one error at a time
return errors[0]
}
/**
* Ensure a prop adherers to at least one of the given prop type validators.
* @param {function[]} validators An array of propType functions.
*/
export const some = validators => (props, propName, componentName, ...rest) => {
if (!Array.isArray(validators)) {
throw new Error([
'Invalid argument supplied to some, expected an instance of array.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}
const errors = _.compact(_.map(validators, (validator) => {
if (!_.isFunction(validator)) {
throw new Error(`some() argument "validators" should contain functions, found: ${typeOf(validator)}.`)
}
return validator(props, propName, componentName, ...rest)
}))
// fail only if all validators failed
if (errors.length === validators.length) {
const error = new Error('One of these validators must pass:')
error.message += `\n${_.map(errors, (err, i) => (`[${i + 1}]: ${err.message}`)).join('\n')}`
return error
}
}
/**
* Ensure a validator passes only when a component has a given propsShape.
* @param {object} propsShape An object describing the prop shape.
* @param {function} validator A propType function.
*/
export const givenProps = (propsShape, validator) => (props, propName, componentName, ...rest) => {
if (!_.isPlainObject(propsShape)) {
throw new Error([
'Invalid argument supplied to givenProps, expected an object.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}
if (typeof validator !== 'function') {
throw new Error([
'Invalid argument supplied to givenProps, expected a function.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}
const shouldValidate = _.keys(propsShape).every((key) => {
const val = propsShape[key]
// require propShape validators to pass or prop values to match
return typeof val === 'function' ? !val(props, key, componentName, ...rest) : val === props[propName]
})
if (!shouldValidate) return
const error = validator(props, propName, componentName, ...rest)
if (error) {
// poor mans shallow pretty print, prevents JSON circular reference errors
const prettyProps = `{ ${_.keys(_.pick(_.keys(propsShape), props)).map((key) => {
const val = props[key]
let renderedValue = val
if (typeof val === 'string') renderedValue = `"${val}"`
else if (Array.isArray(val)) renderedValue = `[${val.join(', ')}]`
else if (_.isObject(val)) renderedValue = '{...}'
return `${key}: ${renderedValue}`
}).join(', ')} }`
error.message = `Given props ${prettyProps}: ${error.message}`
return error
}
}
/**
* Define prop dependencies by requiring other props.
* @param {string[]} requiredProps An array of required prop names.
*/
export const demand = requiredProps => (props, propName, componentName) => {
if (!Array.isArray(requiredProps)) {
throw new Error([
'Invalid `requiredProps` argument supplied to require, expected an instance of array.',
` See \`${propName}\` prop in \`${componentName}\`.`,
].join(''))
}
// skip if prop is undefined
if (props[propName] === undefined) return
const missingRequired = requiredProps.filter(requiredProp => props[requiredProp] === undefined)
if (missingRequired.length > 0) {
return new Error(
`\`${propName}\` prop in \`${componentName}\` requires props: \`${missingRequired.join('`, `')}\`.`,
)
}
}
/**
* Ensure an multiple prop contains a string with only possible values.
* @param {string[]} possible An array of possible values to prop.
*/
export const multipleProp = possible => (props, propName, componentName) => {
if (!Array.isArray(possible)) {
throw new Error([
'Invalid argument supplied to some, expected an instance of array.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}
const propValue = props[propName]
// skip if prop is undefined
if (_.isNil(propValue) || propValue === false) return
const values = propValue
.replace('large screen', 'large-screen')
.replace(/ vertically/g, '-vertically')
.split(' ')
.map(val => _.trim(val).replace('-', ' '))
const invalid = _.difference(values, possible)
// fail only if there are invalid values
if (invalid.length > 0) {
return new Error(`\`${propName}\` prop in \`${componentName}\` has invalid values: \`${invalid.join('`, `')}\`.`)
}
}
/**
* Ensure a component can render as a node passed as a prop value in place of children.
*/
export const contentShorthand = (...args) => every([
disallow(['children']),
PropTypes.node,
])(...args)
/**
* Item shorthand is a description of a component that can be a literal,
* a props object, or an element.
*/
export const itemShorthand = (...args) => every([
disallow(['children']),
PropTypes.oneOfType([
PropTypes.node,
PropTypes.object,
PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.node,
PropTypes.object,
]),
),
]),
])(...args)
/**
* Collection shorthand ensures a prop is an array of item shorthand.
*/
export const collectionShorthand = (...args) => every([
disallow(['children']),
PropTypes.arrayOf(itemShorthand),
])(...args)
/**
* Show a deprecated warning for component props with a help message and optional validator.
* @param {string} help A help message to display with the deprecation warning.
* @param {function} [validator] A propType function.
*/
export const deprecate = (help, validator) => (props, propName, componentName, ...args) => {
if (typeof help !== 'string') {
throw new Error([
'Invalid `help` argument supplied to deprecate, expected a string.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}
// skip if prop is undefined
if (props[propName] === undefined) return
// deprecation error and help
const error = new Error(`The \`${propName}\` prop in \`${componentName}\` is deprecated.`)
if (help) error.message += ` ${help}`
// add optional validation error message
if (validator) {
if (typeof validator === 'function') {
const validationError = validator(props, propName, componentName, ...args)
if (validationError) {
error.message = `${error.message} ${validationError.message}`
}
} else {
throw new Error([
'Invalid argument supplied to deprecate, expected a function.',
`See \`${propName}\` prop in \`${componentName}\`.`,
].join(' '))
}
}
return error
}