diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..76fe15c Binary files /dev/null and b/.DS_Store differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..7919338 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "@armco/utils", + "version": "0.0.4", + "type": "module", + "scripts": { + "build": "rm -rf build && tsc && vite build", + "format": "prettier --write .", + "lint": "eslint .", + "publish:sh": "./publish.sh" + }, + "devDependencies": { + "react": "^18.3.1", + "typescript": "^5.0.2" + }, + "dependencies": { + "@armco/configs": "0.0.2" + }, + "peerDependencies": { + "react": ">16.8.1" + }, + "eslintConfig": { + "plugins": [ + "prettier" + ], + "rules": { + "prettier/prettier": "error" + } + }, + "prettier": "prettier-config-nick", + "main": "build/cjs/index.js", + "module": "build/es/index.js", + "types": "build/types/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/ReStruct-Corporate-Advantage/utils.git" + }, + "files": [ + "build" + ], + "keywords": [ + "components", + "atomic", + "building-blocks", + "foundation" + ], + "license": "ISC", + "bugs": { + "url": "https://github.com/ReStruct-Corporate-Advantage/utils/issues" + }, + "homepage": "https://github.com/ReStruct-Corporate-Advantage/utils#readme" +} diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..a0d2fb4 --- /dev/null +++ b/publish.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +semver=${1:-patch} + +npm run build +set -e +npm --no-git-tag-version version ${semver} +npm publish --access public --loglevel verbose diff --git a/src/HOC.tsx b/src/HOC.tsx new file mode 100644 index 0000000..f8f0cef --- /dev/null +++ b/src/HOC.tsx @@ -0,0 +1,11 @@ +import { ComponentType } from "react" +import { useTheme } from "." // Adjust the import path as needed + +export const withTheme =

( + Component: ComponentType

, +): ComponentType

