import { css, html, svg } from 'lit' import { property, state } from 'lit/decorators.js' import type { DirectiveResult } from 'lit/directive' import type { ClassMapDirective } from 'lit/directives/class-map.js' import { classMap } from 'lit/directives/class-map.js' import type { Ref } from 'lit/directives/ref.js' import { createRef, ref } from 'lit/directives/ref.js' import { when } from 'lit/directives/when.js' import { customElementIfNotExist } from '../../utils/customElementIfNotExist' import { UsBaseElement } from '../base/UsBaseElement' import UsLoader_css from './loader.pcss' import vars_css from './vars.pcss' import type { ILoaderCircle, ILoaderProps, TLoaderTypeProp } from './model/ILoaderProps' declare global { interface HTMLElementTagNameMap { 'us-loader': UsLoader } } @customElementIfNotExist('us-loader') export class UsLoader extends UsBaseElement implements ILoaderProps { static styles = [ UsBaseElement.styles, css([vars_css] as TemplateStringsArray | any), css([UsLoader_css] as TemplateStringsArray | any), ] @property() type: TLoaderTypeProp = 'infinity' @property({ type: Number }) value = 0 @property({ type: Boolean }) showPercentages = false @property() label = '' @property() doneLabel = '' @property() loaderBackgroundColor = 'transparent' @property({ type: Number }) circleLoaderDaimeter = 64 @property({ type: Number }) circleLoaderStroke = 8 @property({ type: Number }) workTime: number | null = null @property({ type: String, attribute: 'data-testid', reflect: true }) dataTestId = 'loader' @state() isDone = false @state() displayedLabel: any @state() linearProgressBarLabel = '' @state() displayedLabelText = '' @state() loaderStrokeWidthBig = 8 @state() loaderStrokeWidthSmall = 4 @state() lengthCompensation = 5 @state() defaultProgressBarLength = '30%' @state() circle_1: ILoaderCircle = { circleDaimeter: this.circleLoaderDaimeter, strokeWidth: this.circleLoaderStroke, radius: 0, strokeDasharray: 0, strokeDashoffset: 0, } @state() circle_2: ILoaderCircle = { circleDaimeter: this.circleLoaderDaimeter, strokeWidth: this.circleLoaderStroke, radius: 0, strokeDasharray: 0, strokeDashoffset: 0, } checkedMark = svg` ` circleRef: Ref = createRef() willUpdate(changedProperties: Map) { if (changedProperties.has('type') || changedProperties.has('circleLoaderDaimeter') || changedProperties.has('circleLoaderStroke')) this.updatedType() if (changedProperties.has('workTime')) this.updatedWorkTime() if (changedProperties.has('value')) this.updatedValue() this.updateLabel() this.updateLabelText() this.updateLinearLoaderLabel() } firstUpdated() { this.updatedWorkTime() } render() { switch (this.type) { case 'infinity-scroll': return this.getInfinityScrollLoader() case 'linear-progress-bar-big': case 'linear-progress-bar-small': return this.getLinearLoader() default: return this.getPrimaryLoader() } } getLoader() { const loaderSize = this.circle_1.circleDaimeter const cxy = loaderSize / 2 return html` ${svg` `} ` } getLinearLoader() { const resultProgressBarLength = this.value ? `calc(${this.value}% + ${this.lengthCompensation}px)` : this.defaultProgressBarLength return html`
${when(this.label || this.value, () => html`

${this.linearProgressBarLabel}

`, )}
` } getInfinityScrollLoader() { return html`
${this.getLoader()} ${when(!!this.label, () => html` ${this.label} `)}
` } getPrimaryLoader() { return html`
${this.getLoader()} ${this.displayedLabel}
${when(!!this.displayedLabelText, () => html` ${this.displayedLabelText} `)}
` } completeFilling(element: SVGElement, radius: number, value: number) { if (!this.workTime) return const dasharray = this.calcStrokeDasharray(radius) const dashoffset = this.calcStrokeDashoffset(value, dasharray) element.style.setProperty('--stroke-min', `${dasharray}px`) element.style.setProperty('--stroke-max', `${dashoffset}px`) element.style.animation = `filling ${this.workTime / 1000}s linear` const setDoneState = () => { this.value = 100 } const options = { once: true } element.addEventListener('animationend', setDoneState, options) } updatedWorkTime() { const circle = this.circleRef.value if (!circle || !this.workTime) return const circleOneRadius = this.circle_1.radius return this.completeFilling(circle, circleOneRadius, this.value) } updatedType() { if (this.type === 'infinity' || this.type === 'infinity-scroll') { this.value = 25 this.circle_1.strokeDashoffset = this.calcStrokeDashoffset(this.value, this.circle_1.strokeDasharray) } if (this.type === 'infinity-scroll') { this.circle_1 = this.circle_2 = this.recalcCircleParams(24, this.loaderStrokeWidthSmall) } else { this.circle_1 = this.recalcCircleParams(this.circleLoaderDaimeter, this.circleLoaderStroke) this.circle_2 = this.recalcCircleParams(this.circleLoaderDaimeter, this.circleLoaderStroke) } } updatedValue() { this.isDone = this.value >= 100 this.value = this.value > 100 ? 100 : this.value this.circle_1.strokeDashoffset = this.calcStrokeDashoffset(this.value, this.circle_1.strokeDasharray) if (this.isDone) { setTimeout(() => { this.dispatchEvent(new CustomEvent('done')) }, 500) } } updateLabel() { if (this.showPercentages && !this.isDone) this.displayedLabel = `${this.value}%` else if (this.isDone) this.displayedLabel = this.checkedMark } updateLabelText() { this.displayedLabelText = this.isDone ? this.doneLabel : this.label } updateLinearLoaderLabel() { if (this.value) this.linearProgressBarLabel = `${this.value}%${this.value < 100 ? '...' : ''}` else this.linearProgressBarLabel = this.label } recalcCircleParams(circleDaimeter: number, strokeWidth: number): ILoaderCircle { const radius = this.calcRadius(circleDaimeter, strokeWidth) const strokeDasharray = this.calcStrokeDasharray(radius) const strokeDashoffset = this.calcStrokeDashoffset(this.value, strokeDasharray) return { circleDaimeter, strokeWidth, radius, strokeDasharray, strokeDashoffset } } calcRadius(circleDaimeter: number, strokeWidth: number): number { return circleDaimeter / 2 - strokeWidth / 2 } calcStrokeDasharray(radius: number): number { const strokeDasharray = Math.PI * radius * 2 return Math.round(strokeDasharray * 100) / 100 } calcStrokeDashoffset(value: number, strokeDasharray: number): number { const currentValue = (value !== undefined && value !== null) ? value : 100 const strokeDashoffset = ((100 - currentValue) / 100) * strokeDasharray return Math.round(strokeDashoffset * 100) / 100 } getLoaderContainerClasses(): DirectiveResult { return classMap({ 'loader-container': true, 'rotation-animation': this.type === 'infinity' || this.type === 'infinity-scroll', }) } getLinearLoaderClasses(): DirectiveResult { return classMap({ 'loader__linear-loader-container': true, 'loader__linear-loader-container_big-progress-bar': this.type === 'linear-progress-bar-big', 'loader__linear-loader-container_small-progress-bar': this.type === 'linear-progress-bar-small', }) } getLoaderTickerAnimationClasses(): DirectiveResult { return classMap({ 'progress-bar-fill': true, 'progress-bar-fill-animation': (this.label && !this.value) || (!this.label && !this.value), }) } }