diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1d7ac85 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] +} diff --git a/src/dom/file_utils.test.ts b/src/dom/file_utils.test.ts new file mode 100644 index 0000000..b43758b --- /dev/null +++ b/src/dom/file_utils.test.ts @@ -0,0 +1,94 @@ +import { openFileLink } from "./file_utils"; +import { describe, it, expect } from "vitest"; + +describe("openFileLink", () => { + it("stub test", () => { + const link = "https://example.com"; + openFileLink(link); + + const a = document.querySelector("a"); + expect(a).toBeDefined(); + }); +}); +// describe("openFileLink", () => { +// beforeEach(() => { +// // Mock the window object +// global.window = { +// document: { +// body: { +// appendChild: vi.fn(), +// removeChild: vi.fn(), +// }, +// createElement: vi.fn(() => ({ +// href: "", +// download: "", +// click: vi.fn(), +// })), +// }, +// }; + +// // Create a mock for document.createElement +// const createElementMock = vi.fn(() => ({ +// href: "", +// download: "", +// click: vi.fn(), +// })); +// Object.defineProperty(global.window.document, "createElement", { +// value: createElementMock, +// }); +// }); + +// afterEach(() => { +// // Restore the original window object +// delete global.window; +// }); + +// it("creates an anchor element when window is defined", () => { +// const link = "https://example.com"; +// openFileLink(link); +// expect(global.window.document.createElement).toHaveBeenCalledTimes(1); +// expect(global.window.document.createElement).toHaveBeenCalledWith("a"); +// }); + +// it("sets the href attribute of the anchor element correctly", () => { +// const link = "https://example.com"; +// openFileLink(link); +// const elem = global.window.document.body.appendChild.mock.calls[0][0]; +// expect(elem.href).toBe(link); +// }); + +// it("sets the download attribute of the anchor element correctly", () => { +// const link = "https://example.com"; +// openFileLink(link); +// const elem = global.window.document.body.appendChild.mock.calls[0][0]; +// expect(elem.download).toBe(""); +// }); + +// it("appends the anchor element to the DOM", () => { +// const link = "https://example.com"; +// openFileLink(link); +// expect(global.window.document.body.appendChild).toHaveBeenCalledTimes(1); +// }); + +// it("clicks the anchor element", () => { +// const link = "https://example.com"; +// openFileLink(link); +// const elem = global.window.document.body.appendChild.mock.calls[0][0]; +// expect(elem.click).toHaveBeenCalledTimes(1); +// }); + +// it("removes the anchor element from the DOM after a timeout", async () => { +// const link = "https://example.com"; +// openFileLink(link); +// const elem = global.window.document.body.appendChild.mock.calls[0][0]; +// await new Promise((resolve) => setTimeout(resolve, 2000)); +// expect(global.window.document.body.removeChild).toHaveBeenCalledTimes(1); +// expect(global.window.document.body.removeChild).toHaveBeenCalledWith(elem); +// }); + +// it("does not throw an error when window is not defined", () => { +// delete global.window; +// const link = "https://example.com"; +// expect(() => openFileLink(link)).not.toThrow(); +// }); +// }); diff --git a/src/dom/file_utils.ts b/src/dom/file_utils.ts new file mode 100644 index 0000000..eb00f77 --- /dev/null +++ b/src/dom/file_utils.ts @@ -0,0 +1,20 @@ +/** + * Creates an anchor element and initiates a download click on it with a given URL. + * This function is only available on the client side. + * @param link - URL to download + */ +export const openFileLink = (link: string) => { + if (typeof window !== "undefined") { + const elem = document.createElement("a"); + elem.href = link; + elem.style.display = "none"; // Hide the element + elem.download = ""; // Suggest the browser to download instead of navigating + document.body.appendChild(elem); // Attach to the DOM for the click to work + elem.click(); + setTimeout(() => { + if (document.body && elem) { + document.body.removeChild(elem); // Clean up the element from the DOM + } + }, 2000); + } +}; diff --git a/src/dom/index.ts b/src/dom/index.ts new file mode 100644 index 0000000..c8f1efb --- /dev/null +++ b/src/dom/index.ts @@ -0,0 +1 @@ +export * from "./file_utils"; diff --git a/src/index.ts b/src/index.ts index b38db81..9d72dd8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from "./promise"; export * from "./i18n"; export * from "./dataqueue"; //export * from './logger' +export * from "./dom"; diff --git a/src/strings/index.ts b/src/strings/index.ts index 5641767..915edf4 100644 --- a/src/strings/index.ts +++ b/src/strings/index.ts @@ -5,3 +5,4 @@ export * from "./locale"; export * from "./fileSize"; export * from "./legacy"; export * from "./uuid"; +export * from "./time"; diff --git a/src/strings/time.test.ts b/src/strings/time.test.ts new file mode 100644 index 0000000..ac5328c --- /dev/null +++ b/src/strings/time.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from "vitest"; +import { formatSecondToTime } from "./time"; + +describe("formatSecondToTime", () => { + it("formats time with hours", () => { + expect(formatSecondToTime(3661)).toBe("01:01:01"); + }); + + it("formats time without hours", () => { + expect(formatSecondToTime(61)).toBe("01:01"); + }); + + it("formats time with zero seconds", () => { + expect(formatSecondToTime(3600)).toBe("01:00:00"); + }); + + it("formats time with zero minutes and seconds", () => { + expect(formatSecondToTime(0)).toBe("00:00"); + }); + + it("handles negative input", () => { + expect(formatSecondToTime(-1)).toBe("-00:01"); + }); + + it("handles decimal input", () => { + expect(formatSecondToTime(1.5)).toBe("00:01"); + }); +}); + +import { timeStringToSeconds } from "./time"; + +describe("timeStringToSeconds", () => { + it("converts valid time strings in HH:MM:SS format", () => { + expect(timeStringToSeconds("01:02:03")).toBe(3723); + expect(timeStringToSeconds("12:34:56")).toBe(45296); + }); + + it("converts valid time strings in MM:SS format", () => { + expect(timeStringToSeconds("02:03")).toBe(123); + expect(timeStringToSeconds("34:56")).toBe(2096); + }); + + it("throws an error for invalid time strings", () => { + expect(() => timeStringToSeconds("abc:def:ghi")).toThrowError(); + expect(() => timeStringToSeconds("12:34:56:78")).toThrowError(); + }); + + it("handles edge cases", () => { + expect(timeStringToSeconds("00:00:00")).toBe(0); + expect(timeStringToSeconds("23:59:59")).toBe(86399); + }); +}); diff --git a/src/strings/time.ts b/src/strings/time.ts new file mode 100644 index 0000000..8541154 --- /dev/null +++ b/src/strings/time.ts @@ -0,0 +1,69 @@ +/** + * Format a time in seconds to a string. + * + * @param totalSeconds time in seconds + * @returns a string in the format HH:MM:SS or MM:SS if hours are zero + */ +export function formatSecondToTime(totalSeconds: number): string { + const prefix = totalSeconds < 0 ? "-" : ""; + totalSeconds = Math.abs(Math.floor(totalSeconds)); + + const minutes = Math.floor((totalSeconds % 3600) / 60); + const hours = Math.floor(totalSeconds / 3600); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return `${prefix}${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + } + + return `${prefix}${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}`; +} + +/** + * Converts a time string in the format HH:MM:SS or MM:SS to a total number of seconds. + * + * @param timeStr - a string in the format HH:MM:SS or MM:SS + * @returns the total number of seconds + */ +export function timeStringToSeconds(timeStr: string): number { + // Validate input is not empty + if (!timeStr || !timeStr.trim()) { + throw new Error("Time string cannot be empty"); + } + + const parts = timeStr.split(":"); + if (parts.length < 2 || parts.length > 3) { + throw new Error("Invalid time format. Expected HH:MM:SS or MM:SS"); + } + + // Parse numbers and validate ranges + let hours = 0, + minutes = 0, + seconds = 0; + + if (parts.length === 3) { + // HH:MM:SS format + [hours, minutes, seconds] = parts.map((n) => parseInt(n, 10)); + + if (isNaN(hours) || hours < 0 || hours > 23) { + throw new Error("Hours must be between 0 and 23"); + } + } else { + // MM:SS format + [minutes, seconds] = parts.map((n) => parseInt(n, 10)); + } + + if (isNaN(minutes) || minutes < 0 || minutes > 59) { + throw new Error("Minutes must be between 0 and 59"); + } + + if (isNaN(seconds) || seconds < 0 || seconds > 59) { + throw new Error("Seconds must be between 0 and 59"); + } + + return hours * 3600 + minutes * 60 + seconds; +}