2024-05-07 22:46:44 +02:00
|
|
|
import {css, html, LitElement, nothing, PropertyValues, TemplateResult} from "lit";
|
2022-02-24 23:52:45 +01:00
|
|
|
import {et2_IInput, et2_IInputNode, et2_ISubmitListener} from "../et2_core_interfaces";
|
2021-08-26 20:59:13 +02:00
|
|
|
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
2024-05-07 22:46:44 +02:00
|
|
|
import {HasSlotController} from "../Et2Widget/slot";
|
2023-03-07 18:51:33 +01:00
|
|
|
import {et2_csvSplit} from "../et2_core_common";
|
2024-03-05 19:51:33 +01:00
|
|
|
import {property} from "lit/decorators/property.js";
|
2024-05-07 22:46:44 +02:00
|
|
|
import {Validator} from "../Validators/Validator";
|
|
|
|
import {ManualMessage} from "../Validators/ManualMessage";
|
|
|
|
import {Required} from "../Validators/Required";
|
|
|
|
import {EgwValidationFeedback} from "../Validators/EgwValidationFeedback";
|
|
|
|
import {dedupeMixin} from "@open-wc/dedupe-mixin";
|
2021-08-25 19:32:15 +02:00
|
|
|
|
2023-11-16 21:13:36 +01:00
|
|
|
|
2021-08-25 19:32:15 +02:00
|
|
|
/**
|
|
|
|
* This mixin will allow any LitElement to become an Et2InputWidget
|
|
|
|
*
|
|
|
|
* Usage:
|
2021-08-27 19:21:40 +02:00
|
|
|
* export class Et2Button extends Et2InputWidget(LitWidget)) {...}
|
2021-08-25 19:32:15 +02:00
|
|
|
*/
|
|
|
|
|
2021-08-27 19:21:40 +02:00
|
|
|
/**
|
|
|
|
* Need to define the interface first, to get around TypeScript issues with protected/public
|
|
|
|
* This must match the public API for Et2InputWidgetClass
|
|
|
|
* @see https://lit.dev/docs/composition/mixins/#typing-the-subclass
|
|
|
|
*/
|
|
|
|
export declare class Et2InputWidgetInterface
|
|
|
|
{
|
2022-06-16 21:59:31 +02:00
|
|
|
readonly : boolean;
|
2024-02-22 22:32:31 +01:00
|
|
|
disabled : boolean;
|
2021-08-27 19:21:40 +02:00
|
|
|
protected value : string | number | Object;
|
2021-08-26 20:59:13 +02:00
|
|
|
|
2022-07-27 19:33:14 +02:00
|
|
|
public required : boolean;
|
|
|
|
|
2021-08-27 19:21:40 +02:00
|
|
|
public set_value(any) : void;
|
|
|
|
|
|
|
|
public get_value() : any;
|
|
|
|
|
2024-02-06 08:21:05 +01:00
|
|
|
public getValue(submit_value? : boolean) : any;
|
2021-08-27 19:21:40 +02:00
|
|
|
|
2021-12-10 19:15:02 +01:00
|
|
|
public set_readonly(boolean) : void;
|
|
|
|
|
2022-06-30 16:38:48 +02:00
|
|
|
public set_validation_error(message : string | false) : void;
|
|
|
|
|
2021-08-27 19:21:40 +02:00
|
|
|
public isDirty() : boolean;
|
2021-08-25 19:32:15 +02:00
|
|
|
|
2021-08-27 19:21:40 +02:00
|
|
|
public resetDirty() : void;
|
|
|
|
|
|
|
|
public isValid(messages : string[]) : boolean;
|
|
|
|
}
|
|
|
|
|
2024-07-16 16:40:46 +02:00
|
|
|
/**
|
|
|
|
* Massively simplified validate, as compared to what ValidatorMixin gives us, since ValidatorMixin extends
|
|
|
|
* FormControlMixin which breaks SlSelect's render()
|
|
|
|
*
|
|
|
|
* We take all validators for the widget, and if there's a value (or field is required) we check the value
|
|
|
|
* with each validator. For array values we check each element with each validator. If the value does not
|
|
|
|
* pass the validator, we collect the message and display feedback to the user.
|
|
|
|
*
|
|
|
|
* We handle validation errors from the server with ManualMessages, which always "fail".
|
|
|
|
* If the value is empty, we only validate if the field is required.
|
|
|
|
*
|
|
|
|
* @param skipManual Do not run any manual validators, used during submit check. We don't want manual validators to block submit.
|
|
|
|
*/
|
|
|
|
export async function validate(widget,skipManual = false)
|
|
|
|
{
|
|
|
|
if(widget.readonly || widget.disabled)
|
|
|
|
{
|
|
|
|
// Don't validate if the widget is read-only, there's nothing the user can do about it
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
let validators = [...(widget.validators || []), ...(widget.defaultValidators || [])];
|
|
|
|
let fieldName = widget.id;
|
|
|
|
let feedbackData = [];
|
|
|
|
let resultPromises = [];
|
|
|
|
(<EgwValidationFeedback>widget.querySelector("egw-validation-feedback"))?.remove();
|
|
|
|
|
|
|
|
// Collect message of a (failing) validator
|
|
|
|
const doValidate = async function(validator, value)
|
|
|
|
{
|
|
|
|
if(validator.config.fieldName)
|
|
|
|
{
|
|
|
|
fieldName = await validator.config.fieldName;
|
|
|
|
}
|
|
|
|
// @ts-ignore [allow-protected]
|
|
|
|
return validator._getMessage({
|
|
|
|
modelValue: value,
|
|
|
|
formControl: widget,
|
|
|
|
fieldName,
|
|
|
|
}).then((message) =>
|
|
|
|
{
|
|
|
|
feedbackData.push({message, type: validator.type, validator});
|
|
|
|
});
|
|
|
|
}.bind(widget);
|
|
|
|
|
|
|
|
// Check if a validator fails
|
|
|
|
const doCheck = async(value, validator) =>
|
|
|
|
{
|
|
|
|
const result = validator.execute(value, validator.param, {node: widget});
|
|
|
|
if(result === true)
|
|
|
|
{
|
|
|
|
resultPromises.push(doValidate(validator, value));
|
|
|
|
}
|
|
|
|
else if(result !== false && typeof result.then === 'function')
|
|
|
|
{
|
|
|
|
result.then(doValidate(validator, value));
|
|
|
|
resultPromises.push(result);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
validators.map(async validator =>
|
|
|
|
{
|
|
|
|
let values = widget.getValue();
|
|
|
|
if(!Array.isArray(values))
|
|
|
|
{
|
|
|
|
values = [values];
|
|
|
|
}
|
|
|
|
if(!values.length)
|
|
|
|
{
|
|
|
|
values = [''];
|
|
|
|
} // so required validation works
|
|
|
|
|
|
|
|
// Run manual validation messages just once, doesn't usually matter what the value is
|
|
|
|
if(validator instanceof ManualMessage)
|
|
|
|
{
|
|
|
|
if(!skipManual)
|
|
|
|
{
|
|
|
|
doCheck(values, validator);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Only validate if field is required, or not required and has a value
|
|
|
|
// Don't bother to validate empty fields
|
|
|
|
else if(widget.required || !widget.required && widget.getValue() != '' && widget.getValue() !== null)
|
|
|
|
{
|
|
|
|
// Validate each individual item
|
|
|
|
values.forEach((value) => doCheck(value, validator));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
widget.validateComplete = Promise.all(resultPromises);
|
|
|
|
|
|
|
|
// Wait until all validation is finished, then update UI
|
|
|
|
widget.validateComplete.then(() =>
|
|
|
|
{
|
|
|
|
// Show feedback from all failing validators
|
|
|
|
if(feedbackData.length > 0)
|
|
|
|
{
|
|
|
|
let feedback = document.createElement("egw-validation-feedback");
|
|
|
|
feedback.feedbackData = feedbackData;
|
|
|
|
feedback.slot = "help-text";
|
|
|
|
widget.append(feedback);
|
|
|
|
if(widget.shadowRoot.querySelector("slot[name='feedback']"))
|
|
|
|
{
|
|
|
|
feedback.slot = "feedback";
|
|
|
|
}
|
|
|
|
else if(<HTMLElement>widget.shadowRoot.querySelector("#help-text"))
|
|
|
|
{
|
|
|
|
// Not always visible?
|
|
|
|
(<HTMLElement>widget.shadowRoot.querySelector("#help-text")).style.display = "initial";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// No place to show the validation error. That's a widget problem, but we'll show it as message
|
|
|
|
widget.egw().message(feedback.textContent, "error");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return widget.validateComplete;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-06-16 21:59:31 +02:00
|
|
|
type Constructor<T = {}> = new (...args : any[]) => T;
|
|
|
|
const Et2InputWidgetMixin = <T extends Constructor<LitElement>>(superclass : T) =>
|
2021-08-27 19:21:40 +02:00
|
|
|
{
|
2022-02-24 23:52:45 +01:00
|
|
|
class Et2InputWidgetClass extends Et2Widget(superclass) implements et2_IInput, et2_IInputNode, et2_ISubmitListener
|
2021-08-27 19:21:40 +02:00
|
|
|
{
|
2022-06-16 21:59:31 +02:00
|
|
|
private __readonly : boolean;
|
2024-03-05 19:51:33 +01:00
|
|
|
private __label : string = "";
|
2021-08-25 19:32:15 +02:00
|
|
|
protected _oldValue : string | number | Object;
|
2021-10-13 15:36:33 +02:00
|
|
|
protected node : HTMLElement;
|
2021-08-25 19:32:15 +02:00
|
|
|
|
2022-07-21 21:45:24 +02:00
|
|
|
// Validators assigned to one specific instance of a widget
|
|
|
|
protected validators : Validator[];
|
|
|
|
// Validators for every instance of a type of widget
|
|
|
|
protected defaultValidators : Validator[];
|
2022-09-21 17:56:15 +02:00
|
|
|
// Promise used during validation
|
|
|
|
protected validateComplete : Promise<undefined>;
|
2023-01-10 00:02:59 +01:00
|
|
|
// Hold on to any server messages while the user edits
|
|
|
|
private _messagesHeldWhileFocused : Validator[];
|
2022-07-21 21:45:24 +02:00
|
|
|
|
2022-08-03 11:39:06 +02:00
|
|
|
protected isSlComponent = false;
|
|
|
|
|
2024-05-07 22:46:44 +02:00
|
|
|
// Allows us to check to see if label or help-text is set. Override to check additional slots.
|
|
|
|
protected readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
|
|
|
|
2021-08-25 19:32:15 +02:00
|
|
|
/** WebComponent **/
|
2021-08-27 19:21:40 +02:00
|
|
|
static get styles()
|
|
|
|
{
|
|
|
|
return [
|
2022-04-21 00:23:53 +02:00
|
|
|
...super.styles,
|
2022-02-25 18:21:16 +01:00
|
|
|
css`
|
2023-01-10 00:02:59 +01:00
|
|
|
/* Allow actually disabled inputs */
|
|
|
|
|
|
|
|
:host([disabled]) {
|
|
|
|
display: initial;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Needed so required can show through */
|
|
|
|
|
|
|
|
::slotted(input), input {
|
|
|
|
background-color: transparent;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Used to allow auto-sizing on slotted inputs */
|
|
|
|
|
|
|
|
.input-group__container > .input-group__input ::slotted(.form-control) {
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
|
2023-11-16 21:13:36 +01:00
|
|
|
.form-control__help-text {
|
2023-01-10 00:02:59 +01:00
|
|
|
position: relative;
|
2024-05-07 22:46:44 +02:00
|
|
|
width: 100%;
|
2023-01-10 00:02:59 +01:00
|
|
|
}
|
2022-04-22 23:21:46 +02:00
|
|
|
`
|
2021-08-27 19:21:40 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2021-08-25 19:32:15 +02:00
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
2023-03-07 18:51:33 +01:00
|
|
|
/**
|
|
|
|
* The label of the widget
|
|
|
|
* Overridden from parent to use our accessors
|
|
|
|
*/
|
|
|
|
label: {
|
|
|
|
type: String, noAccessor: true
|
|
|
|
},
|
2024-05-07 22:46:44 +02:00
|
|
|
|
2021-09-16 21:35:41 +02:00
|
|
|
// readonly is what is in the templates
|
|
|
|
// I put this in here so loadWebComponent finds it when it tries to set it from the template
|
|
|
|
readonly: {
|
2022-06-16 21:59:31 +02:00
|
|
|
type: Boolean,
|
|
|
|
reflect: true
|
2022-01-07 00:22:55 +01:00
|
|
|
},
|
|
|
|
|
2022-02-25 18:21:16 +01:00
|
|
|
required: {
|
2022-02-24 23:52:45 +01:00
|
|
|
type: Boolean,
|
|
|
|
reflect: true
|
|
|
|
},
|
2022-01-07 00:22:55 +01:00
|
|
|
onchange: {
|
|
|
|
type: Function
|
|
|
|
},
|
2022-12-15 21:18:16 +01:00
|
|
|
/**
|
|
|
|
* Have browser focus this input on load.
|
|
|
|
* Overrides etemplate2.focusOnFirstInput(), use only once per page
|
|
|
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attributes
|
|
|
|
*/
|
|
|
|
autofocus: {
|
|
|
|
type: Boolean,
|
|
|
|
reflect: true
|
2023-04-03 12:29:50 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
autocomplete: {
|
|
|
|
type: String
|
2024-04-25 21:05:15 +02:00
|
|
|
},
|
|
|
|
ariaLabel : String,
|
|
|
|
ariaDescription : String,
|
|
|
|
helpText : String,
|
2021-08-25 19:32:15 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-03-05 14:22:45 +01:00
|
|
|
/**
|
|
|
|
* List of properties that get translated
|
|
|
|
* Done separately to not interfere with properties - if we re-define label property,
|
|
|
|
* labels go missing.
|
|
|
|
* @returns object
|
|
|
|
*/
|
|
|
|
static get translate()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.translate,
|
|
|
|
placeholder: true,
|
2024-04-25 21:05:15 +02:00
|
|
|
ariaLabel : true,
|
|
|
|
ariaDescription : true,
|
|
|
|
helpText : true,
|
2022-03-05 14:22:45 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-28 11:12:04 +01:00
|
|
|
/**
|
|
|
|
* Compatibility for deprecated name "needed"
|
|
|
|
*
|
|
|
|
* @deprecated use required instead
|
|
|
|
* @param val
|
|
|
|
*/
|
|
|
|
set needed(val : boolean)
|
|
|
|
{
|
|
|
|
this.required = val;
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Compatibility for deprecated name "needed"
|
|
|
|
*
|
|
|
|
* @deprecated use required instead
|
|
|
|
*/
|
|
|
|
get needed()
|
|
|
|
{
|
|
|
|
return this.required;
|
|
|
|
}
|
|
|
|
|
2021-08-26 20:59:13 +02:00
|
|
|
constructor(...args : any[])
|
2021-08-25 19:32:15 +02:00
|
|
|
{
|
2021-08-26 20:59:13 +02:00
|
|
|
super(...args);
|
2022-07-21 21:45:24 +02:00
|
|
|
|
|
|
|
this.validators = [];
|
|
|
|
this.defaultValidators = [];
|
2023-01-10 00:02:59 +01:00
|
|
|
this._messagesHeldWhileFocused = [];
|
2022-07-26 21:56:17 +02:00
|
|
|
|
2024-03-05 19:51:33 +01:00
|
|
|
this.readonly = false;
|
2024-03-15 21:13:56 +01:00
|
|
|
this.required = false;
|
2023-01-11 19:10:25 +01:00
|
|
|
this._oldValue = this.getValue();
|
2022-08-03 11:39:06 +02:00
|
|
|
|
|
|
|
this.isSlComponent = typeof (<any>this).handleChange === 'function';
|
2023-01-10 00:02:59 +01:00
|
|
|
|
2023-01-11 19:10:25 +01:00
|
|
|
this.et2HandleFocus = this.et2HandleFocus.bind(this);
|
|
|
|
this.et2HandleBlur = this.et2HandleBlur.bind(this);
|
2023-04-03 12:29:50 +02:00
|
|
|
this.autocomplete = 'on';
|
2021-08-25 19:32:15 +02:00
|
|
|
}
|
2021-12-07 21:35:25 +01:00
|
|
|
|
2021-10-13 15:36:33 +02:00
|
|
|
connectedCallback()
|
|
|
|
{
|
|
|
|
super.connectedCallback();
|
2022-03-01 23:15:24 +01:00
|
|
|
this._oldChange = this._oldChange.bind(this);
|
2021-10-13 15:36:33 +02:00
|
|
|
this.node = this.getInputNode();
|
2022-06-23 23:55:42 +02:00
|
|
|
this.updateComplete.then(() =>
|
|
|
|
{
|
2022-08-03 11:39:06 +02:00
|
|
|
this.addEventListener(this.isSlComponent ? 'sl-change' : 'change', this._oldChange);
|
2022-06-23 23:55:42 +02:00
|
|
|
});
|
2023-01-11 19:10:25 +01:00
|
|
|
this.addEventListener("focus", this.et2HandleFocus);
|
|
|
|
this.addEventListener("blur", this.et2HandleBlur);
|
2024-04-26 12:04:37 +02:00
|
|
|
|
|
|
|
// set aria-label and -description fallbacks (done here and not in updated to ensure reliable fallback order)
|
|
|
|
if (!this.ariaLabel) this.ariaLabel = this.label || this.placeholder || this.statustext;
|
|
|
|
if (!this.ariaDescription) this.ariaDescription = this.helpText || (this.statustext !== this.ariaLabel ? this.statustext : '');
|
2024-04-26 14:47:01 +02:00
|
|
|
this._setAriaAttributes();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set aria-attributes on our input node
|
|
|
|
*
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _setAriaAttributes()
|
|
|
|
{
|
2024-04-26 12:04:37 +02:00
|
|
|
// pass them on to input-node, if we have one / this.getInputNode() returns one
|
|
|
|
const input = this.getInputNode();
|
|
|
|
if (input)
|
|
|
|
{
|
|
|
|
input.ariaLabel = this.ariaLabel;
|
|
|
|
input.ariaDescription = this.ariaDescription;
|
|
|
|
}
|
2021-10-13 15:36:33 +02:00
|
|
|
}
|
2021-12-07 21:35:25 +01:00
|
|
|
|
2022-03-01 23:15:24 +01:00
|
|
|
disconnectedCallback()
|
|
|
|
{
|
|
|
|
super.disconnectedCallback();
|
2022-08-03 11:39:06 +02:00
|
|
|
this.removeEventListener(this.isSlComponent ? 'sl-change' : 'change', this._oldChange);
|
2023-01-10 00:02:59 +01:00
|
|
|
|
2023-01-11 19:10:25 +01:00
|
|
|
this.removeEventListener("focus", this.et2HandleFocus);
|
|
|
|
this.removeEventListener("blur", this.et2HandleBlur);
|
2022-03-01 23:15:24 +01:00
|
|
|
}
|
|
|
|
|
2022-02-24 23:52:45 +01:00
|
|
|
/**
|
|
|
|
* A property has changed, and we want to make adjustments to other things
|
|
|
|
* based on that
|
|
|
|
*
|
2024-03-15 21:13:56 +01:00
|
|
|
* @param changedProperties
|
2022-02-24 23:52:45 +01:00
|
|
|
*/
|
|
|
|
updated(changedProperties : PropertyValues)
|
|
|
|
{
|
|
|
|
super.updated(changedProperties);
|
|
|
|
|
2022-02-28 11:12:04 +01:00
|
|
|
// required changed, add / remove validator
|
2022-02-25 18:21:16 +01:00
|
|
|
if(changedProperties.has('required'))
|
2022-02-24 23:52:45 +01:00
|
|
|
{
|
|
|
|
// Remove all existing Required validators (avoids duplicates)
|
2023-03-08 19:00:27 +01:00
|
|
|
this.validators = (this.validators || []).filter((validator) => !(validator instanceof Required))
|
2022-02-25 18:21:16 +01:00
|
|
|
if(this.required)
|
2022-02-24 23:52:45 +01:00
|
|
|
{
|
|
|
|
this.validators.push(new Required());
|
|
|
|
}
|
|
|
|
}
|
2022-09-15 20:28:49 +02:00
|
|
|
|
|
|
|
if(changedProperties.has("value"))
|
|
|
|
{
|
|
|
|
// Base off this.value, not this.getValue(), to ignore readonly
|
|
|
|
this.classList.toggle("hasValue", !(this.value == null || this.value == ""));
|
|
|
|
}
|
2024-04-26 14:47:01 +02:00
|
|
|
|
|
|
|
// pass aria-attributes to our input node
|
|
|
|
if (changedProperties.has('ariaLabel') || changedProperties.has('ariaDescription'))
|
|
|
|
{
|
|
|
|
this._setAriaAttributes();
|
|
|
|
}
|
2022-02-24 23:52:45 +01:00
|
|
|
}
|
|
|
|
|
2022-01-07 00:22:55 +01:00
|
|
|
/**
|
|
|
|
* Change handler calling custom handler set via onchange attribute
|
|
|
|
*
|
|
|
|
* @param _ev
|
|
|
|
* @returns
|
|
|
|
*/
|
2022-03-01 23:15:24 +01:00
|
|
|
_oldChange(_ev : Event) : boolean
|
2022-01-07 00:22:55 +01:00
|
|
|
{
|
2023-02-27 23:31:07 +01:00
|
|
|
if(typeof this.onchange == 'function' && (
|
|
|
|
// If we have an instanceManager, make sure it's ready. Otherwise, we ignore the event
|
|
|
|
!this.getInstanceManager() || this.getInstanceManager().isReady
|
|
|
|
))
|
2022-01-07 00:22:55 +01:00
|
|
|
{
|
|
|
|
// Make sure function gets a reference to the widget, splice it in as 2. argument if not
|
|
|
|
let args = Array.prototype.slice.call(arguments);
|
|
|
|
if(args.indexOf(this) == -1)
|
|
|
|
{
|
|
|
|
args.splice(1, 0, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.onchange(...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-01-10 00:02:59 +01:00
|
|
|
/**
|
|
|
|
* When input receives focus, clear any validation errors.
|
|
|
|
*
|
|
|
|
* If the value is the same on blur, we'll put them back
|
|
|
|
* The ones from the server (ManualMessage) can interfere with submitting.
|
2023-01-11 19:10:25 +01:00
|
|
|
*
|
|
|
|
* Named et2HandleFocus to avoid overwriting handleFocus() in Shoelace components
|
|
|
|
*
|
2023-01-10 00:02:59 +01:00
|
|
|
* @param {FocusEvent} _ev
|
|
|
|
*/
|
2023-01-11 19:10:25 +01:00
|
|
|
et2HandleFocus(_ev : FocusEvent)
|
2023-01-10 00:02:59 +01:00
|
|
|
{
|
|
|
|
if(this._messagesHeldWhileFocused.length > 0)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect any ManualMessages
|
2023-04-13 23:06:31 +02:00
|
|
|
this._messagesHeldWhileFocused = (this.validators || []).filter(
|
|
|
|
(validator) => (validator instanceof ManualMessage)
|
|
|
|
);
|
|
|
|
// Remove ManualMessages from validators list
|
|
|
|
for(let i = 0; i < this.validators.length; i++)
|
|
|
|
{
|
|
|
|
if(this._messagesHeldWhileFocused.indexOf(this.validators[i]) != -1)
|
|
|
|
{
|
|
|
|
this.validators.splice(i, 1);
|
|
|
|
}
|
|
|
|
}
|
2023-01-10 00:02:59 +01:00
|
|
|
|
2023-01-11 19:10:25 +01:00
|
|
|
this.updateComplete.then(() =>
|
|
|
|
{
|
2023-01-12 17:17:29 +01:00
|
|
|
// Remove all messages. Manual will be explicitly replaced, other validators will be re-run on blur.
|
2024-05-07 22:46:44 +02:00
|
|
|
this.querySelectorAll("egw-validation-feedback").forEach(e => e.remove());
|
2023-01-11 19:10:25 +01:00
|
|
|
});
|
2023-01-10 00:02:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If the value is unchanged, put any held validation messages back
|
2023-01-11 19:10:25 +01:00
|
|
|
*
|
|
|
|
* Named et2HandleBlur to avoid overwriting handleBlur() in Shoelace components
|
|
|
|
*
|
2023-01-10 00:02:59 +01:00
|
|
|
* @param {FocusEvent} _ev
|
|
|
|
*/
|
2023-01-11 19:10:25 +01:00
|
|
|
et2HandleBlur(_ev : FocusEvent)
|
2023-01-10 00:02:59 +01:00
|
|
|
{
|
2023-01-11 19:10:25 +01:00
|
|
|
if(this._messagesHeldWhileFocused.length > 0 && this.getValue() == this._oldValue)
|
2023-01-10 00:02:59 +01:00
|
|
|
{
|
|
|
|
this.validators = this.validators.concat(this._messagesHeldWhileFocused);
|
|
|
|
this._messagesHeldWhileFocused = [];
|
|
|
|
}
|
2023-01-12 17:17:29 +01:00
|
|
|
this.updateComplete.then(() =>
|
|
|
|
{
|
|
|
|
this.validate();
|
|
|
|
});
|
2023-01-10 00:02:59 +01:00
|
|
|
}
|
|
|
|
|
2021-08-25 19:32:15 +02:00
|
|
|
set_value(new_value)
|
|
|
|
{
|
2022-01-18 22:13:25 +01:00
|
|
|
this.value = new_value;
|
2022-03-24 16:46:27 +01:00
|
|
|
|
2023-01-11 19:10:25 +01:00
|
|
|
// Save this so we can compare against any user changes
|
|
|
|
this._oldValue = this.getValue();
|
|
|
|
|
2022-03-24 16:46:27 +01:00
|
|
|
if(typeof this._callParser == "function")
|
|
|
|
{
|
|
|
|
this.modelValue = this._callParser(new_value);
|
|
|
|
}
|
2021-08-25 19:32:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
get_value()
|
|
|
|
{
|
|
|
|
return this.getValue();
|
|
|
|
}
|
|
|
|
|
2021-12-07 21:35:25 +01:00
|
|
|
set_readonly(new_value)
|
|
|
|
{
|
2022-06-16 21:59:31 +02:00
|
|
|
this.readonly = new_value;
|
2021-12-07 21:35:25 +01:00
|
|
|
}
|
|
|
|
|
2022-06-16 21:59:31 +02:00
|
|
|
// Deal with Lion readOnly vs etemplate readonly
|
|
|
|
public set readonly(new_value)
|
|
|
|
{
|
|
|
|
this.__readonly = super.__readOnly = new_value;
|
|
|
|
this.requestUpdate("readonly");
|
|
|
|
}
|
|
|
|
|
2024-02-06 08:21:05 +01:00
|
|
|
public get readonly()
|
|
|
|
{
|
|
|
|
return this.__readonly;
|
|
|
|
}
|
2022-06-16 21:59:31 +02:00
|
|
|
|
2024-05-07 22:46:44 +02:00
|
|
|
/**
|
|
|
|
* Was from early days (Lion)
|
|
|
|
* @deprecated
|
|
|
|
* @param {boolean} new_value
|
|
|
|
*/
|
2024-02-06 08:21:05 +01:00
|
|
|
set readOnly(new_value)
|
|
|
|
{
|
|
|
|
this.readonly = new_value;
|
|
|
|
}
|
2022-06-16 21:59:31 +02:00
|
|
|
|
2022-07-26 21:56:17 +02:00
|
|
|
/**
|
|
|
|
* Lion mapping
|
|
|
|
* @deprecated
|
|
|
|
*/
|
|
|
|
get readOnly()
|
2024-02-06 08:21:05 +01:00
|
|
|
{
|
|
|
|
return this.readonly;
|
|
|
|
}
|
2022-06-16 21:59:31 +02:00
|
|
|
|
2024-02-06 08:21:05 +01:00
|
|
|
/**
|
|
|
|
* @param boolean submit_value true: call by etemplate2.(getValues|submit|postSubmit)()
|
|
|
|
*/
|
|
|
|
getValue(submit_value? : boolean)
|
2021-08-25 19:32:15 +02:00
|
|
|
{
|
2023-04-25 17:08:35 +02:00
|
|
|
return this.readonly || this.disabled ? null : (
|
|
|
|
// Give a clone of objects or receiver might use the reference
|
2023-04-26 09:22:30 +02:00
|
|
|
this.value && typeof this.value == "object" ? (typeof this.value.length == "undefined" ? {...this.value} : [...this.value]) : this.value
|
2023-04-25 17:08:35 +02:00
|
|
|
);
|
2021-08-25 19:32:15 +02:00
|
|
|
}
|
|
|
|
|
2023-03-07 18:51:33 +01:00
|
|
|
/**
|
2024-03-05 19:51:33 +01:00
|
|
|
* The label of the widget
|
2023-03-07 18:51:33 +01:00
|
|
|
* Legacy support for labels with %s that get wrapped around the widget
|
|
|
|
*
|
|
|
|
* Not the best way go with webComponents - shouldn't modify their DOM like this
|
|
|
|
*
|
|
|
|
* @param new_label
|
|
|
|
*/
|
2024-03-05 19:51:33 +01:00
|
|
|
@property()
|
2023-03-07 18:51:33 +01:00
|
|
|
set label(new_label : string)
|
|
|
|
{
|
|
|
|
if(!new_label || !new_label.includes("%s"))
|
|
|
|
{
|
2024-12-05 22:27:04 +01:00
|
|
|
super.set_label(new_label);
|
|
|
|
return;
|
2023-03-07 18:51:33 +01:00
|
|
|
}
|
|
|
|
this.__label = new_label;
|
|
|
|
const [pre, post] = et2_csvSplit(new_label, 2, "%s");
|
|
|
|
this.label = pre;
|
|
|
|
if(post?.trim().length > 0)
|
|
|
|
{
|
2024-04-23 16:56:42 +02:00
|
|
|
this.__label = pre;
|
2023-03-23 21:50:35 +01:00
|
|
|
this.updateComplete.then(() =>
|
|
|
|
{
|
2023-03-23 21:42:34 +01:00
|
|
|
const label = document.createElement("et2-description");
|
|
|
|
label.innerText = post;
|
2024-04-24 16:57:50 +02:00
|
|
|
// Add into shadowDOM (may go missing, in which case we need a different strategy)
|
|
|
|
this.shadowRoot?.querySelector(".form-control-input").after(label);
|
2023-03-23 21:50:35 +01:00
|
|
|
});
|
2023-03-07 18:51:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get label()
|
|
|
|
{
|
|
|
|
return this.__label;
|
|
|
|
}
|
2021-08-25 19:32:15 +02:00
|
|
|
|
|
|
|
isDirty()
|
|
|
|
{
|
2022-07-15 17:21:48 +02:00
|
|
|
// Readonly can't be dirty, it can't change
|
2022-07-26 21:56:17 +02:00
|
|
|
if(this.readonly)
|
2022-07-15 17:21:48 +02:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-08-25 19:32:15 +02:00
|
|
|
let value = this.getValue();
|
|
|
|
if(typeof value !== typeof this._oldValue)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if(this._oldValue === value)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
switch(typeof this._oldValue)
|
|
|
|
{
|
|
|
|
case "object":
|
2021-08-26 20:59:13 +02:00
|
|
|
if(Array.isArray(this._oldValue) &&
|
2021-08-25 19:32:15 +02:00
|
|
|
this._oldValue.length !== value.length
|
|
|
|
)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
for(let key in this._oldValue)
|
|
|
|
{
|
|
|
|
if(this._oldValue[key] !== value[key])
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
default:
|
|
|
|
return this._oldValue != value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
resetDirty()
|
|
|
|
{
|
|
|
|
this._oldValue = this.getValue();
|
|
|
|
}
|
|
|
|
|
2022-02-24 23:52:45 +01:00
|
|
|
/**
|
|
|
|
* Used by etemplate2 to determine if we can submit or not
|
|
|
|
*
|
|
|
|
* @param messages
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
2021-08-25 19:32:15 +02:00
|
|
|
isValid(messages)
|
|
|
|
{
|
|
|
|
var ok = true;
|
2022-02-24 23:52:45 +01:00
|
|
|
debugger;
|
2021-08-25 19:32:15 +02:00
|
|
|
|
|
|
|
// Check for required
|
2022-02-28 11:12:04 +01:00
|
|
|
if(this.required && !this.readonly && !this.disabled &&
|
2021-08-25 19:32:15 +02:00
|
|
|
(this.getValue() == null || this.getValue().valueOf() == ''))
|
|
|
|
{
|
|
|
|
messages.push(this.egw().lang('Field must not be empty !!!'));
|
|
|
|
ok = false;
|
|
|
|
}
|
|
|
|
return ok;
|
|
|
|
}
|
|
|
|
|
2024-04-25 21:05:15 +02:00
|
|
|
/**
|
|
|
|
* Get input to e.g. set aria-attributes
|
|
|
|
*/
|
2021-08-25 19:32:15 +02:00
|
|
|
getInputNode()
|
|
|
|
{
|
2024-04-25 21:05:15 +02:00
|
|
|
return this.shadowRoot?.querySelector('input');
|
2021-08-25 19:32:15 +02:00
|
|
|
}
|
2022-02-21 15:56:30 +01:00
|
|
|
|
2022-02-24 18:35:53 +01:00
|
|
|
transformAttributes(attrs)
|
|
|
|
{
|
|
|
|
super.transformAttributes(attrs);
|
2022-02-25 18:21:16 +01:00
|
|
|
|
2023-04-12 23:13:52 +02:00
|
|
|
// Set attributes for the form / autofill. It's the individual widget's
|
|
|
|
// responsibility to do something appropriate with these properties.
|
2023-09-21 16:18:09 +02:00
|
|
|
if(this.autocomplete == "on" && window.customElements.get(this.localName).getPropertyOptions("name") != "undefined" &&
|
|
|
|
this.getArrayMgr("content") !== null
|
|
|
|
)
|
2023-04-12 23:13:52 +02:00
|
|
|
{
|
|
|
|
this.name = this.getArrayMgr("content").explodeKey(this.id).pop();
|
|
|
|
}
|
|
|
|
|
2022-02-24 18:35:53 +01:00
|
|
|
// Check whether an validation error entry exists
|
|
|
|
if(this.id && this.getArrayMgr("validation_errors"))
|
|
|
|
{
|
|
|
|
let val = this.getArrayMgr("validation_errors").getEntry(this.id);
|
|
|
|
if(val)
|
|
|
|
{
|
|
|
|
this.set_validation_error(val);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-21 20:39:00 +02:00
|
|
|
/**
|
|
|
|
* Massively simplified validate, as compared to what ValidatorMixin gives us, since ValidatorMixin extends
|
|
|
|
* FormControlMixin which breaks SlSelect's render()
|
2022-09-21 17:05:51 +02:00
|
|
|
*
|
|
|
|
* We take all validators for the widget, and if there's a value (or field is required) we check the value
|
|
|
|
* with each validator. For array values we check each element with each validator. If the value does not
|
|
|
|
* pass the validator, we collect the message and display feedback to the user.
|
|
|
|
*
|
|
|
|
* We handle validation errors from the server with ManualMessages, which always "fail".
|
|
|
|
* If the value is empty, we only validate if the field is required.
|
2023-04-13 23:06:31 +02:00
|
|
|
*
|
|
|
|
* @param skipManual Do not run any manual validators, used during submit check. We don't want manual validators to block submit.
|
2022-07-21 20:39:00 +02:00
|
|
|
*/
|
2023-04-13 23:06:31 +02:00
|
|
|
async validate(skipManual = false)
|
2022-07-21 20:39:00 +02:00
|
|
|
{
|
2024-10-21 18:10:07 +02:00
|
|
|
return validate(this, skipManual).then(() => this.requestUpdate());
|
2022-07-21 20:39:00 +02:00
|
|
|
}
|
|
|
|
|
2022-03-02 00:55:55 +01:00
|
|
|
set_validation_error(err : string | false)
|
2022-02-21 15:56:30 +01:00
|
|
|
{
|
2022-07-21 20:39:00 +02:00
|
|
|
/* Shoelace uses constraint validation API
|
|
|
|
https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-constraint-validation-api
|
|
|
|
|
|
|
|
if(err === false && this.setCustomValidity)
|
|
|
|
{
|
|
|
|
// Remove custom validity
|
|
|
|
this.setCustomValidity('');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setCustomValidity(err);
|
|
|
|
|
|
|
|
// must call reportValidity() or nothing will happen
|
|
|
|
this.reportValidity();
|
|
|
|
|
|
|
|
*/
|
2022-02-24 18:35:53 +01:00
|
|
|
|
2022-03-02 00:55:55 +01:00
|
|
|
if(err === false)
|
|
|
|
{
|
|
|
|
// Remove all Manual validators
|
2022-06-29 19:49:24 +02:00
|
|
|
this.validators = (this.validators || []).filter((validator) => !(validator instanceof ManualMessage))
|
2022-03-02 00:55:55 +01:00
|
|
|
return;
|
|
|
|
}
|
2022-02-24 18:35:53 +01:00
|
|
|
// Need to change interaction state so messages show up
|
2022-02-25 18:30:55 +01:00
|
|
|
// submitted is a little heavy-handed, especially on first load, but it works
|
|
|
|
this.submitted = true;
|
2022-03-02 00:55:55 +01:00
|
|
|
|
2022-02-24 18:35:53 +01:00
|
|
|
// Add validator
|
|
|
|
this.validators.push(new ManualMessage(err));
|
|
|
|
// Force a validate - not needed normally, but if you call set_validation_error() manually,
|
|
|
|
// it won't show up without validate()
|
|
|
|
this.validate();
|
2022-02-21 15:56:30 +01:00
|
|
|
}
|
2022-02-24 23:52:45 +01:00
|
|
|
|
2022-09-21 17:56:15 +02:00
|
|
|
/**
|
|
|
|
* Get a list of feedback types
|
|
|
|
*
|
|
|
|
* @returns {string[]}
|
|
|
|
*/
|
|
|
|
public get hasFeedbackFor() : string[]
|
|
|
|
{
|
2024-05-07 22:46:44 +02:00
|
|
|
let feedback = (this.querySelector("egw-validation-feedback"))?.feedbackData || [];
|
2022-09-21 17:56:15 +02:00
|
|
|
return feedback.map((f) => f.type);
|
|
|
|
}
|
|
|
|
|
2022-02-24 23:52:45 +01:00
|
|
|
/**
|
|
|
|
* Called whenever the template gets submitted. We return false if the widget
|
|
|
|
* is not valid, which cancels the submission.
|
|
|
|
*
|
|
|
|
* @param _values contains the values which will be sent to the server.
|
|
|
|
* Listeners may change these values before they get submitted.
|
|
|
|
*/
|
|
|
|
async submit(_values) : Promise<boolean>
|
|
|
|
{
|
|
|
|
this.submitted = true;
|
|
|
|
|
2024-05-07 22:46:44 +02:00
|
|
|
// If using validators, run them now
|
2022-02-24 23:52:45 +01:00
|
|
|
if(this.validate)
|
|
|
|
{
|
|
|
|
// Force update now
|
|
|
|
this.validate(true);
|
|
|
|
await this.validateComplete;
|
|
|
|
|
|
|
|
return (this.hasFeedbackFor || []).indexOf("error") == -1;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2024-05-07 22:46:44 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Common sub-template to add a label.
|
|
|
|
* This goes inside the form control wrapper div, before and at the same depth as the input controls.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @returns {TemplateResult} Either a TemplateResult or nothing (the object). Check for nothing to set
|
|
|
|
* 'form-control--has-label' class on the wrapper div.
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _labelTemplate() : TemplateResult | typeof nothing
|
|
|
|
{
|
|
|
|
const hasLabelSlot = this.hasSlotController?.test('label');
|
|
|
|
const hasLabel = this.label ? true : !!hasLabelSlot;
|
|
|
|
return hasLabel ? html`
|
|
|
|
<label
|
|
|
|
id="label"
|
|
|
|
part="form-control-label"
|
|
|
|
class="form-control__label"
|
|
|
|
aria-hidden=${hasLabel ? 'false' : 'true'}
|
|
|
|
@click=${typeof this.handleLabelClick == "function" ? this.handleLabelClick : nothing}
|
|
|
|
>
|
|
|
|
<slot name="label">${this.label}</slot>
|
|
|
|
</label>
|
|
|
|
` : nothing;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _helpTextTemplate() : TemplateResult | typeof nothing
|
|
|
|
{
|
|
|
|
const hasHelpTextSlot = this.hasSlotController?.test('help-text');
|
2024-10-21 18:10:07 +02:00
|
|
|
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot || this.hasFeedbackFor.length > 0;
|
2024-05-07 22:46:44 +02:00
|
|
|
return hasHelpText ? html`
|
|
|
|
<div
|
|
|
|
part="form-control-help-text"
|
|
|
|
id="help-text"
|
|
|
|
class="form-control__help-text"
|
|
|
|
aria-hidden=${hasHelpText ? 'false' : 'true'}
|
|
|
|
>
|
|
|
|
<slot name="help-text">${this.helpText}</slot>
|
|
|
|
</div>` : nothing;
|
|
|
|
}
|
2021-08-27 19:21:40 +02:00
|
|
|
}
|
2021-08-26 20:59:13 +02:00
|
|
|
|
2024-02-22 22:32:31 +01:00
|
|
|
return Et2InputWidgetClass as Constructor & T;
|
2021-08-27 19:21:40 +02:00
|
|
|
}
|
|
|
|
export const Et2InputWidget = dedupeMixin(Et2InputWidgetMixin);
|