* Add masking to Et2Textbox & Et2Number

Also prefix & suffix attribute for Et2Number
This commit is contained in:
nathan 2024-08-02 16:24:52 -06:00
parent 7ea6c338d3
commit efd2159a5f
9 changed files with 538 additions and 134 deletions

View 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>
```

View File

@ -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) {
padding: 0px;
}
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,
/**
* Maximum value
*/
max: Number,
/**
* Step value
*/
step: Number,
/**
* Precision of float number or 0 for integer
*/
precision: Number,
}
}
/**
* Minimum value
*/
@property({type: Number})
min;
/**
* Maximum value
*/
@property({type: Number})
max;
/**
* Step value
*/
@property({type: Number})
step;
/**
* Precision of float number or 0 for integer
*/
@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 !== "")
let formattedValue = this._mask?.unmaskedValue ?? this.value;
if(typeof this.precision !== 'undefined')
{
// 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 !== '.')
formattedValue = parseFloat(parseFloat(<string>formattedValue).toFixed(this.precision));
}
else
{
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)
{
val = val.replace(sep, '.');
formattedValue = formattedValue.replaceAll(this.thousandsSeparator, "");
}
// remove decimal separator
if(typeof formattedValue === 'string' && this.decimalSeparator !== '.')
{
formattedValue = formattedValue.replace(this.decimalSeparator, '.');
}
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 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);

View File

@ -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);
// use decimal separator from user prefs
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;
}

View 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>
```

View File

@ -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,
/**
* 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
*/
validator: String,
onkeypress: Function,
}
}
@property()
value = "";
/**
* Placeholder text to show as a hint when the input is empty.
*/
@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>
`;
}
*/
}

View File

@ -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", () =>
{

View File

@ -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
View File

@ -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"
},

View File

@ -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",