merge master

This commit is contained in:
2024-09-18 16:41:50 +05:30
20 changed files with 3493 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

51
package.json Normal file
View File

@@ -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"
}

8
publish.sh Executable file
View File

@@ -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

11
src/HOC.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { ComponentType } from "react"
import { useTheme } from "." // Adjust the import path as needed
export const withTheme = <P extends object>(
Component: ComponentType<P>,
): ComponentType<P> => {
return (props: P) => {
const { theme, setTheme } = useTheme()
return <Component {...(props as P)} theme={theme} setTheme={setTheme} />
}
}

128
src/adapters.ts Normal file
View File

@@ -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<TreeListData> {
const returnTreeList: Array<TreeListData> = []
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<any> = []
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

45
src/chartGenerators.ts Normal file
View File

@@ -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")
}
}

21
src/contexts.ts Normal file
View File

@@ -0,0 +1,21 @@
import { createContext } from "react"
import { ArContextType, ArThemes, FunctionType } from "@armco/types"
import { Helper } from "."
export const SlotterContext = createContext<{
slotted: Array<string>
setSlotted?: FunctionType
}>({ slotted: [] })
export const ArContext = createContext<ArContextType>({
theme: ArThemes.DARK1,
drawerState: { collapsed: Helper.isMobile() },
notify: () => {},
setDrawerState: () => {},
setLeftPanelContent: () => {},
setLoggedIn: () => {},
setModalState: () => {},
setRightPanelContent: () => {},
setTheme: () => {},
setUser: () => {},
})

546
src/dateHelper.ts Normal file
View File

@@ -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 }

336
src/dateformat.ts Normal file
View File

@@ -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<string, string> = {
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")
}

456
src/domHelper.ts Normal file
View File

@@ -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<boolean>,
) {
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<SVGPathElement> =
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<HTMLIFrameElement>(".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<HTMLIFrameElement>(".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<number> | 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

737
src/gridHelper.ts Normal file
View File

@@ -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<Array<string>>,
rowHeight?: Array<string> | string,
colWidth?: Array<string> | 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<SlotDescriptor>,
rowHeights?: Array<string> | string,
demo?: boolean,
): Array<string> {
const totalRows = Math.max(...slots.map((slot) => slot.row + slot.rowSpan))
const allRowHeights: Array<string> = []
const doc = DomHelper.getDocumentElement(demo)
rowHeights = Array.isArray(rowHeights)
? rowHeights
: (Array.from(
{ length: totalRows },
() => rowHeights || "minmax(2rem, auto)",
) as Array<string>)
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<SlotDescriptor>,
currentRowHeights?: string | Array<string>,
): { gridArea: Array<Array<string>>; rowHeights: Array<string> } {
let gridArea: Array<Array<string>> = [[]]
const rowHeights: Array<string> = []
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<Array<string>>,
rowHeight?: Array<string> | string,
colWidth?: Array<string> | 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<SlotDescriptor>,
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<SlotDescriptor>,
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<SlotDescriptor>,
gridToolbarSpecs: GridToolbarSpecs,
type: string,
isInlineDelete?: boolean,
): Array<SlotDescriptor> {
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<SlotDescriptor>) {
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<SlotDescriptor>,
colSlots: Array<SlotDescriptor>,
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<SlotDescriptor>,
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<string>) {
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<SlotDescriptor>,
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<SlotDescriptor>,
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<SlotDescriptor> = []
if (referenceSlots.length > 0) {
const insertAt: Array<number> = 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<SlotDescriptor>
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<number> = []
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<number> = []
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<string>): Array<SlotDescriptor> {
const slotConfigs: Array<SlotDescriptor> = []
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<SlotDescriptor>
rowHeights: Array<string>
colWidths: Array<string>
} {
// 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

549
src/helper.tsx Normal file
View File

@@ -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 = <Component />
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 = <Component />
if (route.children) {
Helper.populateComponentsInRoutes(route.children, fallback)
}
}
})
}
static recrusiveFilter(
data: Array<any>,
filter: string,
matchCase?: boolean,
searchKeys?: false | Array<string>,
) {
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> | 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<Array<ObjectType>>).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<PageInfoType> = []
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<string> } {
let aggregated: { [key: string]: Array<string> } = {}
data.forEach((item: any) => {
const key = item[aggregator]
let aggregatedArray: Array<string> | undefined = aggregated[key]
if (!aggregatedArray) {
aggregatedArray = []
aggregated[key] = aggregatedArray
}
aggregatedArray.push(item)
})
return aggregated
}
static generateCategories(
data: any,
categories: Array<string> | undefined,
): { [key: string]: { [key: string]: Array<string> } } {
const groups: { [key: string]: { [key: string]: Array<string> } } = {}
if (categories && data) {
categories.forEach((key) => {
let group: { [key: string]: Array<string> } = 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<any> | undefined,
filters: Array<any> | 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<string, string> {
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<string, string> = {}
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<T extends (...args: any[]) => void>(
func: T,
wait?: number,
immediate: boolean = false,
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout
return function (...args: Parameters<T>): 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

170
src/hooks.ts Normal file
View File

@@ -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<T>(
initialState?: T,
): [T | undefined, FunctionType, FunctionType, FunctionType, boolean, boolean] {
const [past, setPast] = useState<T[]>([])
const [present, setPresent] = useState<T | undefined>()
const [future, setFuture] = useState<T[]>([])
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,
}
}

14
src/index.ts Normal file
View File

@@ -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"

247
src/network.ts Normal file
View File

@@ -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()
}
}

53
src/providers.tsx Normal file
View File

@@ -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>(ArThemes.DARK1)
const [isLoggedIn, setLoggedIn] = useState<boolean>()
const [user, setUser] = useState<User>()
const [modalState, setModalState] = useState<ModalProps | undefined>(
undefined,
)
const [notification, notify] = useState<AlertProps | undefined>(undefined)
const [drawerState, setDrawerState] = useState<DrawerProps>({
collapsed: Helper.isMobile(),
})
const [leftPanelContent, setLeftPanelContent] = useState<
PanelContent | undefined
>()
const [rightPanelContent, setRightPanelContent] = useState<
PanelContent | undefined
>()
return (
<ArContext.Provider
value={{
isLoggedIn,
setLoggedIn,
theme,
setTheme,
modalState,
setModalState,
notification,
notify,
drawerState,
setDrawerState,
user,
setUser,
leftPanelContent,
setLeftPanelContent,
rightPanelContent,
setRightPanelContent,
}}
>
{children}
</ArContext.Provider>
)
}

66
src/recursionHelper.tsx Normal file
View File

@@ -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

17
src/validators.ts Normal file
View File

@@ -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

4
tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
}

34
vite.config.ts Normal file
View File

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