outsystems-ui
Version:
OutSystems UI Framework
678 lines (596 loc) • 23.8 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace Providers.OSUI.Datepicker.Flatpickr {
export abstract class AbstractFlatpickr<C extends Flatpickr.AbstractFlatpickrConfig>
extends OSFramework.OSUI.Patterns.DatePicker.AbstractDatePicker<Flatpickr, C>
implements IFlatpickr
{
// Store container HTML element reference that contains an explanation about how to navigate through calendar with keyboard
private _a11yInfoContainerElem: HTMLElement;
// Event OnBodyScroll common behaviour
private _bodyScrollCommonBehaviour: SharedProviderResources.Flatpickr.UpdatePositionOnScroll;
/* Flag to store the satus of the platform input */
private _isPlatformInputDisabled: boolean;
// Store HtmlElement for the provider focus span target
private _providerFocusSpanTarget: HTMLElement;
// Store HtmlElement for the Today Button
private _todayButtonElem: HTMLElement;
// Validation of ZIndex position common behavior
private _zindexCommonBehavior: SharedProviderResources.Flatpickr.UpdateZindex;
// Store pattern input HTML element reference
protected datePickerPlatformInputElem: HTMLInputElement;
// Store the flatpickr input html element that will be added by library
protected flatpickrInputElem: HTMLInputElement;
// Store the provider options
protected flatpickrOpts: FlatpickrOptions;
// Flatpickr onChange (SelectedDate) event
protected onSelectedCallbackEvent: OSFramework.OSUI.Patterns.DatePicker.Callbacks.OSOnChangeEvent;
// Property to store a custom callback called on OnClose Flatpickr event
public onCloseCustomCallback = undefined;
constructor(uniqueId: string, configs: C) {
super(uniqueId, configs);
// Set the default library Event handler that will be used to set on the provider configs
this.configs.OnChange = this.onDateSelectedEvent.bind(this);
// Set the default library Event handler that will be used to set on the provider configs
this.configs.OnClose = this.onDatePickerClose.bind(this);
// Set the default library Event handler that will be used to set on the provider configs
this.configs.OnOpen = this.onDatePickerOpen.bind(this);
}
// Method used to set the needed HTML attributes
private _setAttributes(): void {
// Since a new input will be added by the flatpickr library, we must address it only at onReady
if (this.datePickerPlatformInputElem.nextSibling) {
this.flatpickrInputElem = this.datePickerPlatformInputElem.nextSibling as HTMLInputElement;
// Added the data-input attribute in order to input be styled as all platform inputs
OSFramework.OSUI.Helper.Dom.Attribute.Set(
this.flatpickrInputElem,
OSFramework.OSUI.GlobalEnum.HTMLAttributes.DataInput,
''
);
// If the provider cloned a disabled platform input, remove the disable attribute form the provider input
if (this.flatpickrInputElem.disabled) {
OSFramework.OSUI.Helper.Dom.Attribute.Remove(
this.flatpickrInputElem,
OSFramework.OSUI.GlobalEnum.HTMLAttributes.Disabled
);
}
}
}
// Method used to set the CSS classes to the pattern HTML elements
private _setCalendarCssClasses(): void {
OSFramework.OSUI.Helper.Dom.Styles.AddClass(
this.provider.calendarContainer,
OSFramework.OSUI.Patterns.DatePicker.Enum.CssClass.Calendar
);
// Check if there are any ExtendedClass to be added into our calendar elements
if (this.configs.ExtendedClass !== '') {
OSFramework.OSUI.Helper.Dom.Styles.ExtendedClass(
this.provider.calendarContainer,
'',
this.configs.ExtendedClass
);
}
}
// Set the clientHeight to the parent container as an inline style in order vertical content remains same and avoid content vertical flickering!
private _setParentMinHeight(): void {
OSFramework.OSUI.Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
OSFramework.OSUI.GlobalEnum.InlineStyle.Height,
this.selfElement.clientHeight + OSFramework.OSUI.GlobalEnum.Units.Pixel
);
}
// Method to handle the keydows event for the Today Button
private _todayButtonKeydown(e: KeyboardEvent): void {
switch (e.key) {
case OSFramework.OSUI.GlobalEnum.Keycodes.Tab:
// Prevent the flatpickr default behaviour for tab
return;
case OSFramework.OSUI.GlobalEnum.Keycodes.Enter:
case OSFramework.OSUI.GlobalEnum.Keycodes.Space:
e.preventDefault();
// Set the currentDate at the Datepicker
this.provider.setDate(this.provider.now, true);
// Trigger the jumpIntoDate!
this.jumpIntoToday();
break;
}
}
// Remove the clientHeight that has been assigned before the redraw process!
private _unsetParentMinHeight(): void {
OSFramework.OSUI.Helper.Dom.Styles.RemoveStyleAttribute(
this.selfElement,
OSFramework.OSUI.GlobalEnum.InlineStyle.Height
);
}
// Update certain A11Y properties
private _updateA11yProperties(): void {
// Ensure flatpickrInputElem tabindex is updated based on the platform input status
OSFramework.OSUI.Helper.Dom.Attribute.Set(
this.flatpickrInputElem,
OSFramework.OSUI.Constants.A11YAttributes.TabIndex,
this._isPlatformInputDisabled
? OSFramework.OSUI.Constants.A11YAttributes.States.TabIndexHidden
: OSFramework.OSUI.Constants.A11YAttributes.States.TabIndexShow
);
// Ensure flatpickrInputElem will also be updated for ScreenReaders
OSFramework.OSUI.Helper.Dom.Attribute.Set(
this.flatpickrInputElem,
OSFramework.OSUI.Constants.A11YAttributes.Aria.Hidden,
this._isPlatformInputDisabled.toString()
);
}
/**
* Method used to add the TodayButton at calendar
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected addTodayBtn(): void {
// Create the wrapper container
this._todayButtonElem = document.createElement(OSFramework.OSUI.GlobalEnum.HTMLElement.Div);
this._todayButtonElem.classList.add(Enum.CssClasses.TodayBtn);
// Create the TodayBtn element
const todayBtn = document.createElement(OSFramework.OSUI.GlobalEnum.HTMLElement.Link);
// Set the tabindex for the today button
if (this.provider.isOpen) {
OSFramework.OSUI.Helper.A11Y.TabIndexTrue(todayBtn);
} else {
OSFramework.OSUI.Helper.A11Y.TabIndexFalse(todayBtn);
}
const langCode = l10ns.TodayBtn[this.configs.Lang] !== undefined ? this.configs.Lang : 'en';
todayBtn.innerHTML = l10ns.TodayBtn[langCode].title;
OSFramework.OSUI.Helper.A11Y.AriaLabel(todayBtn, l10ns.TodayBtn[langCode].ariaLabel);
todayBtn.addEventListener(OSFramework.OSUI.GlobalEnum.HTMLEvent.Click, this.todayBtnClick.bind(this));
todayBtn.addEventListener(
OSFramework.OSUI.GlobalEnum.HTMLEvent.keyDown,
this._todayButtonKeydown.bind(this)
);
// Append elements to the proper containers
this._todayButtonElem.appendChild(todayBtn);
// Make sure to append this just before the focus trap span from flatpickr
this._providerFocusSpanTarget = this.provider.calendarContainer.querySelector('.focus-trap-bottom-element');
if (this._providerFocusSpanTarget) {
this.provider.calendarContainer.insertBefore(this._todayButtonElem, this._providerFocusSpanTarget);
} else {
this.provider.calendarContainer.appendChild(this._todayButtonElem);
}
}
/**
* Method that will be triggered at Flatpickr instance is ready
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected createProviderInstance(): void {
// Init provider
this.provider = window.flatpickr(this.datePickerPlatformInputElem, this.flatpickrOpts);
// Set provider Info to be used by setProviderConfigs API calls
this.updateProviderEvents({
name: SharedProviderResources.Flatpickr.Enum.ProviderInfo.Name,
version: SharedProviderResources.Flatpickr.Enum.ProviderInfo.Version,
events: this.provider.config,
});
// Set the needed HTML attributes
this._setAttributes();
// Update the platform input attributes
this.updatePlatformInputAttrs();
// Set accessibility stuff
this.setA11YProperties();
// Since native behaviour could be enabled, check if the calendar container exist!
if (this.provider.calendarContainer !== undefined) {
if (
this.configs.DisableMobile === true ||
OSFramework.OSUI.Helper.DeviceInfo.IsDesktop ||
this.configs.CalendarMode === OSFramework.OSUI.Patterns.DatePicker.Enum.Mode.Range
) {
// Add TodayBtn
if (this.configs.ShowTodayButton) {
this.addTodayBtn();
}
// Set Calendar CSS classes
this._setCalendarCssClasses();
// set the onBodyScroll update calendar position behaviour!
this._bodyScrollCommonBehaviour = new SharedProviderResources.Flatpickr.UpdatePositionOnScroll(
this
);
// set the zindex update position behaviour!
this._zindexCommonBehavior = new SharedProviderResources.Flatpickr.UpdateZindex(this);
}
}
// Due to platform validations every time a new redraw occurs we must ensure we remove the class based on a clone from the platform input!
if (this.flatpickrInputElem !== undefined && this.isBuilt) {
OSFramework.OSUI.Helper.Dom.Styles.RemoveClass(
this.flatpickrInputElem,
OSFramework.OSUI.GlobalEnum.CssClassElements.InputNotValid
);
}
// Remove inline height value style!
this._unsetParentMinHeight();
/**
* Trigger Innitialized Event.
* - This is needed for the patterns based on a provider since at the Initialized Event at the
* Platform side, custom code can be added in order to add customization to the provider.
* - This way, Initialized Event will be triggered every time a redraw occurs.
*/
this.triggerPlatformInitializedEventCallback();
}
/**
* Trigger the jumToDate to now
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected jumpIntoToday(): void {
this.provider.jumpToDate(this.provider.now);
}
/**
* Method used to update the datepicker value when the selected dates are empty,
* to add the custom callback to the OnClose event and remove the tabindex from the today button
*
* @protected
* @memberof AbstractFlatpickr
*/
protected onDatePickerClose(): void {
// Clear the provider selected date if the input is empty
if (this.provider.selectedDates.length === 0) {
this.onDateSelectedEvent(this.provider.selectedDates);
}
// Remove the tabindex from the link inside the today button if it exists
if (this.configs.ShowTodayButton && this._todayButtonElem) {
OSFramework.OSUI.Helper.A11Y.TabIndexFalse(
this._todayButtonElem.querySelector(OSFramework.OSUI.GlobalEnum.HTMLElement.Link)
);
}
// Call the custom callback provided for users to add custom code when a datepicker close
if (
this.onCloseCustomCallback !== undefined &&
typeof this.onCloseCustomCallback === OSFramework.OSUI.Constants.JavaScriptTypes.Function
) {
this.onCloseCustomCallback(this.provider);
}
}
/**
* Method used to add the custom callback to the OnOpen event and add the tabindex for the today button
*
* @protected
* @memberof AbstractFlatpickr
*/
protected onDatePickerOpen(): void {
// Add the tabindex for the link inside the today button if it exists
if (this.configs.ShowTodayButton && this._todayButtonElem) {
OSFramework.OSUI.Helper.A11Y.TabIndexTrue(
this._todayButtonElem.querySelector(OSFramework.OSUI.GlobalEnum.HTMLElement.Link)
);
}
}
/**
* The method used to prepare the pattern before being redrawn in order to prevent possible flickering.
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected prepareConfigs(): void {
// Get the library configurations
this.flatpickrOpts = this.configs.getProviderConfig();
// Instance will be Created!
this.createProviderInstance();
}
/**
* Method used to prepare pattern before being redrawed in order to prevent possible flickerings!
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected prepareToAndRedraw(): void {
this._setParentMinHeight();
this.redraw();
}
/**
* This method has no implementation on this pattern context!
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected setA11YProperties(): void {
// Since native behaviour could be enabled, check if the calendar container exist!
if (this.provider.calendarContainer !== undefined && this.flatpickrInputElem !== undefined) {
// This is needed once library set it as an hidden by default which can not be since otherwise the updating it's value will not be triggered the local variable update.
// Since this will be hidden through css, in terms of accessibility it should not be "visible"!
OSFramework.OSUI.Helper.Dom.Attribute.Set(
this.datePickerPlatformInputElem,
OSFramework.OSUI.Constants.A11YAttributes.TabIndex,
OSFramework.OSUI.Constants.A11YAttributes.States.TabIndexHidden
);
// Ensure datePickerPlatformInputElem will also be hidden for ScreenReaders
OSFramework.OSUI.Helper.Dom.Attribute.Set(
this.datePickerPlatformInputElem,
OSFramework.OSUI.Constants.A11YAttributes.Aria.Hidden,
OSFramework.OSUI.Constants.A11YAttributes.States.True
);
// Ensure A11yContainer will not be direclty visible
OSFramework.OSUI.Helper.Dom.Attribute.Set(
this._a11yInfoContainerElem,
OSFramework.OSUI.Constants.A11YAttributes.Aria.Hidden,
OSFramework.OSUI.Constants.A11YAttributes.States.True
);
this._updateA11yProperties();
// Set the default aria-label value attribute in case user didn't set it!
let ariaLabelValue = Enum.Attribute.DefaultAriaLabel as string;
// Check if aria-label attribute has been added to the default input
if (
this.datePickerPlatformInputElem.hasAttribute(OSFramework.OSUI.Constants.A11YAttributes.Aria.Label)
) {
ariaLabelValue = this.datePickerPlatformInputElem.getAttribute(
OSFramework.OSUI.Constants.A11YAttributes.Aria.Label
);
}
// Set the aria-label attribute value
OSFramework.OSUI.Helper.A11Y.AriaLabel(this.flatpickrInputElem, ariaLabelValue);
// Set the aria-describedby attribute in order to give more context about how to navigate into calendar using keyboard
if (OSFramework.OSUI.Helper.DeviceInfo.IsDesktop)
OSFramework.OSUI.Helper.A11Y.AriaDescribedBy(
this.flatpickrInputElem,
this._a11yInfoContainerElem.id
);
// Check if lang is not EN (default one)
if (this.configs.Lang !== OSFramework.OSUI.Constants.Language.short) {
// Update A11yContainer info based on the given language
this._a11yInfoContainerElem.innerHTML =
Datepicker.Flatpickr.l10ns.A11yContainerInfo[this.configs.Lang] !== undefined
? Datepicker.Flatpickr.l10ns.A11yContainerInfo[this.configs.Lang].htmlTex
: Datepicker.Flatpickr.l10ns.A11yContainerInfo.en.htmlTex;
}
}
}
/**
* This method has no implementation on this pattern context!
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected setCallbacks(): void {
console.log(OSFramework.OSUI.GlobalEnum.WarningMessages.MethodNotImplemented);
}
/**
* Method to set the html elements used
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected setHtmlElements(): void {
// Set the inputHTML element
this.datePickerPlatformInputElem = this.selfElement.querySelector('input.form-control');
// Store the default state of the input
this._isPlatformInputDisabled = this.datePickerPlatformInputElem.disabled;
// Store the reference to the info container about how to use keyboard to navigate through calendar
this._a11yInfoContainerElem = OSFramework.OSUI.Helper.Dom.TagSelector(
this.selfElement.parentElement,
OSFramework.OSUI.Constants.Dot + Enum.CssClasses.AccessibilityContainerInfo
);
// If the input hasn't be added
if (!this.datePickerPlatformInputElem) {
throw new Error(`The datepicker input at DatepickerId '${this.widgetId}' is missing`);
}
}
/**
* Remove all the assigned Events
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected unsetCallbacks(): void {
this.configs.OnChange = undefined;
this.onSelectedCallbackEvent = undefined;
super.unsetCallbacks();
}
/**
* Unsets the refences to the HTML elements.
*
* @protected
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
protected unsetHtmlElements(): void {
this._a11yInfoContainerElem = undefined;
this.datePickerPlatformInputElem = undefined;
}
/**
* Build the Pattern
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public build(): void {
super.build();
this.setHtmlElements();
}
/**
* Method used to change given propertyName at OnParametersChange platform event
*
* @param {string} propertyName the name of the property that will be changed
* @param {unknown} propertyValue the new value that should be assigned to the given property name
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public changeProperty(propertyName: string, propertyValue: unknown): void {
//Storing the current ExtendedClass, before possibly changing this property.
//This will enable us to remove the previous added classes to the element.
const oldExtendedClass = this.configs.ExtendedClass;
super.changeProperty(propertyName, propertyValue);
if (this.isBuilt) {
switch (propertyName) {
case OSFramework.OSUI.Patterns.DatePicker.Enum.Properties.FirstWeekDay:
case OSFramework.OSUI.Patterns.DatePicker.Enum.Properties.MaxDate:
case OSFramework.OSUI.Patterns.DatePicker.Enum.Properties.MinDate:
case OSFramework.OSUI.Patterns.DatePicker.Enum.Properties.ShowTodayButton:
case OSFramework.OSUI.Patterns.DatePicker.Enum.Properties.ShowWeekNumbers:
this.prepareToAndRedraw();
break;
case OSFramework.OSUI.GlobalEnum.CommonPatternsProperties.ExtendedClass:
// Since Calendar element will be added dynamically by the library outside the pattern context
OSFramework.OSUI.Helper.Dom.Styles.ExtendedClass(
this.provider.calendarContainer,
oldExtendedClass,
propertyValue as string
);
break;
}
}
}
/**
* Method used to clear the selected date
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public clear(): void {
const isInputDisable = this.datePickerPlatformInputElem.disabled;
if (isInputDisable === false) {
this.provider.clear();
}
}
/**
* Method used to close DatePicker
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public close(): void {
if (this.provider.isOpen) {
this.provider.close();
if (this.configs.ShowTodayButton) {
OSFramework.OSUI.Helper.A11Y.TabIndexFalse(this._todayButtonElem);
}
}
}
/**
* Method used to disable days on DatePicker
*
* @param disableDays
* @memberof Flatpickr.DisableDays
*/
public disableDays(disableDays: string[]): void {
this.configs.DisabledDays = disableDays;
this.prepareToAndRedraw();
}
/**
* Method used to disable weekdays on DatePicker
*
* @param disableWeekDays
* @memberof Flatpickr.DisableWeekDays
*/
public disableWeekDays(disableWeekDays: number[]): void {
this.configs.DisabledWeekDays = disableWeekDays;
this.prepareToAndRedraw();
}
/**
* Method to remove and destroy DatePicker instance
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public dispose(): void {
if (this.isBuilt) {
this.unsetCallbacks();
this.unsetHtmlElements();
if (this._bodyScrollCommonBehaviour !== undefined) {
this._bodyScrollCommonBehaviour.dispose();
this._bodyScrollCommonBehaviour = undefined;
}
// Wait for _datePickerProviderInputElem be removed from DOM, before destroy the provider instance!
OSFramework.OSUI.Helper.AsyncInvocation(this.provider.destroy);
}
super.dispose();
}
/**
* Method used to update certain properties at OnRender
*
* @memberof AbstractFlatpickr
*/
public onRender(): void {
// Get the current status of the platform input
const isInputDisable = this.datePickerPlatformInputElem.disabled;
// Ensure the platform input status has been updated to prevent multiple calls due to the OnRender event
if (this._isPlatformInputDisabled !== isInputDisable) {
this._isPlatformInputDisabled = isInputDisable;
this._updateA11yProperties();
}
}
/**
* Method used to open DatePicker
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public open(): void {
const isInputDisable = this.datePickerPlatformInputElem.disabled;
if (this.provider.isOpen === false && isInputDisable === false) {
this.provider.open();
// Focus on input element, after the open trigger occur
if (this.flatpickrInputElem) {
this.flatpickrInputElem.focus();
}
if (this.configs.ShowTodayButton) {
OSFramework.OSUI.Helper.A11Y.TabIndexTrue(this._todayButtonElem);
}
}
}
/**
* Method used to regist callback events
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public registerCallback(eventName: string, callback: OSFramework.OSUI.GlobalCallbacks.OSGeneric): void {
switch (eventName) {
case OSFramework.OSUI.Patterns.DatePicker.Enum.DatePickerEvents.OnChange:
this.onSelectedCallbackEvent = callback;
break;
default:
super.registerCallback(eventName, callback);
break;
}
}
/**
* Method used to set the DatePicker as editable on its input
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public setEditableInput(isEditable: boolean): void {
if (this.configs.AllowInput !== isEditable) {
this.configs.AllowInput = isEditable;
this.prepareToAndRedraw();
}
}
/**
* Method used to set the DatePicker language
*
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public setLanguage(value: string): void {
// Set the new Language
this.configs.Lang = value.toLowerCase();
// If provider has been already defined, calendar must be redrawed!
if (this.provider !== undefined) {
this.prepareToAndRedraw();
}
}
/**
* Method used to set all the extended Flatpickr properties across the different types of instances
*
* @param {FlatpickrOptions} newConfigs
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public setProviderConfigs(newConfigs: FlatpickrOptions): void {
this.configs.setExtensibilityConfigs(newConfigs);
this.prepareToAndRedraw();
super.setProviderConfigs(newConfigs);
}
/**
* Method used to update the prompt message
*
* @param promptMessage The new prompt message value
* @memberof Providers.OSUI.DatePicker.Flatpickr.AbstractFlatpickr
*/
public updatePrompt(promptMessage: string): void {
this.flatpickrInputElem.placeholder = promptMessage;
}
// Common methods all DatePickers must implement
protected abstract onDateSelectedEvent(selectedDates: Array<Date>): void;
protected abstract todayBtnClick(event: MouseEvent): void;
protected abstract updatePlatformInputAttrs(): void;
// eslint-disable-next-line @typescript-eslint/member-ordering
public abstract updateInitialDate(start: string, end?: string): void;
}
}