• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 16061365012

03 Jul 2025 10:03PM UTC coverage: 76.877% (-0.1%) from 76.972%
16061365012

Pull #11087

github

web-flow
Merge f7c173b17 into 01d598372
Pull Request #11087: #8338: Implement a terrain layer selector

31243 of 48671 branches covered (64.19%)

124 of 152 new or added lines in 10 files covered. (81.58%)

240 existing lines in 17 files now uncovered.

38832 of 50512 relevant lines covered (76.88%)

36.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.34
/web/client/components/I18N/IntlNumberFormControl.jsx
1
/*
2
 * Copyright 2020, GeoSolutions Sas.
3
 * All rights reserved.
4
 *
5
 * This source code is licensed under the BSD-style license found in the
6
 * LICENSE file in the root directory of this source tree.
7
 */
8
import PropTypes from 'prop-types';
9

10
import React from 'react';
11
import './css/formControlIntl.css';
12
import NumericInput from '../../libs/numeric-input/NumericInput';
13
/**
14
 * Localized Numeric Input. It provides an numeric input value that uses
15
 * separators (eg. ",",".") as they are used in the current selected language from context.
16
 * @prop {string|number} value a numeric value
17
 * @prop {string|number] defaultValue a value to use as default
18
 * @prop {function} onChange handler of change event. the argument of this handler is the current value inserted.
19
 * @prop {number} step the step when you click on arrows (up, down) of the input
20
 * @prop {function} onBlur event handler on blur event
21
 */
