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",
+ },
+ ],
+ },
+ },
+})