import CSS from "./jb-time-input.css"; import VariablesCSS from "./variables.css"; import "jb-time-picker"; import "jb-input"; import "jb-button"; import 'jb-popover'; import type { JBInputWebComponent, JBInputValue } from "jb-input"; import type { JBTimePickerWebComponent, JBTimePickerValueObject, TimeUnitsString, SecondRange } from "jb-time-picker"; import type { JBTimeInputElements, ValidationValue, } from "./types"; import { type ValidationItem, type ValidationResult, type WithValidation, ValidationHelper, type ShowValidationErrorParameters } from "jb-validation"; import { enToFaDigits, faToEnDigits, parseBooleanAttribute } from "jb-core"; import { renderHTML } from "./render"; export * from './types.js'; import { i18n } from 'jb-core/i18n' import type { JBFormInputStandards } from "jb-form"; //TODO: accept js Date value in value setter and extract time from date and return given date with a inputted time //TODO: add picker disabler and handle virtual keyboard for it //TODO: add placeholder handler like date input export class JBTimeInputWebComponent extends HTMLElement implements WithValidation, JBFormInputStandards { static get formAssociated() { return true; } elements!: JBTimeInputElements; #isDirty = false; get value() { return this.elements.input.value; } set value(value: string) { // Invalid assignments are ignored, so they must not consume the clean // initial-value latch either. if (!this.#checkTimeFormatValidation(value)) { return; } this.#isDirty = true; this.#setValue(value); } #setValue(value: string) { const isValid = this.#checkTimeFormatValidation(value); if (isValid && this.elements.input.value !== undefined) { this.elements.input.value = value; this.#setFormValue(); this.updateTimePickerValue(this.hour, this.minute, this.second); } } get form() { return this.#internals!.form; } get name():string { return this.getAttribute("name") || ""; } set name(value: string | null | undefined) { if (value) { this.setAttribute('name', value) } else { this.removeAttribute('name') } } #initialValue = "00:00:00"; /** * Default and reset value. It initializes `value` until the live value is explicitly set. */ get initialValue(): string { return this.#initialValue; } set initialValue(value: string | null) { const normalizedValue = value ?? this.#getDefaultValue(); if (!this.#checkTimeFormatValidation(normalizedValue)) { return; } this.#initialValue = normalizedValue; if (!this.#isDirty) { this.#setValue(this.#initialValue); } } get isDirty(): boolean { return this.value !== this.initialValue; } formResetCallback() { this.#isDirty = false; this.#setValue(this.initialValue); this.#validation.reset(); this.#internals?.setValidity({}, ''); } formDisabledCallback(disabled: boolean) { this.disabled = disabled; } /** * @description will determine if component trigger jb-validation mechanism automatically on user event or it just let user-developer handle validation mechanism by himself */ get isAutoValidationDisabled(): boolean { //currently we only support disable-validation in attribute and only in initiate time but later we can add support for change of this return (!!(this.getAttribute('disable-auto-validation') === '' || this.getAttribute('disable-auto-validation') === 'true')); } #checkValidity(showError: boolean) { if (!this.isAutoValidationDisabled) { if (this.#internals?.states.has("invalid")) { // if we currently showing error to user it make sure error get updated (when failed validation changed of function return different string as an error) showError = true; } return this.#validation.checkValidity({ showError: showError }); } } #validation = new ValidationHelper({ clearValidationError: this.clearValidationError.bind(this), getValue: this.#getValidationValue.bind(this), getValidations: this.#getInsideValidations.bind(this), getValueString: () => this.value ?? "", setValidationResult: this.#setValidationResult.bind(this), showValidationError: this.showValidationError.bind(this) }); get validation() { return this.#validation; } get #inputRanges() { let dividerRange = [2]; const hourRange = [0, 1]; const minuteRange = [3, 4]; let secondRange: SecondRange = [null, null]; let maxCaretPos = 4; if (this.secondEnabled) { secondRange = [6, 7]; dividerRange = [2, 5]; maxCaretPos = 7; } return { dividerRange, hourRange, minuteRange, secondRange, maxCaretPos }; } /** * @description return hour in string base on input value */ get hourString(): string { const val = this.elements.input.value?.slice( this.#inputRanges.hourRange[0], this.#inputRanges.hourRange[1] + 1 ) ?? ""; return val; } /** * * @return {Number} return hour in number base on input value * @memberof JBTimeInputWebComponent */ get hour(): number { let val = this.hourString; if (Number.isNaN(Number(val))) { val = "0"; } return Number(val); } set hour(value: number) { this.#isDirty = true; if (this.hour !== value) { let hour = value; if (hour < 0) { hour = 0; } if (hour > 24) { hour = 24; } const hourString = hour > 9 ? `${hour}` : `0${hour}`; this.elements.input.value = `${hourString}${this.elements.input.value?.slice( this.#inputRanges.hourRange[1] + 1 )}`; this.#setFormValue(); this.updateTimePickerValue(hour, this.minute, this.second); } } /** * @description return minute in string base on input value */ get minuteString(): string { const val = this.elements.input.value?.slice( this.#inputRanges.minuteRange[0], this.#inputRanges.minuteRange[1] + 1 ) ?? ""; return val; } /** * * @description return minute in number. base on input value */ get minute(): number { let val = this.minuteString; if (Number.isNaN(Number(val))) { val = "0"; } return Number(val); } set minute(value: number) { this.#isDirty = true; if (this.minute !== value) { let minute = value; if (minute < 0) { minute = 0; } if (minute > 59) { minute = 59; } const minuteString = minute > 9 ? `${minute}` : `0${minute}`; this.elements.input.value = `${this.elements.input.value?.slice( 0, this.#inputRanges.minuteRange[0] )}${minuteString}${this.elements.input.value?.slice( this.#inputRanges.minuteRange[1] + 1 )}`; this.#setFormValue(); this.updateTimePickerValue(this.hour, minute, this.second); } } /** * @description return minute in string base on input value */ get secondString(): string { if ( this.#inputRanges.secondRange[0] !== null && this.#inputRanges.secondRange[1] !== null ) { const val = this.elements.input.value?.slice( this.#inputRanges.secondRange[0], this.#inputRanges.secondRange[1] + 1 ) ?? ""; return val; } return ""; } /** * * @description second in number. base on input value */ get second(): number | null { if (this.secondEnabled) { let val = this.secondString; if (Number.isNaN(Number(val))) { val = "0"; } return parseInt(val); } return null; } set second(value: string | number | null) { this.#isDirty = true; if (this.secondEnabled && value !== null) { if (this.second !== value) { let second = Math.floor(Number(value)); if (second < 0) { second = 0; } if (second > 59) { second = 59; } const secondString = second > 9 ? `${second}` : `0${second}`; if (this.#inputRanges.secondRange[0]) { this.elements.input.value = `${this.elements.input.value?.slice( 0, this.#inputRanges.secondRange[0] )}${secondString}`; this.#setFormValue(); this.updateTimePickerValue(this.hour, this.minute, second); } } } } #showTimePicker = false; get showTimePicker() { return this.#showTimePicker; } set showTimePicker(value) { this.#showTimePicker = value; if (value == true) { this.#internals?.states?.add("open"); if (this.#internals) this.#internals.ariaExpanded = "true"; this.elements.input.setAttribute("aria-expanded", "true"); this.elements.input.setAttribute("aria-haspopup", "dialog"); // Synchronize before opening in case the input value was changed by an external source // such as browser autofill without passing through the component's value setter. this.updateTimePickerValue(this.hour, this.minute, this.second); this.elements.timePicker.wrapper.open(); } else { this.#internals?.states?.delete("open"); if (this.#internals) this.#internals.ariaExpanded = "false"; this.elements.input.setAttribute("aria-expanded", "false"); this.elements.timePicker.wrapper.close(); } } #secondEnabled = true; get secondEnabled() { return this.#secondEnabled; } set secondEnabled(value) { if (typeof value == "boolean") { this.#secondEnabled = value; this.elements.timePicker.component.secondEnabled = value; if (value == false) { this.#disableSecond(); } else { this.#enableSecond(); } } } //will show persian number even if user type en number but value will be passed as en number #showPersianNumber = i18n.locale.numberingSystem == "arabext"; get showPersianNumber() { return this.#showPersianNumber; } set showPersianNumber(value: boolean) { this.#showPersianNumber = Boolean(value); this.elements.timePicker.component.showPersianNumber = value; this.#setValue(`${this.value}`); } #internals: ElementInternals | null = null; #disabled = false; get disabled() { return this.#disabled; } set disabled(value: boolean) { this.#disabled = value; this.elements.input.disabled = value; if (value) { this.#internals?.states?.add("disabled"); this.#internals!.ariaDisabled = "true"; } else { this.#internals?.states?.delete("disabled"); this.#internals!.ariaDisabled = "false"; } } #required = false; set required(value: boolean) { this.#required = value; this.#validation.checkValiditySync({ showError: false }); this.#internals!.ariaRequired = value ? "true" : "false"; } get required() { return this.#required; } #valueOnInputFocus: string | null = null; #isDirtyOnInputFocus = false; set optionalUnits(value: TimeUnitsString[]) { this.elements.timePicker.component.optionalUnits = value; } get optionalUnits() { return this.elements.timePicker.component.optionalUnits; } set frontalZero(value: boolean) { this.elements.timePicker.component.frontalZero = value; } get frontalZero() { return this.elements.timePicker.component.frontalZero; } constructor() { super(); if (typeof this.attachInternals == "function") { //some browser dont support attachInternals this.#internals = this.attachInternals(); this.#internals.role = "combobox"; this.#internals.ariaHasPopup = "dialog"; this.#internals.ariaExpanded = "false"; } this.#initWebComponent(); } connectedCallback() { // standard web component event that called when all of dom is bounded this.#callOnLoadEvent(); this.#initProp(); this.#callOnInitEvent(); } #callOnLoadEvent() { const event = new CustomEvent("load", { bubbles: true, composed: true }); this.dispatchEvent(event); } #callOnInitEvent() { const event = new CustomEvent("init", { bubbles: true, composed: true }); this.dispatchEvent(event); } #initWebComponent() { const shadowRoot = this.attachShadow({ mode: "open", delegatesFocus: true, clonable: true, serializable: true }); const html = `\n${renderHTML()}`; const element = document.createElement("template"); element.innerHTML = html; shadowRoot.appendChild(element.content.cloneNode(true)); this.elements = { input: shadowRoot.querySelector("jb-input")!, timePicker: { wrapper: shadowRoot.querySelector("jb-popover")!, component: shadowRoot.querySelector("jb-time-picker")!, closeButton: shadowRoot.querySelector(".close-time-picker-button")!, }, }; this.elements.input.setAttribute("inputmode", "none"); this.elements.input.setAttribute("virtualkeyboardpolicy", "manual"); this.#registerEventListener(); } #registerEventListener() { this.elements.input.addEventListener("keydown", this.#onInputKeyDown.bind(this)); this.elements.input.addEventListener("keyup", this.#onInputKeyup.bind(this)); this.elements.input.addEventListener("change", this.#onInputChange.bind(this)); this.elements.input.addEventListener("keypress", this.#onInputKeyPress.bind(this)); this.elements.input.addEventListener("beforeinput", this.#onInputBeforeInput.bind(this), {}); this.elements.input.addEventListener("focus", this.#onInputFocus.bind(this)); this.elements.input.addEventListener("blur", this.#onInputBlur.bind(this)); this.elements.timePicker.component.addEventListener("change", this.#onTimePickerChange.bind(this)); this.elements.timePicker.closeButton.addEventListener("click", () => { this.showTimePicker = false; }); this.elements.timePicker.component.addEventListener("blur", this.#onTimePickerBlur.bind(this)); } #initProp() { //set initial value to input if (!this.#isDirty) { const initialValue = this.#checkTimeFormatValidation(this.initialValue) ? this.initialValue : this.#getDefaultValue(); this.#initialValue = initialValue; this.#setValue(initialValue); } this.elements.input.addEventListener('init', () => { this.elements.input.addStandardValueCallback(this.#standardTimeValue.bind(this)); }); } #resetInputValue() { this.#setValue(this.#getDefaultValue()); } #getDefaultValue() { return this.secondEnabled ? "00:00:00" : "00:00"; } static get observedAttributes() { return [ "label", "message", "value", "name", "placeholder", "autocomplete", "readonly", "disabled", "required", "error", "size", "close-button-text", "frontal-zero", "second-enabled", "optional-units", "show-persian-number", ]; } attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { // do something when an attribute has changed if (oldValue === newValue) { return; } this.#onAttributeChange(name, newValue); } #onAttributeChange(name: string, value: string | null) { switch (name) { case "label": this.elements.input.setAttribute('label', value ?? ""); this.#internals!.ariaLabel = value ?? ""; break; case "message": this.elements.input.setAttribute("message", value ?? ""); this.#internals!.ariaDescription = value ?? ""; break; case "value": this.value = value ?? ""; break; case "name": case "placeholder": case "autocomplete": case "readonly": case "size": if (value === null) { this.elements.input.removeAttribute(name); } else { this.elements.input.setAttribute(name, value); } break; case "close-button-text": this.elements.timePicker.closeButton.innerHTML = value ?? ""; break; case "frontal-zero": this.frontalZero = parseBooleanAttribute(value, false); break; case "second-enabled": this.secondEnabled = parseBooleanAttribute(value, true); break; case "optional-units": this.optionalUnits = this.#parseOptionalUnits(value); break; case "show-persian-number": this.showPersianNumber = parseBooleanAttribute(value, false); break; case "disabled": this.disabled = parseBooleanAttribute(value, false); break; case "required": this.required = parseBooleanAttribute(value, false); break; case "error": this.reportValidity(); break; } } #parseOptionalUnits(value: string | null) { if (!value) { return []; } return value .split(/[,\s]+/g) .filter((unit): unit is TimeUnitsString => ["hour", "minute", "second"].includes(unit)); } #standardTimeValue(inputtedString: string, _oldValue: JBInputValue, prevResult: JBInputValue): JBInputValue { let displayValue = inputtedString; if (this.showPersianNumber) { displayValue = enToFaDigits(displayValue); } return { displayValue, value: inputtedString }; } /** * @public * @description add given number to hour (you can provide negative value for subtract) */ addHour(interval: number) { const hour = this.hour + interval; this.hour = hour; } /** * @public * @description add given number to minute (you can provide negative value for subtract) */ addMinute(interval: number) { const minute = this.minute + interval; this.minute = minute; } /** * @public * @description add given number to second (you can provide negative value for subtract) */ addSecond(interval: number) { const second = (this.second || 0) + interval; this.second = second; } #inputChar(char: string, pos: number) { const { dividerRange, hourRange, minuteRange, secondRange } = this.#inputRanges; if (dividerRange.includes(pos)) { pos++; if (char == ":") { return; } } if (hourRange[0] == pos) { const tailChar = isNaN(Number(this.elements.input.value?.[hourRange[1]])) ? "0" : this.elements.input.value[hourRange[1]]; this.hour = Number(char + tailChar); return; } if (hourRange[1] == pos) { const headChar = isNaN(Number(this.elements.input.value?.[hourRange[0]])) ? "0" : this.elements.input.value?.[hourRange[0]]; this.hour = Number(headChar + char); } if (minuteRange[0] == pos) { const tailChar = isNaN(Number(this.elements.input.value?.[minuteRange[1]])) ? "0" : this.elements.input.value?.[minuteRange[1]]; this.minute = Number(char + tailChar); return; } if (minuteRange[1] == pos) { const headChar = Number.isNaN(Number(this.elements.input.value?.[minuteRange[0]])) ? "0" : this.elements.input.value?.[minuteRange[0]]; this.minute = Number(headChar + char); } if (this.secondEnabled) { if (secondRange[0] == pos && secondRange[1] !== null) { const tailChar = Number.isNaN( Number(this.elements.input.value?.[secondRange[1]]) ) ? "0" : this.elements.input.value?.[secondRange[1]]; this.second = Number(char + tailChar); return; } if (secondRange[1] == pos && secondRange[0] !== null) { const headChar = Number.isNaN( Number(this.elements.input.value?.[secondRange[0]]) ) ? "0" : this.elements.input.value?.[secondRange[0]]; this.second = Number(headChar + char); } } } #onInputKeyDown(e: KeyboardEvent) { const { hourRange, minuteRange, secondRange } = this.#inputRanges; const caretPos = (e.target! as HTMLInputElement).selectionStart; if (e.keyCode == 38 || e.keyCode == 40) { //on up key and down key let interval = 0; if (e.keyCode == 38) { interval = 1; } if (e.keyCode == 40) { interval = -1; } // we use this [...hourRange,hourRange[1]+1] because we want up key work if caret was after the last number too like this=> 12|:39:43 if (caretPos !== null && caretPos !== undefined && [...hourRange, hourRange[1] + 1].includes(caretPos)) { this.elements.timePicker.component.setTimeUnitFocus("hour"); this.addHour(interval); this.elements.input.setSelectionRange(hourRange[0], hourRange[1] + 1); } if (caretPos && [...minuteRange, minuteRange[1] + 1].includes(caretPos)) { this.elements.timePicker.component.setTimeUnitFocus("minute"); this.addMinute(interval); this.elements.input.setSelectionRange(minuteRange[0], minuteRange[1] + 1); } if ( this.secondEnabled && secondRange[1] !== null && [...secondRange, secondRange[1] + 1].includes(caretPos) ) { this.elements.timePicker.component.setTimeUnitFocus("second"); this.addSecond(interval); this.elements.input.setSelectionRange(secondRange[0], secondRange[1] + 1); } e.preventDefault(); } this.#callOnKeyDownEvent(e); } #callOnKeyDownEvent(e: KeyboardEvent) { const keyDownInitObj: KeyboardEventInit = { ...e, cancelable: false }; const event = new KeyboardEvent("keydown", keyDownInitObj); this.dispatchEvent(event); } #onInputInput(e: InputEvent) { this.#checkValidity(false); this.#dispatchInputEvent(e); } #dispatchInputEvent(e: InputEvent) { //TODO: make it cancellable and fake it more because it raise from before input actually const initObj: InputEventInit = { ...e, cancelable: false }; const event = new InputEvent('input', initObj); this.dispatchEvent(event); } #onInputBeforeInput(e: InputEvent) { const target = e.target as JBInputWebComponent; let caretPos = target.selectionStart || 0; const { dividerRange, hourRange, minuteRange, secondRange, maxCaretPos } = this.#inputRanges; const actionPerChar = (inputtedChar: string) => { if (!Number.isNaN(Number(inputtedChar))) { if (dividerRange.includes(caretPos)) { caretPos++; target.setSelectionRange(caretPos, caretPos); } if (caretPos > maxCaretPos) { caretPos = maxCaretPos; } //first we must see what is the inputted char hour or minute or second //handle hour if (hourRange.includes(caretPos)) { this.elements.timePicker.component.setTimeUnitFocus("hour"); if (caretPos == hourRange[0]) { if (inputtedChar > "2") { this.hour = Number(inputtedChar); caretPos++; } else { const tailNum: string = Number.isNaN( Number(this.elements.input.value?.[hourRange[1]]) ) ? "0" : this.elements.input.value?.[hourRange[1]]; this.hour = Number(inputtedChar + tailNum); } } else { if ( inputtedChar > "4" && this.elements.input.value?.[hourRange[0]] == "2" ) { inputtedChar = "4"; } const headChar: string = Number.isNaN( Number(this.elements.input.value?.[hourRange[0]]) ) ? "0" : this.elements.input.value?.[hourRange[0]]; this.hour = Number(headChar + inputtedChar); } } //handle minute if (minuteRange.includes(caretPos)) { this.elements.timePicker.component.setTimeUnitFocus("minute"); if (caretPos == minuteRange[0]) { if (inputtedChar > "5") { this.#inputChar("0", caretPos); caretPos++; } } this.#inputChar(inputtedChar, caretPos); } //handle second if (this.secondEnabled && secondRange.includes(caretPos)) { this.elements.timePicker.component.setTimeUnitFocus("second"); if (caretPos == secondRange[0]) { if (inputtedChar > "5") { this.#inputChar("0", caretPos); caretPos++; } } this.#inputChar(inputtedChar, caretPos); } } }; const inputtedString = faToEnDigits(e.data ?? ""); if (['deleteContentBackward', 'deleteContentForward', 'delete', 'deleteByCut', 'deleteByDrag'].includes(e.inputType)) { //in delete mode if (caretPos == null || caretPos == 0) { return; } // we map to x+1 because carrot is after ":" on back space if (!dividerRange.map((x) => x + 1).includes(caretPos)) { this.#inputChar("0", caretPos - 1); } (e.target as HTMLInputElement).setSelectionRange(caretPos - 1, caretPos - 1); e.preventDefault(); } else if (typeof inputtedString == "string") { //in insert mode if (e.inputType == "insertText") { for (const char of inputtedString) { actionPerChar(char); } target.setSelectionRange(caretPos + 1, caretPos + 1); e.preventDefault(); } if (e.inputType == "insertFromPaste") { this.#handlePaste(inputtedString); e.preventDefault(); } } //because we prevent default in every scenario we have to call input event manually this.#onInputInput(e); } /**@description handle paste on beforeInput */ #handlePaste(pastedValue: string) { const selectionStart = this.elements.input.selectionStart ?? 0; const { maxCaretPos } = this.#inputRanges; const allowedPasteLength = (maxCaretPos + 1) - selectionStart; const replaceValue = pastedValue.replace(/[^0-9:]/g, "").slice(0, allowedPasteLength); replaceValue.split('').forEach((char, i) => { this.#inputChar(char, selectionStart + i); }); } #onInputKeyPress(e: KeyboardEvent) { this.#dispatchOnKeyPressEvent(e); } #dispatchOnKeyPressEvent(e: KeyboardEvent) { const keyPressInitObj: KeyboardEventInit = { ...e, cancelable: false }; const event = new KeyboardEvent("keypress", keyPressInitObj); this.dispatchEvent(event); } #onInputKeyup(e: KeyboardEvent) { const keyUpInitObj: KeyboardEventInit = { ...e, cancelable: false }; const event = new KeyboardEvent("keyup", keyUpInitObj); this.dispatchEvent(event); if (e.keyCode == 13) { this.#onInputEnter(); } } #onInputEnter() { const event = new CustomEvent("enter"); this.dispatchEvent(event); } #onInputChange() { const inputText = this.value; this.#checkValidity(true); const isTimeValid = this.#checkTimeFormatValidation(inputText ?? ""); //TODO: add on paste so component handle paste more smartly if (!isTimeValid) { if (this.#valueOnInputFocus) { this.#setValue(this.#valueOnInputFocus); } else { this.#resetInputValue(); } this.#isDirty = this.#isDirtyOnInputFocus; } // to prevent onChange call twice this.#valueOnInputFocus = this.value; //TODO: check if this works without call // const validationResult = this.triggerInputValidation(true); const event = new Event("change"); this.dispatchEvent(event); } #onInputFocus(e: FocusEvent) { this.#valueOnInputFocus = this.value; this.#isDirtyOnInputFocus = this.#isDirty; this.showTimePicker = true; const event = new FocusEvent("focus", { ...e }); this.dispatchEvent(event); } #onInputBlur(e: FocusEvent) { const focusedElement = e.relatedTarget; if (focusedElement !== this.elements.timePicker.component) { this.showTimePicker = false; } //TODO:check if this works without call // this.triggerInputValidation(true); if (this.#valueOnInputFocus !== this.value) { this.#onInputChange(); } const event = new FocusEvent("blur"); this.dispatchEvent(event); } /** * @description check if time is in proper format of time */ #checkTimeFormatValidation(text: string) { let validationRegex: RegExp | null; if (this.secondEnabled) { validationRegex = /^(?[01]\d|[2][01234]):(?[012345]\d):(?[012345]\d)$/g; } else { validationRegex = /^(?[01]\d|[2][01234]):(?[012345]\d)$/g; } const result = validationRegex.test(text); return result; } showValidationError(error: ShowValidationErrorParameters) { this.#internals?.states?.add("invalid"); this.elements.input.showValidationError?.(error); } clearValidationError() { this.#internals?.states?.delete("invalid"); this.elements.input?.clearValidationError?.(); } /** * @public */ focus() { //public method this.elements.input.focus(); } #onTimePickerChange(e: Event) { const { hour, minute, second } = (e.target as JBTimePickerWebComponent).value; this.hour = hour; this.minute = minute; if (this.secondEnabled) { this.second = second ?? null; } } #onTimePickerBlur(e: FocusEvent) { const newFocusedElement = e.relatedTarget; if (newFocusedElement !== this.elements.input) { this.showTimePicker = false; if (this.#valueOnInputFocus !== this.value) { this.#onInputChange(); } // call onchange event } } /** * @description assign new value to time picker * @public */ updateTimePickerValue( hour: number, minute: number, second: number | null | undefined ) { const valueObj: JBTimePickerValueObject = { hour, minute, }; if (this.secondEnabled && second !== undefined && second !== null) { valueObj.second = second; } this.elements.timePicker.component.value = valueObj; } #disableSecond() { //when user dont want second in time if (/^\d{2}:\d{2}:\d{2}$/.test(this.#initialValue)) { this.#initialValue = this.#initialValue.slice(0, 5); } this.#setValue(`${this.hourString}:${this.minuteString}`); } #enableSecond() { if (/^\d{2}:\d{2}$/.test(this.#initialValue)) { this.#initialValue = `${this.#initialValue}:00`; } this.#setValue(`${this.hourString}:${this.minuteString}:00`); } #getValidationValue(): ValidationValue { return { displayValue: this.elements.input.displayValue, value: this.elements.input.value, valueObject: { hour: this.hour, minute: this.minute, second: this.second } }; } #setFormValue() { this.#internals?.setFormValue(this.value); } #getInsideValidations(): ValidationItem[] { const validationList: ValidationItem[] = []; const errorAttribute = this.getAttribute("error"); if (errorAttribute && errorAttribute.trim().length > 0) { validationList.push({ validator: undefined, message: errorAttribute, stateType: "customError" }); } if (this.required) { const requiredAttribute = this.getAttribute("required"); validationList.push({ validator: ({ value }) => this.#checkTimeFormatValidation(value), message: requiredAttribute && !["", "true"].includes(requiredAttribute) ? requiredAttribute : "Please enter a complete time", stateType: "valueMissing" }); } return validationList; } /** * @public * @description this method used to check for validity but doesn't show error to user and just return the result * this method used by #internal of component */ checkValidity(): boolean { const validationResult = this.#validation.checkValiditySync({ showError: false }); if (!validationResult.isAllValid) { const event = new CustomEvent('invalid'); this.dispatchEvent(event); } return validationResult.isAllValid; } reportValidity(): boolean { const validationResult = this.#validation.checkValiditySync({ showError: true }); if (!validationResult.isAllValid) { const event = new CustomEvent('invalid'); this.dispatchEvent(event); } return validationResult.isAllValid; } /** * @description this method called on every checkValidity calls and update validation result of #internal */ #setValidationResult(result: ValidationResult) { if (result.isAllValid) { this.#internals?.setValidity({}, ''); } else { const states: ValidityStateFlags = {}; let message = ""; result.validationList.forEach((res) => { if (!res.isValid) { if (res.validation.stateType) { states[res.validation.stateType] = true; } if (message == '') { message = res.message ?? ""; } } }); this.#internals?.setValidity(states, message); } } get validationMessage() { return this.#internals?.validationMessage ?? ""; } } const myElementNotExists = !customElements.get("jb-time-input"); if (myElementNotExists) { window.customElements.define("jb-time-input", JBTimeInputWebComponent); }