22
class IntlNumberFormControl extends React.Component {
23

24
    static propTypes = {
1✔
25
        type: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
26
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
27
        defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
28
        onChange: PropTypes.func,
29
        step: PropTypes.number,
30
        locale: PropTypes.string,
31
        disabled: PropTypes.bool,
32
        onBlur: PropTypes.func,
33
        onKeyDown: PropTypes.func,
34
        onKeyUp: PropTypes.func,
35
        inputClassName: PropTypes.string
36
    }
37
    static contextTypes = {
1✔
38
        intl: PropTypes.object
39
    };
40
    constructor(props) {
41
        super(props);
348✔
42
        this.state = {
348✔
43
            inputRef: null
44
        };
45
        this.value = '';
348✔
46
        this.currentInputCursor = null;
348✔
47
    }
48

49
    componentDidUpdate(prevProps) {
50
        const currentValue = this.format(this.props.value || "");
291✔
51
        const prevValue = this.format(prevProps.value || "");
291✔
52
        const currentCursorPos = this.currentInputCursor;
291✔
53
        // update cursor position in case adding/deleting numbers at the middle of the current input value
54
        if (prevValue !== currentValue) {
291✔
55
            this.value = currentValue;      // set value
42✔
56
            const locale = this.context && this.context.intl && this.context.intl.locale || "en-US";
42!
57
            const formatParts = new Intl.NumberFormat(locale).formatToParts(10000.1);
42✔
58
            const groupSeparator = formatParts?.find(part => part.type === 'group').value;
84✔
59
            let isFormattedCurrentVal = currentValue && groupSeparator && (currentValue.includes(groupSeparator));
42✔
60
            let isFormattedPrevVal = prevValue && groupSeparator && (prevValue.includes(groupSeparator));
42✔
61
            if ((isFormattedCurrentVal || isFormattedPrevVal) && this.state && this.state.inputRef) {
42!
62
                let currentValueLength = currentValue.length;           // length of current value
×
63
                let prevValueLength = prevValue.length;                 // length of prev value
×
64
                let groupSeparatorPrevValue   = prevValueLength  - prevValue.replaceAll(groupSeparator, "").length;
×
UNCOV
65
                let groupSeparatorCurrentValue  = currentValueLength - currentValue.replaceAll(groupSeparator, "").length;
×
66

67
                if (currentValueLength - prevValueLength === 2 && groupSeparatorPrevValue < groupSeparatorCurrentValue) {           // in adding numbers and a group separator will be appeared due to insertion
×
68
                    this.state.inputRef.setSelectionRange(currentCursorPos + 2, currentCursorPos + 2 );
×
69
                } else if (currentValueLength - prevValueLength === 1 && groupSeparatorPrevValue === groupSeparatorCurrentValue) {  // in adding numbers and the group separators is the same after insertion
×
70
                    this.state.inputRef.setSelectionRange(currentCursorPos + 1, currentCursorPos + 1);
×
71
                } else if (prevValueLength - currentValueLength === 2 && groupSeparatorPrevValue > groupSeparatorCurrentValue) {    // in deleting numbers and a group separator will be reduced due to deletion
×
72
                    this.state.inputRef.setSelectionRange(currentCursorPos - 2 || 0, currentCursorPos - 2 || 0);
×
73
                } else if (prevValueLength - currentValueLength === 1 && groupSeparatorPrevValue === groupSeparatorCurrentValue) {  // in deleting numbers and the group separators is the same after deletion
×
UNCOV
74
                    this.state.inputRef.setSelectionRange(currentCursorPos - 1 || 0, currentCursorPos - 1 || 0);
×
75
                }
76
            }
77
        }
78
    }
79
    onKeyUp = (ev) =>{
348✔
80
        this.props.onKeyUp && this.props.onKeyUp(ev);
1✔
81
        const currentCursorPos = this.currentInputCursor;
1✔
82
        let isDelete = ev.keyCode === 8 || ev.keyCode === 46;       // delete by delete key or backspace key
1✔
83
        // move the cursor of adding number at the end of input
84
        if (![37, 39, 17].includes(ev.keyCode) && !isDelete && currentCursorPos === ev.target.value.length - 1) {
1!
UNCOV
85
            ev.target.setSelectionRange(-1, -1);
×
86
        }
87
    }
88

89
    render() {
90
        const {onChange, onBlur, disabled, type, step, value, defaultValue, onKeyDown,
91
            ...formProps} = this.props;
639✔
92
        return (
639✔
93
            <NumericInput
94
                id={'intl-numeric'}
95
                step={step}
96
                {...formProps}
97
                {...value !== undefined ? {value: this.format(value) } : {defaultValue: this.format(defaultValue)}}
639✔
98
                format={this.format}
99
                onChange={(val) => {
100
                    val === null ? this.props.onChange("") : this.props.onChange(val.toString());
52✔
101
                }}
102
                onKeyUp={this.onKeyUp}
103
                onBlur={e=>{
104
                    let val = e.target.value;
8✔
105
                    if (onBlur) {
8!
106
                        e.target.value = this.parse(val);
×
UNCOV
107
                        onBlur(e);
×
108
                    }
109
                }}
110
                disabled={disabled || false}
1,225✔
111
                onFocus={(e) =>{
112
                    // save input ref into state to enable getting/updating selection range [input cursor position]
113
                    if (!this.state.inputRef) {
16✔
114
                        this.setState({ inputRef: e.target });
10✔
115
                        return;
10✔
116
                    }
117
                    return;
6✔
118
                }}
119
                onKeyDown={(e) => {
120
                    // store the current cursor before any update
121
                    this.currentInputCursor =  e.target.selectionStart;
11✔
122
                    onKeyDown && onKeyDown(e);
11✔
123
                }
124
                }
125
                parse={this.parse}
126
                onKeyPress={e => {
127
                    const allow = e.key.match(/^[a-zA-Z]*$/);
×
UNCOV
128
                    allow !== null && e.preventDefault();
×
129
                }}
130
                componentClass={"input"}
131
                className={`form-control intl-numeric ${this.props?.inputClassName || ''}`}
1,266✔
132
                locale={this.context && this.context.intl && this.context.intl.locale || "en-US"}
1,918✔
133
            />
134
        );
135
    }
136

137
    parse = value => {
348✔
138
        let formatValue = value;
607✔
139
        // eslint-disable-next-line use-isnan
140
        if (formatValue !== NaN && formatValue !== "NaN") {  // Allow locale string to parse
607!
141
            const locale = this.context && this.context.intl && this.context.intl.locale || "en-US";
607✔
142
            const format = new Intl.NumberFormat(locale);
607✔
143
            const parts = format.formatToParts(12345.6);
607✔
144
            const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i));
6,070✔
145
            const index = new Map(numerals.map((d, i) => [d, i]));
6,070✔
146
            const group = new RegExp(
607✔
147
                `[${parts.find(d => d.type === "group").value}]`,
1,214✔
148
                "g"
149
            );
150
            const decimal = new RegExp(
607✔
151
                `[${parts.find(d => d.type === "decimal").value}]`
2,428✔
152
            );
153
            const numeral = new RegExp(`[${numerals.join("")}]`, "g");
607✔
154
            const rIndex = d => index.get(d);
1,211✔
155

156
            formatValue = (formatValue
607✔
157
                .trim()
158
                .replace(group, "")
159
                .replace(decimal, ".")
160
                .replace(numeral, rIndex));
161
            return formatValue ? +formatValue : NaN;
607✔
162
        }
UNCOV
163
        return "";
×
164
    };
165

166
    format = val => {
348✔
167
        if (!isNaN(val) && val !== "NaN") {
1,765✔
168
            const locale = this.context && this.context.intl && this.context.intl.locale || "en-US";
1,656✔
169
            const formatter = new Intl.NumberFormat(locale, {minimumFractionDigits: 0, maximumFractionDigits: 20});
1,656✔
170
            return formatter.format(val);
1,656✔
171
        }
172
        return "";
109✔
173
    };
174
}
175

176
export default IntlNumberFormControl;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc