1
0
Fork 0
freesewing/sites/shared/components/workbench/menus/shared/inputs.mjs
2023-06-06 11:17:14 -05:00

298 lines
9.2 KiB
JavaScript

import { useCallback, useMemo } from 'react'
import { round, measurementAsMm, measurementAsUnits, formatFraction128 } from 'shared/utils.mjs'
import { ChoiceButton } from 'shared/components/choice-button.mjs'
/*******************************************************************************************
* This file contains the base components to be used by inputs in menus in the workbench
* For the purposes of our menus, we have two main types:
* Sliders for changing numbers
* Lists for changing everything else
*
* Inputs that deal with more specific use cases should wrap one of the above base inputs
*******************************************************************************************/
/** A component that shows a number input to edit a value */
const EditCount = (props) => (
<div className="form-control mb-2 w-full">
<label className="label">
<span className="label-text text-base-content">{props.min}</span>
<span className="label-text font-bold text-base-content">{props.current}</span>
<span className="label-text text-base-content">{props.max}</span>
</label>
<label className="input-group input-group-sm">
<input
type="number"
className={`
input input-sm input-bordered grow text-base-content
`}
value={props.current}
onChange={props.handleChange}
/>
<span className="text-base-content font-bold">#</span>
</label>
</div>
)
/**
* A hook to get the change handler for an input.
* @param {Number|String|Boolean} options.dflt the default value for the input
* @param {Function} options.updateFunc the onChange
* @param {string} options.name the name of the property being changed
* @return the change handler for the input
*/
const useSharedHandlers = ({ dflt, updateFunc, name }) => {
return useCallback(
(newCurrent) => {
if (newCurrent === dflt) newCurrent = undefined
updateFunc([name], newCurrent)
},
[dflt, updateFunc, name]
)
}
/** get the configuration that allows a boolean value to use the list input */
const useBoolConfig = (name, config) => {
return useMemo(
() => ({
list: [false, true],
choiceTitles: {
false: `${name}No`,
true: `${name}Yes`,
},
valueTitles: {
false: 'no',
true: 'yes',
},
...config,
}),
[name, config]
)
}
/** a toggle input for list/boolean values */
export const ListToggle = ({ config, changed, updateFunc, name }) => {
const boolConfig = useBoolConfig(name, config)
const handleChange = useSharedHandlers({ dflt: boolConfig.dflt, updateFunc, name })
const dfltIndex = boolConfig.list.indexOf(boolConfig.dflt)
const doToggle = () =>
handleChange(boolConfig.list[changed ? dfltIndex : Math.abs(dfltIndex - 1)])
const checked = boolConfig.dflt == false ? changed : !changed
return (
<input
type="checkbox"
className={`toggle toggle-sm ${changed ? 'toggle-accent' : 'toggle-secondary'}`}
checked={checked}
onChange={doToggle}
/>
)
}
/**
* An input for selecting and item from a list
* @param {String} options.name the name of the property this input changes
* @param {Object} options.config configuration for the input
* @param {String|Number} options.current the current value of the input
* @param {Function} options.updateFunc the function called by the event handler to update the value
* @param {Boolean} options.compact include descriptions with the list items?
* @param {Function} options.t translation function
*/
export const ListInput = ({ name, config, current, updateFunc, compact = false, t, changed }) => {
const handleChange = useSharedHandlers({
dflt: config.dflt,
updateFunc,
name,
})
return (
<>
<p>{t(`${name}.d`)}</p>
{config.list.map((entry) => {
const titleKey = config.choiceTitles ? config.choiceTitles[entry] : `${name}.o.${entry}`
return (
<ChoiceButton
key={entry}
title={t(`${titleKey}.t`)}
color={entry === config.dflt ? 'primary' : 'secondary'}
active={changed ? current === entry : entry === config.dflt}
onClick={() => handleChange(entry)}
>
{compact ? null : t(`${titleKey}.d`)}
</ChoiceButton>
)
})}
</>
)
}
/** A boolean version of {@see ListInput} that sets up the necessary configuration */
export const BoolInput = (props) => {
const { name, config } = props
const boolConfig = useBoolConfig(name, config)
return <ListInput {...props} config={boolConfig} />
}
/**
* An input component that uses a slider to change a number value
* @param {String} options.name the name of the property being changed by the input
* @param {Object} options.config configuration for the input
* @param {Number} options.current the current value of the input
* @param {Function} options.updateFunc the function called by the event handler to update the value
* @param {Function} options.t translation function
* @param {Boolean} options.override open the text input to allow override of the slider?
* @param {String} options.suffix a suffix to append to value labels
* @param {Function} options.valFormatter a function that accepts a value and formats it for display as a label
* @param {Function} options.setReset a setter for the reset function on the parent component
*/
export const SliderInput = ({
name,
config,
current,
updateFunc,
t,
override,
suffix = '',
valFormatter = (val) => val,
setReset,
children,
changed,
}) => {
const { max, min } = config
const handleChange = useSharedHandlers({
current,
dflt: config.dflt,
updateFunc,
name,
setReset,
})
let currentOrDefault = changed ? current : config.dflt
return (
<>
<p>{t(`${name}.d`)}</p>
<div className="flex flex-row justify-between">
{override ? (
<EditCount
{...{
current: currentOrDefault,
handleChange: (evt) => handleChange(evt.target.value),
min,
max,
t,
}}
/>
) : (
<>
<span className="opacity-50">
<span dangerouslySetInnerHTML={{ __html: valFormatter(min) + suffix }} />
</span>
<span
className={`font-bold ${
currentOrDefault === config.dflt ? 'text-secondary' : 'text-accent'
}`}
>
<span dangerouslySetInnerHTML={{ __html: valFormatter(currentOrDefault) + suffix }} />
</span>
<span className="opacity-50">
<span dangerouslySetInnerHTML={{ __html: valFormatter(max) + suffix }} />
</span>
</>
)}
</div>
<input
type="range"
{...{ min, max, value: currentOrDefault, step: config.step || 0.1 }}
onChange={(evt) => handleChange(evt.target.value)}
className={`
range range-sm mt-1
${currentOrDefault === config.dflt ? 'range-secondary' : 'range-accent'}
`}
/>
{children}
</>
)
}
/** A {@see SliderInput} to handle percentage values */
export const PctInput = ({ current, changed, updateFunc, ...rest }) => {
const factor = 100
let pctCurrent = changed ? current * factor : current
const pctUpdateFunc = useCallback(
(path, newVal) => updateFunc(path, newVal === undefined ? undefined : newVal / factor),
[updateFunc, factor]
)
return (
<SliderInput
{...{
...rest,
current: pctCurrent,
updateFunc: pctUpdateFunc,
suffix: '%',
valFormatter: round,
changed,
}}
/>
)
}
/** A {@see SliderInput} to handle degree values */
export const DegInput = (props) => <SliderInput {...props} suffix="°" valFormatter={round} />
export const MmInput = (props) => {
const { units, updateFunc, current, config } = props
const mmUpdateFunc = useCallback(
(path, newCurrent) => {
const calcCurrent =
typeof newCurrent === 'undefined' ? undefined : measurementAsMm(newCurrent, units)
updateFunc(path, calcCurrent)
},
[updateFunc, units]
)
// add a default step that's appropriate to the unit. can be overwritten by config
const defaultStep = units === 'metric' ? 0.1 : 0.125
return (
<SliderInput
{...{
...props,
config: { step: defaultStep, ...config },
current: current === undefined ? undefined : measurementAsUnits(current, units),
updateFunc: mmUpdateFunc,
valFormatter: (val) => (units === 'imperial' ? formatFraction128(val, null) : val),
suffix: units === 'imperial' ? '"' : 'cm',
}}
/>
)
}
/** A placeholder for an input to handle constant values */
export const ConstantInput = ({
type = 'number',
name,
current,
updateFunc,
t,
changed,
config,
}) => (
<>
<p>{t(`${name}.d`)}</p>
<input
type={type}
className={`
input input-bordered w-full text-base-content
input-${changed ? 'secondary' : 'accent'}
`}
value={changed ? current : config.dflt}
onChange={(evt) => updateFunc([name], evt.target.value)}
/>
</>
)