outsystems-ui
Version:
OutSystems UI Framework
344 lines (306 loc) • 11.5 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Patterns.SectionIndexItem {
/**
* Class that implements the SectionIndexItem pattern.
*
* @export
* @class SectionIndexItem
* @extends {AbstractPattern<SectionIndexItemConfig>}
* @implements {ISectionIndexItem}
*/
export class SectionIndexItem
extends AbstractChild<SectionIndexItemConfig, SectionIndex.ISectionIndex>
implements ISectionIndexItem
{
// Store the on click event
private _eventOnClick: GlobalCallbacks.Generic;
// Event OnScreenScroll
private _eventOnScreenScroll: GlobalCallbacks.Generic;
//Stores the keyboard callback function
private _eventOnkeyBoardPress: GlobalCallbacks.Generic;
// Store the state
private _isActive = false;
// Store the mainContent reference - The one that will have the scroll
private _mainScrollContainerElement: HTMLElement;
// Store TargetElement HTML object
private _targetElement: HTMLElement = undefined;
constructor(uniqueId: string, configs: JSON) {
super(uniqueId, new SectionIndexItemConfig(configs));
}
// Method to set the A11y keyboard navigation
private _onKeyboardPressed(event: KeyboardEvent): void {
// If Enter or Space Keys trigger as a click event!
if (event.key === GlobalEnum.Keycodes.Enter || event.key === GlobalEnum.Keycodes.Space) {
event.preventDefault();
event.stopPropagation();
// Triggered as it was clicked!
this._onSelected(event);
}
}
// Method to check the scroll to know if the target element is visible and sets the item as active
private _onScreenScroll(): void {
this._setTargetElement();
// Get the vertical scroll position value
const scrollYPosition = Behaviors.ScrollVerticalPosition(this._mainScrollContainerElement);
// Threshold value to set element as Active
const thresholdVal = 40;
// Store the offSetValue to be checked
const elementOffsetTopVal = this._targetElement.offsetTop - scrollYPosition.value;
/* Logic behind position validation:
- If click to nanvigate into element the calc
this.TargetElement.offsetTop - scrollYPosition.value
will return 0(zero). That said we're setting a threshold to set as IsActive when
this calc will between -thresholdVal and thresholdVal from that offset!
- AND;
- If scroll has hit the bottom and element doesn't have height enought to be placed at that
offset it should set it as IsActive since it will be the last item in screen inside the scrollContainer.
*/
if (
(this.isFirstChild && scrollYPosition.percentageInView === 0) ||
(elementOffsetTopVal >= -thresholdVal && elementOffsetTopVal <= thresholdVal) ||
(this.isLastChild && scrollYPosition.percentageInView === 100)
) {
this.notifyParent(SectionIndex.Enum.ChildNotifyActionType.Active);
}
}
// Method to handle the click event
private _onSelected(event: Event): void {
event.preventDefault();
event.stopPropagation();
this._setTargetElement();
// Notify parent about this Item Click
this.notifyParent(SectionIndex.Enum.ChildNotifyActionType.Click);
}
// Method to remove Pattern events
private _removeEvents(): void {
this.selfElement.removeEventListener(GlobalEnum.HTMLEvent.Click, this._eventOnClick);
this.selfElement.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnkeyBoardPress);
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
Event.DOMEvents.Listeners.Type.ScreenOnScroll,
this._eventOnScreenScroll
);
}
// Method to add a data attribute to be used in automated tests and to have info on DOM of which element the index is pointing
private _setLinkAttribute(): void {
Helper.Dom.Attribute.Set(this.selfElement, Enum.DataTypes.dataItem, this.configs.ScrollToWidgetId);
}
// Method to add scroll margin styles for the item target
private _setTargetAttributes(): void {
// Set class to target, to avoid inline property styles
Helper.Dom.Styles.AddClass(this._targetElement, Enum.CssClass.ItemTarget);
// Add CSS Variable with the offset value for the scroll target, to be used on the CSS, based on parent topPosition
this.setTargetScrollMargin(
Helper.Dom.Styles.GetCustomProperty(
this.parentObject.selfElement,
SectionIndex.Enum.CssVariable.TopPosition
)
);
}
// Method to set the TargetElement
private _setTargetElement(): void {
// Check if the element has been already defined!
if (this._targetElement === undefined) {
try {
// Can't be used the Helper.Dom.GetElementById since we don't want a through error if the element does not exist!
this._targetElement = document.getElementById(this.configs.ScrollToWidgetId);
this._setTargetAttributes();
} catch (e) {
// Was not able to get Target element!
throw new Error(
`${ErrorCodes.SectionIndexItem.FailToSetTargetElement}: Target Element with the Id '${this.configs.ScrollToWidgetId}' does not exist!`
);
}
}
}
// Method to set the event listeners
private _setUpEvents(): void {
this.selfElement.addEventListener(GlobalEnum.HTMLEvent.Click, this._eventOnClick);
this.selfElement.addEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnkeyBoardPress);
// Add the BodyScroll callback that will be used to update the balloon coodinates
Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
Event.DOMEvents.Listeners.Type.ScreenOnScroll,
this._eventOnScreenScroll
);
}
// Method to remove scroll margin styles on the item target
private _unsetTargetAttributes(): void {
// Set class to target, to avoid inline property styles
Helper.Dom.Styles.RemoveClass(this._targetElement, Enum.CssClass.ItemTarget);
// Add CSS Variable with the offset value for the scroll target, to be used on the CSS
Helper.Dom.Styles.RemoveStyleAttribute(this._targetElement, Enum.CssVariable.ScrollMargin);
}
/**
* Method to add the A11Y attributes values
*
* @protected
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
protected setA11YProperties(): void {
// Set RoleButton attribute
Helper.A11Y.RoleButton(this.selfElement);
// Set TabIndex
Helper.A11Y.TabIndexTrue(this.selfElement);
}
/**
* Method to set the callbacks and event listeners
*
* @protected
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
protected setCallbacks(): void {
this._eventOnClick = this._onSelected.bind(this);
this._eventOnkeyBoardPress = this._onKeyboardPressed.bind(this);
this._eventOnScreenScroll = this._onScreenScroll.bind(this);
}
/**
* Method to set the HTMLElements used
*
* @protected
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
protected setHtmlElements(): void {
// Check if overlay is enabled => If StatusBar is enabled and if is iOS device
/* With the introduction of the ios-bounce the overflow container change from the .active-screen into .content */
if (
Helper.Dom.Attribute.Has(document.body, GlobalEnum.HTMLAttributes.StatusBar) &&
Helper.DeviceInfo.GetOperatingSystem() === GlobalEnum.MobileOS.IOS
) {
this._mainScrollContainerElement = Helper.Dom.ClassSelector(
document,
GlobalEnum.CssClassElements.Content
);
} else {
this._mainScrollContainerElement = Helper.Dom.ClassSelector(
document,
GlobalEnum.CssClassElements.ActiveScreen
);
}
}
/**
* Removes the listeners that were added in the code and unsets the callbacks.
*
* @protected
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
protected unsetCallbacks(): void {
this._removeEvents();
this._eventOnClick = undefined;
this._eventOnkeyBoardPress = undefined;
this._eventOnScreenScroll = undefined;
}
/**
* Method to unset the html elements used
*
* @protected
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
protected unsetHtmlElements(): void {
this._mainScrollContainerElement = undefined;
this._targetElement = undefined;
}
/**
* Method to build the SectionIndexItem.
*
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
public build(): void {
super.build();
this.setParentInfo(
Constants.Dot + SectionIndex.Enum.CssClass.Pattern,
OutSystems.OSUI.Patterns.SectionIndexAPI.GetSectionIndexById
);
// Notify parent about a new instance of this child has been created!
this.notifyParent(SectionIndex.Enum.ChildNotifyActionType.Add);
this.setCallbacks();
this.setHtmlElements();
this._setUpEvents();
this.setA11YProperties();
this._setLinkAttribute();
this.finishBuild();
}
/**
* Applies the changes of state/value of the configurations.
*
* @param {string} propertyName
* @param {unknown} propertyValue
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
public changeProperty(propertyName: string, propertyValue: unknown): void {
super.changeProperty(propertyName, propertyValue);
if (this.isBuilt) {
switch (propertyName) {
case Enum.Properties.ScrollToWidgetId:
console.warn(
`${GlobalEnum.PatternName.SectionIndex} (${this.widgetId}): change to ${Enum.Properties.ScrollToWidgetId} on property ${Enum.Properties.ScrollToWidgetId} is not editable at OnParametersChange.`
);
break;
}
}
}
/**
* Disposes the current pattern.
*
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
public dispose(): void {
this.unsetCallbacks();
this._unsetTargetAttributes();
// Notify parent about this instance will be destroyed
this.notifyParent(SectionIndex.Enum.ChildNotifyActionType.Removed);
//Destroying the base of pattern
super.dispose();
}
/**
* Adds active class from pattern.
*
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
public setIsActive(): void {
if (this._isActive === false) {
this._isActive = true;
Helper.Dom.Styles.AddClass(this.selfElement, Patterns.SectionIndex.Enum.CssClass.IsActiveItem);
}
}
/**
* Set the scroll margin, that acts as scrollIntoView offset
*
* @param {(number | string)} margin
* @memberof SectionIndexItem
*/
public setTargetScrollMargin(margin: number | string): void {
// Add CSS Variable with the offset value for the scroll target, to be used on the CSS
Helper.Dom.Styles.SetStyleAttribute(this._targetElement, Enum.CssVariable.ScrollMargin, margin);
}
/**
* Removes active class from pattern.
*
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
public unsetIsActive(): void {
if (this._isActive) {
this._isActive = false;
Helper.Dom.Styles.RemoveClass(this.selfElement, Patterns.SectionIndex.Enum.CssClass.IsActiveItem);
}
}
/**
* Readable property to get the active state of the element
*
* @readonly
* @type {boolean}
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
public get IsSelected(): boolean {
return this._isActive;
}
/**
* Readable property to get targetElement object
*
* @readonly
* @type {HTMLElement}
* @memberof OSFramework.Patterns.SectionIndexItem.SectionIndexItem
*/
public get TargetElement(): HTMLElement {
return this._targetElement;
}
}
}