outsystems-ui
Version:
OutSystems UI Framework
561 lines (486 loc) • 16.7 kB
text/typescript
/// <reference path="../AbstractProgress.ts" />
// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Patterns.Progress.Circle {
export class Circle extends Progress.AbstractProgress<ProgressCircleConfig> {
private _blockParent: HTMLElement;
// Circunference circle value
private _circleCircumference: number;
private _circleSize = 0;
// SVG defs gradient element
private _gradientElem: SVGDefsElement;
// Flag to check if the resize observer should be added
private _needsResizeObserver = true;
// ResizeOberver
private _resizeObserver: ResizeObserver;
// Store values to be assigned to the circle
private _strokeDasharray: number;
private _strokeDashoffset: number;
// Set as public, so that the coordinates can be customized using extesnibility. LinearHorizontal by default
public linearGradientCoords = {
x1: 1,
x2: 1,
y1: 0,
y2: 1,
};
// Set as public, so that the coordinates can be customized using extesnibility
public radialGradientCoords = {
fr: Enum.DefaultValues.RadialFr,
r: Enum.DefaultValues.RadialRadius,
};
constructor(uniqueId: string, configs: JSON) {
super(uniqueId, new ProgressCircleConfig(configs));
}
// Set the resizeObserver
private _addResizeOberser(): void {
this._resizeObserver = new ResizeObserver((entries) => {
// We wrap it in requestAnimationFrame to avoid this error - ResizeObserver loop limit exceeded
requestAnimationFrame(() => {
if (!Array.isArray(entries) || !entries.length) {
return;
}
if (this.progressElem) {
if (
Helper.Dom.Styles.ContainsClass(
this.progressElem,
ProgressEnum.CssClass.AddInitialAnimation
)
) {
this.progressElem.addEventListener(
GlobalEnum.HTMLEvent.TransitionEnd,
this._updateCircleProps.bind(this)
);
} else {
this._updateCircleProps();
}
}
});
});
this._resizeObserver.observe(this._blockParent);
}
// Check if the resizeOberver does not exist yet!
private _checkResizeObserver(): void {
if (!this._resizeObserver && this._needsResizeObserver) {
// Create the Oberver
this._addResizeOberser();
} else if (this._resizeObserver && this._needsResizeObserver === false) {
this._removeResizeOberver();
}
}
// Convert progress value into offset to assign to our circle
private _progressToOffset(): void {
// Check which size will be applied on ProgressCircle
if (
this.configs.ProgressCircleSize !== OSFramework.OSUI.Constants.EmptyString &&
this.configs.ProgressCircleSize !== (Enum.DefaultValues.Size as string) &&
parseInt(this.configs.ProgressCircleSize) !== 0
) {
// Set the Progress Circle Size variable for calculations
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.ProgressCircleSize,
this.configs.ProgressCircleSize
);
// Set the size of Progress Circle with the value defined through parameter
this._circleSize = this.selfElement.clientWidth;
this._needsResizeObserver = false;
} else {
// Get the pattern parent size, based on lower size by priority (to avoid overflowing the parent, as this will always be a square)
if (this._blockParent.clientWidth > this._blockParent.clientHeight) {
this._circleSize = this._blockParent.clientHeight;
} else {
this._circleSize = this._blockParent.clientWidth;
}
// Set the Progress Circle Size variable for calculations
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.ProgressCircleSize,
this._circleSize + GlobalEnum.Units.Pixel
);
this._needsResizeObserver = true;
}
// Set the css variable to
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.CircleSize,
this._circleSize + GlobalEnum.Units.Pixel
);
const _radius = Math.floor(this._circleSize / 2 - this.configs.Thickness / 2);
this._circleCircumference = _radius * 2 * Math.PI;
// set the base values
this._strokeDashoffset = this._strokeDasharray = this._circleCircumference;
// Set the css variables that will be used at ProgressCircle
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.CircleRadius,
_radius + GlobalEnum.Units.Pixel
);
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.StrokeDasharray,
this._strokeDasharray
);
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.StrokeDashoffset,
this._strokeDashoffset
);
// Ensure that this will run only at the Initialization
if (!this.isBuilt) {
// Make async to ensure that all css variables are assigned
// Update according initial style
Helper.AsyncInvocation(this.addInitialAnimation.bind(this));
} else {
this._checkResizeObserver();
}
}
// Remove the resizeObserver
private _removeResizeOberver(): void {
this._resizeObserver.disconnect();
this._resizeObserver = undefined;
}
// Set the default inline css variables
private _setCssVariables(): void {
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.Thickness,
this.configs.Thickness + GlobalEnum.Units.Pixel
);
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.ProgressColor,
Helper.Dom.Styles.GetColorValueFromColorType(this.configs.ProgressColor)
);
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.Shape,
this.configs.Shape === GlobalEnum.ShapeTypes.Sharp
? ProgressEnum.ShapeTypes.Sharp
: ProgressEnum.ShapeTypes.Round
);
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.TrailColor,
Helper.Dom.Styles.GetColorValueFromColorType(this.configs.TrailColor)
);
}
// Method to compose coordinates string, to avoid code repetition
private _setGradientCoords(gradientType: string): string {
if (gradientType === Enum.GradientName.Radial) {
return `fr="${this.radialGradientCoords.fr}" r="${this.radialGradientCoords.r}"`;
} else {
return `x1="${this.linearGradientCoords.x1}" y1="${this.linearGradientCoords.y1}" x2="${this.linearGradientCoords.x2}" y2="${this.linearGradientCoords.y2}"`;
}
}
// Trigger all the meethods responsible to proper update the Circle
private _updateCircleProps(): void {
this._progressToOffset();
this._updateProgressValue();
}
// Update the valuenow accessibility property
private _updateProgressValue(): void {
// Update the progress value and the valuenow accessibility property
this.updatedProgressValue();
// Update the offset value
this._strokeDashoffset =
this._circleCircumference - (this.configs.Progress / 100) * this._circleCircumference;
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.StrokeDashoffset,
this._strokeDashoffset
);
}
/**
* Add the initial animation to the pattern if it's applicable
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected addInitialAnimation(): void {
// Check if the animation at init should be added
if (this.configs.AnimateInitialProgress) {
this.animateInitial();
this._checkResizeObserver();
// Set the progressValue into the element
this._updateProgressValue();
} else {
// Since the initial animation was not added add the ResizeObserver
this._addResizeOberser();
}
}
/**
* This method has no implementation on this pattern context!
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected setA11YProperties(): void {
if (this.contentElem.innerHTML) Helper.A11Y.AriaLabelledBy(this.selfElement, this.contentElem.id);
else Helper.A11Y.AriaLabel(this.selfElement, ProgressEnum.AriaLabel.Progress);
}
/**
* Method to set the callbacks
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected setCallbacks(): void {
super.setCallbacks();
}
/**
* Method used to set the progrees value
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected setElementProgressValue(value: number): void {
this.configs.Progress = value;
// Add the animate to progress value, on value change
this.animateOnValueChange();
this._checkResizeObserver();
this._updateProgressValue();
}
/**
* Method used to set the HTML elements reference
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected setHtmlElements(): void {
this._blockParent = document.getElementById(this.widgetId).parentElement;
// Set the html reference that will be used to do all the needed calcs
this.progressElem = this.selfElement.querySelector(Constants.Dot + Enum.CssClass.Progress);
// Set Progress Bar content element id
this.contentElem = this.selfElement.querySelector(
OSUI.Constants.Dot + Progress.ProgressEnum.CssClass.ProgressCircleContent
);
// Set the <defs> element when using a svg gradient. Only after built, as the gradient is only available through Client Action
if (this.isBuilt) {
this._gradientElem = this.progressElem.parentElement.querySelector('defs');
}
}
/**
* Method to unset the callbacks
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected unsetCallbacks(): void {
super.unsetCallbacks();
}
/**
* Method to unset the HTML elements reference
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected unsetHtmlElements(): void {
this._blockParent = undefined;
this._gradientElem = undefined;
super.unsetHtmlElements();
}
/**
* Update progress value
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected updateProgressColor(): void {
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.ProgressColor,
Helper.Dom.Styles.GetColorValueFromColorType(this.configs.ProgressColor)
);
}
/**
* Update shape type
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected updateShape(): void {
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.Shape,
this.configs.Shape === GlobalEnum.ShapeTypes.Sharp
? ProgressEnum.ShapeTypes.Sharp
: ProgressEnum.ShapeTypes.Round
);
}
/**
* Update shape thickness
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected updateThickness(): void {
this._updateCircleProps();
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.Thickness,
this.configs.Thickness + GlobalEnum.Units.Pixel
);
}
/**
* Update shape color
*
* @protected
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
protected updateTrailColor(): void {
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
ProgressEnum.InlineStyleProp.TrailColor,
Helper.Dom.Styles.GetColorValueFromColorType(this.configs.TrailColor)
);
}
/**
* Method to build the ProgressCircle
*
* @memberof Circle
*/
public build(): void {
super.build();
this.setHtmlElements();
this.setA11YProperties();
this._setCssVariables();
this._progressToOffset();
this.setCallbacks();
this.finishBuild();
}
/**
* Update property value from a given property name at OnParametersChange
*
* @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 OSFramework.Patterns.Progress.Circle.Circle
*/
public changeProperty(propertyName: string, propertyValue: unknown): void {
// Update the default values
super.changeProperty(propertyName, propertyValue);
switch (propertyName) {
case ProgressEnum.Properties.Thickness:
this.updateThickness();
break;
case ProgressEnum.Properties.Progress:
this.setProgressValue(propertyValue as number);
break;
case ProgressEnum.Properties.ProgressColor:
this.updateProgressColor();
break;
case ProgressEnum.Properties.ProgressCircleSize:
this._resizeObserver?.unobserve(this._blockParent);
this._updateCircleProps();
this._resizeObserver?.observe(this._blockParent);
break;
case ProgressEnum.Properties.Shape:
this.updateShape();
break;
case ProgressEnum.Properties.TrailColor:
this.updateTrailColor();
break;
}
}
/**
* Method to create the SVG Gradient
*
* @param {string} gradientId
* @param {string} gradientName
* @param {unknown} gradientCoords
* @param {string} gradientHtml
* @param {GradientColor} colors
* @memberof Circle
*/
public createSVGGradient(
gradientId: string,
gradientName: string,
gradientCoords: unknown,
gradientLenght: number,
colors: GradientColor
): void {
// Start by removing the current gradient, in case there's already one created
this._gradientElem?.remove();
// Store gradient HTML that will be dynamically created
let _gradient = Constants.EmptyString;
// Create gradient stop elements dynamically, depending on number of color and percentages used
for (let i = 0; i < gradientLenght; i++) {
_gradient += `<stop offset="${
// If the Percentage config is used, use that value, otherwise auto calculate based on the color array length
colors[i].Percentage !== -1 ? colors[i].Percentage : Math.floor((i * 100) / gradientLenght)
}%" stop-color="${colors[i].Color}"/>`;
}
// Compose the final SVG gradient, with the expected html structure
const gradientSVG = `
<defs>
<${gradientName} id="${gradientId}" ${gradientCoords}">
${_gradient}
</${gradientName}>
</defs>`;
// Add the gradient inside the SVG element
this.progressElem.parentElement.innerHTML += gradientSVG;
// As we manipulate the DOM, we must update the elements references
this.setHtmlElements();
// Add the url as a css-variable, so that it can be referenced on the CSS
Helper.Dom.Styles.SetStyleAttribute(
this.selfElement,
Enum.InlineStyleProp.GradientURL,
'url(#' + gradientId + ')'
);
}
/**
* Destroy the ProgressCircle
*
* @memberof OSFramework.Patterns.Progress.Circle.Circle
*/
public dispose(): void {
super.dispose();
this.unsetHtmlElements();
this.unsetCallbacks();
// Check if the resizeOberver already exists
if (this._resizeObserver) {
this._removeResizeOberver();
}
}
/**
* Method to apply a SVG Gradient
*
* @param {string} gradientType
* @param {GradientColor} colors
* @memberof Circle
*/
public progressApplyGradient(gradientType: string, colors: GradientColor): void {
// Call super to clean and validate color string
super.progressApplyGradient(gradientType, colors);
// Store the expected gradient name to be used as html tag
let _gradientName = Enum.GradientName.Linear;
// Store the dynamic svg gradient id, to make sure it's unique for each progressCircle
const _gradientId = Enum.DefaultValues.GradientId + this.uniqueId;
// Set coordinates, according to gradient type
switch (gradientType) {
case ProgressEnum.Gradient.LinearHorizontal:
this.linearGradientCoords.x1 = 1;
this.linearGradientCoords.x2 = 1;
this.linearGradientCoords.y1 = 0;
this.linearGradientCoords.y2 = 1;
break;
case ProgressEnum.Gradient.LinearDiagonally:
this.linearGradientCoords.x1 = 1;
this.linearGradientCoords.x2 = 0;
this.linearGradientCoords.y1 = 0;
this.linearGradientCoords.y2 = 1;
break;
case ProgressEnum.Gradient.LinearVertical:
this.linearGradientCoords.x1 = 1;
this.linearGradientCoords.x2 = 0;
this.linearGradientCoords.y1 = 1;
this.linearGradientCoords.y2 = 1;
break;
case ProgressEnum.Gradient.Radial:
_gradientName = Enum.GradientName.Radial;
break;
}
// Update html string with the expected coordinates attributes
const _gradientCoords = this._setGradientCoords(_gradientName);
// Craete SVG Gradient
this.createSVGGradient(_gradientId, _gradientName, _gradientCoords, this.gradientLength, colors);
}
}
}