tweak-tools
Version:
Tweak your React projects until awesomeness
197 lines (173 loc) • 5.96 kB
text/typescript
import { dequal } from 'dequal/lite'
import { isObject, isEmptyObject } from '.'
import { getValueType, normalize, sanitize } from '../plugin'
import {
CommonOptions,
Data,
DataInput,
DataInputOptions,
InputOptions,
PanelInputOptions,
SpecialInputs,
StoreType,
} from '../types'
type ParsedOptions = {
type?: string
input: any
options: CommonOptions | DataInputOptions | PanelInputOptions
}
export function parseOptions(
_input: any,
key: string,
mergedOptions: Partial<InputOptions> = {},
customType?: string
): ParsedOptions {
// if input isn't an object then we just need to assing default options to it.
if (typeof _input !== 'object' || Array.isArray(_input)) {
return {
type: customType,
input: _input,
options: {
key,
label: key,
optional: false,
disabled: false,
order: 0,
...mergedOptions,
},
}
}
// if it's a custom input, then the input will be under the __customInput key
// so we run the parseOptions function on that key and for its type.
if ('__customInput' in _input) {
/**
* If a custom input uses a non object arg, the only way to parse options
* is { ...myPlugin('value'), label: 'my label' }.
* In that case, the input will be shaped like so:
* { type, __customInput, label }
*/
const { type, __customInput, ...options } = _input
return parseOptions(__customInput, key, options, type)
}
// parse generic options from input object
const {
render,
label,
optional,
order = 0,
disabled,
hint,
onChange,
onEditStart,
onEditEnd,
transient,
...inputWithType
} = _input
const commonOptions = {
render,
key,
label: label ?? key,
hint,
transient: transient ?? !!onChange,
onEditStart,
onEditEnd,
disabled,
optional,
order,
...mergedOptions,
}
let { type, ...input } = inputWithType
type = customType ?? type
if (type in SpecialInputs) {
return { type, input, options: commonOptions }
}
// in case this is a custom input like beziers where the argument is an array,
// then the array could be passed as { value: [0,0,0,0], onChange: () => {} }.
let computedInput
if (customType && isObject(input) && 'value' in input) computedInput = input.value
else computedInput = isEmptyObject(input) ? undefined : input
return {
type,
input: computedInput,
options: {
...commonOptions,
onChange,
optional: commonOptions.optional ?? false,
disabled: commonOptions.disabled ?? false,
},
}
}
/**
* This function is used to normalize the way an input is stored in the store.
* Returns a value in the form of { type, value, settings} by doing different
* checks depending on the input structure.
*
* @param input
* @param path
*/
export function normalizeInput(_input: any, key: string, path: string, data: Data) {
const parsedInputAndOptions = parseOptions(_input, key)
const { type, input: parsedInput, options } = parsedInputAndOptions
if (type) {
if (type in SpecialInputs)
// If the input is a special input then we return it as it is.
return parsedInputAndOptions
// If the type key exists at this point, it must be a forced type or a
// custom plugin defined by the user.
return { type, input: normalize(type, parsedInput, path, data), options }
}
let inputType = getValueType(parsedInput)
if (inputType) return { type: inputType, input: normalize(inputType, parsedInput, path, data), options }
inputType = getValueType({ value: parsedInput })
if (inputType) return { type: inputType, input: normalize(inputType, { value: parsedInput }, path, data), options }
// At this point, the input is not recognized and we return false.
return false
}
export function updateInput(input: DataInput, newValue: any, path: string, store: StoreType, fromPanel: boolean) {
const { value, type, settings } = input
input.value = sanitizeValue({ type, value, settings }, newValue, path, store)
input.fromPanel = fromPanel
}
type SanitizeProps = {
type: string
value: any
settings: object | undefined
}
type ValueErrorType = { type: string; message: string; previousValue: any; error?: unknown }
const ValueError = function (this: ValueErrorType, message: string, value: any, error?: unknown) {
this.type = 'tweak_error'
this.message = 'Tweak: ' + message
this.previousValue = value
this.error = error
} as unknown as { new(message: string, value: any, error?: unknown): ValueErrorType }
export function sanitizeValue({ type, value, settings }: SanitizeProps, newValue: any, path: string, store: StoreType) {
const _newValue = type !== 'SELECT' && typeof newValue === 'function' ? newValue(value) : newValue
let sanitizedNewValue
try {
sanitizedNewValue = sanitize(type, _newValue, settings, value, path, store)
} catch (e) {
throw new ValueError(`The value \`${newValue}\` did not result in a correct value.`, value, e)
}
if (dequal(sanitizedNewValue, value)) {
/**
* @note This makes the update function throw when the new value is the same
* as the previous one. This can happen for example, if the minimum value of
* a number is 30, and the user inputs 15. Then the newValue will be sanitized
* to 30 and subsequent calls like 14, 0, etc. won't result in the component displaying
* the value to be notified (ie there wouldn't be a new render)
*/
/**
* @update 22.10.22 this warning is a bit cumbersome when dragging something, which can
* result in the same value being set. Commenting out.
*/
return value
/*
throw new ValueError(
`The value \`${newValue}\` did not result in a value update, which remained the same: \`${value}\`.
You can ignore this warning if this is the intended behavior.`,
value
)
*/
}
return sanitizedNewValue
}