=> { + return (props: P) => { + const { theme, setTheme } = useTheme() + return + } +} diff --git a/src/adapters.ts b/src/adapters.ts new file mode 100644 index 0000000..fa4fb7d --- /dev/null +++ b/src/adapters.ts @@ -0,0 +1,128 @@ +import { + ComponentDescription, + ProgressiveChartData, + ThreeDChartDataArrayFormat, + ThreeDChartDataObjectFormat, + TreeListData, + RecusionConditionTypes, +} from "@armco/types" +import RecursionHelper from "./recursionHelper" + +const years = Array.from({ length: 60 }, (_, i) => 1964 + i) +const countries = ["IN", "US", "RU", "CN", "UK", "JP", "FR", "IT", "SP"] + +// Shuffle the years +for (let i = years.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[years[i], years[j]] = [years[j], years[i]] +} + +const dummyProgressiveChartData: ThreeDChartDataArrayFormat = countries.flatMap( + (country) => { + return years.map((year) => { + return [year, country, Math.floor(Math.random() * 100000000000000)] as [ + string | number, + string | number, + number, + ] + }) + }, +) + +class Adapter { + static adaptToTreeFromComponentConfig(data: any): Array { + const returnTreeList: Array = [] + Object.keys(data).forEach((key) => { + const groupConfig = data[key as keyof any] + const components = groupConfig.components + components.sort((c1: ComponentDescription, c2: ComponentDescription) => + (c1.name as any) > (c2.name as any) ? 1 : -1, + ) + const obj: TreeListData = { + label: groupConfig.label, + children: [], + data: { ...groupConfig }, + } + components.forEach((item: any) => { + const treeItem: TreeListData = { + label: typeof item === "string" ? item : item.name, + data: { + component: typeof item === "string" ? item : item.name, + hierarchy: key, + }, + } + const children: Array = [] + treeItem.children = children + if (item.variants && Object.keys(item.variants.length > 0)) { + Object.keys(item.variants).forEach((variantKey) => { + const variants = item.variants[variantKey] + treeItem.children && + treeItem.children.push({ + label: variantKey, + data: { name: typeof item === "string" ? item : item.name }, + children: variants.map((v: string) => ({ + label: v, + data: { + component: typeof item === "string" ? item : item.name, + props: { [variantKey]: v }, + hierarchy: key, + }, + })), + }) + }) + } + obj.children && obj.children.push(treeItem) + }) + returnTreeList.push(obj) + }) + RecursionHelper.injectIds({ + data: returnTreeList, + condition: { type: RecusionConditionTypes.KEY_EXISTS, key: "label" }, + iterateOn: "children", + }) + return returnTreeList + } + + static adaptToProgressiveChart( + inputData: ProgressiveChartData, + demo?: boolean, + ): ThreeDChartDataObjectFormat { + if (demo && !inputData) { + inputData = dummyProgressiveChartData + } + let unsortedData: ThreeDChartDataObjectFormat = {} + + // Transform the data into the desired format + if (Array.isArray(inputData)) { + inputData.forEach(([key, xValue, yValue]) => { + if (!unsortedData[key]) { + unsortedData[key] = [[xValue, yValue]] + } else { + unsortedData[key].push([xValue, yValue]) + } + }) + } else { + unsortedData = inputData + } + + // Sort the keys + const keys = Object.keys(unsortedData) + keys.sort((a, b) => { + if (typeof a === "number" && typeof b === "number") { + return a - b + } else { + return a.localeCompare(b) + } + }) + + // Create a new object with the sorted keys + let sortedData: ThreeDChartDataObjectFormat = {} + keys.forEach((key) => { + sortedData[key] = unsortedData[key] + }) + + return sortedData + } +} + +export default Adapter diff --git a/src/chartGenerators.ts b/src/chartGenerators.ts new file mode 100644 index 0000000..6c221b0 --- /dev/null +++ b/src/chartGenerators.ts @@ -0,0 +1,45 @@ +import * as d3 from "d3" +import { ArrayType, ObjectType } from "@armco/types" + +export const generateBubbleChart = (data: ArrayType | ObjectType) => { + const svg = d3.select("#ar-ArViz__chart-container") + if (svg && data) { + svg + .selectAll("circle") + .data(data as ArrayType) + .enter() + .append("circle") + .attr("cx", function (d) { + return (d as ObjectType).x as number + }) + .attr("cy", function (d) { + return (d as ObjectType).y as number + }) + .attr("r", function (d) { + return Math.sqrt((d as ObjectType).val as number) / Math.PI + }) + .attr("fill", function (d) { + return (d as ObjectType).color as string + }) + + // Step 5 + svg + .selectAll("text") + .data(data as ArrayType) + .enter() + .append("text") + .attr("x", function (d) { + const x = (d as ObjectType).x as number + const sqrt = Math.sqrt((d as ObjectType).val as number) as number + return x + sqrt / Math.PI + }) + .attr("y", function (d) { + return ((d as ObjectType).y as number) + 4 + }) + .text(function (d) { + return (d as ObjectType).source as string + }) + .style("font-family", "arial") + .style("font-size", "12px") + } +} diff --git a/src/contexts.ts b/src/contexts.ts new file mode 100644 index 0000000..ca75068 --- /dev/null +++ b/src/contexts.ts @@ -0,0 +1,21 @@ +import { createContext } from "react" +import { ArContextType, ArThemes, FunctionType } from "@armco/types" +import { Helper } from "." + +export const SlotterContext = createContext<{ + slotted: Array + setSlotted?: FunctionType +}>({ slotted: [] }) + +export const ArContext = createContext({ + theme: ArThemes.DARK1, + drawerState: { collapsed: Helper.isMobile() }, + notify: () => {}, + setDrawerState: () => {}, + setLeftPanelContent: () => {}, + setLoggedIn: () => {}, + setModalState: () => {}, + setRightPanelContent: () => {}, + setTheme: () => {}, + setUser: () => {}, +}) diff --git a/src/dateHelper.ts b/src/dateHelper.ts new file mode 100644 index 0000000..edf1e5b --- /dev/null +++ b/src/dateHelper.ts @@ -0,0 +1,546 @@ +import { CalendarDate, Event, ArDateFormats } from "@armco/types" +import { MONTH_INDEX } from "@armco/configs" +import { Helper } from "." + +export interface CalendarOptions { + /** + * Date object indicating the selected start date + */ + startDate?: CalendarDate | null + + /** + * Date object indicating the selected end date + */ + endDate?: CalendarDate | null + + /** + * Calculate dates from sibling months (before and after the current month, based on weekStart) + */ + siblingMonths?: boolean + + /** + * Calculate the week days + */ + weekNumbers?: boolean + + /** + * Day of the week to start the calendar, respects `Date.prototype.getDay` (defaults to `0`, Sunday) + */ + weekStart?: number +} + +export const sub = (num: number, separator?: string) => { + return ("" + num).substring(2) + (separator ? separator : "") +} + +export const str = (num: number, trim?: boolean, separator?: string) => { + const month = MONTH_INDEX[num] + return (trim ? month.substring(0, 4) : month) + (separator ? separator : "") +} + +export type CalendarInstance = Calendar +/** + * Calendar object + */ +class Calendar { + startDate: CalendarDate | null + endDate: CalendarDate | null + siblingMonths: boolean + weekNumbers: boolean + weekStart: number + + /** + * Calendar constructor + * + * @param options Calendar options + */ + constructor({ + startDate = null, + endDate = null, + siblingMonths = false, + weekNumbers = false, + weekStart = 0, + }: CalendarOptions = {}) { + this.startDate = startDate + this.endDate = endDate + this.siblingMonths = siblingMonths + this.weekNumbers = weekNumbers + this.weekStart = weekStart + } + + /** + * Calculate a calendar month + * + * @param year Year + * @param month Month [0-11] + * @return Calendar days + */ + getCalendar(year: number, month: number) { + const date = new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)) + + year = date.getFullYear() + month = date.getMonth() + + const calendar: (CalendarDate | false)[] = [] + const firstDay = date.getDay() + const firstDate = -((7 - this.weekStart + firstDay) % 7) + const lastDate = Calendar.daysInMonth(year, month) + const lastDay = (lastDate - firstDate) % 7 + const lastDatePreviousMonth = Calendar.daysInMonth(year, month - 1) + + let i = firstDate + let currentDay + let currentDate + let currentDateObject: CalendarDate | false = false + let currentWeekNumber = null + let otherMonth + let otherYear + + const max = lastDate - i + (lastDay !== 0 ? 7 - lastDay : 0) + firstDate + + while (i < max) { + currentDate = i + 1 + currentDay = ((i < 1 ? 7 + i : i) + firstDay) % 7 + if (currentDate < 1 || currentDate > lastDate) { + if (this.siblingMonths) { + if (currentDate < 1) { + otherMonth = month - 1 + otherYear = year + if (otherMonth < 0) { + otherMonth = 11 + otherYear-- + } + currentDate = lastDatePreviousMonth + currentDate + } else if (currentDate > lastDate) { + otherMonth = month + 1 + otherYear = year + if (otherMonth > 11) { + otherMonth = 0 + otherYear++ + } + currentDate = i - lastDate + 1 + } + + if (otherMonth !== undefined && otherYear !== undefined) { + currentDateObject = { + day: currentDate, + weekDay: currentDay, + month: otherMonth, + year: otherYear, + siblingMonth: true, + } + } + } else { + currentDateObject = false + } + } else { + currentDateObject = { + day: currentDate, + weekDay: currentDay, + month: month, + year: year, + } + } + + if (currentDateObject && this.weekNumbers) { + if (currentWeekNumber === null) { + currentWeekNumber = Calendar.calculateWeekNumber(currentDateObject) + } else if (currentDay === 1 && currentWeekNumber === 52) { + currentWeekNumber = 1 + } else if (currentDay === 1) { + currentWeekNumber++ + } + currentDateObject.weekNumber = currentWeekNumber + } + + if (currentDateObject && this.startDate) { + currentDateObject.selected = this.isDateSelected(currentDateObject) + } + + calendar.push(currentDateObject) + i++ + } + + return calendar + } + + /** + * Checks if a date is selected + * + * @param date Date object + * @return Selected status of the date + */ + isDateSelected(date: CalendarDate, endDate?: CalendarDate | null) { + // Hack to disregard this.endDate in further calculations for finding selectable candidates (hovered) + // by explicitly sending a "null" endDate, if endDate is not sent at all (undefined) then only function behaves as + // initially intended and considers endDate for calculations + if (!this.startDate || endDate === null) { + return false + } + let startDate = this.startDate + if (endDate) { + if (Calendar.compare(this.startDate, endDate) === 1) { + startDate = endDate + endDate = this.startDate + } + } else { + endDate = this.endDate + } + if ( + date.year === startDate.year && + date.month === startDate.month && + date.day === startDate.day + ) { + return true + } + + if (!endDate) { + return false + } + + if ( + date.year === startDate.year && + date.month === startDate.month && + date.day < startDate.day + ) { + return false + } + + if ( + date.year === endDate.year && + date.month === endDate.month && + date.day > endDate.day + ) { + return false + } + + if (date.year === startDate.year && date.month < startDate.month) { + return false + } + + if (date.year === endDate.year && date.month > endDate.month) { + return false + } + + if (date.year < startDate.year) { + return false + } + + if (date.year > endDate.year) { + return false + } + + return true + } + + /** + * Sets the selected period start + * + * @param date Date object + */ + setStartDate(date: CalendarDate | null) { + this.startDate = date + } + + /** + * Sets the selected period end + * + * @param date Date object + */ + setEndDate(date: CalendarDate | null) { + this.endDate = date + } + + /** + * Sets one selected date + * + * @param date Date object + */ + setDate(date: CalendarDate) { + return this.setStartDate(date) + } + + /** + * Calculates the difference between two dates (date1 - date2), in days + * + * @param dateLeft Date object + * @param dateRight Date object + * @return Days between the dates + */ + static diff(dateLeft: CalendarDate, dateRight: CalendarDate) { + const dateLeftDate = new Date( + Date.UTC(dateLeft.year, dateLeft.month, dateLeft.day, 0, 0, 0, 0), + ) + const dateRightDate = new Date( + Date.UTC(dateRight.year, dateRight.month, dateRight.day, 0, 0, 0, 0), + ) + return Math.ceil( + (dateLeftDate.getTime() - dateRightDate.getTime()) / 86400000, + ) + } + + /** + * Calculates the interval between two dates + * + * @param dateLeft Date object + * @param dateRight Date object + * @return Number of days between dates + */ + static interval(dateLeft: CalendarDate, dateRight: CalendarDate) { + return Math.abs(Calendar.diff(dateLeft, dateRight)) + 1 + } + + /** + * Quickly compare two dates + * + * @param dateLeft Left `CalendarDate` object + * @param dateRight Right `CalendarDate` object + * @return Comparison result: -1 (left < right), 0 (equal) or 1 (left > right) + */ + static compare(dateLeft: CalendarDate, dateRight: CalendarDate) { + if ( + typeof dateLeft !== "object" || + typeof dateRight !== "object" || + dateLeft === null || + dateRight === null + ) { + throw new TypeError("dates must be objects") + } + + if (dateLeft.year < dateRight.year) { + return -1 + } + + if (dateLeft.year > dateRight.year) { + return 1 + } + + if (dateLeft.month < dateRight.month) { + return -1 + } + + if (dateLeft.month > dateRight.month) { + return 1 + } + + if (dateLeft.day < dateRight.day) { + return -1 + } + + if (dateLeft.day > dateRight.day) { + return 1 + } + + return 0 + } + + /** + * Calculates the number of days in a month + * + * @param year Year + * @param month Month [0-11] + * @return Length of the month + */ + static daysInMonth(year: number, month: number) { + return new Date(year, month + 1, 0).getDate() + } + + /** + * Calculates if a given year is a leap year + * + * @param year Year + * @return Leap year or not + */ + static isLeapYear(year: number) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 + } + + /** + * Calculates the week number for a given date + * + * @param date Date object + * @return Week number + */ + // Adapted from http://techblog.procurios.nl/k/news/view/33796/14863/calculate-iso-8601-week-and-year-in-javascript.html + static calculateWeekNumber(date: CalendarDate) { + // Creates the requested date + const current = new Date( + Date.UTC(date.year, date.month, date.day, 0, 0, 0, 0), + ) + + // Create a copy of the object + const target = new Date(current.valueOf()) + + // ISO week date weeks start on monday so correct the day number + const dayNr = (current.getUTCDay() + 6) % 7 + + // ISO 8601 states that week 1 is the week with the first thursday of that + // year. Set the target date to the thursday in the target week. + target.setUTCDate(target.getUTCDate() - dayNr + 3) + + // Store the millisecond value of the target date + const firstThursday = target.valueOf() + + // Set the target to the first thursday of the year + + // First set the target to january first + target.setUTCMonth(0, 1) + + // Not a thursday? Correct the date to the next thursday + if (target.getUTCDay() !== 4) { + target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7)) + } + + // The week number is the number of weeks between the first thursday of the + // year and the thursday in the target week. + // 604800000 = 7 * 24 * 3600 * 1000 + return 1 + Math.ceil((firstThursday - target.getTime()) / 604800000) + } + + static toString( + date: CalendarDate, + separator: string = "/", + format: string = "DDMMYYYY", + ) { + const day = Helper.pad(date.day, separator) + const month = Helper.pad(date.month + 1, separator) + const year = date.year + + switch (format) { + case ArDateFormats.MMDDYY: + return month + day + sub(year) + case ArDateFormats.DDMMYYYY: + return day + month + year + case ArDateFormats.MMDDYYYY: + return month + day + year + case ArDateFormats.DDMMMYY: + return day + str(date.month + 1, true, separator) + sub(year) + case ArDateFormats.DDMMMYYYY: + return day + str(date.month + 1, true, separator) + year + case ArDateFormats.MMMDDYY: + return str(date.month + 1, true, separator) + day + sub(year) + case ArDateFormats.MMMDDYYYY: + return str(date.month + 1, true, separator) + day + year + case ArDateFormats.YYMMDD: + return sub(year) + month + day + case ArDateFormats.YYMMMDD: + return sub(year) + str(date.month + 1, true, separator) + day + case ArDateFormats.YYYYMMDD: + return year + month + day + case ArDateFormats.YYYYMMMDD: + return year + str(date.month + 1, true, separator) + day + case ArDateFormats.DDMMYY: + default: + return day + month + sub(year) + } + } + + static getPreviousMonth(date: Date) { + const month = date.getMonth() + const year = date.getFullYear() + + const previousMonth = month === 0 ? 11 : month - 1 + const previousYear = month === 0 ? year - 1 : year + + const returnDate = new Date() + returnDate.setDate(date.getDate()) + returnDate.setMonth(previousMonth) + returnDate.setFullYear(previousYear) + + return returnDate + } + + static getNextMonth(date: Date) { + const month = date.getMonth() + const year = date.getFullYear() + + const previousMonth = month === 11 ? 0 : month + 1 + const previousYear = month === 11 ? year + 1 : year + + const returnDate = new Date() + returnDate.setDate(date.getDate()) + returnDate.setMonth(previousMonth) + returnDate.setFullYear(previousYear) + + return returnDate + } + + static getPreviousYear(date: Date) { + const year = date.getFullYear() + const returnDate = new Date() + returnDate.setDate(date.getDate()) + returnDate.setMonth(date.getMonth()) + returnDate.setFullYear(year === 1 ? 1 : year - 1) + return returnDate + } + + static getNextYear(date: Date) { + const year = date.getFullYear() + const returnDate = new Date() + returnDate.setDate(date.getDate()) + returnDate.setMonth(date.getMonth()) + returnDate.setFullYear(year + 1) + return returnDate + } +} + +export const toDate = ( + calDate: CalendarDate, + hour?: number, + minute?: number, + ampm: string = "AM", +) => { + if (hour) { + let adjustedHour = hour + if (ampm === "PM" && hour !== 12) { + adjustedHour += 12 // Add 12 hours for PM time, except when it's 12 PM + } else if (ampm === "AM" && hour === 12) { + adjustedHour = 0 // 12 AM is equivalent to 0 hours + } + + return new Date( + calDate.year, + calDate.month, + calDate.day, + adjustedHour, + minute, + ) + } else return new Date(calDate.year, calDate.month, calDate.day) +} + +export const isWithinSchedule = (date: CalendarDate, event: Event) => { + const startDate = event.schedule?.starts + const endDate = event.schedule?.ends + + const startCalDate = startDate && { + day: (startDate as Date).getDate(), + month: (startDate as Date).getMonth(), + year: (startDate as Date).getFullYear(), + } + + const endCalDate = endDate && { + day: (endDate as Date).getDate(), + month: (endDate as Date).getMonth(), + year: (endDate as Date).getFullYear(), + } + + if (startCalDate && endCalDate) { + const checkStart = Calendar.compare(date, startCalDate) + const checkEnd = Calendar.compare(date, endCalDate) + const amPm = (endDate as Date) + ?.toLocaleString("en-US", { hour12: true }) + .split(" ")[2] + const shouldExcludeEnd = + (endDate as Date).getHours() === 0 && + amPm === "AM" && + date.day === endCalDate.day + + return checkStart >= 0 && checkEnd <= 0 && !shouldExcludeEnd + } + return false +} + +/** + * Exports the Calendar + */ +export { Calendar as CalHelper } diff --git a/src/dateformat.ts b/src/dateformat.ts new file mode 100644 index 0000000..e7b31cb --- /dev/null +++ b/src/dateformat.ts @@ -0,0 +1,336 @@ +import { FunctionType } from "@armco/types" +import { ArDateMasks } from "@armco/types" + +const token = + /d{1,4}|D{3,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|W{1,2}|[LlopSZN]|"[^"]*"|'[^']*'/g +const timezone = + /\b(?:[A-Z]{1,3}[A-Z][TC])(?:[-+]\d{4})?|((?:Australian )?(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time)\b/g +const timezoneClip = /[^-+\dA-Z]/g + +/** + * @param {string | number | Date} date + * @param {string} mask + * @param {boolean} utc + * @param {boolean} gmt + */ +export function dateFormat( + date?: number | Date, + mask?: ArDateMasks | string, + utc?: boolean, + gmt?: boolean, +) { + // You can't provide utc if you skip other args (use the 'UTC:' mask prefix) + if (arguments.length === 1 && typeof date === "string" && !/\d/.test(date)) { + mask = date + date = undefined + } + + date = date || date === 0 ? date : new Date() + + if (isNaN(date as number)) { + throw TypeError("Invalid date") + } + + if (!(date instanceof Date)) { + date = new Date(date) + } + + mask = String( + ArDateMasksRecord[mask as string] || mask || ArDateMasksRecord["DEFAULT"], + ) + + // Allow setting the utc/gmt argument via the mask + const maskSlice = mask.slice(0, 4) + if (maskSlice === "UTC:" || maskSlice === "GMT:") { + mask = mask.slice(4) + utc = true + if (maskSlice === "GMT:") { + gmt = true + } + } + + const _ = () => (utc ? "getUTC" : "get") + const d = () => (date as Date)[(_() + "Date") as "getDate" | "getUTCDate"]() + const D = () => (date as Date)[(_() + "Day") as "getDay" | "getUTCDay"]() + const m = () => + (date as Date)[(_() + "Month") as "getMonth" | "getUTCMonth"]() + const y = () => + (date as Date)[(_() + "FullYear") as "getFullYear" | "getUTCFullYear"]() + const H = () => + (date as Date)[(_() + "Hours") as "getHours" | "getUTCHours"]() + const M = () => + (date as Date)[(_() + "Minutes") as "getMinutes" | "getUTCMinutes"]() + const s = () => + (date as Date)[(_() + "Seconds") as "getSeconds" | "getUTCSeconds"]() + const L = () => + (date as Date)[ + (_() + "Milliseconds") as "getMilliseconds" | "getUTCMilliseconds" + ]() + const o = () => (utc ? 0 : (date as Date).getTimezoneOffset()) + const W = () => date instanceof Date && getWeek(date) + const N = () => date instanceof Date && getDayOfWeek(date) + + const flags: { [key: string]: FunctionType } = { + d: () => d(), + dd: () => pad(d()), + ddd: () => i18n.dayNames[D()], + DDD: () => + getDayName({ + y: y(), + m: m(), + d: d(), + _: _(), + dayName: i18n.dayNames[D()], + short: true, + }), + dddd: () => i18n.dayNames[D() + 7], + DDDD: () => + getDayName({ + y: y(), + m: m(), + d: d(), + _: _(), + dayName: i18n.dayNames[D() + 7], + }), + m: () => m() + 1, + mm: () => pad(m() + 1), + mmm: () => i18n.monthNames[m()], + mmmm: () => i18n.monthNames[m() + 12], + yy: () => String(y()).slice(2), + yyyy: () => pad(y(), 4), + h: () => H() % 12 || 12, + hh: () => pad(H() % 12 || 12), + H: () => H(), + HH: () => pad(H()), + M: () => M(), + MM: () => pad(M()), + s: () => s(), + ss: () => pad(s()), + l: () => pad(L(), 3), + L: () => pad(Math.floor(L() / 10)), + t: () => (H() < 12 ? i18n.timeNames[0] : i18n.timeNames[1]), + tt: () => (H() < 12 ? i18n.timeNames[2] : i18n.timeNames[3]), + T: () => (H() < 12 ? i18n.timeNames[4] : i18n.timeNames[5]), + TT: () => (H() < 12 ? i18n.timeNames[6] : i18n.timeNames[7]), + Z: () => + gmt ? "GMT" : utc ? "UTC" : date instanceof Date && formatTimezone(date), + o: () => + (o() > 0 ? "-" : "+") + + pad(Math.floor(Math.abs(o()) / 60) * 100 + (Math.abs(o()) % 60), 4), + p: () => + (o() > 0 ? "-" : "+") + + pad(Math.floor(Math.abs(o()) / 60), 2) + + ":" + + pad(Math.floor(Math.abs(o()) % 60), 2), + S: () => + ["th", "st", "nd", "rd"][ + d() % 10 > 3 ? 0 : (+((d() % 100) - (d() % 10) !== 10) * d()) % 10 + ], + W: () => W(), + WW: () => { + const date = W() + date && pad(date) + }, + N: () => N(), + } + + return mask.replace(token, (match) => { + if (match in flags) { + return flags[match]() + } + return match.slice(1, match.length - 1) + }) +} + +const ArDateMasksRecord: Record = { + DEFAULT: ArDateMasks.DEFAULT, + SHORTDATE: ArDateMasks.SHORTDATE, + PADDEDSHORTDATE: ArDateMasks.PADDEDSHORTDATE, + MEDIUMDATE: ArDateMasks.MEDIUMDATE, + LONGDATE: ArDateMasks.LONGDATE, + FULLDATE: ArDateMasks.FULLDATE, + SHORTTIME: ArDateMasks.SHORTTIME, + MEDIUMTIME: ArDateMasks.MEDIUMTIME, + LONGTIME: ArDateMasks.LONGTIME, + ISODATE: ArDateMasks.ISODATE, + ISOTIME: ArDateMasks.ISOTIME, + ISODATETIME: ArDateMasks.ISODATETIME, + ISOUTCDATETIME: ArDateMasks.ISOUTCDATETIME, + EXPIRESHEADERFORMAT: ArDateMasks.EXPIRESHEADERFORMAT, +} + +// Internationalization strings +export let i18n = { + dayNames: [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ], + monthNames: [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ], + timeNames: ["a", "p", "am", "pm", "A", "P", "AM", "PM"], +} + +const pad = (val: number, len = 2) => String(val).padStart(len, "0") + +/** + * Get day name + * Yesterday, Today, Tomorrow if the date lies within, else fallback to Monday - Sunday + * @param {Object} + * @return {String} + */ +const getDayName = ({ + y, + m, + d, + _, + dayName, + short = false, +}: { + y: number + m: number + d: number + _: string + dayName: string + short?: boolean +}) => { + const today = new Date() + const yesterday = new Date() + const tomorrow = new Date() + const dateAccessor = (_ + "Date") as "getDate" | "getUTCDate" + const monthAccessor = (_ + "Month") as "getMonth" | "getUTCMonth" + const yearAccessor = (_ + "FullYear") as "getFullYear" | "getUTCFullYear" + yesterday.setDate(yesterday[dateAccessor]() - 1) + tomorrow.setDate(tomorrow[dateAccessor]() + 1) + const today_d = () => today[dateAccessor]() + const today_m = () => today[monthAccessor]() + const today_y = () => today[yearAccessor]() + const yesterday_d = () => yesterday[dateAccessor]() + const yesterday_m = () => yesterday[monthAccessor]() + const yesterday_y = () => yesterday[yearAccessor]() + const tomorrow_d = () => tomorrow[dateAccessor]() + const tomorrow_m = () => tomorrow[monthAccessor]() + const tomorrow_y = () => tomorrow[yearAccessor]() + + if (today_y() === y && today_m() === m && today_d() === d) { + return short ? "Tdy" : "Today" + } else if ( + yesterday_y() === y && + yesterday_m() === m && + yesterday_d() === d + ) { + return short ? "Ysd" : "Yesterday" + } else if (tomorrow_y() === y && tomorrow_m() === m && tomorrow_d() === d) { + return short ? "Tmw" : "Tomorrow" + } + return dayName +} + +/** + * Get the ISO 8601 week number + * Based on comments from + * http://techblog.procurios.nl/k/n618/news/view/33796/14863/Calculate-ISO-8601-week-and-year-in-javascript.html + * + * @param {Date} `date` + * @return {Number} + */ +const getWeek = (date: Date) => { + // Remove time components of date + const targetThursday = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + ) + + // Change date to Thursday same week + targetThursday.setDate( + targetThursday.getDate() - ((targetThursday.getDay() + 6) % 7) + 3, + ) + + // Take January 4th as it is always in week 1 (see ISO 8601) + const firstThursday = new Date(targetThursday.getFullYear(), 0, 4) + + // Change date to Thursday same week + firstThursday.setDate( + firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3, + ) + + // Check if daylight-saving-time-switch occurred and correct for it + const ds = + targetThursday.getTimezoneOffset() - firstThursday.getTimezoneOffset() + targetThursday.setHours(targetThursday.getHours() - ds) + + // Number of weeks between target Thursday and first Thursday + const weekDiff = + (targetThursday.getTime() - firstThursday.getTime()) / (86400000 * 7) + return 1 + Math.floor(weekDiff) +} + +/** + * Get ISO-8601 numeric representation of the day of the week + * 1 (for Monday) through 7 (for Sunday) + * + * @param {Date} `date` + * @return {Number} + */ +const getDayOfWeek = (date: Date) => { + let dow = date.getDay() + if (dow === 0) { + dow = 7 + } + return dow +} + +/** + * Get proper timezone abbreviation or timezone offset. + * + * This will fall back to `GMT+xxxx` if it does not recognize the + * timezone within the `timezone` RegEx above. Currently only common + * American and Australian timezone abbreviations are supported. + * + * @param {String | Date} date + * @return {String} + */ +export const formatTimezone = (date: Date) => { + const strDate = String(date) + const match = strDate.match(timezone) || [""] + return match + .pop() + ?.replace(timezoneClip, "") + .replace(/GMT\+0000/g, "UTC") +} diff --git a/src/domHelper.ts b/src/domHelper.ts new file mode 100644 index 0000000..31c5feb --- /dev/null +++ b/src/domHelper.ts @@ -0,0 +1,456 @@ +import { Children } from "react" +import { + CarouselProps, + IconProps, + ArPopoverPositions, + ObjectType, + Position, +} from "@armco/types" +import { Helper } from "." + +class DomHelper { + static style = window.getComputedStyle(document.documentElement) + static fontSize = parseFloat(DomHelper.style.fontSize) + static markerSize = 7 + + static outerWidth(el: HTMLElement) { + let width = el.offsetWidth + const style = getComputedStyle(el) + + width += parseInt(style.marginLeft) + parseInt(style.marginRight) + return width + } + + static translate( + position: number, + metric: "px" | "%", + axis: "horizontal" | "vertical", + ) { + const positionPercent = position === 0 ? position : position + metric + const positionCss = + axis === "horizontal" ? [positionPercent, 0, 0] : [0, positionPercent, 0] + const transitionProp = "translate3d" + + const translatedPosition = "(" + positionCss.join(",") + ")" + + return transitionProp + translatedPosition + } + + static hexToHsl(color: string, returnRaw?: boolean) { + const [r, g, b] = DomHelper.hexToRgb(color) + return DomHelper.rgbToHsl(r, g, b, returnRaw) + } + + static hexToRgb(color: string) { + const r = parseInt(color.substring(1, 3), 16) // Grab the hex representation of red (chars 1-2) and convert to decimal (base 10). + const g = parseInt(color.substring(3, 5), 16) + const b = parseInt(color.substring(5, 7), 16) + return [r, g, b] + } + + /** + * Converts an RGB color value to HSL. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes r, g, and b are contained in the set [0, 255] and + * returns h, s, and l in the set [0, 1]. + * + * @param Number r The red color value + * @param Number g The green color value + * @param Number b The blue color value + * @return Array The HSL representation + */ + static rgbToHsl(r: number, g: number, b: number, returnRaw?: boolean) { + r /= 255 + g /= 255 + b /= 255 + + const max = Math.max(r, g, b), + min = Math.min(r, g, b) + let h, + s, + l = (max + min) / 2 + + if (max === min) { + h = s = 0 // achromatic + } else { + const d = max - min + s = l > 0.5 ? d / (2 - max - min) : d / (max + min) + + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0) + break + case g: + h = (b - r) / d + 2 + break + case b: + h = (r - g) / d + 4 + break + } + + // @ts-ignore + h /= 6 + } + const colorInHSL = "hsl(" + h + ", " + s + "%, " + l + "%)" + return returnRaw ? [h, s, l] : colorInHSL + } + + static hslToRgb(h: number, s: number, l: number) { + let r, g, b + let hue2rgb + if (s === 0) { + r = g = b = l // achromatic + } else { + hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1 + if (t > 1) t -= 1 + if (t < 1 / 6) return p + (q - p) * 6 * t + if (t < 1 / 2) return q + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 + return p + } + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s + const p = 2 * l - q + + r = hue2rgb && hue2rgb(p, q, h + 1 / 3) + g = hue2rgb && hue2rgb(p, q, h) + b = hue2rgb && hue2rgb(p, q, h - 1 / 3) + } + + return [r * 255, g * 255, b * 255] + } + + static rgbToHex(r: number, g: number, b: number) { + function componentToHex(c: number) { + var hex = c.toString(16) + return hex.length === 1 ? "0" + hex : hex + } + return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b) + } + + static hslToHex(h: number, s: number, l: number) { + const [r, g, b] = DomHelper.hslToRgb(h, s, l) + return DomHelper.rgbToHex(Math.round(r), Math.round(g), Math.round(b)) + } + + static implementSvgStyles( + svg: SVGSVGElement, + svgProps?: IconProps["attributes"], + flags?: Array, + ) { + const { size, classes, colors, strokeWidth } = svgProps || {} + const { fillColor, strokeColor } = colors || {} + const [skipPathColorFill] = flags || [] + let styles = null + if (classes) { + styles = Helper.getStylesFromClass(classes) + styles = Object.entries(styles || {}) + .map(([property, value]) => `${property}: ${value}`) + .join("; ") + } + svg.setAttribute("height", size || "1rem") + svg.setAttribute("width", size || "1rem") + svg.setAttribute("class", classes || "") + svg.setAttribute("style", styles || "") + svg.setAttribute("stroke", strokeColor || "black") + strokeWidth + ? svg.setAttribute("stroke-width", strokeWidth) + : strokeColor && svg.setAttribute("stroke-width", "1") + let path: SVGPathElement | NodeListOf = + svg.querySelectorAll("path") + if (path.length === 1 && !skipPathColorFill) { + path = path[0] + // Override stroke and color of "path" node inside SVG at below line. + path && + strokeColor && + !!path.getAttribute("stroke") && + path.setAttribute("stroke", strokeColor) + path && + strokeWidth && + !!path.getAttribute("stroke-width") && + path.setAttribute("stroke-width", strokeWidth) + path && + fillColor && + !!path.getAttribute("fill") && + path.setAttribute("fill", fillColor) + } + svg.style.color = fillColor || "black" + return svg + } + + /** + * Gets the list 'position' relative to a current index + * @param index + */ + static getPosition(index: number, props: CarouselProps): number { + if (props.infiniteLoop) { + // index has to be added by 1 because of the first cloned slide + ++index + } + + if (index === 0) { + return 0 + } + + const childrenLength = Children.count(props.children) + if (props.centerMode && props.axis === "horizontal") { + let currentPosition = -index * props.centerSlidePercentage + const lastPosition = childrenLength - 1 + + if (index && (index !== lastPosition || props.infiniteLoop)) { + currentPosition += (100 - props.centerSlidePercentage) / 2 + } else if (index === lastPosition) { + currentPosition += 100 - props.centerSlidePercentage + } + + return currentPosition + } + + return -index * 100 + } + + /** + * Sets the 'position' transform for sliding animations + * @param position + * @param forceReflow + */ + static setPosition( + position: number, + axis: "horizontal" | "vertical", + ): React.CSSProperties { + const style = {} + ;[ + "WebkitTransform", + "MozTransform", + "MsTransform", + "OTransform", + "transform", + "msTransform", + ].forEach((prop) => { + // @ts-ignore + style[prop] = CSSTranslate(position, "%", axis) + }) + + return style + } + + static isKeyboardEvent( + e?: React.MouseEvent | React.KeyboardEvent, + ): e is React.KeyboardEvent { + return e ? e.hasOwnProperty("key") : false + } + + static getDocumentElement(demo?: boolean) { + // Check if the component is running inside an iframe + if (demo) { + const iframeElement = + document.querySelector(".ar-Editor__frame") + if (iframeElement) { + return iframeElement.contentDocument + } + } + // Return the parent document element by default + return document + } + + static getWindowElement(demo?: boolean) { + // Check if the component is running inside an iframe + if (demo) { + const iframeElement = + document.querySelector(".ar-Editor__frame") + + if (iframeElement) { + return iframeElement.contentWindow + } + } + // Return the parent document element by default + return window + } + + static shouldUsePosition( + position: ArPopoverPositions, + anchorRect: DOMRect, + popoverRect: DOMRect, + demo?: boolean, + ) { + const windowObj = DomHelper.getWindowElement(demo) || window + const exceeds: ObjectType = { + bottom: + anchorRect.top + anchorRect.height + popoverRect.height > + windowObj.innerHeight, + right: + anchorRect.left + anchorRect.width + popoverRect.width > + windowObj.innerWidth, + left: anchorRect.left - popoverRect.width < 0, + top: anchorRect.top - popoverRect.height < 0, + } + if (exceeds[position]) return false + if (position === "left" || position === "right") { + if ( + DomHelper.fontSize > anchorRect.top || + DomHelper.fontSize > anchorRect.bottom + ) + return false + } else { + if ( + DomHelper.fontSize > anchorRect.left || + DomHelper.fontSize > anchorRect.right + ) + return false + } + return position + } + + static adjustPosition( + position: ArPopoverPositions, + anchorRect: DOMRect, + popoverRect: DOMRect, + hideMarker?: boolean, + topOffset?: number, + ) { + const popoverPositions: { [key: string]: Position } = { + [ArPopoverPositions.BOTTOM]: { + top: + anchorRect.top + + anchorRect.height + + (hideMarker ? 0 : DomHelper.markerSize) + + (topOffset || 0), + left: anchorRect.left + (anchorRect.width - popoverRect.width) / 2, + }, + [ArPopoverPositions.RIGHT]: { + top: anchorRect.top + (anchorRect.height - popoverRect.height) / 2, + left: + anchorRect.left + + anchorRect.width + + (hideMarker ? 0 : DomHelper.markerSize), + }, + [ArPopoverPositions.LEFT]: { + top: anchorRect.top + (anchorRect.height - popoverRect.height) / 2, + left: + anchorRect.left - + popoverRect.width - + (hideMarker ? 0 : DomHelper.markerSize), + }, + [ArPopoverPositions.TOP]: { + top: + anchorRect.top - + popoverRect.height - + (hideMarker ? 0 : DomHelper.markerSize) - + (topOffset || 0), + left: anchorRect.left + (anchorRect.width - popoverRect.width) / 2, + }, + } + let { left, top } = popoverPositions[position] + if ( + position === ArPopoverPositions.BOTTOM || + position === ArPopoverPositions.TOP + ) { + if ((left as number) < 0) { + left = DomHelper.fontSize + } + if ((left as number) + popoverRect.width > window.innerWidth) { + left = `calc(100% - ${popoverRect.width}px - ${DomHelper.fontSize}px)` + } + } else { + if ((top as number) < 0) { + top = DomHelper.fontSize + } + if ((top as number) + popoverRect.height > window.innerHeight) { + top = `calc(100% - ${popoverRect.height}px - ${DomHelper.fontSize}px)` + } + } + return { left, top } + } + + static getPositionToUse( + anchorRect: DOMRect, + popoverRect: DOMRect, + requestedPosition?: ArPopoverPositions, + demo?: boolean, + ) { + if (requestedPosition && requestedPosition !== ArPopoverPositions.AUTO) { + return requestedPosition + } + const positionsPriority = [ + ArPopoverPositions.BOTTOM, + ArPopoverPositions.RIGHT, + ArPopoverPositions.LEFT, + ArPopoverPositions.TOP, + ] + + for (const position of positionsPriority) { + if ( + DomHelper.shouldUsePosition(position, anchorRect, popoverRect, demo) + ) { + return position + } + continue + } + + return ArPopoverPositions.BOTTOM + } + + static calculatePopoverPosition( + anchorRect: DOMRect, + popoverRect: DOMRect, + requestedPosition?: ArPopoverPositions, + hideMarker?: boolean, + clickCoordinates?: Array | null, + demo?: boolean, + topOffset?: number, + ) { + const windowObj = DomHelper.getWindowElement(demo) || window + anchorRect = clickCoordinates + ? { + left: clickCoordinates[0], + x: clickCoordinates[0], + top: clickCoordinates[1], + y: clickCoordinates[1], + height: 0, + width: 0, + right: windowObj.screen.width - clickCoordinates[0], + bottom: windowObj.screen.height - clickCoordinates[1], + toJSON: () => {}, + } + : anchorRect + const position = DomHelper.getPositionToUse( + anchorRect, + popoverRect, + requestedPosition, + demo, + ) + const { left, top } = DomHelper.adjustPosition( + position, + anchorRect, + popoverRect, + hideMarker, + topOffset, + ) + return { + leftTop: { + top: ("" + top).startsWith("calc") ? top : top + "px", + left: ("" + left).startsWith("calc") ? left : left + "px", + }, + position, + } + } + + static openInNewTab(url: string) { + const win = window.open(url, "_blank") + if (win != null) { + win.focus() + } + } + + static download(dataURL: any) { + const anchor = document.createElement("a") + anchor.href = dataURL + anchor.download = "edited-image.png" + document.body.appendChild(anchor) + anchor.click() + document.body.removeChild(anchor) + } +} + +export default DomHelper diff --git a/src/gridHelper.ts b/src/gridHelper.ts new file mode 100644 index 0000000..397b42c --- /dev/null +++ b/src/gridHelper.ts @@ -0,0 +1,737 @@ +import { v4 as uuid } from "uuid" +import { FunctionType, GridToolbarSpecs, SlotDescriptor } from "@armco/types" +import { DomHelper } from "." + +class GridHelper { + // For x and y toolbars only + static generateGridToolbarSpecs( + gridArea: Array>, + rowHeight?: Array | string, + colWidth?: Array | string, + ) { + const rowToolsGridArea = gridArea.map((row) => [ + row.every((cell) => cell === row[0]) ? row[0] : "ga-" + uuid(), + ]) + const colToolsGridArea = gridArea[0].map((_, i) => + gridArea.every((row) => row[i] === gridArea[0][i]) + ? gridArea[0][i] + : "ga-" + uuid(), + ) + const rowSlots = GridHelper.countOccurrences( + rowToolsGridArea.map((ga) => ga[0]), + ).map((slotConfig) => { + return { + slot: slotConfig.slotId, + gridArea: slotConfig.slotId, + row: slotConfig.location, + column: 0, + rowSpan: slotConfig.span, + colSpan: 1, + style: { gridArea: slotConfig.slotId }, + } + }) + const colSlots = GridHelper.countOccurrences(colToolsGridArea).map( + (slotConfig) => { + return { + slot: slotConfig.slotId, + gridArea: slotConfig.slotId, + row: 0, + column: slotConfig.location, + rowSpan: 1, + colSpan: slotConfig.span, + style: { gridArea: slotConfig.slotId }, + } + }, + ) + return { + rowTools: { + slots: rowSlots, + gridTemplate: this.generateGridTemplate( + rowToolsGridArea, + rowHeight || "minmax(auto, 1fr)", + colWidth || "1fr", + ), + }, + colTools: { + slots: colSlots, + gridTemplate: this.generateGridTemplate( + [colToolsGridArea], + "1fr", + "1fr", + ), + }, + } + } + + static calculateRowHeights( + slots: Array, + rowHeights?: Array | string, + demo?: boolean, + ): Array { + const totalRows = Math.max(...slots.map((slot) => slot.row + slot.rowSpan)) + const allRowHeights: Array = [] + const doc = DomHelper.getDocumentElement(demo) + rowHeights = Array.isArray(rowHeights) + ? rowHeights + : (Array.from( + { length: totalRows }, + () => rowHeights || "minmax(2rem, auto)", + ) as Array) + + for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) { + if ( + rowHeights[rowIndex] && + rowHeights[rowIndex] !== "" && + rowHeights[rowIndex].indexOf("auto") === -1 + ) { + allRowHeights[rowIndex] = rowHeights[rowIndex] + continue + } + + const rowSlots = slots.filter( + (slot) => slot.row <= rowIndex && rowIndex < slot.row + slot.rowSpan, + ) + const heights = rowSlots.map((slot) => { + const element = doc?.getElementById(slot.slot) + return element + ? Math.floor(element.getBoundingClientRect().height / slot.rowSpan) + : 0 + }) + const maxHeight = Math.max(...heights) + allRowHeights[rowIndex] = + maxHeight === 0 ? "minmax(2rem, auto)" : `${maxHeight}px` + } + + return allRowHeights + } + + static generateGridAreaAndSizes( + slots: Array, + currentRowHeights?: string | Array, + ): { gridArea: Array>; rowHeights: Array } { + let gridArea: Array> = [[]] + const rowHeights: Array = [] + slots.forEach((slot) => { + let endRow = slot.row + slot.rowSpan + let endColumn = slot.column + slot.colSpan + for (let row = slot.row; row < endRow; row++) { + const currentRowHeight = Array.isArray(currentRowHeights) + ? currentRowHeights[row] + : currentRowHeights + if (slot.content) { + rowHeights[row] = currentRowHeight || "auto" + } else { + rowHeights[row] = "minmax(2rem, auto)" + } + if (!gridArea[row]) { + gridArea[row] = [] + } + for (let col = slot.column; col < endColumn; col++) { + gridArea[row][col] = slot.gridArea + } + } + }) + + return { gridArea, rowHeights } + } + + static generateGridTemplate( + gridArea: Array>, + rowHeight?: Array | string, + colWidth?: Array | string, + ): string { + let areas = gridArea + .map( + (row, i) => + `"${row.join(" ")}" ${ + Array.isArray(rowHeight) + ? rowHeight[i] || "auto" + : rowHeight || "minmax(3rem, 1fr)" + }`, + ) + .join(" ") + + const gridTemplateColumns = + " / " + + (Array.isArray(colWidth) + ? colWidth.join(" ") + : Array.from( + { length: gridArea[0].length }, + () => colWidth || "1fr", + ).join(" ")) + areas += gridTemplateColumns + + return areas.trim() + } + + static mergeHandler( + setSlots: FunctionType, + slotConfigs: Array, + minMaxSelections: { + minRow: number + maxRow: number + minColumn: number + maxColumn: number + }, + primarySlotConfig?: SlotDescriptor, + ) { + if (slotConfigs) { + slotConfigs = JSON.parse(JSON.stringify(slotConfigs)) + const selectedSlotConfigs = slotConfigs.filter((sc) => sc.isSelected) + selectedSlotConfigs.sort((a, b) => { + if (a.row !== b.row) { + return a.row - b.row + } else { + return a.column - b.column + } + }) + const firstSlotConfig = selectedSlotConfigs[0] + if (primarySlotConfig) { + primarySlotConfig = slotConfigs.find( + (s) => s.slot === primarySlotConfig?.slot, + ) as SlotDescriptor + primarySlotConfig.row = firstSlotConfig.row + primarySlotConfig.column = firstSlotConfig.column + } else { + primarySlotConfig = firstSlotConfig + } + primarySlotConfig.rowSpan = + minMaxSelections.maxRow - minMaxSelections.minRow + 1 + primarySlotConfig.colSpan = + minMaxSelections.maxColumn - minMaxSelections.minColumn + 1 + selectedSlotConfigs.forEach((ssc) => { + const matchedSlotConfigIndex = slotConfigs.findIndex( + (sc) => sc.slot === ssc.slot, + ) + const matchedSlotConfig = slotConfigs[matchedSlotConfigIndex] + if ( + matchedSlotConfig && + matchedSlotConfig.slot !== primarySlotConfig?.slot + ) { + slotConfigs.splice(matchedSlotConfigIndex, 1) + } + }) + // slotConfigs.forEach((slotConfig) => (slotConfig.isSelected = false)) + setSlots(slotConfigs) + } + } + + static splitHandler( + setSlots: FunctionType, + slotConfigs: Array, + orientation: "horizontal" | "vertical", + placement: "before" | "after", + slot?: SlotDescriptor, + ) { + slotConfigs = JSON.parse(JSON.stringify(slotConfigs)) + let selectedSlots = [] + if (slot) { + slot = slotConfigs.find((s) => s.slot === slot?.slot) + slot && selectedSlots.push(slot) + } else { + selectedSlots = slotConfigs.filter((sc) => sc.isSelected) + } + selectedSlots.forEach((selectedSlot) => { + const { + row, + slot: selectedSlotId, + column, + colSpan, + rowSpan, + } = selectedSlot + const isHorizontal = orientation === "horizontal" + const isAfter = placement === "after" + const slotId = uuid() + const gridAreaValue = `ga-${slotId}` + const span = isHorizontal ? "rowSpan" : "colSpan" + const dimension = isHorizontal ? "row" : "column" + let newDimSpan = selectedSlot[span] + if (newDimSpan > 1) { + newDimSpan = selectedSlot[span] - 1 + selectedSlot[span] = newDimSpan + } + const newSlot: SlotDescriptor = { + slot: slotId, + row: isHorizontal ? (isAfter ? newDimSpan + row : row) : row, + column: isHorizontal ? column : isAfter ? newDimSpan + column : column, + rowSpan: isHorizontal ? 1 : rowSpan, + colSpan: isHorizontal ? colSpan : 1, + gridArea: gridAreaValue, + splitFrom: selectedSlotId, + } + + const shouldUpdateOtherSlots = isHorizontal + ? rowSpan === 1 + : colSpan === 1 + + // Update slot configs + shouldUpdateOtherSlots && + slotConfigs.forEach((slot) => { + const { + row: currRow, + column: currColumn, + slot: currSlot, + colSpan: currColSpan, + rowSpan: currRowSpan, + } = slot + if (isHorizontal) { + if ( + row >= currRow && + row < currRow + currRowSpan && + currSlot !== selectedSlotId + ) { + slot.rowSpan += 1 + } else if (currRow > row) { + slot.row += 1 + } + } else { + if ( + column >= currColumn && + column < currColumn + currColSpan && + currSlot !== selectedSlotId + ) { + slot.colSpan += 1 + } else if (currColumn > column) { + slot.column += 1 + } + } + }) + if (!isAfter) { + selectedSlot[dimension] += 1 + } + slotConfigs.push(newSlot) + }) + + setSlots(slotConfigs) + } + + static removeHandler( + slotConfigs: Array, + gridToolbarSpecs: GridToolbarSpecs, + type: string, + isInlineDelete?: boolean, + ): Array { + slotConfigs = JSON.parse(JSON.stringify(slotConfigs)) + const isRow = type === "row" + const toolbarSlots = isRow + ? gridToolbarSpecs.rowTools.slots + : gridToolbarSpecs.colTools.slots + // Find and sort selected slots from toolbarSpecs + const selectedToolbarSlots = toolbarSlots + .filter((slot) => + isInlineDelete ? slot.isSelectedForInlineDelete : slot.isSelected, + ) + .sort((a, b) => (isRow ? a.row - b.row : a.column - b.column)) + + // Iterate over each selected slot + selectedToolbarSlots.forEach((toolbarSlot) => { + // Find intersecting slots from slotConfigs + const intersectingSlots = slotConfigs.filter((slot) => + isRow + ? toolbarSlot.row >= slot.row && + toolbarSlot.row < slot.row + slot.rowSpan + : toolbarSlot.column >= slot.column && + toolbarSlot.column < slot.column + slot.colSpan, + ) + + // Delete slots with colSpan 1, decrement colSpan of rest by 1 + intersectingSlots.forEach((intersectingSlot) => { + if ( + isRow + ? intersectingSlot.rowSpan <= toolbarSlot.rowSpan + : intersectingSlot.colSpan <= toolbarSlot.colSpan + ) { + const index = slotConfigs.indexOf(intersectingSlot) + if (index !== -1) slotConfigs.splice(index, 1) + } else { + isRow + ? (intersectingSlot.rowSpan -= 1) + : (intersectingSlot.colSpan -= 1) + } + }) + + // Find next slots and decrement their row/column value by 1 + let succeedingSlots = slotConfigs.filter((slot) => + isRow ? slot.row > toolbarSlot.row : slot.column > toolbarSlot.column, + ) + succeedingSlots.forEach((succeedingSlot) => + isRow ? (succeedingSlot.row -= 1) : (succeedingSlot.column -= 1), + ) + + // Find succeeding toolbar slot and decrement their row/column value by 1 + succeedingSlots = toolbarSlots.filter((currToolbarSlot) => + isRow + ? currToolbarSlot.row > toolbarSlot.row + : currToolbarSlot.column > toolbarSlot.column, + ) + succeedingSlots.forEach((succeedingSlot) => + isRow ? (succeedingSlot.row -= 1) : (succeedingSlot.column -= 1), + ) + toolbarSlots.splice(toolbarSlots.indexOf(toolbarSlot), 1) + }) + + return slotConfigs + } + static checkIfAdjacent(slotConfigs: Array) { + const selectedSlotConfigs = slotConfigs.filter((sc) => sc.isSelected) + let minRow = Infinity + let maxRow = -Infinity + let minColumn = Infinity + let maxColumn = -Infinity + let totalCells = 0 + selectedSlotConfigs.forEach((config: SlotDescriptor) => { + if (config.row !== undefined && config.column !== undefined) { + minRow = Math.min(minRow, config.row) + maxRow = Math.max(maxRow, config.row + (config.rowSpan - 1)) + minColumn = Math.min(minColumn, config.column) + maxColumn = Math.max(maxColumn, config.column + (config.colSpan - 1)) + totalCells += config.rowSpan * config.colSpan + } + }) + const rectangleArea = (maxRow - minRow + 1) * (maxColumn - minColumn + 1) + + return { + areAdjacent: + selectedSlotConfigs.length > 1 && rectangleArea === totalCells, + minRow, + maxRow, + minColumn, + maxColumn, + } + } + + static isSlotSelected( + slot: SlotDescriptor, + rowSlots: Array, + colSlots: Array, + selectForDelete?: boolean, + ) { + const { row, rowSpan, column, colSpan } = slot + // For cells spanning multiple rows or columns we check if all rows or columns (toolbar cells) + // corresponding to this slot are selected, if either or all rows or all columns intersecting this cell + // are selected, we mark this cell selected. + const allRowsSelected = rowSlots + .filter((ts) => ts.row < row + rowSpan && ts.row + ts.rowSpan > row) + .every((ts) => + selectForDelete !== undefined + ? ts.isSelectedForInlineDelete + : ts.isSelected, + ) + const allColumnSelected = colSlots + .filter( + (ts) => ts.column < column + colSpan && ts.column + ts.colSpan > column, + ) + .every((ts) => + selectForDelete !== undefined + ? ts.isSelectedForInlineDelete + : ts.isSelected, + ) + return allRowsSelected || allColumnSelected + } + + static selectCellsInSelectedRowCol( + gridToolbarSpecs: GridToolbarSpecs, + slots: Array, + selectForDelete?: boolean, + ) { + const rowSlots = gridToolbarSpecs.rowTools.slots + const colSlots = gridToolbarSpecs.colTools.slots + slots.forEach((slot) => { + slot[ + selectForDelete !== undefined + ? "isSelectedForInlineDelete" + : "isSelected" + ] = this.isSlotSelected(slot, rowSlots, colSlots, selectForDelete) + }) + return [...slots] + } + + static countOccurrences(array: Array) { + const slots: Array<{ slotId: string; span: number; location: number }> = [] + for (let i = 0; i < array.length; i++) { + const value = array[i] + const existingObject = slots.find((obj) => obj.slotId === value) + if (existingObject === undefined) { + slots.push({ slotId: value, span: 1, location: i }) + } else { + existingObject.span++ + } + } + return slots + } + + static isIntersecting( + toolSlot: SlotDescriptor, + slot: SlotDescriptor, + isRow: boolean, + ) { + const dimension = isRow ? "row" : "column" + const span = isRow ? "rowSpan" : "colSpan" + const sRow = slot[dimension] + const sSpan = slot[span] + const tRow = toolSlot[dimension] + const tSpan = toolSlot[span] + return sRow <= tRow && sRow + sSpan >= tRow + tSpan + } + + static findIntersectingSlots( + slots: Array, + toolSlot: SlotDescriptor, + isRow: boolean, + ) { + return slots.filter((slot) => this.isIntersecting(toolSlot, slot, isRow)) + } + + static getCreateOrExtend( + slot: SlotDescriptor, + selectedToolSlot: SlotDescriptor, + placement: "before" | "after", + isRow: boolean, + ) { + const dimension = isRow ? "row" : "column" + const span = isRow ? "rowSpan" : "colSpan" + return placement === "before" + ? slot[dimension] === selectedToolSlot[dimension] + : slot[dimension] + slot[span] === + selectedToolSlot[dimension] + selectedToolSlot[span] + } + + static insertDimension( + type: "row" | "column", + gridToolbarSpecs: GridToolbarSpecs, + slots: Array, + setSlots: FunctionType, + placement: "before" | "after", + selection?: number, + ) { + slots = JSON.parse(JSON.stringify(slots)) + const isRow = type === "row" + let referenceSlots = gridToolbarSpecs[isRow ? "colTools" : "rowTools"].slots + let newSlots: Array = [] + if (referenceSlots.length > 0) { + const insertAt: Array = this.generateInsertionIndexes( + gridToolbarSpecs, + isRow, + placement, + selection, + ) + insertAt.sort() + const trackedSlotsForSpanIncrease: { + [key: string]: { d: SlotDescriptor; count: number } + } = {} + insertAt.forEach((insertionIndex, index) => { + // Insertion indexes represent which row/column new item will end up, and not what was actually selected, + // Below line gets the selected slot by adjusting index back + const selectedToolSlot = + gridToolbarSpecs[isRow ? "rowTools" : "colTools"].slots[ + insertionIndex - index - (placement === "after" ? 1 : 0) + ] + const intersectedSlots = this.findIntersectingSlots( + slots, + selectedToolSlot, + isRow, + ) + const newSlotsAtCurrentDimension = referenceSlots + .map((referenceSlot) => { + const lateralRefToolIntersectedSlot = intersectedSlots.find((s) => + this.isIntersecting(referenceSlot, s, !isRow), + ) + if (lateralRefToolIntersectedSlot) { + const shouldCreate = this.getCreateOrExtend( + lateralRefToolIntersectedSlot, + selectedToolSlot, + placement, + isRow, + ) + if (shouldCreate) { + const slotId = uuid() + const gridArea = `ga-${slotId}` + return { + slot: slotId, + row: isRow ? insertionIndex : referenceSlot.row, + column: isRow ? referenceSlot.column : insertionIndex, + rowSpan: isRow ? 1 : referenceSlot.rowSpan, + colSpan: isRow ? referenceSlot.colSpan : 1, + gridArea, + } + } else { + if ( + !trackedSlotsForSpanIncrease[ + lateralRefToolIntersectedSlot.slot + ] + ) { + trackedSlotsForSpanIncrease[ + lateralRefToolIntersectedSlot.slot + ] = { d: lateralRefToolIntersectedSlot, count: 1 } + } else { + trackedSlotsForSpanIncrease[ + lateralRefToolIntersectedSlot.slot + ].count += 1 + } + } + } + }) + .filter((s) => !!s) as Array + newSlots = newSlots.concat(newSlotsAtCurrentDimension) + }) + insertAt.forEach((dimNum) => { + const fixDim = isRow ? "row" : "column" + const correctableSlots = slots.filter((s) => s[fixDim] >= dimNum) + correctableSlots.forEach((s) => s[fixDim]++) + }) + Object.values(trackedSlotsForSpanIncrease).forEach( + (obj) => (obj.d[isRow ? "rowSpan" : "colSpan"] += obj.count), + ) + } else { + const slotId = uuid() + const gridArea = `ga-${slotId}` + newSlots.push({ + slot: uuid(), + row: 0, + column: 0, + rowSpan: 1, + colSpan: 1, + gridArea, + }) + } + slots = slots.concat(newSlots) + setSlots(slots) + } + + static generateInsertionIndexes( + gridToolbarSpecs: GridToolbarSpecs, + isRow: boolean, + placement: "before" | "after", + selection?: number, + ) { + let insertAt: Array = [] + if (selection !== undefined) { + insertAt.push(placement === "after" ? selection + 1 : selection) + } else { + if (isRow) { + insertAt = this.generateInsertionIndexesForDimension( + gridToolbarSpecs, + "rowTools", + "row", + placement, + isRow, + ) + } else { + insertAt = this.generateInsertionIndexesForDimension( + gridToolbarSpecs, + "colTools", + "column", + placement, + isRow, + ) + } + } + insertAt.sort() + // If multiple row/col to be added, indexes of all rows (columns) except the first one will change by a value + // equal to their index value in insertAt array (second should be incremented by 1, third by 2 and so on hence we can use indexes) + insertAt = insertAt.map((dimNum, index) => dimNum + index) + return insertAt + } + + static generateInsertionIndexesForDimension( + gridToolbarSpecs: GridToolbarSpecs, + tools: "rowTools" | "colTools", + dimension: "row" | "column", + placement: "before" | "after", + isRow: boolean, + ) { + let insertAt: Array = [] + const reverseDimSlots = + gridToolbarSpecs[isRow ? "rowTools" : "colTools"].slots + const selections = gridToolbarSpecs[tools].slots + .filter((s) => s.isSelected) + .map((s) => s[dimension] + (placement === "before" ? 0 : 1)) + if (selections.length > 0) { + insertAt = insertAt.concat(selections) + } else { + if (placement === "before") { + insertAt.push(0) + } else { + insertAt.push( + Math.max(...reverseDimSlots.map((rS) => rS[dimension])) + 1, + ) + } + } + return insertAt + } + + static generateSlots(rows: Array): Array { + const slotConfigs: Array = [] + const processedGridAreas = new Set() + + rows.forEach((row, rowIndex) => { + const parts = row.split(" ") + + parts.forEach((part, partIndex) => { + const gridArea = part.trim() + + if (!processedGridAreas.has(gridArea)) { + processedGridAreas.add(gridArea) + + // Calculate rowSpan and colSpan based on the repetitions in the grid template + const rowSpan = rows.filter((row) => row.includes(gridArea)).length + const colSpan = parts.filter((part) => part === gridArea).length + + slotConfigs.push({ + slot: gridArea, + gridArea, + row: rowIndex, + column: partIndex, + rowSpan, + colSpan, + }) + } + }) + }) + + return slotConfigs + } + + static generateSlotConfigs(gridTemplate: string): { + slotConfigs: Array + rowHeights: Array + colWidths: Array + } { + // Split by double quotes to separate row and column definitions + const parts = gridTemplate + .split('"') + .map((p) => p.trim()) + .filter((p) => p) + let colWidths + + const rowsAndHeights: { [key: string]: string } = {} + + // Identify row and column parts + parts.forEach((part, index) => { + if (part.startsWith("ga-")) { + rowsAndHeights[part] = "auto" + } else { + if (part.includes("calc(")) { + const calcEndIndex = part.lastIndexOf(")") + const calcExpression = part.substring(0, calcEndIndex + 1) + rowsAndHeights[parts[index - 1]] = calcExpression + + const remainingPart = part.substring(calcEndIndex + 1).trim() + if (remainingPart.includes("/")) { + const colPart = remainingPart.split("/")[1].trim() + colWidths = colPart.split(" ").map((p) => p.trim()) + } + } else { + rowsAndHeights[parts[index - 1]] = part + } + } + }) + + const rowHeights = Object.values(rowsAndHeights) + const slotConfigs = this.generateSlots(Object.keys(rowsAndHeights)) + + return { slotConfigs, rowHeights, colWidths: colWidths || [] } + } +} + +export default GridHelper diff --git a/src/helper.tsx b/src/helper.tsx new file mode 100644 index 0000000..7a5eb23 --- /dev/null +++ b/src/helper.tsx @@ -0,0 +1,549 @@ +import { lazy } from "react" +import { + FunctionType, + ObjectType, + PageInfoType, + SearchArgs, + ComponentDescription, + ComponentList, + RouteConfig, +} from "@armco/types" + +const validImageMimeTypes = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/svg+xml", + "image/tiff", + "image/bmp", + "image/x-icon", +] + +interface ReplacerFunction { + (key: string, value: any): any +} + +interface StringifyOnce { + (obj: any, replacer?: ReplacerFunction | null, indent?: number): string +} + +class Helper { + static populatePagesInRoutes(routes: RouteConfig[], fallback: FunctionType) { + routes && + routes.forEach((route) => { + if (typeof route.element === "string") { + const elementName = route.element + const Component: JSX.ElementType = lazy(() => + import(`../pages/${elementName}/${elementName}.tsx`).catch( + fallback, + ), + ) + route.element = + if (route.children) { + Helper.populateComponentsInRoutes(route.children, fallback) + } + } + }) + } + + static populateComponentsInRoutes( + routes: RouteConfig[], + fallback: FunctionType, + ) { + routes && + routes.forEach((route) => { + const hierarchy = route.hierarchy + if (typeof route.element === "string") { + const elementName = route.element + const Component: JSX.ElementType = lazy( + () => + import( + `../components/${hierarchy}/${elementName}/${elementName}.tsx` + ).catch(fallback), + // () => import(`../components/atoms/Component_404`) + ) + route.element = + if (route.children) { + Helper.populateComponentsInRoutes(route.children, fallback) + } + } + }) + } + + static recrusiveFilter( + data: Array, + filter: string, + matchCase?: boolean, + searchKeys?: false | Array, + ) { + if (!filter || !data || data.length === 0) { + return data + } + if (!searchKeys || searchKeys.length === 0) { + searchKeys = ["label", "name", "id", "value"] + } + const dataClone = JSON.parse(JSON.stringify(data)) + const filteredItems = dataClone.filter((obj: any) => { + return ( + searchKeys && + searchKeys.reduce((acc, key) => { + let isMatch = + obj[key] && + (matchCase + ? obj[key].indexOf(filter) > -1 + : obj[key] && + obj[key] + .toString() + .toLowerCase() + .indexOf(filter.toLowerCase()) > -1) + if (obj.children) { + obj.children = Helper.recrusiveFilter( + obj.children, + filter, + matchCase, + ) + isMatch = isMatch || obj.children.length > 0 + } + return acc || isMatch + }, false) + ) + }) + return filteredItems + } + + // static search(args: SearchArgs): ObjectType | undefined { + // const { obj, key, value } = args + // if (Array.isArray(obj)) { + // return obj.find((item) => { + // args.obj = item + // return Helper.search(args) + // }) + // } else { + // if (obj) { + // if (key in obj && (value === undefined || obj[key] === value)) { + // return obj + // } + // return Object.values(obj).find((item) => { + // if (typeof item === "object" && !(item instanceof Date)) { + // args.obj = item as ObjectType + // return Helper.search(args) + // } + // }) as ObjectType + // } + // } + // } + static search({ obj, key, value }: SearchArgs): ObjectType | undefined { + if (Array.isArray(obj)) { + for (const item of obj) { + const result = Helper.search({ obj: item, key, value }) + if (result) return result + } + } else if (obj && typeof obj === "object" && !(obj instanceof Date)) { + if (key in obj && (value === undefined || obj[key] === value)) { + return obj + } + for (const item of Object.values(obj)) { + if (typeof item === "object" && !(item instanceof Date)) { + const result = Helper.search({ obj: item as ObjectType, key, value }) + if (result) return result + } + } + } + return undefined + } + + static filterTreeStructure( + args: SearchArgs, + ): boolean | Array | ObjectType | void { + let { obj, key, value } = args + if (Array.isArray(obj)) { + return obj.filter((item, index) => { + if (typeof item === "object" && !(item instanceof Date)) { + args.obj = item + + const match = Helper.filterTreeStructure(args) + if (Array.isArray(match) && match.length > 0) { + ;(obj as unknown as Array>).splice( + index, + 1, + match, + ) + } + return match + } + return false + }) + } else { + if (obj) { + let found = false + if (key in obj && (value === undefined || obj[key] === value)) { + obj.hasMatched = true + } + Object.keys(obj).forEach((key) => { + const item = (obj as ObjectType)[key] + if (typeof item === "object" && !(item instanceof Date)) { + args.obj = item as ObjectType + + const match = Helper.filterTreeStructure(args) + if (Array.isArray(match) ? match.length > 0 : match) { + found = true + } else { + delete (obj as ObjectType)[key] + } + if (Array.isArray(match) && match.length > 0) { + ;(obj as ObjectType)[key] = match + } + } else + (obj as ObjectType).hasMatched || delete (obj as ObjectType)[key] + }) + return (obj.hasMatched as boolean) || found + } + return false + } + } + + static generateSlices( + count: number, + sliceLength: number, + labelFormat: string, + ) { + let i = 0, + j = 0 + const slices: Array = [] + while (i < count) { + let addUp = sliceLength + if (i + sliceLength > count) { + addUp = count % sliceLength + } + const label = + labelFormat === "range" ? i + 1 + " - " + (i + addUp) : "" + (j + 1) + slices.push({ + label, + name: label.replace(/ /g, "").replace(/-/g, "to"), + sliceIndex: j, + startIndex: i, + endIndex: i + addUp - 1, + }) + i += sliceLength + j++ + } + return slices + } + + static aggregate( + data: any, + aggregator: string, + ): { [key: string]: Array } { + let aggregated: { [key: string]: Array } = {} + data.forEach((item: any) => { + const key = item[aggregator] + let aggregatedArray: Array | undefined = aggregated[key] + if (!aggregatedArray) { + aggregatedArray = [] + aggregated[key] = aggregatedArray + } + aggregatedArray.push(item) + }) + return aggregated + } + + static generateCategories( + data: any, + categories: Array | undefined, + ): { [key: string]: { [key: string]: Array } } { + const groups: { [key: string]: { [key: string]: Array } } = {} + if (categories && data) { + categories.forEach((key) => { + let group: { [key: string]: Array } = Helper.aggregate( + data, + key, + ) + groups[key] = group + }) + } + return groups + } + + static toCamelCase(str: string, separater: string, includeFirst: boolean) { + const strParts = str.split(separater || " ") + return strParts + .map((part, i) => + i === 0 && includeFirst + ? part.charAt(0).toUpperCase() + part.slice(1) + : part, + ) + .join("") + } + + static generateRandomId(length: number = 8) { + return Math.random() + .toString(36) + .substring(2, length + 2) + } + + static copyOrPrompt(text?: string, cb?: FunctionType) { + if (Helper.copyToClipboard(text)) { + cb && cb() + } else { + prompt("Clipboard (Select: ⌘+a > Copy: ⌘+c)", text) + } + } + + static copyToClipboard(text?: string) { + const isNotSafari = typeof (window as any).safari === "undefined" + if (isNotSafari) { + text && navigator.clipboard.writeText(text) + return true + } else { + return false + } + } + + static findComponentDescription( + componentName: string, + components: ComponentList, + ) { + let hierarchy + let selectedItem = components.ATOMS.components.find( + (item: ComponentDescription) => + (typeof item === "string" ? item : item.name) === componentName, + ) + if (!selectedItem) { + selectedItem = components.MOLECULES.components.find( + (item: ComponentDescription) => + (typeof item === "string" ? item : item.name) === componentName, + ) + if (selectedItem) { + hierarchy = "MOLECULES" + } + } else { + hierarchy = "ATOMS" + } + return { + selectedItem: JSON.parse( + JSON.stringify(selectedItem), + ) as ComponentDescription, + hierarchy, + component: componentName, + } + } + + static matchArrayFilters( + searchList: Array | undefined, + filters: Array | false | undefined, + match: string, + type?: string, + ) { + if (filters && searchList) { + return type && type === "starts" + ? searchList.filter( + (item) => + filters.findIndex((al) => + item[match].toLowerCase().startsWith(al.toLowerCase()), + ) > -1, + ) + : searchList.filter( + (item) => + filters.findIndex( + (al) => item[match].toLowerCase() === al.value.toLowerCase(), + ) > -1, + ) + } + return searchList + } + + static importComponent( + name: string, + hierarchy: string, + fallback: FunctionType, + ) { + return lazy( + () => + import(`../components/${hierarchy}/${name}/${name}.tsx`).catch( + fallback, + ), + // () => import(`../components/atoms/Component_404`) + ) + } + + static getStylesFromClass(className: string): Record { + const styleSheet = Array.from(document.styleSheets).find((sheet) => + Array.from(sheet.cssRules).some( + (rule) => + rule instanceof CSSStyleRule && rule.selectorText === `.${className}`, + ), + ) + + if (styleSheet) { + const styleRule = Array.from(styleSheet.cssRules).find( + (rule) => + rule instanceof CSSStyleRule && rule.selectorText === `.${className}`, + ) as CSSStyleRule + + if (styleRule) { + const styles: Record = {} + for (let i = 0; i < styleRule.style.length; i++) { + const prop = styleRule.style[i] + styles[prop] = styleRule.style.getPropertyValue(prop) + } + return styles + } + } + + return {} + } + + static debounce void>( + func: T, + wait?: number, + immediate: boolean = false, + ): (...args: Parameters) => void { + let timeout: NodeJS.Timeout + + return function (...args: Parameters): void { + const later = () => { + timeout = undefined! + + if (!immediate) { + func(...args) + } + } + + clearTimeout(timeout) + + if (immediate && !timeout) { + func(...args) + } + + timeout = setTimeout(later, wait || 1000) + } + } + + static isMobile(): boolean { + let check = false + ;(function (a: string) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( + a, + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( + a.substr(0, 4), + ) + ) + check = true + })( + (navigator.userAgent || + navigator.vendor || + ("opera" in window && window.opera)) as string, + ) + return check + } + + static download(blob: Blob, filename: string) { + const link = document.createElement("a") + link.href = window.URL.createObjectURL(blob) + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + + static pad(num: number, separator?: string) { + return ("" + num).padStart(2, "0") + (separator ? separator : "") + } + + static toReadable(str?: string) { + return str + ?.replace(/([a-z])([A-Z])/g, "$1 $2") + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ") + } + + static generateFileKey(file: File | { name: string }) { + return "size" in file + ? `${file.name}_${file.size}_${file.type}_${file.lastModified}` + : file.name + "-preexisting-" + Date.now() + } + + static convertToBytes(input: string): number { + const trimmedInput = input.trim().toLowerCase() + + try { + if (trimmedInput.endsWith("kb")) { + const number = parseFloat(trimmedInput.slice(0, -2)) + return number * 1024 + } else if (trimmedInput.endsWith("mb")) { + const number = parseFloat(trimmedInput.slice(0, -2)) + return number * 1024 * 1024 + } else if (trimmedInput.endsWith("bytes")) { + const number = parseFloat(trimmedInput.slice(0, -5)) + return number + } else if (!isNaN(parseFloat(trimmedInput))) { + return parseFloat(trimmedInput) + } else { + return -1 + } + } catch (error) { + return -1 + } + } + + static isImage(type: string) { + return !!type && validImageMimeTypes.indexOf(type) > -1 + } + + static stringifyOnce: StringifyOnce = (obj, replacer, indent) => { + const printedObjects: any[] = [] + const printedObjectKeys: string[] = [] + + function printOnceReplacer(key: string, value: any): any { + if (printedObjects.length > 2000) { + // browsers will not print more than 20K, I don't see the point to allow 2K.. algorithm will not be fast anyway if we have too many objects + return "object too long" + } + + let printedObjIndex: number | false = false + printedObjects.forEach((obj, index) => { + if (obj === value) { + printedObjIndex = index + } + }) + + if (key === "") { + // root element + printedObjects.push(obj) + printedObjectKeys.push("root") + return value + } else if (printedObjIndex !== false && typeof value === "object") { + if (printedObjectKeys[printedObjIndex] === "root") { + return "(pointer to root)" + } else { + return ( + "(see " + + (!!value && !!value.constructor + ? value.constructor.name.toLowerCase() + : typeof value) + + " with key " + + printedObjectKeys[printedObjIndex] + + ")" + ) + } + } else { + const qualifiedKey = key || "(empty key)" + printedObjects.push(value) + printedObjectKeys.push(qualifiedKey) + if (replacer) { + return replacer(key, value) + } else { + return value + } + } + } + + return JSON.stringify(obj, printOnceReplacer, indent) + } +} + +export default Helper diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000..e2f8219 --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,170 @@ +import { useContext, useEffect, useState } from "react" +import { + AlertProps, + ArThemes, + DrawerProps, + FunctionType, + ModalProps, + PanelContent, + User, +} from "@armco/types" +import { ArContext, SlotterContext } from "./contexts" + +export function useSlotted(componentName: string) { + const { slotted, setSlotted } = useContext(SlotterContext) + slotted?.indexOf(componentName) === -1 && + setSlotted && + setSlotted([...slotted, componentName]) +} + +export function useStateWithHistory( + initialState?: T, +): [T | undefined, FunctionType, FunctionType, FunctionType, boolean, boolean] { + const [past, setPast] = useState([]) + const [present, setPresent] = useState() + const [future, setFuture] = useState([]) + + useEffect(() => { + !present && setPresent(initialState) + // eslint-disable-next-line + }, [initialState]) + + const undo = () => { + if (past.length === 0) return + + const newPast = [...past] + const newPresent = newPast.pop() + + setPast(newPast) + present && setFuture([present, ...future]) + setPresent(newPresent) + } + + const redo = () => { + if (future.length === 0) return + + const newFuture = [...future] + const newPresent = newFuture.shift() + + past && present && setPast([...past, present]) + setFuture(newFuture) + setPresent(newPresent) + } + + const updatePresent = (newState: any, skipHistory?: boolean) => { + if (!newState) { + debugger + } + !skipHistory && past && present && setPast([...past, present]) + setPresent(newState) + !skipHistory && setFuture([]) + } + + return [ + present, + updatePresent, + undo, + redo, + past.length > 0, + future.length > 0, + ] +} + +export const useTheme = (): { + theme: ArThemes + setTheme: (theme: ArThemes) => void +} => { + const context = useContext(ArContext) + if (!context) { + throw new Error("useTheme must be used within an ArProvider") + } + return { theme: context.theme, setTheme: context.setTheme } +} + +export const useModalState = (): { + modalState?: ModalProps + setModalState: (modalState: ModalProps | undefined) => void +} => { + const context = useContext(ArContext) + if (!context) { + throw new Error("useModalState must be used within an ArProvider") + } + return { + modalState: context.modalState, + setModalState: context.setModalState, + } +} + +export const useNotification = (): { + notification?: AlertProps + notify: (notification: AlertProps | undefined) => void +} => { + const context = useContext(ArContext) + if (!context) { + throw new Error("useNotification must be used within an ArProvider") + } + return { + notification: context.notification, + notify: context.notify, + } +} + +export const useDrawerState = (): { + drawerState: DrawerProps + setDrawerState: (drawerState: DrawerProps) => void +} => { + const context = useContext(ArContext) + if (!context) { + throw new Error("useDrawerState must be used within an ArProvider") + } + return { + drawerState: context.drawerState, + setDrawerState: context.setDrawerState, + } +} + +export const useLoggedIn = (): { + isLoggedIn?: boolean + setLoggedIn: (isLoggedIn: boolean) => void +} => { + const context = useContext(ArContext) + if (!context) { + throw new Error("useLoggedIn must be used within an ArProvider") + } + return { + isLoggedIn: context.isLoggedIn, + setLoggedIn: context.setLoggedIn, + } +} + +export const useUser = (): { + user?: User + setUser: (user: User) => void +} => { + const context = useContext(ArContext) + if (!context) { + throw new Error("useUser must be used within an ArProvider") + } + return { + user: context.user, + setUser: context.setUser, + } +} + +export const usePanelContent = ( + isLeft: boolean, +): { + panelContent?: PanelContent + setPanelContent: (panelContent: PanelContent) => void +} => { + const context = useContext(ArContext) + if (!context) { + throw new Error("usePanelContent must be used within an ArProvider") + } + return { + panelContent: isLeft ? context.leftPanelContent : context.rightPanelContent, + setPanelContent: isLeft + ? context.setLeftPanelContent + : context.setRightPanelContent, + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..61e6a74 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,14 @@ +export { dateFormat as DateFormatter } from "./dateformat" +export { default as DomHelper } from "./domHelper" +export { default as Adapter } from "./adapters" +export { default as Helper } from "./helper" +export { default as Network } from "./network" +export { default as Validator } from "./validators" +export { default as GridHelper } from "./gridHelper" +export { default as RecursionHelper } from "./recursionHelper" +export * from "./dateHelper" +export * from "./chartGenerators" +export * from "./hooks" +export * from "./contexts" +export * from "./providers" +export * from "./HOC" diff --git a/src/network.ts b/src/network.ts new file mode 100644 index 0000000..dfb3679 --- /dev/null +++ b/src/network.ts @@ -0,0 +1,247 @@ +import { FunctionType, ObjectType } from "@armco/types" +import { API_CONFIG } from "@armco/configs" + +interface Options extends RequestInit { + // headers?: { + // body?: object + // Origin?: string + // "Content-Type"?: string + // } + // method?: string + // mode?: string + // credentials?: string + headers?: any + toggleLoader?: Function + extras?: ObjectType +} + +interface RetryOptions { + retryDescriptor?: number[] + maxAttempts?: number + onRetry?: (attempt: number, waitTime: number) => void + onSuccess?: () => void + onFailure?: (error: Error) => void +} + +class Error { + error?: string + message?: string + status: number + extras?: ObjectType + + constructor(message: string, status: number, extras?: ObjectType) { + this.message = message + this.status = status + this.extras = extras + } +} + +export default class Network { + static MAX_ATTEMPTS_LIMIT = 10 + + static async get(url: string, queryParams?: object, options?: Options) { + const urlString = Network.stringifyUrl(url, queryParams) + options = { + method: "GET", + mode: "cors", + credentials: "include", + ...options, + } + return Network.crud(urlString, options) + } + + static async getStatic( + url: string, + queryParams?: object | null, + options?: Options | null, + ) { + let urlString = Network.stringifyUrl(url, queryParams) + const environment = process.env.NODE_ENV || "development" + const host = urlString.startsWith("http") + ? "" + : API_CONFIG.STATIC_HOST[environment as keyof object] + urlString = host ? host + urlString : urlString + options = { + method: "GET", + mode: "cors", + credentials: "include", + ...options, + } + return Network.crud(urlString, options) + } + + static async post( + url: string, + data: object, + queryParams?: object, + options?: Options, + noStringify?: boolean, + noHeaders?: boolean, + ) { + const urlString = Network.stringifyUrl(url, queryParams) + options = { + method: "POST", + // @ts-ignore + body: noStringify ? data : JSON.stringify(data), + mode: "cors", + credentials: "include", + } + !noHeaders && + options && + (options.headers = { "Content-Type": "application/json" }) + // @ts-ignore + return Network.crud(urlString, options) + } + + static async upload( + url: string, + file: File, + queryParams?: object, + options?: Options, + ) { + let urlString = Network.stringifyUrl(url, queryParams) + const environment = process.env.NODE_ENV || "development" + const host = urlString.startsWith("http") + ? "" + : API_CONFIG.STATIC_HOST[environment as keyof object] + urlString = host ? host + urlString : urlString + const formData = new FormData() + formData.append("file", file) + options = { + method: "POST", + mode: "cors", + body: formData, + credentials: "include", + ...options, + } + return Network.crud(urlString, options) + } + + static async put(url: string, data: object, queryParams?: object) { + const urlString = Network.stringifyUrl(url, queryParams) + const options: Options = { + method: "PUT", + body: JSON.stringify(data), + mode: "cors", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + } + return Network.crud(urlString, options) + } + + static async delete(url: string, queryParams: object) { + const urlString = Network.stringifyUrl(url, queryParams) + const options = { + method: "DELETE", + noInject: true, + } + return Network.crud(urlString, options) + } + + static async crud(urlString: string, options: Options) { + const environment = process.env.NODE_ENV || "development" + const host = urlString.startsWith("http") + ? "" + : API_CONFIG.HOST[environment as keyof object] + const extras = options?.extras + urlString = host ? host + urlString : urlString + if (!options.headers) { + options.headers = {} + } + options.toggleLoader && options.toggleLoader({ [urlString]: true }) + options.headers["Origin"] = + window.location.protocol + "//" + window.location.host + const response = await fetch(urlString, options) + const { ok, status, headers } = response + if (ok) { + if (headers.get("content-type")) { + if (headers.get("content-type")!.indexOf("application/json") !== -1) { + const body = await response.json() + options.toggleLoader && options.toggleLoader({ [urlString]: false }) + return { status, body, headers } + } else if ( + headers.get("content-type")!.indexOf("application/zip") !== -1 || + headers.get("content-type")!.indexOf("image/png") !== -1 || + headers.get("content-type")!.indexOf("application/pdf") !== -1 || + headers.get("content-type")!.indexOf("video/mp4") !== -1 || + headers.get("content-type")!.indexOf("image/gif") !== -1 + ) { + const body = await response.blob() + options.toggleLoader && options.toggleLoader({ [urlString]: false }) + return { status, body, headers } + } + } + const body = await response.text() + options.toggleLoader && options.toggleLoader({ [urlString]: false }) + return { status, body, headers } + } + const err = await response.json() + options.toggleLoader && options.toggleLoader({ [urlString]: false }) + console.log(err) + throw new Error(err.error || err.message, status, extras) + } + + static stringifyUrl(url: string, queryParams?: any) { + if (!queryParams) { + return url || "" + } + const arrLength = Object.keys(queryParams).length + return url && arrLength + ? Object.keys(queryParams) + .filter((k) => queryParams[k] !== undefined) + .reduce( + (acc, key, index) => + acc.concat( + `${encodeURIComponent(key)}=${encodeURIComponent( + queryParams[key], + ).replace(/'/g, "%27")}` + (index < arrLength - 1 ? "&" : ""), + ), + url + "?", + ) + : "" + } + + static async retry( + fn: FunctionType, + retryOptions: RetryOptions | undefined = {}, + ) { + let { + retryDescriptor = [1, 3, 5], + maxAttempts = 5, + onRetry, + onSuccess, + onFailure, + } = retryOptions + maxAttempts = Math.min(maxAttempts, Network.MAX_ATTEMPTS_LIMIT) + let attempts = 0 + + const attempt = async () => { + try { + const result = await fn() + onSuccess && onSuccess() + return result + } catch (error) { + if (attempts < Math.min(retryDescriptor.length, maxAttempts)) { + const waitTime = retryDescriptor[attempts] * 1000 + attempts++ + onRetry && onRetry(attempts, waitTime) + console.warn(`Retrying in ${waitTime / 1000} seconds...`) + await new Promise((resolve) => setTimeout(resolve, waitTime)) + return attempt() + } else { + const errorObj = new Error( + "Max retries reached", + 500, + error as ObjectType, + ) + onFailure && onFailure(errorObj) + throw errorObj + } + } + } + + return attempt() + } +} diff --git a/src/providers.tsx b/src/providers.tsx new file mode 100644 index 0000000..df1094c --- /dev/null +++ b/src/providers.tsx @@ -0,0 +1,53 @@ +import { ReactNode, useState } from "react" +import { + AlertProps, + ArThemes, + DrawerProps, + ModalProps, + PanelContent, + User, +} from "@armco/types" +import { ArContext, Helper } from "./index" +export const ArProvider = ({ children }: { children: ReactNode }) => { + const [theme, setTheme] = useState(ArThemes.DARK1) + const [isLoggedIn, setLoggedIn] = useState() + const [user, setUser] = useState() + const [modalState, setModalState] = useState( + undefined, + ) + const [notification, notify] = useState(undefined) + const [drawerState, setDrawerState] = useState({ + collapsed: Helper.isMobile(), + }) + const [leftPanelContent, setLeftPanelContent] = useState< + PanelContent | undefined + >() + const [rightPanelContent, setRightPanelContent] = useState< + PanelContent | undefined + >() + + return ( + + {children} + + ) +} diff --git a/src/recursionHelper.tsx b/src/recursionHelper.tsx new file mode 100644 index 0000000..dd185e7 --- /dev/null +++ b/src/recursionHelper.tsx @@ -0,0 +1,66 @@ +import { v4 as uuid } from "uuid" +import { RecursionArgs, ObjectType, RecusionConditionTypes } from "@armco/types" + +class RecursionHelper { + static recur(args: RecursionArgs) { + const { condition, data, isBrute, preop, postop, iterateOn } = args + if (isBrute) { + if (Array.isArray(data)) { + data.forEach((item) => { + RecursionHelper.recur({ ...args, data: item }) + }) + } else if (typeof data === "object") { + const preopResult = preop && preop(data) + Object.values(data).forEach((item: any) => { + RecursionHelper.recur({ ...args, data: item }) + }) + return (postop && postop(data)) || preopResult + } + } else { + if (Array.isArray(data)) { + data.forEach((item) => { + RecursionHelper.recur({ ...args, data: item }) + }) + } else if ( + typeof data === "object" && + (!condition || RecursionHelper.evaluateCondition(data, condition)) + ) { + const preopResult = preop && preop(data) + if (iterateOn) { + if (data[iterateOn]) { + RecursionHelper.recur({ ...args, data: data[iterateOn] }) + } + } else { + Object.values(data).forEach((item) => { + RecursionHelper.recur({ ...args, data: item }) + }) + } + return (postop && postop(data)) || preopResult + } + } + return null + } + + static injectIds(args: RecursionArgs) { + args.postop = (item) => !item.id && (item.id = uuid()) + RecursionHelper.recur(args) + } + + static evaluateCondition(data: ObjectType, condition: ObjectType): boolean { + if (!data || !condition) { + return false + } + if (condition.type === RecusionConditionTypes.KEY_EXISTS) { + return !!(condition.key && (condition.key as string) in data) + } else if (condition.type === RecusionConditionTypes.KEY_VALUE) { + return !!( + condition.key && + condition.value && + data[condition.key as string] === condition.value + ) + } + return false + } +} + +export default RecursionHelper diff --git a/src/validators.ts b/src/validators.ts new file mode 100644 index 0000000..560e574 --- /dev/null +++ b/src/validators.ts @@ -0,0 +1,17 @@ +import { ObjectType } from "@armco/types" + +class Validator { + static validateTask(taskObj: ObjectType) { + return ( + taskObj && + taskObj.name && + taskObj.client && + taskObj.target && + (taskObj.target as ObjectType).endpoint && + taskObj.schedule && + (taskObj.schedule as ObjectType).cron + ) + } +} + +export default Validator diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..54d05ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..3018e48 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,34 @@ +import { resolve } from "node:path" +import glob from "glob" +import { defineConfig } from "vitest/config" +import react from "@vitejs/plugin-react" +import dts from "vite-plugin-dts" + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), dts({ outDir: "build/types" })], + build: { + outDir: "build", + lib: { + entry: glob.sync(resolve(__dirname, "src/**/*.ts")), + name: "@armco/utils", + formats: ["es", "cjs"], + }, + rollupOptions: { + treeshake: true, + external: ["react", "react/jsx-runtime", "react-dom", "moment"], + output: [ + { + format: "es", + dir: "build/es", + entryFileNames: "[name].js", + }, + { + format: "cjs", + dir: "build/cjs", + entryFileNames: "[name].js", + }, + ], + }, + }, +})