mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-07 08:34:29 +01:00
* Add masking to Et2Textbox & Et2Number
Also prefix & suffix attribute for Et2Number
This commit is contained in:
parent
7ea6c338d3
commit
efd2159a5f
46
api/js/etemplate/Et2Textbox/Et2Number.md
Normal file
46
api/js/etemplate/Et2Textbox/Et2Number.md
Normal file
@ -0,0 +1,46 @@
|
||||
## Examples ##
|
||||
|
||||
### Precision ###
|
||||
|
||||
To enforce a certain number of decimal places, set `precision`.
|
||||
|
||||
```html:preview
|
||||
<et2-number label="Two decimal places" precision="2" value="123.456"></et2-number>
|
||||
<et2-number label="Integers only" precision="0" value="123.456"></et2-number>
|
||||
```
|
||||
|
||||
### Number Format ###
|
||||
|
||||
Normally numbers use the user's number format for thousands and decimal separator from preferences, but it is possible
|
||||
to specify for a particular number. The internal value is not affected.
|
||||
|
||||
```html:preview
|
||||
<et2-number decimalSeparator="p" thousandsSeparator=" " value="1234.56"></et2-number>
|
||||
```
|
||||
|
||||
### Minimum and Maximum ###
|
||||
|
||||
Limit the value with `min` and `max`
|
||||
|
||||
```html:preview
|
||||
<et2-number min="0" label="Greater than 0"></et2-number>
|
||||
<et2-number min="10" max="20" label="Between 10 and 20"></et2-number>
|
||||
```
|
||||
|
||||
### Prefix & Suffix ###
|
||||
|
||||
Use `prefix` and `suffix` attributes to add text before or after the input field. To include HTML or other widgets, use
|
||||
the `prefix` and `suffix` slots instead.
|
||||
|
||||
```html:preview
|
||||
<et2-number prefix="$" value="15.46"></et2-number>
|
||||
```
|
||||
|
||||
### Currency ###
|
||||
|
||||
Using `suffix`,`min` and `precision` together
|
||||
|
||||
```html:preview
|
||||
<et2-number suffix="€" min="5.67" precision="2" label="Price"></et2-number>
|
||||
```
|
||||
|
@ -9,8 +9,32 @@
|
||||
*/
|
||||
|
||||
import {Et2Textbox} from "./Et2Textbox";
|
||||
import {css, html, render} from "lit";
|
||||
import {css, html, nothing} from "lit";
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {number} from "prop-types";
|
||||
|
||||
|
||||
/**
|
||||
* @summary Enter a numeric value. Number formatting comes from preferences by default
|
||||
* @since 23.1
|
||||
*
|
||||
* @dependency sl-input
|
||||
*
|
||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot prefix - Used to prepend a presentational icon or similar element to the combobox.
|
||||
* @slot suffix - Like prefix, but after
|
||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||
*
|
||||
* @event change - Emitted when the control's value changes.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
* @csspart form-control-input - The input's wrapper.
|
||||
* @csspart form-control-help-text - The help text's wrapper.
|
||||
*/
|
||||
|
||||
@customElement("et2-number")
|
||||
export class Et2Number extends Et2Textbox
|
||||
{
|
||||
static get styles()
|
||||
@ -20,50 +44,80 @@ export class Et2Number extends Et2Textbox
|
||||
css`
|
||||
/* Scroll buttons */
|
||||
|
||||
:host(:hover) ::slotted(et2-button-scroll) {
|
||||
display: flex;
|
||||
:host(:hover) et2-button-scroll {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
::slotted(et2-button-scroll) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input--medium .input__suffix ::slotted(et2-button-scroll) {
|
||||
et2-button-scroll {
|
||||
visibility: hidden;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin-left: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
.form-control-input {
|
||||
min-width: min-content;
|
||||
max-width: 6em;
|
||||
max-width: 7em;
|
||||
}
|
||||
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
/**
|
||||
* Minimum value
|
||||
*/
|
||||
min: Number,
|
||||
@property({type: Number})
|
||||
min;
|
||||
|
||||
/**
|
||||
* Maximum value
|
||||
*/
|
||||
max: Number,
|
||||
@property({type: Number})
|
||||
max;
|
||||
|
||||
/**
|
||||
* Step value
|
||||
*/
|
||||
step: Number,
|
||||
@property({type: Number})
|
||||
step;
|
||||
|
||||
|
||||
/**
|
||||
* Precision of float number or 0 for integer
|
||||
*/
|
||||
precision: Number,
|
||||
}
|
||||
}
|
||||
@property({type: Number})
|
||||
precision;
|
||||
|
||||
/**
|
||||
* Thousands separator. Defaults to user preference.
|
||||
*/
|
||||
@property()
|
||||
thousandsSeparator;
|
||||
|
||||
/**
|
||||
* Decimal separator. Defaults to user preference.
|
||||
*/
|
||||
@property()
|
||||
decimalSeparator;
|
||||
|
||||
/**
|
||||
* Text placed before the value
|
||||
* @type {string}
|
||||
*/
|
||||
@property()
|
||||
prefix = "";
|
||||
|
||||
/**
|
||||
* Text placed after the value
|
||||
* @type {string}
|
||||
*/
|
||||
@property()
|
||||
suffix = "";
|
||||
|
||||
inputMode = "numeric";
|
||||
|
||||
get _inputNode() {return this.shadowRoot.querySelector("input");}
|
||||
|
||||
constructor()
|
||||
{
|
||||
@ -76,8 +130,33 @@ export class Et2Number extends Et2Textbox
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
// Add spinners
|
||||
render(this._incrementButtonTemplate(), this);
|
||||
let numberFormat = ".";
|
||||
if(this.egw() && this.egw().preference)
|
||||
{
|
||||
numberFormat = this.egw().preference("number_format", "common") ?? ".";
|
||||
}
|
||||
const decimal = numberFormat ? numberFormat[0] : '.';
|
||||
const thousands = numberFormat ? numberFormat[1] : '';
|
||||
this.decimalSeparator = this.decimalSeparator || decimal || ".";
|
||||
this.thousandsSeparator = this.thousandsSeparator || thousands || "";
|
||||
}
|
||||
|
||||
firstUpdated()
|
||||
{
|
||||
super.firstUpdated();
|
||||
|
||||
// Add content to slots
|
||||
["prefix", "suffix"].forEach(slot =>
|
||||
{
|
||||
if(!this[slot])
|
||||
{
|
||||
return;
|
||||
}
|
||||
this.append(Object.assign(document.createElement("span"), {
|
||||
slot: slot,
|
||||
textContent: this[slot]
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
transformAttributes(attrs)
|
||||
@ -90,7 +169,6 @@ export class Et2Number extends Et2Textbox
|
||||
{
|
||||
attrs.validator = attrs.precision === 0 ? '/^-?[0-9]*$/' : '/^-?[0-9]*[,.]?[0-9]*$/';
|
||||
}
|
||||
attrs.inputmode = "numeric";
|
||||
super.transformAttributes(attrs);
|
||||
}
|
||||
|
||||
@ -114,12 +192,7 @@ export class Et2Number extends Et2Textbox
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
handleBlur()
|
||||
{
|
||||
this.value = this.input.value;
|
||||
super.handleBlur();
|
||||
}
|
||||
|
||||
@property({type: String})
|
||||
set value(val)
|
||||
{
|
||||
if("" + val !== "")
|
||||
@ -142,51 +215,109 @@ export class Et2Number extends Et2Textbox
|
||||
{
|
||||
val = parseFloat(val);
|
||||
}
|
||||
// Put separator back in, if different
|
||||
if(typeof val === 'string' && format && sep && sep !== '.')
|
||||
{
|
||||
val = val.replace('.', sep);
|
||||
}
|
||||
}
|
||||
super.value = val;
|
||||
}
|
||||
|
||||
get value()
|
||||
get value() : string
|
||||
{
|
||||
return super.value;
|
||||
}
|
||||
|
||||
getValue() : any
|
||||
{
|
||||
if(this.value == "" || typeof this.value == "undefined")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
// Needs to be string to pass validator
|
||||
return "" + this.valueAsNumber;
|
||||
}
|
||||
|
||||
get valueAsNumber() : number
|
||||
{
|
||||
let val = super.value;
|
||||
|
||||
if("" + val !== "")
|
||||
{
|
||||
// remove decimal separator from user prefs
|
||||
const format = this.egw().preference('number_format');
|
||||
const sep = format ? format[0] : '.';
|
||||
if(typeof val === 'string' && format && sep && sep !== '.')
|
||||
{
|
||||
val = val.replace(sep, '.');
|
||||
}
|
||||
let formattedValue = this._mask?.unmaskedValue ?? this.value;
|
||||
if(typeof this.precision !== 'undefined')
|
||||
{
|
||||
val = parseFloat(parseFloat(val).toFixed(this.precision));
|
||||
formattedValue = parseFloat(parseFloat(<string>formattedValue).toFixed(this.precision));
|
||||
}
|
||||
else
|
||||
{
|
||||
val = parseFloat(val);
|
||||
formattedValue = parseFloat(<string>formattedValue);
|
||||
}
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove special formatting from a string to get just a number value
|
||||
* @param {string | number} formattedValue
|
||||
* @returns {number}
|
||||
*/
|
||||
stripFormat(formattedValue : string | number)
|
||||
{
|
||||
if("" + formattedValue !== "")
|
||||
{
|
||||
// remove thousands separator
|
||||
if(typeof formattedValue === "string" && this.thousandsSeparator)
|
||||
{
|
||||
formattedValue = formattedValue.replaceAll(this.thousandsSeparator, "");
|
||||
}
|
||||
// remove decimal separator
|
||||
if(typeof formattedValue === 'string' && this.decimalSeparator !== '.')
|
||||
{
|
||||
formattedValue = formattedValue.replace(this.decimalSeparator, '.');
|
||||
}
|
||||
if(typeof this.precision !== 'undefined')
|
||||
{
|
||||
formattedValue = parseFloat(parseFloat(<string>formattedValue).toFixed(this.precision));
|
||||
}
|
||||
else
|
||||
{
|
||||
formattedValue = parseFloat(<string>formattedValue);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
return <number>formattedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options for masking.
|
||||
* Overridden to use number-only masking
|
||||
*
|
||||
* @see https://imask.js.org/guide.html#masked-number
|
||||
*/
|
||||
protected get maskOptions()
|
||||
{
|
||||
let options = {
|
||||
...super.maskOptions,
|
||||
skipInvalid: true,
|
||||
// The initial options need to match an actual number
|
||||
radix: this.decimalSeparator,
|
||||
thousandsSeparator: this.thousandsSeparator,
|
||||
mask: Number
|
||||
}
|
||||
if(typeof this.precision != "undefined")
|
||||
{
|
||||
options.scale = this.precision;
|
||||
}
|
||||
if(typeof this.min != "undefined")
|
||||
{
|
||||
options.min = this.min;
|
||||
}
|
||||
if(typeof this.max != "undefined")
|
||||
{
|
||||
options.max = this.max;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
updateMaskValue()
|
||||
{
|
||||
this._mask.updateValue();
|
||||
this._mask.unmaskedValue = "" + this.value;
|
||||
this._mask.updateValue();
|
||||
}
|
||||
|
||||
|
||||
private handleScroll(e)
|
||||
{
|
||||
if (this.disabled) return;
|
||||
@ -211,14 +342,55 @@ export class Et2Number extends Et2Textbox
|
||||
// No increment buttons on mobile
|
||||
if(typeof egwIsMobile == "function" && egwIsMobile())
|
||||
{
|
||||
return '';
|
||||
return nothing;
|
||||
}
|
||||
// Other reasons for no buttons
|
||||
if(this.disabled || this.readonly || !this.step)
|
||||
{
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return this.disabled ? '' : html`
|
||||
return html`
|
||||
<et2-button-scroll class="et2-number__scrollbuttons" slot="suffix"
|
||||
part="scroll"
|
||||
@et2-scroll=${this.handleScroll}></et2-button-scroll>`;
|
||||
}
|
||||
|
||||
_inputTemplate()
|
||||
{
|
||||
return html`
|
||||
<sl-input
|
||||
part="input"
|
||||
max=${this.max || nothing}
|
||||
min=${this.min || nothing}
|
||||
placeholder=${this.placeholder || nothing}
|
||||
inputmode="numeric"
|
||||
?disabled=${this.disabled}
|
||||
?readonly=${this.readonly}
|
||||
?required=${this.required}
|
||||
.value=${this.formattedValue}
|
||||
@blur=${this.handleBlur}
|
||||
>
|
||||
<slot name="prefix" slot="prefix"></slot>
|
||||
${this.prefix ? html`<span slot="prefix">${this.prefix}</span>` : nothing}
|
||||
${this.suffix ? html`<span slot="suffix">${this.suffix}</span>` : nothing}
|
||||
<slot name="suffix" slot="suffix"></slot>
|
||||
${this._incrementButtonTemplate()}
|
||||
</sl-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number according to user preferences
|
||||
* @param {number} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatNumber(value : number, decimalSeparator : string = ".", thousandsSeparator : string = "") : string
|
||||
{
|
||||
// Split by . because value is a number, so . is decimal separator
|
||||
let parts = ("" + value).split(".");
|
||||
|
||||
parts[0] = parts[0].replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, thousandsSeparator);
|
||||
return parts.join(decimalSeparator);
|
||||
}
|
||||
// @ts-ignore TypeScript is not recognizing that Et2Textbox is a LitElement
|
||||
customElements.define("et2-number", Et2Number);
|
@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import {Et2TextboxReadonly} from "./Et2TextboxReadonly";
|
||||
import {formatNumber} from "./Et2Number";
|
||||
|
||||
export class Et2NumberReadonly extends Et2TextboxReadonly
|
||||
{
|
||||
@ -31,22 +32,11 @@ export class Et2NumberReadonly extends Et2TextboxReadonly
|
||||
}
|
||||
else if("" + val !== "")
|
||||
{
|
||||
if(typeof this.precision !== 'undefined')
|
||||
{
|
||||
val = parseFloat(val).toFixed(this.precision);
|
||||
}
|
||||
else
|
||||
{
|
||||
val = parseFloat(val);
|
||||
}
|
||||
}
|
||||
// use decimal separator from user prefs
|
||||
const format = this.egw().preference('number_format');
|
||||
const sep = format ? format[0] : '.';
|
||||
if(typeof val === 'string' && format && sep && sep !== '.')
|
||||
{
|
||||
val = val.replace('.', sep);
|
||||
const format = this.egw().preference('number_format') ?? ".";
|
||||
val = formatNumber(parseFloat(val), format[0], format[1]);
|
||||
}
|
||||
|
||||
// can not call super.set_value(), as it does not call the setter for value
|
||||
super.value = val;
|
||||
}
|
||||
|
31
api/js/etemplate/Et2Textbox/Et2Textbox.md
Normal file
31
api/js/etemplate/Et2Textbox/Et2Textbox.md
Normal file
@ -0,0 +1,31 @@
|
||||
## Examples
|
||||
|
||||
### Label ###
|
||||
|
||||
Use the `label` attribute to give the input an accessible label.
|
||||
|
||||
Add the `et2-label-fixed` class to force the label to have a fixed width. This helps line up labels and widgets into
|
||||
columns without having to use a grid. See [/getting-started/styling/#fixed-width-labels](styling)
|
||||
|
||||
```html:preview
|
||||
<et2-textbox label="Name"></et2-textbox>
|
||||
```
|
||||
|
||||
### Prefix & Suffix ###
|
||||
|
||||
Use `prefix` and `suffix` slots to add content before or after the text
|
||||
|
||||
```html:preview
|
||||
<et2-textbox>
|
||||
<sl-icon name="youtube" slot="prefix"></sl-icon>
|
||||
<sl-icon name="upload"></sl-icon>
|
||||
</et2-textbox>
|
||||
```
|
||||
|
||||
### Mask ###
|
||||
|
||||
Setting a mask limits what the user can enter into the field.
|
||||
|
||||
```html:preview
|
||||
<et2-textbox label="Part Number" helpText="P[aa]-0000" mask="{P}[aa]-0000"></et2-textbox>
|
||||
```
|
@ -9,12 +9,16 @@
|
||||
*/
|
||||
|
||||
|
||||
import {css, PropertyValues} from "lit";
|
||||
import {css, html, nothing, PropertyValues} from "lit";
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {Regex} from "../Validators/Regex";
|
||||
import {SlInput} from "@shoelace-style/shoelace";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import IMask, {InputMask} from "imask";
|
||||
import {SlInput} from "@shoelace-style/shoelace";
|
||||
|
||||
@customElement("et2-textbox")
|
||||
export class Et2Textbox extends Et2InputWidget(SlInput)
|
||||
{
|
||||
|
||||
@ -42,19 +46,37 @@ export class Et2Textbox extends Et2InputWidget(SlInput)
|
||||
];
|
||||
}
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
@property()
|
||||
value = "";
|
||||
|
||||
/**
|
||||
* Perl regular expression eg. '/^[0-9][a-f]{4}$/i'
|
||||
*
|
||||
* Not to be confused with this.validators, which is a list of validators for this widget
|
||||
* Placeholder text to show as a hint when the input is empty.
|
||||
*/
|
||||
validator: String,
|
||||
onkeypress: Function,
|
||||
}
|
||||
}
|
||||
@property()
|
||||
placeholder;
|
||||
|
||||
/**
|
||||
* Mask the input to enforce format. The mask is enforced as the user types, preventing invalid input.
|
||||
*/
|
||||
@property()
|
||||
mask;
|
||||
|
||||
/**
|
||||
* Disables the input. It is still visible.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@property({type: Boolean})
|
||||
disabled = false;
|
||||
|
||||
@property({type: Function})
|
||||
onkeypress;
|
||||
|
||||
private __validator : any;
|
||||
private _mask : InputMask;
|
||||
protected _value : string = "";
|
||||
|
||||
inputMode = "text";
|
||||
|
||||
|
||||
static get translate()
|
||||
{
|
||||
@ -73,6 +95,20 @@ export class Et2Textbox extends Et2InputWidget(SlInput)
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
disconnectedCallback()
|
||||
{
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("focus", this.handleFocus);
|
||||
}
|
||||
|
||||
firstUpdated()
|
||||
{
|
||||
if(this.maskOptions.mask)
|
||||
{
|
||||
this.updateMask();
|
||||
}
|
||||
}
|
||||
|
||||
/** @param changedProperties */
|
||||
updated(changedProperties : PropertyValues)
|
||||
{
|
||||
@ -83,8 +119,13 @@ export class Et2Textbox extends Et2InputWidget(SlInput)
|
||||
this.validators = (this.validators || []).filter((validator) => !(validator instanceof Regex))
|
||||
this.validators.push(new Regex(this.validator));
|
||||
}
|
||||
if(changedProperties.has('mask'))
|
||||
{
|
||||
this.updateMask();
|
||||
}
|
||||
}
|
||||
|
||||
@property()
|
||||
get validator()
|
||||
{
|
||||
return this.__validator;
|
||||
@ -107,7 +148,114 @@ export class Et2Textbox extends Et2InputWidget(SlInput)
|
||||
this.requestUpdate("validator");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore TypeScript is not recognizing that Et2Textbox is a LitElement
|
||||
customElements.define("et2-textbox", Et2Textbox);
|
||||
/**
|
||||
* Get the options for masking.
|
||||
* Can be overridden by subclass for additional options.
|
||||
*
|
||||
* @see https://imask.js.org/guide.html#masked
|
||||
*/
|
||||
protected get maskOptions()
|
||||
{
|
||||
return {
|
||||
mask: this.mask,
|
||||
lazy: this.placeholder ? true : false,
|
||||
autofix: true,
|
||||
eager: "append",
|
||||
overwrite: "shift"
|
||||
}
|
||||
}
|
||||
|
||||
protected updateMask()
|
||||
{
|
||||
const input = this.shadowRoot.querySelector("input")
|
||||
if(!this._mask)
|
||||
{
|
||||
this._mask = IMask(input, this.maskOptions);
|
||||
this.addEventListener("focus", this.handleFocus)
|
||||
window.setTimeout(() =>
|
||||
{
|
||||
this._mask.updateControl();
|
||||
}, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._mask.updateOptions(this.maskOptions);
|
||||
}
|
||||
|
||||
if(this._mask)
|
||||
{
|
||||
this.updateMaskValue();
|
||||
}
|
||||
}
|
||||
|
||||
protected updateMaskValue()
|
||||
{
|
||||
this._mask.unmaskedValue = "" + this.value;
|
||||
this._mask.updateValue();
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
this._mask.updateControl();
|
||||
});
|
||||
}
|
||||
|
||||
protected handleFocus(event)
|
||||
{
|
||||
if(this._mask)
|
||||
{
|
||||
// this._mask.updateValue();
|
||||
}
|
||||
}
|
||||
|
||||
protected _inputTemplate()
|
||||
{
|
||||
return html`
|
||||
<sl-input
|
||||
part="input"
|
||||
placeholder=${this.placeholder || nothing}
|
||||
inputmode="${this.inputMode}"
|
||||
?disabled=${this.disabled}
|
||||
?readonly=${this.readonly}
|
||||
?required=${this.required}
|
||||
.value=${this.value}
|
||||
@input=${(e) =>
|
||||
{
|
||||
if(this.__mask)
|
||||
{
|
||||
this.__mask.updateCursor(this.__mask.cursorPos)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<slot name="prefix" slot="prefix"></slot>
|
||||
<slot name="suffix" slot="suffix"></slot>
|
||||
</sl-input>
|
||||
`;
|
||||
}
|
||||
|
||||
/*
|
||||
render()
|
||||
{
|
||||
const labelTemplate = this._labelTemplate();
|
||||
const helpTemplate = this._helpTextTemplate();
|
||||
|
||||
return html`
|
||||
<div
|
||||
part="form-control"
|
||||
class=${classMap({
|
||||
'form-control': true,
|
||||
'form-control--medium': true,
|
||||
'form-control--has-label': labelTemplate !== nothing,
|
||||
'form-control--has-help-text': helpTemplate !== nothing
|
||||
})}
|
||||
>
|
||||
${labelTemplate}
|
||||
<div part="form-control-input" class="form-control-input">
|
||||
${this._inputTemplate()}
|
||||
</div>
|
||||
${helpTemplate}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
@ -65,6 +65,14 @@ describe("Number widget", () =>
|
||||
assert.equal(element.value, "1", "Wrong number of decimals");
|
||||
})
|
||||
|
||||
it("Min limit", () =>
|
||||
{
|
||||
element.value = 0;
|
||||
element.min = 2;
|
||||
element.value = "1.234";
|
||||
assert.equal(element.value, "2", "Value allowed below minimum");
|
||||
});
|
||||
|
||||
describe("Check number preferences", () =>
|
||||
{
|
||||
|
||||
|
@ -9,7 +9,6 @@ export class Regex extends Pattern
|
||||
*/
|
||||
static async getMessage(data)
|
||||
{
|
||||
// TODO: This is a poor error message, it shows the REGEX
|
||||
return data.formControl.egw().lang("'%1' has an invalid format !!!", data.params);
|
||||
return data.formControl.egw().lang("'%1' does not match the required pattern '%2'", data.modelValue, data.params);
|
||||
}
|
||||
}
|
79
package-lock.json
generated
79
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"colortranslator": "^1.9.2",
|
||||
"core-js": "^3.29.1",
|
||||
"dexie": "^3.2.4",
|
||||
"imask": "^7.6.1",
|
||||
"lit": "^2.7.5",
|
||||
"lit-flatpickr": "^0.3.0",
|
||||
"shortcut-buttons-flatpickr": "^0.4.0",
|
||||
@ -299,12 +300,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@75lb/deep-merge": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.1.tgz",
|
||||
"integrity": "sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz",
|
||||
"integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.assignwith": "^4.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"typical": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -2260,6 +2262,19 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime-corejs3": {
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.25.0.tgz",
|
||||
"integrity": "sha512-BOehWE7MgQ8W8Qn0CQnMtg2tHPHPulcS/5AVpFvs2KCK1ET+0WqZqPvnpRpFN81gYoFopdIEJX9Sgjw3ZBccPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js-pure": "^3.30.2",
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
|
||||
@ -4179,12 +4194,6 @@
|
||||
"integrity": "sha512-ARATsLdrGPUnaBvxLhUlnltcMgn7pQG312S8ccdYlnyijabrX9RN/KN/iGj9Am96CoW8e/K9628BA7Bv4XHdrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@ -4197,16 +4206,6 @@
|
||||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
||||
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@ -6957,6 +6956,17 @@
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-pure": {
|
||||
"version": "3.37.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz",
|
||||
"integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
@ -7002,12 +7012,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/custom-element-jet-brains-integration": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/custom-element-jet-brains-integration/-/custom-element-jet-brains-integration-1.2.1.tgz",
|
||||
@ -9635,6 +9639,18 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/imask": {
|
||||
"version": "7.6.1",
|
||||
"resolved": "https://registry.npmjs.org/imask/-/imask-7.6.1.tgz",
|
||||
"integrity": "sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs3": "^7.24.4"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
|
||||
@ -10948,12 +10964,6 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.assignwith": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz",
|
||||
"integrity": "sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
@ -13082,8 +13092,7 @@
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
"version": "0.15.2",
|
||||
@ -13314,7 +13323,7 @@
|
||||
"version": "2.79.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
@ -89,6 +89,7 @@
|
||||
"colortranslator": "^1.9.2",
|
||||
"core-js": "^3.29.1",
|
||||
"dexie": "^3.2.4",
|
||||
"imask": "^7.6.1",
|
||||
"lit": "^2.7.5",
|
||||
"lit-flatpickr": "^0.3.0",
|
||||
"shortcut-buttons-flatpickr": "^0.4.0",
|
||||
|
Loading…
Reference in New Issue
Block a user