@opengeoweb/form-fields 14.0.1 → 14.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.esm.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { FormControl, FormHelperText, InputLabel, Select, RadioGroup, TextField, InputAdornment } from '@mui/material';
3
- import { useFormContext, useController, useForm, FormProvider } from 'react-hook-form';
3
+ import { useFormContext, useController, useForm, FormProvider, Controller } from 'react-hook-form';
4
4
  import { isEqual } from 'lodash';
5
5
  import { dateUtils } from '@opengeoweb/shared';
6
6
  import 'i18next';
7
7
  import { useTranslation } from 'react-i18next';
8
- import React from 'react';
8
+ import React, { useState } from 'react';
9
9
  import { DateTimePicker } from '@mui/x-date-pickers';
10
10
  import { CalendarToday } from '@opengeoweb/theme';
11
11
  import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
@@ -482,8 +482,7 @@ var ReactHookFormSelect = function ReactHookFormSelect(_ref) {
482
482
  // default props
483
483
  _onChange(changeEvent, child);
484
484
  },
485
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
486
- value: value || '',
485
+ value: value != null ? value : '',
487
486
  variant: "filled"
488
487
  }, otherProps, {
489
488
  children: children
@@ -678,12 +677,10 @@ var ReactHookFormNumberField = function ReactHookFormNumberField(_ref) {
678
677
  var before = inputElement.value.slice(0, inputElement.selectionStart);
679
678
  var after = inputElement.value.slice(inputElement.selectionEnd);
680
679
  var newValue = before + clipboardText + after;
681
- // eslint-disable-next-line no-useless-escape
682
- if (inputMode === 'numeric' && !/^\-?[0-9]+$/.exec(newValue)) {
680
+ if (inputMode === 'numeric' && !/^-?[0-9]+$/.exec(newValue)) {
683
681
  event.preventDefault();
684
682
  }
685
- // eslint-disable-next-line no-useless-escape
686
- if (inputMode === 'decimal' && !/^\-?[0-9]*(\.[0-9]+)?$/.exec(newValue)) {
683
+ if (inputMode === 'decimal' && !/^-?[0-9]*(.[0-9]+)?$/.exec(newValue)) {
687
684
  event.preventDefault();
688
685
  }
689
686
  };
@@ -702,9 +699,50 @@ var ReactHookFormNumberField = function ReactHookFormNumberField(_ref) {
702
699
  event.preventDefault();
703
700
  }
704
701
  };
705
- var _React$useState = React.useState(value === null || isNaN(value) ? '' : value),
706
- displayValue = _React$useState[0],
707
- setDisplayValue = _React$useState[1];
702
+ // Transform number value to string for display, following React Hook Form transform pattern
703
+ var formatValueForDisplay = React.useCallback(function (numValue) {
704
+ if (numValue === null || numValue === undefined || isNaN(numValue)) {
705
+ return '';
706
+ }
707
+ return String(numValue);
708
+ }, []);
709
+ // Local state to preserve string precision during editing (e.g., trailing zeros), only sync from form value when not actively editing
710
+ var _React$useState = React.useState(null),
711
+ localValue = _React$useState[0],
712
+ setLocalValue = _React$useState[1];
713
+ var isFocusedRef = React.useRef(false);
714
+ var previousValueRef = React.useRef(value);
715
+ var lastSetValueRef = React.useRef(null);
716
+ // Sync local value from form value when value changes externally, but only when not actively editing and the change wasn't from our own update
717
+ React.useEffect(function () {
718
+ if (!isFocusedRef.current && value !== previousValueRef.current) {
719
+ var wasOurUpdate = lastSetValueRef.current !== null && lastSetValueRef.current === value;
720
+ if (!wasOurUpdate) {
721
+ setLocalValue(null); // Clear local value to use form value
722
+ } else if (localValue !== null) {
723
+ // It was our update, but check if localValue still represents this number accurately
724
+ var numFromLocal = convertNumericInputValue(localValue, inputMode);
725
+ if (numFromLocal !== value) {
726
+ // Local value no longer matches form value, clear it
727
+ setLocalValue(null);
728
+ }
729
+ }
730
+ previousValueRef.current = value;
731
+ }
732
+ }, [value, localValue, inputMode]);
733
+ // Compute display value: prefer local value if it has more precision than number representation
734
+ var displayValue = React.useMemo(function () {
735
+ // Always use local value if it exists and represents the same number as form value
736
+ if (localValue !== null) {
737
+ var numFromLocal = convertNumericInputValue(localValue, inputMode);
738
+ var valuesMatch = numFromLocal === value || numFromLocal === null && value === null || numFromLocal !== null && typeof numFromLocal === 'number' && typeof value === 'number' && isNaN(numFromLocal) && isNaN(value);
739
+ if (valuesMatch) {
740
+ return localValue;
741
+ }
742
+ }
743
+ // When no local value or it doesn't match, use formatted form value
744
+ return formatValueForDisplay(value);
745
+ }, [value, localValue, inputMode, formatValueForDisplay]);
708
746
  return jsx(ReactHookFormFormControl, {
709
747
  className: className,
710
748
  disabled: disabled,
@@ -719,12 +757,30 @@ var ReactHookFormNumberField = function ReactHookFormNumberField(_ref) {
719
757
  inputRef: ref,
720
758
  label: label,
721
759
  name: name,
760
+ onBlur: function onBlur(evt) {
761
+ isFocusedRef.current = false;
762
+ // Keep local value on blur to preserve precision (e.g., trailing zeros), it will be cleared only if form value changes externally
763
+ previousValueRef.current = value;
764
+ otherProps.onBlur == null || otherProps.onBlur(evt);
765
+ },
722
766
  onChange: function onChange(evt) {
723
- var value = evt.target.value;
724
- setDisplayValue(value);
725
- onChangeField(convertNumericInputValue(value, inputMode));
767
+ var inputValue = evt.target.value;
768
+ // Update local value to preserve string precision during editing
769
+ setLocalValue(inputValue);
770
+ // Convert to number for form value
771
+ var numericValue = convertNumericInputValue(inputValue, inputMode);
772
+ lastSetValueRef.current = numericValue;
773
+ onChangeField(numericValue);
726
774
  _onChange(null);
727
775
  },
776
+ onFocus: function onFocus(evt) {
777
+ isFocusedRef.current = true;
778
+ // Initialize local value with current display value when focusing
779
+ if (localValue === null) {
780
+ setLocalValue(formatValueForDisplay(value));
781
+ }
782
+ otherProps.onFocus == null || otherProps.onFocus(evt);
783
+ },
728
784
  onKeyDown: onKeyDown,
729
785
  size: size,
730
786
  type: "text",
@@ -940,6 +996,131 @@ var ReactHookFormHiddenInput = function ReactHookFormHiddenInput(_ref) {
940
996
  }, props)) : null;
941
997
  };
942
998
 
999
+ function decimalToDegreesMinutes(value) {
1000
+ var sign = value < 0 ? '-' : '';
1001
+ var abs = Math.abs(value);
1002
+ var degrees = Math.floor(abs);
1003
+ var minutes = Math.round((abs - degrees) * 60);
1004
+ // normalize rounding overflow (e.g. 59.9 min → +1 degree)
1005
+ if (minutes === 60) {
1006
+ abs += 1;
1007
+ return decimalToDegreesMinutes(sign === '-' ? -abs : abs);
1008
+ }
1009
+ return "" + sign + degrees + "\xB0" + minutes.toString().padStart(2, '0') + "\u2032";
1010
+ }
1011
+ function degreesMinutesToDecimal(input) {
1012
+ var raw = input.toString().trim();
1013
+ var negative = raw.startsWith('-');
1014
+ var digits = raw.replace(/[^0-9]/g, '');
1015
+ var sign = negative ? -1 : 1;
1016
+ if (digits.length <= 2) {
1017
+ var _deg = parseInt(digits || '0', 10);
1018
+ return sign * _deg;
1019
+ }
1020
+ // split into degrees + minutes
1021
+ var deg = parseInt(digits.slice(0, digits.length - 2), 10);
1022
+ var min = parseInt(digits.slice(-2), 10);
1023
+ // normalize minutes overflow (e.g. 65 → 1°05′)
1024
+ var extraDeg = Math.floor(min / 60);
1025
+ min %= 60;
1026
+ return sign * (deg + extraDeg + min / 60);
1027
+ }
1028
+ var DegreesMinutesField = function DegreesMinutesField(_ref) {
1029
+ var name = _ref.name,
1030
+ label = _ref.label,
1031
+ _ref$disabled = _ref.disabled,
1032
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
1033
+ _ref$isReadOnly = _ref.isReadOnly,
1034
+ isReadOnly = _ref$isReadOnly === void 0 ? false : _ref$isReadOnly,
1035
+ _ref$size = _ref.size,
1036
+ size = _ref$size === void 0 ? 'small' : _ref$size,
1037
+ _ref$isLongitude = _ref.isLongitude,
1038
+ isLongitude = _ref$isLongitude === void 0 ? false : _ref$isLongitude,
1039
+ t = _ref.t;
1040
+ var _useFormContext = useFormContext(),
1041
+ control = _useFormContext.control;
1042
+ var _useState = useState(null),
1043
+ localInput = _useState[0],
1044
+ setLocalInput = _useState[1];
1045
+ var _useState2 = useState(false),
1046
+ isEditing = _useState2[0],
1047
+ setIsEditing = _useState2[1];
1048
+ return jsx(Controller, {
1049
+ control: control,
1050
+ name: name,
1051
+ render: function render(_ref2) {
1052
+ var _fieldState$error;
1053
+ var field = _ref2.field,
1054
+ fieldState = _ref2.fieldState;
1055
+ var value = field.value,
1056
+ onChange = field.onChange;
1057
+ var handleChange = function handleChange(e) {
1058
+ setLocalInput(e.target.value);
1059
+ setIsEditing(true);
1060
+ };
1061
+ var handleBlur = function handleBlur() {
1062
+ if (localInput != null && localInput !== '-' && localInput !== '') {
1063
+ var input = localInput.replace(/[^\d-]/g, '');
1064
+ var negative = input.startsWith('-');
1065
+ var digits = input.replace(/[^0-9]/g, '');
1066
+ // Latitude: 4 digits (DDMM), Longitude: 5 (DDDMM)
1067
+ var maxLen = isLongitude ? 5 : 4;
1068
+ if (digits.length > maxLen) {
1069
+ digits = digits.slice(0, maxLen);
1070
+ }
1071
+ var normalized = negative ? "-" + digits : digits;
1072
+ try {
1073
+ var decimal = degreesMinutesToDecimal(normalized);
1074
+ onChange(decimal);
1075
+ } catch (_unused) {
1076
+ // Invalid, do not update form value
1077
+ }
1078
+ }
1079
+ setIsEditing(false);
1080
+ setLocalInput(null);
1081
+ };
1082
+ var displayValue;
1083
+ if (isEditing) {
1084
+ displayValue = localInput != null ? localInput : '';
1085
+ } else if (value != null) {
1086
+ displayValue = decimalToDegreesMinutes(value);
1087
+ } else {
1088
+ displayValue = '';
1089
+ }
1090
+ return jsx(TextField, {
1091
+ disabled: disabled,
1092
+ error: !!fieldState.error,
1093
+ helperText: (_fieldState$error = fieldState.error) == null ? void 0 : _fieldState$error.message,
1094
+ label: label,
1095
+ onBlur: handleBlur,
1096
+ onChange: handleChange,
1097
+ placeholder: isLongitude ? 'DDD°MM′' : 'DD°MM′',
1098
+ size: size,
1099
+ slotProps: {
1100
+ input: {
1101
+ readOnly: isReadOnly
1102
+ }
1103
+ },
1104
+ value: displayValue
1105
+ });
1106
+ },
1107
+ rules: {
1108
+ validate: function validate(value) {
1109
+ if (value == null || isNaN(value)) {
1110
+ return true;
1111
+ } // let empty pass
1112
+ if (!isLongitude && (value < -90 || value > 90)) {
1113
+ return t('form-fields-error-latitude-minutes');
1114
+ }
1115
+ if (isLongitude && (value < -180 || value > 180)) {
1116
+ return t('form-fields-error-longitude-minutes');
1117
+ }
1118
+ return true;
1119
+ }
1120
+ }
1121
+ });
1122
+ };
1123
+
943
1124
  // hidden input helpers, should not be part of send formdata
944
1125
  var HIDDEN_INPUT_HELPER_IS_DRAFT = 'IS_DRAFT';
945
1126
  var useDraftFormHelpers = function useDraftFormHelpers(isDefaultDraft) {
@@ -954,8 +1135,7 @@ var useDraftFormHelpers = function useDraftFormHelpers(isDefaultDraft) {
954
1135
  setValue(HIDDEN_INPUT_HELPER_IS_DRAFT, isDefaultDraft, {
955
1136
  shouldDirty: false
956
1137
  });
957
- // eslint-disable-next-line react-hooks/exhaustive-deps
958
- }, []);
1138
+ }, [isDefaultDraft, setValue]);
959
1139
  var isDraft = function isDraft() {
960
1140
  return getValues(HIDDEN_INPUT_HELPER_IS_DRAFT) === true;
961
1141
  };
@@ -983,4 +1163,4 @@ var useDraftFormHelpers = function useDraftFormHelpers(isDefaultDraft) {
983
1163
  };
984
1164
  };
985
1165
 
986
- export { FORM_FIELDS_NAMESPACE, HIDDEN_INPUT_HELPER_IS_DRAFT, ReactHookKeyboardDateTimePicker as ReactHookFormDateTime, ReactHookFormFormControl, ReactHookFormHiddenInput, ReactHookFormNumberField, ReactHookFormProviderWrapper as ReactHookFormProvider, ReactHookFormRadioGroup, ReactHookFormSelect, ReactHookFormTextField, countIntersectionPoints, defaultFormOptions, errorMessages, formFieldsTranslations, getDateValue, getDeepProperty, getFormattedValueForDatePicker, hasIntersectionWithFIR, hasMulitpleIntersections, hasValidGeometry, isEmpty, isGeometryDirty, isInteger, isLatitude, isLongitude, isMaximumOneDrawing, isNonOrBothCoordinates, isValidGeoJsonCoordinates, isValidMax, isValidMin, isXHoursAfter, isXHoursBefore, useDraftFormHelpers };
1166
+ export { DegreesMinutesField, FORM_FIELDS_NAMESPACE, HIDDEN_INPUT_HELPER_IS_DRAFT, ReactHookKeyboardDateTimePicker as ReactHookFormDateTime, ReactHookFormFormControl, ReactHookFormHiddenInput, ReactHookFormNumberField, ReactHookFormProviderWrapper as ReactHookFormProvider, ReactHookFormRadioGroup, ReactHookFormSelect, ReactHookFormTextField, countIntersectionPoints, defaultFormOptions, errorMessages, formFieldsTranslations, getDateValue, getDeepProperty, getFormattedValueForDatePicker, hasIntersectionWithFIR, hasMulitpleIntersections, hasValidGeometry, isEmpty, isGeometryDirty, isInteger, isLatitude, isLongitude, isMaximumOneDrawing, isNonOrBothCoordinates, isValidGeoJsonCoordinates, isValidMax, isValidMin, isXHoursAfter, isXHoursBefore, useDraftFormHelpers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeoweb/form-fields",
3
- "version": "14.0.1",
3
+ "version": "14.2.1",
4
4
  "description": "GeoWeb form-fields library for the opengeoweb project",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,11 +8,11 @@
8
8
  "url": "git@gitlab.com:opengeoweb/opengeoweb.git"
9
9
  },
10
10
  "dependencies": {
11
- "@opengeoweb/theme": "14.0.1",
11
+ "@opengeoweb/theme": "14.2.1",
12
12
  "react-hook-form": "^7.50.1",
13
13
  "@mui/x-date-pickers": "^8.11.2",
14
14
  "lodash": "^4.17.21",
15
- "@opengeoweb/shared": "14.0.1",
15
+ "@opengeoweb/shared": "14.2.1",
16
16
  "react-i18next": "^15.1.1",
17
17
  "@mui/material": "^7.0.1",
18
18
  "i18next": "^25.0.1",
@@ -0,0 +1,15 @@
1
+ import type { TFunction } from 'i18next';
2
+ import React from 'react';
3
+ export declare function decimalToDegreesMinutes(value: number): string;
4
+ export declare function degreesMinutesToDecimal(input: string | number): number;
5
+ interface Props {
6
+ name: string;
7
+ label: string;
8
+ disabled?: boolean;
9
+ isReadOnly?: boolean;
10
+ size?: 'small' | 'medium';
11
+ isLongitude?: boolean;
12
+ t: TFunction;
13
+ }
14
+ export declare const DegreesMinutesField: React.FC<Props>;
15
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -7,6 +7,7 @@ export { default as ReactHookFormDateTime } from './ReactHookFormDateTime';
7
7
  export * from './ReactHookFormDateTime';
8
8
  export { default as ReactHookFormProvider } from './ReactHookFormProvider';
9
9
  export { default as ReactHookFormHiddenInput } from './ReactHookFormHiddenInput';
10
+ export { DegreesMinutesField } from './DegreesMinutesField';
10
11
  export { defaultFormOptions } from './ReactHookFormProvider';
11
12
  export { errorMessages, isMaximumOneDrawing, hasValidGeometry, isGeometryDirty, isValidGeoJsonCoordinates, hasIntersectionWithFIR, countIntersectionPoints, isValidMax, isValidMin, isInteger, hasMulitpleIntersections, isLatitude, isLongitude, isNonOrBothCoordinates, isEmpty, isXHoursAfter, isXHoursBefore, } from './utils';
12
13
  export { getDeepProperty } from './formUtils';