import { html, nothing, PropertyValueMap, unsafeCSS } from "lit"; import { property } from "lit/decorators.js"; import eleStyle from "./f-countdown.scss?inline"; import { FRoot } from "../../mixins/components/f-root/f-root"; import { FDiv } from "../f-div/f-div"; import { flowElement } from "../../utils"; import getCustomFillColor from "../../utils/get-custom-fill-color"; import { ref, createRef, Ref } from "lit/directives/ref.js"; import { FText } from "../f-text/f-text"; import { classMap } from "lit-html/directives/class-map.js"; import { validateHTMLColor, validateHTMLColorName } from "validate-color"; import { keyed } from "lit/directives/keyed.js"; const states = ["primary", "danger", "warning", "success", "default"] as const; const sizes = ["large", "medium", "small", "x-small"] as const; const categories = ["fill", "outline"] as const; const placements = ["left", "right", "bottom", "top", "none"] as const; export type FCountdownStateProp = (typeof states)[number]; export type FCountdownCategoryProp = (typeof categories)[number]; export type FCountdownSizesProp = (typeof sizes)[number]; export type FCountdownLabelProp = (typeof placements)[number]; export type FCountdownDuration = number | string; @flowElement("f-countdown") export class FCountdown extends FRoot { /** * css loaded from scss file */ static styles = [unsafeCSS(eleStyle), ...FDiv.styles, ...FText.styles]; /** * @attribute toggle accordion */ @property({ reflect: true, type: String }) category?: FCountdownCategoryProp = "fill"; /** * @attribute toggle accordion */ @property({ reflect: true, type: String }) duration?: FCountdownDuration = 5; /** * @attribute Each variant has various sizes. By default medium is the default size. */ @property({ type: String, reflect: true }) size?: FCountdownSizesProp = "medium"; /** * @attribute The states on tags are to indicate various degrees of emphasis of the action. */ @property({ reflect: true, type: String }) state?: FCountdownStateProp = "default"; /** * @attribute The states on tags are to indicate various degrees of emphasis of the action. */ @property({ reflect: true, type: String, attribute: "label-placement" }) labelPlacement?: FCountdownLabelProp = "left"; fill = ""; remaining = 0; timerText = this.convertSecondsToMinutesAndSeconds(this.secondsDuration); timerRef: Ref = createRef(); currentProgress = 0; interval = 0; countdownId = 0; /** * apply inline styles to shadow-dom for custom fill. */ get applyStyles() { if (this.fill) { return `background: ${this.fill};`; } return ""; } get applyPieStyles() { return `background: var(--color-input-default); animation-duration: ${this.secondsDuration}s`; } get maskStyles() { return `animation-duration: ${this.secondsDuration}s`; } get outlineTimerStyle() { if (this.fill) { return `--duration: ${this.secondsDuration}; --state-color:${this.fill}`; } return `--duration: ${this.secondsDuration}; --state-color:${ this.state !== "default" ? `var(--color-${this.state}-default)` : `var(--color-primary-default)` };`; } get secondsDuration() { if (this.duration) { const timer = String(this.duration).split(":"); if (timer.length === 1) { return Number(timer[0]); } else { return Number(timer[0]) * 60 + Number(timer[1]); } } else return 5; } get labelSize() { switch (this.size) { case "large": return "medium"; case "medium": return "small"; case "small": return "x-small"; case "x-small": return "x-small"; default: return "x-small"; } } get countdownWidth() { return this.labelPlacement === "left" ? "55px" : ""; } get countdownAlignment() { return this.labelPlacement === "left" ? "middle-right" : "middle-center"; } init() { this.interval = setInterval(() => { this.remaining -= 1; this.timerText = this.convertSecondsToMinutesAndSeconds(this.remaining); if (this.remaining === 0) { this.remaining = this.secondsDuration; this.timerText = this.convertSecondsToMinutesAndSeconds(this.secondsDuration); } this.updateTimerText(); }, 1000); } convertSecondsToMinutesAndSeconds(seconds: string | number) { const minutes = Math.floor(Number(seconds) / 60); const remainingSeconds = Number(seconds) % 60; if (minutes === 0) { return remainingSeconds + "s"; } return minutes + "m " + remainingSeconds + "s"; } updateTimerText() { if (this.timerRef.value) { this.timerRef.value.textContent = this.timerText; } } disconnectedCallback() { clearInterval(this.interval); super.disconnectedCallback(); } validateDurationProperties() { if (!this.duration) { throw new Error("f-countdown: Duration is required"); } const time = this.duration; const regexNum = /^\d+$/; if (typeof time === "string") { if (time.includes(":")) { const [min, sec] = time.split(":"); if (!regexNum.test(min) || !regexNum.test(sec)) { throw new Error("f-countdown: Please enter numeric values for minutes and seconds"); } if (Number(min) >= 60 || Number(sec) > 60) { throw new Error( "f-countdown: Please enter valid values for minutes (less than 60) and seconds (less than 60)" ); } } else if (!regexNum.test(time)) { throw new Error("f-countdown: Please enter a numeric value for time"); } else if (Number(time) >= 3600) { throw new Error("f-countdown: Please enter a value for time less than 3600 seconds"); } } else if (time >= 3600) { throw new Error("f-countdown: Please enter a value for time less than 3600 seconds"); } } validateStateProperties() { if ( this.state?.includes("custom") && this.fill && !validateHTMLColor(this.fill) && !validateHTMLColorName(this.fill) ) { throw new Error("f-countdown : enter correct color-name or hex-color-code"); } } render() { this.countdownId += 1; this.fill = getCustomFillColor(this.state ?? ""); this.validateDurationProperties(); this.validateStateProperties(); //top and left label placement const labelTopAndLeft = this.labelPlacement === "left" || this.labelPlacement === "top" ? html` ` : nothing; //bottom and right label placemet const labelBottomAndRight = this.labelPlacement === "right" || this.labelPlacement === "bottom" ? html` ` : nothing; //fill category timer structure const fillTimer = keyed( this.countdownId, html`
` ); const outlineTimer = keyed( this.countdownId, html`
` ); const classes: Record = { "f-countdown-wrapper": this.category === "fill", "f-countdown-outline-wrapper": this.category === "outline" }; this.classList.forEach(cl => { classes[cl] = true; }); return html` ${labelTopAndLeft} ${this.category === "fill" ? fillTimer : outlineTimer} ${labelBottomAndRight} `; } protected updated(changedProperties: PropertyValueMap | Map) { super.updated(changedProperties); clearInterval(this.interval); this.remaining = this.secondsDuration; this.init(); } } /** * Required for typescript */ declare global { interface HTMLElementTagNameMap { "f-countdown": FCountdown; } }