UNPKG

tweak-tools

Version:

Tweak your React projects until awesomeness

197 lines (173 loc) 5.96 kB
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 }