@onesy/ui-react
Version:
UI for React
465 lines (453 loc) • 17.5 kB
JavaScript
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
const _excluded = ["ref", "tonal", "color", "size", "value", "valueDefault", "onChange", "selecting", "selectingDefault", "onChangeSelecting", "format", "dayTime", "hour", "minute", "second", "autoNext", "min", "max", "validate", "readOnly", "disabled", "valid", "renderValue", "onDoneSelecting", "onClick", "className", "BackgroundProps"];
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import React from 'react';
import { classNames, style, useOnesyTheme } from '@onesy/style-react';
import { clamp, getLeadingZerosNumber, is, isEnvironment, unique } from '@onesy/utils';
import { OnesyDate, is as isOnesyDate, set } from '@onesy/date';
import RoundMeterElement from '../RoundMeter';
import PathElement from '../Path';
import { staticClassName } from '../utils';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const useStyle = style(theme => ({
root: {
userSelect: 'none',
touchAction: 'none',
'& .onesy-RoundMeter-children, & .onesy-RoundMeter-labels': {
pointerEvents: 'none'
},
'& svg > *': {
cursor: 'grab'
}
},
mouseDown: {
'& svg > *': {
cursor: 'grabbing'
}
}
}), {
name: 'onesy-Clock'
});
const Clock = props__ => {
const theme = useOnesyTheme();
const props = _objectSpread(_objectSpread(_objectSpread({}, theme?.ui?.elements?.all?.props?.default), theme?.ui?.elements?.onesyClock?.props?.default), props__);
const RoundMeter = theme?.elements?.RoundMeter || RoundMeterElement;
const Path = theme?.elements?.Path || PathElement;
const {
ref,
tonal = true,
color = 'primary',
size = 'regular',
value: value_,
valueDefault,
onChange,
selecting: selecting_,
selectingDefault,
onChangeSelecting,
format = '12',
dayTime = 'am',
hour = true,
minute = true,
second = false,
autoNext,
min,
max,
validate,
readOnly,
disabled,
valid: valid_,
renderValue,
onDoneSelecting,
onClick: onClick_,
className,
BackgroundProps
} = props,
other = _objectWithoutProperties(props, _excluded);
const {
classes
} = useStyle();
const [value, setValue] = React.useState((valueDefault !== undefined ? valueDefault : value_) || new OnesyDate());
const [selecting, setSelecting] = React.useState((selectingDefault !== undefined ? selectingDefault : selecting_) || 'hour');
const [mouseDown, setMouseDown] = React.useState(false);
const refs = {
root: React.useRef(null),
middle: React.useRef(null),
mouseDown: React.useRef(null),
value: React.useRef(null),
selecting: React.useRef(null),
autoNext: React.useRef(null),
hour: React.useRef(null),
minute: React.useRef(null),
second: React.useRef(null),
format: React.useRef(null),
dayTime: React.useRef(null),
previous: React.useRef(selecting)
};
refs.mouseDown.current = mouseDown;
refs.value.current = value;
refs.hour.current = hour;
refs.minute.current = minute;
refs.second.current = second;
refs.selecting.current = selecting;
refs.autoNext.current = autoNext;
refs.format.current = format;
refs.dayTime.current = dayTime;
const resolve = (valueNew = refs.value.current, dayTimeValue = refs.dayTime.current) => {
// Resolve the range value
const valueHour = valueNew.hour;
if (format === '12') {
if (dayTimeValue === 'am' && valueHour > 12) return set(valueHour - 12, 'hour', valueNew);
if (dayTimeValue === 'pm' && valueHour < 12) return set(valueHour + 12, 'hour', valueNew);
}
return valueNew;
};
const inputToValue = (valueNew_0, unit = refs.selecting.current) => {
let onesyDate = new OnesyDate(refs.value.current);
let valueTime = valueNew_0;
if (is('string', valueTime) && valueTime.startsWith('0')) valueTime = valueTime.slice(1);
valueTime = +valueTime;
if (unit === 'hour') onesyDate = set(format === '12' && dayTime === 'pm' ? valueTime + 12 : valueTime, 'hour', onesyDate);else onesyDate = set(valueTime, unit, onesyDate);
return resolve(onesyDate);
};
const onMove = (x_, y_) => {
const rectMiddle = refs.middle.current.getBoundingClientRect();
const x = x_ - rectMiddle.x;
const y = y_ - rectMiddle.y;
const radians = Math.atan2(x, y);
const degrees = radians * 180 / Math.PI;
const angle = 180 - degrees;
// Make array of values
// for hours, minutes and seconds
// with +- 50% around the value
// Find item in that array that this angle fits within
let valuesAll = [];
if (refs.selecting.current === 'hour') {
const part = 360 / 12;
valuesAll = Array.from({
length: 12
}).map((item, index_) => [part * index_ - part / 2, part * index_ + part / 2]);
let index = valuesAll.findIndex(item_0 => angle >= item_0[0] && angle <= item_0[1]);
if (index === -1 || index === 0) index = refs.format.current === '24' ? 0 : 12;
if (refs.format.current === '24') {
let within = false;
const labelElements = refs.root.current.querySelectorAll('.onesy-RoundMeter-labels');
const elements = {
outer: labelElements[0],
inner: labelElements[1]
};
const rects = {
outer: elements.outer.getBoundingClientRect(),
inner: elements.inner.getBoundingClientRect()
};
const part_ = Math.abs(Math.abs(rects.outer.x) - Math.abs(rects.inner.x));
const valueMoved = Math.sqrt(x ** 2 + y ** 2);
const middleInner = Math.abs(Math.abs(rectMiddle.x) - Math.abs(rects.inner.x));
if (valueMoved <= middleInner + part_ / 2) within = true;
if (within) index += 12;
index = clamp(index, 0, 23);
}
// Validate
if (!valid(inputToValue(index, 'hour'), 'hour')) return;
// Update values
onUpdate(inputToValue(index, 'hour'));
} else if (['minute', 'second'].includes(refs.selecting.current)) {
const part_0 = 360 / 60;
valuesAll = Array.from({
length: 60
}).map((item_1, index__0) => [part_0 * index__0 - part_0 / 2, part_0 * index__0 + part_0 / 2]);
let index_0 = valuesAll.findIndex(item_2 => angle >= item_2[0] && angle <= item_2[1]);
if (index_0 === -1 || index_0 === 0) index_0 = 0;
// Validate
if (!valid(inputToValue(index_0), refs.selecting.current)) return;
// Update values
onUpdate(inputToValue(index_0));
}
};
React.useEffect(() => {
const onMouseUp = () => {
if (refs.mouseDown.current) {
setMouseDown(false);
// Auto next
if (refs.autoNext.current) {
if (['hour', 'minute', 'second'].includes(refs.selecting.current)) {
let valueSelecting;
if (refs.selecting.current === 'second') valueSelecting = 'hour';
if (refs.selecting.current === 'minute') valueSelecting = refs.second.current ? 'second' : 'hour';
if (refs.selecting.current === 'hour' && refs.minute.current) valueSelecting = 'minute';
onUpdateSelecting(valueSelecting);
}
}
if (is('function', onDoneSelecting)) onDoneSelecting(refs.value.current, refs.selecting.current);
}
};
// Mouse move
const onMouseMove = event => {
if (refs.mouseDown.current) {
const {
clientY,
clientX
} = event;
onMove(clientX, clientY);
}
};
// Touch move
const onTouchMove = event_0 => {
if (refs.mouseDown.current) {
const {
clientY: clientY_0,
clientX: clientX_0
} = event_0.touches[0];
onMove(clientX_0, clientY_0);
}
};
const rootDocument = isEnvironment('browser') ? refs.root.current?.ownerDocument || window.document : undefined;
rootDocument.addEventListener('mouseup', onMouseUp);
rootDocument.addEventListener('mousemove', onMouseMove);
rootDocument.addEventListener('touchend', onMouseUp);
rootDocument.addEventListener('touchmove', onTouchMove, {
passive: true
});
return () => {
rootDocument.removeEventListener('mousemove', onMouseMove);
rootDocument.removeEventListener('mouseup', onMouseUp);
rootDocument.removeEventListener('touchmove', onTouchMove);
rootDocument.removeEventListener('touchend', onMouseUp);
};
}, []);
React.useEffect(() => {
if (value_ !== undefined && value_ !== value) setValue(value_);
}, [value_]);
const updateTransitions = () => {
// Add momentary transition to the OnesyRoundMeter-children > *
// if selecting value updates
if (refs.root.current) {
let elementChildren = refs.root.current.getElementsByClassName('onesy-RoundMeter-children')[0];
let elementLabels = refs.root.current.getElementsByClassName('onesy-RoundMeter-labels')[0];
if (elementChildren && elementLabels) {
elementChildren = Array.from(elementChildren.children);
elementLabels = Array.from(elementLabels.children);
elementChildren.forEach(item_3 => item_3.style.transition = 'transform .3s');
elementLabels.forEach(item_4 => item_4.style.transition = 'fill .3s');
setTimeout(() => {
[...elementChildren, ...elementLabels].forEach(item_5 => item_5.style.removeProperty('transition'));
}, 300);
}
}
};
React.useEffect(() => {
if (selecting_ !== undefined && selecting_ !== selecting) {
setSelecting(selecting_);
refs.previous.current = selecting_;
updateTransitions();
}
}, [selecting_]);
React.useEffect(() => {
if (selecting !== refs.previous.current) {
refs.previous.current = selecting;
updateTransitions();
}
}, [selecting]);
const onUpdate = valueNew_1 => {
const newValue = valueNew_1?.milliseconds || null;
const previousValue = refs.value.current.milliseconds;
if (newValue === previousValue) return;
if (!(readOnly || disabled)) {
// Inner controlled value
if (!props.hasOwnProperty('value')) setValue(valueNew_1);
if (is('function', onChange)) onChange(valueNew_1);
}
};
const onUpdateSelecting = valueNew_2 => {
if (!(readOnly || disabled)) {
// Inner controlled selecting
if (!props.hasOwnProperty('selecting')) setSelecting(valueNew_2);
if (is('function', onChangeSelecting)) onChangeSelecting(valueNew_2);
}
};
const valid = (...args) => {
if (is('function', valid_)) return valid_(...args);
const onesyDate_0 = args[0];
if (min || max || validate) {
let response = true;
if (is('function', validate)) response = validate(onesyDate_0);
if (min !== undefined) response = response && isOnesyDate(onesyDate_0, 'after or same', min);
if (max !== undefined) response = response && isOnesyDate(onesyDate_0, 'before or same', max);
return response;
}
return true;
};
const onMouseDown = () => {
setMouseDown(true);
};
const onClick = event_1 => {
const {
clientX: x_0,
clientY: y_0
} = event_1;
onMove(x_0, y_0);
if (is('function', onClick_)) onClick_(event_1);
};
const palette = React.useMemo(() => {
if (['inherit', 'default'].includes(color)) return theme.methods.color(theme.palette.text.default.primary);
if (color === 'themed') return theme.methods.color(theme.palette.text.default.secondary);
if (color === 'inverted') return theme.methods.color(theme.palette.background.default.primary);
return theme.methods.color(theme.palette.color[color]?.main || color);
}, [color, theme]);
let valueClock = '';
let valueClock24 = 0;
let valuePosition;
let labels = [];
let lowerPointer = false;
const colors = {
regular: 'currentColor',
inverse: theme.methods.palette.color.value(undefined, 90, true, palette)
};
if (selecting === 'hour') {
// Value
valueClock = valueClock24 = value?.hour;
if (format === '24' && valueClock > 11) lowerPointer = true;
if (valueClock > 12) valueClock -= 12;
valuePosition = 100 / 12 * valueClock;
// Labels
if (format === '12') labels = unique([
// 12 hours
...Array.from({
length: 12
}).map((item_6, index_1) => ({
value: index_1 === 0 ? 12 : index_1,
padding: theme.methods.space.value(2.5, 'px'),
style: {
fontSize: 14,
opacity: valid(inputToValue(index_1 === 0 ? 12 : index_1, 'hour'), 'hour') ? 1 : 0.27,
fill: valueClock === 12 && index_1 === 0 || valueClock === index_1 ? colors.inverse : colors.regular
},
position: index_1 * (100 / 12)
}))], 'position');else {
labels = [unique([
// 0-11 hours
...Array.from({
length: 12
}).map((item_7, index_2) => ({
value: index_2 === 0 ? '00' : index_2,
padding: theme.methods.space.value(2.5, 'px'),
style: {
fontSize: 14,
opacity: valid(inputToValue(index_2 === 0 ? 0 : index_2, 'hour'), 'hour') ? 1 : 0.27,
fill: valueClock24 === index_2 ? colors.inverse : colors.regular
},
position: index_2 * (100 / 12)
}))], 'position'), unique([
// 12-23 hours
...Array.from({
length: 12
}).map((item_8, index_3) => ({
value: 12 + index_3,
padding: theme.methods.space.value(6, 'px'),
style: {
fontSize: 14,
opacity: valid(inputToValue(12 + index_3, 'hour'), 'hour') ? 1 : 0.27,
fill: valueClock24 === 12 + index_3 ? colors.inverse : colors.regular
},
position: index_3 * (100 / 12)
}))], 'position')];
}
}
if (selecting === 'minute') {
// Value
valueClock = value?.minute;
valuePosition = 100 / 60 * valueClock;
// Labels
labels = unique([
// 59 minutes
...Array.from({
length: 12
}).map((item_9, index_4) => ({
value: index_4 === 0 ? '00' : getLeadingZerosNumber(60 / 12 * index_4),
padding: theme.methods.space.value(2.5, 'px'),
style: {
fontSize: 14,
opacity: valid(inputToValue(index_4 === 0 ? 0 : 60 / 12 * index_4), 'minute') ? 1 : 0.27,
fill: valueClock === 60 / 12 * index_4 ? colors.inverse : colors.regular
},
position: index_4 * (100 / 12)
}))], 'position');
}
if (selecting === 'second') {
// Value
valueClock = value?.second;
valuePosition = 100 / 60 * valueClock;
// Labels
labels = unique([
// 59 seconds
...Array.from({
length: 12
}).map((item_10, index_5) => ({
value: index_5 === 0 ? '00' : getLeadingZerosNumber(60 / 12 * index_5),
padding: theme.methods.space.value(2.5, 'px'),
style: {
fontSize: 14,
opacity: valid(inputToValue(index_5 === 0 ? 0 : 60 / 12 * index_5, 'second'), 'second') ? 1 : 0.27,
fill: valueClock === 60 / 12 * index_5 ? colors.inverse : colors.regular
},
position: index_5 * (100 / 12)
}))], 'position');
}
return /*#__PURE__*/_jsxs(RoundMeter, _objectSpread(_objectSpread({
ref: item_11 => {
if (ref) {
if (is('function', ref)) ref(item_11);else ref.current = item_11;
}
refs.root.current = item_11;
},
tonal: tonal,
color: color,
size: size,
labels: labels,
arcsVisible: false,
childrenPosition: "pre-marks",
onClick: onClick,
background: true,
BackgroundProps: {
fill: theme.methods.palette.color.value(undefined, 70, true, palette),
onMouseDown: onMouseDown,
onTouchStart: onMouseDown
},
renderLabel: is('function', renderValue) ? (x_1, y_1, valueItem, otherProps) => renderValue(value, selecting, x_1, y_1, valueItem, otherProps) : undefined
}, other), {}, {
className: classNames([staticClassName('Clock', theme) && ['onesy-Clock-round-meter'], className, classes.root, mouseDown && classes.mouseDown]),
children: [/*#__PURE__*/_jsx(Path, {
ref: refs.middle,
Component: "circle",
r: "4",
cx: "120",
cy: "120",
style: {
stroke: 'none',
fill: palette[40]
}
}), /*#__PURE__*/_jsx(Path, {
d: "M 120 119 L 195 119 A 1 1 0 0 1 195 121 L 120 121 A 1 1 0 0 1 121 119",
value: valuePosition,
style: {
transformOrigin: '50% 50%',
fill: palette[40],
stroke: 'none'
}
}), /*#__PURE__*/_jsx(Path, {
Component: "circle",
r: "24",
cx: lowerPointer ? 182 : 212.5,
cy: "120",
value: valuePosition,
style: {
transformOrigin: 'center',
fill: palette[40],
stroke: 'none'
}
})]
}));
};
Clock.displayName = 'onesy-Clock';
export default Clock;